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>
This commit is contained in:
parent
4ead3ed16e
commit
00fa939acf
22 changed files with 1652 additions and 480 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
8
lib/config/testdata/bad/dns-ttl-custom.yaml
vendored
Normal file
8
lib/config/testdata/bad/dns-ttl-custom.yaml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
dns_ttl:
|
||||
forward: 60.0
|
||||
reverse: "600"
|
||||
|
||||
bots:
|
||||
- name: "test"
|
||||
user_agent_regex: ".*"
|
||||
action: "DENY"
|
||||
8
lib/config/testdata/good/dns-ttl-custom.yaml
vendored
Normal file
8
lib/config/testdata/good/dns-ttl-custom.yaml
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
dns_ttl:
|
||||
forward: 600
|
||||
reverse: 600
|
||||
|
||||
bots:
|
||||
- name: "test"
|
||||
user_agent_regex: ".*"
|
||||
action: "DENY"
|
||||
|
|
@ -15,7 +15,6 @@
|
|||
"nb",
|
||||
"nl",
|
||||
"nn",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
"ru",
|
||||
"tr",
|
||||
|
|
|
|||
|
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"loading": "Ładowanie...",
|
||||
"why_am_i_seeing": "Dlaczego to widzę?",
|
||||
"protected_by": "Chronione przez",
|
||||
"protected_from": "Przed",
|
||||
"made_with": "Stworzone z ❤️ w 🇨🇦",
|
||||
"mascot_design": "Projekt maskotki:",
|
||||
"ai_companies_explanation": "Widzisz to, ponieważ administrator tej strony skonfigurował Anubisa, aby chronić serwer przed masowym skanowaniem treści przez firmy tworzące AI. Powoduje to obciążenie i przestoje, przez co zasoby strony stają się niedostępne dla wszystkich.",
|
||||
"anubis_compromise": "Anubis jest kompromisem. Używa mechanizmu Proof-of-Work w stylu Hashcash — proponowanego systemu ograniczania spamu e-mail. Pomysł polega na tym, że dla indywidualnych użytkowników dodatkowe obciążenie jest niezauważalne, ale w skali masowego skanowania koszt szybko rośnie.",
|
||||
"hack_purpose": "Docelowo jest to rozwiązanie tymczasowe, aby zyskać czas na ulepszenie metod identyfikacji przeglądarek bez interfejsu graficznego (np. poprzez analizę renderowania czcionek), by w przyszłości nie musieć wyświetlać strony z zadaniem Proof-of-Work użytkownikom, którzy najprawdopodobniej są prawidłowi.",
|
||||
"simplified_explanation": "To zabezpieczenie przed botami i złośliwymi żądaniami, podobne do CAPTCHA. Jednak zamiast wykonywać zadanie samodzielnie, przeglądarka otrzymuje obliczenie do wykonania, aby potwierdzić, że jest prawidłowym klientem. Ten mechanizm to <a href=\"https://en.wikipedia.org/wiki/Proof_of_work\">Proof of Work</a>. Zadanie trwa kilka sekund i uzyskujesz dostęp do strony. Dziękujemy za cierpliwość.",
|
||||
"jshelter_note": "Uwaga: Anubis wymaga nowoczesnych funkcji JavaScript, które wtyczki typu JShelter mogą blokować. Wyłącz JShelter lub podobne dodatki dla tej domeny.",
|
||||
"version_info": "Ta strona działa na Anubis w wersji",
|
||||
"try_again": "Spróbuj ponownie",
|
||||
"go_home": "Wróć na stronę główną",
|
||||
"contact_webmaster": "lub jeśli uważasz, że nie powinieneś być blokowany, skontaktuj się z administratorem pod adresem",
|
||||
"connection_security": "Poczekaj chwilę, sprawdzamy bezpieczeństwo Twojego połączenia.",
|
||||
"javascript_required": "Niestety, aby przejść tę próbę, musisz włączyć obsługę JavaScript. Jest to konieczne, ponieważ firmy zajmujące się sztuczną inteligencją zmieniły umowę społeczną dotyczącą funkcjonowania hostingu stron internetowych. Rozwiązanie bez obsługi JavaScript jest w trakcie opracowywania.",
|
||||
"benchmark_requires_js": "Uruchomienie narzędzia testowego wymaga włączonego JavaScript.",
|
||||
"difficulty": "Trudność:",
|
||||
"algorithm": "Algorytm:",
|
||||
"compare": "Porównaj:",
|
||||
"time": "Czas",
|
||||
"iters": "Iteracje",
|
||||
"time_a": "Czas A",
|
||||
"iters_a": "Iteracje A",
|
||||
"time_b": "Czas B",
|
||||
"iters_b": "Iteracje B",
|
||||
"static_check_endpoint": "To jedynie punkt kontrolny do użytku przez Twój reverse proxy.",
|
||||
"authorization_required": "Wymagane uwierzytelnienie",
|
||||
"cookies_disabled": "Twoja przeglądarka blokuje ciasteczka. Anubis wymaga ich, aby potwierdzić, że jesteś prawidłowym klientem. Włącz ciasteczka dla tej domeny.",
|
||||
"access_denied": "Brak dostępu: kod błędu",
|
||||
"dronebl_entry": "DroneBL zgłosił wpis",
|
||||
"see_dronebl_lookup": "zobacz",
|
||||
"internal_server_error": "Błąd wewnętrzny serwera: administrator błędnie skonfigurował Anubis. Skontaktuj się z administratorem i poproś o sprawdzenie logów",
|
||||
"invalid_redirect": "Nieprawidłowe przekierowanie",
|
||||
"redirect_not_parseable": "Nie można odczytać adresu przekierowania",
|
||||
"redirect_domain_not_allowed": "Domena przekierowania niedozwolona",
|
||||
"missing_required_forwarded_headers": "Brak wymaganych nagłówków X-Forwarded-*",
|
||||
"failed_to_sign_jwt": "Nie udało się podpisać JWT",
|
||||
"invalid_invocation": "Nieprawidłowe wywołanie MakeChallenge",
|
||||
"client_error_browser": "Błąd klienta: upewnij się, że Twoja przeglądarka jest aktualna i spróbuj ponownie później.",
|
||||
"oh_noes": "O nie!",
|
||||
"benchmarking_anubis": "Testowanie wydajności Anubis!",
|
||||
"you_are_not_a_bot": "Nie jesteś botem!",
|
||||
"making_sure_not_bot": "Sprawdzamy, czy nie jesteś botem!",
|
||||
"celphase": "CELPHASE",
|
||||
"js_web_crypto_error": "Twoja przeglądarka nie obsługuje web.crypto. Czy korzystasz z bezpiecznego połączenia?",
|
||||
"js_web_workers_error": "Twoja przeglądarka nie obsługuje web workers (Anubis ich używa, by nie zawieszać przeglądarki). Czy masz zainstalowaną wtyczkę typu JShelter?",
|
||||
"js_cookies_error": "Twoja przeglądarka nie zapisuje ciasteczek. Anubis używa ich do przechowywania podpisanego tokenu potwierdzającego przejście zabezpieczenia. Włącz zapis ciasteczek dla tej domeny. Nazwy ciasteczek mogą zmieniać się bez zapowiedzi. Nazwy oraz zawartość ciasteczek nie są cześcią publicznego API.",
|
||||
"js_context_not_secure": "Kontekst nie jest bezpieczny!",
|
||||
"js_context_not_secure_msg": "Spróbuj połączyć się przez HTTPS lub poinformuj administratora, by skonfigurował HTTPS. Więcej informacji na <a href=\"https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure\">MDN</a>.",
|
||||
"js_calculating": "Obliczanie...",
|
||||
"js_missing_feature": "Brakująca funkcja",
|
||||
"js_challenge_error": "Błąd wyzwania!",
|
||||
"js_challenge_error_msg": "Nie udało się ustalić algorytmu sprawdzającego. Możesz spróbować odświeżyć stronę.",
|
||||
"js_calculating_difficulty": "Obliczanie...<br/>Trudność:",
|
||||
"js_speed": "Prędkość:",
|
||||
"js_verification_longer": "Weryfikacja trwa dłużej niż zwykle. Proszę nie odświeżać strony.",
|
||||
"js_success": "Sukces!",
|
||||
"js_done_took": "Gotowe! Zajęło to",
|
||||
"js_iterations": "iteracji",
|
||||
"js_finished_reading": "Skończyłem czytać, kontynuuj →",
|
||||
"js_calculation_error": "Błąd obliczeń!",
|
||||
"js_calculation_error_msg": "Nie udało się obliczyć zadania:"
|
||||
}
|
||||
|
|
@ -24,7 +24,6 @@ func TestLocalizationService(t *testing.T) {
|
|||
"nb": "Laster inn...",
|
||||
"nl": "Laden...",
|
||||
"nn": "Lastar inn...",
|
||||
"pl": "Ładowanie...",
|
||||
"pt-BR": "Carregando...",
|
||||
"tr": "Yükleniyor...",
|
||||
"ru": "Загрузка...",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +92,22 @@ func ParseConfig(ctx context.Context, fin io.Reader, fname string, defaultDiffic
|
|||
|
||||
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)
|
||||
|
|
@ -139,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 {
|
||||
|
|
@ -219,19 +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 len(validationErrs) > 0 {
|
||||
return nil, fmt.Errorf("errors validating policy config JSON %s: %w", fname, errors.Join(validationErrs...))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue