diff --git a/adapter/inbound/addition.go b/adapter/inbound/addition.go index 894910aac1..1fb6e0b2c9 100644 --- a/adapter/inbound/addition.go +++ b/adapter/inbound/addition.go @@ -2,6 +2,7 @@ package inbound import ( "net" + "net/netip" C "github.com/metacubex/mihomo/constant" ) @@ -70,4 +71,15 @@ func WithDSCP(dscp uint8) Addition { } } +func WithSrcAddrPort(ap netip.AddrPort) Addition { + return func(metadata *C.Metadata) { + if ap.IsValid() { + metadata.SrcIP = ap.Addr() + if p := ap.Port(); p != 0 { + metadata.SrcPort = p + } + } + } +} + func Placeholder(metadata *C.Metadata) {} diff --git a/common/httputils/addr.go b/common/httputils/addr.go index eb3fcf80fd..60efbec9fb 100644 --- a/common/httputils/addr.go +++ b/common/httputils/addr.go @@ -3,6 +3,9 @@ package httputils import ( "context" "net" + "net/netip" + "strconv" + "strings" C "github.com/metacubex/mihomo/constant" @@ -43,3 +46,27 @@ func NewAddrContext(addr *NetAddr, ctx context.Context) context.Context { }, }) } + +func ClientAddrPortFromHeader(r *http.Request, header string) netip.AddrPort { + if header != "" { + if v := r.Header.Get(header); v != "" { + if i := strings.Index(v, ","); i >= 0 { + v = v[:i] + } + + var port uint16 + v = strings.TrimSpace(v) + if h, p, err := net.SplitHostPort(v); err == nil { + v = h + if p, err := strconv.ParseUint(p, 10, 16); err == nil { + port = uint16(p) + } + } + + if addr, err := netip.ParseAddr(v); err == nil { + return netip.AddrPortFrom(addr.Unmap(), port) + } + } + } + return netip.AddrPort{} +} diff --git a/docs/config.yaml b/docs/config.yaml index b23a35f2c0..6859f5b9f5 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -2460,6 +2460,7 @@ listeners: # # rate-limit: 0 # fallback 双向转发限速,单位 bit/s;0 表示不限速 ### 注意,对于vless listener, 如果 "allow-insecure" 不为 true, 至少需要填写 “certificate和private-key” 或 “shadow-tls” 或 “res-tls” 或 “jls-config” 或 “reality-config” 或 “decryption” 的其中一项 ### # allow-insecure: false # 是否允许不开启tls加密(注意:仅用于有 nginx, caddy 前置的情况) + # trusted-proxy-header: "" # header to read real client IP from (e.g. X-Forwarded-For) - name: anytls-in-1 type: anytls @@ -2629,6 +2630,7 @@ listeners: # password: "example" ### 注意,对于trojan listener, 如果 "allow-insecure" 不为 true, 至少需要填写 “certificate和private-key” 或 “shadow-tls” 或 “res-tls” 或 “jls-config” 或 “reality-config” 或 “ss-option” 的其中一项 ### # allow-insecure: false # 是否允许不开启tls加密(注意:仅用于有 nginx, caddy 前置的情况) + # trusted-proxy-header: "" # header to read real client IP from (e.g. X-Forwarded-For) - name: hysteria2-in-1 type: hysteria2 diff --git a/listener/config/trojan.go b/listener/config/trojan.go index e3a75bc9d8..f056f42750 100644 --- a/listener/config/trojan.go +++ b/listener/config/trojan.go @@ -13,23 +13,24 @@ type TrojanUser struct { } type TrojanServer struct { - Enable bool - Listen string - Users []TrojanUser - WsPath string - GrpcServiceName string - Certificate string - PrivateKey string - ClientAuthType string - ClientAuthCert string - EchKey string - AllowInsecure bool - ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` - ResTLS ResTLS `yaml:"res-tls" json:"res-tls,omitempty"` - JLSConfig JLSConfig `yaml:"jls-config" json:"jls-config,omitempty"` - RealityConfig reality.Config `yaml:"reality-config" json:"reality-config,omitempty"` - MuxOption sing.MuxOption - TrojanSSOption TrojanSSOption + Enable bool + Listen string + Users []TrojanUser + WsPath string + GrpcServiceName string + Certificate string + PrivateKey string + ClientAuthType string + ClientAuthCert string + EchKey string + AllowInsecure bool + TrustedProxyHeader string + ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` + ResTLS ResTLS `yaml:"res-tls" json:"res-tls,omitempty"` + JLSConfig JLSConfig `yaml:"jls-config" json:"jls-config,omitempty"` + RealityConfig reality.Config `yaml:"reality-config" json:"reality-config,omitempty"` + MuxOption sing.MuxOption + TrojanSSOption TrojanSSOption } // TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 diff --git a/listener/config/vless.go b/listener/config/vless.go index f16470bda3..0838e35269 100644 --- a/listener/config/vless.go +++ b/listener/config/vless.go @@ -14,24 +14,25 @@ type VlessUser struct { } type VlessServer struct { - Enable bool - Listen string - Users []VlessUser - Decryption string - WsPath string - XHTTPConfig XHTTPConfig - GrpcServiceName string - Certificate string - PrivateKey string - ClientAuthType string - ClientAuthCert string - EchKey string - AllowInsecure bool - ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` - ResTLS ResTLS `yaml:"res-tls" json:"res-tls,omitempty"` - JLSConfig JLSConfig `yaml:"jls-config" json:"jls-config,omitempty"` - RealityConfig reality.Config `yaml:"reality-config" json:"reality-config,omitempty"` - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` + Enable bool + Listen string + Users []VlessUser + Decryption string + WsPath string + XHTTPConfig XHTTPConfig + GrpcServiceName string + Certificate string + PrivateKey string + ClientAuthType string + ClientAuthCert string + EchKey string + AllowInsecure bool + TrustedProxyHeader string + ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"` + ResTLS ResTLS `yaml:"res-tls" json:"res-tls,omitempty"` + JLSConfig JLSConfig `yaml:"jls-config" json:"jls-config,omitempty"` + RealityConfig reality.Config `yaml:"reality-config" json:"reality-config,omitempty"` + MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } type XHTTPConfig struct { diff --git a/listener/hysteria2_realm/validate.go b/listener/hysteria2_realm/validate.go index 2008a2c76b..b654abeae9 100644 --- a/listener/hysteria2_realm/validate.go +++ b/listener/hysteria2_realm/validate.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/metacubex/http" + "github.com/metacubex/mihomo/common/httputils" ) const ( @@ -42,20 +43,10 @@ func writeJSON(w http.ResponseWriter, status int, v any) { } func clientIP(r *http.Request, header string) string { - if header != "" { - if v := r.Header.Get(header); v != "" { - if i := strings.Index(v, ","); i >= 0 { - v = v[:i] - } - v = strings.TrimSpace(v) - if h, _, err := net.SplitHostPort(v); err == nil { - v = h - } - if addr, err := netip.ParseAddr(v); err == nil { - return addr.Unmap().String() - } - } + if ap := httputils.ClientAddrPortFromHeader(r, header); ap.IsValid() { + return ap.Addr().String() } + host := r.RemoteAddr if h, _, err := net.SplitHostPort(host); err == nil { host = h diff --git a/listener/inbound/trojan.go b/listener/inbound/trojan.go index 526b7c742d..21228a29c6 100644 --- a/listener/inbound/trojan.go +++ b/listener/inbound/trojan.go @@ -11,21 +11,22 @@ import ( type TrojanOption struct { BaseOption - Users []TrojanUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - GrpcServiceName string `inbound:"grpc-service-name,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - ClientAuthType string `inbound:"client-auth-type,omitempty"` - ClientAuthCert string `inbound:"client-auth-cert,omitempty"` - EchKey string `inbound:"ech-key,omitempty"` - AllowInsecure bool `inbound:"allow-insecure,omitempty"` - ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` - ResTLS ResTLS `inbound:"res-tls,omitempty"` - JLSConfig JLSConfig `inbound:"jls-config,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` - SSOption TrojanSSOption `inbound:"ss-option,omitempty"` + Users []TrojanUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + GrpcServiceName string `inbound:"grpc-service-name,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + AllowInsecure bool `inbound:"allow-insecure,omitempty"` + TrustedProxyHeader string `inbound:"trusted-proxy-header,omitempty"` + ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` + ResTLS ResTLS `inbound:"res-tls,omitempty"` + JLSConfig JLSConfig `inbound:"jls-config,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` + SSOption TrojanSSOption `inbound:"ss-option,omitempty"` } type TrojanUser struct { @@ -67,22 +68,23 @@ func NewTrojan(options *TrojanOption) (*Trojan, error) { Base: base, config: options, vs: LC.TrojanServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - GrpcServiceName: options.GrpcServiceName, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - ClientAuthType: options.ClientAuthType, - ClientAuthCert: options.ClientAuthCert, - EchKey: options.EchKey, - AllowInsecure: options.AllowInsecure, - ShadowTLS: options.ShadowTLS.Build(), - ResTLS: options.ResTLS.Build(), - JLSConfig: options.JLSConfig.Build(), - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + GrpcServiceName: options.GrpcServiceName, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, + EchKey: options.EchKey, + AllowInsecure: options.AllowInsecure, + TrustedProxyHeader: options.TrustedProxyHeader, + ShadowTLS: options.ShadowTLS.Build(), + ResTLS: options.ResTLS.Build(), + JLSConfig: options.JLSConfig.Build(), + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), TrojanSSOption: LC.TrojanSSOption{ Enabled: options.SSOption.Enabled, Method: options.SSOption.Method, diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go index 503caf5e9f..4d2a61af10 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -11,22 +11,23 @@ import ( type VlessOption struct { BaseOption - Users []VlessUser `inbound:"users"` - Decryption string `inbound:"decryption,omitempty"` - WsPath string `inbound:"ws-path,omitempty"` - XHTTPConfig XHTTPConfig `inbound:"xhttp-config,omitempty"` - GrpcServiceName string `inbound:"grpc-service-name,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - ClientAuthType string `inbound:"client-auth-type,omitempty"` - ClientAuthCert string `inbound:"client-auth-cert,omitempty"` - EchKey string `inbound:"ech-key,omitempty"` - AllowInsecure bool `inbound:"allow-insecure,omitempty"` - ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` - ResTLS ResTLS `inbound:"res-tls,omitempty"` - JLSConfig JLSConfig `inbound:"jls-config,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` + Users []VlessUser `inbound:"users"` + Decryption string `inbound:"decryption,omitempty"` + WsPath string `inbound:"ws-path,omitempty"` + XHTTPConfig XHTTPConfig `inbound:"xhttp-config,omitempty"` + GrpcServiceName string `inbound:"grpc-service-name,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + ClientAuthType string `inbound:"client-auth-type,omitempty"` + ClientAuthCert string `inbound:"client-auth-cert,omitempty"` + EchKey string `inbound:"ech-key,omitempty"` + AllowInsecure bool `inbound:"allow-insecure,omitempty"` + TrustedProxyHeader string `inbound:"trusted-proxy-header,omitempty"` + ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"` + ResTLS ResTLS `inbound:"res-tls,omitempty"` + JLSConfig JLSConfig `inbound:"jls-config,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` } type VlessUser struct { @@ -113,24 +114,25 @@ func NewVless(options *VlessOption) (*Vless, error) { Base: base, config: options, vs: LC.VlessServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - Decryption: options.Decryption, - WsPath: options.WsPath, - XHTTPConfig: options.XHTTPConfig.Build(), - GrpcServiceName: options.GrpcServiceName, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - ClientAuthType: options.ClientAuthType, - ClientAuthCert: options.ClientAuthCert, - EchKey: options.EchKey, - AllowInsecure: options.AllowInsecure, - ShadowTLS: options.ShadowTLS.Build(), - ResTLS: options.ResTLS.Build(), - JLSConfig: options.JLSConfig.Build(), - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), + Enable: true, + Listen: base.RawAddress(), + Users: users, + Decryption: options.Decryption, + WsPath: options.WsPath, + XHTTPConfig: options.XHTTPConfig.Build(), + GrpcServiceName: options.GrpcServiceName, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + ClientAuthType: options.ClientAuthType, + ClientAuthCert: options.ClientAuthCert, + EchKey: options.EchKey, + AllowInsecure: options.AllowInsecure, + TrustedProxyHeader: options.TrustedProxyHeader, + ShadowTLS: options.ShadowTLS.Build(), + ResTLS: options.ResTLS.Build(), + JLSConfig: options.JLSConfig.Build(), + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), }, }, nil } diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index e1237ddc88..0b1a95f7ee 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -8,6 +8,7 @@ import ( "time" "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/common/httputils" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ech" C "github.com/metacubex/mihomo/constant" @@ -172,7 +173,7 @@ func New(config LC.VlessServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit http.Error(w, err.Error(), 500) return } - sl.HandleConn(conn, tunnel, additions...) + sl.HandleHTTPConn(r, conn, tunnel, additions...) }) httpServer.Handler = httpMux httpServer.Protocols.SetHTTP1(true) @@ -181,8 +182,8 @@ func New(config LC.VlessServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit if config.GrpcServiceName != "" { httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, - ConnHandler: func(conn net.Conn) { - sl.HandleConn(conn, tunnel, additions...) + ConnHandler: func(conn net.Conn, r *http.Request) { + sl.HandleHTTPConn(r, conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, }) @@ -229,8 +230,8 @@ func New(config LC.VlessServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit ScMaxBufferedPosts: config.XHTTPConfig.ScMaxBufferedPosts, ScMaxEachPostBytes: config.XHTTPConfig.ScMaxEachPostBytes, }, - ConnHandler: func(conn net.Conn) { - sl.HandleConn(conn, tunnel, additions...) + ConnHandler: func(conn net.Conn, r *http.Request) { + sl.HandleHTTPConn(r, conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, }) @@ -325,6 +326,13 @@ func (l *Listener) AddrList() (addrList []net.Addr) { return } +func (l *Listener) HandleHTTPConn(r *http.Request, conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + if ap := httputils.ClientAddrPortFromHeader(r, l.config.TrustedProxyHeader); ap.IsValid() { + additions = append(slices.Clip(additions), inbound.WithSrcAddrPort(ap)) + } + l.HandleConn(conn, tunnel, additions...) +} + func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { ctx := sing.WithAdditions(context.TODO(), additions...) if l.decryption != nil { diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index 156432b9e2..178cc89480 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -215,7 +215,7 @@ func New(config LC.VmessServer, lc C.InboundListenConfig, tunnel C.Tunnel, addit if config.GrpcServiceName != "" { httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, - ConnHandler: func(conn net.Conn) { + ConnHandler: func(conn net.Conn, r *http.Request) { sl.HandleConn(conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, diff --git a/listener/trojan/server.go b/listener/trojan/server.go index ff2fbd72aa..6cb095df12 100644 --- a/listener/trojan/server.go +++ b/listener/trojan/server.go @@ -9,6 +9,7 @@ import ( "time" "github.com/metacubex/mihomo/adapter/inbound" + "github.com/metacubex/mihomo/common/httputils" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/component/ca" @@ -30,6 +31,7 @@ import ( "github.com/metacubex/http" "github.com/metacubex/smux" "github.com/metacubex/tls" + "golang.org/x/exp/slices" ) type Listener struct { @@ -168,7 +170,7 @@ func New(config LC.TrojanServer, lc C.InboundListenConfig, tunnel C.Tunnel, addi http.Error(w, err.Error(), 500) return } - sl.HandleConn(conn, tunnel, additions...) + sl.HandleHTTPConn(r, conn, tunnel, additions...) }) httpServer.Handler = httpMux httpServer.Protocols.SetHTTP1(true) @@ -177,8 +179,8 @@ func New(config LC.TrojanServer, lc C.InboundListenConfig, tunnel C.Tunnel, addi if config.GrpcServiceName != "" { httpServer.Handler = gun.NewServerHandler(gun.ServerOption{ ServiceName: config.GrpcServiceName, - ConnHandler: func(conn net.Conn) { - sl.HandleConn(conn, tunnel, additions...) + ConnHandler: func(conn net.Conn, r *http.Request) { + sl.HandleHTTPConn(r, conn, tunnel, additions...) }, HttpHandler: httpServer.Handler, }) @@ -262,6 +264,13 @@ func (l *Listener) AddrList() (addrList []net.Addr) { return } +func (l *Listener) HandleHTTPConn(r *http.Request, conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + if ap := httputils.ClientAddrPortFromHeader(r, l.config.TrustedProxyHeader); ap.IsValid() { + additions = append(slices.Clip(additions), inbound.WithSrcAddrPort(ap)) + } + l.HandleConn(conn, tunnel, additions...) +} + func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { defer conn.Close() diff --git a/transport/gun/server.go b/transport/gun/server.go index 8c88a306f7..172c3b6c60 100644 --- a/transport/gun/server.go +++ b/transport/gun/server.go @@ -16,7 +16,7 @@ import ( type ServerOption struct { ServiceName string - ConnHandler func(conn net.Conn) + ConnHandler func(conn net.Conn, r *http.Request) HttpHandler http.Handler } @@ -50,7 +50,7 @@ func NewServerHandler(options ServerOption) http.Handler { // so call N.NewDeadlineConn to add a safe wrapper ExtendedConn: N.NewDeadlineConn(conn), } - connHandler(wrapper) + connHandler(wrapper, request) wrapper.CloseWrapper() return diff --git a/transport/xhttp/server.go b/transport/xhttp/server.go index d56ae98aae..618b41f035 100644 --- a/transport/xhttp/server.go +++ b/transport/xhttp/server.go @@ -19,7 +19,7 @@ import ( type ServerOption struct { Config - ConnHandler func(net.Conn) + ConnHandler func(net.Conn, *http.Request) HttpHandler http.Handler } @@ -97,7 +97,7 @@ func (s *httpSession) markConnected() { type requestHandler struct { config Config - connHandler func(net.Conn) + connHandler func(net.Conn, *http.Request) httpHandler http.Handler xPaddingBytes Range @@ -456,7 +456,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } httputils.SetAddrFromRequest(&conn.NetAddr, r) - go h.connHandler(N.NewDeadlineConn(conn)) + go h.connHandler(N.NewDeadlineConn(conn), r) select { case <-r.Context().Done(): @@ -484,7 +484,7 @@ func (h *requestHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } httputils.SetAddrFromRequest(&conn.NetAddr, r) - go h.connHandler(N.NewDeadlineConn(conn)) + go h.connHandler(N.NewDeadlineConn(conn), r) select { case <-r.Context().Done(): diff --git a/transport/xhttp/server_test.go b/transport/xhttp/server_test.go index b194186b80..636a583620 100644 --- a/transport/xhttp/server_test.go +++ b/transport/xhttp/server_test.go @@ -84,7 +84,7 @@ func TestServerHandlerModeRestrictions(t *testing.T) { } handler, err := NewServerHandler(ServerOption{ Config: config, - ConnHandler: func(conn net.Conn) { + ConnHandler: func(conn net.Conn, r *http.Request) { _ = conn.Close() }, })