diff --git a/.traefik.yml b/.traefik.yml index 54da752..616c956 100755 --- a/.traefik.yml +++ b/.traefik.yml @@ -13,5 +13,7 @@ testData: api: "https://get.geojs.io/v1/ip/country/{ip}" cachesize: 15 forcemonthlyupdate: true + allowunknowncountries: false + unknowncountryapiresponse: "nil" countries: - CH \ No newline at end of file diff --git a/geoblock.go b/geoblock.go index 7d25042..9a3648d 100755 --- a/geoblock.go +++ b/geoblock.go @@ -18,18 +18,21 @@ const ( xForwardedFor = "X-Forwarded-For" xRealIp = "X-Real-IP" NumberOfHoursInMonth = 30 * 24 + UnknownCountryCode = "AA" ) // Config the plugin configuration. type Config struct { - AllowLocalRequests bool `yaml:"allowlocalrequests"` - LogLocalRequests bool `yaml:"loglocalrequests"` - LogAllowedRequests bool `yaml:"logallowedrequests"` - LogAPIRequests bool `yaml:"logapirequests"` - Api string `yaml:"api"` - CacheSize int `yaml:"cachesize"` - ForceMonthlyUpdate bool `yaml:"forcemonthlyupdate"` - Countries []string `yaml:"countries,omitempty"` + AllowLocalRequests bool `yaml:"allowlocalrequests"` + LogLocalRequests bool `yaml:"loglocalrequests"` + LogAllowedRequests bool `yaml:"logallowedrequests"` + LogAPIRequests bool `yaml:"logapirequests"` + Api string `yaml:"api"` + CacheSize int `yaml:"cachesize"` + ForceMonthlyUpdate bool `yaml:"forcemonthlyupdate"` + AllowUnknownCountries bool `yaml:"allowunknowncountries"` + UnknownCountryAPIResponse string `yaml:"unknowncountryapiresponse"` + Countries []string `yaml:"countries,omitempty"` } type IpEntry struct { @@ -44,17 +47,19 @@ func CreateConfig() *Config { // GeoBlock a Traefik plugin. type GeoBlock struct { - next http.Handler - allowLocalRequests bool - logLocalRequests bool - logAllowedRequests bool - logAPIRequests bool - apiUri string - ForceMonthlyUpdate bool - countries []string - privateIPRanges []*net.IPNet - database *lru.LRUCache - name string + next http.Handler + allowLocalRequests bool + logLocalRequests bool + logAllowedRequests bool + logAPIRequests bool + apiUri string + forceMonthlyUpdate bool + allowUnknownCountries bool + unknownCountryCode string + countries []string + privateIPRanges []*net.IPNet + database *lru.LRUCache + name string } // New created a new GeoBlock plugin. @@ -72,6 +77,8 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h log.Println("log local requests: ", config.LogLocalRequests) log.Println("log allowed requests: ", config.LogAllowedRequests) log.Println("log api requests: ", config.LogAPIRequests) + log.Println("allow unknown countries: ", config.AllowUnknownCountries) + log.Println("unknown country api response: ", config.UnknownCountryAPIResponse) log.Println("allowed countries: ", config.Countries) cache, err := lru.NewLRUCache(config.CacheSize) @@ -80,17 +87,19 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h } return &GeoBlock{ - next: next, - allowLocalRequests: config.AllowLocalRequests, - logLocalRequests: config.LogLocalRequests, - logAllowedRequests: config.LogAllowedRequests, - logAPIRequests: config.LogAPIRequests, - apiUri: config.Api, - ForceMonthlyUpdate: config.ForceMonthlyUpdate, - countries: config.Countries, - privateIPRanges: InitPrivateIPBlocks(), - database: cache, - name: name, + next: next, + allowLocalRequests: config.AllowLocalRequests, + logLocalRequests: config.LogLocalRequests, + logAllowedRequests: config.LogAllowedRequests, + logAPIRequests: config.LogAPIRequests, + apiUri: config.Api, + forceMonthlyUpdate: config.ForceMonthlyUpdate, + allowUnknownCountries: config.AllowUnknownCountries, + unknownCountryCode: config.UnknownCountryAPIResponse, + countries: config.Countries, + privateIPRanges: InitPrivateIPBlocks(), + database: cache, + name: name, }, nil } @@ -139,7 +148,7 @@ func (a *GeoBlock) ServeHTTP(rw http.ResponseWriter, req *http.Request) { log.Println("Loaded from database: ", entry) // check if existing entry was made more than a month ago, if so update the entry - if time.Since(entry.Timestamp).Hours() >= NumberOfHoursInMonth && a.ForceMonthlyUpdate { + if time.Since(entry.Timestamp).Hours() >= NumberOfHoursInMonth && a.forceMonthlyUpdate { entry, err = a.CreateNewIPEntry(ipAddressString) if err != nil { @@ -149,7 +158,7 @@ func (a *GeoBlock) ServeHTTP(rw http.ResponseWriter, req *http.Request) { } } - var isAllowed bool = StringInSlice(entry.Country, a.countries) + var isAllowed bool = StringInSlice(entry.Country, a.countries) || (entry.Country == UnknownCountryCode && a.allowUnknownCountries) if !isAllowed { log.Printf("%s: request denied [%s] for country [%s]", a.name, ipAddress, entry.Country) @@ -245,6 +254,11 @@ func (a *GeoBlock) CallGeoJS(ipAddress string) (string, error) { sb := string(body) countryCode := strings.TrimSuffix(sb, "\n") + // api response for unknown country + if len([]rune(countryCode)) == len(a.unknownCountryCode) && countryCode == a.unknownCountryCode { + return UnknownCountryCode, nil + } + // this could possible cause a DoS attack if len([]rune(countryCode)) != 2 { return "", fmt.Errorf("API response has more than 2 characters") diff --git a/geoblock_test.go b/geoblock_test.go index 4f34667..6d01c6a 100755 --- a/geoblock_test.go +++ b/geoblock_test.go @@ -11,11 +11,12 @@ import ( ) const ( - xForwardedFor = "X-Forwarded-For" - CA = "99.220.109.148" - CH = "82.220.110.18" - PrivateRange = "192.168.1.1" - Invalid = "192.168.1.X" + xForwardedFor = "X-Forwarded-For" + CA = "99.220.109.148" + CH = "82.220.110.18" + PrivateRange = "192.168.1.1" + Invalid = "192.168.1.X" + UnknownCountryIpGoogle = "66.249.93.100" ) func TestEmptyApi(t *testing.T) { @@ -109,6 +110,72 @@ func TestAllowedContry(t *testing.T) { assertStatusCode(t, recorder.Result(), http.StatusOK) } +func TestAllowedUnknownContry(t *testing.T) { + cfg := GeoBlock.CreateConfig() + + cfg.AllowLocalRequests = false + cfg.LogLocalRequests = false + cfg.AllowUnknownCountries = true + cfg.UnknownCountryAPIResponse = "nil" + cfg.Api = "https://get.geojs.io/v1/ip/country/{ip}" + cfg.Countries = append(cfg.Countries, "CH") + cfg.CacheSize = 10 + + 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, UnknownCountryIpGoogle) + + handler.ServeHTTP(recorder, req) + + assertStatusCode(t, recorder.Result(), http.StatusOK) +} + +func TestDenyUnknownContry(t *testing.T) { + cfg := GeoBlock.CreateConfig() + + cfg.AllowLocalRequests = false + cfg.LogLocalRequests = false + cfg.AllowUnknownCountries = false + cfg.UnknownCountryAPIResponse = "nil" + cfg.Api = "https://get.geojs.io/v1/ip/country/{ip}" + cfg.Countries = append(cfg.Countries, "CH") + cfg.CacheSize = 10 + + 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, UnknownCountryIpGoogle) + + handler.ServeHTTP(recorder, req) + + assertStatusCode(t, recorder.Result(), http.StatusForbidden) +} + func TestAllowedContryCacheLookUp(t *testing.T) { cfg := GeoBlock.CreateConfig() diff --git a/readme.md b/readme.md index 4fcd622..ab63f69 100755 --- a/readme.md +++ b/readme.md @@ -18,6 +18,8 @@ my-GeoBlock: api: "https://get.geojs.io/v1/ip/country/{ip}" cachesize: 15 forcemonthlyupdate: false + allowunknowncountries: false + unknowncountrycode: "nil" countries: - AF # Afghanistan - AL # Albania @@ -293,5 +295,11 @@ Defines the max size of the [LRU](https://en.wikipedia.org/wiki/Cache_replacemen ### Force monthly update `forcemonthlyupdate` Even if an IP stays in the cache for a period of a month (about 30 x 24 hours), it must be fetch again after a month. +### Allow unknown countries `allowunknowncountries` +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` +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. + ### Countries A list of country codes from which connections to the service should be allowed