mirror of
				https://github.com/coredns/coredns.git
				synced 2025-10-30 09:43:17 -04:00 
			
		
		
		
	Create geoip plugin (#4688)
* Create geoip plugin Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Move DBFILE bullet below example Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/README.md Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove plugin name test case Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove languages option Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update free database link Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove last language bits Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Use 127.0.0.1 as probing IP Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/geoip.go Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Update plugin/geoip/geoip.go Co-authored-by: Miek Gieben <miek@miek.nl> Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Use relative path for fixtures dir Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Set names with default string zero value Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove unused db types Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove non city databases in testdata Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Remove create databases main Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Fix metadata label format test case Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Fix import path block Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * go fmt after changes Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Tidy up go.mod and go.sum Signed-off-by: Sven Nebel <nebel.sven@gmail.com> * Add plugin to CODEOWNERS Signed-off-by: Sven Nebel <nebel.sven@gmail.com> Co-authored-by: Miek Gieben <miek@miek.nl>
This commit is contained in:
		| @@ -30,6 +30,7 @@ go.mod                  @miekg @chrisohaver @johnbelamaric @yongtang @stp-ip | ||||
| /plugin/etcd/           @miekg @nitisht | ||||
| /plugin/file/           @miekg @yongtang @stp-ip | ||||
| /plugin/forward/        @johnbelamaric @miekg @rdrozhdzh | ||||
| /plugin/geoip/          @miekg @snebel29 | ||||
| /plugin/grpc/           @inigohu @miekg @zouyee | ||||
| /plugin/health/         @fastest963 @miekg @zouyee | ||||
| /plugin/hosts/          @johnbelamaric @pmoroney | ||||
|   | ||||
| @@ -11,6 +11,7 @@ package dnsserver | ||||
| // care what plugin above them are doing. | ||||
| var Directives = []string{ | ||||
| 	"metadata", | ||||
| 	"geoip", | ||||
| 	"cancel", | ||||
| 	"tls", | ||||
| 	"reload", | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import ( | ||||
| 	_ "github.com/coredns/coredns/plugin/etcd" | ||||
| 	_ "github.com/coredns/coredns/plugin/file" | ||||
| 	_ "github.com/coredns/coredns/plugin/forward" | ||||
| 	_ "github.com/coredns/coredns/plugin/geoip" | ||||
| 	_ "github.com/coredns/coredns/plugin/grpc" | ||||
| 	_ "github.com/coredns/coredns/plugin/health" | ||||
| 	_ "github.com/coredns/coredns/plugin/hosts" | ||||
|   | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @@ -23,6 +23,7 @@ require ( | ||||
| 	github.com/opentracing/opentracing-go v1.2.0 | ||||
| 	github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 | ||||
| 	github.com/openzipkin/zipkin-go v0.2.2 | ||||
| 	github.com/oschwald/geoip2-golang v1.5.0 | ||||
| 	github.com/philhofer/fwd v1.1.1 // indirect | ||||
| 	github.com/prometheus/client_golang v1.11.0 | ||||
| 	github.com/prometheus/client_model v0.2.0 | ||||
|   | ||||
							
								
								
									
										5
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								go.sum
									
									
									
									
									
								
							| @@ -342,6 +342,10 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS | ||||
| github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= | ||||
| github.com/openzipkin/zipkin-go v0.2.2 h1:nY8Hti+WKaP0cRsSeQ026wU03QsM762XBeCXBb9NAWI= | ||||
| github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= | ||||
| github.com/oschwald/geoip2-golang v1.5.0 h1:igg2yQIrrcRccB1ytFXqBfOHCjXWIoMv85lVJ1ONZzw= | ||||
| github.com/oschwald/geoip2-golang v1.5.0/go.mod h1:xdvYt5xQzB8ORWFqPnqMwZpCpgNagttWdoZLlJQzg7s= | ||||
| github.com/oschwald/maxminddb-golang v1.8.0 h1:Uh/DSnGoxsyp/KYbY1AuP0tYEwfs0sCph9p/UMXK/Hk= | ||||
| github.com/oschwald/maxminddb-golang v1.8.0/go.mod h1:RXZtst0N6+FY/3qCNmZMBApR19cdQj43/NM9VkrNAis= | ||||
| github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= | ||||
| github.com/philhofer/fwd v1.1.1 h1:GdGcTjf5RNAxwS4QLsiMzJYj5KEvPJD3Abr261yRQXQ= | ||||
| github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= | ||||
| @@ -558,6 +562,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w | ||||
| golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
| # log:log | ||||
|  | ||||
| metadata:metadata | ||||
| geoip:geoip | ||||
| cancel:cancel | ||||
| tls:tls | ||||
| reload:reload | ||||
|   | ||||
							
								
								
									
										73
									
								
								plugin/geoip/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								plugin/geoip/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| # geoip | ||||
|  | ||||
| ## Name | ||||
| *geoip* - Lookup maxmind geoip2 databases using the client IP, then add associated geoip data to the context request. | ||||
|  | ||||
| ## Description | ||||
| The *geoip* plugin add geo location data associated with the client IP, it allows you to configure a [geoIP2 maxmind database](https://dev.maxmind.com/geoip/docs/databases) to add the geo location data associated with the IP address. | ||||
|  | ||||
| The data is added leveraging the *metadata* plugin, values can then be retrieved using it as well, for example: | ||||
|  | ||||
| ```go | ||||
| import ( | ||||
|     "strconv" | ||||
|     "github.com/coredns/coredns/plugin/metadata" | ||||
| ) | ||||
| // ... | ||||
| if getLongitude := metadata.ValueFunc(ctx, "geoip/longitude"); getLongitude != nil { | ||||
|     if longitude, err := strconv.ParseFloat(getLongitude(), 64); err == nil { | ||||
| 		// Do something useful with longitude. | ||||
| 	} | ||||
| } else { | ||||
|     // The metadata label geoip/longitude for some reason, was not set. | ||||
| } | ||||
| // ... | ||||
| ``` | ||||
|  | ||||
| ## Databases | ||||
| The supported databases use city schema such as `City` and `Enterprise`. Other databases types with different schemas are not supported yet.  | ||||
|  | ||||
| You can download a [free and public City database](https://dev.maxmind.com/geoip/geolite2-free-geolocation-data). | ||||
|  | ||||
| ## Syntax | ||||
| ```txt | ||||
| geoip [DBFILE] | ||||
| ``` | ||||
| * **DBFILE** the mmdb database file path. | ||||
|  | ||||
| ## Examples | ||||
| The following configuration configures the `City` database. | ||||
| ```txt | ||||
| . { | ||||
|     geoip /opt/geoip2/db/GeoLite2-City.mmdb | ||||
|     metadata # Note that metadata plugin must be enabled as well. | ||||
| } | ||||
| ``` | ||||
|  | ||||
| ## Metadatada Labels | ||||
| A limited set of fields will be exported as labels, all values are stored using strings **regardless of their underlying value type**, and therefore you may have to convert it back to its original type, note that numeric values are always represented in base 10. | ||||
|  | ||||
| | Label                                | Type      | Example          | Description | ||||
| | :----------------------------------- | :-------- | :--------------  | :------------------ | ||||
| | `geoip/city/name`                    | `string`  | `Cambridge`      | Then city name in English language. | ||||
| | `geoip/country/code`                 | `string`  | `GB`             | Country [ISO 3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) code. | ||||
| | `geoip/country/name`                 | `string`  | `United Kingdom` | The country name in English language. | ||||
| | `geoip/country/is_in_european_union` | `bool`    | `false`          | Either `true` or `false`. | ||||
| | `geoip/continent/code`               | `string`  | `EU`             | See [Continent codes](#ContinentCodes). | ||||
| | `geoip/continent/name`               | `string`  | `Europe`         | The continent name in English language. | ||||
| | `geoip/latitude`                     | `float64` | `52.2242`        | Base 10, max available precision. | ||||
| | `geoip/longitude`                    | `float64` | `0.1315`         | Base 10, max available precision. | ||||
| | `geoip/timezone`                     | `string`  | `Europe/London`  | The timezone. | ||||
| | `geoip/postalcode`                   | `string`  | `CB4`            | The postal code. | ||||
|  | ||||
| ## Continent Codes | ||||
|  | ||||
| | Value | Continent (EN) | | ||||
| | :---- | :------------- | | ||||
| | AF    | Africa         | | ||||
| | AN    | Antarctica     | | ||||
| | AS    | Asia           | | ||||
| | EU    | Europe         | | ||||
| | NA    | North America  | | ||||
| | OC    | Oceania        | | ||||
| | SA    | South America  | | ||||
							
								
								
									
										58
									
								
								plugin/geoip/city.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								plugin/geoip/city.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| package geoip | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"strconv" | ||||
|  | ||||
| 	"github.com/coredns/coredns/plugin/metadata" | ||||
|  | ||||
| 	"github.com/oschwald/geoip2-golang" | ||||
| ) | ||||
|  | ||||
| const defaultLang = "en" | ||||
|  | ||||
| func (g GeoIP) setCityMetadata(ctx context.Context, data *geoip2.City) { | ||||
| 	// Set labels for city, country and continent names. | ||||
| 	cityName := data.City.Names[defaultLang] | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/city/name", func() string { | ||||
| 		return cityName | ||||
| 	}) | ||||
| 	countryName := data.Country.Names[defaultLang] | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/country/name", func() string { | ||||
| 		return countryName | ||||
| 	}) | ||||
| 	continentName := data.Continent.Names[defaultLang] | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/continent/name", func() string { | ||||
| 		return continentName | ||||
| 	}) | ||||
|  | ||||
| 	countryCode := data.Country.IsoCode | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/country/code", func() string { | ||||
| 		return countryCode | ||||
| 	}) | ||||
| 	isInEurope := strconv.FormatBool(data.Country.IsInEuropeanUnion) | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/country/is_in_european_union", func() string { | ||||
| 		return isInEurope | ||||
| 	}) | ||||
| 	continentCode := data.Continent.Code | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/continent/code", func() string { | ||||
| 		return continentCode | ||||
| 	}) | ||||
|  | ||||
| 	latitude := strconv.FormatFloat(float64(data.Location.Latitude), 'f', -1, 64) | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/latitude", func() string { | ||||
| 		return latitude | ||||
| 	}) | ||||
| 	longitude := strconv.FormatFloat(float64(data.Location.Longitude), 'f', -1, 64) | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/longitude", func() string { | ||||
| 		return longitude | ||||
| 	}) | ||||
| 	timeZone := data.Location.TimeZone | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/timezone", func() string { | ||||
| 		return timeZone | ||||
| 	}) | ||||
| 	postalCode := data.Postal.Code | ||||
| 	metadata.SetValueFunc(ctx, pluginName+"/postalcode", func() string { | ||||
| 		return postalCode | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										95
									
								
								plugin/geoip/geoip.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								plugin/geoip/geoip.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | ||||
| // Package geoip implements a max mind database plugin. | ||||
| package geoip | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/coredns/coredns/plugin" | ||||
| 	clog "github.com/coredns/coredns/plugin/pkg/log" | ||||
| 	"github.com/coredns/coredns/request" | ||||
|  | ||||
| 	"github.com/miekg/dns" | ||||
| 	"github.com/oschwald/geoip2-golang" | ||||
| ) | ||||
|  | ||||
| var log = clog.NewWithPlugin(pluginName) | ||||
|  | ||||
| // GeoIP is a plugin that add geo location data to the request context by looking up a maxmind | ||||
| // geoIP2 database, and which data can be later consumed by other middlewares. | ||||
| type GeoIP struct { | ||||
| 	Next plugin.Handler | ||||
| 	db   db | ||||
| } | ||||
|  | ||||
| type db struct { | ||||
| 	*geoip2.Reader | ||||
| 	// provides defines the schemas that can be obtained by querying this database, by using | ||||
| 	// bitwise operations. | ||||
| 	provides int | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	city = 1 << iota | ||||
| ) | ||||
|  | ||||
| var probingIP = net.ParseIP("127.0.0.1") | ||||
|  | ||||
| func newGeoIP(dbPath string) (*GeoIP, error) { | ||||
| 	reader, err := geoip2.Open(dbPath) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("failed to open database file: %v", err) | ||||
| 	} | ||||
| 	db := db{Reader: reader} | ||||
| 	schemas := []struct { | ||||
| 		provides int | ||||
| 		name     string | ||||
| 		validate func() error | ||||
| 	}{ | ||||
| 		{name: "city", provides: city, validate: func() error { _, err := reader.City(probingIP); return err }}, | ||||
| 	} | ||||
| 	// Query the database to figure out the database type. | ||||
| 	for _, schema := range schemas { | ||||
| 		if err := schema.validate(); err != nil { | ||||
| 			// If we get an InvalidMethodError then we know this database does not provide that schema. | ||||
| 			if _, ok := err.(geoip2.InvalidMethodError); !ok { | ||||
| 				return nil, fmt.Errorf("unexpected failure looking up database %q schema %q: %v", filepath.Base(dbPath), schema.name, err) | ||||
| 			} | ||||
| 		} else { | ||||
| 			db.provides = db.provides | schema.provides | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if db.provides&city == 0 { | ||||
| 		return nil, fmt.Errorf("database does not provide city schema") | ||||
| 	} | ||||
|  | ||||
| 	return &GeoIP{db: db}, nil | ||||
| } | ||||
|  | ||||
| // ServeDNS implements the plugin.Handler interface. | ||||
| func (g GeoIP) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) { | ||||
| 	return plugin.NextOrFailure(pluginName, g.Next, ctx, w, r) | ||||
| } | ||||
|  | ||||
| // Metadata implements the metadata.Provider Interface in the metadata plugin, and is used to store | ||||
| // the data associated with the source IP of every request. | ||||
| func (g GeoIP) Metadata(ctx context.Context, state request.Request) context.Context { | ||||
| 	srcIP := net.ParseIP(state.IP()) | ||||
|  | ||||
| 	switch { | ||||
| 	case g.db.provides&city == city: | ||||
| 		data, err := g.db.City(srcIP) | ||||
| 		if err != nil { | ||||
| 			log.Debugf("Setting up metadata failed due to database lookup error: %v", err) | ||||
| 			return ctx | ||||
| 		} | ||||
| 		g.setCityMetadata(ctx, data) | ||||
| 	} | ||||
| 	return ctx | ||||
| } | ||||
|  | ||||
| // Name implements the Handler interface. | ||||
| func (g GeoIP) Name() string { return pluginName } | ||||
							
								
								
									
										61
									
								
								plugin/geoip/geoip_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								plugin/geoip/geoip_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| package geoip | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/coredns/coredns/plugin/metadata" | ||||
| 	"github.com/coredns/coredns/plugin/test" | ||||
| 	"github.com/coredns/coredns/request" | ||||
| ) | ||||
|  | ||||
| func TestMetadata(t *testing.T) { | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		dbPath        string | ||||
| 		label         string | ||||
| 		expectedValue string | ||||
| 	}{ | ||||
| 		{cityDBPath, "geoip/city/name", "Cambridge"}, | ||||
|  | ||||
| 		{cityDBPath, "geoip/country/code", "GB"}, | ||||
| 		{cityDBPath, "geoip/country/name", "United Kingdom"}, | ||||
| 		// is_in_european_union is set to true only to work around bool zero value, and test is really being set. | ||||
| 		{cityDBPath, "geoip/country/is_in_european_union", "true"}, | ||||
|  | ||||
| 		{cityDBPath, "geoip/continent/code", "EU"}, | ||||
| 		{cityDBPath, "geoip/continent/name", "Europe"}, | ||||
|  | ||||
| 		{cityDBPath, "geoip/latitude", "52.2242"}, | ||||
| 		{cityDBPath, "geoip/longitude", "0.1315"}, | ||||
| 		{cityDBPath, "geoip/timezone", "Europe/London"}, | ||||
| 		{cityDBPath, "geoip/postalcode", "CB4"}, | ||||
| 	} | ||||
|  | ||||
| 	for i, _test := range tests { | ||||
| 		geoIP, err := newGeoIP(_test.dbPath) | ||||
| 		if err != nil { | ||||
| 			t.Fatalf("Test %d: unable to create geoIP plugin: %v", i, err) | ||||
| 		} | ||||
| 		state := request.Request{ | ||||
| 			W: &test.ResponseWriter{RemoteIP: "81.2.69.142"}, // This IP should be be part of the CDIR address range used to create the database fixtures. | ||||
| 		} | ||||
| 		ctx := metadata.ContextWithMetadata(context.Background()) | ||||
| 		rCtx := geoIP.Metadata(ctx, state) | ||||
| 		if fmt.Sprintf("%p", ctx) != fmt.Sprintf("%p", rCtx) { | ||||
| 			t.Errorf("Test %d: returned context is expected to be the same one passed in the Metadata function", i) | ||||
| 		} | ||||
|  | ||||
| 		fn := metadata.ValueFunc(ctx, _test.label) | ||||
| 		if fn == nil { | ||||
| 			t.Errorf("Test %d: label %q not set in metadata plugin context", i, _test.label) | ||||
| 			continue | ||||
| 		} | ||||
| 		value := fn() | ||||
| 		if value != _test.expectedValue { | ||||
| 			t.Errorf("Test %d: expected value for label %q should be %q, got %q instead", | ||||
| 				i, _test.label, _test.expectedValue, value) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										53
									
								
								plugin/geoip/setup.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								plugin/geoip/setup.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| package geoip | ||||
|  | ||||
| import ( | ||||
| 	"github.com/coredns/caddy" | ||||
| 	"github.com/coredns/coredns/core/dnsserver" | ||||
| 	"github.com/coredns/coredns/plugin" | ||||
| ) | ||||
|  | ||||
| const pluginName = "geoip" | ||||
|  | ||||
| func init() { plugin.Register(pluginName, setup) } | ||||
|  | ||||
| func setup(c *caddy.Controller) error { | ||||
| 	geoip, err := geoipParse(c) | ||||
| 	if err != nil { | ||||
| 		return plugin.Error(pluginName, err) | ||||
| 	} | ||||
|  | ||||
| 	dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { | ||||
| 		geoip.Next = next | ||||
| 		return geoip | ||||
| 	}) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func geoipParse(c *caddy.Controller) (*GeoIP, error) { | ||||
| 	var dbPath string | ||||
|  | ||||
| 	for c.Next() { | ||||
| 		if !c.NextArg() { | ||||
| 			return nil, c.ArgErr() | ||||
| 		} | ||||
| 		if dbPath != "" { | ||||
| 			return nil, c.Errf("configuring multiple databases is not supported") | ||||
| 		} | ||||
| 		dbPath = c.Val() | ||||
| 		// There shouldn't be any more arguments. | ||||
| 		if len(c.RemainingArgs()) != 0 { | ||||
| 			return nil, c.ArgErr() | ||||
| 		} | ||||
| 		// The plugin should not have any config block. | ||||
| 		if c.NextBlock() { | ||||
| 			return nil, c.Err("unexpected config block") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	geoIP, err := newGeoIP(dbPath) | ||||
| 	if err != nil { | ||||
| 		return geoIP, c.Err(err.Error()) | ||||
| 	} | ||||
| 	return geoIP, nil | ||||
| } | ||||
							
								
								
									
										109
									
								
								plugin/geoip/setup_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								plugin/geoip/setup_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,109 @@ | ||||
| package geoip | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/coredns/caddy" | ||||
| 	"github.com/coredns/coredns/core/dnsserver" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| 	fixturesDir   = "./testdata" | ||||
| 	cityDBPath    = filepath.Join(fixturesDir, "GeoLite2-City.mmdb") | ||||
| 	unknownDBPath = filepath.Join(fixturesDir, "GeoLite2-UnknownDbType.mmdb") | ||||
| ) | ||||
|  | ||||
| func TestProbingIP(t *testing.T) { | ||||
| 	if probingIP == nil { | ||||
| 		t.Fatalf("Invalid probing IP: %q", probingIP) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestSetup(t *testing.T) { | ||||
| 	c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath)) | ||||
| 	plugins := dnsserver.GetConfig(c).Plugin | ||||
| 	if len(plugins) != 0 { | ||||
| 		t.Fatalf("Expected zero plugins after setup, %d found", len(plugins)) | ||||
| 	} | ||||
| 	if err := setup(c); err != nil { | ||||
| 		t.Fatalf("Expected no errors, but got: %v", err) | ||||
| 	} | ||||
| 	plugins = dnsserver.GetConfig(c).Plugin | ||||
| 	if len(plugins) != 1 { | ||||
| 		t.Fatalf("Expected one plugin after setup, %d found", len(plugins)) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestGeoIPParse(t *testing.T) { | ||||
| 	c := caddy.NewTestController("dns", fmt.Sprintf("%s %s", pluginName, cityDBPath)) | ||||
| 	if err := setup(c); err != nil { | ||||
| 		t.Fatalf("Expected no errors, but got: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	tests := []struct { | ||||
| 		shouldErr      bool | ||||
| 		config         string | ||||
| 		expectedErr    string | ||||
| 		expectedDBType int | ||||
| 	}{ | ||||
| 		// Valid | ||||
| 		{false, fmt.Sprintf("%s %s\n", pluginName, cityDBPath), "", city}, | ||||
|  | ||||
| 		// Invalid | ||||
| 		{true, pluginName, "Wrong argument count", 0}, | ||||
| 		{true, fmt.Sprintf("%s %s {\n\tlanguages en fr es zh-CN\n}\n", pluginName, cityDBPath), "unexpected config block", 0}, | ||||
| 		{true, fmt.Sprintf("%s %s\n%s %s\n", pluginName, cityDBPath, pluginName, cityDBPath), "configuring multiple databases is not supported", 0}, | ||||
| 		{true, fmt.Sprintf("%s 1 2 3", pluginName), "Wrong argument count", 0}, | ||||
| 		{true, fmt.Sprintf("%s { }", pluginName), "Error during parsing", 0}, | ||||
| 		{true, fmt.Sprintf("%s /dbpath { city }", pluginName), "unexpected config block", 0}, | ||||
| 		{true, fmt.Sprintf("%s /invalidPath\n", pluginName), "failed to open database file: open /invalidPath: no such file or directory", 0}, | ||||
| 		{true, fmt.Sprintf("%s %s\n", pluginName, unknownDBPath), "reader does not support the \"UnknownDbType\" database type", 0}, | ||||
| 	} | ||||
|  | ||||
| 	for i, test := range tests { | ||||
| 		c := caddy.NewTestController("dns", test.config) | ||||
| 		geoIP, err := geoipParse(c) | ||||
|  | ||||
| 		if test.shouldErr && err == nil { | ||||
| 			t.Errorf("Test %d: expected error but found none for input %s", i, test.config) | ||||
| 		} | ||||
|  | ||||
| 		if err != nil { | ||||
| 			if !test.shouldErr { | ||||
| 				t.Errorf("Test %d: expected no error but found one for input %s, got: %v", i, test.config, err) | ||||
| 			} | ||||
|  | ||||
| 			if !strings.Contains(err.Error(), test.expectedErr) { | ||||
| 				t.Errorf("Test %d: expected error to contain: %v, found error: %v, input: %s", i, test.expectedErr, err, test.config) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if geoIP.db.Reader == nil { | ||||
| 			t.Errorf("Test %d: after parsing database reader should be initialized", i) | ||||
| 		} | ||||
|  | ||||
| 		if geoIP.db.provides&test.expectedDBType == 0 { | ||||
| 			t.Errorf("Test %d: expected db type %d not found, database file provides %d", i, test.expectedDBType, geoIP.db.provides) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Set nil probingIP to test unexpected validate error() | ||||
| 	defer func(ip net.IP) { probingIP = ip }(probingIP) | ||||
| 	probingIP = nil | ||||
|  | ||||
| 	c = caddy.NewTestController("dns", fmt.Sprintf("%s %s\n", pluginName, cityDBPath)) | ||||
| 	_, err := geoipParse(c) | ||||
| 	if err != nil { | ||||
| 		expectedErr := "unexpected failure looking up database" | ||||
| 		if !strings.Contains(err.Error(), expectedErr) { | ||||
| 			t.Errorf("expected error to contain: %s", expectedErr) | ||||
| 		} | ||||
| 	} else { | ||||
| 		t.Errorf("with a nil probingIP test is expected to fail") | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								plugin/geoip/testdata/GeoLite2-City.mmdb
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								plugin/geoip/testdata/GeoLite2-City.mmdb
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								plugin/geoip/testdata/GeoLite2-UnknownDbType.mmdb
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.2 KiB | 
							
								
								
									
										112
									
								
								plugin/geoip/testdata/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								plugin/geoip/testdata/README.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| # testdata | ||||
| This directory contains mmdb database files used during the testing of this plugin. | ||||
|  | ||||
| # Create mmdb database files | ||||
| If you need to change them to add a new value, or field the best is to recreate them, the code snipped used to create them initially is provided next. | ||||
|  | ||||
| ```golang | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"log" | ||||
| 	"net" | ||||
| 	"os" | ||||
|  | ||||
| 	"github.com/maxmind/mmdbwriter" | ||||
| 	"github.com/maxmind/mmdbwriter/inserter" | ||||
| 	"github.com/maxmind/mmdbwriter/mmdbtype" | ||||
| ) | ||||
|  | ||||
| const cdir = "81.2.69.142/32" | ||||
|  | ||||
| // Create new mmdb database fixtures in this directory. | ||||
| func main() { | ||||
| 	createCityDB("GeoLite2-City.mmdb", "DBIP-City-Lite") | ||||
| 	// Create unkwnon database type. | ||||
| 	createCityDB("GeoLite2-UnknownDbType.mmdb", "UnknownDbType") | ||||
| } | ||||
|  | ||||
| func createCityDB(dbName, dbType string) { | ||||
| 	// Load a database writer. | ||||
| 	writer, err := mmdbwriter.New(mmdbwriter.Options{DatabaseType: dbType}) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// Define and insert the new data. | ||||
| 	_, ip, err := net.ParseCIDR(cdir) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// TODO(snebel29): Find an alternative location in Europe Union. | ||||
| 	record := mmdbtype.Map{ | ||||
| 		"city": mmdbtype.Map{ | ||||
| 			"geoname_id": mmdbtype.Uint64(2653941), | ||||
| 			"names":      mmdbtype.Map{ | ||||
| 				"en": mmdbtype.String("Cambridge"), | ||||
| 				"es": mmdbtype.String("Cambridge"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		"continent": mmdbtype.Map{ | ||||
| 			"code":       mmdbtype.String("EU"), | ||||
| 			"geoname_id": mmdbtype.Uint64(6255148), | ||||
| 			"names":      mmdbtype.Map{ | ||||
| 				"en": mmdbtype.String("Europe"), | ||||
| 				"es": mmdbtype.String("Europa"), | ||||
| 			}, | ||||
| 		}, | ||||
| 		"country": mmdbtype.Map{ | ||||
| 			"iso_code":             mmdbtype.String("GB"), | ||||
| 			"geoname_id":           mmdbtype.Uint64(2635167), | ||||
| 			"names":                mmdbtype.Map{ | ||||
| 				"en": mmdbtype.String("United Kingdom"), | ||||
| 				"es": mmdbtype.String("Reino Unido"), | ||||
| 			}, | ||||
| 			"is_in_european_union": mmdbtype.Bool(true), | ||||
| 		}, | ||||
| 		"location": mmdbtype.Map{ | ||||
| 			"accuracy_radius": mmdbtype.Uint16(200), | ||||
| 			"latitude":        mmdbtype.Float64(52.2242), | ||||
| 			"longitude":       mmdbtype.Float64(0.1315), | ||||
| 			"metro_code":      mmdbtype.Uint64(0), | ||||
| 			"time_zone":       mmdbtype.String("Europe/London"), | ||||
| 		}, | ||||
| 		"postal": mmdbtype.Map{ | ||||
| 			"code": mmdbtype.String("CB4"), | ||||
| 		}, | ||||
| 		"registered_country": mmdbtype.Map{ | ||||
| 			"iso_code":             mmdbtype.String("GB"), | ||||
| 			"geoname_id":           mmdbtype.Uint64(2635167), | ||||
| 			"names":                mmdbtype.Map{"en": mmdbtype.String("United Kingdom")}, | ||||
| 			"is_in_european_union": mmdbtype.Bool(false), | ||||
| 		}, | ||||
| 		"subdivisions": mmdbtype.Slice{ | ||||
| 			mmdbtype.Map{ | ||||
| 				"iso_code":   mmdbtype.String("ENG"), | ||||
| 				"geoname_id": mmdbtype.Uint64(6269131), | ||||
| 				"names":      mmdbtype.Map{"en": mmdbtype.String("England")}, | ||||
| 			}, | ||||
| 			mmdbtype.Map{ | ||||
| 				"iso_code":   mmdbtype.String("CAM"), | ||||
| 				"geoname_id": mmdbtype.Uint64(2653940), | ||||
| 				"names":      mmdbtype.Map{"en": mmdbtype.String("Cambridgeshire")}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	if err := writer.InsertFunc(ip, inserter.TopLevelMergeWith(record)); err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	// Write the DB to the filesystem. | ||||
| 	fh, err := os.Create(dbName) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| 	_, err = writer.WriteTo(fh) | ||||
| 	if err != nil { | ||||
| 		log.Fatal(err) | ||||
| 	} | ||||
| } | ||||
| ``` | ||||
| @@ -72,12 +72,12 @@ func TestLabelFormat(t *testing.T) { | ||||
| 		{"plugin/LABEL", true}, | ||||
| 		{"p/LABEL", true}, | ||||
| 		{"plugin/L", true}, | ||||
| 		{"PLUGIN/LABEL/SUB-LABEL", true}, | ||||
| 		// fails | ||||
| 		{"LABEL", false}, | ||||
| 		{"plugin.LABEL", false}, | ||||
| 		{"/NO-PLUGIN-NOT-ACCEPTED", false}, | ||||
| 		{"ONLY-PLUGIN-NOT-ACCEPTED/", false}, | ||||
| 		{"PLUGIN/LABEL/SUB-LABEL", false}, | ||||
| 		{"/", false}, | ||||
| 		{"//", false}, | ||||
| 	} | ||||
|   | ||||
| @@ -56,17 +56,13 @@ type Provider interface { | ||||
| // Func is the type of function in the metadata, when called they return the value of the label. | ||||
| type Func func() string | ||||
|  | ||||
| // IsLabel checks that the provided name is a valid label name, i.e. two words separated by a slash. | ||||
| // IsLabel checks that the provided name is a valid label name, i.e. two or more words separated by a slash. | ||||
| func IsLabel(label string) bool { | ||||
| 	p := strings.Index(label, "/") | ||||
| 	if p <= 0 || p >= len(label)-1 { | ||||
| 		// cannot accept namespace empty nor label empty | ||||
| 		return false | ||||
| 	} | ||||
| 	if strings.LastIndex(label, "/") != p { | ||||
| 		// several slash in the Label | ||||
| 		return false | ||||
| 	} | ||||
| 	return true | ||||
|  | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user