package api import ( "net" "net/http" "net/netip" "strings" "github.com/ponzischeme89/memby/server/internal/config" ) // clientIP is where a request came from and how that was worked out. Via is one of // "forwarded", "real-ip", "socket" or "none". It is for operational logging and the // admin console's directory only — never authentication or access control. type clientIP struct { Addr string Via string } // String is the address, or "unknown" when none could be determined, matching what the // login history and the trailer log previously recorded. func (c clientIP) String() string { if c.Addr == "" { return "unknown" } return c.Addr } // resolveClientIP works out the originating address of an incoming request. It believes // X-Forwarded-For and X-Real-IP only when the immediate peer is a configured trusted // proxy, so a client that reaches the gateway directly cannot spoof its address with a // header. func (s *Server) resolveClientIP(r *http.Request) clientIP { trusted := s.cfg.TrustedProxies if trusted == nil { trusted = config.DefaultTrustedProxyRanges() } return clientIPFrom(r.RemoteAddr, r.Header, trusted) } func clientIPFrom(remoteAddr string, header http.Header, trusted []netip.Prefix) clientIP { socket := parseHostAddr(remoteAddr) if !socket.IsValid() { return clientIP{Via: "none"} } fromSocket := clientIP{Addr: socket.String(), Via: "socket"} if !prefixesContain(trusted, socket) { // The peer is not a known proxy, so nothing it forwarded is believed. return fromSocket } // X-Forwarded-For grows by one entry per hop, so the rightmost is the nearest // proxy. Walking right to left, the first entry that is not itself a trusted proxy // is the originating client. forwarded := forwardedChain(header) for i := len(forwarded) - 1; i >= 0; i-- { if !prefixesContain(trusted, forwarded[i]) { return clientIP{Addr: forwarded[i].String(), Via: "forwarded"} } } // Every forwarded hop was itself trusted. If there were any, the leftmost is the // genuine origin — a direct LAN client behind the household proxy. Otherwise fall // back to a single X-Real-IP, then to the socket. if len(forwarded) > 0 { return clientIP{Addr: forwarded[0].String(), Via: "forwarded"} } if realIP := parseAddr(header.Get("X-Real-IP")); realIP.IsValid() { return clientIP{Addr: realIP.String(), Via: "real-ip"} } return fromSocket } func parseHostAddr(remoteAddr string) netip.Addr { remoteAddr = strings.TrimSpace(remoteAddr) if host, _, err := net.SplitHostPort(remoteAddr); err == nil { remoteAddr = host } return parseAddr(remoteAddr) } func parseAddr(value string) netip.Addr { addr, err := netip.ParseAddr(strings.TrimSpace(value)) if err != nil { return netip.Addr{} } return addr.Unmap() } func forwardedChain(header http.Header) []netip.Addr { var out []netip.Addr for _, value := range header.Values("X-Forwarded-For") { for _, part := range strings.Split(value, ",") { if addr := parseAddr(part); addr.IsValid() { out = append(out, addr) } } } return out } func prefixesContain(prefixes []netip.Prefix, addr netip.Addr) bool { for _, prefix := range prefixes { if prefix.Contains(addr) { return true } } return false }