diff --git a/.gitignore b/.gitignore index aadc890f..56ee5625 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,5 @@ tests/docker_logs .nginx-validate.* /docs/ /dev/bootstrap/GeoLite2-ASN/ -/dev/bootstrap/GeoLite2-City/ /dev/bootstrap/monitoring/ /scripts/ diff --git a/README.md b/README.md index 3858dede..5737f158 100644 --- a/README.md +++ b/README.md @@ -116,8 +116,8 @@ cp api/.env.sample api/.env cp proxy/.env.sample proxy/.env cp dnscheck/.env.sample dnscheck/.env -# 2. MaxMind GeoLite2 databases (mounted by the proxy and dnscheck) -# Place them under dev/bootstrap/GeoLite2-ASN/ and dev/bootstrap/GeoLite2-City/ +# 2. MaxMind GeoLite2-ASN database (mounted by the proxy and dnscheck) +# Place GeoLite2-ASN.mmdb under dev/bootstrap/GeoLite2-ASN/ ``` Then: diff --git a/api/go.mod b/api/go.mod index 357b23e7..f7998ef7 100644 --- a/api/go.mod +++ b/api/go.mod @@ -41,7 +41,6 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect @@ -86,7 +85,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect @@ -106,6 +104,7 @@ require ( go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/trace v1.41.0 // indirect go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/tools v0.44.0 // indirect ) @@ -123,7 +122,7 @@ require ( github.com/montanaflynn/stats v0.7.1 // indirect github.com/pkg/errors v0.9.1 github.com/sqids/sqids-go v0.4.1 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.57.0 github.com/valyala/tcplisten v1.0.0 // indirect @@ -132,7 +131,7 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect golang.org/x/crypto v0.52.0 - golang.org/x/sync v0.20.0 - golang.org/x/sys v0.45.0 // indirect + golang.org/x/sync v0.22.0 + golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.37.0 ) diff --git a/api/go.sum b/api/go.sum index f584ab04..4706b1ef 100644 --- a/api/go.sum +++ b/api/go.sum @@ -196,7 +196,6 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= @@ -235,8 +234,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw= github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM= github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg= @@ -294,6 +293,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= @@ -314,8 +315,8 @@ golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -330,8 +331,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/blocklists/go.mod b/blocklists/go.mod index f7fb0033..063299a6 100644 --- a/blocklists/go.mod +++ b/blocklists/go.mod @@ -31,7 +31,6 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/montanaflynn/stats v0.7.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect @@ -42,20 +41,19 @@ require ( github.com/yuin/gopher-lua v1.1.1 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.52.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect golang.org/x/text v0.37.0 // indirect google.golang.org/protobuf v1.36.8 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/robfig/cron/v3 v3.0.1 github.com/rs/zerolog v1.34.0 github.com/spf13/cast v1.7.0 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 ) diff --git a/blocklists/go.sum b/blocklists/go.sum index 2dc245b5..861cbefa 100644 --- a/blocklists/go.sum +++ b/blocklists/go.sum @@ -100,8 +100,6 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -121,8 +119,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -150,6 +148,8 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= @@ -160,8 +160,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -170,8 +170,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -189,5 +189,3 @@ google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXn gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/compose.dnscheck.yml b/compose.dnscheck.yml index c1541053..391c3423 100644 --- a/compose.dnscheck.yml +++ b/compose.dnscheck.yml @@ -16,7 +16,6 @@ services: volumes: - ./dnscheck:/app - ./dev/bootstrap/GeoLite2-ASN/:/opt/dnscheck/GeoIP - - ./dev/bootstrap/GeoLite2-City/:/opt/dnscheck/GeoIPCity # - ./dev/certs:/certs env_file: - ./dnscheck/.env diff --git a/dnscheck/api/check.go b/dnscheck/api/check.go index 17ed772c..f2acdbd2 100644 --- a/dnscheck/api/check.go +++ b/dnscheck/api/check.go @@ -49,10 +49,7 @@ func (s *APIServer) DnsCheck() fiber.Handler { return HandleError(c, err, ErrFailedToUnmarshalRecord) } - return c.Status(200).JSON(dns.DNSCheckResponse{ - Status: dnsRecord.Status, - ProfileId: dnsRecord.ProfileId, - }) + return c.Status(200).JSON(dns.DNSCheckResponse(dnsRecord)) } return handler } diff --git a/dnscheck/cache/cache.go b/dnscheck/cache/cache.go index 3e577c1c..6ef03af7 100644 --- a/dnscheck/cache/cache.go +++ b/dnscheck/cache/cache.go @@ -16,8 +16,7 @@ type Cache interface { // New creates a new Cache instance whose entries expire after ttl. func New(cacheType string, ttl time.Duration) (Cache, error) { - switch cacheType { - case CacheTypeBigCache: + if cacheType == CacheTypeBigCache { return NewBigcache(ttl) } return nil, errors.New("unknown cache type") diff --git a/dnscheck/config/config.go b/dnscheck/config/config.go index 04f7cb85..82490f06 100644 --- a/dnscheck/config/config.go +++ b/dnscheck/config/config.go @@ -83,9 +83,14 @@ type CacheConfig struct { HMACKey string } +// DefaultGeoIPDBReload is how often the ASN database file is checked for a +// refreshed build when GEOIP_DB_RELOAD is unset. +const DefaultGeoIPDBReload = 15 * time.Minute + // GeoLookupConfig represents access to the MaxMind GeoIP ASN database type GeoLookupConfig struct { - DBASNFile string + DBASNFile string + ReloadEvery time.Duration } // IsValid check whether config section is valid @@ -123,8 +128,18 @@ func New() (*Config, error) { return nil, errors.New("CACHE_HMAC_KEY environment variable is required") } + geoReload := DefaultGeoIPDBReload + if raw := os.Getenv("GEOIP_DB_RELOAD"); raw != "" { + parsed, err := time.ParseDuration(raw) + if err != nil || parsed <= 0 { + return nil, fmt.Errorf("GEOIP_DB_RELOAD must be a positive duration, got %q", raw) + } + geoReload = parsed + } + geoLookup := &GeoLookupConfig{ - DBASNFile: os.Getenv("GEOIP_DB_ASN_FILE"), + DBASNFile: os.Getenv("GEOIP_DB_ASN_FILE"), + ReloadEvery: geoReload, } if err := geoLookup.IsValid(); err != nil { return nil, err diff --git a/dnscheck/config/config_test.go b/dnscheck/config/config_test.go index 57497b48..8f100d53 100644 --- a/dnscheck/config/config_test.go +++ b/dnscheck/config/config_test.go @@ -129,3 +129,35 @@ func TestNewCacheTTLDefaultsAndValidates(t *testing.T) { } } } + +// specRef: dnscheck-behaviour.md #S8 +func TestNewGeoIPReloadDefaultsAndValidates(t *testing.T) { + t.Setenv("CACHE_HMAC_KEY", "test-key") + t.Setenv("GEOIP_DB_ASN_FILE", "/opt/dnscheck/GeoLite2-ASN.mmdb") + t.Setenv("DNS_AUTH_SERVER_IP_RANGE", "10.5.0.0/16") + + t.Setenv("GEOIP_DB_RELOAD", "") + cfg, err := New() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.GeoLookupConfig.ReloadEvery != DefaultGeoIPDBReload { + t.Errorf("ReloadEvery = %v, want default %v", cfg.GeoLookupConfig.ReloadEvery, DefaultGeoIPDBReload) + } + + t.Setenv("GEOIP_DB_RELOAD", "1h") + cfg, err = New() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.GeoLookupConfig.ReloadEvery != time.Hour { + t.Errorf("ReloadEvery = %v, want 1h", cfg.GeoLookupConfig.ReloadEvery) + } + + for _, bad := range []string{"daily", "-15m", "0"} { + t.Setenv("GEOIP_DB_RELOAD", bad) + if _, err := New(); err == nil { + t.Errorf("expected an error for GEOIP_DB_RELOAD=%q", bad) + } + } +} diff --git a/dnscheck/dns/handler.go b/dnscheck/dns/handler.go index e6921fb3..a2d65dda 100644 --- a/dnscheck/dns/handler.go +++ b/dnscheck/dns/handler.go @@ -171,7 +171,9 @@ func (h *Handler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) { default: msg.Ns = h.createSOA() } - w.WriteMsg(&msg) + if err := w.WriteMsg(&msg); err != nil { + log.Error().Err(err).Msg("Failed to write DNS response") + } } func (h *Handler) extractConfiguredProfileId(r *dns.Msg) (profileId string) { @@ -203,7 +205,7 @@ func (h *Handler) createSOA() []dns.RR { Ttl: TTL}, Ns: "ns1." + dom, Mbox: "hostmaster." + dom, - Serial: uint32(time.Now().Truncate(time.Hour).Unix()), + Serial: soaSerial(time.Now()), Refresh: 28800, Retry: 7200, Expire: 604800, @@ -212,6 +214,16 @@ func (h *Handler) createSOA() []dns.RR { } } +// soaSerial is the hour-truncated Unix time. RFC 1035 ยง3.3.13 makes SERIAL a +// 32-bit unsigned value, which Unix seconds exceed only in 2106. +func soaSerial(now time.Time) uint32 { + s := now.Truncate(time.Hour).Unix() + if s < 0 || s > 1<<32-1 { + return 0 + } + return uint32(s) +} + // clientIP returns the transport-level source address of the query. It is read // straight from the socket address and never resolved. func clientIP(addr net.Addr) (net.IP, error) { diff --git a/dnscheck/dns/server.go b/dnscheck/dns/server.go index f361c1b8..62fbe22a 100644 --- a/dnscheck/dns/server.go +++ b/dnscheck/dns/server.go @@ -1,12 +1,14 @@ package dns import ( + "context" "fmt" "github.com/dnscheck/cache" "github.com/dnscheck/config" "github.com/dnscheck/internal/maxmind" "github.com/miekg/dns" + "github.com/rs/zerolog/log" ) // GeoLookuper resolves a client IP to its ASN record. @@ -37,6 +39,13 @@ func New(config *config.Config, cache cache.Cache) (*DNSServer, error) { return nil, fmt.Errorf("geoip: %w", err) } srv.GeoLookup = geoLookup + // The file is refreshed on disk by geoipupdate; follow it without a restart. + go geoLookup.Watch(context.Background(), config.GeoLookupConfig.ReloadEvery) + log.Info(). + Str("path", config.GeoLookupConfig.DBASNFile). + Time("build_time", geoLookup.Stats().BuildTime). + Dur("reload_every", config.GeoLookupConfig.ReloadEvery). + Msg("GeoIP ASN database loaded") // DNS srv.DNSTCP = &dns.Server{Addr: ":53", Net: "tcp"} diff --git a/dnscheck/go.mod b/dnscheck/go.mod index b4dceed5..24151c9a 100644 --- a/dnscheck/go.mod +++ b/dnscheck/go.mod @@ -6,11 +6,14 @@ require ( github.com/go-playground/validator/v10 v10.25.0 github.com/gofiber/fiber/v2 v2.52.12 github.com/miekg/dns v1.1.62 - github.com/oschwald/geoip2-golang v1.11.0 github.com/rs/zerolog v1.34.0 ) -require golang.org/x/crypto v0.52.0 // indirect +require ( + github.com/oschwald/geoip2-golang/v2 v2.4.0 // indirect + github.com/oschwald/maxminddb-golang/v2 v2.6.0 // indirect + golang.org/x/crypto v0.55.0 // indirect +) require ( github.com/allegro/bigcache/v3 v3.1.0 @@ -24,19 +27,18 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/oschwald/maxminddb-golang v1.13.0 // indirect github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/tinylib/msgp v1.2.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.54.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/mod v0.39.0 // indirect + golang.org/x/net v0.58.0 // indirect + golang.org/x/sync v0.22.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/tools v0.49.0 // indirect ) require github.com/ivpn/dns/libs v0.0.0 diff --git a/dnscheck/go.sum b/dnscheck/go.sum index c4f8871b..bd04af32 100644 --- a/dnscheck/go.sum +++ b/dnscheck/go.sum @@ -3,8 +3,6 @@ github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpx github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -37,22 +35,20 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= -github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w= -github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= -github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= -github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= +github.com/oschwald/geoip2-golang/v2 v2.4.0 h1:JdVymxpwFf7o+3o53Sw2gCYBX8maA5DWxcgzNb14yJU= +github.com/oschwald/geoip2-golang/v2 v2.4.0/go.mod h1:VJW7lAC5Dw4WH42mjhUFkxf7+v3K1YOafLD8iBiszsc= +github.com/oschwald/maxminddb-golang/v2 v2.6.0 h1:pRlHCdJmc+4uxMOSthmKDt5HOw3JTX8TJZlhyP5ew0w= +github.com/oschwald/maxminddb-golang/v2 v2.6.0/go.mod h1:sjqpB3z2BZrMduDp9TAUTCkZDoT3nDhixUc4Dge2qRQ= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= @@ -61,22 +57,22 @@ github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1S github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= -golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= diff --git a/dnscheck/internal/maxmind/maxmind.go b/dnscheck/internal/maxmind/maxmind.go index ad2b0fb1..e7787f0e 100644 --- a/dnscheck/internal/maxmind/maxmind.go +++ b/dnscheck/internal/maxmind/maxmind.go @@ -1,60 +1,71 @@ package maxmind import ( + "context" "fmt" - "net" + "net/netip" + "time" - "github.com/oschwald/geoip2-golang" + "github.com/ivpn/dns/libs/geoipdb" ) // GeoLookupManager answers ASN lookups from a MaxMind database that is opened -// once and shared by every request; geoip2.Reader is safe for concurrent use. +// once, shared by every request, and reopened in place when the file on disk +// is refreshed. type GeoLookupManager struct { - asnDB *geoip2.Reader + db *geoipdb.Reader } // NewGeoLookupManager opens the ASN database and fails if the file is missing, // unreadable or not an ASN-capable database type. func NewGeoLookupManager(dbASNFile string) (*GeoLookupManager, error) { - asnDB, err := geoip2.Open(dbASNFile) + db, err := geoipdb.Open(dbASNFile) if err != nil { return nil, fmt.Errorf("cannot open geoip ASN database %q: %w", dbASNFile, err) } + return &GeoLookupManager{db: db}, nil +} - // geoip2 only reports a database/method mismatch at lookup time, so probe - // once here rather than on every request. - if _, err := asnDB.ASN(net.IPv4(192, 0, 2, 1)); err != nil { - asnDB.Close() - return nil, fmt.Errorf("geoip database %q does not support ASN lookups: %w", dbASNFile, err) - } +// Reload checks the file now and reopens it if it changed; a broken replacement +// is rejected and the loaded database keeps serving. Production relies on +// Watch, which runs the same check on a timer; Reload is the synchronous +// entry point for tests and for a manual "reload now" trigger. +func (g *GeoLookupManager) Reload() (bool, error) { + return g.db.Reload() +} - return &GeoLookupManager{asnDB: asnDB}, nil +// Watch reloads the database on a timer until ctx is cancelled. +func (g *GeoLookupManager) Watch(ctx context.Context, every time.Duration) { + g.db.Watch(ctx, every) +} + +// Stats reports the build time and reload counters of the loaded database. +func (g *GeoLookupManager) Stats() geoipdb.Stats { + return g.db.Stats() } // Close releases the underlying database. func (g *GeoLookupManager) Close() error { - return g.asnDB.Close() + return g.db.Close() } // GetGeoLookup returns the ASN record for ip. An address that is not in the // database yields an empty record and no error. func (g *GeoLookupManager) GetGeoLookup(ip string) (*GeoLookup, error) { - ipnet := net.ParseIP(ip) - if ipnet == nil { + addr, err := netip.ParseAddr(ip) + if err != nil { return nil, fmt.Errorf("invalid IP address %q", ip) } + addr = addr.Unmap() - asn, err := g.asnDB.ASN(ipnet) + rec := &GeoLookup{IPAddress: addr.String()} + asn, err := g.db.ASN(addr) if err != nil { return nil, fmt.Errorf("cannot get ASN: %w", err) } - if asn == nil { - asn = &geoip2.ASN{} + if asn != nil { + rec.ASN = asn.AutonomousSystemNumber + rec.ASNOrganization = asn.AutonomousSystemOrganization } - - return &GeoLookup{ - IPAddress: ipnet.String(), - ASN: asn.AutonomousSystemNumber, - ASNOrganization: asn.AutonomousSystemOrganization, - }, nil + return rec, nil } diff --git a/dnscheck/internal/maxmind/maxmind_test.go b/dnscheck/internal/maxmind/maxmind_test.go index ac53c442..42a66f75 100644 --- a/dnscheck/internal/maxmind/maxmind_test.go +++ b/dnscheck/internal/maxmind/maxmind_test.go @@ -4,13 +4,98 @@ import ( "os" "path/filepath" "testing" + "time" ) const ( - asnFixture = "testdata/GeoLite2-ASN.mmdb" - cityFixture = "testdata/GeoLite2-City.mmdb" + asnFixture = "testdata/GeoLite2-ASN.mmdb" // build 2026-09-04 + newerFixture = "testdata/GeoLite2-ASN.newer.mmdb" // same networks, build 2026-09-18 + cityFixture = "testdata/GeoLite2-City.mmdb" ) +// installFixture mirrors geoipupdate: write a temporary file, then rename it +// over the target. +func installFixture(t *testing.T, src, dst string, mtime time.Time) { + t.Helper() + data, err := os.ReadFile(src) + if err != nil { + t.Fatal(err) + } + tmp := dst + ".temporary" + if err := os.WriteFile(tmp, data, 0o644); err != nil { + t.Fatal(err) + } + if err := os.Chtimes(tmp, mtime, mtime); err != nil { + t.Fatal(err) + } + if err := os.Rename(tmp, dst); err != nil { + t.Fatal(err) + } +} + +// A database refreshed on disk is served after the next reload; the process +// never has to restart. +// +// specRef: dnscheck-behaviour.md #S6 +func TestGeoLookupManagerReloadsReplacedDatabase(t *testing.T) { + path := filepath.Join(t.TempDir(), "GeoLite2-ASN.mmdb") + installFixture(t, asnFixture, path, time.Now().Add(-time.Hour)) + g, err := NewGeoLookupManager(path) + if err != nil { + t.Fatalf("open: %v", err) + } + defer g.Close() + if got := g.Stats().BuildTime.UTC().Format("2006-01-02"); got != "2026-09-04" { + t.Fatalf("initial build date %s, want 2026-09-04", got) + } + + installFixture(t, newerFixture, path, time.Now()) + changed, err := g.Reload() + if err != nil || !changed { + t.Fatalf("reload: changed=%v err=%v", changed, err) + } + if got := g.Stats().BuildTime.UTC().Format("2006-01-02"); got != "2026-09-18" { + t.Errorf("build date after reload %s, want 2026-09-18", got) + } + got, err := g.GetGeoLookup("8.8.8.8") + if err != nil || got.ASN != 15169 { + t.Errorf("lookup after reload: %+v err=%v", got, err) + } +} + +// A broken replacement (corrupt file or City edition on the ASN path) is +// rejected and the previously loaded database keeps answering. +// +// specRef: dnscheck-behaviour.md #S7 +func TestGeoLookupManagerKeepsOldDatabaseWhenReplacementIsBad(t *testing.T) { + path := filepath.Join(t.TempDir(), "GeoLite2-ASN.mmdb") + installFixture(t, asnFixture, path, time.Now().Add(-time.Hour)) + g, err := NewGeoLookupManager(path) + if err != nil { + t.Fatalf("open: %v", err) + } + defer g.Close() + + installFixture(t, cityFixture, path, time.Now()) + if _, err := g.Reload(); err == nil { + t.Fatal("expected an error for a City database on the ASN path") + } + if err := os.WriteFile(path, []byte("not an mmdb"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := g.Reload(); err == nil { + t.Fatal("expected an error for a corrupt replacement") + } + + if s := g.Stats(); s.Failures != 2 || s.BuildTime.UTC().Format("2006-01-02") != "2026-09-04" { + t.Errorf("stats after failed reloads: %+v", s) + } + got, err := g.GetGeoLookup("8.8.8.8") + if err != nil || got.ASN != 15169 { + t.Errorf("old database not served after failed reloads: %+v err=%v", got, err) + } +} + // specRef: dnscheck-behaviour.md #S2 func TestNewGeoLookupManagerRejectsMissingFile(t *testing.T) { _, err := NewGeoLookupManager(filepath.Join(t.TempDir(), "missing.mmdb")) diff --git a/tests/bootstrap/geolite/GeoLite2-City.mmdb b/dnscheck/internal/maxmind/testdata/GeoLite2-ASN.newer.mmdb similarity index 92% rename from tests/bootstrap/geolite/GeoLite2-City.mmdb rename to dnscheck/internal/maxmind/testdata/GeoLite2-ASN.newer.mmdb index 5ca39518..06f44556 100644 Binary files a/tests/bootstrap/geolite/GeoLite2-City.mmdb and b/dnscheck/internal/maxmind/testdata/GeoLite2-ASN.newer.mmdb differ diff --git a/libs/dnsstamps/dnsstamps.go b/libs/dnsstamps/dnsstamps.go index 9ba29ffd..fb7bd24e 100644 --- a/libs/dnsstamps/dnsstamps.go +++ b/libs/dnsstamps/dnsstamps.go @@ -99,15 +99,16 @@ func NewServerStampFromString(stampStr string) (ServerStamp, error) { return ServerStamp{}, errors.New("stamp is too short") } - if bin[0] == uint8(StampProtoTypePlain) { + switch bin[0] { + case uint8(StampProtoTypePlain): return newPlainServerStamp(bin) - } else if bin[0] == uint8(StampProtoTypeDNSCrypt) { + case uint8(StampProtoTypeDNSCrypt): return newDNSCryptServerStamp(bin) - } else if bin[0] == uint8(StampProtoTypeDoH) { + case uint8(StampProtoTypeDoH): return newDoHServerStamp(bin) - } else if bin[0] == uint8(StampProtoTypeTLS) { + case uint8(StampProtoTypeTLS): return newDoTOrDoQServerStamp(bin, StampProtoTypeTLS, defaultDoTPort) - } else if bin[0] == uint8(StampProtoTypeDoQ) { + case uint8(StampProtoTypeDoQ): return newDoTOrDoQServerStamp(bin, StampProtoTypeDoQ, defaultDoQPort) } return ServerStamp{}, errors.New("unsupported stamp version or protocol") @@ -317,6 +318,18 @@ func newPlainServerStamp(bin []byte) (ServerStamp, error) { return stamp, nil } +// lenByte is the one-byte length prefix the stamp format uses for every +// variable field; a value longer than 255 bytes cannot be encoded and is +// truncated to the maximum rather than wrapped. Written as an explicit +// comparison because gosec (G115) only credits a bounds check in that form. +func lenByte[T ~string | ~[]byte](v T) uint8 { + n := len(v) + if n > 255 { + n = 255 + } + return uint8(n) +} + func (stamp *ServerStamp) dnsCryptString() string { bin := make([]uint8, 9) bin[0] = uint8(StampProtoTypeDNSCrypt) @@ -326,13 +339,13 @@ func (stamp *ServerStamp) dnsCryptString() string { if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(defaultDNSCryptPort)) { serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(defaultDNSCryptPort))] } - bin = append(bin, uint8(len(serverAddrStr))) + bin = append(bin, lenByte(serverAddrStr)) bin = append(bin, []uint8(serverAddrStr)...) - bin = append(bin, uint8(len(stamp.ServerPk))) + bin = append(bin, lenByte(stamp.ServerPk)) bin = append(bin, stamp.ServerPk...) - bin = append(bin, uint8(len(stamp.ProviderName))) + bin = append(bin, lenByte(stamp.ProviderName)) bin = append(bin, []uint8(stamp.ProviderName)...) str := base64.RawURLEncoding.EncodeToString(bin) @@ -349,7 +362,7 @@ func (stamp *ServerStamp) dohString() string { if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(defaultDoHPort)) { serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(defaultDoHPort))] } - bin = append(bin, uint8(len(serverAddrStr))) + bin = append(bin, lenByte(serverAddrStr)) bin = append(bin, []uint8(serverAddrStr)...) if len(stamp.Hashes) == 0 { @@ -357,19 +370,21 @@ func (stamp *ServerStamp) dohString() string { } else { last := len(stamp.Hashes) - 1 for i, hash := range stamp.Hashes { - vlen := len(hash) + // Low 7 bits carry the length (hashes are 32-byte SHA-256 digests), + // the high bit flags that another hash follows. + vlen := lenByte(hash) & 0x7f if i < last { vlen |= 0x80 } - bin = append(bin, uint8(vlen)) + bin = append(bin, vlen) bin = append(bin, hash...) } } - bin = append(bin, uint8(len(stamp.ProviderName))) + bin = append(bin, lenByte(stamp.ProviderName)) bin = append(bin, []uint8(stamp.ProviderName)...) - bin = append(bin, uint8(len(stamp.Path))) + bin = append(bin, lenByte(stamp.Path)) bin = append(bin, []uint8(stamp.Path)...) str := base64.RawURLEncoding.EncodeToString(bin) @@ -385,7 +400,7 @@ func (stamp *ServerStamp) dotOrDoqString(stampType StampProtoType, defaultPort u if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(int(defaultPort))) { serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(int(defaultPort)))] } - bin = append(bin, uint8(len(serverAddrStr))) + bin = append(bin, lenByte(serverAddrStr)) bin = append(bin, []uint8(serverAddrStr)...) if len(stamp.Hashes) == 0 { @@ -393,16 +408,18 @@ func (stamp *ServerStamp) dotOrDoqString(stampType StampProtoType, defaultPort u } else { last := len(stamp.Hashes) - 1 for i, hash := range stamp.Hashes { - vlen := len(hash) + // Low 7 bits carry the length (hashes are 32-byte SHA-256 digests), + // the high bit flags that another hash follows. + vlen := lenByte(hash) & 0x7f if i < last { vlen |= 0x80 } - bin = append(bin, uint8(vlen)) + bin = append(bin, vlen) bin = append(bin, hash...) } } - bin = append(bin, uint8(len(stamp.ProviderName))) + bin = append(bin, lenByte(stamp.ProviderName)) bin = append(bin, []uint8(stamp.ProviderName)...) str := base64.RawURLEncoding.EncodeToString(bin) @@ -418,7 +435,7 @@ func (stamp *ServerStamp) plainString() string { if strings.HasSuffix(serverAddrStr, ":"+strconv.Itoa(defaultPlainPort)) { serverAddrStr = serverAddrStr[:len(serverAddrStr)-1-len(strconv.Itoa(defaultPlainPort))] } - bin = append(bin, uint8(len(serverAddrStr))) + bin = append(bin, lenByte(serverAddrStr)) bin = append(bin, []uint8(serverAddrStr)...) str := base64.RawURLEncoding.EncodeToString(bin) diff --git a/libs/geoipdb/reader.go b/libs/geoipdb/reader.go new file mode 100644 index 00000000..f8de0a41 --- /dev/null +++ b/libs/geoipdb/reader.go @@ -0,0 +1,211 @@ +// Package geoipdb serves ASN lookups from a MaxMind database file and reopens +// the file when it is replaced on disk, so a refreshed GeoLite2 build is used +// without restarting the process. +package geoipdb + +import ( + "context" + "errors" + "fmt" + "math" + "net/netip" + "os" + "sync" + "sync/atomic" + "time" + + "github.com/oschwald/geoip2-golang/v2" + "github.com/rs/zerolog/log" +) + +// DefaultReloadInterval is how often Watch checks the file when the caller +// passes a non-positive interval. +const DefaultReloadInterval = 15 * time.Minute + +// Stats describes the database currently being served. +type Stats struct { + BuildTime time.Time // build_epoch recorded in the database metadata + LoadedAt time.Time // when the current file was opened + Reloads uint64 // successful swaps since Open + Failures uint64 // replacements rejected since Open + LastError string // most recent reload error; empty after a success +} + +// fileSig is what Reload compares to decide whether the file was replaced. +// geoipupdate writes a temporary file and renames it over the target, which +// always changes the modification time. +type fileSig struct { + size int64 + modTime time.Time +} + +func (s fileSig) equal(o fileSig) bool { + return s.size == o.size && s.modTime.Equal(o.modTime) +} + +// Reader is safe for concurrent use and lock-free on the lookup path: the +// current database is an atomic pointer, and a swap simply publishes a new +// one. The previous database is never closed explicitly; lookups still +// holding it finish and the garbage collector reclaims it. +// +// The file is read into memory rather than mmapped. An mmap follows whatever +// a writer later does to the same inode, so an in-place overwrite would +// corrupt the database mid-lookup; a private copy is immune to that, and +// GeoLite2-ASN is small enough (~11 MB) for the copy to be free. It also +// means dropping a database releases nothing but heap. +type Reader struct { + path string + cur atomic.Pointer[geoip2.Reader] + + mu sync.Mutex // guards sig and stats; serialises Reload callers + sig fileSig + stats Stats +} + +// Open opens an ASN-capable database. A missing, unreadable, corrupt or +// wrong-edition file is an error. +func Open(path string) (*Reader, error) { + if path == "" { + return nil, errors.New("geoip database path is required") + } + db, sig, err := openFile(path) + if err != nil { + return nil, err + } + r := &Reader{ + path: path, + sig: sig, + stats: Stats{BuildTime: buildTime(db), LoadedAt: time.Now()}, + } + r.cur.Store(db) + return r, nil +} + +func openFile(path string) (*geoip2.Reader, fileSig, error) { + info, err := os.Stat(path) + if err != nil { + return nil, fileSig{}, err + } + if !info.Mode().IsRegular() { + return nil, fileSig{}, fmt.Errorf("%s is not a regular file", path) + } + data, err := os.ReadFile(path) + if err != nil { + return nil, fileSig{}, err + } + db, err := geoip2.OpenBytes(data) + if err != nil { + return nil, fileSig{}, fmt.Errorf("open %s: %w", path, err) + } + // geoip2 reports an edition/method mismatch only at lookup time, so probe + // once here rather than on the first real query. + if _, err := db.ASN(netip.MustParseAddr("192.0.2.1")); err != nil { + return nil, fileSig{}, fmt.Errorf("%s does not support ASN lookups: %w", path, err) + } + return db, fileSig{size: info.Size(), modTime: info.ModTime()}, nil +} + +func buildTime(db *geoip2.Reader) time.Time { + epoch := db.Metadata().BuildEpoch + if epoch > math.MaxInt64 { + epoch = math.MaxInt64 + } + return time.Unix(int64(epoch), 0).UTC() +} + +// Path returns the file the reader watches. +func (r *Reader) Path() string { + return r.path +} + +// ASN looks up ip in the database currently loaded. An address outside the +// database yields a record with HasData() false and no error. IPv4-mapped +// IPv6 addresses are unmapped first so both forms of an IPv4 address hit the +// same entry. +func (r *Reader) ASN(ip netip.Addr) (*geoip2.ASN, error) { + db := r.cur.Load() + if db == nil { + return nil, errors.New("geoip database is closed") + } + return db.ASN(ip.Unmap()) +} + +// Stats returns a snapshot of the reader state. +func (r *Reader) Stats() Stats { + r.mu.Lock() + defer r.mu.Unlock() + return r.stats +} + +// Reload reopens the file if its size or modification time changed since it +// was last loaded. It reports whether a new database is now being served. A +// replacement that cannot be opened or is not an ASN database is rejected and +// the previous database keeps serving. +func (r *Reader) Reload() (bool, error) { + r.mu.Lock() + defer r.mu.Unlock() + + if r.cur.Load() == nil { + return false, errors.New("geoip database is closed") + } + + info, err := os.Stat(r.path) + if err != nil { + return false, r.failLocked(err) + } + if r.sig.equal(fileSig{size: info.Size(), modTime: info.ModTime()}) { + return false, nil + } + + db, sig, err := openFile(r.path) + if err != nil { + return false, r.failLocked(err) + } + + r.cur.Store(db) + r.sig = sig + r.stats.BuildTime = buildTime(db) + r.stats.LoadedAt = time.Now() + r.stats.Reloads++ + r.stats.LastError = "" + + log.Info().Str("path", r.path).Time("build_time", r.stats.BuildTime).Msg("GeoIP database reloaded") + return true, nil +} + +// failLocked records a rejected replacement; r.mu must be held. +func (r *Reader) failLocked(err error) error { + r.stats.Failures++ + r.stats.LastError = err.Error() + log.Error().Err(err).Str("path", r.path).Msg("GeoIP database reload rejected; previous database kept") + return err +} + +// Watch calls Reload every interval until ctx is cancelled. +func (r *Reader) Watch(ctx context.Context, every time.Duration) { + if r == nil { + return + } + if every <= 0 { + every = DefaultReloadInterval + } + ticker := time.NewTicker(every) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + _, _ = r.Reload() + } + } +} + +// Close stops serving lookups; later calls to ASN return an error. The +// in-memory database holds no file mapping, so there is nothing else to +// release, and it is not closed explicitly because a lookup may still be +// using it. +func (r *Reader) Close() error { + r.cur.Store(nil) + return nil +} diff --git a/libs/geoipdb/reader_test.go b/libs/geoipdb/reader_test.go new file mode 100644 index 00000000..28a03994 --- /dev/null +++ b/libs/geoipdb/reader_test.go @@ -0,0 +1,298 @@ +package geoipdb + +import ( + "context" + "net/netip" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/oschwald/geoip2-golang/v2" +) + +const ( + asnFixture = "testdata/GeoLite2-ASN.mmdb" // build 2026-09-04 + newerFixture = "testdata/GeoLite2-ASN.newer.mmdb" // same networks, build 2026-09-18 + cityFixture = "testdata/GeoLite2-City.mmdb" +) + +// installFixture copies src to dst with a stable mtime so a later replacement +// always changes the on-disk signature the reloader compares. +func installFixture(t *testing.T, src, dst string, mtime time.Time) { + t.Helper() + data, err := os.ReadFile(src) + if err != nil { + t.Fatalf("read %s: %v", src, err) + } + // Same write-then-rename sequence geoipupdate uses. + tmp := dst + ".temporary" + if err := os.WriteFile(tmp, data, 0o644); err != nil { + t.Fatal(err) + } + if err := os.Chtimes(tmp, mtime, mtime); err != nil { + t.Fatal(err) + } + if err := os.Rename(tmp, dst); err != nil { + t.Fatal(err) + } +} + +func openTemp(t *testing.T) (*Reader, string) { + t.Helper() + path := filepath.Join(t.TempDir(), "GeoLite2-ASN.mmdb") + installFixture(t, asnFixture, path, time.Now().Add(-time.Hour)) + r, err := Open(path) + if err != nil { + t.Fatalf("open: %v", err) + } + t.Cleanup(func() { _ = r.Close() }) + return r, path +} + +func mustASN(t *testing.T, r *Reader, ip string) *geoip2.ASN { + t.Helper() + rec, err := r.ASN(netip.MustParseAddr(ip)) + if err != nil { + t.Fatalf("lookup %s: %v", ip, err) + } + return rec +} + +func TestOpenRejectsMissingFile(t *testing.T) { + if _, err := Open(filepath.Join(t.TempDir(), "missing.mmdb")); err == nil { + t.Fatal("expected an error for a missing file") + } +} + +func TestOpenRejectsEmptyPath(t *testing.T) { + if _, err := Open(""); err == nil { + t.Fatal("expected an error for an empty path") + } +} + +func TestOpenRejectsNonASNDatabase(t *testing.T) { + if _, err := Open(cityFixture); err == nil { + t.Fatal("expected an error when the path points at a City database") + } +} + +func TestASNReturnsRecordAndBuildTime(t *testing.T) { + r, _ := openTemp(t) + + rec := mustASN(t, r, "8.8.8.8") + if rec.AutonomousSystemNumber != 15169 { + t.Errorf("got ASN %d, want 15169", rec.AutonomousSystemNumber) + } + if got := r.Stats().BuildTime.UTC().Format("2006-01-02"); got != "2026-09-04" { + t.Errorf("got build date %s, want 2026-09-04", got) + } + if rec := mustASN(t, r, "203.0.113.5"); rec.AutonomousSystemNumber != 0 { + t.Errorf("unknown IP yielded ASN %d, want 0", rec.AutonomousSystemNumber) + } +} + +func TestReloadWithoutChangeIsNoop(t *testing.T) { + r, _ := openTemp(t) + + changed, err := r.Reload() + if err != nil { + t.Fatalf("reload: %v", err) + } + if changed { + t.Fatal("reload reported a change for an untouched file") + } + if s := r.Stats(); s.Reloads != 0 || s.Failures != 0 { + t.Errorf("stats after noop: %+v", s) + } +} + +func TestReloadPicksUpReplacedFile(t *testing.T) { + r, path := openTemp(t) + installFixture(t, newerFixture, path, time.Now()) + + changed, err := r.Reload() + if err != nil { + t.Fatalf("reload: %v", err) + } + if !changed { + t.Fatal("reload did not notice the replaced file") + } + s := r.Stats() + if got := s.BuildTime.UTC().Format("2006-01-02"); got != "2026-09-18" { + t.Errorf("got build date %s after reload, want 2026-09-18", got) + } + if s.Reloads != 1 || s.Failures != 0 || s.LastError != "" { + t.Errorf("stats after reload: %+v", s) + } + if rec := mustASN(t, r, "8.8.8.8"); rec.AutonomousSystemNumber != 15169 { + t.Errorf("lookup after reload: got ASN %d, want 15169", rec.AutonomousSystemNumber) + } +} + +// The overwrite here is deliberately in place (no rename): the loaded copy +// must not depend on the file's inode staying intact. +func TestReloadKeepsServingWhenReplacementIsCorrupt(t *testing.T) { + r, path := openTemp(t) + if err := os.WriteFile(path, []byte("not an mmdb"), 0o644); err != nil { + t.Fatal(err) + } + + changed, err := r.Reload() + if err == nil { + t.Fatal("expected an error for a corrupt replacement") + } + if changed { + t.Fatal("a failed reload must not report a change") + } + s := r.Stats() + if got := s.BuildTime.UTC().Format("2006-01-02"); got != "2026-09-04" { + t.Errorf("build date changed to %s after a failed reload", got) + } + if s.Failures != 1 || s.LastError == "" { + t.Errorf("stats after failed reload: %+v", s) + } + if rec := mustASN(t, r, "8.8.8.8"); rec.AutonomousSystemNumber != 15169 { + t.Errorf("old database not served after failed reload: ASN %d", rec.AutonomousSystemNumber) + } +} + +func TestReloadKeepsServingWhenReplacementIsWrongType(t *testing.T) { + r, path := openTemp(t) + installFixture(t, cityFixture, path, time.Now()) + + if _, err := r.Reload(); err == nil { + t.Fatal("expected an error for a City database on the ASN path") + } + if rec := mustASN(t, r, "8.8.8.8"); rec.AutonomousSystemNumber != 15169 { + t.Errorf("old database not served after wrong-type reload: ASN %d", rec.AutonomousSystemNumber) + } +} + +func TestReloadKeepsServingWhenFileVanishes(t *testing.T) { + r, path := openTemp(t) + if err := os.Remove(path); err != nil { + t.Fatal(err) + } + + if _, err := r.Reload(); err == nil { + t.Fatal("expected an error when the file is gone") + } + if rec := mustASN(t, r, "8.8.8.8"); rec.AutonomousSystemNumber != 15169 { + t.Errorf("old database not served after the file vanished: ASN %d", rec.AutonomousSystemNumber) + } +} + +// Lookups in flight while the file is swapped must never touch an unmapped +// reader. Run with -race. +func TestReloadIsSafeUnderConcurrentLookups(t *testing.T) { + r, path := openTemp(t) + + stop := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-stop: + return + default: + if rec, err := r.ASN(netip.MustParseAddr("8.8.8.8")); err != nil || rec.AutonomousSystemNumber != 15169 { + t.Errorf("lookup during reload: rec=%+v err=%v", rec, err) + return + } + } + } + }() + } + for i := 0; i < 20; i++ { + src := asnFixture + if i%2 == 0 { + src = newerFixture + } + installFixture(t, src, path, time.Now().Add(time.Duration(i)*time.Second)) + if _, err := r.Reload(); err != nil { + t.Fatalf("reload %d: %v", i, err) + } + } + close(stop) + wg.Wait() +} + +func TestWatchReloadsOnInterval(t *testing.T) { + r, path := openTemp(t) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + done := make(chan struct{}) + go func() { + r.Watch(ctx, 10*time.Millisecond) + close(done) + }() + + installFixture(t, newerFixture, path, time.Now()) + deadline := time.Now().Add(5 * time.Second) + for r.Stats().Reloads == 0 { + if time.Now().After(deadline) { + t.Fatal("watcher did not reload the replaced file") + } + time.Sleep(5 * time.Millisecond) + } + if got := r.Stats().BuildTime.UTC().Format("2006-01-02"); got != "2026-09-18" { + t.Errorf("got build date %s after watched reload, want 2026-09-18", got) + } + + cancel() + select { + case <-done: + case <-time.After(time.Second): + t.Fatal("watcher did not stop on context cancel") + } +} + +func TestCloseThenASNReturnsError(t *testing.T) { + r, _ := openTemp(t) + if err := r.Close(); err != nil { + t.Fatalf("close: %v", err) + } + if _, err := r.ASN(netip.MustParseAddr("8.8.8.8")); err == nil { + t.Fatal("expected an error from a closed reader") + } +} + +func BenchmarkASN(b *testing.B) { + r, err := Open(asnFixture) + if err != nil { + b.Fatal(err) + } + defer r.Close() + ip := netip.MustParseAddr("8.8.8.8") + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := r.ASN(ip); err != nil { + b.Fatal(err) + } + } + }) +} + +// Baseline: the bare geoip2 reader without the reload guard. +func BenchmarkASNRawReader(b *testing.B) { + db, err := geoip2.Open(asnFixture) + if err != nil { + b.Fatal(err) + } + defer db.Close() + ip := netip.MustParseAddr("8.8.8.8") + b.ReportAllocs() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if _, err := db.ASN(ip); err != nil { + b.Fatal(err) + } + } + }) +} diff --git a/libs/geoipdb/testdata/GeoLite2-ASN.mmdb b/libs/geoipdb/testdata/GeoLite2-ASN.mmdb new file mode 100644 index 00000000..9e2b2d5f Binary files /dev/null and b/libs/geoipdb/testdata/GeoLite2-ASN.mmdb differ diff --git a/libs/geoipdb/testdata/GeoLite2-ASN.newer.mmdb b/libs/geoipdb/testdata/GeoLite2-ASN.newer.mmdb new file mode 100644 index 00000000..06f44556 Binary files /dev/null and b/libs/geoipdb/testdata/GeoLite2-ASN.newer.mmdb differ diff --git a/libs/geoipdb/testdata/GeoLite2-City.mmdb b/libs/geoipdb/testdata/GeoLite2-City.mmdb new file mode 100644 index 00000000..67c4e793 Binary files /dev/null and b/libs/geoipdb/testdata/GeoLite2-City.mmdb differ diff --git a/libs/go.mod b/libs/go.mod index 6b921a04..875b48fa 100644 --- a/libs/go.mod +++ b/libs/go.mod @@ -6,30 +6,31 @@ require ( github.com/alicebob/miniredis/v2 v2.38.0 github.com/getsentry/sentry-go v0.31.1 github.com/getsentry/sentry-go/zerolog v0.31.1 + github.com/oschwald/geoip2-golang/v2 v2.4.0 github.com/redis/go-redis/v9 v9.7.3 github.com/rs/zerolog v1.34.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.12.1 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/montanaflynn/stats v0.7.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/oschwald/maxminddb-golang/v2 v2.6.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/yuin/gopher-lua v1.1.1 // indirect go.uber.org/atomic v1.11.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/crypto v0.52.0 // indirect - golang.org/x/sync v0.20.0 // indirect + golang.org/x/sync v0.22.0 // indirect golang.org/x/text v0.37.0 // indirect ) @@ -41,5 +42,5 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect go.mongodb.org/mongo-driver v1.17.3 - golang.org/x/sys v0.45.0 // indirect + golang.org/x/sys v0.47.0 // indirect ) diff --git a/libs/go.sum b/libs/go.sum index a68d918f..524ce24a 100644 --- a/libs/go.sum +++ b/libs/go.sum @@ -84,12 +84,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/oschwald/geoip2-golang/v2 v2.4.0 h1:JdVymxpwFf7o+3o53Sw2gCYBX8maA5DWxcgzNb14yJU= +github.com/oschwald/geoip2-golang/v2 v2.4.0/go.mod h1:VJW7lAC5Dw4WH42mjhUFkxf7+v3K1YOafLD8iBiszsc= +github.com/oschwald/maxminddb-golang/v2 v2.6.0 h1:pRlHCdJmc+4uxMOSthmKDt5HOw3JTX8TJZlhyP5ew0w= +github.com/oschwald/maxminddb-golang/v2 v2.6.0/go.mod h1:sjqpB3z2BZrMduDp9TAUTCkZDoT3nDhixUc4Dge2qRQ= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -97,8 +99,8 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= @@ -124,6 +126,8 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= @@ -134,8 +138,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -144,8 +148,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/libs/store/mongodb.go b/libs/store/mongodb.go index 3a58b8c9..3a285f8c 100644 --- a/libs/store/mongodb.go +++ b/libs/store/mongodb.go @@ -130,7 +130,7 @@ func (db *MongoDB) connect() error { tlsOpts := &tls.Config{ Certificates: []tls.Certificate{cert}, RootCAs: caCertPool, - InsecureSkipVerify: db.Config.TLSInsecureSkipVerify, + InsecureSkipVerify: db.Config.TLSInsecureSkipVerify, //nolint:gosec // operator opt-in for dev/test stacks only; false in production config } clientOpts.SetTLSConfig(tlsOpts) diff --git a/libs/store/store.go b/libs/store/store.go index ad6813b3..87636b06 100644 --- a/libs/store/store.go +++ b/libs/store/store.go @@ -19,8 +19,7 @@ type Store interface { // NewStore creates a new Db instance func New(dbType string, dbConfig *Config) (Store, error) { - switch dbType { - case DbTypeMongoDb: + if dbType == DbTypeMongoDb { return NewMongoDB(dbConfig) } return nil, errors.New("unknown db type") diff --git a/proxy/config/config.go b/proxy/config/config.go index 495aa507..1e27a334 100644 --- a/proxy/config/config.go +++ b/proxy/config/config.go @@ -126,6 +126,9 @@ type ServicesConfig struct { CatalogPath string CatalogReloadEvery time.Duration GeoIPASNDBPath string + // GEOIP_DB_RELOAD: how often the ASN database file is checked for a + // refreshed build and reopened in place. + GeoIPASNDBReloadEvery time.Duration } // FilteringConfig holds global filter master switches. These are operator-level @@ -397,6 +400,14 @@ func New() (*Config, error) { } geoIPASNDBPath := strings.TrimSpace(os.Getenv("GEOIP_DB_ASN_FILE")) + geoIPASNDBReloadEveryStr := strings.TrimSpace(os.Getenv("GEOIP_DB_RELOAD")) + if geoIPASNDBReloadEveryStr == "" { + geoIPASNDBReloadEveryStr = "15m" + } + geoIPASNDBReloadEvery, err := time.ParseDuration(geoIPASNDBReloadEveryStr) + if err != nil || geoIPASNDBReloadEvery <= 0 { + return nil, fmt.Errorf("GEOIP_DB_RELOAD must be a positive duration, got %q", geoIPASNDBReloadEveryStr) + } cacheAddrs := strings.Split(os.Getenv("CACHE_ADDRESSES"), ",") @@ -412,9 +423,10 @@ func New() (*Config, error) { MaxGoroutines: loadMaxGoroutines(), }, Services: &ServicesConfig{ - CatalogPath: servicesCatalogPath, - CatalogReloadEvery: servicesCatalogReloadEvery, - GeoIPASNDBPath: geoIPASNDBPath, + CatalogPath: servicesCatalogPath, + CatalogReloadEvery: servicesCatalogReloadEvery, + GeoIPASNDBPath: geoIPASNDBPath, + GeoIPASNDBReloadEvery: geoIPASNDBReloadEvery, }, Filtering: &FilteringConfig{ CNAMEUncloakingEnabled: getEnvBoolDefault("CNAME_UNCLOAKING_ENABLED", true), diff --git a/proxy/go.mod b/proxy/go.mod index aee5bf9c..4baa2591 100644 --- a/proxy/go.mod +++ b/proxy/go.mod @@ -10,13 +10,12 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/ivpn/dns/libs v0.0.0 github.com/miekg/dns v1.1.72 - github.com/oschwald/geoip2-golang v1.13.0 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/quic-go/quic-go v0.60.0 github.com/redis/go-redis/v9 v9.7.3 github.com/rs/zerolog v1.34.0 - github.com/stretchr/testify v1.11.1 + github.com/stretchr/testify v1.12.1 github.com/testcontainers/testcontainers-go v0.43.0 github.com/testcontainers/testcontainers-go/modules/redis v0.43.0 github.com/testcontainers/testcontainers-go/modules/toxiproxy v0.43.0 @@ -38,7 +37,6 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/docker v28.5.1+incompatible // indirect github.com/docker/go-connections v0.6.0 // indirect @@ -70,8 +68,8 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/oschwald/maxminddb-golang v1.13.0 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/oschwald/geoip2-golang/v2 v2.4.0 // indirect + github.com/oschwald/maxminddb-golang/v2 v2.6.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect @@ -93,6 +91,7 @@ require ( go.opentelemetry.io/otel/trace v1.44.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -107,13 +106,13 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/quic-go/qpack v0.6.0 // indirect go.mongodb.org/mongo-driver v1.17.3 - golang.org/x/crypto v0.54.0 // indirect + golang.org/x/crypto v0.55.0 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/mod v0.39.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sync v0.22.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect - golang.org/x/tools v0.48.0 // indirect + golang.org/x/text v0.41.0 // indirect + golang.org/x/tools v0.49.0 // indirect gonum.org/v1/gonum v0.17.0 // indirect ) diff --git a/proxy/go.sum b/proxy/go.sum index 33d4fd7d..92a50b20 100644 --- a/proxy/go.sum +++ b/proxy/go.sum @@ -146,16 +146,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI= -github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo= -github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU= -github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o= +github.com/oschwald/geoip2-golang/v2 v2.4.0 h1:JdVymxpwFf7o+3o53Sw2gCYBX8maA5DWxcgzNb14yJU= +github.com/oschwald/geoip2-golang/v2 v2.4.0/go.mod h1:VJW7lAC5Dw4WH42mjhUFkxf7+v3K1YOafLD8iBiszsc= +github.com/oschwald/maxminddb-golang/v2 v2.6.0 h1:pRlHCdJmc+4uxMOSthmKDt5HOw3JTX8TJZlhyP5ew0w= +github.com/oschwald/maxminddb-golang/v2 v2.6.0/go.mod h1:sjqpB3z2BZrMduDp9TAUTCkZDoT3nDhixUc4Dge2qRQ= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= @@ -187,8 +185,8 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/testcontainers/testcontainers-go v0.43.0 h1:oEQx5MW2DGd9z3AeEQfB2lPM0eLs7ztyaGRu75bFo5A= github.com/testcontainers/testcontainers-go v0.43.0/go.mod h1:+VxkT2NQnKOZPKi6praMuMKYHYyOGXr0XSBSlSMCzFo= github.com/testcontainers/testcontainers-go/modules/redis v0.43.0 h1:qzATMhrltLr07KcGl/d674ouqI0AFtf6wnQb3VnqP7M= @@ -234,20 +232,22 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74= +golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= @@ -273,15 +273,15 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= diff --git a/proxy/internal/asnlookup/lookup.go b/proxy/internal/asnlookup/lookup.go index de8bf056..119412d4 100644 --- a/proxy/internal/asnlookup/lookup.go +++ b/proxy/internal/asnlookup/lookup.go @@ -1,22 +1,27 @@ package asnlookup import ( + "context" "errors" "fmt" "net" + "net/netip" + "time" - "github.com/oschwald/geoip2-golang" + "github.com/ivpn/dns/libs/geoipdb" ) +// Lookup answers ASN queries from a GeoLite2-ASN file and follows the file +// when it is refreshed on disk. type Lookup struct { - db *geoip2.Reader + db *geoipdb.Reader } func New(mmdbPath string) (*Lookup, error) { if mmdbPath == "" { return nil, errors.New("ASN MMDB path is required") } - db, err := geoip2.Open(mmdbPath) + db, err := geoipdb.Open(mmdbPath) if err != nil { return nil, err } @@ -27,10 +32,11 @@ func (l *Lookup) ASN(ip net.IP) (uint, error) { if l == nil || l.db == nil { return 0, nil } - if ip == nil { + addr, ok := netip.AddrFromSlice(ip) + if !ok { return 0, nil } - rec, err := l.db.ASN(ip) + rec, err := l.db.ASN(addr) if err != nil { return 0, fmt.Errorf("asn lookup: %w", err) } @@ -40,6 +46,33 @@ func (l *Lookup) ASN(ip net.IP) (uint, error) { return rec.AutonomousSystemNumber, nil } +// Reload checks the file now and reopens it if it changed; see +// geoipdb.Reader.Reload. Production relies on Watch, which runs the same check +// on a timer; Reload is the synchronous entry point for tests and for a manual +// "reload now" trigger. +func (l *Lookup) Reload() (bool, error) { + if l == nil || l.db == nil { + return false, nil + } + return l.db.Reload() +} + +// Watch reloads the database on a timer until ctx is cancelled. +func (l *Lookup) Watch(ctx context.Context, every time.Duration) { + if l == nil || l.db == nil { + return + } + l.db.Watch(ctx, every) +} + +// Stats reports the build time and reload counters of the loaded database. +func (l *Lookup) Stats() geoipdb.Stats { + if l == nil || l.db == nil { + return geoipdb.Stats{} + } + return l.db.Stats() +} + func (l *Lookup) Close() error { if l == nil || l.db == nil { return nil diff --git a/proxy/internal/asnlookup/lookup_test.go b/proxy/internal/asnlookup/lookup_test.go new file mode 100644 index 00000000..43018f6f --- /dev/null +++ b/proxy/internal/asnlookup/lookup_test.go @@ -0,0 +1,123 @@ +package asnlookup + +import ( + "net" + "os" + "path/filepath" + "testing" + "time" +) + +const ( + asnFixture = "testdata/GeoLite2-ASN.mmdb" // build 2026-09-04 + newerFixture = "testdata/GeoLite2-ASN.newer.mmdb" // same networks, build 2026-09-18 +) + +func install(t *testing.T, src, dst string, mtime time.Time) { + t.Helper() + data, err := os.ReadFile(src) + if err != nil { + t.Fatal(err) + } + tmp := dst + ".temporary" + if err := os.WriteFile(tmp, data, 0o644); err != nil { + t.Fatal(err) + } + if err := os.Chtimes(tmp, mtime, mtime); err != nil { + t.Fatal(err) + } + if err := os.Rename(tmp, dst); err != nil { + t.Fatal(err) + } +} + +// specRef: proxy-filtering-behaviour.md #GEO1 +func TestNewRequiresPath(t *testing.T) { + if _, err := New(""); err == nil { + t.Fatal("expected an error for an empty path") + } +} + +func TestASNReturnsNumberAndZeroForUnknownOrNil(t *testing.T) { + l, err := New(asnFixture) + if err != nil { + t.Fatalf("open: %v", err) + } + defer l.Close() + + if asn, err := l.ASN(net.ParseIP("8.8.8.8")); err != nil || asn != 15169 { + t.Errorf("got asn=%d err=%v, want 15169", asn, err) + } + if asn, err := l.ASN(net.ParseIP("203.0.113.5")); err != nil || asn != 0 { + t.Errorf("unknown IP: got asn=%d err=%v, want 0", asn, err) + } + // A records carry the 4-byte form; net.ParseIP yields the 16-byte form. + if asn, err := l.ASN(net.ParseIP("8.8.8.8").To4()); err != nil || asn != 15169 { + t.Errorf("4-byte IPv4: got asn=%d err=%v, want 15169", asn, err) + } + if asn, err := l.ASN(net.IP([]byte{1, 2, 3})); err != nil || asn != 0 { + t.Errorf("malformed IP: got asn=%d err=%v, want 0", asn, err) + } + if asn, err := l.ASN(nil); err != nil || asn != 0 { + t.Errorf("nil IP: got asn=%d err=%v, want 0", asn, err) + } + var none *Lookup + if asn, err := none.ASN(net.ParseIP("8.8.8.8")); err != nil || asn != 0 { + t.Errorf("nil lookup: got asn=%d err=%v, want 0", asn, err) + } +} + +// A database refreshed on disk (write + rename, as geoipupdate does) is served +// after the next reload without reopening the Lookup. +// +// specRef: proxy-filtering-behaviour.md #GEO2 +func TestReloadServesReplacedDatabase(t *testing.T) { + path := filepath.Join(t.TempDir(), "GeoLite2-ASN.mmdb") + install(t, asnFixture, path, time.Now().Add(-time.Hour)) + l, err := New(path) + if err != nil { + t.Fatalf("open: %v", err) + } + defer l.Close() + if got := l.Stats().BuildTime.UTC().Format("2006-01-02"); got != "2026-09-04" { + t.Fatalf("initial build date %s, want 2026-09-04", got) + } + + install(t, newerFixture, path, time.Now()) + changed, err := l.Reload() + if err != nil || !changed { + t.Fatalf("reload: changed=%v err=%v", changed, err) + } + if got := l.Stats().BuildTime.UTC().Format("2006-01-02"); got != "2026-09-18" { + t.Errorf("build date after reload %s, want 2026-09-18", got) + } + if asn, err := l.ASN(net.ParseIP("8.8.8.8")); err != nil || asn != 15169 { + t.Errorf("lookup after reload: asn=%d err=%v", asn, err) + } +} + +// A corrupt replacement is rejected and the loaded database keeps serving. +// +// specRef: proxy-filtering-behaviour.md #GEO3 +func TestReloadKeepsOldDatabaseWhenReplacementIsCorrupt(t *testing.T) { + path := filepath.Join(t.TempDir(), "GeoLite2-ASN.mmdb") + install(t, asnFixture, path, time.Now().Add(-time.Hour)) + l, err := New(path) + if err != nil { + t.Fatalf("open: %v", err) + } + defer l.Close() + + if err := os.WriteFile(path, []byte("not an mmdb"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := l.Reload(); err == nil { + t.Fatal("expected an error for a corrupt replacement") + } + if s := l.Stats(); s.Failures != 1 || s.LastError == "" { + t.Errorf("stats after failed reload: %+v", s) + } + if asn, err := l.ASN(net.ParseIP("8.8.8.8")); err != nil || asn != 15169 { + t.Errorf("old database not served after failed reload: asn=%d err=%v", asn, err) + } +} diff --git a/proxy/internal/asnlookup/testdata/GeoLite2-ASN.mmdb b/proxy/internal/asnlookup/testdata/GeoLite2-ASN.mmdb new file mode 100644 index 00000000..9e2b2d5f Binary files /dev/null and b/proxy/internal/asnlookup/testdata/GeoLite2-ASN.mmdb differ diff --git a/proxy/internal/asnlookup/testdata/GeoLite2-ASN.newer.mmdb b/proxy/internal/asnlookup/testdata/GeoLite2-ASN.newer.mmdb new file mode 100644 index 00000000..06f44556 Binary files /dev/null and b/proxy/internal/asnlookup/testdata/GeoLite2-ASN.newer.mmdb differ diff --git a/proxy/internal/metrics/geoipdb.go b/proxy/internal/metrics/geoipdb.go new file mode 100644 index 00000000..abc88f35 --- /dev/null +++ b/proxy/internal/metrics/geoipdb.go @@ -0,0 +1,44 @@ +package metrics + +import ( + "github.com/ivpn/dns/libs/geoipdb" + "github.com/prometheus/client_golang/prometheus" +) + +// GeoIPDBSource is what the GeoIP gauges read; *asnlookup.Lookup satisfies it. +type GeoIPDBSource interface { + Stats() geoipdb.Stats +} + +// ObserveGeoIPDB registers gauges describing the GeoLite2-ASN database in use. +// The build timestamp is the end-to-end freshness signal: it only moves when +// the on-disk file was refreshed and the process picked it up. +func ObserveGeoIPDB(reg prometheus.Registerer, src GeoIPDBSource) { + reg.MustRegister( + prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "proxy_dns_geoip_db_build_timestamp_seconds", + Help: "Build time (Unix seconds) of the GeoLite2-ASN database currently serving lookups.", + }, func() float64 { return float64(src.Stats().BuildTime.Unix()) }), + prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "proxy_dns_geoip_db_loaded_timestamp_seconds", + Help: "Time (Unix seconds) the current GeoLite2-ASN file was opened by this process.", + }, func() float64 { return float64(src.Stats().LoadedAt.Unix()) }), + prometheus.NewCounterFunc(prometheus.CounterOpts{ + Name: "proxy_dns_geoip_db_reloads_total", + Help: "Successful in-process reloads of the GeoLite2-ASN database.", + }, func() float64 { return float64(src.Stats().Reloads) }), + prometheus.NewCounterFunc(prometheus.CounterOpts{ + Name: "proxy_dns_geoip_db_reload_failures_total", + Help: "Replacement GeoLite2-ASN files rejected (unreadable, corrupt or wrong edition); the previous database kept serving.", + }, func() float64 { return float64(src.Stats().Failures) }), + prometheus.NewGaugeFunc(prometheus.GaugeOpts{ + Name: "proxy_dns_geoip_db_reload_error", + Help: "1 while the most recent reload attempt failed, 0 after a successful load.", + }, func() float64 { + if src.Stats().LastError != "" { + return 1 + } + return 0 + }), + ) +} diff --git a/proxy/internal/metrics/geoipdb_test.go b/proxy/internal/metrics/geoipdb_test.go new file mode 100644 index 00000000..32114531 --- /dev/null +++ b/proxy/internal/metrics/geoipdb_test.go @@ -0,0 +1,63 @@ +package metrics + +import ( + "testing" + "time" + + "github.com/ivpn/dns/libs/geoipdb" + "github.com/prometheus/client_golang/prometheus" + dto "github.com/prometheus/client_model/go" +) + +type fakeGeoIPDB struct{ stats geoipdb.Stats } + +func (f fakeGeoIPDB) Stats() geoipdb.Stats { return f.stats } + +func gaugeValue(t *testing.T, reg *prometheus.Registry, name string) float64 { + t.Helper() + families, err := reg.Gather() + if err != nil { + t.Fatal(err) + } + for _, mf := range families { + if mf.GetName() != name { + continue + } + m := mf.GetMetric()[0] + if mf.GetType() == dto.MetricType_COUNTER { + return m.GetCounter().GetValue() + } + return m.GetGauge().GetValue() + } + t.Fatalf("metric %s not registered", name) + return 0 +} + +func TestObserveGeoIPDBExposesBuildAndReloadState(t *testing.T) { + build := time.Date(2026, 9, 18, 13, 50, 42, 0, time.UTC) + loaded := build.Add(2 * time.Hour) + reg := prometheus.NewRegistry() + ObserveGeoIPDB(reg, fakeGeoIPDB{stats: geoipdb.Stats{ + BuildTime: build, + LoadedAt: loaded, + Reloads: 3, + Failures: 1, + LastError: "boom", + }}) + + if got := gaugeValue(t, reg, "proxy_dns_geoip_db_build_timestamp_seconds"); got != float64(build.Unix()) { + t.Errorf("build timestamp gauge = %v, want %v", got, build.Unix()) + } + if got := gaugeValue(t, reg, "proxy_dns_geoip_db_loaded_timestamp_seconds"); got != float64(loaded.Unix()) { + t.Errorf("loaded timestamp gauge = %v, want %v", got, loaded.Unix()) + } + if got := gaugeValue(t, reg, "proxy_dns_geoip_db_reloads_total"); got != 3 { + t.Errorf("reloads counter = %v, want 3", got) + } + if got := gaugeValue(t, reg, "proxy_dns_geoip_db_reload_failures_total"); got != 1 { + t.Errorf("failures counter = %v, want 1", got) + } + if got := gaugeValue(t, reg, "proxy_dns_geoip_db_reload_error"); got != 1 { + t.Errorf("reload error gauge = %v, want 1", got) + } +} diff --git a/proxy/server/server.go b/proxy/server/server.go index 0a643039..6ff39c9e 100644 --- a/proxy/server/server.go +++ b/proxy/server/server.go @@ -123,7 +123,15 @@ func NewServer(serverConfig *config.Config, collectorChannels map[string]channel log.Error().Err(err).Str("path", serverConfig.Services.GeoIPASNDBPath).Msg("Failed to open ASN MMDB") return nil, fmt.Errorf("ASN lookup: %w", err) } - log.Info().Str("catalog", serverConfig.Services.CatalogPath).Str("geodb", serverConfig.Services.GeoIPASNDBPath).Msg("Services blocking enabled") + // The file is refreshed on disk by geoipupdate; follow it without a restart. + go lookup.Watch(context.Background(), serverConfig.Services.GeoIPASNDBReloadEvery) + metrics.ObserveGeoIPDB(prometheus.DefaultRegisterer, lookup) + log.Info(). + Str("catalog", serverConfig.Services.CatalogPath). + Str("geodb", serverConfig.Services.GeoIPASNDBPath). + Time("geodb_build", lookup.Stats().BuildTime). + Dur("geodb_reload", serverConfig.Services.GeoIPASNDBReloadEvery). + Msg("Services blocking enabled") domainFilter := filter.NewDomainFilter(dnsProxy, cache, servicesCatalog) domainFilter.Metrics = server.Metrics diff --git a/tests/bootstrap/geolite/README.md b/tests/bootstrap/geolite/README.md index 9b93c2f0..1c1f4d14 100644 --- a/tests/bootstrap/geolite/README.md +++ b/tests/bootstrap/geolite/README.md @@ -1,6 +1,6 @@ -These are stub .mmdb files for backend E2E tests, NOT full GeoLite2 databases. +This is a stub GeoLite2-ASN .mmdb for backend E2E tests, NOT the full GeoLite2 database. -They contain only two entries (AS15169 Google, AS13335 Cloudflare). -City lookups return empty records but won't crash. +It contains only the networks the tests query (Google, Cloudflare, Apple, Microsoft ASNs). +The proxy and dnscheck read the ASN edition only; no City database is mounted. Regenerate: `cd tests && python3 scripts/generate_stub_mmdb.py` diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 2b0e5569..28d409d0 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -72,7 +72,6 @@ services: volumes: - ../dnscheck:/app - ./bootstrap/geolite/:/opt/dnscheck/GeoIP - - ./bootstrap/geolite/:/opt/dnscheck/GeoIPCity env_file: - ./config/dnscheck.env diff --git a/tests/scripts/generate_stub_mmdb.py b/tests/scripts/generate_stub_mmdb.py index 58a1bc32..1a71534a 100644 --- a/tests/scripts/generate_stub_mmdb.py +++ b/tests/scripts/generate_stub_mmdb.py @@ -18,12 +18,11 @@ Usage: python scripts/generate_stub_mmdb.py - Writes the backend E2E stubs to bootstrap/geolite/ (both files carry - the ASN payload; the "City" file is a copy so mounts never fail). + Writes the backend E2E ASN stub to bootstrap/geolite/. python scripts/generate_stub_mmdb.py --out-dir ../dnscheck/internal/maxmind/testdata --city-typed - Writes the dnscheck unit-test fixtures. --city-typed makes the City - file a real GeoLite2-City database so a wrong-type file can be tested. + Writes the dnscheck unit-test fixtures. --city-typed additionally + writes a real GeoLite2-City database so a wrong-type file can be tested. """ import argparse @@ -37,7 +36,7 @@ parser.add_argument( "--city-typed", action="store_true", - help="write GeoLite2-City.mmdb with database_type GeoLite2-City instead of copying the ASN stub", + help="also write GeoLite2-City.mmdb with database_type GeoLite2-City (dnscheck wrong-type fixture)", ) args = parser.parse_args() os.makedirs(args.out_dir, exist_ok=True) @@ -75,8 +74,8 @@ writer.to_db_file(out_asn) print(f"Wrote {out_asn}") -out_city = os.path.join(args.out_dir, "GeoLite2-City.mmdb") if args.city_typed: + out_city = os.path.join(args.out_dir, "GeoLite2-City.mmdb") city_writer = MMDBWriter( ip_version=4, database_type="GeoLite2-City", @@ -87,6 +86,4 @@ {"country": {"iso_code": "US", "names": {"en": "United States"}}}, ) city_writer.to_db_file(out_city) -else: - writer.to_db_file(out_city) -print(f"Wrote {out_city}") + print(f"Wrote {out_city}")