Korzen
Bot Protection

Go net/http

Bot Protection in einem Go-Server nur mit net/http: ein Client für den Prozess, der ganze Aufruf unter einem Timeout und die IP des Besuchers.

Diese Anleitung nutzt nur die Standardbibliothek: net/http und einen http.Client für den Prozess, der seine Verbindungen offen hält. Sein Timeout begrenzt den ganzen Aufruf, vom Verbinden bis zum Lesen des Bodys.

Der Tag

Setzen Sie den Tag in den <head> des Layouts, das Ihre Registrierungs-, Anmelde- und Checkout-Seiten rendert:

<script src="https://korzen.dev/v1/tag.js" data-site-key="YOUR_SITE_KEY" async></script>

Rufen Sie auf der Seite vor dem Senden des Formulars await window.korzen?.ready() auf: Es meldet, was der Besucher seit der letzten Meldung des Tags getan hat, und wird in jedem Fall innerhalb weniger Sekunden aufgelöst. Der Tag

Der Aufruf

Der Client ist eine einzige Datei, nur mit der Standardbibliothek, korzen.go: Er fragt /v1/decide, gibt bei Ihrem Timeout auf und lässt den Besucher bei jedem Fehler durch. Laden Sie ihn in ein Paket Ihres Moduls und lesen Sie ihn, bevor Sie ihn committen:

mkdir -p internal/korzen
curl -o internal/korzen/korzen.go https://korzen.dev/bot-protection/sdk/korzen.go

Ein Client für den Prozess: Er hält seine Verbindungen offen, und sein Timeout begrenzt den ganzen Aufruf. Protect fragt vor dem Handler, der das Konto anlegt, und legt die Antwort in den Context der Anfrage:

var bot = &korzen.Client{APIKey: os.Getenv("KORZEN_API_KEY"), URL: "https://korzen.dev", Timeout: 200 * time.Millisecond}

mux.Handle("POST /signup", bot.Protect(http.HandlerFunc(signup), korzen.Options{
	IP: korzen.RemoteIP, // see "The visitor's IP"
	OnBlock: func(w http.ResponseWriter, r *http.Request, d korzen.Decision) bool {
		http.Error(w, "We could not create your account.", http.StatusBadRequest)
		return true
	},
	// Your own check for challenge runs in signup: return false to go on to it. Without
	// OnChallenge, a challenged post is refused (403).
	OnChallenge: func(http.ResponseWriter, *http.Request, korzen.Decision) bool { return false },
}))

func signup(w http.ResponseWriter, r *http.Request) {
	d, _ := korzen.FromContext(r.Context()) // no answer: the zero Decision
	if d.Action == korzen.Challenge {
		// your CAPTCHA, or an email or phone check, before going on
	}
	user, err := createAccount(r) // your own code
	// ...
	saveRequestID(user.ID, d.RequestID) // see "Keep the request_id"
}

Um stattdessen in einem Handler zu fragen: d, err := bot.Decide(r.Context(), korzen.FromHTTP(r, korzen.RemoteIP(r))). Bei jedem Fehler ist d.Action allow, und err sagt, warum, für Ihr Log. Eine Antwort, deren action nicht allow, challenge oder block ist, gilt ebenfalls als Fehler; das *korzen.Problem einer Fehlerantwort steckt in err (errors.As). Um jeden Aufruf zu protokollieren, setzen Sie OnAnswer des Clients: Es erhält den Status, Korzen-Request-Id und die RateLimit-Header.

Die IP des Besuchers

r.RemoteAddr ist die Adresse der Verbindung: die Ihres Proxys, falls Sie einen haben. korzen.RemoteIP liest sie: Nehmen Sie es, wenn nichts vor Ihrem Server steht. korzen.CloudflareIP liest CF-Connecting-IP, was nur richtig ist, wenn Ihr Server Traffic ausschließlich von Cloudflare annimmt; sonst könnte ein Besucher ihn schreiben. Hinter Ihrem eigenen Load Balancer nimmt korzen.LastForwardedIP den letzten Eintrag von X-Forwarded-For, den er angehängt hat.

Die Aktion anwenden

Lesen Sie nur action. Bei block antworten Sie mit demselben Fehler, den die Aktion zeigt, wenn sie aus einem anderen Grund scheitert, damit ein Bot nichts lernt. Bei challenge nutzen Sie die Prüfung, die Sie schon haben (ein CAPTCHA, ein Code per E-Mail oder Telefon); ohne eine machen Sie weiter. Das Konto beginnt im Monitor-Modus, in dem action immer allow ist: Für Besucher ändert sich nichts, bis Sie den Block-Modus einschalten. Oder schicken Sie den Besucher zu unserer Challenge-Seite unter challenge.url: siehe die Challenge-Seite.

Ohne OnChallenge lässt die Middleware keine Challenge durch: Ein Seitenaufruf (GET) geht zu unserer Challenge-Seite, ein abgeschicktes Formular oder ein API-Aufruf wird mit 403 abgelehnt. Schützen Sie auch die Seite, die das Formular zeigt: Dort trifft eine Person auf die Challenge, und ihr Pass lässt das Formular durch. Mit einer eigenen Prüfung geben Sie OnChallenge an und führen sie dort aus. Die Middleware schickt die Person an die aufgerufene Adresse zurück, gelesen aus X-Forwarded-Host und X-Forwarded-Proto, wenn Ihr Proxy sie setzt; hinter einem Proxy, der Host: localhost und keinen der beiden weiterreicht (nginx standardmäßig), geben Sie PublicURL: "https://shop.example" an.

Die request_id aufbewahren

Jede Antwort hat eine request_id. Bewahren Sie sie bei dem auf, was die Aktion erstellt hat, in einer Spalte des Kontos oder der Bestellung, oder in einer Logzeile mit deren ID. Sie benennt diese Entscheidung, wenn Sie uns dazu etwas fragen, und lässt Sie später herausfinden, welche Antwort ein Fake-Konto durchgelassen hat. Wenn sich das Konto als gut oder schlecht herausstellt, melden Sie es zurück: bot.Feedback(ctx, []korzen.Feedback{{RequestID: id, Outcome: "chargeback"}}, korzen.Batch{}) (siehe Feedback).

Auf Ihrem Rechner testen

Führen Sie go run . aus und öffnen Sie Ihren Server auf localhost. Seiten auf localhost akzeptieren den Tag immer und erscheinen nie als Domain. Lokal ist die IP 127.0.0.1 oder ::1, und das ist in Ordnung. Läuft die App auf Ihrem Rechner in Docker, sieht sie stattdessen die Adresse der Bridge, etwa 172.17.0.1: Die Challenge-Seite bringt Sie trotzdem zu localhost zurück, denn der Tag hat Ihre Seite dort gesehen.

  1. Setzen Sie KORZEN_API_KEY in der Umgebung und öffnen Sie die Registrierungsseite. In den Entwicklertools des Browsers erscheint nach ein paar Sekunden das _kz-Cookie.
  2. Registrieren Sie sich. Die Entscheidung erscheint auf der Seite Entscheidungen des Dashboards, unter dem Pfad, den Sie gesendet haben.
  3. Prüfen Sie fail-open: Setzen Sie einen falschen Schlüssel und registrieren Sie sich erneut. Der Aufruf antwortet 401, und die Registrierung geht durch.

Häufige Fehler

  • Ein HTTPClient mit einem neuen http.Transport pro Aufruf, oder der Standard der Bibliothek, der zwei Verbindungen offen hält. Jede neue ist eine neue TLS-Verbindung: Lassen Sie HTTPClient leer, oder geben Sie Ihrem korzen.NewTransport(), einmal für den Prozess.
  • CF-Connecting-IP auf einem Server lesen, vor dem Cloudflare nicht steht. Ein Bot schreibt den Header und wählt seine eigene IP.
  • /v1/decide aus dem Browser aufrufen. Der API key würde jeden Besucher erreichen; der Aufruf gehört auf Ihren Server.
  • Kein Timeout, oder ein Fehler, der die Registrierung stoppt. Jede andere Antwort als 200, und keine Antwort rechtzeitig, bedeutet: den Besucher durchlassen.
  • Eine Zeile in Ihrem Log, die mit korzen: the visitor's IP sent to Bot Protection beginnt. Der Client schreibt sie einmal, bei der ersten Entscheidung, deren IP aus Ihrem eigenen Netz stammt (die eines Load Balancers, eines Proxys, eines Containers), was Bot Protection wie keine IP wertet: Setzen Sie Options.IP auf die Adresse des Besuchers (die IP des Besuchers). In Docker auf Ihrem Rechner ist es die Adresse der Bridge, und dort harmlos.