From 364c6a6e6a26a635eaa730d5d12f9bab9e45a342 Mon Sep 17 00:00:00 2001 From: Shtorm <108103062+shtorm-7@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:20:03 +0300 Subject: [PATCH] xhttp: Port upstream Xray fixes and config checks --- option/v2ray_transport.go | 60 +++++++++++++++++++++++++++++----- transport/v2rayxhttp/dialer.go | 6 +++- transport/v2rayxhttp/server.go | 43 ++++++++++++------------ transport/v2rayxhttp/utils.go | 16 +-------- 4 files changed, 79 insertions(+), 46 deletions(-) diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index 67e31df7..c229d54f 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -1,6 +1,7 @@ package option import ( + "math/big" "net/http" "reflect" "strings" @@ -186,6 +187,18 @@ const ( PlacementAuto = "auto" ) +var V2RayXHTTPPredefinedSessionIDTable = map[string]string{ + "ALPHABET": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "BASE36": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "Base62": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", + "HEX": "0123456789ABCDEF", + "alphabet": "abcdefghijklmnopqrstuvwxyz", + "base36": "0123456789abcdefghijklmnopqrstuvwxyz", + "hex": "0123456789abcdef", + "number": "0123456789", +} + func (c V2RayXHTTPOptions) MarshalJSON() ([]byte, error) { return json.Marshal((*_V2RayXHTTPOptions)(&c)) } @@ -202,6 +215,9 @@ func (c *V2RayXHTTPOptions) UnmarshalJSON(bytes []byte) error { default: return E.New("unsupported mode: " + c.Mode) } + if c.Download != nil && c.Mode == "stream-one" { + return E.New(`can not use "download" in "stream-one" mode`) + } err = checkV2RayXHTTPBaseOptions(c.Mode, &c.V2RayXHTTPBaseOptions) if err != nil { return err @@ -297,6 +313,27 @@ func checkV2RayXHTTPBaseOptions(mode string, options *V2RayXHTTPBaseOptions) err } } + if options.SessionIDTable != "" { + if predefined, ok := V2RayXHTTPPredefinedSessionIDTable[options.SessionIDTable]; ok { + options.SessionIDTable = predefined + } + if options.SessionIDLength.From <= 0 { + return E.New("session_id_length.from must be greater than 0") + } + room := new(big.Int) + for length := options.SessionIDLength.From; length <= options.SessionIDLength.To; length++ { + room.Add(room, new(big.Int).Exp(big.NewInt(int64(len(options.SessionIDTable))), big.NewInt(int64(length)), nil)) + } + if room.Cmp(big.NewInt(2<<30)) < 0 { + return E.New("session_id_table or session_id_length is too small") + } + for i := 0; i < len(options.SessionIDTable); i++ { + if options.SessionIDTable[i] >= 0x80 { + return E.New("session_id_table must contain only ASCII characters") + } + } + } + if options.SeqPlacement != "path" && options.SeqKey == "" { switch options.SeqPlacement { case "cookie", "query": @@ -325,14 +362,17 @@ func checkV2RayXHTTPBaseOptions(mode string, options *V2RayXHTTPBaseOptions) err if options.Xmux == nil { options.Xmux = &V2RayXHTTPXmuxOptions{} - options.Xmux.MaxConcurrency.From = 1 - options.Xmux.MaxConcurrency.To = 1 + } + if options.Xmux.MaxConnections.To > 0 && options.Xmux.MaxConcurrency.To > 0 { + return E.New("max_connections cannot be specified together with max_concurrency") + } + if *options.Xmux == (V2RayXHTTPXmuxOptions{}) { + options.Xmux.MaxConnections.From = 3 + options.Xmux.MaxConnections.To = 3 options.Xmux.HMaxRequestTimes.From = 600 options.Xmux.HMaxRequestTimes.To = 900 options.Xmux.HMaxReusableSecs.From = 1800 options.Xmux.HMaxReusableSecs.To = 3000 - } else if options.Xmux.MaxConnections.To > 0 && options.Xmux.MaxConcurrency.To > 0 { - return E.New("max_connections cannot be specified together with max_concurrency") } return nil } @@ -343,8 +383,10 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string { if path == "" || path[0] != '/' { path = "/" + path } - if path[len(path)-1] != '/' { - path = path + "/" + if c.GetNormalizedSessionPlacement() == PlacementPath || c.GetNormalizedSeqPlacement() == PlacementPath { + if path[len(path)-1] != '/' { + path = path + "/" + } } return path } @@ -385,7 +427,7 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkHTTPMethod() string { } func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() badoption.Range[int] { - if c.ScMaxEachPostBytes == nil { + if c.ScMaxEachPostBytes == nil || c.ScMaxEachPostBytes.To == 0 { return badoption.Range[int]{ From: 1000000, To: 1000000, @@ -395,7 +437,7 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() badoption.Rang } func (c *V2RayXHTTPBaseOptions) GetNormalizedScMinPostsIntervalMs() badoption.Range[int] { - if c.ScMinPostsIntervalMs == nil { + if c.ScMinPostsIntervalMs == nil || c.ScMinPostsIntervalMs.To == 0 { return badoption.Range[int]{ From: 30, To: 30, @@ -413,7 +455,7 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxBufferedPosts() int { } func (c *V2RayXHTTPBaseOptions) GetNormalizedScStreamUpServerSecs() badoption.Range[int] { - if c.ScStreamUpServerSecs == nil { + if c.ScStreamUpServerSecs == nil || c.ScStreamUpServerSecs.To == 0 { return badoption.Range[int]{ From: 20, To: 80, diff --git a/transport/v2rayxhttp/dialer.go b/transport/v2rayxhttp/dialer.go index 18921829..82c9f28a 100644 --- a/transport/v2rayxhttp/dialer.go +++ b/transport/v2rayxhttp/dialer.go @@ -79,7 +79,11 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio method = c.options.GetNormalizedUplinkHTTPMethod() // stream-up/one } reqCtx, cancel := context.WithCancel(context.WithoutCancel(ctx)) - req, _ := http.NewRequestWithContext(reqCtx, method, url, body) + req, err := http.NewRequestWithContext(reqCtx, method, url, body) + if err != nil { + cancel() + return nil, nil, nil, err + } FillStreamRequest(req, sessionId, "", c.options) wrc = &WaitReadCloser{wait: done.New(), Cancel: cancel} go func() { diff --git a/transport/v2rayxhttp/server.go b/transport/v2rayxhttp/server.go index 52b1308c..2c4eeaba 100644 --- a/transport/v2rayxhttp/server.go +++ b/transport/v2rayxhttp/server.go @@ -22,7 +22,6 @@ import ( "github.com/sagernet/sing-box/common/kmutex" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/xray/buf" - xnet "github.com/sagernet/sing-box/common/xray/net" "github.com/sagernet/sing-box/common/xray/signal/done" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -35,7 +34,6 @@ import ( N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" aTLS "github.com/sagernet/sing/common/tls" - sHttp "github.com/sagernet/sing/protocol/http" ) var _ adapter.V2RayServerTransport = (*Server)(nil) @@ -143,6 +141,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusBadRequest) return } + obfsPaddingAccepted := s.options.XPaddingObfsMode && paddingValue != "" sessionId, seqStr := ExtractMetaFromRequest(s.options, request, s.path) if s.options.Mode != "" && s.options.Mode != "auto" { if sessionId == "" { @@ -159,17 +158,6 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { } } } - var forwardedAddrs []xnet.Address - if len(s.options.TrustedXForwardedFor) > 0 { - for _, key := range s.options.TrustedXForwardedFor { - if len(request.Header.Values(key)) > 0 { - forwardedAddrs = parseXForwardedFor(request.Header) - break - } - } - } else { - forwardedAddrs = parseXForwardedFor(request.Header) - } var remoteAddr net.Addr var err error remoteAddr, err = net.ResolveTCPAddr("tcp", request.RemoteAddr) @@ -185,10 +173,20 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { Port: remoteAddr.(*net.TCPAddr).Port, } } - if len(forwardedAddrs) > 0 && forwardedAddrs[0].Family().IsIP() { - remoteAddr = &net.TCPAddr{ - IP: forwardedAddrs[0].IP(), - Port: 0, + if forwardedFor := request.Header.Get("X-Forwarded-For"); forwardedFor != "" { + if slices.ContainsFunc(s.options.TrustedXForwardedFor, func(key string) bool { + return len(request.Header.Values(key)) > 0 + }) { + if forwardedAddress := parseXForwardedFor(request.Header)[0]; forwardedAddress.Family().IsIP() { + remoteAddr = &net.TCPAddr{ + IP: forwardedAddress.IP(), + Port: 0, + } + } + } else if len(s.options.TrustedXForwardedFor) == 0 { + s.logger.WarnContext(request.Context(), "received \"X-Forwarded-For\" from ", remoteAddr, " but \"trusted_x_forwarded_for\" is not configured; ignoring it and using the real remote address") + } else { + s.logger.ErrorContext(request.Context(), "ignored potentially forged \"X-Forwarded-For\" from ", remoteAddr, ": ", forwardedFor) } } var currentSession *httpSession @@ -228,8 +226,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Cache-Control", "no-store") writer.WriteHeader(http.StatusOK) scStreamUpServerSecs := s.options.GetNormalizedScStreamUpServerSecs() - referrer := request.Header.Get("Referer") - if referrer != "" && scStreamUpServerSecs.To > 0 { + if (request.Header.Get("Referer") != "" || obfsPaddingAccepted) && scStreamUpServerSecs.To > 0 { go func() { timer := time.NewTimer(0) if !timer.Stop() { @@ -384,16 +381,20 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { Reader: request.Body, ResponseWriter: writer, } + localAddress := s.localAddr + if requestLocalAddress, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && requestLocalAddress != nil { + localAddress = requestLocalAddress + } conn := splitConn{ writer: httpSC, reader: httpSC, remoteAddr: remoteAddr, - localAddr: s.localAddr, + localAddr: localAddress, } if sessionId != "" { // if not stream-one conn.reader = currentSession.uploadQueue } - s.handler.NewConnectionEx(v2rayhttp.HWIDContext(request.Context(), request.Header), &conn, sHttp.SourceAddress(request), M.Socksaddr{}, func(it error) {}) + s.handler.NewConnectionEx(v2rayhttp.HWIDContext(request.Context(), request.Header), &conn, M.SocksaddrFromNet(remoteAddr).Unwrap(), M.Socksaddr{}, func(it error) {}) // "A ResponseWriter may not be used after [Handler.ServeHTTP] has returned." select { case <-request.Context().Done(): diff --git a/transport/v2rayxhttp/utils.go b/transport/v2rayxhttp/utils.go index 96c9b8d5..37de5c97 100644 --- a/transport/v2rayxhttp/utils.go +++ b/transport/v2rayxhttp/utils.go @@ -14,24 +14,10 @@ import ( "github.com/sagernet/sing-box/option" ) -// PredefinedTable maps named charsets to their alphabets for session ID generation. - -var PredefinedTable = map[string]string{ - "ALPHABET": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "Alphabet": "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", - "BASE36": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "Base62": "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", - "HEX": "0123456789ABCDEF", - "alphabet": "abcdefghijklmnopqrstuvwxyz", - "base36": "0123456789abcdefghijklmnopqrstuvwxyz", - "hex": "0123456789abcdef", - "number": "0123456789", -} - func GenerateSessionID(options *option.V2RayXHTTPBaseOptions) string { length := options.SessionIDLength.Rand() table := options.SessionIDTable - if predefined, ok := PredefinedTable[table]; ok { + if predefined, ok := option.V2RayXHTTPPredefinedSessionIDTable[table]; ok { table = predefined } if table != "" && length > 0 {