Compare commits

..

1 Commits

Author SHA1 Message Date
Xe Iaso
462f692355 docs: document how to import the default config
Signed-off-by: Xe Iaso <me@xeiaso.net>
2026-01-06 15:47:21 -05:00
52 changed files with 591 additions and 1816 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ jobs:
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: '1.25.4' go-version: stable
- name: Check go.mod and go.sum in main directory - name: Check go.mod and go.sum in main directory
run: | run: |
@@ -73,4 +73,4 @@ jobs:
exit 1 exit 1
fi 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,10 +26,11 @@ jobs:
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
node-version: '24.11.0' node-version: latest
- uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with: with:
go-version: '1.25.4' go-version: stable
- name: Cache playwright binaries - name: Cache playwright binaries
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
# Assert behaviour that only genuine browsers display. This ensures that modern Chrome
# or Firefox versions will get through without a challenge.
#
# These rules have been known to be bypassed by some of the worst automated scrapers.
# Use at your own risk.
- name: realistic-browser-catchall
expression:
all:
- '"User-Agent" in headers'
- '( userAgent.contains("Firefox") ) || ( userAgent.contains("Chrome") ) || ( userAgent.contains("Safari") )'
- '"Accept" in headers'
- '"Sec-Fetch-Dest" in headers'
- '"Sec-Fetch-Mode" in headers'
- '"Sec-Fetch-Site" in headers'
- '"Accept-Encoding" in headers'
- '( headers["Accept-Encoding"].contains("zstd") || headers["Accept-Encoding"].contains("br") )'
- '"Accept-Language" in headers'
action: WEIGH
weight:
adjust: -10
# The Upgrade-Insecure-Requests header is typically sent by browsers, but not always
- name: upgrade-insecure-requests
expression: '"Upgrade-Insecure-Requests" in headers'
action: WEIGH
weight:
adjust: -2
# Chrome should behave like Chrome
- name: chrome-is-proper
expression:
all:
- userAgent.contains("Chrome")
- '"Sec-Ch-Ua" in headers'
- 'headers["Sec-Ch-Ua"].contains("Chromium")'
- '"Sec-Ch-Ua-Mobile" in headers'
- '"Sec-Ch-Ua-Platform" in headers'
action: WEIGH
weight:
adjust: -5
- name: should-have-accept
expression: '!("Accept" in headers)'
action: WEIGH
weight:
adjust: 5
# Generic catchall rule
- name: generic-browser
user_agent_regex: >-
Mozilla|Opera
action: WEIGH
weight:
adjust: 10

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,42 +24,6 @@ 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)) - 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. - 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 ### 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: 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:
@@ -80,31 +44,6 @@ logging:
Additionally, information about [how Anubis uses each logging level](./admin/policies.mdx#log-levels) has been added to the documentation. 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 ## v1.23.1: Lyse Hext - Echo 1
- Fix `SERVE_ROBOTS_TXT` setting after the double slash fix broke it. - Fix `SERVE_ROBOTS_TXT` setting after the double slash fix broke it.

View File

@@ -12,6 +12,7 @@ To use it in your Anubis configuration:
action: CHALLENGE action: CHALLENGE
challenge: challenge:
difficulty: 1 # Number of seconds to wait before refreshing the page 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 algorithm: metarefresh # Specify a non-JS challenge method
``` ```

View File

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

View File

@@ -233,27 +233,6 @@ 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. 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` ### `segments`
Available in `bot` expressions. Available in `bot` expressions.
@@ -287,99 +266,6 @@ This is useful if you want to write rules that allow requests that have no query
- size(segments(path)) < 2 - 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 ## 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. 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

@@ -13,6 +13,8 @@ bots:
- # This correlates to data/bots/ai-catchall.yaml in the source tree - # This correlates to data/bots/ai-catchall.yaml in the source tree
import: (data)/bots/ai-catchall.yaml import: (data)/bots/ai-catchall.yaml
- import: (data)/bots/cloudflare-workers.yaml - import: (data)/bots/cloudflare-workers.yaml
# Import all the rules in the default configuration
- import: (data)/meta/default-config.yaml
``` ```
Of note, a bot rule can either have inline bot configuration or import a bot config snippet. You cannot do both in a single bot rule. Of note, a bot rule can either have inline bot configuration or import a bot config snippet. You cannot do both in a single bot rule.
@@ -35,6 +37,33 @@ config.BotOrImport: rule definition is invalid, you must set either bot rules or
Paths can either be prefixed with `(data)` to import from the [the data folder in the Anubis source tree](https://github.com/TecharoHQ/anubis/tree/main/data) or anywhere on the filesystem. If you don't have access to the Anubis source tree, check /usr/share/docs/anubis/data or in the tarball you extracted Anubis from. Paths can either be prefixed with `(data)` to import from the [the data folder in the Anubis source tree](https://github.com/TecharoHQ/anubis/tree/main/data) or anywhere on the filesystem. If you don't have access to the Anubis source tree, check /usr/share/docs/anubis/data or in the tarball you extracted Anubis from.
## Importing the default configuration
If you want to base your configuration off of the default configuration, import `(data)/meta/default-config.yaml`:
```yaml
bots:
- import: (data)/meta/default-config.yaml
# Write your rules here
```
This will keep your configuration up to date as Anubis adapts to emerging threats.
## How do I exempt most modern browsers from Anubis challenges?
If you want to exempt most modern browsers from Anubis challenges, import `(data)/common/acts-like-browser.yaml`:
```yaml
bots:
- import: (data)/meta/default-config.yaml
- import: (data)/common/acts-like-browser.yaml
# Write your rules here
```
These rules will allow traffic that "looks like" it's from a modern copy of Edge, Safari, Chrome, or Firefox. These rules used to be enabled by default, however user reports have suggested that AI scraper bots have adapted to conform to these rules to scrape without regard for the infrastructure they are attacking.
Use these rules at your own risk.
## Importing from imports ## Importing from imports
You can also import from an imported file in case you want to import an entire folder of rules at once. You can also import from an imported file in case you want to import an entire folder of rules at once.

View File

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

View File

@@ -84,6 +84,7 @@ This rule has been known to have a high false positive rate in testing. Please u
action: CHALLENGE action: CHALLENGE
challenge: challenge:
difficulty: 16 # impossible difficulty: 16 # impossible
report_as: 4 # lie to the operator
algorithm: slow # intentionally waste CPU cycles and time algorithm: slow # intentionally waste CPU cycles and time
``` ```
@@ -92,6 +93,7 @@ Challenges can be configured with these settings:
| Key | Example | Description | | 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. | | `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. | | `algorithm` | `"fast"` | The challenge method to use. See [the list of challenge methods](./configuration/challenges/) for more information. |
### Remote IP based filtering ### Remote IP based filtering

View File

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

View File

@@ -1,70 +0,0 @@
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)
}

View File

@@ -1,174 +0,0 @@
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
}

View File

@@ -1,308 +0,0 @@
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 { if rule.Challenge.Difficulty == 0 {
rule.Challenge.Difficulty = chall.Difficulty rule.Challenge.Difficulty = chall.Difficulty
} }
if rule.Challenge.ReportAs != 0 { 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) rule.Challenge.ReportAs = chall.Difficulty
} }
if rule.Challenge.Algorithm == "" { if rule.Challenge.Algorithm == "" {
rule.Challenge.Algorithm = chall.Method rule.Challenge.Algorithm = chall.Method
@@ -648,6 +648,7 @@ func (s *Server) check(r *http.Request, lg *slog.Logger) (policy.CheckResult, *p
return cr("default/allow", config.RuleAllow, weight), &policy.Bot{ return cr("default/allow", config.RuleAllow, weight), &policy.Bot{
Challenge: &config.ChallengeRules{ Challenge: &config.ChallengeRules{
Difficulty: s.policy.DefaultDifficulty, Difficulty: s.policy.DefaultDifficulty,
ReportAs: s.policy.DefaultDifficulty,
Algorithm: config.DefaultAlgorithm, Algorithm: config.DefaultAlgorithm,
}, },
Rules: &checker.List{}, Rules: &checker.List{},

View File

@@ -464,6 +464,10 @@ func TestCheckDefaultDifficultyMatchesPolicy(t *testing.T) {
if bot.Challenge.Difficulty != i { if bot.Challenge.Difficulty != i {
t.Errorf("Challenge.Difficulty is wrong, wanted %d, got: %d", i, bot.Challenge.Difficulty) 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,6 +36,7 @@ func TestBasic(t *testing.T) {
Challenge: &config.ChallengeRules{ Challenge: &config.ChallengeRules{
Algorithm: "fast", Algorithm: "fast",
Difficulty: 0, Difficulty: 0,
ReportAs: 0,
}, },
} }
const challengeStr = "hunter" const challengeStr = "hunter"

View File

@@ -332,7 +332,6 @@ type fileConfig struct {
Thresholds []Threshold `json:"thresholds"` Thresholds []Threshold `json:"thresholds"`
StatusCodes StatusCodes `json:"status_codes"` StatusCodes StatusCodes `json:"status_codes"`
DNSBL bool `json:"dnsbl"` DNSBL bool `json:"dnsbl"`
DNSTTL DnsTTL `json:"dns_ttl"`
Logging *Logging `json:"logging"` Logging *Logging `json:"logging"`
} }
@@ -388,10 +387,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
Challenge: http.StatusOK, Challenge: http.StatusOK,
Deny: http.StatusOK, Deny: http.StatusOK,
}, },
DNSTTL: DnsTTL{
Forward: 300,
Reverse: 300,
},
Store: &Store{ Store: &Store{
Backend: "memory", Backend: "memory",
}, },
@@ -407,8 +402,7 @@ func Load(fin io.Reader, fname string) (*Config, error) {
} }
result := &Config{ result := &Config{
DNSBL: c.DNSBL, DNSBL: c.DNSBL,
DNSTTL: c.DNSTTL,
OpenGraph: OpenGraph{ OpenGraph: OpenGraph{
Enabled: c.OpenGraph.Enabled, Enabled: c.OpenGraph.Enabled,
ConsiderHost: c.OpenGraph.ConsiderHost, ConsiderHost: c.OpenGraph.ConsiderHost,
@@ -475,29 +469,6 @@ func Load(fin io.Reader, fname string) (*Config, error) {
return result, nil 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 { type Config struct {
Impressum *Impressum Impressum *Impressum
Store *Store Store *Store
@@ -507,7 +478,6 @@ type Config struct {
StatusCodes StatusCodes StatusCodes StatusCodes
Logging *Logging Logging *Logging
DNSBL bool DNSBL bool
DNSTTL DnsTTL
} }
func (c Config) Valid() error { func (c Config) Valid() error {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,6 @@ import (
"math/rand/v2" "math/rand/v2"
"strings" "strings"
"github.com/TecharoHQ/anubis/internal/dns"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/ref"
@@ -16,7 +15,7 @@ import (
// variables and functions that are passed into the CEL scope so that // variables and functions that are passed into the CEL scope so that
// Anubis can fail loudly and early when something is invalid instead // Anubis can fail loudly and early when something is invalid instead
// of blowing up at runtime. // of blowing up at runtime.
func BotEnvironment(dnsObj *dns.Dns) (*cel.Env, error) { func BotEnvironment() (*cel.Env, error) {
return New( return New(
// Variables exposed to CEL programs: // Variables exposed to CEL programs:
cel.Variable("remoteAddress", cel.StringType), cel.Variable("remoteAddress", cel.StringType),
@@ -58,118 +57,6 @@ func BotEnvironment(dnsObj *dns.Dns) (*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.Function("segments",
cel.Overload("segments_string_list_string", cel.Overload("segments_string_list_string",
[]*cel.Type{cel.StringType}, []*cel.Type{cel.StringType},

View File

@@ -1,29 +1,13 @@
package expressions package expressions
import ( import (
"context"
"errors"
"net"
"strings"
"testing" "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"
"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) { func TestBotEnvironment(t *testing.T) {
dnsObj := newTestDNS(300, 300) env, err := BotEnvironment()
env, err := BotEnvironment(dnsObj)
if err != nil { if err != nil {
t.Fatalf("failed to create bot environment: %v", err) t.Fatalf("failed to create bot environment: %v", err)
} }
@@ -251,344 +235,6 @@ 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) { func TestThresholdEnvironment(t *testing.T) {

View File

@@ -11,7 +11,6 @@ import (
"time" "time"
"github.com/TecharoHQ/anubis/internal" "github.com/TecharoHQ/anubis/internal"
"github.com/TecharoHQ/anubis/internal/dns"
"github.com/TecharoHQ/anubis/lib/config" "github.com/TecharoHQ/anubis/lib/config"
"github.com/TecharoHQ/anubis/lib/policy/checker" "github.com/TecharoHQ/anubis/lib/policy/checker"
"github.com/TecharoHQ/anubis/lib/store" "github.com/TecharoHQ/anubis/lib/store"
@@ -43,8 +42,6 @@ type ParsedConfig struct {
StatusCodes config.StatusCodes StatusCodes config.StatusCodes
DefaultDifficulty int DefaultDifficulty int
DNSBL bool DNSBL bool
DnsCache *dns.DnsCache
Dns *dns.Dns
Logger *slog.Logger Logger *slog.Logger
} }
@@ -69,45 +66,6 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
result := newParsedConfig(c) result := newParsedConfig(c)
result.DefaultDifficulty = defaultDifficulty 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 { for _, b := range c.Bots {
if berr := b.Valid(); berr != nil { if berr := b.Valid(); berr != nil {
validationErrs = append(validationErrs, berr) validationErrs = append(validationErrs, berr)
@@ -158,7 +116,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
} }
if b.Expression != nil { if b.Expression != nil {
c, err := NewCELChecker(b.Expression, result.Dns) c, err := NewCELChecker(b.Expression)
if err != nil { if err != nil {
validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err)) validationErrs = append(validationErrs, fmt.Errorf("while processing rule %s expressions: %w", b.Name, err))
} else { } else {
@@ -168,7 +126,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
if b.ASNs != nil { if b.ASNs != nil {
if !hasThothClient { if !hasThothClient {
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) 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)
continue continue
} }
@@ -177,7 +135,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
if b.GeoIP != nil { if b.GeoIP != nil {
if !hasThothClient { if !hasThothClient {
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) 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)
continue continue
} }
@@ -187,6 +145,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
if b.Challenge == nil { if b.Challenge == nil {
parsedBot.Challenge = &config.ChallengeRules{ parsedBot.Challenge = &config.ChallengeRules{
Difficulty: defaultDifficulty, Difficulty: defaultDifficulty,
ReportAs: defaultDifficulty,
Algorithm: "fast", Algorithm: "fast",
} }
} else { } else {
@@ -196,7 +155,7 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
} }
if parsedBot.Challenge.Algorithm == "slow" { if parsedBot.Challenge.Algorithm == "slow" {
lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name) slog.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", parsedBot.Name)
} }
} }
@@ -213,20 +172,17 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
for _, t := range c.Thresholds { for _, t := range c.Thresholds {
if t.Challenge != nil && t.Challenge.Algorithm == "slow" { if t.Challenge != nil && t.Challenge.Algorithm == "slow" {
lg.Warn("use of deprecated algorithm \"slow\" detected, please update this to \"fast\" when possible", "name", t.Name) slog.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 t.Name == "legacy-anubis-behaviour" && t.Expression.String() == "true" {
if !warnedAboutThresholds.Load() { if !warnedAboutThresholds.Load() {
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/") 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/")
warnedAboutThresholds.Store(true) warnedAboutThresholds.Store(true)
} }
t.Challenge.Difficulty = defaultDifficulty t.Challenge.Difficulty = defaultDifficulty
t.Challenge.ReportAs = defaultDifficulty
} }
threshold, err := ParsedThresholdFromConfig(t) threshold, err := ParsedThresholdFromConfig(t)
@@ -238,6 +194,40 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
result.Thresholds = append(result.Thresholds, threshold) 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 { if len(validationErrs) > 0 {
return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...)) return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...))
} }

View File

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

View File

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

View File

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

View File

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

View File

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