package admin import ( "net" "net/http" ) // IPAllow is middleware that rejects any request whose source IP is not within // the configured allowlist. It is the first line of defence in front of the // admin backend (doc/06 §2: 管理后台不得暴露公网). // // The source address is taken from the TCP peer (RemoteAddr) ONLY. Proxy // headers like X-Forwarded-For are intentionally ignored: the admin port is // reached directly over an SSH tunnel / intranet, so trusting client-supplied // headers would let an attacker spoof the allowlist. type IPAllow struct { allow []*net.IPNet sec *SecurityLog next http.Handler } // NewIPAllow wraps next with the allowlist check. func NewIPAllow(allow []*net.IPNet, sec *SecurityLog, next http.Handler) *IPAllow { return &IPAllow{allow: allow, sec: sec, next: next} } func (m *IPAllow) ServeHTTP(w http.ResponseWriter, r *http.Request) { ip := clientIP(r.RemoteAddr) if ip == nil || !m.allowed(ip) { if m.sec != nil { m.sec.IPBlocked(r.Context(), hostOnly(r.RemoteAddr), r.URL.Path) } http.Error(w, "forbidden", http.StatusForbidden) return } m.next.ServeHTTP(w, r) } func (m *IPAllow) allowed(ip net.IP) bool { for _, n := range m.allow { if n.Contains(ip) { return true } } return false } // clientIP extracts the net.IP from a "host:port" RemoteAddr. func clientIP(remoteAddr string) net.IP { return net.ParseIP(hostOnly(remoteAddr)) } func hostOnly(remoteAddr string) string { host, _, err := net.SplitHostPort(remoteAddr) if err != nil { // RemoteAddr may already be a bare host in some test setups. return remoteAddr } return host }