Compare commits

...

7 Commits

Author SHA1 Message Date
Jason Cameron
51e63bad25 fix: pin Node.js and Go versions in CI configuration files 2025-11-30 21:58:20 -05:00
dependabot[bot]
21d7753b1c build(deps): bump actions-hub/kubectl in the github-actions group (#1303)
Bumps the github-actions group with 1 update: [actions-hub/kubectl](https://github.com/actions-hub/kubectl).


Updates `actions-hub/kubectl` from 1.34.1 to 1.34.2
- [Release notes](https://github.com/actions-hub/kubectl/releases)
- [Commits](f14933a23b...1d2c1e96fe)

---
updated-dependencies:
- dependency-name: actions-hub/kubectl
  dependency-version: 1.34.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Co-authored-by: Jason Cameron <git@jasoncameron.dev>
2025-11-30 13:53:32 -05:00
tbodt
43b8658bfd Show how to use subrequest auth with Caddy (#1312)
Signed-off-by: tbodt <tblodt@icloud.com>
2025-11-27 09:04:28 -05:00
The Ninth
00fa939acf Implement FCrDNS and other DNS features (#1308)
* Implement FCrDNS and other DNS features

* Redesign DNS cache and methods

* Fix DNS cache

* Rename regexSafe arg

* Alter verifyFCrDNS(addr) behaviour

* Remove unused dnsCache field from Server struct

* Upd expressions docs

* Update docs/docs/CHANGELOG.md

Signed-off-by: Xe Iaso <me@xeiaso.net>

* refactor(dns): simplify FCrDNS logging

* docs: clarify verifyFCrDNS behavior

Add a note to the documentation for `verifyFCrDNS` to clarify that it returns true when no PTR records are found for the given IP address.

* fix(dns): Improve FCrDNS error handling and tests

The `VerifyFCrDNS` function previously ignored errors returned from reverse DNS lookups. This could lead to incorrect passes when a DNS failure (other than a simple 'not found') occurred. This change ensures that any error from a reverse lookup will cause the FCrDNS check to fail.

The test suite for FCrDNS has been updated to reflect this change. The mock DNS lookups now simulate both 'not found' errors and other generic DNS errors. The test cases have been updated to ensure that the function behaves correctly in both scenarios, resolving a situation where two test cases were effectively duplicates.

* docs: Update FCrDNS documentation and spelling

Corrected a typo in the `verifyFCrDNS` function documentation.

Additionally, updated the spelling exception list to include new terms and remove redundant entries.

* chore: update spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
Co-authored-by: Xe Iaso <me@xeiaso.net>
2025-11-26 22:24:45 -05:00
Xe Iaso
4ead3ed16e fix(config): deprecate the report_as field for challenges (#1311)
* fix(config): deprecate the report_as field for challenges

This was a bad idea when it was added and it is irresponsible to
continue to have it. It causes more UX problems than it fixes with
slight of hand.

Closes: #1310
Closes: #1307
Signed-off-by: Xe Iaso <me@xeiaso.net>

* fix(policy): use the new logger for config validation messages

Signed-off-by: Xe Iaso <me@xeiaso.net>

* docs(admin/thresholds): remove this report_as setting

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-25 23:25:17 -05:00
bplajzer
1f9c2272e6 add Polish language translation (#1309)
* feat(localization): add Polish language translation

* feat(localization): add Polish language translation
2025-11-24 11:55:47 -05:00
Xe Iaso
b11d8132dd chore: add dependabot cooldown (#1302)
* chore: add dependabot cooldown

One of the things I need to worry about with Anubis is the idea that
could pwn a dependency and then get malicious code into prod without
realizing it, a-la Jia Tan. Given that Anubis relies on tools like
Dependabot to manage updating dependencies (good for other reasons),
it makes sense to have Dependabot have a 7 day cooldown for new
versions of dependencies.

This follows the advice from Yossarian on their blog at [1]. Thanks
for the post and easy to copy/paste snippets!

[1]: https://blog.yossarian.net/2025/11/21/We-should-all-be-using-dependency-cooldowns

Signed-off-by: Xe Iaso <me@xeiaso.net>

* chore: update spelling

Signed-off-by: Xe Iaso <me@xeiaso.net>

---------

Signed-off-by: Xe Iaso <me@xeiaso.net>
2025-11-21 19:05:26 +00:00
50 changed files with 1816 additions and 507 deletions

View File

@@ -10,3 +10,5 @@ ABee
tencent
maintnotifications
azurediamond
cooldown
verifyfcrdns

View File

@@ -1,392 +1,400 @@
acs
Actorified
actorifiedstore
actorify
Aibrew
alibaba
alrest
amazonbot
anthro
anubis
anubistest
apnic
APNICRANDNETAU
Applebot
archlinux
asnc
asnchecker
asns
aspirational
atuin
azuretools
badregexes
bbolt
bdba
berr
bezier
bingbot
Bitcoin
bitrate
Bluesky
blueskybot
boi
Bokm
botnet
botstopper
BPort
Brightbot
broked
buildah
byteslice
Bytespider
cachebuster
cachediptoasn
Caddyfile
caninetools
Cardyb
celchecker
celphase
cerr
certresolver
cespare
CGNAT
cgr
chainguard
chall
challengemozilla
challengetest
checkpath
checkresult
chibi
cidranger
ckie
cloudflare
Codespaces
confd
connnection
containerbuild
containerregistry
coreutils
Cotoyogi
Cromite
crt
Cscript
daemonizing
dayjob
DDOS
Debian
debrpm
decaymap
devcontainers
Diffbot
discordapp
discordbot
distros
dnf
dnsbl
dnserr
domainhere
dracula
dronebl
droneblresponse
dropin
dsilence
duckduckbot
eerror
ellenjoe
emacs
enbyware
etld
everyones
evilbot
evilsite
expressionorlist
externalagent
externalfetcher
extldflags
facebookgo
Factset
fahedouch
fastcgi
fediverse
ffprobe
financials
finfos
Firecrawl
flagenv
Fordola
forgejo
forwardauth
fsys
fullchain
gaissmai
Galvus
geoip
geoipchecker
gha
GHSA
Ghz
gipc
gitea
godotenv
goland
gomod
goodbot
googlebot
gopsutil
govulncheck
goyaml
GPG
GPT
gptbot
Graphene
grpcprom
grw
gzw
Hashcash
hashrate
headermap
healthcheck
healthz
hec
helpdesk
Hetzner
hmc
homelab
hostable
htmlc
htmx
httpdebug
Huawei
huawei
hypertext
iaskspider
iaso
iat
ifm
Imagesift
imgproxy
impressum
inbox
ingressed
inp
internets
IPTo
iptoasn
isp
iss
isset
ivh
Jenomis
JGit
jhjj
joho
journalctl
jshelter
JWTs
kagi
kagibot
Keyfunc
keypair
KHTML
kinda
KUBECONFIG
lcj
ldflags
letsencrypt
Lexentale
lfc
lgbt
licend
licstart
lightpanda
limsa
Linting
listor
LLU
loadbalancer
lol
lominsa
maintainership
malware
mcr
memes
metarefresh
metrix
mimi
Minfilia
mistralai
mnt
Mojeek
mojeekbot
mozilla
myclient
mymaster
mypass
myuser
nbf
nepeat
netsurf
nginx
nicksnyder
nobots
NONINFRINGEMENT
nosleep
nullglob
oci
OCOB
ogtag
oklch
omgili
omgilibot
openai
opengraph
openrc
oswald
pag
palemoon
Pangu
parseable
passthrough
Patreon
pgrep
phrik
pidfile
pids
pipefail
pki
podkova
podman
Postgre
poststart
prebaked
privkey
promauto
promhttp
proofofwork
publicsuffix
purejs
pwcmd
pwuser
qualys
qwant
qwantbot
rac
rawler
rcvar
redhat
redir
redirectscheme
refactors
remoteip
reputational
risc
ruleset
runlevels
RUnlock
runtimedir
runtimedirectory
Ryzen
sas
sasl
screenshots
searchbot
searx
sebest
secretplans
Semrush
Seo
setsebool
shellcheck
shirou
shopt
Sidetrade
simprint
sitemap
sls
sni
Spambot
sparkline
spyderbot
srv
stackoverflow
startprecmd
stoppostcmd
storetest
subgrid
subr
subrequest
SVCNAME
tagline
tarballs
tarrif
taviso
tbn
tbr
techaro
techarohq
templ
templruntime
testarea
Thancred
thoth
thothmock
Tik
Timpibot
TLog
traefik
trunc
uberspace
Unbreak
unbreakdocker
unifiedjs
unmarshal
unparseable
uvx
UXP
valkey
Varis
Velen
vendored
vhosts
VKE
vnd
VPS
Vultr
weblate
webmaster
webpage
websecure
websites
Webzio
whois
wildbase
withthothmock
wolfbeast
wordpress
Workaround
workaround
workdir
wpbot
XCircle
xeiaso
xeserv
xesite
xess
xff
XForwarded
XNG
XOB
XOriginal
XReal
yae
YAMLTo
Yda
yeet
yeetfile
yourdomain
yyz
Zenos
zizmor
zombocom
zos
acs
Actorified
actorifiedstore
actorify
Aibrew
alibaba
alrest
amazonbot
anthro
anubis
anubistest
apnic
APNICRANDNETAU
Applebot
archlinux
arpa
asnc
asnchecker
asns
aspirational
atuin
azuretools
badregexes
bbolt
bdba
berr
bezier
bingbot
Bitcoin
bitrate
Bluesky
blueskybot
boi
Bokm
botnet
botstopper
BPort
Brightbot
broked
buildah
byteslice
Bytespider
cachebuster
cachediptoasn
Caddyfile
caninetools
Cardyb
celchecker
celphase
cerr
certresolver
cespare
CGNAT
cgr
chainguard
chall
challengemozilla
challengetest
checkpath
checkresult
chibi
cidranger
ckie
cloudflare
Codespaces
confd
connnection
containerbuild
containerregistry
coreutils
Cotoyogi
Cromite
crt
Cscript
daemonizing
dayjob
DDOS
Debian
debrpm
decaymap
devcontainers
Diffbot
discordapp
discordbot
distros
dnf
dnsbl
dnserr
DNSTTL
domainhere
dracula
dronebl
droneblresponse
dropin
dsilence
duckduckbot
eerror
ellenjoe
emacs
enbyware
etld
everyones
evilbot
evilsite
expressionorlist
externalagent
externalfetcher
extldflags
facebookgo
Factset
fahedouch
fastcgi
FCr
fcrdns
fediverse
ffprobe
financials
finfos
Firecrawl
flagenv
Fordola
forgejo
forwardauth
fsys
fullchain
gaissmai
Galvus
geoip
geoipchecker
gha
GHSA
Ghz
gipc
gitea
godotenv
goland
gomod
goodbot
googlebot
gopsutil
govulncheck
goyaml
GPG
GPT
gptbot
Graphene
grpcprom
grw
gzw
Hashcash
hashrate
headermap
healthcheck
healthz
hec
helpdesk
Hetzner
hmc
homelab
hostable
htmlc
htmx
httpdebug
huawei
hypertext
iaskspider
iaso
iat
ifm
Imagesift
imgproxy
impressum
inbox
ingressed
inp
internets
IPTo
iptoasn
isp
iss
isset
ivh
Jenomis
JGit
jhjj
joho
journalctl
jshelter
JWTs
kagi
kagibot
Keyfunc
keypair
KHTML
kinda
KUBECONFIG
lcj
ldflags
letsencrypt
Lexentale
lfc
lgbt
licend
licstart
lightpanda
limsa
Linting
listor
LLU
loadbalancer
lol
lominsa
maintainership
malware
mcr
memes
metarefresh
metrix
mimi
Minfilia
mistralai
mnt
Mojeek
mojeekbot
mozilla
myclient
mymaster
mypass
myuser
nbf
nepeat
netsurf
nginx
nicksnyder
nobots
NONINFRINGEMENT
nosleep
nullglob
oci
OCOB
ogtag
oklch
omgili
omgilibot
openai
opendns
opengraph
openrc
oswald
pag
palemoon
Pangu
parseable
passthrough
Patreon
pgrep
phrik
pidfile
pids
pipefail
pki
podkova
podman
Postgre
poststart
prebaked
privkey
promauto
promhttp
proofofwork
publicsuffix
purejs
pwcmd
pwuser
qualys
qwant
qwantbot
rac
rawler
rcvar
redhat
redir
redirectscheme
refactors
remoteip
reputational
risc
ruleset
runlevels
RUnlock
runtimedir
runtimedirectory
Ryzen
sas
sasl
screenshots
searchbot
searx
sebest
secretplans
Semrush
Seo
setsebool
shellcheck
shirou
shopt
Sidetrade
simprint
sitemap
sls
sni
snipster
Spambot
sparkline
spyderbot
srv
stackoverflow
startprecmd
stoppostcmd
storetest
subgrid
subr
subrequest
SVCNAME
tagline
tarballs
tarrif
taviso
tbn
tbr
techaro
techarohq
telegrambot
templ
templruntime
testarea
Thancred
thoth
thothmock
Tik
Timpibot
TLog
traefik
trunc
uberspace
Unbreak
unbreakdocker
unifiedjs
unmarshal
unparseable
uvx
UXP
valkey
Varis
Velen
vendored
verify
vhosts
vkbot
VKE
vnd
VPS
Vultr
weblate
webmaster
webpage
websecure
websites
Webzio
whois
wildbase
withthothmock
wolfbeast
wordpress
workaround
workdir
wpbot
XCircle
xeiaso
xeserv
xesite
xess
xff
XForwarded
XNG
XOB
XOriginal
XReal
yae
YAMLTo
Yda
yeet
yeetfile
yourdomain
yyz
Zenos
zizmor
zombocom
zos

View File

@@ -8,6 +8,8 @@ updates:
github-actions:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: gomod
directory: /
@@ -17,6 +19,8 @@ updates:
gomod:
patterns:
- "*"
cooldown:
default-days: 7
- package-ecosystem: npm
directory: /
@@ -26,3 +30,5 @@ updates:
npm:
patterns:
- "*"
cooldown:
default-days: 7

View File

@@ -24,11 +24,10 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
node-version: '24.11.0'
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- name: install node deps
run: |

View File

@@ -28,11 +28,10 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
node-version: '24.11.0'
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9

View File

@@ -38,11 +38,10 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
node-version: '24.11.0'
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9

View File

@@ -53,14 +53,14 @@ jobs:
push: true
- name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1
uses: actions-hub/kubectl@1d2c1e96fe0ae23b0c95ee8240ae151b1e638c23 # v1.34.2
env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with:
args: apply -k docs/manifest
- name: Apply k8s manifests to limsa lominsa
uses: actions-hub/kubectl@f14933a23bc8c582b5aa7d108defd8e2cb9fa86d # v1.34.1
uses: actions-hub/kubectl@1d2c1e96fe0ae23b0c95ee8240ae151b1e638c23 # v1.34.2
env:
KUBE_CONFIG: ${{ secrets.LIMSA_LOMINSA_KUBECONFIG }}
with:

View File

@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- name: Check go.mod and go.sum in main directory
run: |
@@ -73,4 +73,4 @@ jobs:
exit 1
fi
echo "SUCCESS: go.mod and go.sum in test directory are tidy"
echo "SUCCESS: go.mod and go.sum in test directory are tidy"

View File

@@ -26,11 +26,10 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
node-version: '24.11.0'
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- name: Cache playwright binaries
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0

View File

@@ -27,11 +27,10 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
node-version: '24.11.0'
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- name: install node deps
run: |

View File

@@ -28,11 +28,10 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
node-version: '24.11.0'
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- name: install node deps
run: |

View File

@@ -35,11 +35,10 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with:
node-version: latest
node-version: '24.11.0'
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- uses: ko-build/setup-ko@d006021bd0c28d1ce33a07e7943d48b079944c8d # v0.9

View File

@@ -37,7 +37,7 @@ jobs:
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version: stable
go-version: '1.25.4'
- name: Run CI
run: go run ./utils/cmd/backoff-retry bash test/ssh-ci/rigging.sh ${{ matrix.host }}

View File

@@ -50,8 +50,7 @@ bots:
# user_agent_regex: (?i:bot|crawler)
# action: CHALLENGE
# challenge:
# difficulty: 16 # impossible
# report_as: 4 # lie to the operator
# difficulty: 16 # impossible
# algorithm: slow # intentionally waste CPU cycles and time
# Requires a subscription to Thoth to use, see
@@ -249,7 +248,6 @@ thresholds:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
algorithm: metarefresh
difficulty: 1
report_as: 1
# For clients that are browser-like but have either gained points from custom rules or
# report as a standard browser.
- name: moderate-suspicion
@@ -262,7 +260,6 @@ thresholds:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 2 # two leading zeros, very fast for most clients
report_as: 2
- name: mild-proof-of-work
expression:
all:
@@ -273,7 +270,6 @@ thresholds:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 4
report_as: 4
# For clients that are browser like and have gained many points from custom rules
- name: extreme-suspicion
expression: weight >= 30
@@ -282,4 +278,3 @@ thresholds:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 6
report_as: 6

View File

@@ -0,0 +1,6 @@
- name: telegrambot
action: ALLOW
expression:
all:
- userAgent.matches("TelegramBot")
- verifyFCrDNS(remoteAddress, "ptr\\.telegram\\.org$")

View File

@@ -0,0 +1,6 @@
- name: vkbot
action: ALLOW
expression:
all:
- userAgent.matches("vkShare[^+]+\\+http\\://vk\\.com/dev/Share")
- verifyFCrDNS(remoteAddress, "^snipster\\d+\\.go\\.mail\\.ru$")

View File

@@ -8,3 +8,4 @@
- import: (data)/crawlers/marginalia.yaml
- import: (data)/crawlers/mojeekbot.yaml
- import: (data)/crawlers/commoncrawl.yaml
- import: (data)/crawlers/yandexbot.yaml

View File

@@ -0,0 +1,6 @@
- name: yandexbot
action: ALLOW
expression:
all:
- userAgent.matches("\\+http\\://yandex\\.com/bots")
- verifyFCrDNS(remoteAddress, "^.*\\.yandex\\.(ru|com|net)$")

View File

@@ -35,7 +35,6 @@
# action: CHALLENGE
# challenge:
# difficulty: 16 # impossible
# report_as: 4 # lie to the operator
# algorithm: slow # intentionally waste CPU cycles and time
# Requires a subscription to Thoth to use, see

View File

@@ -0,0 +1,2 @@
- import: (data)/clients/telegram-preview.yaml
- import: (data)/clients/vk-preview.yaml

View File

@@ -24,6 +24,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Open Graph passthrough now reuses the configured target Host/SNI/TLS settings, so metadata fetches succeed when the upstream certificate differs from the public domain. ([1283](https://github.com/TecharoHQ/anubis/pull/1283))
- Stabilize the CVE-2025-24369 regression test by always submitting an invalid proof instead of relying on random POW failures.
### Deprecate `report_as` in challenge configuration
Previously Anubis let you lie to users about the difficulty of a challenge to interfere with operators of malicious scrapers as a psychological attack:
```yaml
bots:
# Punish any bot with "bot" in the user-agent string
# This is known to have a high false-positive rate, use at your own risk
- name: generic-bot-catchall
user_agent_regex: (?i:bot|crawler)
action: CHALLENGE
challenge:
difficulty: 16 # impossible
report_as: 4 # lie to the operator
algorithm: slow # intentionally waste CPU cycles and time
```
This has turned out to be a bad idea because it has caused massive user experience problems and has been removed. If you are using this setting, you will get a warning in your logs like this:
```json
{
"time": "2025-11-25T23:10:31.092201549-05:00",
"level": "WARN",
"source": {
"function": "github.com/TecharoHQ/anubis/lib/policy.ParseConfig",
"file": "/home/xe/code/TecharoHQ/anubis/lib/policy/policy.go",
"line": 201
},
"msg": "use of deprecated report_as setting detected, please remove this from your policy file when possible",
"at": "config-validate",
"name": "mild-suspicion"
}
```
To remove this warning, remove this setting from your policy file.
### Logging customization
Anubis now supports the ability to log to multiple backends ("sinks"). This allows you to have Anubis [log to a file](./admin/policies.mdx#file-sink) instead of just logging to standard out. You can also customize the [logging level](./admin/policies.mdx#log-levels) in the policy file:
@@ -44,6 +80,31 @@ logging:
Additionally, information about [how Anubis uses each logging level](./admin/policies.mdx#log-levels) has been added to the documentation.
### DNS Features
- CEL expressions for:
- FCrDNS checks
- Forward DNS queries
- Reverse DNS queries
- `arpaReverseIP` to transform IPv4/6 addresses into ARPA reverse IP notation.
- `regexSafe` to escape regex special characters (useful for including `remoteAddress` or headers in regular expressions).
- DNS cache and other optimizations to minimize unnecessary DNS queries.
The DNS cache TTL can be changed in the bots config like this:
```yaml
dns_ttl:
forward: 600
reverse: 600
```
The default value for both forward and reverse queries is 300 seconds.
The `verifyFCrDNS` CEL function has two overloads:
- `(addr)`
Simply verifies that the remote side has PTR records pointing to the target address.
- `(addr, ptrPattern)`
Verifies that the remote side refers to a specific domain and that this domain points to the target IP.
## v1.23.1: Lyse Hext - Echo 1
- Fix `SERVE_ROBOTS_TXT` setting after the double slash fix broke it.

View File

@@ -12,7 +12,6 @@ To use it in your Anubis configuration:
action: CHALLENGE
challenge:
difficulty: 1 # Number of seconds to wait before refreshing the page
report_as: 4 # Unused by this challenge method
algorithm: metarefresh # Specify a non-JS challenge method
```

View File

@@ -12,7 +12,6 @@ To use it in your Anubis configuration:
action: CHALLENGE
challenge:
difficulty: 1 # Number of seconds to wait before refreshing the page
report_as: 4 # Unused by this challenge method
algorithm: preact
```

View File

@@ -233,6 +233,27 @@ 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.
### `regexSafe`
Available in `bot` expressions.
```ts
function regexSafe(input: string): string;
```
`regexSafe` takes a string and escapes it for safe use inside of a regular expression. This is useful when you are creating regular expressions from headers or variables such as `remoteAddress`.
| Input | Output |
| :------------------------ | :------------------------------ |
| `regexSafe("1.2.3.4")` | `1\\.2\\.3\\.4` |
| `regexSafe("techaro.lol")` | `techaro\\.lol` |
| `regexSafe("star*")` | `star\\*` |
| `regexSafe("plus+")` | `plus\\+` |
| `regexSafe("{braces}")` | `\\{braces\\}` |
| `regexSafe("start^")` | `start\\^` |
| `regexSafe("back\\slash")` | `back\\\\slash` |
| `regexSafe("dash-dash")` | `dash\\-dash` |
### `segments`
Available in `bot` expressions.
@@ -266,6 +287,99 @@ This is useful if you want to write rules that allow requests that have no query
- size(segments(path)) < 2
```
### DNS Functions
Anubis can also perform DNS lookups as a part of its expression evaluation. This can be useful for doing things like checking for a valid [Forward-confirmed reverse DNS (FCrDNS)](https://en.wikipedia.org/wiki/Forward-confirmed_reverse_DNS) record.
#### `arpaReverseIP`
Available in `bot` expressions.
```ts
function arpaReverseIP(ip: string): string;
```
`arpaReverseIP` takes an IP address and returns its value in [ARPA notation](https://www.ietf.org/rfc/rfc2317.html). This can be useful when matching PTR record patterns.
| Input | Output |
| :----------------------------- | :------------------------------------------------------------------- |
| `arpaReverseIP("1.2.3.4")` | `4.3.2.1` |
| `arpaReverseIP("2001:db8::1")` | `1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2` |
#### `lookupHost`
Available in `bot` expressions.
```ts
function lookupHost(host: string): string[];
```
`lookupHost` performs a DNS lookup for the given hostname and returns a list of IP addresses.
```yaml
- name: cloudflare-ip-in-host-header
action: DENY
expression: '"104.16.0.0" in lookupHost(headers["Host"])'
```
#### `reverseDNS`
Available in `bot` expressions.
```ts
function reverseDNS(ip: string): string[];
```
`reverseDNS` takes an IP address and returns the DNS names associated with it. This is useful when you want to check PTR records of an IP address.
```yaml
- name: allow-googlebot
action: ALLOW
expression: 'reverseDNS(remoteAddress).endsWith(".googlebot.com")'
```
::: warning
Do not use this for validating the legitimacy of an IP address. It is possible for DNS records to be out of date or otherwise manipulated. Use [`verifyFCrDNS`](#verifyfcrdns) instead for a more reliable result.
:::
#### `verifyFCrDNS`
Available in `bot` expressions.
```ts
function verifyFCrDNS(ip: string): bool;
function verifyFCrDNS(ip: string, pattern: string): bool;
```
`verifyFCrDNS` checks if the reverse DNS of an IP address matches its forward DNS. This is a common technique to filter out spam and bot traffic. `verifyFCrDNS` comes in two forms:
- `verifyFCrDNS(remoteAddress)` will check that the reverse DNS of the remote address resolves back to the remote address. If no PTR records, returns true.
- `verifyFCrDNS(remoteAddress, pattern)` will check that the reverse DNS of the remote address is matching with pattern and that name resolves back to the remote address.
This is best used in rules like this:
```yaml
- name: require-fcrdns-for-post
action: DENY
expression:
all:
- method == "POST"
- "!verifyFCrDNS(remoteAddress)"
```
Here is an another example that allows requests from telegram:
```yaml
- name: telegrambot
action: ALLOW
expression:
all:
- userAgent.matches("TelegramBot")
- verifyFCrDNS(remoteAddress, "ptr\\.telegram\\.org$")
```
## 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.

View File

@@ -156,3 +156,68 @@ server {
```
</details>
## Caddy
Anubis can be used with the [`forward_auth`](https://caddyserver.com/docs/caddyfile/directives/forward_auth) directive in Caddy.
First, the `TARGET` environment variable in Anubis must be set to a space, eg:
<Tabs>
<TabItem value="env-file" label="Environment file" default>
```shell
# anubis.env
TARGET=" "
# ...
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
```yaml
services:
anubis-caddy:
image: ghcr.io/techarohq/anubis:latest
environment:
TARGET: " "
# ...
```
</TabItem>
<TabItem value="k8s" label="Kubernetes">
Inside your Deployment, StatefulSet, or Pod:
```yaml
- name: anubis
image: ghcr.io/techarohq/anubis:latest
env:
- name: TARGET
value: " "
# ...
```
</TabItem>
</Tabs>
Then configure the necessary directives in your site block:
```caddy
route {
# Assumption: Anubis is running in the same network namespace as
# caddy on localhost TCP port 8923
reverse_proxy /.within.website/* 127.0.0.1:8923
forward_auth 127.0.0.1:8923 {
uri /.within.website/x/cmd/anubis/api/check
trusted_proxies private_ranges
@unauthorized status 401
handle_response @unauthorized {
redir * /.within.website/?redir={uri} 307
}
}
}
```
If you want to use this for multiple sites, you can create a [snippet](https://caddyserver.com/docs/caddyfile/concepts#snippets) and import it in multiple site blocks.

View File

@@ -41,7 +41,6 @@ thresholds:
challenge:
algorithm: metarefresh
difficulty: 1
report_as: 1
- name: moderate-suspicion
expression:
@@ -52,7 +51,6 @@ thresholds:
challenge:
algorithm: fast
difficulty: 2
report_as: 2
- name: extreme-suspicion
expression: weight >= 20
@@ -60,7 +58,6 @@ thresholds:
challenge:
algorithm: fast
difficulty: 4
report_as: 4
```
This defines a suite of 4 thresholds:
@@ -130,7 +127,6 @@ action: CHALLENGE
challenge:
algorithm: metarefresh
difficulty: 1
report_as: 1
```
</td>

View File

@@ -84,7 +84,6 @@ This rule has been known to have a high false positive rate in testing. Please u
action: CHALLENGE
challenge:
difficulty: 16 # impossible
report_as: 4 # lie to the operator
algorithm: slow # intentionally waste CPU cycles and time
```
@@ -93,7 +92,6 @@ Challenges can be configured with these settings:
| Key | Example | Description |
| :----------- | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `difficulty` | `4` | The challenge difficulty (number of leading zeros) for proof-of-work. See [Why does Anubis use Proof-of-Work?](/docs/design/why-proof-of-work) for more details. |
| `report_as` | `4` | What difficulty the UI should report to the user. Useful for messing with industrial-scale scraping efforts. |
| `algorithm` | `"fast"` | The challenge method to use. See [the list of challenge methods](./configuration/challenges/) for more information. |
### Remote IP based filtering

View File

@@ -49,7 +49,6 @@ bots:
# action: CHALLENGE
# challenge:
# difficulty: 16 # impossible
# report_as: 4 # lie to the operator
# algorithm: slow # intentionally waste CPU cycles and time
- name: rss-feed-blog
@@ -105,7 +104,6 @@ thresholds:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/metarefresh
algorithm: metarefresh
difficulty: 1
report_as: 1
# For clients that are browser-like but have either gained points from custom rules or
# report as a standard browser.
- name: moderate-suspicion
@@ -122,7 +120,6 @@ thresholds:
# challenge data, and forwards that to the client.
algorithm: preact
difficulty: 1
report_as: 1
- name: mild-proof-of-work
expression:
all:
@@ -133,7 +130,6 @@ thresholds:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 2 # two leading zeros, very fast for most clients
report_as: 2
# For clients that are browser like and have gained many points from custom rules
- name: extreme-suspicion
expression: weight >= 30
@@ -142,7 +138,6 @@ thresholds:
# https://anubis.techaro.lol/docs/admin/configuration/challenges/proof-of-work
algorithm: fast
difficulty: 4
report_as: 4
dnsbl: false

70
internal/dns/cache.go Normal file
View File

@@ -0,0 +1,70 @@
package dns
import (
"log/slog"
"time"
"github.com/TecharoHQ/anubis/lib/store"
_ "github.com/TecharoHQ/anubis/lib/store/all"
)
type DnsCache struct {
forward store.JSON[[]string]
reverse store.JSON[[]string]
forwardTTL time.Duration
reverseTTL time.Duration
}
func NewDNSCache(forwardTTL int, reverseTTL int, backend store.Interface) *DnsCache {
return &DnsCache{
forward: store.JSON[[]string]{
Underlying: backend,
Prefix: "forwardDNS",
},
reverse: store.JSON[[]string]{
Underlying: backend,
Prefix: "reverseDNS",
},
forwardTTL: time.Duration(forwardTTL) * time.Second,
reverseTTL: time.Duration(reverseTTL) * time.Second,
}
}
func (d *Dns) getCachedForward(host string) ([]string, bool) {
if d.cache == nil {
return nil, false
}
if cached, err := d.cache.forward.Get(d.ctx, host); err == nil {
slog.Debug("DNS: forward cache hit", "name", host, "ips", cached)
return cached, true
}
slog.Debug("DNS: forward cache miss", "name", host)
return nil, false
}
func (d *Dns) getCachedReverse(addr string) ([]string, bool) {
if d.cache == nil {
return nil, false
}
if cached, err := d.cache.reverse.Get(d.ctx, addr); err == nil {
slog.Debug("DNS: reverse cache hit", "addr", addr, "names", cached)
return cached, true
}
slog.Debug("DNS: reverse cache miss", "addr", addr)
return nil, false
}
func (d *Dns) forwardCachePut(host string, entries []string) {
if d.cache == nil {
return
}
d.cache.forward.Set(d.ctx, host, entries, d.cache.forwardTTL)
}
func (d *Dns) reverseCachePut(addr string, entries []string) {
if d.cache == nil {
return
}
d.cache.reverse.Set(d.ctx, addr, entries, d.cache.reverseTTL)
}

174
internal/dns/dns.go Normal file
View File

@@ -0,0 +1,174 @@
package dns
import (
"context"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"net"
"regexp"
"slices"
"strings"
)
var (
DNSLookupAddr = net.LookupAddr
DNSLookupHost = net.LookupHost
)
type Dns struct {
cache *DnsCache
ctx context.Context
}
func New(ctx context.Context, cache *DnsCache) *Dns {
return &Dns{
cache: cache,
ctx: ctx,
}
}
// ReverseDNS performs a reverse DNS lookup for the given IP address and trims the trailing dot from the results.
func (d *Dns) ReverseDNS(addr string) ([]string, error) {
slog.Debug("DNS: performing reverse lookup", "addr", addr)
if cached, ok := d.getCachedReverse(addr); ok {
return cached, nil
}
names, err := DNSLookupAddr(addr)
if err != nil {
if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
slog.Debug("DNS: no PTR record found", "addr", addr)
return []string{}, nil
}
slog.Error("DNS: reverse lookup failed", "addr", addr, "err", err)
return nil, err
}
slog.Debug("DNS: reverse lookup successful", "addr", addr, "names", names)
trimmedNames := make([]string, len(names))
for i, name := range names {
trimmedNames[i] = strings.TrimSuffix(name, ".")
}
d.reverseCachePut(addr, trimmedNames)
return trimmedNames, nil
}
// LookupHost performs a forward DNS lookup for the given hostname.
func (d *Dns) LookupHost(host string) ([]string, error) {
slog.Debug("DNS: performing forward lookup", "host", host)
if cached, ok := d.getCachedForward(host); ok {
return cached, nil
}
addrs, err := DNSLookupHost(host)
if err != nil {
if dnsErr, ok := err.(*net.DNSError); ok && dnsErr.IsNotFound {
slog.Debug("DNS: no A/AAAA record found", "host", host)
return []string{}, nil
}
slog.Error("DNS: forward lookup failed", "host", host, "err", err)
return nil, err
}
slog.Debug("DNS: forward lookup successful", "host", host, "addrs", addrs)
d.forwardCachePut(host, addrs)
return addrs, nil
}
// verifyFCrDNSInternal performs the second half of the FCrDNS check, using a
// pre-fetched list of names to perform the forward lookups.
func (d *Dns) verifyFCrDNSInternal(addr string, names []string) bool {
for _, name := range names {
if cached, err := d.LookupHost(name); err == nil {
if slices.Contains(cached, addr) {
slog.Info("DNS: forward lookup confirmed original IP", "name", name, "addr", addr)
return true
}
continue
}
}
slog.Info("DNS: could not confirm original IP in forward lookups", "addr", addr)
return false
}
// VerifyFCrDNS performs a forward-confirmed reverse DNS (FCrDNS) lookup for the given IP address,
// optionally matching against a provided pattern.
func (d *Dns) VerifyFCrDNS(addr string, pattern *string) bool {
var patternVal string
if pattern != nil {
patternVal = *pattern
}
slog.Debug("DNS: performing FCrDNS lookup", "addr", addr, "pattern", patternVal)
names, err := d.ReverseDNS(addr)
if err != nil {
return false
}
if len(names) == 0 {
return pattern == nil // If no pattern specified, check is passed
}
// If a pattern is provided, check for a match.
if pattern != nil {
anyNameMatched := false
for _, name := range names {
matched, err := regexp.MatchString(*pattern, name)
if err != nil {
slog.Error("DNS: verifyFCrDNS invalid regex pattern", "err", err)
return false // Invalid pattern is a failure.
}
if matched {
anyNameMatched = true
break
}
}
if !anyNameMatched {
slog.Debug("DNS: FCrDNS no PTR matches the pattern", "addr", addr, "pattern", *pattern)
return false
}
slog.Debug("DNS: FCrDNS PTR matched pattern, proceeding with forward check", "addr", addr, "pattern", *pattern)
}
// If we're here, either there was no pattern, or the pattern matched.
// Proceed with the forward lookup confirmation.
return d.verifyFCrDNSInternal(addr, names)
}
// ArpaReverseIP performs translation from ip v4/v6 to arpa reverse notation
func (d *Dns) ArpaReverseIP(addr string) (string, error) {
ip := net.ParseIP(addr)
if ip == nil {
return addr, errors.New("invalid IP address")
}
if ipv4 := ip.To4(); ipv4 != nil {
return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0]), nil
}
ipv6 := ip.To16()
if ipv6 == nil {
return addr, errors.New("invalid IPv6 address")
}
hexBytes := make([]byte, hex.EncodedLen(len(ipv6)))
hex.Encode(hexBytes, ipv6)
var sb strings.Builder
sb.Grow(len(hexBytes)*2 - 1)
for i := len(hexBytes) - 1; i >= 0; i-- {
sb.WriteByte(hexBytes[i])
if i > 0 {
sb.WriteByte('.')
}
}
return sb.String(), nil
}

308
internal/dns/dns_test.go Normal file
View File

@@ -0,0 +1,308 @@
package dns
import (
"context"
"errors"
"net"
"reflect"
"testing"
"time"
"github.com/TecharoHQ/anubis/lib/store/memory"
)
// newTestDNS is a helper function to create a new Dns object with an in-memory cache for testing.
func newTestDNS(forwardTTL int, reverseTTL int) *Dns {
ctx := context.Background()
memStore := memory.New(ctx)
cache := NewDNSCache(forwardTTL, reverseTTL, memStore)
return New(ctx, cache)
}
// mockLookupAddr is a mock implementation of the net.LookupAddr function.
func mockLookupAddr(addr string) ([]string, error) {
switch addr {
case "8.8.8.8":
return []string{"dns.google."}, nil
case "1.1.1.1":
return []string{"one.one.one.one."}, nil
case "208.67.222.222":
return []string{"resolver1.opendns.com."}, nil
case "9.9.9.9":
return nil, &net.DNSError{Err: "no such host", Name: "9.9.9.9", IsNotFound: true}
case "1.2.3.4":
return nil, errors.New("unknown error")
default:
return nil, &net.DNSError{Err: "no such host", Name: addr, IsNotFound: true}
}
}
// mockLookupHost is a mock implementation of the net.LookupHost function.
func mockLookupHost(host string) ([]string, error) {
switch host {
case "dns.google":
return []string{"8.8.8.8", "8.8.4.4"}, nil
case "one.one.one.one":
return []string{"1.1.1.1", "1.0.0.1"}, nil
case "resolver1.opendns.com":
return []string{"208.67.222.222"}, nil
case "example.com":
return nil, &net.DNSError{Err: "no such host", Name: "example.com", IsNotFound: true}
default:
return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true}
}
}
func TestMain(m *testing.M) {
// Before all tests
originalLookupAddr := DNSLookupAddr
originalLookupHost := DNSLookupHost
DNSLookupAddr = mockLookupAddr
DNSLookupHost = mockLookupHost
// Run tests
exitCode := m.Run()
// After all tests
DNSLookupAddr = originalLookupAddr
DNSLookupHost = originalLookupHost
// Exit
if exitCode != 0 {
panic(exitCode)
}
}
func TestDns_ArpaReverseIP(t *testing.T) {
d := newTestDNS(0, 0)
tests := []struct {
name string
ip string
want string
wantErr bool
}{
{"ipv4", "192.0.2.1", "1.2.0.192", false},
{"ipv6", "2001:db8::1", "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2", false},
{"invalid ip", "invalid", "invalid", true},
{"ipv4-mapped ipv6", "::ffff:192.0.2.1", "1.2.0.192", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := d.ArpaReverseIP(tt.ip)
if (err != nil) != tt.wantErr {
t.Errorf("ArpaReverseIP() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("ArpaReverseIP() = %v, want %v", got, tt.want)
}
})
}
}
func TestDns_ReverseDNS(t *testing.T) {
d := newTestDNS(1, 1) // short TTL for testing cache
// First call - cache miss
t.Run("cache miss", func(t *testing.T) {
got, err := d.ReverseDNS("8.8.8.8")
if err != nil {
t.Fatalf("ReverseDNS() error = %v", err)
}
want := []string{"dns.google"}
if !reflect.DeepEqual(got, want) {
t.Errorf("ReverseDNS() = %v, want %v", got, want)
}
})
// Second call - cache hit
t.Run("cache hit", func(t *testing.T) {
// Temporarily replace lookup function to ensure cache is used
originalLookupAddr := DNSLookupAddr
DNSLookupAddr = func(addr string) ([]string, error) {
return nil, errors.New("should not be called")
}
defer func() { DNSLookupAddr = originalLookupAddr }()
got, err := d.ReverseDNS("8.8.8.8")
if err != nil {
t.Fatalf("ReverseDNS() error = %v", err)
}
want := []string{"dns.google"}
if !reflect.DeepEqual(got, want) {
t.Errorf("ReverseDNS() = %v, want %v", got, want)
}
})
// Test cache expiration
t.Run("cache expiration", func(t *testing.T) {
time.Sleep(2 * time.Second)
// Now the cache should be expired
// We expect the mock to be called again
// To test this we will change the mock to return something different
originalLookupAddr := DNSLookupAddr
DNSLookupAddr = func(addr string) ([]string, error) {
if addr == "8.8.8.8" {
return []string{"expired.google."}, nil
}
return mockLookupAddr(addr)
}
defer func() { DNSLookupAddr = originalLookupAddr }()
got, err := d.ReverseDNS("8.8.8.8")
if err != nil {
t.Fatalf("ReverseDNS() error = %v", err)
}
want := []string{"expired.google"}
if !reflect.DeepEqual(got, want) {
t.Errorf("ReverseDNS() = %v, want %v", got, want)
}
})
// Test not found
t.Run("not found", func(t *testing.T) {
got, err := d.ReverseDNS("9.9.9.9")
if err != nil {
t.Fatalf("ReverseDNS() error = %v", err)
}
if len(got) != 0 {
t.Errorf("ReverseDNS() = %v, want empty slice", got)
}
})
}
func TestDns_LookupHost(t *testing.T) {
d := newTestDNS(1, 1)
t.Run("cache miss", func(t *testing.T) {
got, err := d.LookupHost("dns.google")
if err != nil {
t.Fatalf("LookupHost() error = %v", err)
}
want := []string{"8.8.8.8", "8.8.4.4"}
if !reflect.DeepEqual(got, want) {
t.Errorf("LookupHost() = %v, want %v", got, want)
}
})
t.Run("cache hit", func(t *testing.T) {
originalLookupHost := DNSLookupHost
DNSLookupHost = func(host string) ([]string, error) {
return nil, errors.New("should not be called")
}
defer func() { DNSLookupHost = originalLookupHost }()
got, err := d.LookupHost("dns.google")
if err != nil {
t.Fatalf("LookupHost() error = %v", err)
}
want := []string{"8.8.8.8", "8.8.4.4"}
if !reflect.DeepEqual(got, want) {
t.Errorf("LookupHost() = %v, want %v", got, want)
}
})
t.Run("cache expiration", func(t *testing.T) {
time.Sleep(2 * time.Second)
originalLookupHost := DNSLookupHost
DNSLookupHost = func(host string) ([]string, error) {
if host == "dns.google" {
return []string{"9.9.9.9"}, nil
}
return mockLookupHost(host)
}
defer func() { DNSLookupHost = originalLookupHost }()
got, err := d.LookupHost("dns.google")
if err != nil {
t.Fatalf("LookupHost() error = %v", err)
}
want := []string{"9.9.9.9"}
if !reflect.DeepEqual(got, want) {
t.Errorf("LookupHost() = %v, want %v", got, want)
}
})
t.Run("not found", func(t *testing.T) {
got, err := d.LookupHost("example.com")
if err != nil {
t.Fatalf("LookupHost() error = %v", err)
}
if len(got) != 0 {
t.Errorf("LookupHost() = %v, want empty slice", got)
}
})
}
func TestDns_VerifyFCrDNS(t *testing.T) {
d := newTestDNS(1, 1)
// Helper to convert string to *string
p := func(s string) *string {
return &s
}
tests := []struct {
name string
ip string
pattern *string
want bool
}{
// Cases without pattern
{"valid no pattern", "8.8.8.8", nil, true},
{"valid partial no pattern", "1.1.1.1", nil, true},
{"not found no pattern", "9.9.9.9", nil, true},
{"unknown error no pattern", "1.2.3.4", nil, false},
// Cases with pattern
{"valid match", "8.8.8.8", p(`.*\.google$`), true},
{"valid no match", "8.8.8.8", p(`\.com$`), false},
{"not found with pattern", "9.9.9.9", p(".*"), false},
{"unknown error with pattern", "1.2.3.4", p(".*"), false},
{"invalid pattern", "8.8.8.8", p(`[`), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := d.VerifyFCrDNS(tt.ip, tt.pattern); got != tt.want {
t.Errorf("VerifyFCrDNS() = %v, want %v", got, tt.want)
}
})
}
t.Run("reverse cache hit", func(t *testing.T) {
// Prime the cache
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
t.Fatalf("VerifyFCrDNS() priming failed, got %v, want true", got)
}
// Now test with a failing lookup to ensure cache is used
originalLookupAddr := DNSLookupAddr
DNSLookupAddr = func(addr string) ([]string, error) {
return nil, errors.New("should not be called")
}
defer func() { DNSLookupAddr = originalLookupAddr }()
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
t.Errorf("VerifyFCrDNS() = %v, want true", got)
}
})
t.Run("forward cache hit", func(t *testing.T) {
// Prime the cache
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
t.Fatalf("VerifyFCrDNS() priming failed, got %v, want true", got)
}
// Now test with a failing lookup to ensure cache is used
originalLookupHost := DNSLookupHost
DNSLookupHost = func(host string) ([]string, error) {
return nil, errors.New("should not be called")
}
defer func() { DNSLookupHost = originalLookupHost }()
if got := d.VerifyFCrDNS("8.8.8.8", nil); got != true {
t.Errorf("VerifyFCrDNS() = %v, want true", got)
}
})
}

View File

@@ -167,8 +167,8 @@ func (s *Server) hydrateChallengeRule(rule *policy.Bot, chall *challenge.Challen
if rule.Challenge.Difficulty == 0 {
rule.Challenge.Difficulty = chall.Difficulty
}
if rule.Challenge.ReportAs == 0 {
rule.Challenge.ReportAs = chall.Difficulty
if rule.Challenge.ReportAs != 0 {
s.logger.Warn("[DEPRECATION] the report_as field in this bot rule is deprecated, see https://github.com/TecharoHQ/anubis/issues/1310 for more information", "bot_name", rule.Name, "difficulty", rule.Challenge.Difficulty, "report_as", rule.Challenge.ReportAs)
}
if rule.Challenge.Algorithm == "" {
rule.Challenge.Algorithm = chall.Method
@@ -648,7 +648,6 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
return cr("default/allow", config.RuleAllow, weight), &policy.Bot{
Challenge: &config.ChallengeRules{
Difficulty: s.policy.DefaultDifficulty,
ReportAs: s.policy.DefaultDifficulty,
Algorithm: config.DefaultAlgorithm,
},
Rules: &checker.List{},

View File

@@ -464,10 +464,6 @@ func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
if bot.Challenge.Difficulty != i {
t.Errorf("Challenge.Difficulty is wrong, wanted %d, got: %d", i, bot.Challenge.Difficulty)
}
if bot.Challenge.ReportAs != i {
t.Errorf("Challenge.ReportAs is wrong, wanted %d, got: %d", i, bot.Challenge.ReportAs)
}
})
}
}

View File

@@ -36,7 +36,6 @@ func TestBasic(t *testing.T) {
Challenge: &config.ChallengeRules{
Algorithm: "fast",
Difficulty: 0,
ReportAs: 0,
},
}
const challengeStr = "hunter"

View File

@@ -332,6 +332,7 @@ type fileConfig struct {
Thresholds []Threshold `json:"thresholds"`
StatusCodes StatusCodes `json:"status_codes"`
DNSBL bool `json:"dnsbl"`
DNSTTL DnsTTL `json:"dns_ttl"`
Logging *Logging `json:"logging"`
}
@@ -387,6 +388,10 @@ func Load(fin io.Reader, fname string) (*Config, error) {
Challenge: http.StatusOK,
Deny: http.StatusOK,
},
DNSTTL: DnsTTL{
Forward: 300,
Reverse: 300,
},
Store: &Store{
Backend: "memory",
},
@@ -402,7 +407,8 @@ func Load(fin io.Reader, fname string) (*Config, error) {
}
result := &Config{
DNSBL: c.DNSBL,
DNSBL: c.DNSBL,
DNSTTL: c.DNSTTL,
OpenGraph: OpenGraph{
Enabled: c.OpenGraph.Enabled,
ConsiderHost: c.OpenGraph.ConsiderHost,
@@ -469,6 +475,29 @@ func Load(fin io.Reader, fname string) (*Config, error) {
return result, nil
}
type DnsTTL struct {
Forward int `json:"forward"`
Reverse int `json:"reverse"`
}
func (sc DnsTTL) Valid() error {
var errs []error
if sc.Forward < 0 {
errs = append(errs, fmt.Errorf("%w: forward TTL is %d", ErrStatusCodeNotValid, sc.Forward))
}
if sc.Reverse < 0 {
errs = append(errs, fmt.Errorf("%w: reverse TTL is %d", ErrStatusCodeNotValid, sc.Reverse))
}
if len(errs) != 0 {
return fmt.Errorf("dns TTL values not valid:\n%w", errors.Join(errs...))
}
return nil
}
type Config struct {
Impressum *Impressum
Store *Store
@@ -478,6 +507,7 @@ type Config struct {
StatusCodes StatusCodes
Logging *Logging
DNSBL bool
DNSTTL DnsTTL
}
func (c Config) Valid() error {

View File

@@ -110,7 +110,6 @@ func TestBotValid(t *testing.T) {
PathRegex: p("Mozilla"),
Challenge: &ChallengeRules{
Difficulty: -1,
ReportAs: 4,
Algorithm: "fast",
},
},
@@ -124,7 +123,6 @@ func TestBotValid(t *testing.T) {
PathRegex: p("Mozilla"),
Challenge: &ChallengeRules{
Difficulty: 420,
ReportAs: 4,
Algorithm: "fast",
},
},
@@ -361,7 +359,6 @@ func TestBotConfigZero(t *testing.T) {
b.Challenge = &ChallengeRules{
Difficulty: 4,
ReportAs: 4,
Algorithm: DefaultAlgorithm,
}
if b.Zero() {

View File

@@ -0,0 +1,8 @@
dns_ttl:
forward: 60.0
reverse: "600"
bots:
- name: "test"
user_agent_regex: ".*"
action: "DENY"

View File

@@ -0,0 +1,8 @@
dns_ttl:
forward: 600
reverse: 600
bots:
- name: "test"
user_agent_regex: ".*"
action: "DENY"

View File

@@ -18,7 +18,6 @@ thresholds:
challenge:
algorithm: metarefresh
difficulty: 1
report_as: 1
- name: moderate-suspicion
expression:
all:
@@ -28,11 +27,9 @@ thresholds:
challenge:
algorithm: fast
difficulty: 2
report_as: 2
- name: extreme-suspicion
expression: weight >= 20
action: CHALLENGE
challenge:
algorithm: fast
difficulty: 4
report_as: 4

View File

@@ -24,7 +24,6 @@ var (
Challenge: &ChallengeRules{
Algorithm: "fast",
Difficulty: anubis.DefaultDifficulty,
ReportAs: anubis.DefaultDifficulty,
},
},
}

View File

@@ -32,7 +32,6 @@ func TestThresholdValid(t *testing.T) {
Challenge: &ChallengeRules{
Algorithm: "fast",
Difficulty: 1,
ReportAs: 1,
},
},
err: nil,

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/dns"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/policy/expressions"
"github.com/google/cel-go/cel"
@@ -16,8 +17,8 @@ type CELChecker struct {
src string
}
func NewCELChecker(cfg *config.ExpressionOrList) (*CELChecker, error) {
env, err := expressions.BotEnvironment()
func NewCELChecker(cfg *config.ExpressionOrList, dnsObj *dns.Dns) (*CELChecker, error) {
env, err := expressions.BotEnvironment(dnsObj)
if err != nil {
return nil, err
}

View File

@@ -4,6 +4,7 @@ import (
"math/rand/v2"
"strings"
"github.com/TecharoHQ/anubis/internal/dns"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
@@ -15,7 +16,7 @@ import (
// variables and functions that are passed into the CEL scope so that
// Anubis can fail loudly and early when something is invalid instead
// of blowing up at runtime.
func BotEnvironment() (*cel.Env, error) {
func BotEnvironment(dnsObj *dns.Dns) (*cel.Env, error) {
return New(
// Variables exposed to CEL programs:
cel.Variable("remoteAddress", cel.StringType),
@@ -57,6 +58,118 @@ func BotEnvironment() (*cel.Env, error) {
),
),
cel.Function("reverseDNS",
cel.Overload("reverseDNS_string_list_string",
[]*cel.Type{cel.StringType},
cel.ListType(cel.StringType),
cel.UnaryBinding(func(addr ref.Val) ref.Val {
addrStr, ok := addr.(types.String)
if !ok {
return types.ValOrErr(addr, "addr is not a string, but is %T", addr)
}
names, err := dnsObj.ReverseDNS(string(addrStr))
if err != nil {
return types.NewStringList(types.DefaultTypeAdapter, []string{})
}
return types.NewStringList(types.DefaultTypeAdapter, names)
}),
),
),
cel.Function("lookupHost",
cel.Overload("lookupHost_string_list_string",
[]*cel.Type{cel.StringType},
cel.ListType(cel.StringType),
cel.UnaryBinding(func(host ref.Val) ref.Val {
hostStr, ok := host.(types.String)
if !ok {
return types.ValOrErr(host, "host is not a string, but is %T", host)
}
addrs, err := dnsObj.LookupHost(string(hostStr))
if err != nil {
return types.NewStringList(types.DefaultTypeAdapter, []string{})
}
return types.NewStringList(types.DefaultTypeAdapter, addrs)
}),
),
),
cel.Function("verifyFCrDNS",
cel.Overload("verifyFCrDNS_string_bool",
[]*cel.Type{cel.StringType},
cel.BoolType,
cel.UnaryBinding(func(addr ref.Val) ref.Val {
addrStr, ok := addr.(types.String)
if !ok {
return types.ValOrErr(addr, "addr is not a string")
}
return types.Bool(dnsObj.VerifyFCrDNS(string(addrStr), nil))
}),
),
cel.Overload("verifyFCrDNS_string_string_bool",
[]*cel.Type{cel.StringType, cel.StringType},
cel.BoolType,
cel.BinaryBinding(func(addr, pattern ref.Val) ref.Val {
addrStr, ok := addr.(types.String)
if !ok {
return types.ValOrErr(addr, "addr is not a string")
}
patternStr, ok := pattern.(types.String)
if !ok {
return types.ValOrErr(pattern, "pattern is not a string")
}
p := string(patternStr)
return types.Bool(dnsObj.VerifyFCrDNS(string(addrStr), &p))
}),
),
),
// arpaReverseIP transforms ip into arpa reverse notation like this
// 1.2.3.4 -> 4.3.2.1
// 2001:db8::1 -> 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2
cel.Function("arpaReverseIP",
cel.Overload("arpaReverseIP_string_string",
[]*cel.Type{cel.StringType},
cel.StringType,
cel.UnaryBinding(func(addr ref.Val) ref.Val {
s, ok := addr.(types.String)
if !ok {
return types.ValOrErr(addr, "addr is not a string")
}
reversedIp, err := dnsObj.ArpaReverseIP(string(s))
if err != nil {
return types.ValOrErr(addr, "%s", err.Error())
}
return types.String(reversedIp)
}),
),
),
// regexSafe escapes a string for insertion into a regular expression
cel.Function("regexSafe",
cel.Overload("regexSafe_string_string",
[]*cel.Type{cel.StringType},
cel.StringType,
cel.UnaryBinding(func(str ref.Val) ref.Val {
s, ok := str.(types.String)
if !ok {
return types.ValOrErr(str, "addr is not a string")
}
escapes := []string{"\\", ".", ":", "*", "?", "-", "[", "]", "(", ")", "+", "{", "}", "|", "^", "$"}
r := string(s)
for _, escape := range escapes {
r = strings.ReplaceAll(r, escape, "\\"+escape)
}
return types.String(r)
}),
),
),
cel.Function("segments",
cel.Overload("segments_string_list_string",
[]*cel.Type{cel.StringType},

View File

@@ -1,13 +1,29 @@
package expressions
import (
"context"
"errors"
"net"
"strings"
"testing"
"github.com/TecharoHQ/anubis/internal/dns"
"github.com/TecharoHQ/anubis/lib/store/memory"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// newTestDNS is a helper function to create a new Dns object with an in-memory cache for testing.
func newTestDNS(forwardTTL int, reverseTTL int) *dns.Dns {
ctx := context.Background()
memStore := memory.New(ctx)
cache := dns.NewDNSCache(forwardTTL, reverseTTL, memStore)
return dns.New(ctx, cache)
}
func TestBotEnvironment(t *testing.T) {
env, err := BotEnvironment()
dnsObj := newTestDNS(300, 300)
env, err := BotEnvironment(dnsObj)
if err != nil {
t.Fatalf("failed to create bot environment: %v", err)
}
@@ -235,6 +251,344 @@ func TestBotEnvironment(t *testing.T) {
}
})
})
t.Run("regexSafe", func(t *testing.T) {
tests := []struct {
name string
expression string
expected types.String
description string
}{
{
name: "complex-test",
expression: `regexSafe("^(test1|test2|)[a-z]+$")`,
expected: types.String("\\^\\(test1\\|test2\\|\\)\\[a\\-z\\]\\+\\$"),
description: "should escape all reserved regex characters",
},
{
name: "backslash-test",
expression: `regexSafe("use \\\\ for special characters escaping\t, one/\"\\\"/for/cel and one/for/regex")`,
expected: types.String("use \\\\\\\\ for special characters escaping\t, one/\"\\\\\"/for/cel and one/for/regex"),
description: "should escape double-backslashes as double-double-backslashes and ignore cel escaping and forward slashes",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prog, err := Compile(env, tt.expression)
if err != nil {
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
}
result, _, err := prog.Eval(map[string]interface{}{})
if err != nil {
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
}
if result != tt.expected {
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
}
})
}
t.Run("function-compilation", func(t *testing.T) {
src := `regexSafe(".*")`
_, err := Compile(env, src)
if err != nil {
t.Fatalf("failed to compile regexSafe expression: %v", err)
}
})
})
t.Run("dnsFunctions", func(t *testing.T) {
originalDNSLookupAddr := dns.DNSLookupAddr
originalDNSLookupHost := dns.DNSLookupHost
defer func() {
dns.DNSLookupAddr = originalDNSLookupAddr
dns.DNSLookupHost = originalDNSLookupHost
}()
t.Run("reverseDNS", func(t *testing.T) {
tests := []struct {
name string
addr string
mockReturn []string
mockError error
expression string
expected ref.Val
description string
}{
{
name: "success",
addr: "8.8.8.8",
mockReturn: []string{"dns.google."},
expression: `reverseDNS("8.8.8.8")`,
expected: types.NewStringList(types.DefaultTypeAdapter, []string{"dns.google"}),
description: "should return domain names for an IP",
},
{
name: "not-found",
addr: "127.0.0.1",
mockReturn: []string{},
mockError: &net.DNSError{IsNotFound: true},
expression: `reverseDNS("127.0.0.1")`,
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
description: "should return an empty list when not found",
},
{
name: "error",
addr: "error-addr",
mockError: errors.New("some dns error"),
expression: `reverseDNS("error-addr")`,
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
description: "should return empty list on error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dns.DNSLookupAddr = func(addr string) ([]string, error) {
if addr == tt.addr {
return tt.mockReturn, tt.mockError
}
return nil, errors.New("unexpected address for reverse lookup")
}
prog, err := Compile(env, tt.expression)
if err != nil {
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
}
result, _, err := prog.Eval(map[string]interface{}{})
if err != nil {
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
}
if result.Equal(tt.expected) != types.True {
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
}
})
}
})
t.Run("lookupHost", func(t *testing.T) {
tests := []struct {
name string
host string
mockReturn []string
mockError error
expression string
expected ref.Val
description string
}{
{
name: "success",
host: "dns.google",
mockReturn: []string{"8.8.8.8", "8.8.4.4"},
expression: `lookupHost("dns.google")`,
expected: types.NewStringList(types.DefaultTypeAdapter, []string{"8.8.8.8", "8.8.4.4"}),
description: "should return IPs for a domain name",
},
{
name: "not-found",
host: "nonexistent.domain.example.com",
mockReturn: []string{},
mockError: &net.DNSError{IsNotFound: true},
expression: `lookupHost("nonexistent.domain.example.com")`,
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
description: "should return an empty list when not found",
},
{
name: "error",
host: "error-host",
mockError: errors.New("some dns error"),
expression: `lookupHost("error-host")`,
expected: types.NewStringList(types.DefaultTypeAdapter, []string{}),
description: "should return empty list on error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dns.DNSLookupHost = func(host string) ([]string, error) {
if host == tt.host {
return tt.mockReturn, tt.mockError
}
return nil, errors.New("unexpected host for forward lookup")
}
prog, err := Compile(env, tt.expression)
if err != nil {
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
}
result, _, err := prog.Eval(map[string]interface{}{})
if err != nil {
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
}
if result.Equal(tt.expected) != types.True {
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
}
})
}
})
t.Run("verifyFCrDNS", func(t *testing.T) {
tests := []struct {
name string
addr string
reverseMockReturn []string
reverseMockError error
forwardMockReturn map[string][]string // name -> ips
forwardMockError map[string]error
expression string
expected types.Bool
description string
}{
{
name: "success",
addr: "8.8.8.8",
reverseMockReturn: []string{"dns.google."},
forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8", "8.8.4.4"}},
expression: `verifyFCrDNS("8.8.8.8")`,
expected: types.Bool(true),
description: "should return true for valid FCrDNS",
},
{
name: "failure",
addr: "1.2.3.4",
reverseMockReturn: []string{"spoofed.example.com."},
forwardMockReturn: map[string][]string{"spoofed.example.com": {"5.6.7.8"}},
expression: `verifyFCrDNS("1.2.3.4")`,
expected: types.Bool(false),
description: "should return false for invalid FCrDNS",
},
{
name: "reverse-lookup-fails",
addr: "1.1.1.1",
reverseMockError: errors.New("reverse lookup failed"),
expression: `verifyFCrDNS("1.1.1.1")`,
expected: types.Bool(false),
description: "should return false if reverse lookup fails",
},
{
name: "success-with-pattern",
addr: "8.8.8.8",
reverseMockReturn: []string{"dns.google."},
forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8"}},
expression: `verifyFCrDNS("8.8.8.8", "dns.google")`,
expected: types.Bool(true),
description: "should return true for valid FCrDNS with matching pattern",
},
{
name: "failure-with-pattern",
addr: "8.8.8.8",
reverseMockReturn: []string{"dns.google."},
forwardMockReturn: map[string][]string{"dns.google": {"8.8.8.8"}},
expression: `verifyFCrDNS("8.8.8.8", "wrong.pattern")`,
expected: types.Bool(false),
description: "should return false for FCrDNS with non-matching pattern",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dns.DNSLookupAddr = func(addr string) ([]string, error) {
if addr == tt.addr {
return tt.reverseMockReturn, tt.reverseMockError
}
return nil, errors.New("unexpected address for reverse lookup")
}
dns.DNSLookupHost = func(host string) ([]string, error) {
host = strings.TrimSuffix(host, ".")
if ips, ok := tt.forwardMockReturn[host]; ok {
return ips, nil
}
if err, ok := tt.forwardMockError[host]; ok {
return nil, err
}
return nil, &net.DNSError{IsNotFound: true}
}
prog, err := Compile(env, tt.expression)
if err != nil {
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
}
result, _, err := prog.Eval(map[string]interface{}{})
if err != nil {
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
}
if result.Equal(tt.expected) != types.True {
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
}
})
}
})
t.Run("arpaReverseIP", func(t *testing.T) {
tests := []struct {
name string
expression string
expected types.String
description string
evalError bool
}{
{
name: "ipv4",
expression: `arpaReverseIP("1.2.3.4")`,
expected: types.String("4.3.2.1"),
description: "should correctly reverse an IPv4 address",
},
{
name: "ipv6",
expression: `arpaReverseIP("2001:db8::1")`,
expected: types.String("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2"),
description: "should correctly reverse an IPv6 address",
},
{
name: "ipv6-full",
expression: `arpaReverseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334")`,
expected: types.String("4.3.3.7.0.7.3.0.e.2.a.8.0.0.0.0.0.0.0.0.3.a.5.8.8.b.d.0.1.0.0.2"),
description: "should correctly reverse a fully expanded IPv6 address",
},
{
name: "ipv6-loopback",
expression: `arpaReverseIP("::1")`,
expected: types.String("1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0"),
description: "should correctly reverse the IPv6 loopback address",
},
{
name: "invalid-ip",
expression: `arpaReverseIP("not-an-ip")`,
evalError: true,
description: "should error on an invalid IP",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
prog, err := Compile(env, tt.expression)
if err != nil {
t.Fatalf("failed to compile expression %q: %v", tt.expression, err)
}
result, _, err := prog.Eval(map[string]interface{}{})
if tt.evalError {
if err == nil {
t.Errorf("%s: expected an evaluation error, but got none", tt.description)
}
return
}
if err != nil {
t.Fatalf("failed to evaluate expression %q: %v", tt.expression, err)
}
if result.Equal(tt.expected) != types.True {
t.Errorf("%s: expected %v, got %v", tt.description, tt.expected, result)
}
})
}
})
})
}
func TestThresholdEnvironment(t *testing.T) {

View File

@@ -11,6 +11,7 @@ import (
"time"
"github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/dns"
"github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/policy/checker"
"github.com/TecharoHQ/anubis/lib/store"
@@ -42,6 +43,8 @@ type ParsedConfig struct {
StatusCodes config.StatusCodes
DefaultDifficulty int
DNSBL bool
DnsCache *dns.DnsCache
Dns *dns.Dns
Logger *slog.Logger
}
@@ -66,6 +69,45 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
result := newParsedConfig(c)
result.DefaultDifficulty = defaultDifficulty
if c.Logging.Level != nil {
logLevel = c.Logging.Level.String()
}
switch c.Logging.Sink {
case config.LogSinkStdio:
result.Logger = internal.InitSlog(logLevel, os.Stderr)
case config.LogSinkFile:
out := &logrotate.Logger{
Filename: c.Logging.Parameters.Filename,
FilenameTimeFormat: time.RFC3339,
MaxBytes: c.Logging.Parameters.MaxBytes,
MaxAge: c.Logging.Parameters.MaxAge,
MaxBackups: c.Logging.Parameters.MaxBackups,
LocalTime: c.Logging.Parameters.UseLocalTime,
Compress: c.Logging.Parameters.Compress,
}
result.Logger = internal.InitSlog(logLevel, out)
}
lg := result.Logger.With("at", "config-validate")
stFac, ok := store.Get(c.Store.Backend)
switch ok {
case true:
store, err := stFac.Build(ctx, c.Store.Parameters)
if err != nil {
validationErrs = append(validationErrs, err)
} else {
result.Store = store
}
case false:
validationErrs = append(validationErrs, config.ErrUnknownStoreBackend)
}
result.DnsCache = dns.NewDNSCache(result.orig.DNSTTL.Forward, result.orig.DNSTTL.Reverse, result.Store)
result.Dns = dns.New(ctx, result.DnsCache)
for _, b := range c.Bots {
if berr := b.Valid(); berr != nil {
validationErrs = append(validationErrs, berr)
@@ -116,7 +158,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
}
if b.Expression != nil {
c, err := NewCELChecker(b.Expression)
c, err := NewCELChecker(b.Expression, result.Dns)
if err != nil {
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
} else {
@@ -126,7 +168,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
if b.ASNs != nil {
if !hasThothClient {
slog.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "asn", "settings", b.ASNs)
lg.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "asn", "settings", b.ASNs)
continue
}
@@ -135,7 +177,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
if b.GeoIP != nil {
if !hasThothClient {
slog.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "geoip", "settings", b.GeoIP)
lg.Warn("You have specified a Thoth specific check but you have no Thoth client configured. Please read https://anubis.techaro.lol/docs/admin/thoth for more information", "check", "geoip", "settings", b.GeoIP)
continue
}
@@ -145,7 +187,6 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
if b.Challenge == nil {
parsedBot.Challenge = &config.ChallengeRules{
Difficulty: defaultDifficulty,
ReportAs: defaultDifficulty,
Algorithm: "fast",
}
} else {
@@ -155,7 +196,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
}
if parsedBot.Challenge.Algorithm == "slow" {
slog.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name)
lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name)
}
}
@@ -172,17 +213,20 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
for _, t := range c.Thresholds {
if t.Challenge != nil && t.Challenge.Algorithm == "slow" {
slog.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name)
lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name)
}
if t.Challenge != nil && t.Challenge.ReportAs != 0 {
lg.Warn("use of deprecated report_as setting detected, please remove this from your policy file when possible", "name", t.Name)
}
if t.Name == "legacy-anubis-behaviour" && t.Expression.String() == "true" {
if !warnedAboutThresholds.Load() {
slog.Warn("configuration file does not contain thresholds, see docs for details on how to upgrade", "fname", fname, "docs_url", "https://anubis.techaro.lol/docs/admin/configuration/thresholds/")
lg.Warn("configuration file does not contain thresholds, see docs for details on how to upgrade", "fname", fname, "docs_url", "https://anubis.techaro.lol/docs/admin/configuration/thresholds/")
warnedAboutThresholds.Store(true)
}
t.Challenge.Difficulty = defaultDifficulty
t.Challenge.ReportAs = defaultDifficulty
}
threshold, err := ParsedThresholdFromConfig(t)
@@ -194,40 +238,6 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
result.Thresholds = append(result.Thresholds, threshold)
}
stFac, ok := store.Get(c.Store.Backend)
switch ok {
case true:
store, err := stFac.Build(ctx, c.Store.Parameters)
if err != nil {
validationErrs = append(validationErrs, err)
} else {
result.Store = store
}
case false:
validationErrs = append(validationErrs, config.ErrUnknownStoreBackend)
}
if c.Logging.Level != nil {
logLevel = c.Logging.Level.String()
}
switch c.Logging.Sink {
case config.LogSinkStdio:
result.Logger = internal.InitSlog(logLevel, os.Stderr)
case config.LogSinkFile:
out := &logrotate.Logger{
Filename: c.Logging.Parameters.Filename,
FilenameTimeFormat: time.RFC3339,
MaxBytes: c.Logging.Parameters.MaxBytes,
MaxAge: c.Logging.Parameters.MaxAge,
MaxBackups: c.Logging.Parameters.MaxBackups,
LocalTime: c.Logging.Parameters.UseLocalTime,
Compress: c.Logging.Parameters.Compress,
}
result.Logger = internal.InitSlog(logLevel, out)
}
if len(validationErrs) > 0 {
return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...))
}

View File

@@ -4,5 +4,4 @@ bots:
action: CHALLENGE
challenge:
difficulty: 16
report_as: 4
algorithm: hunter2 # invalid algorithm

View File

@@ -42,4 +42,3 @@ thresholds:
challenge:
algorithm: fast
difficulty: 1
report_as: 1

View File

@@ -42,4 +42,3 @@ thresholds:
challenge:
algorithm: fast
difficulty: 0
report_as: 0

View File

@@ -4,7 +4,6 @@ bots:
action: CHALLENGE
challenge:
difficulty: 2
report_as: 2
algorithm: fast
status_codes:

View File

@@ -155,7 +155,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
return;
}
status.innerHTML = `${t('calculating_difficulty')} ${rules.report_as}, `;
status.innerHTML = `${t('calculating_difficulty')} ${rules.difficulty}, `;
progress.style.display = "inline-block";
// the whole text, including "Speed:", as a single node, because some browsers
@@ -166,7 +166,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
let lastSpeedUpdate = 0;
let showingApology = false;
const likelihood = Math.pow(16, -rules.report_as);
const likelihood = Math.pow(16, -rules.difficulty);
try {
const t0 = Date.now();