Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP

This commit is contained in:
Sergei Maklagin
2026-05-11 00:59:35 +03:00
parent 652e0baf57
commit 3bd162ed6f
241 changed files with 36409 additions and 4086 deletions

View File

@@ -3,7 +3,6 @@ package xhttp
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"io"
"net"
@@ -13,8 +12,10 @@ import (
"sync"
common "github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/buf"
"github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
)
// interface to abstract between use of browser dialer, vs net/http
@@ -24,8 +25,8 @@ type DialerClient interface {
// ctx, url, sessionId, body, uploadOnly
OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
// ctx, url, sessionId, seqStr, body, contentLength
PostPacket(context.Context, string, string, string, io.Reader, int64) error
// ctx, url, sessionId, seqStr, payload
PostPacket(context.Context, string, string, string, buf.MultiBuffer) error
}
// implements xhttp.DialerClient in terms of direct network connections
@@ -60,30 +61,7 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
method = c.options.GetNormalizedUplinkHTTPMethod() // stream-up/one
}
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
req.Header = c.options.GetRequestHeader()
length := int(c.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if c.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.options.XPaddingPlacement,
Key: c.options.XPaddingKey,
Header: c.options.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
ApplyXPaddingToRequest(req, config)
ApplyMetaToRequest(c.options, req, sessionId, "")
if method == c.options.GetNormalizedUplinkHTTPMethod() && !c.options.NoGRPCHeader {
req.Header.Set("Content-Type", "application/grpc")
}
FillStreamRequest(req, sessionId, "", c.options)
wrc = &WaitReadCloser{Wait: make(chan struct{})}
go func() {
resp, err := c.client.Do(req)
@@ -107,76 +85,13 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessio
return
}
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
var encodedData string
dataPlacement := c.options.GetNormalizedUplinkDataPlacement()
if dataPlacement != option.PlacementBody {
data, err := io.ReadAll(body)
if err != nil {
return err
}
encodedData = base64.RawURLEncoding.EncodeToString(data)
body = nil
contentLength = 0
}
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, payload buf.MultiBuffer) error {
method := c.options.GetNormalizedUplinkHTTPMethod()
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, nil)
if err != nil {
return err
}
req.ContentLength = contentLength
req.Header = c.options.GetRequestHeader()
if dataPlacement != option.PlacementBody {
key := c.options.UplinkDataKey
chunkSize := int(c.options.UplinkChunkSize)
switch dataPlacement {
case option.PlacementHeader:
for i := 0; i < len(encodedData); i += chunkSize {
end := i + chunkSize
if end > len(encodedData) {
end = len(encodedData)
}
chunk := encodedData[i:end]
headerKey := fmt.Sprintf("%s-%d", key, i/chunkSize)
req.Header.Set(headerKey, chunk)
}
req.Header.Set(key+"-Length", fmt.Sprintf("%d", len(encodedData)))
req.Header.Set(key+"-Upstream", "1")
case option.PlacementCookie:
for i := 0; i < len(encodedData); i += chunkSize {
end := i + chunkSize
if end > len(encodedData) {
end = len(encodedData)
}
chunk := encodedData[i:end]
cookieName := fmt.Sprintf("%s_%d", key, i/chunkSize)
req.AddCookie(&http.Cookie{Name: cookieName, Value: chunk})
}
req.AddCookie(&http.Cookie{Name: key + "_upstream", Value: "1"})
}
}
length := int(c.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if c.options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: c.options.XPaddingPlacement,
Key: c.options.XPaddingKey,
Header: c.options.XPaddingHeader,
RawURL: url,
}
config.Method = PaddingMethod(c.options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: url,
}
}
ApplyXPaddingToRequest(req, config)
ApplyMetaToRequest(c.options, req, sessionId, seqStr)
FillPacketRequest(req, sessionId, seqStr, payload, c.options)
if c.httpVersion != "1.1" {
resp, err := c.client.Do(req)
if err != nil {
@@ -185,12 +100,16 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessio
}
io.Copy(io.Discard, resp.Body)
defer resp.Body.Close()
if resp.StatusCode != 200 {
return E.New("bad status code: ", resp.Status)
}
} else {
// stringify the entire HTTP/1.1 request so it can be
// safely retried. if instead req.Write is called multiple
// times, the body is already drained after the first
// request
requestBuff := new(bytes.Buffer)
requestBuff.Grow(512 + int(req.ContentLength))
common.Must(req.Write(requestBuff))
var uploadConn any
var h1UploadConn *H1Conn