Skip to content

Commit

Permalink
feat: Blacklist mode (#29)
Browse files Browse the repository at this point in the history
Added support for blacklist mode to geoblock.go

Co-authored-by: Thomas Meckel <tmeckel@users.noreply.github.com>
  • Loading branch information
tmeckel and tmeckel committed Dec 29, 2022
1 parent 3ffcf3f commit c95af23
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 25 deletions.
9 changes: 7 additions & 2 deletions geoblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type Config struct {
ForceMonthlyUpdate bool `yaml:"forceMonthlyUpdate"`
AllowUnknownCountries bool `yaml:"allowUnknownCountries"`
UnknownCountryAPIResponse string `yaml:"unknownCountryApiResponse"`
BlackListMode bool `yaml:"blacklist"`
Countries []string `yaml:"countries,omitempty"`
}

Expand All @@ -64,6 +65,7 @@ type GeoBlock struct {
forceMonthlyUpdate bool
allowUnknownCountries bool
unknownCountryCode string
blackListMode bool
countries []string
privateIPRanges []*net.IPNet
database *lru.LRUCache
Expand Down Expand Up @@ -96,7 +98,8 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
infoLogger.Printf("force monthly update: %t", config.ForceMonthlyUpdate)
infoLogger.Printf("allow unknown countries: %t", config.AllowUnknownCountries)
infoLogger.Printf("unknown country api response: %s", config.UnknownCountryAPIResponse)
infoLogger.Printf("allowed countries: %v", config.Countries)
infoLogger.Printf("blacklist mode: %t", config.BlackListMode)
infoLogger.Printf("countries: %v", config.Countries)

cache, err := lru.NewLRUCache(config.CacheSize)
if err != nil {
Expand All @@ -114,6 +117,7 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
forceMonthlyUpdate: config.ForceMonthlyUpdate,
allowUnknownCountries: config.AllowUnknownCountries,
unknownCountryCode: config.UnknownCountryAPIResponse,
blackListMode: config.BlackListMode,
countries: config.Countries,
privateIPRanges: initPrivateIPBlocks(),
database: cache,
Expand Down Expand Up @@ -178,7 +182,8 @@ func (a *GeoBlock) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
}
}

isAllowed := stringInSlice(entry.Country, a.countries) || (entry.Country == unknownCountryCode && a.allowUnknownCountries)
isAllowed := (stringInSlice(entry.Country, a.countries) != a.blackListMode) ||
(entry.Country == unknownCountryCode && a.allowUnknownCountries)

if !isAllowed {
infoLogger.Printf("%s: request denied [%s] for country [%s]", a.name, ipAddress, entry.Country)
Expand Down
72 changes: 63 additions & 9 deletions geoblock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import (
)

const (
xForwardedFor = "X-Forwarded-For"
caExampleIP = "99.220.109.148"
chExampleIP = "82.220.110.18"
privateRangeIP = "192.168.1.1"
invalidIP = "192.168.1.X"
unknownCountryIPGoogle = "66.249.93.100"
apiURI = "https://get.geojs.io/v1/ip/country/{ip}"
xForwardedFor = "X-Forwarded-For"
caExampleIP = "99.220.109.148"
chExampleIP = "82.220.110.18"
privateRangeIP = "192.168.1.1"
invalidIP = "192.168.1.X"
unknownCountry = "1.1.1.1"
apiURI = "https://get.geojs.io/v1/ip/country/{ip}"
)

func TestEmptyApi(t *testing.T) {
Expand Down Expand Up @@ -139,7 +139,7 @@ func TestAllowedUnknownCountry(t *testing.T) {
t.Fatal(err)
}

req.Header.Add(xForwardedFor, unknownCountryIPGoogle)
req.Header.Add(xForwardedFor, unknownCountry)

handler.ServeHTTP(recorder, req)

Expand All @@ -165,7 +165,7 @@ func TestDenyUnknownCountry(t *testing.T) {
t.Fatal(err)
}

req.Header.Add(xForwardedFor, unknownCountryIPGoogle)
req.Header.Add(xForwardedFor, unknownCountry)

handler.ServeHTTP(recorder, req)

Expand Down Expand Up @@ -226,6 +226,60 @@ func TestDeniedCountry(t *testing.T) {
assertStatusCode(t, recorder.Result(), http.StatusForbidden)
}

func TestAllowBlacklistMode(t *testing.T) {
cfg := createTesterConfig()
cfg.BlackListMode = true
cfg.Countries = append(cfg.Countries, "CH")

ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})

handler, err := geoblock.New(ctx, next, cfg, "GeoBlock")
if err != nil {
t.Fatal(err)
}

recorder := httptest.NewRecorder()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
if err != nil {
t.Fatal(err)
}

req.Header.Add(xForwardedFor, caExampleIP)

handler.ServeHTTP(recorder, req)

assertStatusCode(t, recorder.Result(), http.StatusOK)
}

func TestDenyBlacklistMode(t *testing.T) {
cfg := createTesterConfig()
cfg.BlackListMode = true
cfg.Countries = append(cfg.Countries, "CH")

ctx := context.Background()
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})

handler, err := geoblock.New(ctx, next, cfg, "GeoBlock")
if err != nil {
t.Fatal(err)
}

recorder := httptest.NewRecorder()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
if err != nil {
t.Fatal(err)
}

req.Header.Add(xForwardedFor, chExampleIP)

handler.ServeHTTP(recorder, req)

assertStatusCode(t, recorder.Result(), http.StatusForbidden)
}

func TestAllowLocalIP(t *testing.T) {
cfg := createTesterConfig()
cfg.Countries = append(cfg.Countries, "CH")
Expand Down
29 changes: 15 additions & 14 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# GeoBlock

Simple plugin for [Traefik](https://github.com/containous/traefik) to block request based on their country of origin. Uses [GeoJs.io](https://www.geojs.io/).
Simple plugin for [Traefik](https://github.com/containous/traefik) to block or allow requests based on their country of origin. Uses [GeoJs.io](https://www.geojs.io/).

## Configuration

Expand All @@ -10,9 +10,9 @@ It is possible to install the [plugin locally](https://traefik.io/blog/using-pri

Depending on your setup, the installation steps might differ from the one described here. This example assumes that your Traefik instance runs in a Docker container and uses the [official image](https://hub.docker.com/_/traefik/).

Download the latest release of the plugin and save it to a location the Traefik container can reach. Below is an example of a possible setup. Notice how the plugin source is mapped into the container (`/plugin/geoblock:/plugins-local/src/github.com/PascalMinder/geoblock/`):
Download the latest release of the plugin and save it to a location the Traefik container can reach. Below is an example of a possible setup. Notice how the plugin source is mapped into the container (`/plugin/geoblock:/plugins-local/src/github.com/PascalMinder/geoblock/`) via a volume bind mount:

docker-compose.yml
#### `docker-compose.yml`

````yml
version: "3.7"
Expand All @@ -37,13 +37,11 @@ services:
- traefik.http.routers.hello.entrypoints=http
- traefik.http.routers.hello.rule=Host(`hello.localhost`)
- traefik.http.services.hello.loadbalancer.server.port=80
- traefik.http.routers.hello.middlewares=my-plugin@file
- traefik.http.routers.hello.middlewares=my-plugin@file
````

To complete the setup, the Traefik configuration must be extended with the plugin. For this you must create the `traefik.yml` and the dynamic-configuration.yml` files if not present already.

traefik.yml

````yml
log:
level: INFO
Expand All @@ -54,7 +52,7 @@ experimental:
moduleName: github.com/PascalMinder/geoblock
````

dynamic-configuration.yml
#### `dynamic-configuration.yml`

````yml
http:
Expand All @@ -76,16 +74,13 @@ http:
- CH
````

### Traefik Pilot
### Traefik Plugin registry

To install the plugin with Traefik Pilot, follow the instruction on their website.
This procedure will install the plugin via the [Traefik Plugin registry](https://plugins.traefik.io/install).

Add the following to your `traefik-config.yml`

```yml
pilot:
token: "xxxx-your-token-xxxx"
experimental:
plugins:
GeoBlock:
Expand Down Expand Up @@ -166,6 +161,7 @@ This configuration might not work. It's just to give you an idea how to configur
- `logLocalRequests`: If set to true, will log every connection from any IP in the private IP range
- `api`: API URI used for querying the country associated with the connecting IP
- `countries`: list of allowed countries
- `backListMode`: set to `false` so the plugin is running in `whitelist mode`

````yml
my-GeoBlock:
Expand All @@ -181,6 +177,7 @@ my-GeoBlock:
forceMonthlyUpdate: false
allowUnknownCountries: false
unknownCountryApiResponse: "nil"
backListMode: false
countries:
- AF # Afghanistan
- AL # Albania
Expand Down Expand Up @@ -471,10 +468,14 @@ Even if an IP stays in the cache for a period of a month (about 30 x 24 hours),

Some IP addresses have no country associated with them. If this option is set to true, all IPs with no associated country are also allowed.

### Unknown country api response`unknownCountryApiResponse`
### Unknown country api response `unknownCountryApiResponse`

The API uri can be customized. This options allows to customize the response string of the API when a IP with no associated country is requested.

### Back list mode `blackListMode`

When set to `true` the filter logic is inverted, i.e. requests originating from countries listed in the [`countries`](#countries-countries) list are **blocked**. Default: `false`.

### Countries `countries`

A list of country codes from which connections to the service should be allowed
A list of country codes from which connections to the service should be allowed. Logic can be inverted by using the [`blackListMode`](#back-list-mode-blacklistmode).

0 comments on commit c95af23

Please sign in to comment.