diff --git a/README.md b/README.md index 86e8835..2168b80 100644 --- a/README.md +++ b/README.md @@ -55,3 +55,38 @@ echo "1.1.1.1" | zannotate --geoasn --geoasn-database=/path-to-downloaded-file/G ```shell {"ip":"1.1.1.1","geoasn":{"asn":13335,"org":"CLOUDFLARENET"}} ``` + +# Input/Output + +## Output +By default, ZAnnotate reads new-line delimited IP addresses from standard input and outputs a JSON object per line to standard output like: + +```shell +echo "1.1.1.1" | zannotate --rdns --geoasn --geoasn-database=/path-to-geo-asn.mmdb +``` + +```json +{"ip":"1.1.1.1","geoasn":{"asn":13335,"org":"CLOUDFLARENET"},"rdns":{"domain_names":["one.one.one.one"]}} +``` + +If an IP address cannot be annotated, either because of an error or lack of data, there will be an empty field for that annotation. +For example, if an IP address is private and therefore has no RDNS or ASN data, the output will look like: +```shell +echo "127.0.0.1" | zannotate --rdns --geoasn --geoasn-database=/path-to-geo-asn.mmdb +``` + +```json +{"geoasn":{},"rdns":{},"ip":"127.0.0.1"} +``` + +## Input +You may wish to annotate data that is already in JSON format. You'll then need to use the `--input-file-type=json` flag. +This will insert a `zannotate` field into the existing JSON object. For example: + +```shell +echo '{"ip": "1.1.1.1"}' | ./zannotate --rdns --geoasn --geoasn-database=/path-to-geo-asn.mmdb --input-file-type=json +``` + +```json +{"ip":"1.1.1.1","zannotate":{"geoasn":{"asn":13335,"org":"CLOUDFLARENET"},"rdns":{"domain_names":["one.one.one.one"]}}} +``` diff --git a/go.mod b/go.mod index 1fb2e70..d431145 100644 --- a/go.mod +++ b/go.mod @@ -8,14 +8,38 @@ require ( github.com/osrg/gobgp/v3 v3.37.0 github.com/sirupsen/logrus v1.9.3 github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 + github.com/zmap/zdns/v2 v2.0.5 gotest.tools/v3 v3.5.2 ) require ( github.com/asergeyev/nradix v0.0.0-20220715161825-e451993e425c // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/censys/cidranger v1.1.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oschwald/maxminddb-golang v1.13.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.22.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.63.0 // indirect + github.com/prometheus/procfs v0.16.0 // indirect + github.com/weppos/publicsuffix-go v0.40.3-0.20250311103038-7794c8c0723b // indirect + github.com/zmap/dns v1.1.67 // indirect + github.com/zmap/go-dns-root-anchors v0.0.0-20250415191259-6d65fb878756 // indirect + github.com/zmap/zcrypto v0.0.0-20250416162916-8ff8dfaa718d // indirect + github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6 // indirect + github.com/zmap/zgrab2 v0.2.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/mod v0.25.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.26.0 // indirect + golang.org/x/tools v0.33.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect ) diff --git a/go.sum b/go.sum index acef9c0..5c519b4 100644 --- a/go.sum +++ b/go.sum @@ -1,42 +1,104 @@ github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4= github.com/asergeyev/nradix v0.0.0-20220715161825-e451993e425c h1:cN6WRmhJkh/u5bvf/XXjoqcHxljVKIz3Nt7q2dVJySo= github.com/asergeyev/nradix v0.0.0-20220715161825-e451993e425c/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/censys/cidranger v1.1.3 h1:YZxgTxj1N9e283yhWybErvuV28TluEUa/3WlIwDrp9k= +github.com/censys/cidranger v1.1.3/go.mod h1:QQ2LmUiOSV/1o7qUG8Bcx+uAWwC9bfSKUHV51EnGcZg= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8= github.com/osrg/gobgp/v3 v3.37.0 h1:+ObuOdvj7G7nxrT0fKFta+EAupdWf/q1WzbXydr8IOY= github.com/osrg/gobgp/v3 v3.37.0/go.mod h1:kVHVFy1/fyZHJ8P32+ctvPeJogn9qKwa1YCeMRXXrP0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +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/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= +github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= +github.com/prometheus/procfs v0.16.0 h1:xh6oHhKwnOJKMYiYBDWmkHqQPyiY40sny36Cmx2bbsM= +github.com/prometheus/procfs v0.16.0/go.mod h1:8veyXUu3nGP7oaCxhX6yeaM5u4stL2FeMXnCqhDthZg= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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/weppos/publicsuffix-go v0.40.3-0.20250311103038-7794c8c0723b h1:PFOWooJRLwIuZk9i3ihzKzZffPrAVyOCzPInvLbn140= +github.com/weppos/publicsuffix-go v0.40.3-0.20250311103038-7794c8c0723b/go.mod h1:EACzvcFHnxqmDapI/oqMjtpXz+mtjNzJe7r1zhRczZ0= +github.com/zmap/dns v1.1.67 h1:6WXzSZdzGMOAFmockRtjNc7F3t4YIuDm/gkmitp56Ec= +github.com/zmap/dns v1.1.67/go.mod h1:/Zt3MfW9PFlp3pN3VdTF2Mi6q6b+o0iy46MesRiM434= +github.com/zmap/go-dns-root-anchors v0.0.0-20250415191259-6d65fb878756 h1:yeprVDswfVwnP3uCPm1h1vUZYUOMk7Ue56//8ttmUwA= +github.com/zmap/go-dns-root-anchors v0.0.0-20250415191259-6d65fb878756/go.mod h1:W5CEzaf+B3Pg1hzUQotwQ2EBg7jB49DkmKuiNuWPO80= github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 h1:DjHnADS2r2zynZ3WkCFAQ+PNYngMSNceRROi0pO6c3M= github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837/go.mod h1:9vp0bxqozzQwcjBwenEXfKVq8+mYbwHkQ1NF9Ap0DMw= +github.com/zmap/zcrypto v0.0.0-20250416162916-8ff8dfaa718d h1:QcrqQ8a285ozWrRrsPUtYl4y+9YJAM4gSTF3EQDTQk8= +github.com/zmap/zcrypto v0.0.0-20250416162916-8ff8dfaa718d/go.mod h1:Zz4/7kyRgJXC+PTpLV4tIgaCMTHWnNbgOLZoXuVrkws= +github.com/zmap/zdns/v2 v2.0.5 h1:RNrKZWki/LzKIXiHcO3oJwVM0mXkwu5mNs5s3Phniy0= +github.com/zmap/zdns/v2 v2.0.5/go.mod h1:Q0RdCE5MdkTUHm3CUscxke3hyba6fjV8kIuCS/yeiDo= +github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6 h1:XYA+NN2AS4mRmIDVu2nCtrjU17zKlRihO3MnlcmueUw= +github.com/zmap/zflags v1.4.0-beta.1.0.20200204220219-9d95409821b6/go.mod h1:HXDUD+uue8yeLHr0eXx1lvY6CvMiHbTKw5nGmA9OUoo= +github.com/zmap/zgrab2 v0.2.0 h1:j48+zkSw4rbvQOq9em5MnPxwP5QyUmzTXSCtQzZ2MnI= +github.com/zmap/zgrab2 v0.2.0/go.mod h1:vM5eYaxZTjIGZe9oijtxjU4EfucELIr9mbG7Chxmn2I= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/rdns.go b/rdns.go index a17ff79..5003938 100644 --- a/rdns.go +++ b/rdns.go @@ -1,5 +1,5 @@ /* - * ZAnnotate Copyright 2018 Regents of the University of Michigan + * ZAnnotate Copyright 2025 Regents of the University of Michigan * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy @@ -15,18 +15,33 @@ package zannotate import ( + "context" "flag" + "fmt" "net" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/zmap/dns" + "github.com/zmap/zdns/v2/src/zdns" ) +type RDNSOutput struct { + DomainNames []string `json:"domain_names,omitempty"` +} + type RDNSAnnotatorFactory struct { BasePluginConf RawResolvers string + zdnsConfig *zdns.ResolverConfig + timeoutSecs int } type RDNSAnnotator struct { - Factory *RDNSAnnotatorFactory - Id int + Factory *RDNSAnnotatorFactory + Id int + zdnsResolver *zdns.Resolver } // RDNS Annotator Factory (Global) @@ -38,7 +53,35 @@ func (a *RDNSAnnotatorFactory) MakeAnnotator(i int) Annotator { return &v } -func (a *RDNSAnnotatorFactory) Initialize(conf *GlobalConf) error { +func (a *RDNSAnnotatorFactory) Initialize(_ *GlobalConf) error { + a.zdnsConfig = zdns.NewResolverConfig() + a.zdnsConfig.NetworkTimeout = time.Second * 5 + if len(strings.TrimSpace(a.RawResolvers)) > 0 { + // Parse and Validate the User-Specified Resolvers + // 1. split on comma + resolvers := strings.Split(a.RawResolvers, ",") + // 2. trim whitespace + for _, resolver := range resolvers { + trimmedString := strings.TrimSpace(resolver) + // 3. validate IP + ip := net.ParseIP(trimmedString) + if ip == nil { + return fmt.Errorf("failed to parse dns server IP address: %s", trimmedString) + } + // 4. Differentiate between IPv4 and IPv6 + ns := zdns.NameServer{ + IP: ip, + Port: 53, + DomainName: "", + } + if ip.To4() != nil { + a.zdnsConfig.ExternalNameServersV4 = append(a.zdnsConfig.ExternalNameServersV4, ns) + } else { + a.zdnsConfig.ExternalNameServersV6 = append(a.zdnsConfig.ExternalNameServersV6, ns) + } + } + } + return nil } @@ -57,13 +100,18 @@ func (a *RDNSAnnotatorFactory) IsEnabled() bool { func (a *RDNSAnnotatorFactory) AddFlags(flags *flag.FlagSet) { // Reverse DNS Lookup flags.BoolVar(&a.Enabled, "rdns", false, "reverse dns lookup") - flags.StringVar(&a.RawResolvers, "rdns-dns-servers", "", "list of DNS servers to use for DNS lookups") + flags.StringVar(&a.RawResolvers, "rdns-dns-servers", "", "list of DNS servers to use for DNS lookups, comma-separated IP list. If empty, will use system defaults") flags.IntVar(&a.Threads, "rdns-threads", 100, "how many reverse dns threads") + flags.IntVar(&a.timeoutSecs, "rdns-timeout", 2, "timeout for each rdns query, in seconds") } // RDNS Annotator (Per-Worker) -func (a *RDNSAnnotator) Initialize() error { +func (a *RDNSAnnotator) Initialize() (err error) { + a.zdnsResolver, err = zdns.InitResolver(a.Factory.zdnsConfig) + if err != nil { + return fmt.Errorf("failed to initialize zdns resolver: %w", err) + } return nil } @@ -71,15 +119,49 @@ func (a *RDNSAnnotator) GetFieldName() string { return "rdns" } +// Annotate performs a reverse DNS lookup for the given IP address and returns the results. +// If an error occurs or a lookup fails, it returns nil func (a *RDNSAnnotator) Annotate(ip net.IP) interface{} { - return nil + q := zdns.Question{ + Type: dns.TypePTR, + Class: dns.ClassINET, + Name: ip.String(), + } + output := &RDNSOutput{} + res, _, status, err := a.zdnsResolver.ExternalLookup(context.Background(), &q, nil) + if err != nil { + log.Debug("encountered error when resolving rdns for ", ip.String(), ": ", err) + return output + } + if status != zdns.StatusNoError { + log.Debug("could not resolve rdns for ", ip.String(), " with status: ", status) + return output + } + if res == nil { + // this should never happen, but this will be more helpful than a panic + log.Fatalf("zdns returned a nil result without erroring, zannotate cannot continue") + } + output.DomainNames = make([]string, 0, len(res.Answers)) + for _, answer := range res.Answers { + if castAns, ok := answer.(zdns.Answer); ok { + // Sometimes, CNAME records are returned in addition to PTR records. We'll ignore all non-PTR records. + // This replicates the behavior of Go's net.LookupAddr + if castAns.Type != "PTR" { + continue + } + // remove trailing period from domain name, ex: example.com. -> example.com + output.DomainNames = append(output.DomainNames, strings.TrimSuffix(castAns.Answer, ".")) + } + } + return output } func (a *RDNSAnnotator) Close() error { + a.zdnsResolver.Close() return nil } -//func init() { -// s := new(RDNSAnnotatorFactory) -// RegisterAnnotator(s) -//} +func init() { + s := new(RDNSAnnotatorFactory) + RegisterAnnotator(s) +} diff --git a/rdns_test.go b/rdns_test.go new file mode 100644 index 0000000..1160ce3 --- /dev/null +++ b/rdns_test.go @@ -0,0 +1,68 @@ +/* + * ZAnnotate Copyright 2025 Regents of the University of Michigan + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package zannotate + +import ( + "net" + "reflect" + "testing" +) + +func TestRDNSAnnotate(t *testing.T) { + tests := []struct { + testName string // name of the subtest + ip net.IP // input IP address + expectedResponse *RDNSOutput + }{ + { + testName: "Positive Test Case", + ip: net.ParseIP("1.1.1.1"), + expectedResponse: &RDNSOutput{ + DomainNames: []string{"one.one.one.one"}, + }, + }, { + testName: "Negative Test Case", + ip: net.ParseIP("127.0.0.1"), + expectedResponse: &RDNSOutput{}, + }, { + testName: "IPv6 Test Case", + ip: net.ParseIP("2001:4860:4860::8888"), + expectedResponse: &RDNSOutput{ + DomainNames: []string{"dns.google"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + var factory RDNSAnnotatorFactory + err := factory.Initialize(nil) + if err != nil { + t.Fatalf("failed to initialize RDNS annotator factory: %v", err) + } + annotator := factory.MakeAnnotator(0) + err = annotator.Initialize() + if err != nil { + t.Fatalf("failed to initialize RDNS annotator: %v", err) + } + rawResult := annotator.Annotate(tt.ip) + if rawResult == nil && tt.expectedResponse != nil { + t.Errorf("RDNS annotator returned a nil result, expected %v", tt.expectedResponse) + } + if !reflect.DeepEqual(rawResult, tt.expectedResponse) { + t.Errorf("expected domain names %v, got %v", tt.expectedResponse, rawResult) + } + }) + } +}