From b46e0813fed106080b700118119c5e2e23dd01f4 Mon Sep 17 00:00:00 2001 From: Veillax <126523099+Veillax@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:07:03 -0600 Subject: [PATCH] Answer domain redirects from the edge, with or without Traefik Co-Authored-By: Claude Sonnet 5.5 --- blacktip.go | 4 ++-- go.mod | 2 +- go.sum | 12 ++---------- redirect_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ traefik.go | 30 ++++++++++++++++++++++++++++++ web.go | 10 ++++++++++ 6 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 redirect_test.go diff --git a/blacktip.go b/blacktip.go index 2db922c..14fe679 100644 --- a/blacktip.go +++ b/blacktip.go @@ -505,14 +505,14 @@ func (b *Blacktip) newMapState(m model.Mapping, sig string) *mapState { for i := range ms.health { ms.health[i] = model.TargetHealth{Healthy: true, Detail: "not checked yet"} } - if model.Web(m.Proto) { + if model.Web(m.Proto) && !m.IsRedirect() { // a redirect has nothing behind it to relay to or check if b.tr != nil { ms.startRelay() } else { ms.initProxy() } } - if m.Proto != model.ProtoUDP && m.HealthSeconds != -1 { + if m.Proto != model.ProtoUDP && m.HealthSeconds != -1 && !m.IsRedirect() { go b.healthLoop(ctx, ms) } return ms diff --git a/go.mod b/go.mod index 984bd7b..8d25153 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.veillax.com/Veillax/blacktip go 1.27.3 require ( - git.veillax.com/Veillax/reef v0.1.7 + git.veillax.com/Veillax/reef v0.1.10 github.com/emersion/go-msgauth v0.7.0 golang.org/x/crypto v0.58.0 ) diff --git a/go.sum b/go.sum index c53abab..8594258 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,5 @@ -git.veillax.com/Veillax/reef v0.1.0 h1:O0OhEKFx2CsvACJ20sfzJNjGsngomtfDdKdxu4ROpCs= -git.veillax.com/Veillax/reef v0.1.0/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= -git.veillax.com/Veillax/reef v0.1.3 h1:RxSq2xwaup1+MieAkhlvOTZnYwPf19thFHEAe/7VxsI= -git.veillax.com/Veillax/reef v0.1.3/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= -git.veillax.com/Veillax/reef v0.1.5 h1:BydjLJsfGgU4zEIqUsePJRjPi9NgbFdmvq0M8dvJCgM= -git.veillax.com/Veillax/reef v0.1.5/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= -git.veillax.com/Veillax/reef v0.1.6 h1:NgZ4u2Z3hJ+BYD/zX9lov8fox4s2i8txtYNOpIsikVo= -git.veillax.com/Veillax/reef v0.1.6/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= -git.veillax.com/Veillax/reef v0.1.7 h1:5C2udonlJac2xvDOE5e+zDxej+AbZ9ttrNSE4owmwuU= -git.veillax.com/Veillax/reef v0.1.7/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= +git.veillax.com/Veillax/reef v0.1.10 h1:b15oD1vyrGOriCHhipQtJ3j7Cy5ForYWvfJkqifLQPg= +git.veillax.com/Veillax/reef v0.1.10/go.mod h1:ZBUd/BARO5kkpUNTP81TTY1aQXLHFR6bKDhCxckjHso= github.com/Microsoft/go-winio v0.6.4 h1:kJF2pamtYCWXf0xAT8sAkwKC0pMl2HYBrRPHAqyUc+g= github.com/Microsoft/go-winio v0.6.4/go.mod h1:1xl5JXG4ze/m74GH6Dc4Mwxfc3VL5siRVQjIAlHH07U= github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc= diff --git a/redirect_test.go b/redirect_test.go new file mode 100644 index 0000000..1702289 --- /dev/null +++ b/redirect_test.go @@ -0,0 +1,44 @@ +package blacktip + +import ( + "net/http" + "net/http/httptest" + "testing" + + "git.veillax.com/Veillax/reef/model" +) + +// A redirect mapping answers from the edge itself: no proxy, relay or target is needed. +func TestRedirectMappingAnswersWithoutATarget(t *testing.T) { + b := &Blacktip{logq: make(chan model.LogEntry, 8)} + mk := func(m model.Mapping) *listener { + return &listener{b: b, kind: model.ProtoHTTP, maps: []*mapState{{b: b, m: m}}} + } + cases := []struct { + name string + m model.Mapping + uri string + code int + loc string + }{ + {"temporary, no path", model.Mapping{Hostname: "old.test", RedirectTo: "https://new.test/home"}, "/a?b=1", http.StatusFound, "https://new.test/home"}, + {"permanent keeps path and query", model.Mapping{Hostname: "old.test", RedirectTo: "https://new.test", RedirectPermanent: true, RedirectKeepPath: true}, "/a/b?c=d", http.StatusMovedPermanently, "https://new.test/a/b?c=d"}, + } + for _, c := range cases { + r := httptest.NewRequest("GET", "http://old.test"+c.uri, nil) + w := httptest.NewRecorder() + mk(c.m).ServeHTTP(w, r) + if w.Code != c.code || w.Header().Get("Location") != c.loc { + t.Errorf("%s: got %d %q, want %d %q", c.name, w.Code, w.Header().Get("Location"), c.code, c.loc) + } + } + // a hostname nobody serves is still a 404 + w := httptest.NewRecorder() + mk(cases[0].m).ServeHTTP(w, httptest.NewRequest("GET", "http://other.test/", nil)) + if w.Code != http.StatusNotFound { + t.Errorf("unknown host: got %d", w.Code) + } + if e := <-b.logq; e.Result != "redirect" || e.Status != http.StatusFound { + t.Errorf("access log entry: %+v", e) + } +} diff --git a/traefik.go b/traefik.go index 3e36f9f..04a5ea6 100644 --- a/traefik.go +++ b/traefik.go @@ -244,6 +244,36 @@ func (t *traefikMgr) sync(ctx context.Context, maps []*mapState, binds map[strin } for _, ms := range maps { m := ms.m + if m.IsRedirect() { // answered by Traefik itself: no relay, login or target + rule, wildcard := hostRule(m.Hostname) + var eps []string + for _, ip := range t.bindsOf(m) { + eps = append(eps, epName(ip, m.Port)) + } + prio := 5000 + if wildcard { + prio = 2000 + } + to := strings.ReplaceAll(m.RedirectTo, "$", "$$") // "$" would otherwise start a group reference + regex, repl := "^.*", to + if m.RedirectKeepPath { + regex, repl = "^https?://[^/]+(.*)", strings.TrimRight(to, "/")+"${1}" + } + mws["rd-"+m.ID] = map[string]any{"redirectRegex": map[string]any{"regex": regex, "replacement": repl, "permanent": m.RedirectPermanent}} + router := map[string]any{"rule": rule, "entryPoints": eps, "service": "noop@internal", "middlewares": []string{"rd-" + m.ID}, "priority": prio} + if m.Proto == model.ProtoHTTPS { + tlsCfg := map[string]any{} + if !t.cfg.NoACME { + tlsCfg["certResolver"] = "le" + if wildcard { + tlsCfg["domains"] = []map[string]any{{"main": m.Hostname}} + } + } + router["tls"] = tlsCfg + } + routers["m-"+m.ID] = router + continue + } if ms.relayPort == 0 { continue } diff --git a/web.go b/web.go index 1909791..8d70ebb 100644 --- a/web.go +++ b/web.go @@ -264,6 +264,16 @@ func (l *listener) ServeHTTP(w http.ResponseWriter, r *http.Request) { b.logAccess(entry) }() + if ms.m.IsRedirect() { + code := http.StatusFound + if ms.m.RedirectPermanent { + code = http.StatusMovedPermanently + } + entry.Result = "redirect" + http.Redirect(rec, r, ms.m.RedirectLocation(r.URL.RequestURI()), code) + return + } + secure := l.kind == model.ProtoHTTPS scheme := "http" if secure {