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
# 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.

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

@@ -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"`

View File

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

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 {
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)

View File

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

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