xhttp: Port upstream Xray fixes and config checks

This commit is contained in:
Shtorm
2026-10-19 15:20:03 +03:00
parent cb165a381c
commit 364c6a6e6a
4 changed files with 79 additions and 46 deletions
+51 -9
View File
@@ -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,
+5 -1
View File
@@ -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() {
+22 -21
View File
@@ -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():
+1 -15
View File
@@ -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 {