settings: allow transparent backends that don't set all values

This commit is contained in:
WeebDataHoarder
2025-04-30 20:54:50 +02:00
parent 4ce6d9efa3
commit a9f03267b6
12 changed files with 95 additions and 53 deletions

View File

@@ -72,6 +72,14 @@ backends:
# http2-enabled: true # http2-enabled: true
# tls-skip-verify: 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 # List of strings you can replace to alter the presentation on challenge/error templates
# Can use other languages. # Can use other languages.
# Note raw HTML is allowed, be careful with it. # Note raw HTML is allowed, be careful with it.

View File

@@ -12,6 +12,7 @@ import (
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/common/types/traits"
"maps"
"net/http" "net/http"
"net/netip" "net/netip"
"net/textproto" "net/textproto"
@@ -41,6 +42,8 @@ type RequestData struct {
State StateInterface State StateInterface
CookiePrefix string CookiePrefix string
ExtraHeaders http.Header
r *http.Request r *http.Request
fp map[string]string fp map[string]string
@@ -61,18 +64,18 @@ func CreateRequestData(r *http.Request, state StateInterface) (*http.Request, *R
data.Time = time.Now().UTC() data.Time = time.Now().UTC()
data.State = state data.State = state
data.ExtraHeaders = make(http.Header)
data.fp = make(map[string]string, 2) data.fp = make(map[string]string, 2)
if fp := utils.GetTLSFingerprint(r); fp != nil { if fp := utils.GetTLSFingerprint(r); fp != nil {
if ja3nPtr := fp.JA3N(); ja3nPtr != nil { if ja3nPtr := fp.JA3N(); ja3nPtr != nil {
ja3n := ja3nPtr.String() ja3n := ja3nPtr.String()
data.fp["ja3n"] = ja3n data.fp["ja3n"] = ja3n
r.Header.Set("X-TLS-Fingerprint-JA3N", ja3n)
} }
if ja4Ptr := fp.JA4(); ja4Ptr != nil { if ja4Ptr := fp.JA4(); ja4Ptr != nil {
ja4 := ja4Ptr.String() ja4 := ja4Ptr.String()
data.fp["ja4"] = ja4 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()) 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)
} }

View File

@@ -33,7 +33,7 @@ func ServeChallengeScript(w http.ResponseWriter, r *http.Request, reg *Registrat
"Random": utils.CacheBust(), "Random": utils.CacheBust(),
"Challenge": reg.Name, "Challenge": reg.Name,
"ChallengeScript": script, "ChallengeScript": script,
"Strings": data.State.Options().Strings, "Strings": data.State.Strings(),
}) })
if err != nil { if err != nil {
//TODO: log //TODO: log

View File

@@ -3,7 +3,7 @@ package challenge
import ( import (
"crypto/ed25519" "crypto/ed25519"
"git.gammaspectra.live/git/go-away/lib/policy" "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" "github.com/google/cel-go/cel"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -114,7 +114,7 @@ type StateInterface interface {
Settings() policy.StateSettings Settings() policy.StateSettings
Options() settings.Settings Strings() utils.Strings
GetBackend(host string) http.Handler GetBackend(host string) http.Handler
} }

View File

@@ -157,7 +157,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
return backend return backend
} }
cleanupRequest := func(r *http.Request, fromChallenge bool) { cleanupRequest := func(r *http.Request, fromChallenge bool, ruleName string, ruleAction policy.RuleAction) {
if fromChallenge { if fromChallenge {
r.Header.Del("Referer") r.Header.Del("Referer")
} }
@@ -175,7 +175,8 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
} }
r.URL.RawQuery = q.Encode() 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 // delete cookies set by go-away to prevent user tracking that way
cookies := r.Cookies() cookies := r.Cookies()
@@ -189,7 +190,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
for _, rule := range state.rules { for _, rule := range state.rules {
next, err := rule.Evaluate(lg, w, r, func() http.Handler { next, err := rule.Evaluate(lg, w, r, func() http.Handler {
cleanupRequest(r, true) cleanupRequest(r, true, rule.Name, rule.Action)
return getBackend() return getBackend()
}) })
if err != nil { if err != nil {
@@ -208,10 +209,7 @@ func (state *State) handleRequest(w http.ResponseWriter, r *http.Request) {
// default pass // default pass
_, _ = action.Pass{}.Handle(lg, w, r, func() http.Handler { _, _ = action.Pass{}.Handle(lg, w, r, func() http.Handler {
r.Header.Set("X-Away-Rule", "DEFAULT") cleanupRequest(r, false, "DEFAULT", policy.RuleActionPASS)
r.Header.Set("X-Away-Action", "PASS")
cleanupRequest(r, false)
return getBackend() return getBackend()
}) })
} }

View File

@@ -4,7 +4,6 @@ import (
"crypto/ed25519" "crypto/ed25519"
"git.gammaspectra.live/git/go-away/lib/challenge" "git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/lib/policy" "git.gammaspectra.live/git/go-away/lib/policy"
"git.gammaspectra.live/git/go-away/lib/settings"
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"github.com/google/cel-go/cel" "github.com/google/cel-go/cel"
"log/slog" "log/slog"
@@ -99,8 +98,8 @@ func (state *State) Settings() policy.StateSettings {
return state.settings return state.settings
} }
func (state *State) Options() settings.Settings { func (state *State) Strings() utils.Strings {
return state.opt return state.opt.Strings
} }
func (state *State) GetBackend(host string) http.Handler { func (state *State) GetBackend(host string) http.Handler {

View File

@@ -1,6 +1,7 @@
package settings package settings
import ( import (
"git.gammaspectra.live/git/go-away/lib/challenge"
"git.gammaspectra.live/git/go-away/utils" "git.gammaspectra.live/git/go-away/utils"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@@ -27,6 +28,10 @@ type Backend struct {
// GoDNS Resolve URL using the Go DNS server // GoDNS Resolve URL using the Go DNS server
// Only relevant when running with CGO enabled // Only relevant when running with CGO enabled
GoDNS bool `yaml:"go-dns"` 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) { func (b Backend) Create() (*httputil.ReverseProxy, error) {
@@ -53,10 +58,10 @@ func (b Backend) Create() (*httputil.ReverseProxy, error) {
transport.TLSClientConfig.ServerName = b.Host transport.TLSClientConfig.ServerName = b.Host
} }
if b.IpHeader != "" || b.Host != "" { if b.IpHeader != "" || b.Host != "" || !b.Transparent {
director := proxy.Director director := proxy.Director
proxy.Director = func(req *http.Request) { proxy.Director = func(req *http.Request) {
if b.IpHeader != "" { if b.IpHeader != "" && !b.Transparent {
if ip := utils.GetRemoteAddress(req.Context()); ip != nil { if ip := utils.GetRemoteAddress(req.Context()); ip != nil {
req.Header.Set(b.IpHeader, ip.Addr().Unmap().String()) req.Header.Set(b.IpHeader, ip.Addr().Unmap().String())
} }
@@ -64,6 +69,13 @@ func (b Backend) Create() (*httputil.ReverseProxy, error) {
if b.Host != "" { if b.Host != "" {
req.Host = b.Host req.Host = b.Host
} }
if !b.Transparent {
data := challenge.RequestDataFromContext(req.Context())
if data != nil {
data.RequestHeaders(req.Header)
}
}
director(req) director(req)
} }
} }

View File

@@ -1,6 +1,9 @@
package settings package settings
import "maps" import (
"git.gammaspectra.live/git/go-away/utils"
"maps"
)
type Settings struct { type Settings struct {
Bind Bind `yaml:"bind"` Bind Bind `yaml:"bind"`
@@ -10,7 +13,7 @@ type Settings struct {
BindDebug string `yaml:"bind-debug"` BindDebug string `yaml:"bind-debug"`
BindMetrics string `yaml:"bind-metrics"` 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 to add to challenge/error pages like privacy/impressum.
Links []Link `yaml:"links"` Links []Link `yaml:"links"`

View File

@@ -1,13 +1,10 @@
package settings package settings
import ( import (
"html/template" "git.gammaspectra.live/git/go-away/utils"
"maps"
) )
type Strings map[string]string var DefaultStrings = utils.NewStrings(map[string]string{
var DefaultStrings = make(Strings).set(map[string]string{
"title_challenge": "Checking you are not a bot", "title_challenge": "Checking you are not a bot",
"title_error": "Oh no!", "title_error": "Oh no!",
@@ -39,17 +36,3 @@ var DefaultStrings = make(Strings).set(map[string]string{
"status_challenge_done_took": "Done! Took", "status_challenge_done_took": "Done! Took",
"status_error": "Error:", "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)
}

View File

@@ -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 { if data, err := os.ReadFile(state.opt.ChallengeTemplate); err == nil && len(data) > 0 {
name := path.Base(state.Options().ChallengeTemplate) name := path.Base(state.opt.ChallengeTemplate)
err := initTemplate(name, string(data)) err := initTemplate(name, string(data))
if err != nil { 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 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) state.networks = make(map[string]cidranger.Ranger)

View File

@@ -59,9 +59,9 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
input["Random"] = utils.CacheBust() input["Random"] = utils.CacheBust()
input["Path"] = state.UrlPath() input["Path"] = state.UrlPath()
input["Links"] = state.Options().Links input["Links"] = state.opt.Links
input["Strings"] = state.Options().Strings input["Strings"] = state.opt.Strings
for k, v := range state.Options().ChallengeTemplateOverrides { for k, v := range state.opt.ChallengeTemplateOverrides {
input[k] = v input[k] = v
} }
@@ -72,7 +72,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status
maps.Copy(input, params) maps.Copy(input, params)
if _, ok := input["Title"]; !ok { 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) { 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)) 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 { if err != nil {
state.ErrorPage(w, r, http.StatusInternalServerError, err, "") state.ErrorPage(w, r, http.StatusInternalServerError, err, "")
} else { } else {
@@ -116,13 +116,13 @@ func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int
"Error": err.Error(), "Error": err.Error(),
"Path": state.UrlPath(), "Path": state.UrlPath(),
"Theme": "", "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": "", "Challenge": "",
"Redirect": redirect, "Redirect": redirect,
"Links": state.Options().Links, "Links": state.opt.Links,
"Strings": state.Options().Strings, "Strings": state.opt.Strings,
} }
for k, v := range state.Options().ChallengeTemplateOverrides { for k, v := range state.opt.ChallengeTemplateOverrides {
input[k] = v 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 { if err2 != nil {
// nested errors! // nested errors!
panic(err2) panic(err2)

26
utils/strings.go Normal file
View File

@@ -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)
}