mirror of
https://github.com/MeshTender/MeshTender.git
synced 2026-09-02 10:03:43 +00:00
119 lines
3.5 KiB
Go
119 lines
3.5 KiB
Go
package core
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/jleight/meshtender/internal/web"
|
|
)
|
|
|
|
// proxyHeaders are the forwarding-related headers worth surfacing first when
|
|
// diagnosing how the reverse proxy presents the client to the app.
|
|
var proxyHeaders = []string{
|
|
"X-Forwarded-For",
|
|
"X-Forwarded-Proto",
|
|
"X-Forwarded-Host",
|
|
"X-Forwarded-Port",
|
|
"X-Real-IP",
|
|
"Forwarded",
|
|
"Via",
|
|
"CF-Connecting-IP",
|
|
"True-Client-IP",
|
|
"Fastly-Client-IP",
|
|
"Fly-Client-IP",
|
|
"X-Cluster-Client-IP",
|
|
}
|
|
|
|
// redactedHeaders carry credentials and are masked in the full dump, since this
|
|
// page is a prime candidate to be screenshotted while debugging.
|
|
var redactedHeaders = map[string]bool{
|
|
"Cookie": true,
|
|
"Authorization": true,
|
|
"Proxy-Authorization": true,
|
|
}
|
|
|
|
type headerKV struct {
|
|
Name string
|
|
Value string
|
|
}
|
|
|
|
// pageProxyTest dumps the request details that determine the client IP the app
|
|
// records, so an admin can confirm the reverse proxy is configured correctly.
|
|
// chi's RealIP middleware has already rewritten RemoteAddr from the forwarding
|
|
// headers by the time this runs; CaptureRemoteAddr preserved the true TCP peer.
|
|
func (s *Handlers) pageProxyTest(w http.ResponseWriter, r *http.Request) {
|
|
resolved := web.ClientIP(r) // what rate-limiting and audit logs record
|
|
rawPeer := web.RawRemoteAddr(r) // the actual connecting socket
|
|
rawHost := rawPeer
|
|
if h, _, err := net.SplitHostPort(rawPeer); err == nil {
|
|
rawHost = h
|
|
}
|
|
|
|
// Curated forwarding headers (always shown, blanks included so a missing one
|
|
// is visible).
|
|
forwarding := make([]headerKV, 0, len(proxyHeaders))
|
|
for _, n := range proxyHeaders {
|
|
forwarding = append(forwarding, headerKV{Name: n, Value: r.Header.Get(n)})
|
|
}
|
|
|
|
// Full header dump, sorted, with credentials redacted.
|
|
var all []headerKV
|
|
for name, vals := range r.Header {
|
|
v := strings.Join(vals, ", ")
|
|
if redactedHeaders[http.CanonicalHeaderKey(name)] {
|
|
v = "(redacted)"
|
|
}
|
|
all = append(all, headerKV{Name: name, Value: v})
|
|
}
|
|
sort.Slice(all, func(i, j int) bool { return all[i].Name < all[j].Name })
|
|
|
|
scheme := "http"
|
|
if r.TLS != nil {
|
|
scheme = "https"
|
|
}
|
|
|
|
// Trusted-proxy diagnostics: which ranges are trusted, whether the peer is one,
|
|
// and how each X-Forwarded-For hop is classified (the resolved client is the
|
|
// rightmost untrusted hop).
|
|
trusted := s.Cfg.TrustedProxies
|
|
trustedList := make([]string, 0, len(trusted))
|
|
for _, n := range trusted {
|
|
trustedList = append(trustedList, n.String())
|
|
}
|
|
type xffHop struct {
|
|
IP string
|
|
Trusted bool
|
|
Selected bool
|
|
}
|
|
var chain []xffHop
|
|
for _, raw := range strings.Split(r.Header.Get("X-Forwarded-For"), ",") {
|
|
ip := strings.TrimSpace(raw)
|
|
if ip == "" {
|
|
continue
|
|
}
|
|
chain = append(chain, xffHop{IP: ip, Trusted: web.IsTrustedProxy(ip, trusted), Selected: ip == resolved})
|
|
}
|
|
|
|
s.Render(w, r, "proxy_test.html", map[string]any{
|
|
"ResolvedIP": resolved,
|
|
"RawPeer": rawPeer,
|
|
"PeerTrusted": web.IsTrustedProxy(rawHost, trusted),
|
|
"TrustedProxies": trustedList,
|
|
"XFFChain": chain,
|
|
// HeaderApplied is true when a forwarding header changed the recorded IP
|
|
// away from the real peer — i.e. the proxy's headers are being trusted.
|
|
// When false, either this was a direct connection (fine) or the proxy
|
|
// isn't sending forwarding headers (you'd be logging the proxy's IP).
|
|
"HeaderApplied": rawHost != resolved,
|
|
"Forwarding": forwarding,
|
|
"AllHeaders": all,
|
|
"Host": r.Host,
|
|
"Proto": r.Proto,
|
|
"Scheme": scheme,
|
|
"Method": r.Method,
|
|
"RequestURI": r.RequestURI,
|
|
})
|
|
}
|