From a9f03267b629b20136e1d3f2e3f7dc1c46026c5d Mon Sep 17 00:00:00 2001 From: WeebDataHoarder Date: Wed, 30 Apr 2025 20:54:50 +0200 Subject: [PATCH] settings: allow transparent backends that don't set all values --- examples/config.yml | 8 ++++++++ lib/challenge/data.go | 17 +++++++++++++++-- lib/challenge/script.go | 2 +- lib/challenge/types.go | 4 ++-- lib/http.go | 12 +++++------- lib/interface.go | 5 ++--- lib/settings/backend.go | 16 ++++++++++++++-- lib/settings/settings.go | 7 +++++-- lib/settings/strings.go | 21 ++------------------- lib/state.go | 10 +++++----- lib/template.go | 20 ++++++++++---------- utils/strings.go | 26 ++++++++++++++++++++++++++ 12 files changed, 95 insertions(+), 53 deletions(-) create mode 100644 utils/strings.go diff --git a/examples/config.yml b/examples/config.yml index 61be74e..49a7ded 100644 --- a/examples/config.yml +++ b/examples/config.yml @@ -72,6 +72,14 @@ backends: # http2-enabled: true # tls-skip-verify: true + # Example HTTPS transparent backend with host/SNI override, HTTP/2, and subdirectory + #"ssl.example.com": + # url: "https://ssl.example.com/subdirectory/" + # host: ssl.example.com + # http2-enabled: true + # ip-header: "-" + # transparent: true + # List of strings you can replace to alter the presentation on challenge/error templates # Can use other languages. # Note raw HTML is allowed, be careful with it. diff --git a/lib/challenge/data.go b/lib/challenge/data.go index 0174616..5eaa2f3 100644 --- a/lib/challenge/data.go +++ b/lib/challenge/data.go @@ -12,6 +12,7 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/traits" + "maps" "net/http" "net/netip" "net/textproto" @@ -41,6 +42,8 @@ type RequestData struct { State StateInterface CookiePrefix string + ExtraHeaders http.Header + r *http.Request fp map[string]string @@ -61,18 +64,18 @@ func CreateRequestData(r *http.Request, state StateInterface) (*http.Request, *R data.Time = time.Now().UTC() data.State = state + data.ExtraHeaders = make(http.Header) + data.fp = make(map[string]string, 2) if fp := utils.GetTLSFingerprint(r); fp != nil { if ja3nPtr := fp.JA3N(); ja3nPtr != nil { ja3n := ja3nPtr.String() data.fp["ja3n"] = ja3n - r.Header.Set("X-TLS-Fingerprint-JA3N", ja3n) } if ja4Ptr := fp.JA4(); ja4Ptr != nil { ja4 := ja4Ptr.String() data.fp["ja4"] = ja4 - r.Header.Set("X-TLS-Fingerprint-JA4", ja4) } } @@ -257,4 +260,14 @@ func (d *RequestData) RequestHeaders(headers http.Header) { headers.Set(fmt.Sprintf("X-Away-Challenge-%s-State", c.Name), d.ChallengeState[id].String()) } } + + if ja4, ok := d.fp["fp4"]; ok { + headers.Set("X-TLS-Fingerprint-JA4", ja4) + } + + if ja3n, ok := d.fp["ja3n"]; ok { + headers.Set("X-TLS-Fingerprint-JA3N", ja3n) + } + + maps.Copy(headers, d.ExtraHeaders) } diff --git a/lib/challenge/script.go b/lib/challenge/script.go index 6699dde..459ec4f 100644 --- a/lib/challenge/script.go +++ b/lib/challenge/script.go @@ -33,7 +33,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat "Random": utils.CacheBust(), "Challenge": reg.Name, "ChallengeScript": script, - "Strings": data.State.Options().Strings, + "Strings": data.State.Strings(), }) if err != nil { //TODO: log diff --git a/lib/challenge/types.go b/lib/challenge/types.go index 750ea8e..570fe58 100644 --- a/lib/challenge/types.go +++ b/lib/challenge/types.go @@ -3,7 +3,7 @@ package challenge import ( "crypto/ed25519" "git.gammaspectra.live/git/go-away/lib/policy" - "git.gammaspectra.live/git/go-away/lib/settings" + "git.gammaspectra.live/git/go-away/utils" "github.com/google/cel-go/cel" "log/slog" "net/http" @@ -114,7 +114,7 @@ type StateInterface interface { Settings() policy.StateSettings - Options() settings.Settings + Strings() utils.Strings GetBackend(host string) http.Handler } diff --git a/lib/http.go b/lib/http.go index f301bc1..8ae60f3 100644 --- a/lib/http.go +++ b/lib/http.go @@ -157,7 +157,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) { return backend } - cleanupRequest := func(r *http.Request, fromChallenge bool) { + cleanupRequest := func(r *http.Request, fromChallenge bool, ruleName string, ruleAction policy.RuleAction) { if fromChallenge { r.Header.Del("Referer") } @@ -175,7 +175,8 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) { } r.URL.RawQuery = q.Encode() - data.RequestHeaders(r.Header) + data.ExtraHeaders.Set("X-Away-Rule", ruleName) + data.ExtraHeaders.Set("X-Away-Action", string(ruleAction)) // delete cookies set by go-away to prevent user tracking that way cookies := r.Cookies() @@ -189,7 +190,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) { for _, rule := range state.rules { next, err := rule.Evaluate(lg, w, r, func() http.Handler { - cleanupRequest(r, true) + cleanupRequest(r, true, rule.Name, rule.Action) return getBackend() }) if err != nil { @@ -208,10 +209,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) { // default pass _, _ = action.Pass{}.Handle(lg, w, r, func() http.Handler { - r.Header.Set("X-Away-Rule", "DEFAULT") - r.Header.Set("X-Away-Action", "PASS") - - cleanupRequest(r, false) + cleanupRequest(r, false, "DEFAULT", policy.RuleActionPASS) return getBackend() }) } diff --git a/lib/interface.go b/lib/interface.go index e891ead..f91377b 100644 --- a/lib/interface.go +++ b/lib/interface.go @@ -4,7 +4,6 @@ import ( "crypto/ed25519" "git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/policy" - "git.gammaspectra.live/git/go-away/lib/settings" "git.gammaspectra.live/git/go-away/utils" "github.com/google/cel-go/cel" "log/slog" @@ -99,8 +98,8 @@ func (state *State) Settings() policy.StateSettings { return state.settings } -func (state *State) Options() settings.Settings { - return state.opt +func (state *State) Strings() utils.Strings { + return state.opt.Strings } func (state *State) GetBackend(host string) http.Handler { diff --git a/lib/settings/backend.go b/lib/settings/backend.go index 5c3b33a..8eb1fc6 100644 --- a/lib/settings/backend.go +++ b/lib/settings/backend.go @@ -1,6 +1,7 @@ package settings import ( + "git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/utils" "net/http" "net/http/httputil" @@ -27,6 +28,10 @@ type Backend struct { // GoDNS Resolve URL using the Go DNS server // Only relevant when running with CGO enabled GoDNS bool `yaml:"go-dns"` + + // Transparent Do not add extra headers onto this backend + // This prevents GoAway headers from being set, or other state + Transparent bool `yaml:"transparent"` } func (b Backend) Create() (*httputil.ReverseProxy, error) { @@ -53,10 +58,10 @@ func (b Backend) Create() (*httputil.ReverseProxy, error) { transport.TLSClientConfig.ServerName = b.Host } - if b.IpHeader != "" || b.Host != "" { + if b.IpHeader != "" || b.Host != "" || !b.Transparent { director := proxy.Director proxy.Director = func(req *http.Request) { - if b.IpHeader != "" { + if b.IpHeader != "" && !b.Transparent { if ip := utils.GetRemoteAddress(req.Context()); ip != nil { req.Header.Set(b.IpHeader, ip.Addr().Unmap().String()) } @@ -64,6 +69,13 @@ func (b Backend) Create() (*httputil.ReverseProxy, error) { if b.Host != "" { req.Host = b.Host } + + if !b.Transparent { + data := challenge.RequestDataFromContext(req.Context()) + if data != nil { + data.RequestHeaders(req.Header) + } + } director(req) } } diff --git a/lib/settings/settings.go b/lib/settings/settings.go index d8ca8e4..dffe8f5 100644 --- a/lib/settings/settings.go +++ b/lib/settings/settings.go @@ -1,6 +1,9 @@ package settings -import "maps" +import ( + "git.gammaspectra.live/git/go-away/utils" + "maps" +) type Settings struct { Bind Bind `yaml:"bind"` @@ -10,7 +13,7 @@ type Settings struct { BindDebug string `yaml:"bind-debug"` BindMetrics string `yaml:"bind-metrics"` - Strings Strings `yaml:"strings"` + Strings utils.Strings `yaml:"strings"` // Links to add to challenge/error pages like privacy/impressum. Links []Link `yaml:"links"` diff --git a/lib/settings/strings.go b/lib/settings/strings.go index 9a4648a..adea6b5 100644 --- a/lib/settings/strings.go +++ b/lib/settings/strings.go @@ -1,13 +1,10 @@ package settings import ( - "html/template" - "maps" + "git.gammaspectra.live/git/go-away/utils" ) -type Strings map[string]string - -var DefaultStrings = make(Strings).set(map[string]string{ +var DefaultStrings = utils.NewStrings(map[string]string{ "title_challenge": "Checking you are not a bot", "title_error": "Oh no!", @@ -39,17 +36,3 @@ var DefaultStrings = make(Strings).set(map[string]string{ "status_challenge_done_took": "Done! Took", "status_error": "Error:", }) - -func (s Strings) set(v map[string]string) Strings { - maps.Copy(s, v) - return s -} - -func (s Strings) Get(value string) template.HTML { - v, ok := (s)[value] - if !ok { - // fallback - return template.HTML("string:" + value) - } - return template.HTML(v) -} diff --git a/lib/state.go b/lib/state.go index b853297..d820d71 100644 --- a/lib/state.go +++ b/lib/state.go @@ -99,18 +99,18 @@ func NewState(p policy.Policy, opt settings.Settings, settings policy.StateSetti } } - if templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"] == nil { + if templates["challenge-"+state.opt.ChallengeTemplate+".gohtml"] == nil { - if data, err := os.ReadFile(state.Options().ChallengeTemplate); err == nil && len(data) > 0 { - name := path.Base(state.Options().ChallengeTemplate) + if data, err := os.ReadFile(state.opt.ChallengeTemplate); err == nil && len(data) > 0 { + name := path.Base(state.opt.ChallengeTemplate) err := initTemplate(name, string(data)) if err != nil { - return nil, fmt.Errorf("error loading template %s: %w", state.Options().ChallengeTemplate, err) + return nil, fmt.Errorf("error loading template %s: %w", state.opt.ChallengeTemplate, err) } state.opt.ChallengeTemplate = name } - return nil, fmt.Errorf("no template defined for %s", state.Options().ChallengeTemplate) + return nil, fmt.Errorf("no template defined for %s", state.opt.ChallengeTemplate) } state.networks = make(map[string]cidranger.Ranger) diff --git a/lib/template.go b/lib/template.go index 93243ed..c582b5a 100644 --- a/lib/template.go +++ b/lib/template.go @@ -59,9 +59,9 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status input["Random"] = utils.CacheBust() input["Path"] = state.UrlPath() - input["Links"] = state.Options().Links - input["Strings"] = state.Options().Strings - for k, v := range state.Options().ChallengeTemplateOverrides { + input["Links"] = state.opt.Links + input["Strings"] = state.opt.Strings + for k, v := range state.opt.ChallengeTemplateOverrides { input[k] = v } @@ -72,7 +72,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status maps.Copy(input, params) if _, ok := input["Title"]; !ok { - input["Title"] = state.Options().Strings.Get("title_challenge") + input["Title"] = state.opt.Strings.Get("title_challenge") } if data.GetOptBool(challenge.RequestOptCacheMetaTags, false) { @@ -95,7 +95,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status buf := bytes.NewBuffer(make([]byte, 0, 8192)) - err := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input) + err := templates["challenge-"+state.opt.ChallengeTemplate+".gohtml"].Execute(buf, input) if err != nil { state.ErrorPage(w, r, http.StatusInternalServerError, err, "") } else { @@ -116,13 +116,13 @@ func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int "Error": err.Error(), "Path": state.UrlPath(), "Theme": "", - "Title": template.HTML(string(state.Options().Strings.Get("title_error")) + " " + http.StatusText(status)), + "Title": template.HTML(string(state.opt.Strings.Get("title_error")) + " " + http.StatusText(status)), "Challenge": "", "Redirect": redirect, - "Links": state.Options().Links, - "Strings": state.Options().Strings, + "Links": state.opt.Links, + "Strings": state.opt.Strings, } - for k, v := range state.Options().ChallengeTemplateOverrides { + for k, v := range state.opt.ChallengeTemplateOverrides { input[k] = v } @@ -142,7 +142,7 @@ func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int } } - err2 := templates["challenge-"+state.Options().ChallengeTemplate+".gohtml"].Execute(buf, input) + err2 := templates["challenge-"+state.opt.ChallengeTemplate+".gohtml"].Execute(buf, input) if err2 != nil { // nested errors! panic(err2) diff --git a/utils/strings.go b/utils/strings.go new file mode 100644 index 0000000..c94e644 --- /dev/null +++ b/utils/strings.go @@ -0,0 +1,26 @@ +package utils + +import ( + "html/template" + "maps" +) + +type Strings map[string]string + +func (s Strings) set(v map[string]string) Strings { + maps.Copy(s, v) + return s +} + +func (s Strings) Get(value string) template.HTML { + v, ok := (s)[value] + if !ok { + // fallback + return template.HTML("string:" + value) + } + return template.HTML(v) +} + +func NewStrings[T ~map[string]string](v T) Strings { + return make(Strings).set(v) +}