Add Snell protocol. Refactor MASQUE HTTP/2, Fair Queue. Update XHTTP, OpenVPN, Sudoku, Fallback. Fixes

This commit is contained in:
Shtorm
2026-06-26 01:25:57 +03:00
parent d174962a04
commit edf38d33d6
107 changed files with 5346 additions and 708 deletions

View File

@@ -18,6 +18,8 @@ import (
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/congestion"
"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"
@@ -31,6 +33,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
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"
)
@@ -49,7 +52,7 @@ type Server struct {
options *option.V2RayXHTTPOptions
host string
path string
sessionMu sync.Mutex
sessionMu *kmutex.Kmutex[string]
sessions sync.Map
}
@@ -62,6 +65,7 @@ func NewServer(ctx context.Context, logger logger.ContextLogger, options option.
options: &options,
host: options.Host,
path: options.GetNormalizedPath(),
sessionMu: kmutex.New[string](),
}
if server.network() == N.NetworkTCP {
protocols := new(http.Protocols)
@@ -80,11 +84,21 @@ func NewServer(ctx context.Context, logger logger.ContextLogger, options option.
},
}
} else {
congestionControlFactory, err := congestion.NewCongestionControl(options.CongestionController, options.CWND, ntp.TimeFuncFromContext(ctx))
if err != nil {
return nil, err
}
server.quicConfig = &quic.Config{
DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows,
}
server.http3Server = &http3.Server{
Handler: server,
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
if congestionControlFactory != nil {
conn.SetCongestionControl(congestionControlFactory(conn))
}
return log.ContextWithNewID(ctx)
},
}
}
return server, nil
@@ -102,7 +116,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
return
}
WriteResponseHeader(writer, request.Method, request.Header, s.options)
length := int(s.options.GetNormalizedXPaddingBytes().Rand())
length := s.options.GetNormalizedXPaddingBytes().Rand()
config := XPaddingConfig{Length: length}
if s.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
@@ -125,15 +139,25 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
validRange := s.options.GetNormalizedXPaddingBytes()
paddingValue, paddingPlacement := ExtractXPaddingFromRequest(&s.options.V2RayXHTTPBaseOptions, request, s.options.XPaddingObfsMode)
if !IsPaddingValid(&s.options.V2RayXHTTPBaseOptions, paddingValue, validRange.From, validRange.To, PaddingMethod(s.options.XPaddingMethod)) {
s.logger.ErrorContext(request.Context(), "invalid padding ("+paddingPlacement+") length:", int32(len(paddingValue)))
s.logger.ErrorContext(request.Context(), "invalid padding ("+paddingPlacement+") length:", len(paddingValue))
writer.WriteHeader(http.StatusBadRequest)
return
}
sessionId, seqStr := ExtractMetaFromRequest(s.options, request, s.path)
if sessionId == "" && s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-one" && s.options.Mode != "stream-up" {
s.logger.ErrorContext(request.Context(), "stream-one mode is not allowed")
writer.WriteHeader(http.StatusBadRequest)
return
if s.options.Mode != "" && s.options.Mode != "auto" {
if sessionId == "" {
if s.options.Mode != "stream-one" && s.options.Mode != "stream-up" {
s.logger.ErrorContext(request.Context(), "stream-one mode is not allowed")
writer.WriteHeader(http.StatusBadRequest)
return
}
} else {
if s.options.Mode == "stream-one" {
s.logger.ErrorContext(request.Context(), "session is not allowed in stream-one mode")
writer.WriteHeader(http.StatusBadRequest)
return
}
}
}
var forwardedAddrs []xnet.Address
if len(s.options.TrustedXForwardedFor) > 0 {
@@ -171,7 +195,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if sessionId != "" {
currentSession = s.upsertSession(sessionId)
}
scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To)
scMaxEachPostBytes := s.options.GetNormalizedScMaxEachPostBytes().To
uplinkDataPlacement := s.options.GetNormalizedUplinkDataPlacement()
uplinkDataKey := s.options.UplinkDataKey
isUplinkRequest := false
@@ -207,12 +231,22 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
referrer := request.Header.Get("Referer")
if referrer != "" && scStreamUpServerSecs.To > 0 {
go func() {
timer := time.NewTimer(0)
if !timer.Stop() {
<-timer.C
}
defer timer.Stop()
for {
_, err := httpSC.Write(bytes.Repeat([]byte{'X'}, int(s.options.GetNormalizedXPaddingBytes().Rand())))
_, err := httpSC.Write(bytes.Repeat([]byte{'X'}, s.options.GetNormalizedXPaddingBytes().Rand()))
if err != nil {
break
return
}
timer.Reset(time.Duration(scStreamUpServerSecs.Rand()) * time.Second)
select {
case <-timer.C:
case <-httpSC.Wait():
return
}
time.Sleep(time.Duration(scStreamUpServerSecs.Rand()) * time.Second)
}
}()
}
@@ -327,7 +361,11 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
// after GET is done, the connection is finished. disable automatic
// session reaping, and handle it in defer
currentSession.isFullyConnected.Close()
defer s.sessions.Delete(sessionId)
defer func() {
s.sessionMu.Lock(sessionId)
defer s.sessionMu.Unlock(sessionId)
s.sessions.Delete(sessionId)
}()
}
// magic header instructs nginx + apache to not buffer response body
writer.Header().Set("X-Accel-Buffering", "no")
@@ -410,32 +448,27 @@ func (s *Server) network() string {
}
func (s *Server) upsertSession(sessionId string) *httpSession {
// fast path
s.sessionMu.Lock(sessionId)
defer s.sessionMu.Unlock(sessionId)
currentSessionAny, ok := s.sessions.Load(sessionId)
if ok {
return currentSessionAny.(*httpSession)
}
// slow path
s.sessionMu.Lock()
defer s.sessionMu.Unlock()
currentSessionAny, ok = s.sessions.Load(sessionId)
if ok {
return currentSessionAny.(*httpSession)
}
session := &httpSession{
uploadQueue: NewUploadQueue(s.options.GetNormalizedScMaxBufferedPosts()),
isFullyConnected: done.New(),
}
s.sessions.Store(sessionId, session)
shouldReap := done.New()
go func() {
time.Sleep(30 * time.Second)
shouldReap.Close()
}()
go func() {
reapTimer := time.NewTimer(30 * time.Second)
defer reapTimer.Stop()
select {
case <-shouldReap.Wait():
s.sessions.Delete(sessionId)
case <-reapTimer.C:
s.sessionMu.Lock(sessionId)
if current, ok := s.sessions.Load(sessionId); ok && current.(*httpSession) == session {
s.sessions.Delete(sessionId)
}
s.sessionMu.Unlock(sessionId)
session.uploadQueue.Close()
case <-session.isFullyConnected.Wait():
}