diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 2a3663d2..ec57f6ca 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -87,6 +87,11 @@ extldflags facebookgo Factset fastcgi +FCr +fcrdns +fcrdnscheck +fdc +fdns fediverse finfos Firecrawl @@ -136,6 +141,7 @@ impressum inp IPTo iptoasn +isp iss isset ivh @@ -227,6 +233,7 @@ redir redirectscheme refactors relayd +remotehost reputational reqmeta risc diff --git a/cmd/anubis/main.go b/cmd/anubis/main.go index 19f1ecec..b0f21ba0 100644 --- a/cmd/anubis/main.go +++ b/cmd/anubis/main.go @@ -30,6 +30,7 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/internal" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/internal/thoth" libanubis "github.com/TecharoHQ/anubis/lib" botPolicy "github.com/TecharoHQ/anubis/lib/policy" @@ -278,7 +279,8 @@ func main() { log.Fatalf("you can't set COOKIE_DOMAIN and COOKIE_DYNAMIC_DOMAIN at the same time") } - ctx := context.Background() + fdns := fcrdns.NewFCrDNS() + ctx := fcrdns.With(context.Background(), fdns) // Thoth configuration switch { @@ -384,6 +386,7 @@ func main() { } s, err := libanubis.New(libanubis.Options{ + FCrDNS: fdns, BasePrefix: *basePrefix, StripBasePrefix: *stripBasePrefix, Next: rp, diff --git a/data/crawlers/applebot.yaml b/data/crawlers/applebot.yaml index e75dfe14..27e3fd2a 100644 --- a/data/crawlers/applebot.yaml +++ b/data/crawlers/applebot.yaml @@ -1,20 +1,16 @@ # Indexing for search and Siri # https://support.apple.com/en-us/119829 - name: applebot - user_agent_regex: Applebot + expression: + all: + - userAgent.matches("Applebot") + - fcrdns.check("\\.applebot\\.apple\\.com$") action: ALLOW - # https://search.developer.apple.com/applebot.json - remote_addresses: [ - "17.241.208.160/27", - "17.241.193.160/27", - "17.241.200.160/27", - "17.22.237.0/24", - "17.22.245.0/24", - "17.22.253.0/24", - "17.241.75.0/24", - "17.241.219.0/24", - "17.241.227.0/24", - "17.246.15.0/24", - "17.246.19.0/24", - "17.246.23.0/24", - ] +- name: not-applebot + expression: + all: + - userAgent.matches("Applebot") + - '!(fcrdns.check("\\.applebot\\.apple\\.com$"))' + action: WEIGH + weight: + adjust: 5 diff --git a/data/crawlers/bingbot.yaml b/data/crawlers/bingbot.yaml index 2f7885dd..c78c0360 100644 --- a/data/crawlers/bingbot.yaml +++ b/data/crawlers/bingbot.yaml @@ -1,34 +1,15 @@ - name: bingbot - user_agent_regex: \+http\://www\.bing\.com/bingbot\.htm + # https://www.bing.com/webmasters/help/how-to-verify-bingbot-3905dc26 + expression: + all: + - userAgent.matches("\\+http\\://www\\.bing\\.com/bingbot\\.htm") + - fcrdns.check("\\.search\\.msn\\.com$") action: ALLOW - # https://www.bing.com/toolbox/bingbot.json - remote_addresses: [ - "157.55.39.0/24", - "207.46.13.0/24", - "40.77.167.0/24", - "13.66.139.0/24", - "13.66.144.0/24", - "52.167.144.0/24", - "13.67.10.16/28", - "13.69.66.240/28", - "13.71.172.224/28", - "139.217.52.0/28", - "191.233.204.224/28", - "20.36.108.32/28", - "20.43.120.16/28", - "40.79.131.208/28", - "40.79.186.176/28", - "52.231.148.0/28", - "20.79.107.240/28", - "51.105.67.0/28", - "20.125.163.80/28", - "40.77.188.0/22", - "65.55.210.0/24", - "199.30.24.0/23", - "40.77.202.0/24", - "40.77.139.0/25", - "20.74.197.0/28", - "20.15.133.160/27", - "40.77.177.0/24", - "40.77.178.0/23" - ] +- name: not-bingbot + expression: + all: + - userAgent.matches("\\+http\\://www\\.bing\\.com/bingbot\\.htm") + - '!(fcrdns.check("\\.search\\.msn\\.com$"))' + action: WEIGH + weight: + adjust: 5 diff --git a/data/crawlers/googlebot.yaml b/data/crawlers/googlebot.yaml index f1735126..f12dc8d4 100644 --- a/data/crawlers/googlebot.yaml +++ b/data/crawlers/googlebot.yaml @@ -1,263 +1,15 @@ - name: googlebot - user_agent_regex: \+http\://www\.google\.com/bot\.html + # https://developers.google.com/search/docs/crawling-indexing/verifying-googlebot + expression: + all: + - userAgent.matches("\\+http\\://www\\.google\\.com/bot\\.html") + - fcrdns.check("\\.googlebot\\.com$") action: ALLOW - # https://developers.google.com/static/search/apis/ipranges/googlebot.json - remote_addresses: [ - "2001:4860:4801:10::/64", - "2001:4860:4801:11::/64", - "2001:4860:4801:12::/64", - "2001:4860:4801:13::/64", - "2001:4860:4801:14::/64", - "2001:4860:4801:15::/64", - "2001:4860:4801:16::/64", - "2001:4860:4801:17::/64", - "2001:4860:4801:18::/64", - "2001:4860:4801:19::/64", - "2001:4860:4801:1a::/64", - "2001:4860:4801:1b::/64", - "2001:4860:4801:1c::/64", - "2001:4860:4801:1d::/64", - "2001:4860:4801:1e::/64", - "2001:4860:4801:1f::/64", - "2001:4860:4801:20::/64", - "2001:4860:4801:21::/64", - "2001:4860:4801:22::/64", - "2001:4860:4801:23::/64", - "2001:4860:4801:24::/64", - "2001:4860:4801:25::/64", - "2001:4860:4801:26::/64", - "2001:4860:4801:27::/64", - "2001:4860:4801:28::/64", - "2001:4860:4801:29::/64", - "2001:4860:4801:2::/64", - "2001:4860:4801:2a::/64", - "2001:4860:4801:2b::/64", - "2001:4860:4801:2c::/64", - "2001:4860:4801:2d::/64", - "2001:4860:4801:2e::/64", - "2001:4860:4801:2f::/64", - "2001:4860:4801:31::/64", - "2001:4860:4801:32::/64", - "2001:4860:4801:33::/64", - "2001:4860:4801:34::/64", - "2001:4860:4801:35::/64", - "2001:4860:4801:36::/64", - "2001:4860:4801:37::/64", - "2001:4860:4801:38::/64", - "2001:4860:4801:39::/64", - "2001:4860:4801:3a::/64", - "2001:4860:4801:3b::/64", - "2001:4860:4801:3c::/64", - "2001:4860:4801:3d::/64", - "2001:4860:4801:3e::/64", - "2001:4860:4801:40::/64", - "2001:4860:4801:41::/64", - "2001:4860:4801:42::/64", - "2001:4860:4801:43::/64", - "2001:4860:4801:44::/64", - "2001:4860:4801:45::/64", - "2001:4860:4801:46::/64", - "2001:4860:4801:47::/64", - "2001:4860:4801:48::/64", - "2001:4860:4801:49::/64", - "2001:4860:4801:4a::/64", - "2001:4860:4801:4b::/64", - "2001:4860:4801:4c::/64", - "2001:4860:4801:50::/64", - "2001:4860:4801:51::/64", - "2001:4860:4801:52::/64", - "2001:4860:4801:53::/64", - "2001:4860:4801:54::/64", - "2001:4860:4801:55::/64", - "2001:4860:4801:56::/64", - "2001:4860:4801:60::/64", - "2001:4860:4801:61::/64", - "2001:4860:4801:62::/64", - "2001:4860:4801:63::/64", - "2001:4860:4801:64::/64", - "2001:4860:4801:65::/64", - "2001:4860:4801:66::/64", - "2001:4860:4801:67::/64", - "2001:4860:4801:68::/64", - "2001:4860:4801:69::/64", - "2001:4860:4801:6a::/64", - "2001:4860:4801:6b::/64", - "2001:4860:4801:6c::/64", - "2001:4860:4801:6d::/64", - "2001:4860:4801:6e::/64", - "2001:4860:4801:6f::/64", - "2001:4860:4801:70::/64", - "2001:4860:4801:71::/64", - "2001:4860:4801:72::/64", - "2001:4860:4801:73::/64", - "2001:4860:4801:74::/64", - "2001:4860:4801:75::/64", - "2001:4860:4801:76::/64", - "2001:4860:4801:77::/64", - "2001:4860:4801:78::/64", - "2001:4860:4801:79::/64", - "2001:4860:4801:80::/64", - "2001:4860:4801:81::/64", - "2001:4860:4801:82::/64", - "2001:4860:4801:83::/64", - "2001:4860:4801:84::/64", - "2001:4860:4801:85::/64", - "2001:4860:4801:86::/64", - "2001:4860:4801:87::/64", - "2001:4860:4801:88::/64", - "2001:4860:4801:90::/64", - "2001:4860:4801:91::/64", - "2001:4860:4801:92::/64", - "2001:4860:4801:93::/64", - "2001:4860:4801:94::/64", - "2001:4860:4801:95::/64", - "2001:4860:4801:96::/64", - "2001:4860:4801:a0::/64", - "2001:4860:4801:a1::/64", - "2001:4860:4801:a2::/64", - "2001:4860:4801:a3::/64", - "2001:4860:4801:a4::/64", - "2001:4860:4801:a5::/64", - "2001:4860:4801:c::/64", - "2001:4860:4801:f::/64", - "192.178.5.0/27", - "192.178.6.0/27", - "192.178.6.128/27", - "192.178.6.160/27", - "192.178.6.192/27", - "192.178.6.32/27", - "192.178.6.64/27", - "192.178.6.96/27", - "34.100.182.96/28", - "34.101.50.144/28", - "34.118.254.0/28", - "34.118.66.0/28", - "34.126.178.96/28", - "34.146.150.144/28", - "34.147.110.144/28", - "34.151.74.144/28", - "34.152.50.64/28", - "34.154.114.144/28", - "34.155.98.32/28", - "34.165.18.176/28", - "34.175.160.64/28", - "34.176.130.16/28", - "34.22.85.0/27", - "34.64.82.64/28", - "34.65.242.112/28", - "34.80.50.80/28", - "34.88.194.0/28", - "34.89.10.80/28", - "34.89.198.80/28", - "34.96.162.48/28", - "35.247.243.240/28", - "66.249.64.0/27", - "66.249.64.128/27", - "66.249.64.160/27", - "66.249.64.224/27", - "66.249.64.32/27", - "66.249.64.64/27", - "66.249.64.96/27", - "66.249.65.0/27", - "66.249.65.128/27", - "66.249.65.160/27", - "66.249.65.192/27", - "66.249.65.224/27", - "66.249.65.32/27", - "66.249.65.64/27", - "66.249.65.96/27", - "66.249.66.0/27", - "66.249.66.128/27", - "66.249.66.160/27", - "66.249.66.192/27", - "66.249.66.224/27", - "66.249.66.32/27", - "66.249.66.64/27", - "66.249.66.96/27", - "66.249.68.0/27", - "66.249.68.128/27", - "66.249.68.32/27", - "66.249.68.64/27", - "66.249.68.96/27", - "66.249.69.0/27", - "66.249.69.128/27", - "66.249.69.160/27", - "66.249.69.192/27", - "66.249.69.224/27", - "66.249.69.32/27", - "66.249.69.64/27", - "66.249.69.96/27", - "66.249.70.0/27", - "66.249.70.128/27", - "66.249.70.160/27", - "66.249.70.192/27", - "66.249.70.224/27", - "66.249.70.32/27", - "66.249.70.64/27", - "66.249.70.96/27", - "66.249.71.0/27", - "66.249.71.128/27", - "66.249.71.160/27", - "66.249.71.192/27", - "66.249.71.224/27", - "66.249.71.32/27", - "66.249.71.64/27", - "66.249.71.96/27", - "66.249.72.0/27", - "66.249.72.128/27", - "66.249.72.160/27", - "66.249.72.192/27", - "66.249.72.224/27", - "66.249.72.32/27", - "66.249.72.64/27", - "66.249.72.96/27", - "66.249.73.0/27", - "66.249.73.128/27", - "66.249.73.160/27", - "66.249.73.192/27", - "66.249.73.224/27", - "66.249.73.32/27", - "66.249.73.64/27", - "66.249.73.96/27", - "66.249.74.0/27", - "66.249.74.128/27", - "66.249.74.160/27", - "66.249.74.192/27", - "66.249.74.32/27", - "66.249.74.64/27", - "66.249.74.96/27", - "66.249.75.0/27", - "66.249.75.128/27", - "66.249.75.160/27", - "66.249.75.192/27", - "66.249.75.224/27", - "66.249.75.32/27", - "66.249.75.64/27", - "66.249.75.96/27", - "66.249.76.0/27", - "66.249.76.128/27", - "66.249.76.160/27", - "66.249.76.192/27", - "66.249.76.224/27", - "66.249.76.32/27", - "66.249.76.64/27", - "66.249.76.96/27", - "66.249.77.0/27", - "66.249.77.128/27", - "66.249.77.160/27", - "66.249.77.192/27", - "66.249.77.224/27", - "66.249.77.32/27", - "66.249.77.64/27", - "66.249.77.96/27", - "66.249.78.0/27", - "66.249.78.32/27", - "66.249.79.0/27", - "66.249.79.128/27", - "66.249.79.160/27", - "66.249.79.192/27", - "66.249.79.224/27", - "66.249.79.32/27", - "66.249.79.64/27", - "66.249.79.96/27" - ] +- name: not-googlebot + expression: + all: + - userAgent.matches("\\+http\\://www\\.google\\.com/bot\\.html") + - '!(fcrdns.check("\\.googlebot\\.com$"))' + action: WEIGH + weight: + adjust: 5 diff --git a/data/crawlers/mojeekbot.yaml b/data/crawlers/mojeekbot.yaml index 40661203..82d0c69a 100644 --- a/data/crawlers/mojeekbot.yaml +++ b/data/crawlers/mojeekbot.yaml @@ -1,5 +1,15 @@ - name: mojeekbot - user_agent_regex: \+https\://www\.mojeek\.com/bot\.html - action: ALLOW # https://www.mojeek.com/bot.html - remote_addresses: [ "5.102.173.71/32" ] \ No newline at end of file + expression: + all: + - userAgent.matches("\\+https\\://www\\.mojeek\\.com/bot\\.html") + - fcrdns.check("\\.mojeek\\.com$") + action: ALLOW +- name: not-mojeekbot + expression: + all: + - userAgent.matches("\\+https\\://www\\.mojeek\\.com/bot\\.html") + - '!(fcrdns.check("\\.mojeek\\.com$"))' + action: WEIGH + weight: + adjust: 5 diff --git a/data/crawlers/qwantbot.yaml b/data/crawlers/qwantbot.yaml index a4021549..271f2597 100644 --- a/data/crawlers/qwantbot.yaml +++ b/data/crawlers/qwantbot.yaml @@ -1,5 +1,16 @@ - name: qwantbot - user_agent_regex: \+https\://help\.qwant\.com/bot/ + # https://help.qwant.com/bot/ + expression: + all: + - userAgent.matches("\\+https\\://help\\.qwant\\.com/bot/") + - fcrdns.check("\\.qwant\\.com$") action: ALLOW - # https://help.qwant.com/wp-content/uploads/sites/2/2025/01/qwantbot.json - remote_addresses: [ "91.242.162.0/24" ] +- name: not-qwantbot + # https://help.qwant.com/bot/ + expression: + all: + - userAgent.matches("\\+https\\://help\\.qwant\\.com/bot/") + - '!(fcrdns.check("\\.qwant\\.com$"))' + action: WEIGH + weight: + adjust: 5 diff --git a/docs/docs/CHANGELOG.md b/docs/docs/CHANGELOG.md index 519d49d0..a603a243 100644 --- a/docs/docs/CHANGELOG.md +++ b/docs/docs/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Implement a [forward-confirming reverse DNS-based check method](./admin/policies.mdx#reverse-dns-based-filtering) and [expression method](./admin/configuration/expressions.mdx#fcrdnscheck) ([#431](https://github.com/TecharoHQ/anubis/issues/431)) +- Add default rules for increasing the weight of clients that pretend to be search engines but are not valid +- Fix OpenGraph passthrough ([#717](https://github.com/TecharoHQ/anubis/issues/717)) - Determine the `BIND_NETWORK`/`--bind-network` value from the bind address ([#677](https://github.com/TecharoHQ/anubis/issues/677)) - Implement localization system. Find locale files in lib/localization/locales/. diff --git a/docs/docs/admin/configuration/expressions.mdx b/docs/docs/admin/configuration/expressions.mdx index 0786c220..e210173a 100644 --- a/docs/docs/admin/configuration/expressions.mdx +++ b/docs/docs/admin/configuration/expressions.mdx @@ -108,6 +108,7 @@ Anubis exposes the following variables to expressions: | `query` | `map[string, string]` | The [query parameters](https://web.dev/articles/url-parts#query) of the request being processed. | `?foo=bar` -> `{"foo": "bar"}` | | `remoteAddress` | `string` | The IP address of the client. | `1.1.1.1` | | `userAgent` | `string` | The [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/User-Agent) string in the request being processed. | `Mozilla/5.0 Gecko/20100101 Firefox/137.0` | +| `fcrdns` | `fcrdns` | Custom object for performing the forward-confirmed reverse DNS check. | `fcrdns.check("\\.qwant\\.com$")` | Of note: in many languages when you look up a key in a map and there is nothing there, the language will return some "falsy" value like `undefined` in JavaScript, `None` in Python, or the zero value of the type in Go. In CEL, if you try to look up a value that does not exist, execution of the expression will fail and Anubis will return an error. @@ -167,6 +168,26 @@ This is best applied when doing explicit block rules, eg: It seems counter-intuitive to allow known bad clients through sometimes, but this allows you to confuse attackers by making Anubis' behavior random. Adjust the thresholds and numbers as facts and circumstances demand. +### `fcrdns.check` + +```ts +function check(pattern: string): bool; +``` + +`fcrdns.check` accepts a regex string and performs the same check as the `domain_regex` field. For more details, read [Reverse DNS based filtering](../policies#reverse-dns-based-filtering). + +Example for search engine bots: + +```yaml +- name: qwantbot + # https://help.qwant.com/bot/ + expression: + all: + - userAgent.matches("\\+https\\://help\\.qwant\\.com/bot/") + - fcrdns.check("\\.qwant\\.com$") + action: ALLOW +``` + ## Life advice Expressions are very powerful. This is a benefit and a burden. If you are not careful with your expression targeting, you will be liable to get yourself into trouble. If you are at all in doubt, throw a `CHALLENGE` over a `DENY`. Legitimate users can easily work around a `CHALLENGE` result with a [proof of work challenge](../../design/why-proof-of-work.mdx). Bots are less likely to be able to do this. diff --git a/docs/docs/admin/policies.mdx b/docs/docs/admin/policies.mdx index 4633cdeb..9ba0326b 100644 --- a/docs/docs/admin/policies.mdx +++ b/docs/docs/admin/policies.mdx @@ -233,6 +233,41 @@ remote_addresses: +### Reverse DNS based filtering + +The `domain_regex` field can be used to verify some legitimate bots using DNS and the client IP address. It works in three steps: + +1. Reverse DNS records for the client IP are looked up. +2. Any records that match the `domain_regex` are queried. +3. The client passes the challenge if the domain has an `A` or `AAAA` record that matches the client IP. + +You will need to look at the bot owner's website to find out what the reverse DNS of legitimate requests should look like. Use `remote_addresses` instead if the bot you are working with cannot be identified with reverse DNS. It is recommended to use an `expression` with [`fcrdns.check`](./configuration/expressions.mdx#fcrdnscheck) instead to avoid unnecessary DNS requests if you are making a rule for search engine bots. + +Example: + + + + +```json +{ + "name": "deny-bad-isp", + "action": "DENY", + "domain_regex": "\\.bad-isp\\.com$" +} +``` + + + + +```yaml +- name: deny-bad-isp + action: DENY + domain_regex: \.bad-isp\.com$ +``` + + + + ## Imprint / Impressum support Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information. diff --git a/internal/fcrdns/context.go b/internal/fcrdns/context.go new file mode 100644 index 00000000..61073344 --- /dev/null +++ b/internal/fcrdns/context.go @@ -0,0 +1,14 @@ +package fcrdns + +import "context" + +type ctxKey struct{} + +func With(ctx context.Context, fcrdns *FCrDNS) context.Context { + return context.WithValue(ctx, ctxKey{}, fcrdns) +} + +func FromContext(ctx context.Context) (*FCrDNS, bool) { + cli, ok := ctx.Value(ctxKey{}).(*FCrDNS) + return cli, ok +} diff --git a/internal/fcrdns/fcrdns.go b/internal/fcrdns/fcrdns.go new file mode 100644 index 00000000..d2d8fd01 --- /dev/null +++ b/internal/fcrdns/fcrdns.go @@ -0,0 +1,107 @@ +package fcrdns + +import ( + "context" + "net" + "net/netip" + "regexp" + "strings" + "time" + + "github.com/TecharoHQ/anubis/decaymap" +) + +type FCrDNS struct { + resolver *net.Resolver + forwardLookupCacheV4 *decaymap.Impl[string, []netip.Addr] + forwardLookupCacheV6 *decaymap.Impl[string, []netip.Addr] + reverseLookupCache *decaymap.Impl[string, []string] +} + +func NewFCrDNS() *FCrDNS { + return &FCrDNS{ + resolver: &net.Resolver{}, + forwardLookupCacheV4: decaymap.New[string, []netip.Addr](), + forwardLookupCacheV6: decaymap.New[string, []netip.Addr](), + reverseLookupCache: decaymap.New[string, []string](), + } +} + +func (f *FCrDNS) Check(ip string, allowedDomainRegex *regexp.Regexp) (bool, error) { + clientAddr, err := netip.ParseAddr(ip) + if err != nil { + return false, err + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + hosts, err := f.reverseLookup(ctx, ip) + if err != nil { + return false, err + } + + for _, host := range hosts { + if !allowedDomainRegex.MatchString(host) { + continue + } + + addresses, err := f.forwardLookup(ctx, host, clientAddr.Is6()) + if err != nil { + return false, err + } + for _, addr := range addresses { + if addr == clientAddr { + return true, nil + } + } + } + + return false, nil +} + +func (f *FCrDNS) Cleanup() { + f.forwardLookupCacheV4.Cleanup() + f.forwardLookupCacheV6.Cleanup() + f.reverseLookupCache.Cleanup() +} + +func (f *FCrDNS) reverseLookup(ctx context.Context, addr string) ([]string, error) { + if result, ok := f.reverseLookupCache.Get(addr); ok { + return result, nil + } + + rawHosts, err := f.resolver.LookupAddr(ctx, addr) + if err != nil { + return []string{}, err + } + + hosts := []string{} + for _, host := range rawHosts { + hosts = append(hosts, strings.TrimSuffix(host, ".")) + } + + f.reverseLookupCache.Set(addr, hosts, time.Hour) + return hosts, nil +} + +func (f *FCrDNS) forwardLookup(ctx context.Context, host string, ipv6 bool) ([]netip.Addr, error) { + cache := f.forwardLookupCacheV4 + network := "ip4" + if ipv6 { + cache = f.forwardLookupCacheV6 + network = "ip6" + } + + if result, ok := cache.Get(host); ok { + return result, nil + } + + result, err := f.resolver.LookupNetIP(ctx, network, host) + if err != nil { + return []netip.Addr{}, err + } + + cache.Set(host, result, time.Hour) + return result, nil +} diff --git a/internal/fcrdns/fcrdns_test.go b/internal/fcrdns/fcrdns_test.go new file mode 100644 index 00000000..04500c71 --- /dev/null +++ b/internal/fcrdns/fcrdns_test.go @@ -0,0 +1,97 @@ +package fcrdns + +import ( + "net/netip" + "regexp" + "testing" + "time" +) + +func TestFCrDNSCheck(t *testing.T) { + localhostRegex := regexp.MustCompile("^localhost$") + localhost := netip.MustParseAddr("127.0.0.1") + localhostV6 := netip.MustParseAddr("::1") + + tests := []struct { + name string + clientIp string + host string + hostIp netip.Addr + regexp *regexp.Regexp + expected bool + }{ + { + name: "IPv4", + clientIp: "127.0.0.1", + host: "localhost", + hostIp: localhost, + regexp: localhostRegex, + expected: true, + }, + { + name: "IPv6", + clientIp: "::1", + host: "localhost", + hostIp: localhostV6, + regexp: localhostRegex, + expected: true, + }, + { + name: "No regexp match", + clientIp: "127.0.0.1", + host: "localhost", + hostIp: localhost, + regexp: regexp.MustCompile("^remotehost$"), + expected: false, + }, + { + name: "No reverse DNS record", + clientIp: "127.0.0.1", + regexp: localhostRegex, + expected: false, + }, + { + name: "No forward DNS record", + clientIp: "127.0.0.1", + host: "localhost", + regexp: localhostRegex, + expected: false, + }, + { + name: "IP mismatch", + clientIp: "127.0.0.1", + host: "localhost", + hostIp: netip.IPv4Unspecified(), + regexp: localhostRegex, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := NewFCrDNS() + f.resolver = nil // There shouldn't be any network real requests for these tests. + + if tt.host != "" { + f.reverseLookupCache.Set(tt.clientIp, []string{tt.host}, time.Hour) + + if !tt.hostIp.IsValid() { + f.forwardLookupCacheV4.Set(tt.host, []netip.Addr{}, time.Hour) + f.forwardLookupCacheV6.Set(tt.host, []netip.Addr{}, time.Hour) + } else if tt.hostIp.Is6() { + f.forwardLookupCacheV6.Set(tt.host, []netip.Addr{tt.hostIp}, time.Hour) + } else { + f.forwardLookupCacheV4.Set(tt.host, []netip.Addr{tt.hostIp}, time.Hour) + } + } else { + f.reverseLookupCache.Set(tt.clientIp, []string{}, time.Hour) + } + + if ok, err := f.Check(tt.clientIp, tt.regexp); err != nil { + t.Errorf("unexpected error: %v", err) + } else if ok != tt.expected { + t.Errorf("expected: %t, got: %t", tt.expected, ok) + } + }) + } +} diff --git a/lib/anubis.go b/lib/anubis.go index 940be040..434af928 100644 --- a/lib/anubis.go +++ b/lib/anubis.go @@ -24,6 +24,7 @@ import ( "github.com/TecharoHQ/anubis/decaymap" "github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal/dnsbl" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/localization" @@ -69,6 +70,7 @@ type Server struct { policy *policy.ParsedConfig DNSBLCache *decaymap.Impl[string, dnsbl.DroneBLResponse] OGTags *ogtags.OGTagCache + FCrDNS *fcrdns.FCrDNS cookieName string ed25519Priv ed25519.PrivateKey hs512Secret []byte @@ -500,4 +502,5 @@ func (s *Server) check(r *http.Request) (policy.CheckResult, *policy.Bot, error) func (s *Server) CleanupDecayMap() { s.DNSBLCache.Cleanup() s.OGTags.Cleanup() + s.FCrDNS.Cleanup() } diff --git a/lib/anubis_test.go b/lib/anubis_test.go index e3089a84..f76a5c6f 100644 --- a/lib/anubis_test.go +++ b/lib/anubis_test.go @@ -15,6 +15,7 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/data" "github.com/TecharoHQ/anubis/internal" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/internal/thoth/thothmock" "github.com/TecharoHQ/anubis/lib/policy" "github.com/TecharoHQ/anubis/lib/policy/config" @@ -29,6 +30,9 @@ func loadPolicies(t *testing.T, fname string, difficulty int) *policy.ParsedConf ctx := thothmock.WithMockThoth(t) + fdns := fcrdns.NewFCrDNS() + ctx = fcrdns.With(ctx, fdns) + if fname == "" { fname = "./testdata/test_config.yaml" } @@ -175,7 +179,10 @@ func TestLoadPolicies(t *testing.T) { } defer fin.Close() - if _, err := policy.ParseConfig(t.Context(), fin, fname, 4); err != nil { + fdns := fcrdns.NewFCrDNS() + ctx := fcrdns.With(t.Context(), fdns) + + if _, err := policy.ParseConfig(ctx, fin, fname, 4); err != nil { t.Fatal(err) } }) diff --git a/lib/config.go b/lib/config.go index 8041da85..25ad3223 100644 --- a/lib/config.go +++ b/lib/config.go @@ -18,6 +18,7 @@ import ( "github.com/TecharoHQ/anubis/decaymap" "github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal/dnsbl" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/internal/ogtags" "github.com/TecharoHQ/anubis/lib/challenge" "github.com/TecharoHQ/anubis/lib/localization" @@ -30,6 +31,7 @@ import ( type Options struct { Next http.Handler + FCrDNS *fcrdns.FCrDNS Policy *policy.ParsedConfig Target string CookieDynamicDomain bool @@ -116,6 +118,7 @@ func New(opts Options) (*Server, error) { opts: opts, DNSBLCache: decaymap.New[string, dnsbl.DroneBLResponse](), OGTags: ogtags.NewOGTagCache(opts.Target, opts.Policy.OpenGraph), + FCrDNS: opts.FCrDNS, cookieName: cookieName, } diff --git a/lib/config_test.go b/lib/config_test.go index 71cb7347..711a32ad 100644 --- a/lib/config_test.go +++ b/lib/config_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/TecharoHQ/anubis" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/internal/thoth/thothmock" "github.com/TecharoHQ/anubis/lib/policy" ) @@ -26,7 +27,10 @@ func TestBadConfigs(t *testing.T) { for _, st := range finfos { st := st t.Run(st.Name(), func(t *testing.T) { - if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("policy", "config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty); err == nil { + fdns := fcrdns.NewFCrDNS() + ctx := fcrdns.With(t.Context(), fdns) + + if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("policy", "config", "testdata", "bad", st.Name()), anubis.DefaultDifficulty); err == nil { t.Fatal(err) } else { t.Log(err) @@ -46,13 +50,17 @@ func TestGoodConfigs(t *testing.T) { t.Run(st.Name(), func(t *testing.T) { t.Run("with-thoth", func(t *testing.T) { ctx := thothmock.WithMockThoth(t) + fdns := fcrdns.NewFCrDNS() + ctx = fcrdns.With(ctx, fdns) if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err != nil { t.Fatal(err) } }) t.Run("without-thoth", func(t *testing.T) { - if _, err := LoadPoliciesOrDefault(t.Context(), filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err != nil { + fdns := fcrdns.NewFCrDNS() + ctx := fcrdns.With(t.Context(), fdns) + if _, err := LoadPoliciesOrDefault(ctx, filepath.Join("policy", "config", "testdata", "good", st.Name()), anubis.DefaultDifficulty); err != nil { t.Fatal(err) } }) diff --git a/lib/policy/bot.go b/lib/policy/bot.go index 479bccc3..d7556fe9 100644 --- a/lib/policy/bot.go +++ b/lib/policy/bot.go @@ -2,6 +2,7 @@ package policy import ( "fmt" + "regexp" "github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/lib/policy/checker" @@ -9,11 +10,12 @@ import ( ) type Bot struct { - Rules checker.Impl - Challenge *config.ChallengeRules - Weight *config.Weight - Name string - Action config.Rule + Rules checker.Impl + Challenge *config.ChallengeRules + Weight *config.Weight + Name string + Action config.Rule + DomainRegex *regexp.Regexp } func (b Bot) Hash() string { diff --git a/lib/policy/celchecker.go b/lib/policy/celchecker.go index c2cc3356..ada2e623 100644 --- a/lib/policy/celchecker.go +++ b/lib/policy/celchecker.go @@ -5,6 +5,7 @@ import ( "net/http" "github.com/TecharoHQ/anubis/internal" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/lib/policy/config" "github.com/TecharoHQ/anubis/lib/policy/expressions" "github.com/google/cel-go/cel" @@ -13,10 +14,11 @@ import ( type CELChecker struct { program cel.Program + fcrdns *fcrdns.FCrDNS src string } -func NewCELChecker(cfg *config.ExpressionOrList) (*CELChecker, error) { +func NewCELChecker(cfg *config.ExpressionOrList, fcrdns *fcrdns.FCrDNS) (*CELChecker, error) { env, err := expressions.BotEnvironment() if err != nil { return nil, err @@ -28,8 +30,9 @@ func NewCELChecker(cfg *config.ExpressionOrList) (*CELChecker, error) { } return &CELChecker{ - src: cfg.String(), - program: program, + program, + fcrdns, + cfg.String(), }, nil } @@ -38,7 +41,7 @@ func (cc *CELChecker) Hash() string { } func (cc *CELChecker) Check(r *http.Request) (bool, error) { - result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r}) + result, _, err := cc.program.ContextEval(r.Context(), &CELRequest{r, cc.fcrdns}) if err != nil { return false, err @@ -53,6 +56,7 @@ func (cc *CELChecker) Check(r *http.Request) (bool, error) { type CELRequest struct { *http.Request + fcrdns *fcrdns.FCrDNS } func (cr *CELRequest) Parent() cel.Activation { return nil } @@ -73,6 +77,8 @@ func (cr *CELRequest) ResolveName(name string) (any, bool) { return expressions.URLValues{Values: cr.URL.Query()}, true case "headers": return expressions.HTTPHeaders{Header: cr.Header}, true + case "fcrdns": + return expressions.FCrDNS{FCrDNS: cr.fcrdns, Ip: cr.Header.Get("X-Real-Ip")}, true default: return nil, false } diff --git a/lib/policy/checker.go b/lib/policy/checker.go index 5753e144..a0048807 100644 --- a/lib/policy/checker.go +++ b/lib/policy/checker.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/TecharoHQ/anubis/internal" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/gaissmai/bart" ) @@ -158,3 +159,30 @@ func NewHeadersChecker(headermap map[string]string) (checker.Impl, error) { return result, nil } + +type FCrDNSChecker struct { + fcrdns *fcrdns.FCrDNS + regexp *regexp.Regexp + hash string +} + +func NewFCrDNSChecker(fcrdns *fcrdns.FCrDNS, domainRexStr string) (checker.Impl, error) { + rex, err := regexp.Compile(strings.TrimSpace(domainRexStr)) + if err != nil { + return nil, fmt.Errorf("%w: regex %s failed parse: %w", ErrMisconfiguration, domainRexStr, err) + } + return &FCrDNSChecker{fcrdns, rex, internal.FastHash(domainRexStr)}, nil +} + +func (fdc *FCrDNSChecker) Check(r *http.Request) (bool, error) { + host := r.Header.Get("X-Real-Ip") + if host == "" { + return false, fmt.Errorf("%w: header X-Real-Ip is not set", ErrMisconfiguration) + } + + return fdc.fcrdns.Check(host, fdc.regexp) +} + +func (fdc *FCrDNSChecker) Hash() string { + return fdc.hash +} diff --git a/lib/policy/config/config.go b/lib/policy/config/config.go index 18aceb09..3db5439e 100644 --- a/lib/policy/config/config.go +++ b/lib/policy/config/config.go @@ -25,6 +25,7 @@ var ( ErrInvalidUserAgentRegex = errors.New("config.Bot: invalid user agent regex") ErrInvalidPathRegex = errors.New("config.Bot: invalid path regex") ErrInvalidHeadersRegex = errors.New("config.Bot: invalid headers regex") + ErrInvalidDomainRegex = errors.New("config.Bot: invalid domain regex") ErrInvalidCIDR = errors.New("config.Bot: invalid CIDR") ErrRegexEndsWithNewline = errors.New("config.Bot: regular expression ends with newline (try >- instead of > in yaml)") ErrInvalidImportStatement = errors.New("config.ImportStatement: invalid source file") @@ -59,6 +60,7 @@ type BotConfig struct { UserAgentRegex *string `json:"user_agent_regex,omitempty" yaml:"user_agent_regex,omitempty"` PathRegex *string `json:"path_regex,omitempty" yaml:"path_regex,omitempty"` HeadersRegex map[string]string `json:"headers_regex,omitempty" yaml:"headers_regex,omitempty"` + DomainRegex *string `json:"domain_regex,omitempty" yaml:"domain_regex,omitempty"` Expression *ExpressionOrList `json:"expression,omitempty" yaml:"expression,omitempty"` Challenge *ChallengeRules `json:"challenge,omitempty" yaml:"challenge,omitempty"` Weight *Weight `json:"weight,omitempty" yaml:"weight,omitempty"` @@ -80,6 +82,7 @@ func (b BotConfig) Zero() bool { b.Action != "", len(b.RemoteAddr) != 0, b.Challenge != nil, + b.DomainRegex != nil, b.GeoIP != nil, b.ASNs != nil, } { @@ -156,7 +159,11 @@ func (b *BotConfig) Valid() error { } } } - + if b.DomainRegex != nil { + if _, err := regexp.Compile(*b.DomainRegex); err != nil { + errs = append(errs, ErrInvalidDomainRegex, err) + } + } if b.Expression != nil { if err := b.Expression.Valid(); err != nil { errs = append(errs, err) diff --git a/lib/policy/config/config_test.go b/lib/policy/config/config_test.go index 730b3d66..bb716d1a 100644 --- a/lib/policy/config/config_test.go +++ b/lib/policy/config/config_test.go @@ -167,6 +167,16 @@ func TestBotValid(t *testing.T) { }, err: nil, }, + { + name: "reverse dns", + bot: BotConfig{ + Name: "search-bot", + Action: RuleChallenge, + UserAgentRegex: p("SearchBot"), + DomainRegex: p("example.com"), + }, + err: nil, + }, { name: "weight rule without weight", bot: BotConfig{ diff --git a/lib/policy/expressions/environment.go b/lib/policy/expressions/environment.go index 6f46377a..7ea3febf 100644 --- a/lib/policy/expressions/environment.go +++ b/lib/policy/expressions/environment.go @@ -2,11 +2,13 @@ package expressions import ( "math/rand/v2" + "regexp" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/ext" + "github.com/google/cel-go/interpreter" ) // BotEnvironment creates a new CEL environment, this is the set of @@ -15,6 +17,7 @@ import ( // of blowing up at runtime. func BotEnvironment() (*cel.Env, error) { return New( + cel.Types(FCrDNSType), // Variables exposed to CEL programs: cel.Variable("remoteAddress", cel.StringType), cel.Variable("host", cel.StringType), @@ -23,6 +26,20 @@ func BotEnvironment() (*cel.Env, error) { cel.Variable("path", cel.StringType), cel.Variable("query", cel.MapType(cel.StringType, cel.StringType)), cel.Variable("headers", cel.MapType(cel.StringType, cel.StringType)), + cel.Variable("fcrdns", FCrDNSType), + cel.Function("check", cel.MemberOverload("fcrdns_check_string", []*cel.Type{FCrDNSType, cel.StringType}, cel.BoolType, + cel.BinaryBinding(func(lhs, rhs ref.Val) ref.Val { + f, ok := lhs.Value().(FCrDNS) + if !ok { + return types.ValOrErr(types.False, "receiver is not a fcrdns instance, but is %T", lhs) + } + pattern, ok := rhs.Value().(string) + if !ok { + return types.ValOrErr(types.False, "value is not a string, but is %T", rhs) + } + + return f.check(pattern) + }))), ) } @@ -64,6 +81,29 @@ func New(opts ...cel.EnvOption) (*cel.Env, error) { return cel.NewEnv(args...) } +var fcrdnsRegexOptimization = &interpreter.RegexOptimization{ + Function: "check", + RegexIndex: 1, + Factory: func(call interpreter.InterpretableCall, regexPattern string) (interpreter.InterpretableCall, error) { + compiledRegex, err := regexp.Compile(regexPattern) + if err != nil { + return nil, err + } + + return interpreter.NewCall(call.ID(), call.Function(), call.OverloadID(), call.Args(), func(values ...ref.Val) ref.Val { + if len(values) != 2 { + return types.NoSuchOverloadErr() + } + + in, ok := values[0].Value().(FCrDNS) + if !ok { + return types.NoSuchOverloadErr() + } + return in.checkOptimized(compiledRegex) + }), nil + }, +} + // Compile takes CEL environment and syntax tree then emits an optimized // Program for execution. func Compile(env *cel.Env, src string) (cel.Program, error) { @@ -79,8 +119,9 @@ func Compile(env *cel.Env, src string) (cel.Program, error) { return env.Program( ast, + // optimize regular expressions right now instead of on the fly + cel.OptimizeRegex(interpreter.MatchesRegexOptimization, fcrdnsRegexOptimization), cel.EvalOptions( - // optimize regular expressions right now instead of on the fly cel.OptOptimize, ), ) diff --git a/lib/policy/expressions/fcrdns.go b/lib/policy/expressions/fcrdns.go new file mode 100644 index 00000000..d45ffcd1 --- /dev/null +++ b/lib/policy/expressions/fcrdns.go @@ -0,0 +1,64 @@ +package expressions + +import ( + "reflect" + "regexp" + + "github.com/TecharoHQ/anubis/internal/fcrdns" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" +) + +var FCrDNSType *types.Type = types.NewObjectType("fcrdns") + +// FCrDNS is a type to expose the FCrDNS test to CEL programs. +type FCrDNS struct { + FCrDNS *fcrdns.FCrDNS + Ip string +} + +func (f FCrDNS) ConvertToNative(typeDesc reflect.Type) (any, error) { + return nil, ErrNotImplemented +} + +func (f FCrDNS) ConvertToType(typeVal ref.Type) ref.Val { + switch typeVal { + case FCrDNSType: + return f + case types.TypeType: + return FCrDNSType + } + + return types.NewErr("can't convert from %q to %q", FCrDNSType, typeVal) +} + +func (f FCrDNS) Equal(other ref.Val) ref.Val { + return types.Bool(false) // We don't want to compare FCrDNS instances +} + +func (f FCrDNS) Type() ref.Type { + return FCrDNSType +} + +func (f FCrDNS) Value() any { return f } + +func (f FCrDNS) check(pattern string) ref.Val { + rex, err := regexp.Compile(pattern) + if err != nil { + return types.ValOrErr(types.False, "%w: Failed to compiler regexp pattern %s", err, pattern) + } + return f.checkOptimized(rex) +} + +func (f FCrDNS) checkOptimized(pattern *regexp.Regexp) ref.Val { + if f.Ip == "" { + return types.ValOrErr(types.False, "header X-Real-Ip is not set") + } + + res, err := f.FCrDNS.Check(f.Ip, pattern) + v := types.Bool(res) + if err != nil { + return types.ValOrErr(v, "%w: FCrDNS check failed", err) + } + return v +} diff --git a/lib/policy/policy.go b/lib/policy/policy.go index 9ee6efcb..b398ef1f 100644 --- a/lib/policy/policy.go +++ b/lib/policy/policy.go @@ -8,6 +8,7 @@ import ( "log/slog" "sync/atomic" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/internal/thoth" "github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/TecharoHQ/anubis/lib/policy/config" @@ -54,6 +55,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic var validationErrs []error tc, hasThothClient := thoth.FromContext(ctx) + fcrdns, hasFCrDNS := fcrdns.FromContext(ctx) result := NewParsedConfig(c) result.DefaultDifficulty = defaultDifficulty @@ -108,8 +110,9 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic } if b.Expression != nil { - c, err := NewCELChecker(b.Expression) - if err != nil { + if !hasFCrDNS { + validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s: no FCrDNS client in the context. This is a bug", b.Name)) + } else if c, err := NewCELChecker(b.Expression, fcrdns); err != nil { validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err)) } else { cl = append(cl, c) @@ -134,6 +137,16 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic cl = append(cl, tc.GeoIPCheckerFor(b.GeoIP.Countries)) } + if b.DomainRegex != nil { + if !hasFCrDNS { + validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s: no FCrDNS client in the context. This is a bug", b.Name)) + } else if c, err := NewFCrDNSChecker(fcrdns, *b.DomainRegex); err != nil { + validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s domain regex: %w", b.Name, err)) + } else { + cl = append(cl, c) + } + } + if b.Challenge == nil { parsedBot.Challenge = &config.ChallengeRules{ Difficulty: defaultDifficulty, diff --git a/lib/policy/policy_test.go b/lib/policy/policy_test.go index 9ada1c95..a5451bf9 100644 --- a/lib/policy/policy_test.go +++ b/lib/policy/policy_test.go @@ -7,11 +7,14 @@ import ( "github.com/TecharoHQ/anubis" "github.com/TecharoHQ/anubis/data" + "github.com/TecharoHQ/anubis/internal/fcrdns" "github.com/TecharoHQ/anubis/internal/thoth/thothmock" ) func TestDefaultPolicyMustParse(t *testing.T) { ctx := thothmock.WithMockThoth(t) + fdns := fcrdns.NewFCrDNS() + ctx = fcrdns.With(ctx, fdns) fin, err := data.BotPolicies.Open("botPolicies.json") if err != nil { @@ -25,7 +28,6 @@ func TestDefaultPolicyMustParse(t *testing.T) { } func TestGoodConfigs(t *testing.T) { - finfos, err := os.ReadDir("config/testdata/good") if err != nil { t.Fatal(err) @@ -42,6 +44,8 @@ func TestGoodConfigs(t *testing.T) { defer fin.Close() ctx := thothmock.WithMockThoth(t) + fdns := fcrdns.NewFCrDNS() + ctx = fcrdns.With(ctx, fdns) if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty); err != nil { t.Fatal(err) } @@ -54,7 +58,10 @@ func TestGoodConfigs(t *testing.T) { } defer fin.Close() - if _, err := ParseConfig(t.Context(), fin, fin.Name(), anubis.DefaultDifficulty); err != nil { + fdns := fcrdns.NewFCrDNS() + ctx := fcrdns.With(t.Context(), fdns) + + if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty); err != nil { t.Fatal(err) } }) @@ -63,8 +70,6 @@ func TestGoodConfigs(t *testing.T) { } func TestBadConfigs(t *testing.T) { - ctx := thothmock.WithMockThoth(t) - finfos, err := os.ReadDir("config/testdata/bad") if err != nil { t.Fatal(err) @@ -79,6 +84,10 @@ func TestBadConfigs(t *testing.T) { } defer fin.Close() + ctx := thothmock.WithMockThoth(t) + fdns := fcrdns.NewFCrDNS() + ctx = fcrdns.With(ctx, fdns) + if _, err := ParseConfig(ctx, fin, fin.Name(), anubis.DefaultDifficulty); err == nil { t.Fatal(err) } else {