Skip to content

Commit 5d9fc45

Browse files
authored
Event stats with GeoIP info (#160)
* add geoip lookup for new registrations - no stats printing yet * add mmdb paths to config file * stats for expired registrations * stats for closed connections including geoip * omit empty field in bettter event logging and generalize err handling * handle nil err pointer deref * missed negate on bool check * remove reg expire msg and add extra info to tun close msg * remove omit for default values on v6 and tun count * add libver and gen to closed proxy event log * disable tests requiring installed mmdb * fix linter issue
1 parent f356d6f commit 5d9fc45

File tree

13 files changed

+549
-133
lines changed

13 files changed

+549
-133
lines changed

.github/workflows/golang.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,4 @@ jobs:
6363
uses: golangci/golangci-lint-action@v3
6464
with:
6565
version: latest
66-
args: --disable structcheck,govet
66+
args: -v --disable structcheck,govet

application/config.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ detector_filter_list = [
9999
# Log level, one of the following: info, error, warn, debug, trace
100100
log_level = "error"
101101

102+
# MaxMind Database files - if empty then the Empty Geoip lookup will be used (effictvely disaling
103+
# lookup). The default path used by the maxmind geoiupdate tool is `/usr/local/share/GeoIP`.
104+
# repo - https://github.com/maxmind/geoipupdate/blob/main/pkg/geoipupdate/defaults_notwin.go#L15
105+
geoip_cc_db_path = ""
106+
geoip_asn_db_path = ""
107+
102108
### ZMQ sockets to connect to and subscribe
103109

104110
## Registration API

application/geoip/empty.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package geoip
2+
3+
import (
4+
"errors"
5+
"net"
6+
)
7+
8+
var (
9+
// ErrNoDatabasesProvided indicates that no databases were provided so we are going to return a
10+
// struct implementing the Database interface so stations that don't support geoip can still
11+
// operate normally.
12+
ErrNoDatabasesProvided = errors.New("no databases provided - using empty Geoip")
13+
)
14+
15+
// EmptyDatabase provides the Geoip functionality that we need using the MaxMind GeoIP service
16+
type EmptyDatabase struct {
17+
}
18+
19+
// ASN returns the Autonomous System Number (ASN) associated with the provided IP.
20+
// The Empty Database Returns 0.
21+
func (mmdb *EmptyDatabase) ASN(ip net.IP) (uint, error) {
22+
return 0, nil
23+
}
24+
25+
// CC returns the ISO country code associated with the provided IP. The Empty Database Returns an
26+
// empty string.
27+
func (mmdb *EmptyDatabase) CC(ip net.IP) (string, error) {
28+
return "", nil
29+
}

application/geoip/geoip.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package geoip
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net"
7+
8+
"github.com/oschwald/geoip2-golang"
9+
)
10+
11+
// Database provides the minimal useful interface for looking up relevant information so we
12+
// aren't tied tp the types / interface of a specific GeoIP library
13+
type Database interface {
14+
ASN(ip net.IP) (uint, error)
15+
CC(ip net.IP) (string, error)
16+
}
17+
18+
var (
19+
// ErrMissingDB indicates that one or more of the utility databases was not provided. The wrapped
20+
// error message should indicate which database was missing.
21+
ErrMissingDB = errors.New("missing one or more geoip DBs")
22+
)
23+
24+
// DBConfig contains options used for GeoIP lookup - including paths to database files
25+
type DBConfig struct {
26+
CCDBPath string `toml:"geoip_cc_db_path"`
27+
ASNDBPath string `toml:"geoip_asn_db_path"`
28+
}
29+
30+
// New returns a database given a config. If the provided config is nil, the Empty Database will
31+
// be returned.
32+
func New(conf *DBConfig) (Database, error) {
33+
34+
// If no config is provided or the config provided contains no DB paths return the empty db.
35+
if conf == nil {
36+
return &EmptyDatabase{}, fmt.Errorf("%w: no config", ErrMissingDB)
37+
} else if conf.ASNDBPath == "" && conf.CCDBPath == "" {
38+
return &EmptyDatabase{}, fmt.Errorf("%w: no asn or cc db files", ErrMissingDB)
39+
}
40+
41+
db := &maxMindDatabase{DBConfig: conf}
42+
43+
err := db.init()
44+
if errors.Is(err, ErrMissingDB) {
45+
return db, err
46+
} else if err != nil {
47+
return nil, err
48+
}
49+
50+
return db, nil
51+
}
52+
53+
// maxMindDatabase provides the GeoIP functionality that we need using the MaxMind GeoIP service
54+
type maxMindDatabase struct {
55+
*DBConfig
56+
57+
asnReader *geoip2.Reader
58+
ccReader *geoip2.Reader
59+
}
60+
61+
// ASN returns the Autonomous System Number (ASN) associated with the provided IP.
62+
func (mmdb *maxMindDatabase) init() error {
63+
var err error
64+
65+
if mmdb.ASNDBPath != "" {
66+
mmdb.asnReader, err = geoip2.Open(mmdb.DBConfig.ASNDBPath)
67+
if err != nil {
68+
return err
69+
}
70+
}
71+
72+
if mmdb.CCDBPath != "" {
73+
mmdb.ccReader, err = geoip2.Open(mmdb.DBConfig.CCDBPath)
74+
if err != nil {
75+
return err
76+
}
77+
}
78+
79+
if mmdb.ccReader == nil && mmdb.asnReader != nil {
80+
return fmt.Errorf("%w: no cc db file", ErrMissingDB)
81+
} else if mmdb.ccReader != nil && mmdb.asnReader == nil {
82+
return fmt.Errorf("%w: no asn db file", ErrMissingDB)
83+
}
84+
85+
return nil
86+
}
87+
88+
// ASN returns the Autonomous System Number (ASN) associated with the provided IP.
89+
func (mmdb *maxMindDatabase) ASN(ipAddress net.IP) (uint, error) {
90+
if mmdb == nil || mmdb.asnReader == nil {
91+
return 0, nil
92+
}
93+
94+
record, err := mmdb.asnReader.ASN(ipAddress)
95+
if err != nil {
96+
return 0, err
97+
}
98+
99+
return record.AutonomousSystemNumber, nil
100+
}
101+
102+
// CC returns the ISO country code associated with the provided IP.
103+
func (mmdb *maxMindDatabase) CC(ipAddress net.IP) (string, error) {
104+
if mmdb == nil || mmdb.ccReader == nil {
105+
return "", nil
106+
}
107+
108+
record, err := mmdb.ccReader.Country(ipAddress)
109+
if err != nil {
110+
return "", err
111+
}
112+
113+
return record.Country.IsoCode, nil
114+
}

application/geoip/geoip_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package geoip
2+
3+
import (
4+
"net"
5+
"path/filepath"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func DisabledTestGeoIPMaxMind(t *testing.T) {
12+
dbDir := "/opt/mmdb/"
13+
c := &DBConfig{
14+
ASNDBPath: filepath.Join(dbDir, "GeoLite2-ASN.mmdb"),
15+
CCDBPath: filepath.Join(dbDir, "GeoLite2-Country.mmdb"),
16+
}
17+
18+
db, err := New(c)
19+
require.Nil(t, err)
20+
21+
// If you are using strings that may be invalid, check that ip is not nil
22+
ip := net.ParseIP("81.2.69.142")
23+
cc, err := db.CC(ip)
24+
require.Nil(t, err)
25+
26+
asn, err := db.ASN(ip)
27+
require.Nil(t, err)
28+
29+
t.Log(cc)
30+
t.Log(asn)
31+
}
32+
33+
func DisabledTestGeoIPNoASN(t *testing.T) {
34+
dbDir := "/opt/mmdb/"
35+
c := &DBConfig{
36+
CCDBPath: filepath.Join(dbDir, "GeoLite2-Country.mmdb"),
37+
}
38+
39+
db, err := New(c)
40+
require.ErrorIs(t, err, ErrMissingDB)
41+
require.NotNil(t, db)
42+
43+
// If you are using strings that may be invalid, check that ip is not nil
44+
ip := net.ParseIP("81.2.69.142")
45+
cc, err := db.CC(ip)
46+
require.Nil(t, err)
47+
require.Equal(t, "GB", cc)
48+
49+
asn, err := db.ASN(ip)
50+
require.Nil(t, err)
51+
require.Equal(t, uint(0), asn)
52+
}
53+
54+
func DisabledTestGeoIPNoCC(t *testing.T) {
55+
dbDir := "/opt/mmdb/"
56+
c := &DBConfig{
57+
ASNDBPath: filepath.Join(dbDir, "GeoLite2-ASN.mmdb"),
58+
CCDBPath: "",
59+
}
60+
61+
db, err := New(c)
62+
require.ErrorIs(t, err, ErrMissingDB)
63+
require.NotNil(t, db)
64+
65+
// If you are using strings that may be invalid, check that ip is not nil
66+
ip := net.ParseIP("81.2.69.142")
67+
cc, err := db.CC(ip)
68+
require.Nil(t, err)
69+
require.Equal(t, "", cc)
70+
71+
asn, err := db.ASN(ip)
72+
require.Nil(t, err)
73+
require.Equal(t, uint(20712), asn)
74+
}
75+
76+
func TestGeoIPEmpty(t *testing.T) {
77+
c := &DBConfig{
78+
ASNDBPath: "",
79+
CCDBPath: "",
80+
}
81+
82+
_, err := New(nil)
83+
require.ErrorIs(t, err, ErrMissingDB)
84+
85+
db, err := New(c)
86+
require.ErrorIs(t, err, ErrMissingDB)
87+
require.NotNil(t, db)
88+
89+
// If you are using strings that may be invalid, check that ip is not nil
90+
ip := net.ParseIP("81.2.69.142")
91+
cc, err := db.CC(ip)
92+
require.Nil(t, err)
93+
require.Equal(t, "", cc)
94+
95+
asn, err := db.ASN(ip)
96+
require.Nil(t, err)
97+
require.Equal(t, uint(0), asn)
98+
}
99+
100+
func TestGeoIPNonMissingError(t *testing.T) {
101+
c := &DBConfig{
102+
ASNDBPath: "",
103+
CCDBPath: "abcdef",
104+
}
105+
106+
db, err := New(c)
107+
require.NotErrorIs(t, err, ErrMissingDB)
108+
require.Nil(t, db)
109+
}

application/lib/config_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,8 @@ func TestConfigParse(t *testing.T) {
2020
lc := c.LivenessConfig()
2121
require.NotEqual(t, "", lc.CacheDuration)
2222
require.NotEqual(t, "", lc.CacheDurationNonLive)
23+
24+
// var db geoip.Database
25+
require.NotNil(t, c.RegConfig.DBConfig)
26+
// require.IsType(t, db, c.RegConfig.DBConfig)
2327
}

0 commit comments

Comments
 (0)