mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
150
transport/v2rayxhttp/utils.go
Normal file
150
transport/v2rayxhttp/utils.go
Normal 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
|
||||
}
|
||||
@@ -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 "", ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user