diff --git a/CHALLENGES.md b/CHALLENGES.md index ba7985b..abbd33f 100644 --- a/CHALLENGES.md +++ b/CHALLENGES.md @@ -13,14 +13,15 @@ For example, this allows verifying the user cookies against the backend to have Example on Forgejo, checks that current user is authenticated: ```yaml http-cookie-check: - mode: http - url: http://forgejo:3000/user/stopwatches - # url: http://forgejo:3000/repo/search - # url: http://forgejo:3000/notifications/new + runtime: http parameters: + http-url: http://forgejo:3000/user/stopwatches + # http-url: http://forgejo:3000/repo/search + # http-url: http://forgejo:3000/notifications/new http-method: GET http-cookie: i_like_gitea http-code: 200 + verify-probability: 0.1 ``` ### preload-link @@ -35,16 +36,9 @@ Example: ```yaml self-preload-link: condition: '"Sec-Fetch-Mode" in headers && headers["Sec-Fetch-Mode"] == "navigate"' - mode: "preload-link" - runtime: - # verifies that result = key - mode: "key" - probability: 0.1 + runtime: "preload-link" parameters: preload-early-hint-deadline: 3s - key-code: 200 - key-mime: text/css - key-content: "" ``` ## Non-JavaScript @@ -76,20 +70,6 @@ Requires HTTP and HTML response parsing and logic, displays challenge site. Servers a challenge page with a linked resource that is loaded by the browser, which solves the challenge. Page refreshes a few seconds later via [Refresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Refresh). -Example: -```yaml - self-resource-load: - mode: "resource-load" - runtime: - # verifies that result = key - mode: "key" - probability: 0.1 - parameters: - key-code: 200 - key-mime: text/css - key-content: "" -``` - ## Custom JavaScript ### js-pow-sha256 @@ -101,18 +81,18 @@ Has the user solve a Proof of Work using SHA256 hashes, with configurable diffic Example: ```yaml js-pow-sha256: - # Asset must be under challenges/{name}/static/{asset} - # Other files here will be available under that path - mode: js - asset: load.mjs + runtime: js parameters: - # difficulty is number of bits that must be set to 0 from start - # Anubis challenge difficulty 5 becomes 5 * 8 = 20 - difficulty: 20 - runtime: - mode: wasm - # Verify must be under challenges/{name}/runtime/{asset} - asset: runtime.wasm - probability: 0.02 + # specifies the folder path that assets are under + # can be either embedded or external path + # defaults to name of challenge + path: "js-pow-sha256" + # needs to be under static folder + js-loader: load.mjs + # needs to be under runtime folder + wasm-runtime: runtime.wasm + wasm-runtime-settings: + difficulty: 20 + verify-probability: 0.02 ``` diff --git a/README.md b/README.md index eaeb121..b5ffd04 100644 --- a/README.md +++ b/README.md @@ -42,26 +42,19 @@ Rules and conditions are served with this environment: ``` remoteAddress (net.IP) - Connecting client remote address from headers or properties + remoteAddress.network(networkName string) bool - Check whether a given IP is listed on the underlying defined network + remoteAddress.network(networkCIDR string) bool - Check whether a given IP is listed on the CIDR host (string) - HTTP Host method (string) - HTTP Method/Verb userAgent (string) - HTTP User-Agent header path (string) - HTTP request Path query (map[string]string) - HTTP request Query arguments headers (map[string]string) - HTTP request headers - +fp (map[string]string) - Available fingerprints + Only available when TLS is enabled - fpJA3N (string) JA3N TLS Fingerprint - fpJA4 (string) JA4 TLS Fingerprint -``` - -Additionally, these functions are available: -``` -Check whether a given IP is listed on the underlying defined network or CIDR - inNetwork(networkName string, address net.IP) bool - inNetwork(networkCIDR string, address net.IP) bool - -Check whether a given IP is listed on the provided DNSBL - inDNSBL(address net.IP) bool + fp.ja3n (string) JA3N TLS Fingerprint + fp.ja4 (string) JA4 TLS Fingerprint ``` ### Template support @@ -77,14 +70,23 @@ External templates for your site can be loaded specifying a full path to the `.g ### Extended rule actions -In addition to the common PASS / CHALLENGE / DENY rules, we offer CHECK and POISON. +In addition to the common PASS / CHALLENGE / DENY rules, go-away offers more actions that can be extended via code. + +| Action | Behavior | Terminating | +|:---------:|:------------------------------------------------------------------------|:-----------:| +| PASS | Passes the request to the backend immediately | Yes | +| DENY | Denies the request with a descriptive page | Yes | +| BLOCK | Denies the request with a response code | Yes | +| DROP | Drops the connection without sending a reply | Yes | +| CHALLENGE | Issues a challenge that when passed, acts like PASS | Yes | +| CHECK | Issues a challenge that when passed, continues executing rules | No | +| PROXY | Proxies request to a different backend, with optional path replacements | Yes | + CHECK allows the client to be challenged but continue matching rules after these, for example, chaining a list of challenges that must be passed. For example, you could use this to implement browser in checks without explicitly allowing all requests, and later deferring to a secondary check/challenge. -POISON sends defined responses to bad clients that will annoy them. -This must be configured by the operator, some networks have been seen to only stop when served back this output. -Currently, an HTML payload exists that uncompressed to about one GiB of nonsense DOM. You could use this to send garbage for would-be training data. +PROXY allows the operator to send matching requests to a different backend, for example, a poison generator or a scraping maze. ### Multiple challenge matching @@ -94,7 +96,8 @@ For example: ```yaml - name: standard-browser action: challenge - challenges: [http-cookie-check, self-preload-link, self-meta-refresh, self-resource-load, js-pow-sha256] + settings: + challenges: [http-cookie-check, self-preload-link, self-meta-refresh, self-resource-load, js-pow-sha256] conditions: - '($is-generic-browser)' ``` @@ -394,6 +397,7 @@ services: # specify a DNSBL for usage in conditions. Defaults to DroneBL # GOAWAY_DNSBL: "dnsbl.dronebl.org" + # Backend to match. Can be subdomain or full wildcards, "*.example.com" or "*" GOAWAY_BACKEND: "git.example.com=http://forgejo:3000" # additional backends can be specified via more command arguments diff --git a/build-poison.sh b/build-poison.sh deleted file mode 100755 index 7de3ee4..0000000 --- a/build-poison.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -set -e -set -o pipefail - -cd "$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)" - - -go run ./generate-poison -path ./poison/ \ No newline at end of file diff --git a/cmd/generate-poison/main.go b/cmd/generate-poison/main.go deleted file mode 100644 index f7c4bb8..0000000 --- a/cmd/generate-poison/main.go +++ /dev/null @@ -1,180 +0,0 @@ -package main - -import ( - "bytes" - "compress/gzip" - "flag" - "fmt" - "github.com/andybalholm/brotli" - "github.com/klauspost/compress/zstd" - "io" - "math/rand/v2" - "os" - "path" - "slices" - "strings" - "sync" -) - -type poisonCharacterGenerator struct { - Header []byte - AllowedBytes []byte - Repeat int - counter int -} - -func (r *poisonCharacterGenerator) Read(p []byte) (n int, err error) { - if len(r.Header) > 0 { - copy(p, r.Header) - nn := min(len(r.Header), len(p)) - r.Header = r.Header[nn:] - p = p[nn:] - } - - stride := min(len(p), r.Repeat) - for i := 0; i < len(p); i += stride { - copy(p[i:], bytes.Repeat([]byte{r.AllowedBytes[r.counter]}, stride)) - r.counter = (r.counter + 1) % len(r.AllowedBytes) - } - return len(p), nil -} - -type poisonValuesGenerator struct { - Header []byte - AllowedValues [][]byte - counter int -} - -func (r *poisonValuesGenerator) Read(p []byte) (n int, err error) { - var i int - - if len(r.Header) > 0 { - copy(p, r.Header) - nn := min(len(r.Header), len(p)) - r.Header = r.Header[nn:] - i += nn - - for i < len(p) { - copy(p[i:], r.AllowedValues[r.counter]) - i += len(r.AllowedValues[r.counter]) - r.counter = (r.counter + 1) % len(r.AllowedValues) - if r.counter == 0 { - break - } - } - } - - for i < len(p) { - buf := slices.Repeat(r.AllowedValues[r.counter], len(r.AllowedValues)-r.counter) - copy(p[i:], buf) - i += len(buf) - r.counter = (r.counter + 1) % len(r.AllowedValues) - } - return len(p), nil -} - -func main() { - - outputPath := flag.String("path", "./", "path to poison files") - - flag.Parse() - - const Gigabyte = 1024 * 1024 * 1024 - - compressPoison(*outputPath, "text/html", &poisonValuesGenerator{ - Header: []byte(fmt.Sprintf("
\n"), - []byte("
\n"),
- []byte(" "),
- []byte("