Skip to content

Commit c95af23

Browse files
authored
feat: Blacklist mode (#29)
Added support for blacklist mode to geoblock.go Co-authored-by: Thomas Meckel <tmeckel@users.noreply.github.com>
1 parent 3ffcf3f commit c95af23

File tree

3 files changed

+85
-25
lines changed

3 files changed

+85
-25
lines changed

geoblock.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type Config struct {
3939
ForceMonthlyUpdate bool `yaml:"forceMonthlyUpdate"`
4040
AllowUnknownCountries bool `yaml:"allowUnknownCountries"`
4141
UnknownCountryAPIResponse string `yaml:"unknownCountryApiResponse"`
42+
BlackListMode bool `yaml:"blacklist"`
4243
Countries []string `yaml:"countries,omitempty"`
4344
}
4445

@@ -64,6 +65,7 @@ type GeoBlock struct {
6465
forceMonthlyUpdate bool
6566
allowUnknownCountries bool
6667
unknownCountryCode string
68+
blackListMode bool
6769
countries []string
6870
privateIPRanges []*net.IPNet
6971
database *lru.LRUCache
@@ -96,7 +98,8 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
9698
infoLogger.Printf("force monthly update: %t", config.ForceMonthlyUpdate)
9799
infoLogger.Printf("allow unknown countries: %t", config.AllowUnknownCountries)
98100
infoLogger.Printf("unknown country api response: %s", config.UnknownCountryAPIResponse)
99-
infoLogger.Printf("allowed countries: %v", config.Countries)
101+
infoLogger.Printf("blacklist mode: %t", config.BlackListMode)
102+
infoLogger.Printf("countries: %v", config.Countries)
100103

101104
cache, err := lru.NewLRUCache(config.CacheSize)
102105
if err != nil {
@@ -114,6 +117,7 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
114117
forceMonthlyUpdate: config.ForceMonthlyUpdate,
115118
allowUnknownCountries: config.AllowUnknownCountries,
116119
unknownCountryCode: config.UnknownCountryAPIResponse,
120+
blackListMode: config.BlackListMode,
117121
countries: config.Countries,
118122
privateIPRanges: initPrivateIPBlocks(),
119123
database: cache,
@@ -178,7 +182,8 @@ func (a *GeoBlock) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
178182
}
179183
}
180184

181-
isAllowed := stringInSlice(entry.Country, a.countries) || (entry.Country == unknownCountryCode && a.allowUnknownCountries)
185+
isAllowed := (stringInSlice(entry.Country, a.countries) != a.blackListMode) ||
186+
(entry.Country == unknownCountryCode && a.allowUnknownCountries)
182187

183188
if !isAllowed {
184189
infoLogger.Printf("%s: request denied [%s] for country [%s]", a.name, ipAddress, entry.Country)

geoblock_test.go

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ import (
1111
)
1212

1313
const (
14-
xForwardedFor = "X-Forwarded-For"
15-
caExampleIP = "99.220.109.148"
16-
chExampleIP = "82.220.110.18"
17-
privateRangeIP = "192.168.1.1"
18-
invalidIP = "192.168.1.X"
19-
unknownCountryIPGoogle = "66.249.93.100"
20-
apiURI = "https://get.geojs.io/v1/ip/country/{ip}"
14+
xForwardedFor = "X-Forwarded-For"
15+
caExampleIP = "99.220.109.148"
16+
chExampleIP = "82.220.110.18"
17+
privateRangeIP = "192.168.1.1"
18+
invalidIP = "192.168.1.X"
19+
unknownCountry = "1.1.1.1"
20+
apiURI = "https://get.geojs.io/v1/ip/country/{ip}"
2121
)
2222

2323
func TestEmptyApi(t *testing.T) {
@@ -139,7 +139,7 @@ func TestAllowedUnknownCountry(t *testing.T) {
139139
t.Fatal(err)
140140
}
141141

142-
req.Header.Add(xForwardedFor, unknownCountryIPGoogle)
142+
req.Header.Add(xForwardedFor, unknownCountry)
143143

144144
handler.ServeHTTP(recorder, req)
145145

@@ -165,7 +165,7 @@ func TestDenyUnknownCountry(t *testing.T) {
165165
t.Fatal(err)
166166
}
167167

168-
req.Header.Add(xForwardedFor, unknownCountryIPGoogle)
168+
req.Header.Add(xForwardedFor, unknownCountry)
169169

170170
handler.ServeHTTP(recorder, req)
171171

@@ -226,6 +226,60 @@ func TestDeniedCountry(t *testing.T) {
226226
assertStatusCode(t, recorder.Result(), http.StatusForbidden)
227227
}
228228

229+
func TestAllowBlacklistMode(t *testing.T) {
230+
cfg := createTesterConfig()
231+
cfg.BlackListMode = true
232+
cfg.Countries = append(cfg.Countries, "CH")
233+
234+
ctx := context.Background()
235+
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
236+
237+
handler, err := geoblock.New(ctx, next, cfg, "GeoBlock")
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
242+
recorder := httptest.NewRecorder()
243+
244+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
245+
if err != nil {
246+
t.Fatal(err)
247+
}
248+
249+
req.Header.Add(xForwardedFor, caExampleIP)
250+
251+
handler.ServeHTTP(recorder, req)
252+
253+
assertStatusCode(t, recorder.Result(), http.StatusOK)
254+
}
255+
256+
func TestDenyBlacklistMode(t *testing.T) {
257+
cfg := createTesterConfig()
258+
cfg.BlackListMode = true
259+
cfg.Countries = append(cfg.Countries, "CH")
260+
261+
ctx := context.Background()
262+
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {})
263+
264+
handler, err := geoblock.New(ctx, next, cfg, "GeoBlock")
265+
if err != nil {
266+
t.Fatal(err)
267+
}
268+
269+
recorder := httptest.NewRecorder()
270+
271+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
272+
if err != nil {
273+
t.Fatal(err)
274+
}
275+
276+
req.Header.Add(xForwardedFor, chExampleIP)
277+
278+
handler.ServeHTTP(recorder, req)
279+
280+
assertStatusCode(t, recorder.Result(), http.StatusForbidden)
281+
}
282+
229283
func TestAllowLocalIP(t *testing.T) {
230284
cfg := createTesterConfig()
231285
cfg.Countries = append(cfg.Countries, "CH")

readme.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# GeoBlock
22

3-
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/).
3+
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/).
44

55
## Configuration
66

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

1111
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/).
1212

13-
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/`):
13+
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:
1414

15-
docker-compose.yml
15+
#### `docker-compose.yml`
1616

1717
````yml
1818
version: "3.7"
@@ -37,13 +37,11 @@ services:
3737
- traefik.http.routers.hello.entrypoints=http
3838
- traefik.http.routers.hello.rule=Host(`hello.localhost`)
3939
- traefik.http.services.hello.loadbalancer.server.port=80
40-
- traefik.http.routers.hello.middlewares=my-plugin@file
40+
- traefik.http.routers.hello.middlewares=my-plugin@file
4141
````
4242

4343
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.
4444

45-
traefik.yml
46-
4745
````yml
4846
log:
4947
level: INFO
@@ -54,7 +52,7 @@ experimental:
5452
moduleName: github.com/PascalMinder/geoblock
5553
````
5654

57-
dynamic-configuration.yml
55+
#### `dynamic-configuration.yml`
5856

5957
````yml
6058
http:
@@ -76,16 +74,13 @@ http:
7674
- CH
7775
````
7876

79-
### Traefik Pilot
77+
### Traefik Plugin registry
8078

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

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

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

170166
````yml
171167
my-GeoBlock:
@@ -181,6 +177,7 @@ my-GeoBlock:
181177
forceMonthlyUpdate: false
182178
allowUnknownCountries: false
183179
unknownCountryApiResponse: "nil"
180+
backListMode: false
184181
countries:
185182
- AF # Afghanistan
186183
- AL # Albania
@@ -471,10 +468,14 @@ Even if an IP stays in the cache for a period of a month (about 30 x 24 hours),
471468

472469
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.
473470

474-
### Unknown country api response`unknownCountryApiResponse`
471+
### Unknown country api response `unknownCountryApiResponse`
475472

476473
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.
477474

475+
### Back list mode `blackListMode`
476+
477+
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`.
478+
478479
### Countries `countries`
479480

480-
A list of country codes from which connections to the service should be allowed
481+
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 commit comments

Comments
 (0)