Skip to content

Commit

Permalink
Add option to allow IPs with no associated country
Browse files Browse the repository at this point in the history
  • Loading branch information
PascalMinder committed Dec 30, 2021
1 parent 7240d91 commit e863304
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 37 deletions.
2 changes: 2 additions & 0 deletions .traefik.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ testData:
api: "https://get.geojs.io/v1/ip/country/{ip}"
cachesize: 15
forcemonthlyupdate: true
allowunknowncountries: false
unknowncountryapiresponse: "nil"
countries:
- CH
78 changes: 46 additions & 32 deletions geoblock.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,21 @@ const (
xForwardedFor = "X-Forwarded-For"
xRealIp = "X-Real-IP"
NumberOfHoursInMonth = 30 * 24
UnknownCountryCode = "AA"

This comment has been minimized.

Copy link
@Thom-x

Thom-x Dec 30, 2021

You didn't put this value in the README.
Could be good to mention it ?

This comment has been minimized.

Copy link
@PascalMinder

PascalMinder Dec 30, 2021

Author Owner

Hmm, the value is never used outside the plugin. I just thought to choose a value, which is not used in the ISO 3166-1 alpha-2.
AA is in the group "User-assigned": free for assignment at the disposal of users (from Wikipedia)
https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2#AA

I can add it to the description

This comment has been minimized.

Copy link
@Thom-x

Thom-x Dec 30, 2021

Oh I get it ! I was thinking you must put "AA" in the list as well. No need in this case.

)

// 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 {
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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")
Expand Down
77 changes: 72 additions & 5 deletions geoblock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()

Expand Down
8 changes: 8 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ my-GeoBlock:
api: "https://get.geojs.io/v1/ip/country/{ip}"
cachesize: 15
forcemonthlyupdate: false
allowunknowncountries: false
unknowncountrycode: "nil"

This comment has been minimized.

Copy link
@ezruneko

ezruneko Dec 31, 2021

The first! Amazing job! thank you for your support. The second this option is wrong you need to change to "unknowncountryapiresponse"

countries:
- AF # Afghanistan
- AL # Albania
Expand Down Expand Up @@ -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

0 comments on commit e863304

Please sign in to comment.