Block requests from TOR

This commit is contained in:
2026-06-23 18:26:51 +02:00
parent 01be0c523e
commit 1cfbb8b8fe
3 changed files with 75 additions and 1 deletions
+37
View File
@@ -8,6 +8,7 @@ import (
"net/http/httptest"
"net/url"
"path/filepath"
"strings"
"testing"
)
@@ -552,3 +553,39 @@ func TestDeleteUser(t *testing.T) {
t.Fatalf("bobs antwort verwaist: %+v", tv.Replies)
}
}
// postFormFrom verhält sich wie postForm, setzt aber X-Real-IP -> middleware.RealIP
// überschreibt damit r.RemoteAddr, sodass blockTOR die simulierte Quell-IP sieht.
func postFormFrom(t *testing.T, c *http.Client, urlStr, realIP string, form url.Values) *http.Response {
t.Helper()
req, err := http.NewRequest(http.MethodPost, urlStr, strings.NewReader(form.Encode()))
if err != nil {
t.Fatalf("request %s: %v", urlStr, err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("X-Real-IP", realIP)
resp, err := c.Do(req)
if err != nil {
t.Fatalf("POST %s: %v", urlStr, err)
}
return resp
}
// TestBlockTOREntry prüft, dass Beiträge aus dem TOR-Netz (Default 10.89.0.0/16)
// mit 403 abgewiesen werden, aus einem anderen Netz aber durchgehen.
func TestBlockTOREntry(t *testing.T) {
srv := newTestServer(t)
c := registerAndLogin(t, srv, "toruser")
resp := postFormFrom(t, c, srv.URL+"/entry/create", "10.89.1.2", url.Values{"content": {"aus tor"}})
resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Fatalf("TOR-Beitrag: erwartet 403, bekam %d", resp.StatusCode)
}
resp = postFormFrom(t, c, srv.URL+"/entry/create", "203.0.113.7", url.Values{"content": {"normal"}})
resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
t.Fatalf("Nicht-TOR-Beitrag: erwartet 201, bekam %d", resp.StatusCode)
}
}
+1 -1
View File
@@ -113,7 +113,7 @@ func routes() http.Handler {
// --- Geschützt (Session erforderlich) ---
r.Group(func(r chi.Router) {
r.Use(requireAuth)
r.Post("/entry/create", handleCreateEntry)
r.With(blockTOR).Post("/entry/create", handleCreateEntry)
r.Post("/entry/{pid}/vote", handleVote)
r.Post("/entry/{pid}/bump", handleBump)
r.Post("/entry/{pid}/edit", handleEditEntry)
+37
View File
@@ -2,6 +2,8 @@ package main
import (
"context"
"log"
"net"
"net/http"
)
@@ -9,6 +11,41 @@ type ctxKey string
const uidKey ctxKey = "uid"
// torNet ist das interne Netz, über das TOR-Anfragen beim Backend ankommen:
// der Onion-Hidden-Service reicht über einen lokalen Proxy weiter, eine echte
// Client-IP gibt es dabei nicht -> die Anfragen erscheinen mit einer IP aus
// diesem Netz (siehe Impression-Log: 10.89.0.0). Beiträge von dort werden
// vorerst komplett abgewiesen; eine Moderations-/Freigabeseite kommt später.
// Per KVER_TOR_NET (CIDR) konfigurierbar; leer schaltet die Sperre ab.
var torNet *net.IPNet
func init() {
cidr := envOr("KVER_TOR_NET", "10.89.0.0/16")
if cidr == "" {
return
}
if _, n, err := net.ParseCIDR(cidr); err == nil {
torNet = n
} else {
log.Printf("KVER_TOR_NET ignoriert (ungültiges CIDR %q): %v", cidr, err)
}
}
// blockTOR weist Anfragen aus dem TOR-Netz ab (siehe torNet). Die volle IP wird
// hier nur für diese Zugriffsentscheidung geprüft, nicht gespeichert oder
// geloggt. Ist kein Netz konfiguriert, ist die Middleware ein No-op.
func blockTOR(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if torNet != nil {
if ip := clientIP(r.RemoteAddr); ip != nil && torNet.Contains(ip) {
writeError(w, http.StatusForbidden, "Beiträge über TOR sind derzeit nicht möglich.")
return
}
}
next.ServeHTTP(w, r)
})
}
// requireAuth lehnt Anfragen ohne gültige Session ab und legt die uid in den Context.
func requireAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {