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

@@ -8,7 +8,7 @@ type NetBuffer struct {
}
func (n *NetBuffer) Get() []byte {
return *(n.buf.Get().(*[]byte))
return *n.buf.Get().(*[]byte)
}
func (n *NetBuffer) Put(buf []byte) {

View File

@@ -1,12 +1,17 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.11
// protoc v6.31.1
// source: transport/v2raygrpc/stream.proto
package v2raygrpc
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
)
const (
@@ -83,13 +88,10 @@ func file_transport_v2raygrpc_stream_proto_rawDescGZIP() []byte {
return file_transport_v2raygrpc_stream_proto_rawDescData
}
var (
file_transport_v2raygrpc_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
file_transport_v2raygrpc_stream_proto_goTypes = []any{
(*Hunk)(nil), // 0: transport.v2raygrpc.Hunk
}
)
var file_transport_v2raygrpc_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_v2raygrpc_stream_proto_goTypes = []any{
(*Hunk)(nil), // 0: transport.v2raygrpc.Hunk
}
var file_transport_v2raygrpc_stream_proto_depIdxs = []int32{
0, // 0: transport.v2raygrpc.GunService.Tun:input_type -> transport.v2raygrpc.Hunk
0, // 1: transport.v2raygrpc.GunService.Tun:output_type -> transport.v2raygrpc.Hunk

View File

@@ -1,8 +1,13 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v6.31.1
// source: transport/v2raygrpc/stream.proto
package v2raygrpc
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"

View File

@@ -183,12 +183,14 @@ func NewConnection(meta ConnMetadata, writer PacketWriter, closer io.Closer, con
return !isTerminating() && (conn.sendingWorker.UpdateNecessary() || conn.receivingWorker.UpdateNecessary())
},
isTerminating,
conn.updateTask)
conn.updateTask,
)
conn.pingUpdater = NewUpdater(
5000,
func() bool { return !isTerminated() },
isTerminated,
conn.updateTask)
conn.updateTask,
)
conn.pingUpdater.WakeUp()
return conn

View File

@@ -176,15 +176,15 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
}
scMaxEachPostBytes := options.GetNormalizedScMaxEachPostBytes()
scMinPostsIntervalMs := options.GetNormalizedScMinPostsIntervalMs()
if scMaxEachPostBytes.From <= buf.Size {
panic("`scMaxEachPostBytes` should be bigger than " + strconv.Itoa(buf.Size))
if scMaxEachPostBytes.From <= 0 {
panic("`scMaxEachPostBytes` should be bigger than 0")
}
maxUploadSize := scMaxEachPostBytes.Rand()
// WithSizeLimit(0) will still allow single bytes to pass, and a lot of
// code relies on this behavior. Subtract 1 so that together with
// uploadWriter wrapper, exact size limits can be enforced
// uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize - 1))
uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(maxUploadSize - buf.Size))
uploadPipeReader, uploadPipeWriter := pipe.New(pipe.WithSizeLimit(max(0, maxUploadSize-buf.Size)))
conn.writer = uploadWriter{
uploadPipeWriter,
maxUploadSize,
@@ -193,48 +193,53 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
var seq int64
var lastWrite time.Time
for {
wroteRequest := done.New()
ctx := httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
WroteRequest: func(httptrace.WroteRequestInfo) {
wroteRequest.Close()
},
})
// this intentionally makes a shallow-copy of the struct so we
// can reassign Path (potentially concurrently)
url := requestURL
seqStr := strconv.FormatInt(seq, 10)
seq += 1
if scMinPostsIntervalMs.From > 0 {
time.Sleep(time.Duration(scMinPostsIntervalMs.Rand())*time.Millisecond - time.Since(lastWrite))
}
// by offloading the uploads into a buffered pipe, multiple conn.Write
// calls get automatically batched together into larger POST requests.
// without batching, bandwidth is extremely limited.
chunk, err := uploadPipeReader.ReadMultiBuffer()
remainder, err := uploadPipeReader.ReadMultiBuffer()
if err != nil {
break
}
lastWrite = time.Now()
if xmuxClient != nil && (xmuxClient.LeftRequests.Add(-1) <= 0 ||
(xmuxClient.UnreusableAt != time.Time{} && lastWrite.After(xmuxClient.UnreusableAt))) {
httpClient, xmuxClient = c.getHTTPClient()
}
go func() {
err := httpClient.PostPacket(
ctx,
url.String(),
sessionId,
seqStr,
&buf.MultiBufferContainer{MultiBuffer: chunk},
int64(chunk.Len()),
)
wroteRequest.Close()
if err != nil {
uploadPipeReader.Interrupt()
doSplit := atomic.Bool{}
for doSplit.Store(true); doSplit.Load(); {
var chunk buf.MultiBuffer
remainder, chunk = buf.SplitSize(remainder, maxUploadSize)
if chunk.IsEmpty() {
break
}
wroteRequest := done.New()
ctx := httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
WroteRequest: func(httptrace.WroteRequestInfo) {
wroteRequest.Close()
},
})
seqStr := strconv.FormatInt(seq, 10)
seq += 1
if scMinPostsIntervalMs.From > 0 {
time.Sleep(time.Duration(scMinPostsIntervalMs.Rand())*time.Millisecond - time.Since(lastWrite))
}
lastWrite = time.Now()
if xmuxClient != nil && (xmuxClient.LeftRequests.Add(-1) <= 0 ||
(xmuxClient.UnreusableAt != time.Time{} && lastWrite.After(xmuxClient.UnreusableAt))) {
httpClient, xmuxClient = c.getHTTPClient()
}
go func() {
err := httpClient.PostPacket(
ctx,
requestURL.String(),
sessionId,
seqStr,
chunk,
)
wroteRequest.Close()
if err != nil {
uploadPipeReader.Interrupt()
doSplit.Store(false)
}
}()
if _, ok := httpClient.(*DefaultDialerClient); ok {
<-wroteRequest.Wait()
}
}()
if _, ok := httpClient.(*DefaultDialerClient); ok {
<-wroteRequest.Wait()
}
}
}()

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

View File

@@ -9,6 +9,7 @@ import (
"net"
"net/http"
"os"
"slices"
"strconv"
"strings"
"sync"
@@ -18,6 +19,8 @@ import (
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter"
"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"
@@ -67,7 +70,7 @@ func NewServer(ctx context.Context, logger logger.ContextLogger, options option.
server.httpServer = &http.Server{
Handler: server,
ReadHeaderTimeout: time.Second * 4,
MaxHeaderBytes: 8192,
MaxHeaderBytes: options.GetNormalizedServerMaxHeaderBytes(),
Protocols: protocols,
BaseContext: func(net.Listener) context.Context {
return ctx
@@ -98,8 +101,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusNotFound)
return
}
writer.Header().Set("Access-Control-Allow-Origin", "*")
writer.Header().Set("Access-Control-Allow-Methods", "*")
WriteResponseHeader(writer, request.Method, request.Header, s.options)
length := int(s.options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if s.options.XPaddingObfsMode {
@@ -115,7 +117,11 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
Header: "X-Padding",
}
}
ApplyXPaddingToHeader(writer.Header(), config)
ApplyXPaddingToResponse(writer, config)
if request.Method == "OPTIONS" {
writer.WriteHeader(http.StatusOK)
return
}
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)) {
@@ -129,7 +135,17 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusBadRequest)
return
}
forwardedAddrs := parseXForwardedFor(request.Header)
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)
@@ -156,22 +172,14 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
currentSession = s.upsertSession(sessionId)
}
scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To)
uplinkHTTPMethod := s.options.GetNormalizedUplinkHTTPMethod()
isUplinkRequest := false
if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
isUplinkRequest = true
}
uplinkDataPlacement := s.options.GetNormalizedUplinkDataPlacement()
uplinkDataKey := s.options.UplinkDataKey
switch uplinkDataPlacement {
case option.PlacementHeader:
if request.Header.Get(uplinkDataKey+"-Upstream") == "1" {
isUplinkRequest = true
}
case option.PlacementCookie:
if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" {
isUplinkRequest = true
}
isUplinkRequest := false
switch request.Method {
case "GET":
isUplinkRequest = seqStr != ""
default:
isUplinkRequest = true
}
if isUplinkRequest && sessionId != "" { // stream-up, packet-up
if seqStr == "" {
@@ -221,65 +229,79 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusBadRequest)
return
}
var payload []byte
if uplinkDataPlacement != option.PlacementBody {
var encodedStr string
switch uplinkDataPlacement {
case option.PlacementHeader:
dataLenStr := request.Header.Get(uplinkDataKey + "-Length")
if dataLenStr != "" {
dataLen, _ := strconv.Atoi(dataLenStr)
var chunks []string
i := 0
for {
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
chunks = append(chunks, chunk)
i++
}
encodedStr = strings.Join(chunks, "")
if len(encodedStr) != dataLen {
encodedStr = ""
}
}
case option.PlacementCookie:
var chunks []string
i := 0
for {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := request.Cookie(cookieName); c != nil {
chunks = append(chunks, c.Value)
i++
} else {
break
}
}
if len(chunks) > 0 {
encodedStr = strings.Join(chunks, "")
var headerPayload []byte
if uplinkDataPlacement == option.PlacementAuto || uplinkDataPlacement == option.PlacementHeader {
var headerPayloadChunks []string
for i := 0; true; i++ {
chunk := request.Header.Get(fmt.Sprintf("%s-%d", uplinkDataKey, i))
if chunk == "" {
break
}
headerPayloadChunks = append(headerPayloadChunks, chunk)
}
if encodedStr != "" {
payload, err = base64.RawURLEncoding.DecodeString(encodedStr)
} else {
s.logger.ErrorContext(request.Context(), err, "failed to extract data from key "+uplinkDataKey+" placed in "+uplinkDataPlacement)
writer.WriteHeader(http.StatusInternalServerError)
headerPayloadEncoded := strings.Join(headerPayloadChunks, "")
headerPayload, err = base64.RawURLEncoding.DecodeString(headerPayloadEncoded)
if err != nil {
s.logger.InfoContext(request.Context(), err, "Invalid base64 in header's payload")
writer.WriteHeader(http.StatusBadRequest)
return
}
} else {
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
}
var cookiePayload []byte
if uplinkDataPlacement == option.PlacementAuto || uplinkDataPlacement == option.PlacementCookie {
var cookiePayloadChunks []string
for i := 0; true; i++ {
cookieName := fmt.Sprintf("%s_%d", uplinkDataKey, i)
if c, _ := request.Cookie(cookieName); c != nil {
cookiePayloadChunks = append(cookiePayloadChunks, c.Value)
} else {
break
}
}
cookiePayloadEncoded := strings.Join(cookiePayloadChunks, "")
cookiePayload, err = base64.RawURLEncoding.DecodeString(cookiePayloadEncoded)
if err != nil {
s.logger.InfoContext(request.Context(), err, "Invalid base64 in cookies' payload")
writer.WriteHeader(http.StatusBadRequest)
return
}
}
var bodyPayload []byte
if uplinkDataPlacement == option.PlacementAuto || uplinkDataPlacement == option.PlacementBody {
var readErr error
if request.ContentLength > int64(scMaxEachPostBytes) {
s.logger.ErrorContext(request.Context(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.")
writer.WriteHeader(http.StatusRequestEntityTooLarge)
return
}
if request.ContentLength > 0 {
bodyPayload = make([]byte, request.ContentLength)
_, readErr = io.ReadFull(request.Body, bodyPayload)
} else {
bodyPayload, readErr = buf.ReadAllToBytes(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
}
if readErr != nil {
s.logger.InfoContext(request.Context(), readErr, "failed to read body payload")
writer.WriteHeader(http.StatusBadRequest)
return
}
}
var payload []byte
switch uplinkDataPlacement {
case option.PlacementHeader:
payload = headerPayload
case option.PlacementCookie:
payload = cookiePayload
case option.PlacementBody:
payload = bodyPayload
case option.PlacementAuto:
payload = slices.Concat(headerPayload, cookiePayload, bodyPayload)
}
if len(payload) > scMaxEachPostBytes {
s.logger.ErrorContext(request.Context(), "Too large upload. scMaxEachPostBytes is set to ", scMaxEachPostBytes, "but request size exceed it. Adjust scMaxEachPostBytes on the server to be at least as large as client.")
writer.WriteHeader(http.StatusRequestEntityTooLarge)
return
}
if err != nil {
s.logger.InfoContext(request.Context(), err, "failed to upload (ReadAll)")
writer.WriteHeader(http.StatusInternalServerError)
return
}
seq, err := strconv.ParseUint(seqStr, 10, 64)
if err != nil {
s.logger.InfoContext(request.Context(), err, "failed to upload (ParseUint)")
@@ -295,6 +317,10 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.WriteHeader(http.StatusInternalServerError)
return
}
if len(bodyPayload) == 0 {
// Methods without a body are usually cached by default.
writer.Header().Set("Cache-Control", "no-store")
}
writer.WriteHeader(http.StatusOK)
} else if request.Method == "GET" || sessionId == "" { // stream-down, stream-one
if sessionId != "" {
@@ -422,17 +448,17 @@ func ExtractMetaFromRequest(options *option.V2RayXHTTPOptions, req *http.Request
seqPlacement := options.GetNormalizedSeqPlacement()
sessionKey := options.GetNormalizedSessionKey()
seqKey := options.GetNormalizedSeqKey()
if sessionPlacement == option.PlacementPath && seqPlacement == option.PlacementPath {
subpath := strings.Split(req.URL.Path[len(path):], "/")
if len(subpath) > 0 {
sessionId = subpath[0]
}
if len(subpath) > 1 {
seqStr = subpath[1]
}
return sessionId, seqStr
var subpath []string
pathPart := 0
if sessionPlacement == option.PlacementPath || seqPlacement == option.PlacementPath {
subpath = strings.Split(req.URL.Path[len(path):], "/")
}
switch sessionPlacement {
case option.PlacementPath:
if len(subpath) > pathPart {
sessionId = subpath[pathPart]
pathPart += 1
}
case option.PlacementQuery:
sessionId = req.URL.Query().Get(sessionKey)
case option.PlacementHeader:
@@ -443,6 +469,11 @@ func ExtractMetaFromRequest(options *option.V2RayXHTTPOptions, req *http.Request
}
}
switch seqPlacement {
case option.PlacementPath:
if len(subpath) > pathPart {
seqStr = subpath[pathPart]
pathPart += 1
}
case option.PlacementQuery:
seqStr = req.URL.Query().Get(seqKey)
case option.PlacementHeader:

View File

@@ -0,0 +1,150 @@
package xhttp
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"github.com/sagernet/sing-box/common/xray/buf"
"github.com/sagernet/sing-box/common/xray/utils"
"github.com/sagernet/sing-box/option"
)
func FillStreamRequest(request *http.Request, sessionId string, seqStr string, options *option.V2RayXHTTPBaseOptions) {
request.Header = options.GetRequestHeader()
length := int(options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: options.XPaddingPlacement,
Key: options.XPaddingKey,
Header: options.XPaddingHeader,
RawURL: request.URL.String(),
}
config.Method = PaddingMethod(options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: request.URL.String(),
}
}
ApplyXPaddingToRequest(request, config)
ApplyMetaToRequest(options, request, sessionId, "")
if request.Body != nil && !options.NoGRPCHeader { // stream-up/one
request.Header.Set("Content-Type", "application/grpc")
}
}
func FillPacketRequest(request *http.Request, sessionId string, seqStr string, payload buf.MultiBuffer, options *option.V2RayXHTTPBaseOptions) error {
dataPlacement := options.GetNormalizedUplinkDataPlacement()
if dataPlacement == option.PlacementBody || dataPlacement == option.PlacementAuto {
request.Header = options.GetRequestHeader()
request.Body = io.NopCloser(&buf.MultiBufferContainer{MultiBuffer: payload})
request.ContentLength = int64(payload.Len())
} else {
data := make([]byte, payload.Len())
payload.Copy(data)
buf.ReleaseMulti(payload)
switch dataPlacement {
case option.PlacementHeader:
request.Header = GetRequestHeaderWithPayload(data, options)
case option.PlacementCookie:
request.Header = options.GetRequestHeader()
for _, cookie := range GetRequestCookiesWithPayload(data, options) {
request.AddCookie(cookie)
}
}
}
length := int(options.GetNormalizedXPaddingBytes().Rand())
config := XPaddingConfig{Length: length}
if options.XPaddingObfsMode {
config.Placement = XPaddingPlacement{
Placement: options.XPaddingPlacement,
Key: options.XPaddingKey,
Header: options.XPaddingHeader,
RawURL: request.URL.String(),
}
config.Method = PaddingMethod(options.XPaddingMethod)
} else {
config.Placement = XPaddingPlacement{
Placement: option.PlacementQueryInHeader,
Key: "x_padding",
Header: "Referer",
RawURL: request.URL.String(),
}
}
ApplyXPaddingToRequest(request, config)
ApplyMetaToRequest(options, request, sessionId, seqStr)
return nil
}
func WriteResponseHeader(writer http.ResponseWriter, requestMethod string, requestHeader http.Header, options *option.V2RayXHTTPOptions) {
// CORS headers for the browser dialer
if origin := requestHeader.Get("Origin"); origin == "" {
writer.Header().Set("Access-Control-Allow-Origin", "*")
} else {
// Chrome says: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.
writer.Header().Set("Access-Control-Allow-Origin", origin)
}
if options.GetNormalizedSessionPlacement() == option.PlacementCookie ||
options.GetNormalizedSeqPlacement() == option.PlacementCookie ||
options.XPaddingPlacement == option.PlacementCookie ||
options.GetNormalizedUplinkDataPlacement() == option.PlacementCookie {
writer.Header().Set("Access-Control-Allow-Credentials", "true")
}
if requestMethod == "OPTIONS" {
requestedMethod := requestHeader.Get("Access-Control-Request-Method")
if requestedMethod != "" {
writer.Header().Set("Access-Control-Allow-Methods", requestedMethod)
} else {
writer.Header().Set("Access-Control-Allow-Methods", "*")
}
requestedHeaders := requestHeader.Get("Access-Control-Request-Headers")
if requestedHeaders == "" {
writer.Header().Set("Access-Control-Allow-Headers", "*")
} else {
writer.Header().Set("Access-Control-Allow-Headers", requestedHeaders)
}
}
}
func GetRequestHeader(options *option.V2RayXHTTPBaseOptions) http.Header {
header := http.Header{}
for k, v := range options.Headers {
header.Add(k, v)
}
utils.TryDefaultHeadersWith(header, "fetch")
return header
}
func GetRequestHeaderWithPayload(payload []byte, options *option.V2RayXHTTPBaseOptions) http.Header {
header := GetRequestHeader(options)
key := options.UplinkDataKey
encodedData := base64.RawURLEncoding.EncodeToString(payload)
for i := 0; len(encodedData) > 0; i++ {
chunkSize := min(int(options.GetNormalizedUplinkChunkSize().Rand()), len(encodedData))
chunk := encodedData[:chunkSize]
encodedData = encodedData[chunkSize:]
headerKey := fmt.Sprintf("%s-%d", key, i)
header.Set(headerKey, chunk)
}
return header
}
func GetRequestCookiesWithPayload(payload []byte, options *option.V2RayXHTTPBaseOptions) []*http.Cookie {
cookies := []*http.Cookie{}
key := options.UplinkDataKey
encodedData := base64.RawURLEncoding.EncodeToString(payload)
for i := 0; len(encodedData) > 0; i++ {
chunkSize := min(int(options.GetNormalizedUplinkChunkSize().Rand()), len(encodedData))
chunk := encodedData[:chunkSize]
encodedData = encodedData[chunkSize:]
cookieName := fmt.Sprintf("%s_%d", key, i)
cookies = append(cookies, &http.Cookie{Name: cookieName, Value: chunk})
}
return cookies
}

View File

@@ -145,6 +145,17 @@ func ApplyPaddingToCookie(req *http.Request, name, value string) {
})
}
func ApplyPaddingToResponseCookie(writer http.ResponseWriter, name, value string) {
if name == "" || value == "" {
return
}
http.SetCookie(writer, &http.Cookie{
Name: name,
Value: value,
Path: "/",
})
}
func ApplyPaddingToQuery(u *url.URL, key, value string) {
if u == nil || key == "" || value == "" {
return
@@ -193,6 +204,19 @@ func ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) {
}
}
func ApplyXPaddingToResponse(writer http.ResponseWriter, config XPaddingConfig) {
placement := config.Placement.Placement
if placement == option.PlacementHeader || placement == option.PlacementQueryInHeader {
ApplyXPaddingToHeader(writer.Header(), config)
return
}
paddingValue := GeneratePadding(config.Method, config.Length)
switch placement {
case option.PlacementCookie:
ApplyPaddingToResponseCookie(writer, config.Placement.Key, paddingValue)
}
}
func ExtractXPaddingFromRequest(options *option.V2RayXHTTPBaseOptions, req *http.Request, obfsMode bool) (string, string) {
if req == nil {
return "", ""

View File

@@ -245,16 +245,16 @@ func (e remoteEndpoint) SrcToString() string {
}
func (e remoteEndpoint) DstToString() string {
return (netip.AddrPort)(e).String()
return netip.AddrPort(e).String()
}
func (e remoteEndpoint) DstToBytes() []byte {
b, _ := (netip.AddrPort)(e).MarshalBinary()
b, _ := netip.AddrPort(e).MarshalBinary()
return b
}
func (e remoteEndpoint) DstIP() netip.Addr {
return (netip.AddrPort)(e).Addr()
return netip.AddrPort(e).Addr()
}
func (e remoteEndpoint) SrcIP() netip.Addr {