mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-08-03 12:25:17 +03:00
Compare commits
63 Commits
v1.12.12-e
...
v1.12.22-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d16078651 | ||
|
|
18b1101fbe | ||
|
|
4ebe870306 | ||
|
|
50c5e9df0d | ||
|
|
c8a993834e | ||
|
|
260bbbfb45 | ||
|
|
82337299b9 | ||
|
|
c229c79dcc | ||
|
|
f63091d14d | ||
|
|
1c4a01ee90 | ||
|
|
4d7f99310c | ||
|
|
6fc511f56e | ||
|
|
d18d2b352a | ||
|
|
534128bba9 | ||
|
|
736a7368c6 | ||
|
|
e7a9c90213 | ||
|
|
0f3774e501 | ||
|
|
2f8e656522 | ||
|
|
3ba30e3f00 | ||
|
|
f2639a5829 | ||
|
|
69bebbda82 | ||
|
|
00b2c042ee | ||
|
|
d9eb8f3ab6 | ||
|
|
58025a01f8 | ||
|
|
99cad72ea8 | ||
|
|
6e96d620fe | ||
|
|
596291567f | ||
|
|
a2a5f46cb6 | ||
|
|
f6da8e52b4 | ||
|
|
51ce402dbb | ||
|
|
8b404b5a4c | ||
|
|
b27d707668 | ||
|
|
3ce94d50dd | ||
|
|
29d56fca9c | ||
|
|
ab18010ee1 | ||
|
|
e69c202c79 | ||
|
|
0a812f2a46 | ||
|
|
fffe9fc566 | ||
|
|
6fdf27a701 | ||
|
|
7fa7d4f0a9 | ||
|
|
f511ebc1d4 | ||
|
|
84bbdc2eba | ||
|
|
568612fc70 | ||
|
|
d78828fd81 | ||
|
|
f56d9ab945 | ||
|
|
86fabd6a22 | ||
|
|
24a1e7cee4 | ||
|
|
223dd8bb1a | ||
|
|
68448de7d0 | ||
|
|
1ebff74c21 | ||
|
|
f0cd3422c1 | ||
|
|
e385a98ced | ||
|
|
670f32baee | ||
|
|
287fe834db | ||
|
|
d7f0cea4ff | ||
|
|
d8b470d1ba | ||
|
|
984fc295b3 | ||
|
|
2747a00ba2 | ||
|
|
48e76038d0 | ||
|
|
6421252d44 | ||
|
|
216c4c8bd4 | ||
|
|
5841d410a1 | ||
|
|
63c8207d7a |
2
.github/setup_go_for_windows7.sh
vendored
2
.github/setup_go_for_windows7.sh
vendored
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
VERSION="1.25.3"
|
VERSION="1.25.7"
|
||||||
|
|
||||||
mkdir -p $HOME/go
|
mkdir -p $HOME/go
|
||||||
cd $HOME/go
|
cd $HOME/go
|
||||||
|
|||||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.7
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -107,15 +107,15 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
|
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.7
|
||||||
- name: Setup Go 1.24
|
- name: Setup Go 1.24
|
||||||
if: matrix.legacy_go124
|
if: matrix.legacy_go124
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.24.6
|
go-version: ~1.24.10
|
||||||
- name: Cache Go for Windows 7
|
- name: Cache Go for Windows 7
|
||||||
if: matrix.legacy_win7
|
if: matrix.legacy_win7
|
||||||
id: cache-go-for-windows7
|
id: cache-go-for-windows7
|
||||||
@@ -123,7 +123,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/go/go_win7
|
~/go/go_win7
|
||||||
key: go_win7_1253
|
key: go_win7_1255
|
||||||
- name: Setup Go for Windows 7
|
- name: Setup Go for Windows 7
|
||||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -300,7 +300,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.7
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -380,7 +380,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.7
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
id: setup-ndk
|
id: setup-ndk
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
@@ -479,7 +479,7 @@ jobs:
|
|||||||
if: matrix.if
|
if: matrix.if
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.7
|
||||||
- name: Set tag
|
- name: Set tag
|
||||||
if: matrix.if
|
if: matrix.if
|
||||||
run: |-
|
run: |-
|
||||||
|
|||||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -28,11 +28,11 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ~1.24.6
|
go-version: ~1.24.10
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v8
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
version: v2.4.0
|
version: latest
|
||||||
args: --timeout=30m
|
args: --timeout=30m
|
||||||
install-mode: binary
|
install-mode: binary
|
||||||
verify: false
|
verify: false
|
||||||
|
|||||||
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.7
|
||||||
- name: Check input version
|
- name: Check input version
|
||||||
if: github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch'
|
||||||
run: |-
|
run: |-
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ^1.25.3
|
go-version: ^1.25.7
|
||||||
- name: Setup Android NDK
|
- name: Setup Android NDK
|
||||||
if: matrix.os == 'android'
|
if: matrix.os == 'android'
|
||||||
uses: nttld/setup-ndk@v1
|
uses: nttld/setup-ndk@v1
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
FROM --platform=$BUILDPLATFORM golang:1.25-alpine AS builder
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="shtorm-7"
|
||||||
COPY . /go/src/github.com/sagernet/sing-box
|
COPY . /go/src/github.com/sagernet/sing-box
|
||||||
WORKDIR /go/src/github.com/sagernet/sing-box
|
WORKDIR /go/src/github.com/sagernet/sing-box
|
||||||
ARG TARGETOS TARGETARCH
|
ARG TARGETOS TARGETARCH
|
||||||
@@ -18,10 +18,8 @@ RUN set -ex \
|
|||||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||||
./cmd/sing-box
|
./cmd/sing-box
|
||||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
LABEL maintainer="shtorm-7"
|
||||||
RUN set -ex \
|
RUN set -ex \
|
||||||
&& apk upgrade \
|
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||||
&& apk add bash tzdata ca-certificates nftables \
|
|
||||||
&& rm -rf /var/cache/apk/*
|
|
||||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||||
ENTRYPOINT ["sing-box"]
|
ENTRYPOINT ["sing-box"]
|
||||||
|
|||||||
4
Makefile
4
Makefile
@@ -38,7 +38,7 @@ fmt:
|
|||||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||||
|
|
||||||
fmt_install:
|
fmt_install:
|
||||||
go install -v mvdan.cc/gofumpt@v0.8.0
|
go install -v mvdan.cc/gofumpt@latest
|
||||||
go install -v github.com/daixiang0/gci@latest
|
go install -v github.com/daixiang0/gci@latest
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
@@ -49,7 +49,7 @@ lint:
|
|||||||
GOOS=freebsd golangci-lint run ./...
|
GOOS=freebsd golangci-lint run ./...
|
||||||
|
|
||||||
lint_install:
|
lint_install:
|
||||||
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
|
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||||
|
|
||||||
proto:
|
proto:
|
||||||
@go run ./cmd/internal/protogen
|
@go run ./cmd/internal/protogen
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Sing-box with extended features.
|
|||||||
* Mieru
|
* Mieru
|
||||||
* XHTTP
|
* XHTTP
|
||||||
* SDNS (DNSCrypt)
|
* SDNS (DNSCrypt)
|
||||||
|
* Extended Wireguard options
|
||||||
* Unified delay
|
* Unified delay
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ type DNSClient interface {
|
|||||||
Start()
|
Start()
|
||||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||||
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
|
||||||
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
|
||||||
ClearCache()
|
ClearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
_, myMetadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
myMetadata := ContextFrom(ctx)
|
_, myMetadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
myMetadata.Source = source
|
myMetadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
metadata := ContextFrom(ctx)
|
_, metadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||||
metadata := ContextFrom(ctx)
|
_, metadata := ExtendContext(ctx)
|
||||||
if source.IsValid() {
|
if source.IsValid() {
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule clients/android updated: a4e3c00f0f...eb87216961
Submodule clients/apple updated: 84d8cf1757...97402ba8b6
File diff suppressed because it is too large
Load Diff
@@ -303,8 +303,6 @@ find:
|
|||||||
metadata.Protocol = C.ProtocolQUIC
|
metadata.Protocol = C.ProtocolQUIC
|
||||||
fingerprint, err := ja3.Compute(buffer.Bytes())
|
fingerprint, err := ja3.Compute(buffer.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metadata.Protocol = C.ProtocolQUIC
|
|
||||||
metadata.Client = C.ClientChromium
|
|
||||||
metadata.SniffContext = fragments
|
metadata.SniffContext = fragments
|
||||||
return E.Cause1(ErrNeedMoreData, err)
|
return E.Cause1(ErrNeedMoreData, err)
|
||||||
}
|
}
|
||||||
@@ -334,7 +332,7 @@ find:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
||||||
if maybeUQUIC(fingerprint) {
|
if isQUICGo(fingerprint) {
|
||||||
metadata.Client = C.ClientQUICGo
|
metadata.Client = C.ClientQUICGo
|
||||||
} else {
|
} else {
|
||||||
metadata.Client = C.ClientChromium
|
metadata.Client = C.ClientChromium
|
||||||
|
|||||||
@@ -1,21 +1,29 @@
|
|||||||
package sniff
|
package sniff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/ja3"
|
"github.com/sagernet/sing-box/common/ja3"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
|
const (
|
||||||
// The cronet without this behavior does not have version 115
|
// X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
|
||||||
var uQUICChrome115 = &ja3.ClientHello{
|
x25519Kyber768Draft00 uint16 = 0x11EC // 4588
|
||||||
Version: tls.VersionTLS12,
|
// renegotiation_info extension used by Go crypto/tls
|
||||||
CipherSuites: []uint16{4865, 4866, 4867},
|
extensionRenegotiationInfo uint16 = 0xFF01 // 65281
|
||||||
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
|
)
|
||||||
EllipticCurves: []uint16{29, 23, 24},
|
|
||||||
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
|
|
||||||
}
|
|
||||||
|
|
||||||
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
|
// isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
|
||||||
return !uQUICChrome115.Equals(fingerprint, true)
|
// Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium
|
||||||
|
// since it uses the same TLS fingerprint, so it will be identified as Chromium.
|
||||||
|
func isQUICGo(fingerprint *ja3.ClientHello) bool {
|
||||||
|
for _, curve := range fingerprint.EllipticCurves {
|
||||||
|
if curve == x25519Kyber768Draft00 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, ext := range fingerprint.Extensions {
|
||||||
|
if ext == extensionRenegotiationInfo {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
188
common/sniff/quic_capture_test.go
Normal file
188
common/sniff/quic_capture_test.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package sniff_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/quic-go"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/common/sniff"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
const testSNI = "test.example.com"
|
||||||
|
|
||||||
|
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer udpConn.Close()
|
||||||
|
|
||||||
|
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||||
|
packetsChan := make(chan [][]byte, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var packets [][]byte
|
||||||
|
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, _, err := udpConn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
packets = append(packets, buf[:n])
|
||||||
|
}
|
||||||
|
packetsChan <- packets
|
||||||
|
}()
|
||||||
|
|
||||||
|
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: testSNI,
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
NextProtos: []string{"h3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||||
|
|
||||||
|
select {
|
||||||
|
case packets := <-packetsChan:
|
||||||
|
t.Logf("Captured %d packets", len(packets))
|
||||||
|
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
for i, pkt := range packets {
|
||||||
|
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
|
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
|
||||||
|
if metadata.Domain != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
|
||||||
|
t.Logf("Domain: %s", metadata.Domain)
|
||||||
|
t.Logf("Client: %s", metadata.Client)
|
||||||
|
t.Logf("Protocol: %s", metadata.Protocol)
|
||||||
|
|
||||||
|
// The client should be identified as quic-go, not chromium
|
||||||
|
// Current issue: it's being identified as chromium
|
||||||
|
if metadata.Client == "chromium" {
|
||||||
|
t.Log("WARNING: quic-go is being misidentified as chromium!")
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const testSNI = "test.example.com"
|
||||||
|
|
||||||
|
// Create UDP listener to capture ALL initial packets
|
||||||
|
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer udpConn.Close()
|
||||||
|
|
||||||
|
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||||
|
|
||||||
|
// Channel to receive captured packets
|
||||||
|
packetsChan := make(chan [][]byte, 1)
|
||||||
|
|
||||||
|
// Start goroutine to capture packets
|
||||||
|
go func() {
|
||||||
|
var packets [][]byte
|
||||||
|
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
for i := 0; i < 5; i++ { // Capture up to 5 packets
|
||||||
|
buf := make([]byte, 2048)
|
||||||
|
n, _, err := udpConn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
packets = append(packets, buf[:n])
|
||||||
|
}
|
||||||
|
packetsChan <- packets
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Create QUIC client connection (will fail but we capture the initial packet)
|
||||||
|
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: testSNI,
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
NextProtos: []string{"h3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// This will fail (no server) but sends initial packet
|
||||||
|
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||||
|
|
||||||
|
// Wait for captured packets
|
||||||
|
select {
|
||||||
|
case packets := <-packetsChan:
|
||||||
|
t.Logf("Captured %d QUIC packets", len(packets))
|
||||||
|
|
||||||
|
for i, packet := range packets {
|
||||||
|
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test sniffer with first packet
|
||||||
|
if len(packets) > 0 {
|
||||||
|
var metadata adapter.InboundContext
|
||||||
|
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
|
||||||
|
|
||||||
|
t.Logf("First packet sniff error: %v", err)
|
||||||
|
t.Logf("Protocol: %s", metadata.Protocol)
|
||||||
|
t.Logf("Domain: %s", metadata.Domain)
|
||||||
|
t.Logf("Client: %s", metadata.Client)
|
||||||
|
|
||||||
|
// If first packet needs more data, try with subsequent packets
|
||||||
|
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
|
||||||
|
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
|
||||||
|
t.Log("First packet needs more data, trying subsequent packets with shared context...")
|
||||||
|
for i := 1; i < len(packets); i++ {
|
||||||
|
// Reuse same metadata to accumulate fragments
|
||||||
|
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
|
||||||
|
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
|
||||||
|
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print hex dump for debugging
|
||||||
|
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
|
||||||
|
|
||||||
|
// Log final results
|
||||||
|
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
|
||||||
|
|
||||||
|
// Verify SNI extraction
|
||||||
|
if metadata.Domain == "" {
|
||||||
|
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
|
||||||
|
} else {
|
||||||
|
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check client identification - quic-go should be identified as quic-go, not chromium
|
||||||
|
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("Timeout waiting for QUIC packets")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
|
|||||||
var metadata adapter.InboundContext
|
var metadata adapter.InboundContext
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Empty(t, metadata.Client)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
|||||||
var metadata adapter.InboundContext
|
var metadata adapter.InboundContext
|
||||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
require.Empty(t, metadata.Client)
|
||||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const (
|
|||||||
Size = 8192
|
Size = 8192
|
||||||
)
|
)
|
||||||
|
|
||||||
var zero = [Size * 10]byte{0}
|
var ErrBufferFull = E.New("buffer is full")
|
||||||
|
|
||||||
var pool = bytespool.GetPool(Size)
|
var pool = bytespool.GetPool(Size)
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ func (b *Buffer) Bytes() []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extend increases the buffer size by n bytes, and returns the extended part.
|
// Extend increases the buffer size by n bytes, and returns the extended part.
|
||||||
// It panics if result size is larger than buf.Size.
|
// It panics if result size is larger than size of this buffer.
|
||||||
func (b *Buffer) Extend(n int32) []byte {
|
func (b *Buffer) Extend(n int32) []byte {
|
||||||
end := b.end + n
|
end := b.end + n
|
||||||
if end > int32(len(b.v)) {
|
if end > int32(len(b.v)) {
|
||||||
@@ -152,7 +152,7 @@ func (b *Buffer) Extend(n int32) []byte {
|
|||||||
}
|
}
|
||||||
ext := b.v[b.end:end]
|
ext := b.v[b.end:end]
|
||||||
b.end = end
|
b.end = end
|
||||||
copy(ext, zero[:])
|
clear(ext)
|
||||||
return ext
|
return ext
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ func (b *Buffer) Resize(from, to int32) {
|
|||||||
b.start += from
|
b.start += from
|
||||||
b.Check()
|
b.Check()
|
||||||
if b.end > oldEnd {
|
if b.end > oldEnd {
|
||||||
copy(b.v[oldEnd:b.end], zero[:])
|
clear(b.v[oldEnd:b.end])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,6 +244,14 @@ func (b *Buffer) Cap() int32 {
|
|||||||
return int32(len(b.v))
|
return int32(len(b.v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Available returns the available capacity of the buffer content.
|
||||||
|
func (b *Buffer) Available() int32 {
|
||||||
|
if b == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int32(len(b.v)) - b.end
|
||||||
|
}
|
||||||
|
|
||||||
// IsEmpty returns true if the buffer is empty.
|
// IsEmpty returns true if the buffer is empty.
|
||||||
func (b *Buffer) IsEmpty() bool {
|
func (b *Buffer) IsEmpty() bool {
|
||||||
return b.Len() == 0
|
return b.Len() == 0
|
||||||
@@ -258,13 +266,16 @@ func (b *Buffer) IsFull() bool {
|
|||||||
func (b *Buffer) Write(data []byte) (int, error) {
|
func (b *Buffer) Write(data []byte) (int, error) {
|
||||||
nBytes := copy(b.v[b.end:], data)
|
nBytes := copy(b.v[b.end:], data)
|
||||||
b.end += int32(nBytes)
|
b.end += int32(nBytes)
|
||||||
|
if nBytes < len(data) {
|
||||||
|
return nBytes, ErrBufferFull
|
||||||
|
}
|
||||||
return nBytes, nil
|
return nBytes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteByte writes a single byte into the buffer.
|
// WriteByte writes a single byte into the buffer.
|
||||||
func (b *Buffer) WriteByte(v byte) error {
|
func (b *Buffer) WriteByte(v byte) error {
|
||||||
if b.IsFull() {
|
if b.IsFull() {
|
||||||
return E.New("buffer full")
|
return ErrBufferFull
|
||||||
}
|
}
|
||||||
b.v[b.end] = v
|
b.v[b.end] = v
|
||||||
b.end++
|
b.end++
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ var ErrReadTimeout = E.New("IO timeout")
|
|||||||
|
|
||||||
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
|
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
|
||||||
type TimeoutReader interface {
|
type TimeoutReader interface {
|
||||||
|
Reader
|
||||||
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
|
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func Compact(mb MultiBuffer) MultiBuffer {
|
|||||||
|
|
||||||
for i := 1; i < len(mb); i++ {
|
for i := 1; i < len(mb); i++ {
|
||||||
curr := mb[i]
|
curr := mb[i]
|
||||||
if last.Len()+curr.Len() > Size {
|
if curr.Len() > last.Available() {
|
||||||
mb2 = append(mb2, last)
|
mb2 = append(mb2, last)
|
||||||
last = curr
|
last = curr
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) {
|
|||||||
// BufferedWriter is a Writer with internal buffer.
|
// BufferedWriter is a Writer with internal buffer.
|
||||||
type BufferedWriter struct {
|
type BufferedWriter struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
writer Writer
|
writer Writer
|
||||||
buffer *Buffer
|
buffer *Buffer
|
||||||
buffered bool
|
buffered bool
|
||||||
|
flushNext bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBufferedWriter creates a new BufferedWriter.
|
// NewBufferedWriter creates a new BufferedWriter.
|
||||||
@@ -161,6 +162,12 @@ func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if w.flushNext {
|
||||||
|
w.buffered = false
|
||||||
|
w.flushNext = false
|
||||||
|
return w.flushInternal()
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +208,13 @@ func (w *BufferedWriter) SetBuffered(f bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetFlushNext will wait the next WriteMultiBuffer to flush and set buffered = false
|
||||||
|
func (w *BufferedWriter) SetFlushNext() {
|
||||||
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
w.flushNext = true
|
||||||
|
}
|
||||||
|
|
||||||
// ReadFrom implements io.ReaderFrom.
|
// ReadFrom implements io.ReaderFrom.
|
||||||
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
|
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
|
||||||
if err := w.SetBuffered(false); err != nil {
|
if err := w.SetBuffered(false); err != nil {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
// Must panics if err is not nil.
|
// Must panics if err is not nil.
|
||||||
func Must(err error) {
|
func Must(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -17,3 +19,14 @@ func Must2(v interface{}, err error) interface{} {
|
|||||||
func Error2(v interface{}, err error) error {
|
func Error2(v interface{}, err error) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CloseIfExists call obj.Close() if obj is not nil.
|
||||||
|
func CloseIfExists(obj any) error {
|
||||||
|
if obj != nil {
|
||||||
|
v := reflect.ValueOf(obj)
|
||||||
|
if !v.IsNil() {
|
||||||
|
return Close(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ func RandBetween(from int64, to int64) int64 {
|
|||||||
if from == to {
|
if from == to {
|
||||||
return from
|
return from
|
||||||
}
|
}
|
||||||
|
if from > to {
|
||||||
|
from, to = to, from
|
||||||
|
}
|
||||||
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
||||||
return from + bigInt.Int64()
|
return from + bigInt.Int64()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ func (c *Range) Build() *Range {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Range) MarshalJSON() ([]byte, error) {
|
func (c *Range) MarshalJSON() ([]byte, error) {
|
||||||
|
if c.From == c.To {
|
||||||
|
return json.Marshal(c.From)
|
||||||
|
}
|
||||||
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
|
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +53,15 @@ func (c *Range) UnmarshalJSON(content []byte) error {
|
|||||||
rangeValue.From, rangeValue.To = int32(from), int32(to)
|
rangeValue.From, rangeValue.To = int32(from), int32(to)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
err := json.Unmarshal(content, &rangeValue)
|
var int32Value int32
|
||||||
if err != nil {
|
err := json.Unmarshal(content, &int32Value)
|
||||||
return err
|
if err == nil {
|
||||||
|
rangeValue.From, rangeValue.To = int32Value, int32Value
|
||||||
|
} else {
|
||||||
|
err := json.Unmarshal(content, &rangeValue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rangeValue.From > rangeValue.To {
|
if rangeValue.From > rangeValue.To {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package pipe
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"runtime"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -136,11 +135,10 @@ func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
|
|||||||
|
|
||||||
if p.data == nil {
|
if p.data == nil {
|
||||||
p.data = mb
|
p.data = mb
|
||||||
return nil
|
} else {
|
||||||
|
p.data, _ = buf.MergeMulti(p.data, mb)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
p.data, _ = buf.MergeMulti(p.data, mb)
|
|
||||||
return errSlowDown
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||||
@@ -155,30 +153,23 @@ func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == errSlowDown {
|
if err == errBufferFull {
|
||||||
p.readSignal.Signal()
|
if p.option.discardOverflow {
|
||||||
|
buf.ReleaseMulti(mb)
|
||||||
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload.
|
return nil
|
||||||
runtime.Gosched()
|
}
|
||||||
return nil
|
select {
|
||||||
|
case <-p.writeSignal.Wait():
|
||||||
|
continue
|
||||||
|
case <-p.done.Wait():
|
||||||
|
buf.ReleaseMulti(mb)
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == errBufferFull && p.option.discardOverflow {
|
buf.ReleaseMulti(mb)
|
||||||
buf.ReleaseMulti(mb)
|
p.readSignal.Signal()
|
||||||
return nil
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
if err != errBufferFull {
|
|
||||||
buf.ReleaseMulti(mb)
|
|
||||||
p.readSignal.Signal()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-p.writeSignal.Wait():
|
|
||||||
case <-p.done.Wait():
|
|
||||||
return io.ErrClosedPipe
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,16 +191,19 @@ func (p *pipe) Interrupt() {
|
|||||||
p.Lock()
|
p.Lock()
|
||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if !p.data.IsEmpty() {
|
||||||
|
buf.ReleaseMulti(p.data)
|
||||||
|
p.data = nil
|
||||||
|
if p.state == closed {
|
||||||
|
p.state = errord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if p.state == closed || p.state == errord {
|
if p.state == closed || p.state == errord {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
p.state = errord
|
p.state = errord
|
||||||
|
|
||||||
if !p.data.IsEmpty() {
|
|
||||||
buf.ReleaseMulti(p.data)
|
|
||||||
p.data = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
common.Must(p.done.Close())
|
common.Must(p.done.Close())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package signal
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
@@ -14,10 +15,12 @@ type ActivityUpdater interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ActivityTimer struct {
|
type ActivityTimer struct {
|
||||||
sync.RWMutex
|
mu sync.RWMutex
|
||||||
updated chan struct{}
|
updated chan struct{}
|
||||||
checkTask *task.Periodic
|
checkTask *task.Periodic
|
||||||
onTimeout func()
|
onTimeout func()
|
||||||
|
consumed atomic.Bool
|
||||||
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ActivityTimer) Update() {
|
func (t *ActivityTimer) Update() {
|
||||||
@@ -37,39 +40,39 @@ func (t *ActivityTimer) check() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *ActivityTimer) finish() {
|
func (t *ActivityTimer) finish() {
|
||||||
t.Lock()
|
t.once.Do(func() {
|
||||||
defer t.Unlock()
|
t.consumed.Store(true)
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
if t.onTimeout != nil {
|
common.CloseIfExists(t.checkTask)
|
||||||
t.onTimeout()
|
t.onTimeout()
|
||||||
t.onTimeout = nil
|
})
|
||||||
}
|
|
||||||
if t.checkTask != nil {
|
|
||||||
t.checkTask.Close()
|
|
||||||
t.checkTask = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
|
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
|
||||||
|
if t.consumed.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
t.finish()
|
t.finish()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
checkTask := &task.Periodic{
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
// double check, just in case
|
||||||
|
if t.consumed.Load() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newCheckTask := &task.Periodic{
|
||||||
Interval: timeout,
|
Interval: timeout,
|
||||||
Execute: t.check,
|
Execute: t.check,
|
||||||
}
|
}
|
||||||
|
common.CloseIfExists(t.checkTask)
|
||||||
t.Lock()
|
t.checkTask = newCheckTask
|
||||||
|
|
||||||
if t.checkTask != nil {
|
|
||||||
t.checkTask.Close()
|
|
||||||
}
|
|
||||||
t.checkTask = checkTask
|
|
||||||
t.Unlock()
|
|
||||||
t.Update()
|
t.Update()
|
||||||
common.Must(checkTask.Start())
|
common.Must(newCheckTask.Start())
|
||||||
}
|
}
|
||||||
|
|
||||||
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
|
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
|
||||||
|
|||||||
28
common/xray/utils/browser.go
Normal file
28
common/xray/utils/browser.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/klauspost/cpuid/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ChromeVersion() int {
|
||||||
|
// Use only CPU info as seed for PRNG
|
||||||
|
seed := int64(cpuid.CPU.Family + cpuid.CPU.Model + cpuid.CPU.PhysicalCores + cpuid.CPU.LogicalCores + cpuid.CPU.CacheLine)
|
||||||
|
rng := rand.New(rand.NewSource(seed))
|
||||||
|
// Start from Chrome 144 released on 2026.1.13
|
||||||
|
releaseDate := time.Date(2026, 1, 13, 0, 0, 0, 0, time.UTC)
|
||||||
|
version := 144
|
||||||
|
now := time.Now()
|
||||||
|
// Each version has random 25-45 day interval
|
||||||
|
for releaseDate.Before(now) {
|
||||||
|
releaseDate = releaseDate.AddDate(0, 0, rng.Intn(21)+25)
|
||||||
|
version++
|
||||||
|
}
|
||||||
|
return version - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChromeUA provides default browser User-Agent based on CPU-seeded PRNG.
|
||||||
|
var ChromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + strconv.Itoa(ChromeVersion()) + ".0.0.0 Safari/537.36"
|
||||||
24
common/xray/utils/padding.go
Normal file
24
common/xray/utils/padding.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// 8 ÷ (397/62)
|
||||||
|
h2packCorrectionFactor = 1.2493702770780857
|
||||||
|
base62TotalCharsNum = 62
|
||||||
|
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
)
|
||||||
|
|
||||||
|
// H2Base62Pad generates a base62 padding string for HTTP/2 header
|
||||||
|
// The total len will be slightly longer than the input to match the length after h2(h3 also) header huffman encoding
|
||||||
|
func H2Base62Pad[T int32 | int64 | int](expectedLen T) string {
|
||||||
|
actualLenFloat := float64(expectedLen) * h2packCorrectionFactor
|
||||||
|
actualLen := int(actualLenFloat)
|
||||||
|
result := make([]byte, actualLen)
|
||||||
|
for i := range actualLen {
|
||||||
|
result[i] = base62Chars[rand.N(base62TotalCharsNum)]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
@@ -85,10 +85,14 @@ func ParseString(str string) (UUID, error) {
|
|||||||
b := uuid.Bytes()
|
b := uuid.Bytes()
|
||||||
|
|
||||||
for _, byteGroup := range byteGroups {
|
for _, byteGroup := range byteGroups {
|
||||||
if text[0] == '-' {
|
if len(text) > 0 && text[0] == '-' {
|
||||||
text = text[1:]
|
text = text[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(text) < byteGroup {
|
||||||
|
return uuid, E.New("invalid UUID: ", str)
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
|
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
|
||||||
return uuid, err
|
return uuid, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
if c.cache != nil {
|
if c.cache != nil {
|
||||||
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||||
if loaded {
|
if loaded {
|
||||||
<-cond
|
select {
|
||||||
|
case <-cond:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
defer func() {
|
defer func() {
|
||||||
c.cacheLock.Delete(question)
|
c.cacheLock.Delete(question)
|
||||||
@@ -154,7 +158,11 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
} else if c.transportCache != nil {
|
} else if c.transportCache != nil {
|
||||||
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||||
if loaded {
|
if loaded {
|
||||||
<-cond
|
select {
|
||||||
|
case <-cond:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
defer func() {
|
defer func() {
|
||||||
c.transportCacheLock.Delete(question)
|
c.transportCacheLock.Delete(question)
|
||||||
@@ -353,68 +361,6 @@ func (c *Client) ClearCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool) {
|
|
||||||
if c.disableCache || c.independentCache {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
if dns.IsFqdn(domain) {
|
|
||||||
domain = domain[:len(domain)-1]
|
|
||||||
}
|
|
||||||
dnsName := dns.Fqdn(domain)
|
|
||||||
if strategy == C.DomainStrategyIPv4Only {
|
|
||||||
addresses, err := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return addresses, true
|
|
||||||
}
|
|
||||||
} else if strategy == C.DomainStrategyIPv6Only {
|
|
||||||
addresses, err := c.questionCache(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeAAAA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return addresses, true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response4, _ := c.loadResponse(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if response4 == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
response6, _ := c.loadResponse(dns.Question{
|
|
||||||
Name: dnsName,
|
|
||||||
Qtype: dns.TypeAAAA,
|
|
||||||
Qclass: dns.ClassINET,
|
|
||||||
}, nil)
|
|
||||||
if response6 == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
return sortAddresses(MessageToAddresses(response4), MessageToAddresses(response6), strategy), true
|
|
||||||
}
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
|
|
||||||
if c.disableCache || c.independentCache || len(message.Question) != 1 {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
question := message.Question[0]
|
|
||||||
response, ttl := c.loadResponse(question, nil)
|
|
||||||
if response == nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
|
||||||
response.Id = message.Id
|
|
||||||
return response, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
||||||
if strategy == C.DomainStrategyPreferIPv6 {
|
if strategy == C.DomainStrategyPreferIPv6 {
|
||||||
return append(response6, response4...)
|
return append(response6, response4...)
|
||||||
|
|||||||
176
dns/router.go
176
dns/router.go
@@ -214,97 +214,95 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
|||||||
}
|
}
|
||||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||||
var (
|
var (
|
||||||
|
response *mDNS.Msg
|
||||||
transport adapter.DNSTransport
|
transport adapter.DNSTransport
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
response, cached := r.client.ExchangeCache(ctx, message)
|
var metadata *adapter.InboundContext
|
||||||
if !cached {
|
ctx, metadata = adapter.ExtendContext(ctx)
|
||||||
var metadata *adapter.InboundContext
|
metadata.Destination = M.Socksaddr{}
|
||||||
ctx, metadata = adapter.ExtendContext(ctx)
|
metadata.QueryType = message.Question[0].Qtype
|
||||||
metadata.Destination = M.Socksaddr{}
|
switch metadata.QueryType {
|
||||||
metadata.QueryType = message.Question[0].Qtype
|
case mDNS.TypeA:
|
||||||
switch metadata.QueryType {
|
metadata.IPVersion = 4
|
||||||
case mDNS.TypeA:
|
case mDNS.TypeAAAA:
|
||||||
metadata.IPVersion = 4
|
metadata.IPVersion = 6
|
||||||
case mDNS.TypeAAAA:
|
}
|
||||||
metadata.IPVersion = 6
|
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
||||||
}
|
if options.Transport != nil {
|
||||||
metadata.Domain = FqdnToDomain(message.Question[0].Name)
|
transport = options.Transport
|
||||||
if options.Transport != nil {
|
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
||||||
transport = options.Transport
|
|
||||||
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
|
||||||
options.Strategy = legacyTransport.LegacyStrategy()
|
|
||||||
}
|
|
||||||
if !options.ClientSubnet.IsValid() {
|
|
||||||
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.Strategy == C.DomainStrategyAsIS {
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
options.Strategy = r.defaultDomainStrategy
|
options.Strategy = legacyTransport.LegacyStrategy()
|
||||||
}
|
}
|
||||||
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
if !options.ClientSubnet.IsValid() {
|
||||||
} else {
|
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
|
||||||
var (
|
|
||||||
rule adapter.DNSRule
|
|
||||||
ruleIndex int
|
|
||||||
)
|
|
||||||
ruleIndex = -1
|
|
||||||
for {
|
|
||||||
dnsCtx := adapter.OverrideContext(ctx)
|
|
||||||
dnsOptions := options
|
|
||||||
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
|
||||||
if rule != nil {
|
|
||||||
switch action := rule.Action().(type) {
|
|
||||||
case *R.RuleActionReject:
|
|
||||||
switch action.Method {
|
|
||||||
case C.RuleActionRejectMethodDefault:
|
|
||||||
return &mDNS.Msg{
|
|
||||||
MsgHdr: mDNS.MsgHdr{
|
|
||||||
Id: message.Id,
|
|
||||||
Rcode: mDNS.RcodeRefused,
|
|
||||||
Response: true,
|
|
||||||
},
|
|
||||||
Question: []mDNS.Question{message.Question[0]},
|
|
||||||
}, nil
|
|
||||||
case C.RuleActionRejectMethodDrop:
|
|
||||||
return nil, tun.ErrDrop
|
|
||||||
}
|
|
||||||
case *R.RuleActionPredefined:
|
|
||||||
return action.Response(message), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var responseCheck func(responseAddrs []netip.Addr) bool
|
|
||||||
if rule != nil && rule.WithAddressLimit() {
|
|
||||||
responseCheck = func(responseAddrs []netip.Addr) bool {
|
|
||||||
metadata.DestinationAddresses = responseAddrs
|
|
||||||
return rule.MatchAddressLimit(metadata)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
|
||||||
dnsOptions.Strategy = r.defaultDomainStrategy
|
|
||||||
}
|
|
||||||
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
|
||||||
var rejected bool
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, ErrResponseRejectedCached) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
|
||||||
} else if errors.Is(err, ErrResponseRejected) {
|
|
||||||
rejected = true
|
|
||||||
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
} else if len(message.Question) > 0 {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
|
||||||
} else {
|
|
||||||
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if responseCheck != nil && rejected {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if options.Strategy == C.DomainStrategyAsIS {
|
||||||
|
options.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
response, err = r.client.Exchange(ctx, transport, message, options, nil)
|
||||||
|
} else {
|
||||||
|
var (
|
||||||
|
rule adapter.DNSRule
|
||||||
|
ruleIndex int
|
||||||
|
)
|
||||||
|
ruleIndex = -1
|
||||||
|
for {
|
||||||
|
dnsCtx := adapter.OverrideContext(ctx)
|
||||||
|
dnsOptions := options
|
||||||
|
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
|
||||||
|
if rule != nil {
|
||||||
|
switch action := rule.Action().(type) {
|
||||||
|
case *R.RuleActionReject:
|
||||||
|
switch action.Method {
|
||||||
|
case C.RuleActionRejectMethodDefault:
|
||||||
|
return &mDNS.Msg{
|
||||||
|
MsgHdr: mDNS.MsgHdr{
|
||||||
|
Id: message.Id,
|
||||||
|
Rcode: mDNS.RcodeRefused,
|
||||||
|
Response: true,
|
||||||
|
},
|
||||||
|
Question: []mDNS.Question{message.Question[0]},
|
||||||
|
}, nil
|
||||||
|
case C.RuleActionRejectMethodDrop:
|
||||||
|
return nil, tun.ErrDrop
|
||||||
|
}
|
||||||
|
case *R.RuleActionPredefined:
|
||||||
|
return action.Response(message), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var responseCheck func(responseAddrs []netip.Addr) bool
|
||||||
|
if rule != nil && rule.WithAddressLimit() {
|
||||||
|
responseCheck = func(responseAddrs []netip.Addr) bool {
|
||||||
|
metadata.DestinationAddresses = responseAddrs
|
||||||
|
return rule.MatchAddressLimit(metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if dnsOptions.Strategy == C.DomainStrategyAsIS {
|
||||||
|
dnsOptions.Strategy = r.defaultDomainStrategy
|
||||||
|
}
|
||||||
|
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
|
||||||
|
var rejected bool
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrResponseRejectedCached) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
|
||||||
|
} else if errors.Is(err, ErrResponseRejected) {
|
||||||
|
rejected = true
|
||||||
|
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else if len(message.Question) > 0 {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
|
||||||
|
} else {
|
||||||
|
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if responseCheck != nil && rejected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -327,7 +325,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
|||||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||||
var (
|
var (
|
||||||
responseAddrs []netip.Addr
|
responseAddrs []netip.Addr
|
||||||
cached bool
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
printResult := func() {
|
printResult := func() {
|
||||||
@@ -347,13 +344,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
err = E.Cause(err, "lookup ", domain)
|
err = E.Cause(err, "lookup ", domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
|
|
||||||
if cached {
|
|
||||||
if len(responseAddrs) == 0 {
|
|
||||||
return nil, E.New("lookup ", domain, ": empty result (cached)")
|
|
||||||
}
|
|
||||||
return responseAddrs, nil
|
|
||||||
}
|
|
||||||
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
r.logger.DebugContext(ctx, "lookup domain ", domain)
|
||||||
ctx, metadata := adapter.ExtendContext(ctx)
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
metadata.Destination = M.Socksaddr{}
|
metadata.Destination = M.Socksaddr{}
|
||||||
@@ -388,9 +378,11 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
|
|||||||
case *R.RuleActionReject:
|
case *R.RuleActionReject:
|
||||||
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
return nil, &R.RejectedError{Cause: action.Error(ctx)}
|
||||||
case *R.RuleActionPredefined:
|
case *R.RuleActionPredefined:
|
||||||
|
responseAddrs = nil
|
||||||
if action.Rcode != mDNS.RcodeSuccess {
|
if action.Rcode != mDNS.RcodeSuccess {
|
||||||
err = RcodeError(action.Rcode)
|
err = RcodeError(action.Rcode)
|
||||||
} else {
|
} else {
|
||||||
|
err = nil
|
||||||
for _, answer := range action.Answer {
|
for _, answer := range action.Answer {
|
||||||
switch record := answer.(type) {
|
switch record := answer.(type) {
|
||||||
case *mDNS.A:
|
case *mDNS.A:
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
|||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
buffer.Reset()
|
||||||
_, _, err := buffer.ReadPacketFrom(packetConn)
|
_, _, err := buffer.ReadPacketFrom(packetConn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, io.ErrShortBuffer) {
|
if errors.Is(err, io.ErrShortBuffer) {
|
||||||
|
|||||||
@@ -2,6 +2,70 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 1.12.22
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.21
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.20
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.19
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.18
|
||||||
|
|
||||||
|
* Add fallback routing rule for `auto_redirect` **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Adds a fallback iproute2 rule checked after system default rules (32766: main, 32767: default),
|
||||||
|
ensuring traffic is routed to the sing-box table when no route is found in system tables.
|
||||||
|
|
||||||
|
The rule index can be customized via `auto_redirect_iproute2_fallback_rule_index` (default: 32768).
|
||||||
|
|
||||||
|
#### 1.12.17
|
||||||
|
|
||||||
|
* Update uTLS to v1.8.2 **1**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
This update fixes missing padding extension for Chrome 120+ fingerprints.
|
||||||
|
|
||||||
|
Also, documentation has been updated with a warning about uTLS fingerprinting vulnerabilities.
|
||||||
|
uTLS is not recommended for censorship circumvention due to fundamental architectural limitations;
|
||||||
|
use NaiveProxy instead for TLS fingerprint resistance.
|
||||||
|
|
||||||
|
#### 1.12.16
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.15
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.14
|
||||||
|
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
#### 1.12.13
|
||||||
|
|
||||||
|
* Fix naive inbound
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
|
||||||
|
because system extensions require signatures to function, we have had to temporarily halt its release.__
|
||||||
|
|
||||||
|
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
|
||||||
|
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
|
||||||
|
|
||||||
#### 1.12.12
|
#### 1.12.12
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
|
|
||||||
!!! failure ""
|
!!! failure ""
|
||||||
|
|
||||||
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected).
|
Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected)
|
||||||
|
|
||||||
## :material-graph: Requirements
|
## :material-graph: Requirements
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
|
|||||||
|
|
||||||
## :material-download: Download
|
## :material-download: Download
|
||||||
|
|
||||||
* [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)
|
* ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~
|
||||||
* TestFlight (Beta)
|
* TestFlight (Beta)
|
||||||
|
|
||||||
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
|
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
|
||||||
@@ -26,15 +26,15 @@ TestFlight quota is only available to [sponsors](https://github.com/sponsors/nek
|
|||||||
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
|
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
|
||||||
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
|
or sending us your Apple ID [via email](mailto:contact@sagernet.org).
|
||||||
|
|
||||||
## :material-file-download: Download (macOS standalone version)
|
## ~~:material-file-download: Download (macOS standalone version)~~
|
||||||
|
|
||||||
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm)
|
* ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
brew install sfm
|
# brew install sfm
|
||||||
```
|
```
|
||||||
|
|
||||||
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases)
|
* ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~
|
||||||
|
|
||||||
## :material-source-repository: Source code
|
## :material-source-repository: Source code
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
"method": "2022-blake3-aes-128-gcm",
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||||
|
"managed": false,
|
||||||
"multiplex": {}
|
"multiplex": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -86,6 +87,10 @@ Both if empty.
|
|||||||
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
||||||
| other methods | any string |
|
| other methods | any string |
|
||||||
|
|
||||||
|
#### managed
|
||||||
|
|
||||||
|
Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user.
|
||||||
|
|
||||||
#### multiplex
|
#### multiplex
|
||||||
|
|
||||||
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
"method": "2022-blake3-aes-128-gcm",
|
"method": "2022-blake3-aes-128-gcm",
|
||||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||||
|
"managed": false,
|
||||||
"multiplex": {}
|
"multiplex": {}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -86,6 +87,10 @@ See [Listen Fields](/configuration/shared/listen/) for details.
|
|||||||
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
||||||
| other methods | 任意字符串 |
|
| other methods | 任意字符串 |
|
||||||
|
|
||||||
|
#### managed
|
||||||
|
|
||||||
|
默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。
|
||||||
|
|
||||||
#### multiplex
|
#### multiplex
|
||||||
|
|
||||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.12.18"
|
||||||
|
|
||||||
|
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [loopback_address](#loopback_address)
|
:material-plus: [loopback_address](#loopback_address)
|
||||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
|||||||
"auto_redirect": true,
|
"auto_redirect": true,
|
||||||
"auto_redirect_input_mark": "0x2023",
|
"auto_redirect_input_mark": "0x2023",
|
||||||
"auto_redirect_output_mark": "0x2024",
|
"auto_redirect_output_mark": "0x2024",
|
||||||
|
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||||
"loopback_address": [
|
"loopback_address": [
|
||||||
"10.7.0.1"
|
"10.7.0.1"
|
||||||
],
|
],
|
||||||
@@ -278,6 +283,17 @@ Connection output mark used by `auto_redirect`.
|
|||||||
|
|
||||||
`0x2024` is used by default.
|
`0x2024` is used by default.
|
||||||
|
|
||||||
|
#### auto_redirect_iproute2_fallback_rule_index
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.12.18"
|
||||||
|
|
||||||
|
Linux iproute2 fallback rule index generated by `auto_redirect`.
|
||||||
|
|
||||||
|
This rule is checked after system default rules (32766: main, 32767: default),
|
||||||
|
routing traffic to the sing-box table only when no route is found in system tables.
|
||||||
|
|
||||||
|
`32768` is used by default.
|
||||||
|
|
||||||
#### loopback_address
|
#### loopback_address
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "sing-box 1.12.18 中的更改"
|
||||||
|
|
||||||
|
:material-plus: [auto_redirect_iproute2_fallback_rule_index](#auto_redirect_iproute2_fallback_rule_index)
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [loopback_address](#loopback_address)
|
:material-plus: [loopback_address](#loopback_address)
|
||||||
@@ -63,6 +67,7 @@ icon: material/new-box
|
|||||||
"auto_redirect": true,
|
"auto_redirect": true,
|
||||||
"auto_redirect_input_mark": "0x2023",
|
"auto_redirect_input_mark": "0x2023",
|
||||||
"auto_redirect_output_mark": "0x2024",
|
"auto_redirect_output_mark": "0x2024",
|
||||||
|
"auto_redirect_iproute2_fallback_rule_index": 32768,
|
||||||
"loopback_address": [
|
"loopback_address": [
|
||||||
"10.7.0.1"
|
"10.7.0.1"
|
||||||
],
|
],
|
||||||
@@ -277,6 +282,17 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
默认使用 `0x2024`。
|
默认使用 `0x2024`。
|
||||||
|
|
||||||
|
#### auto_redirect_iproute2_fallback_rule_index
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.12.18 起"
|
||||||
|
|
||||||
|
`auto_redirect` 生成的 iproute2 回退规则索引。
|
||||||
|
|
||||||
|
此规则在系统默认规则(32766: main,32767: default)之后检查,
|
||||||
|
仅当系统路由表中未找到路由时才将流量路由到 sing-box 路由表。
|
||||||
|
|
||||||
|
默认使用 `32768`。
|
||||||
|
|
||||||
#### loopback_address
|
#### loopback_address
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|||||||
@@ -230,9 +230,18 @@ The path to the server private key, in PEM format.
|
|||||||
|
|
||||||
==Client only==
|
==Client only==
|
||||||
|
|
||||||
!!! failure ""
|
!!! failure "Not Recommended"
|
||||||
|
|
||||||
There is no evidence that GFW detects and blocks servers based on TLS client fingerprinting, and using an imperfect emulation that has not been security reviewed could pose security risks.
|
uTLS has had repeated fingerprinting vulnerabilities discovered by researchers.
|
||||||
|
|
||||||
|
uTLS is a Go library that attempts to imitate browser TLS fingerprints by copying
|
||||||
|
ClientHello structure. However, browsers use completely different TLS stacks
|
||||||
|
(Chrome uses BoringSSL, Firefox uses NSS) with distinct implementation behaviors
|
||||||
|
that cannot be replicated by simply copying the handshake format, making detection possible.
|
||||||
|
Additionally, the library lacks active maintenance and has poor code quality,
|
||||||
|
making it unsuitable for censorship circumvention.
|
||||||
|
|
||||||
|
For TLS fingerprint resistance, use [NaiveProxy](/configuration/inbound/naive/) instead.
|
||||||
|
|
||||||
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.
|
uTLS is a fork of "crypto/tls", which provides ClientHello fingerprinting resistance.
|
||||||
|
|
||||||
|
|||||||
@@ -220,9 +220,16 @@ TLS 版本值:
|
|||||||
|
|
||||||
==仅客户端==
|
==仅客户端==
|
||||||
|
|
||||||
!!! failure ""
|
!!! failure "不推荐"
|
||||||
|
|
||||||
没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患。
|
uTLS 已被研究人员多次发现其指纹可被识别的漏洞。
|
||||||
|
|
||||||
|
uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。
|
||||||
|
然而,浏览器使用完全不同的 TLS 实现(Chrome 使用 BoringSSL,Firefox 使用 NSS),
|
||||||
|
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
|
||||||
|
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
|
||||||
|
|
||||||
|
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
|
||||||
|
|
||||||
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ icon: material/horse
|
|||||||
|
|
||||||
# Trojan
|
# Trojan
|
||||||
|
|
||||||
Torjan is the most commonly used TLS proxy made in China. It can be used in various combinations,
|
Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations.
|
||||||
but only the combination of uTLS and multiplexing is recommended.
|
|
||||||
|
|
||||||
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|
| Protocol and implementation combination | Specification | Resists passive detection | Resists active probes |
|
||||||
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
|
|-----------------------------------------|----------------------------------------------------------------------|---------------------------|-----------------------|
|
||||||
@@ -140,11 +139,7 @@ but only the combination of uTLS and multiplexing is recommended.
|
|||||||
"password": "password",
|
"password": "password",
|
||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org"
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": "firefox"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -171,11 +166,7 @@ but only the combination of uTLS and multiplexing is recommended.
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org",
|
||||||
"certificate_path": "/path/to/certificate.pem",
|
"certificate_path": "/path/to/certificate.pem"
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": "firefox"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
@@ -198,11 +189,7 @@ but only the combination of uTLS and multiplexing is recommended.
|
|||||||
"tls": {
|
"tls": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.org",
|
"server_name": "example.org",
|
||||||
"insecure": true,
|
"insecure": true
|
||||||
"utls": {
|
|
||||||
"enabled": true,
|
|
||||||
"fingerprint": "firefox"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"multiplex": {
|
"multiplex": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"detour": "direct",
|
"detour": "direct",
|
||||||
// for getting existing WARP device profile
|
// For getting existing WARP device profile, else sing-box will create new profile
|
||||||
"id": "",
|
"id": "",
|
||||||
"private_key": "",
|
"private_key": "",
|
||||||
"auth_token": ""
|
"auth_token": ""
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"experimental": {
|
"experimental": {
|
||||||
"cache_file": {
|
"cache_file": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"store_warp_config": true
|
"store_warp_config": true // For saving WARP device profiles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
52
examples/wireguard/client.json
Normal file
52
examples/wireguard/client.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "wireguard",
|
||||||
|
"tag": "wireguard-out",
|
||||||
|
"mtu": 1408,
|
||||||
|
"address": null,
|
||||||
|
"private_key": "",
|
||||||
|
"listen_port": 10000,
|
||||||
|
"peers": [
|
||||||
|
{
|
||||||
|
"address": "example.com",
|
||||||
|
"port": 10001,
|
||||||
|
"reserved": "AAAA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"udp_timeout": "5m0s",
|
||||||
|
// Extended options
|
||||||
|
"preallocated_buffers_per_pool": 256, // Set limit for preallocated buffers (can be useful for devices with low RAM)
|
||||||
|
"disable_pauses": true, // Disable pauses when android device in sleep mode
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "wireguard-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"server": "example.com",
|
"server": "example.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668",
|
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668",
|
||||||
"tls": {
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.com",
|
"server_name": "example.com",
|
||||||
"alpn": "h2" // h3 for QUIC
|
"alpn": "h2" // h3 for QUIC
|
||||||
@@ -39,34 +39,65 @@
|
|||||||
"host": "example.com",
|
"host": "example.com",
|
||||||
"path": "/xhttp",
|
"path": "/xhttp",
|
||||||
"domain_strategy": "prefer_ipv4",
|
"domain_strategy": "prefer_ipv4",
|
||||||
"xmux": {
|
"x_padding_bytes": "100-1000",
|
||||||
"max_concurrency": "0-1",
|
"no_grpc_header": false, // stream-up/one, client only
|
||||||
"max_connections": "0-1",
|
"sc_max_each_post_bytes": 1000000, // packet-up only
|
||||||
"c_max_reuse_times": "0-1",
|
"sc_min_posts_interval_ms": 30, // packet-up, client only
|
||||||
"h_max_request_times": "0-1",
|
"xmux": { // h2/h3 mainly, client only
|
||||||
"h_max_reusable_secs": "0-1",
|
"max_concurrency": "16-32",
|
||||||
"h_keep_alive_period": 60
|
"max_connections": 0,
|
||||||
|
"c_max_reuse_times": 0,
|
||||||
|
"h_max_request_times": "600-900",
|
||||||
|
"h_max_reusable_secs": "1800-3000",
|
||||||
|
"h_keep_alive_period": 0
|
||||||
},
|
},
|
||||||
|
"x_padding_obfs_mode": false,
|
||||||
|
"x_padding_key": "",
|
||||||
|
"x_padding_header": "",
|
||||||
|
"x_padding_placement": "",
|
||||||
|
"x_padding_method": "",
|
||||||
|
"uplink_http_method": "",
|
||||||
|
"session_placement": "",
|
||||||
|
"session_key": "",
|
||||||
|
"seq_placement": "",
|
||||||
|
"seq_key": "",
|
||||||
|
"uplink_data_placement": "",
|
||||||
|
"uplink_data_key": "",
|
||||||
|
"uplink_chunk_size": 0,
|
||||||
|
"server": "example.com",
|
||||||
|
"server_port": 443,
|
||||||
"download": {
|
"download": {
|
||||||
"mode": "",
|
|
||||||
"host": "example.com",
|
"host": "example.com",
|
||||||
"path": "/xhttp",
|
"path": "/xhttp",
|
||||||
"domain_strategy": "prefer_ipv4",
|
"domain_strategy": "prefer_ipv4",
|
||||||
"x_padding_bytes": "0-0",
|
"x_padding_bytes": "100-1000",
|
||||||
"sc_max_each_post_bytes": "0-0",
|
"no_grpc_header": false, // stream-up/one, client only
|
||||||
"sc_min_posts_interval_ms": "0-0",
|
"sc_max_each_post_bytes": 1000000, // packet-up only
|
||||||
"sc_stream_up_server_secs": "0-0",
|
"sc_min_posts_interval_ms": 30, // packet-up, client only
|
||||||
"xmux": {
|
"xmux": { // h2/h3 mainly, client only
|
||||||
"max_concurrency": "0-1",
|
"max_concurrency": "16-32",
|
||||||
"max_connections": "0-1",
|
"max_connections": 0,
|
||||||
"c_max_reuse_times": "0-1",
|
"c_max_reuse_times": 0,
|
||||||
"h_max_request_times": "0-1",
|
"h_max_request_times": "600-900",
|
||||||
"h_max_reusable_secs": "0-1",
|
"h_max_reusable_secs": "1800-3000",
|
||||||
"h_keep_alive_period": 60
|
"h_keep_alive_period": 0
|
||||||
},
|
},
|
||||||
|
"x_padding_obfs_mode": false,
|
||||||
|
"x_padding_key": "",
|
||||||
|
"x_padding_header": "",
|
||||||
|
"x_padding_placement": "",
|
||||||
|
"x_padding_method": "",
|
||||||
|
"uplink_http_method": "",
|
||||||
|
"session_placement": "",
|
||||||
|
"session_key": "",
|
||||||
|
"seq_placement": "",
|
||||||
|
"seq_key": "",
|
||||||
|
"uplink_data_placement": "",
|
||||||
|
"uplink_data_key": "",
|
||||||
|
"uplink_chunk_size": 0,
|
||||||
"server": "example.com",
|
"server": "example.com",
|
||||||
"server_port": 443,
|
"server_port": 443,
|
||||||
"tls": {
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#outbound
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.com",
|
"server_name": "example.com",
|
||||||
"alpn": "h2" // h3 for QUIC
|
"alpn": "h2" // h3 for QUIC
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
|
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tls": {
|
"tls": { // https://sing-box.sagernet.org/configuration/shared/tls/#inbound
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"server_name": "example.com",
|
"server_name": "example.com",
|
||||||
"alpn": "h2", // h3 for QUIC
|
"alpn": "h2", // h3 for QUIC
|
||||||
@@ -33,6 +33,23 @@
|
|||||||
"type": "xhttp",
|
"type": "xhttp",
|
||||||
"mode": "stream-up",
|
"mode": "stream-up",
|
||||||
"path": "/xhttp",
|
"path": "/xhttp",
|
||||||
|
"x_padding_bytes": "100-1000",
|
||||||
|
"no_sse_header": false, // server only
|
||||||
|
"sc_max_buffered_posts": 30, // packet-up, server only
|
||||||
|
"sc_stream_up_server_secs": "20-80", // stream-up, server only
|
||||||
|
"x_padding_obfs_mode": false,
|
||||||
|
"x_padding_key": "",
|
||||||
|
"x_padding_header": "",
|
||||||
|
"x_padding_placement": "",
|
||||||
|
"x_padding_method": "",
|
||||||
|
"uplink_http_method": "",
|
||||||
|
"session_placement": "",
|
||||||
|
"session_key": "",
|
||||||
|
"seq_placement": "",
|
||||||
|
"seq_key": "",
|
||||||
|
"uplink_data_placement": "",
|
||||||
|
"uplink_data_key": "",
|
||||||
|
"uplink_chunk_size": 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
18
go.mod
18
go.mod
@@ -19,11 +19,10 @@ require (
|
|||||||
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
|
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
|
||||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0
|
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0
|
||||||
github.com/metacubex/utls v1.8.3
|
github.com/metacubex/utls v1.8.4
|
||||||
github.com/mholt/acmez/v3 v3.1.2
|
github.com/mholt/acmez/v3 v3.1.2
|
||||||
github.com/miekg/dns v1.1.67
|
github.com/miekg/dns v1.1.67
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
github.com/quic-go/quic-go v0.54.0
|
|
||||||
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
|
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
|
||||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
||||||
github.com/sagernet/cors v1.2.1
|
github.com/sagernet/cors v1.2.1
|
||||||
@@ -31,15 +30,15 @@ require (
|
|||||||
github.com/sagernet/gomobile v0.1.8
|
github.com/sagernet/gomobile v0.1.8
|
||||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
|
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
|
||||||
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
|
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
|
||||||
github.com/sagernet/sing v0.7.13
|
github.com/sagernet/sing v0.7.18
|
||||||
github.com/sagernet/sing-mux v0.3.3
|
github.com/sagernet/sing-mux v0.3.4
|
||||||
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
|
github.com/sagernet/sing-quic v0.5.3
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||||
github.com/sagernet/sing-tun v0.7.3
|
github.com/sagernet/sing-tun v0.7.11
|
||||||
github.com/sagernet/sing-vmess v0.2.7
|
github.com/sagernet/sing-vmess v0.2.7
|
||||||
github.com/sagernet/smux v1.5.34-mod.2
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1
|
||||||
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2
|
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2
|
||||||
github.com/sagernet/wireguard-go v0.0.1-beta.7
|
github.com/sagernet/wireguard-go v0.0.1-beta.7
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||||
@@ -66,7 +65,6 @@ require (
|
|||||||
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
@@ -150,7 +148,9 @@ require (
|
|||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.0.1
|
replace github.com/sagernet/wireguard-go => github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0
|
||||||
|
|
||||||
|
replace github.com/sagernet/tailscale => github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0
|
||||||
|
|
||||||
replace github.com/sagernet/sing-dns => github.com/shtorm-7/sing-dns v0.4.6-extended-1.0.0
|
replace github.com/sagernet/sing-dns => github.com/shtorm-7/sing-dns v0.4.6-extended-1.0.0
|
||||||
|
|
||||||
|
|||||||
46
go.sum
46
go.sum
@@ -32,7 +32,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
|
|||||||
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||||
@@ -130,8 +129,8 @@ github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos
|
|||||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
|
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
|
||||||
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
|
||||||
github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
|
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
|
||||||
github.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
||||||
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
|
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
|
||||||
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||||
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
|
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
|
||||||
@@ -151,8 +150,6 @@ github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyf
|
|||||||
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
||||||
@@ -174,46 +171,38 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
|
|||||||
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
|
||||||
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
|
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
|
||||||
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
|
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
|
||||||
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
|
||||||
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
|
github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||||
github.com/sagernet/sing v0.7.13/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
||||||
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
|
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||||
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
|
github.com/sagernet/sing-quic v0.5.3 h1:K937DKJN98xqyztijRkLJqbBfyV4rEZcYxFyP3EBikU=
|
||||||
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=
|
github.com/sagernet/sing-quic v0.5.3/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
|
||||||
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
|
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||||
github.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmVes=
|
github.com/sagernet/sing-tun v0.7.11 h1:qB7jy8JKqXg73fYBsDkBSy4ulRSbLrFut0e+y+QPhqU=
|
||||||
github.com/sagernet/sing-tun v0.7.3/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
|
github.com/sagernet/sing-tun v0.7.11/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
|
||||||
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
|
github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
|
||||||
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
|
||||||
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||||
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
|
||||||
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2 h1:MO7s4ni2bSfAOhcan2rdQSWCztkMXmqyg6jYPZp8bEE=
|
|
||||||
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
||||||
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTVtJ5jDTsTk5wtIIapZTRg=
|
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTVtJ5jDTsTk5wtIIapZTRg=
|
||||||
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
|
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
|
||||||
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.0.1 h1:IjsKFhL4HlvRo6IpU5kjLnL7TkP5vvCTJyOD6QzKADo=
|
github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0 h1:Yp4dIRwiwLda9JXyGMHkfYRr2r01NarkzsNd/oi10dk=
|
||||||
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.0.1/go.mod h1:DHxMTUaBGHP3tf8nJ/N8AkcoJDD0PHECLhTfLsw+ylQ=
|
github.com/shtorm-7/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0/go.mod h1:+znUAXWwgcgza5mb5do8j9RC95rpY9lbSc/TyEyCGa4=
|
||||||
|
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0 h1:o/AAMCZPDCrwat2m0rAicFJ+iHfuzBR4nNueORUiEtM=
|
||||||
|
github.com/shtorm-7/wireguard-go v0.0.1-beta.7-extended-1.2.0/go.mod h1:3Ps4sTih9KeKik6xsMdIa+2TWDgTb+ysnq+ztxespk8=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
|
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
|
||||||
@@ -271,8 +260,6 @@ go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
|||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
@@ -306,7 +293,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,37 +61,40 @@ func checkOptions(options *Options) error {
|
|||||||
|
|
||||||
func checkInbounds(inbounds []Inbound) error {
|
func checkInbounds(inbounds []Inbound) error {
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, inbound := range inbounds {
|
for i, inbound := range inbounds {
|
||||||
if inbound.Tag == "" {
|
tag := inbound.Tag
|
||||||
continue
|
if tag == "" {
|
||||||
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
if seen[inbound.Tag] {
|
if seen[tag] {
|
||||||
return E.New("duplicate inbound tag: ", inbound.Tag)
|
return E.New("duplicate inbound tag: ", tag)
|
||||||
}
|
}
|
||||||
seen[inbound.Tag] = true
|
seen[tag] = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
|
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
|
||||||
seen := make(map[string]bool)
|
seen := make(map[string]bool)
|
||||||
for _, outbound := range outbounds {
|
for i, outbound := range outbounds {
|
||||||
if outbound.Tag == "" {
|
tag := outbound.Tag
|
||||||
continue
|
if tag == "" {
|
||||||
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
if seen[outbound.Tag] {
|
if seen[tag] {
|
||||||
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
|
return E.New("duplicate outbound/endpoint tag: ", tag)
|
||||||
}
|
}
|
||||||
seen[outbound.Tag] = true
|
seen[tag] = true
|
||||||
}
|
}
|
||||||
for _, endpoint := range endpoints {
|
for i, endpoint := range endpoints {
|
||||||
if endpoint.Tag == "" {
|
tag := endpoint.Tag
|
||||||
continue
|
if tag == "" {
|
||||||
|
tag = F.ToString(i)
|
||||||
}
|
}
|
||||||
if seen[endpoint.Tag] {
|
if seen[tag] {
|
||||||
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
|
return E.New("duplicate outbound/endpoint tag: ", tag)
|
||||||
}
|
}
|
||||||
seen[endpoint.Tag] = true
|
seen[tag] = true
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,33 +11,34 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type TunInboundOptions struct {
|
type TunInboundOptions struct {
|
||||||
InterfaceName string `json:"interface_name,omitempty"`
|
InterfaceName string `json:"interface_name,omitempty"`
|
||||||
MTU uint32 `json:"mtu,omitempty"`
|
MTU uint32 `json:"mtu,omitempty"`
|
||||||
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
Address badoption.Listable[netip.Prefix] `json:"address,omitempty"`
|
||||||
AutoRoute bool `json:"auto_route,omitempty"`
|
AutoRoute bool `json:"auto_route,omitempty"`
|
||||||
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
|
IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"`
|
||||||
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
|
IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"`
|
||||||
AutoRedirect bool `json:"auto_redirect,omitempty"`
|
AutoRedirect bool `json:"auto_redirect,omitempty"`
|
||||||
AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
|
AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"`
|
||||||
AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"`
|
AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"`
|
||||||
LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"`
|
AutoRedirectIPRoute2FallbackRuleIndex int `json:"auto_redirect_iproute2_fallback_rule_index,omitempty"`
|
||||||
StrictRoute bool `json:"strict_route,omitempty"`
|
LoopbackAddress badoption.Listable[netip.Addr] `json:"loopback_address,omitempty"`
|
||||||
RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
|
StrictRoute bool `json:"strict_route,omitempty"`
|
||||||
RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"`
|
RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"`
|
||||||
RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
|
RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"`
|
||||||
RouteExcludeAddressSet badoption.Listable[string] `json:"route_exclude_address_set,omitempty"`
|
RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"`
|
||||||
IncludeInterface badoption.Listable[string] `json:"include_interface,omitempty"`
|
RouteExcludeAddressSet badoption.Listable[string] `json:"route_exclude_address_set,omitempty"`
|
||||||
ExcludeInterface badoption.Listable[string] `json:"exclude_interface,omitempty"`
|
IncludeInterface badoption.Listable[string] `json:"include_interface,omitempty"`
|
||||||
IncludeUID badoption.Listable[uint32] `json:"include_uid,omitempty"`
|
ExcludeInterface badoption.Listable[string] `json:"exclude_interface,omitempty"`
|
||||||
IncludeUIDRange badoption.Listable[string] `json:"include_uid_range,omitempty"`
|
IncludeUID badoption.Listable[uint32] `json:"include_uid,omitempty"`
|
||||||
ExcludeUID badoption.Listable[uint32] `json:"exclude_uid,omitempty"`
|
IncludeUIDRange badoption.Listable[string] `json:"include_uid_range,omitempty"`
|
||||||
ExcludeUIDRange badoption.Listable[string] `json:"exclude_uid_range,omitempty"`
|
ExcludeUID badoption.Listable[uint32] `json:"exclude_uid,omitempty"`
|
||||||
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
|
ExcludeUIDRange badoption.Listable[string] `json:"exclude_uid_range,omitempty"`
|
||||||
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
|
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
|
||||||
ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"`
|
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
|
||||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"`
|
||||||
Stack string `json:"stack,omitempty"`
|
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||||
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
Stack string `json:"stack,omitempty"`
|
||||||
|
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
||||||
InboundOptions
|
InboundOptions
|
||||||
|
|
||||||
// Deprecated: removed
|
// Deprecated: removed
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package option
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
|
Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/utils"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
@@ -110,7 +110,6 @@ type V2RayHTTPUpgradeOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type V2RayXHTTPBaseOptions struct {
|
type V2RayXHTTPBaseOptions struct {
|
||||||
Mode string `json:"mode"`
|
|
||||||
Host string `json:"host,omitempty"`
|
Host string `json:"host,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
Headers map[string]string `json:"headers,omitempty"`
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
@@ -123,13 +122,29 @@ type V2RayXHTTPBaseOptions struct {
|
|||||||
ScMaxBufferedPosts int64 `json:"sc_max_buffered_posts,omitempty"`
|
ScMaxBufferedPosts int64 `json:"sc_max_buffered_posts,omitempty"`
|
||||||
ScStreamUpServerSecs Xbadoption.Range `json:"sc_stream_up_server_secs"`
|
ScStreamUpServerSecs Xbadoption.Range `json:"sc_stream_up_server_secs"`
|
||||||
Xmux *V2RayXHTTPXmuxOptions `json:"xmux"`
|
Xmux *V2RayXHTTPXmuxOptions `json:"xmux"`
|
||||||
|
XPaddingObfsMode bool `json:"x_padding_obfs_mode,omitempty"`
|
||||||
|
XPaddingKey string `json:"x_padding_key,omitempty"`
|
||||||
|
XPaddingHeader string `json:"x_padding_header,omitempty"`
|
||||||
|
XPaddingPlacement string `json:"x_padding_placement,omitempty"`
|
||||||
|
XPaddingMethod string `json:"x_padding_method,omitempty"`
|
||||||
|
UplinkHTTPMethod string `json:"uplink_http_method,omitempty"`
|
||||||
|
SessionPlacement string `json:"session_placement,omitempty"`
|
||||||
|
SessionKey string `json:"session_key,omitempty"`
|
||||||
|
SeqPlacement string `json:"seq_placement,omitempty"`
|
||||||
|
SeqKey string `json:"seq_key,omitempty"`
|
||||||
|
UplinkDataPlacement string `json:"uplink_data_placement,omitempty"`
|
||||||
|
UplinkDataKey string `json:"uplink_data_key,omitempty"`
|
||||||
|
UplinkChunkSize uint32 `json:"uplink_chunk_size,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type V2RayXHTTPOptions struct {
|
type _V2RayXHTTPOptions struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
V2RayXHTTPBaseOptions
|
V2RayXHTTPBaseOptions
|
||||||
Download *V2RayXHTTPDownloadOptions `json:"download"`
|
Download *V2RayXHTTPDownloadOptions `json:"download"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type V2RayXHTTPOptions _V2RayXHTTPOptions
|
||||||
|
|
||||||
type V2RayXHTTPDownloadOptions struct {
|
type V2RayXHTTPDownloadOptions struct {
|
||||||
V2RayXHTTPBaseOptions
|
V2RayXHTTPBaseOptions
|
||||||
ServerOptions
|
ServerOptions
|
||||||
@@ -137,6 +152,158 @@ type V2RayXHTTPDownloadOptions struct {
|
|||||||
Detour string `json:"detour,omitempty"`
|
Detour string `json:"detour,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlacementQueryInHeader = "queryInHeader"
|
||||||
|
PlacementCookie = "cookie"
|
||||||
|
PlacementHeader = "header"
|
||||||
|
PlacementQuery = "query"
|
||||||
|
PlacementPath = "path"
|
||||||
|
PlacementBody = "body"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c V2RayXHTTPOptions) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal((*_V2RayXHTTPOptions)(&c))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPOptions) UnmarshalJSON(bytes []byte) error {
|
||||||
|
err := json.Unmarshal(bytes, (*_V2RayXHTTPOptions)(c))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch c.Mode {
|
||||||
|
case "":
|
||||||
|
c.Mode = "auto"
|
||||||
|
case "auto", "packet-up", "stream-up", "stream-one":
|
||||||
|
default:
|
||||||
|
return E.New("unsupported mode: " + c.Mode)
|
||||||
|
}
|
||||||
|
err = checkV2RayXHTTPBaseOptions(c.Mode, &c.V2RayXHTTPBaseOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c.Download != nil {
|
||||||
|
err = checkV2RayXHTTPBaseOptions(c.Mode, &c.Download.V2RayXHTTPBaseOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkV2RayXHTTPBaseOptions(mode string, options *V2RayXHTTPBaseOptions) error {
|
||||||
|
// Priority (client): host > serverName > address
|
||||||
|
for k := range options.Headers {
|
||||||
|
if strings.ToLower(k) == "host" {
|
||||||
|
return E.New(`"headers" can't contain "host"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.XPaddingBytes.From <= 0 || options.XPaddingBytes.To <= 0 {
|
||||||
|
return E.New("xPaddingBytes cannot be disabled")
|
||||||
|
}
|
||||||
|
if options.XPaddingKey == "" {
|
||||||
|
options.XPaddingKey = "x_padding"
|
||||||
|
}
|
||||||
|
if options.XPaddingHeader == "" {
|
||||||
|
options.XPaddingHeader = "X-Padding"
|
||||||
|
}
|
||||||
|
switch options.XPaddingPlacement {
|
||||||
|
case "":
|
||||||
|
options.XPaddingPlacement = "queryInHeader"
|
||||||
|
case "cookie", "header", "query", "queryInHeader":
|
||||||
|
default:
|
||||||
|
return E.New("unsupported padding placement: " + options.XPaddingPlacement)
|
||||||
|
}
|
||||||
|
switch options.XPaddingMethod {
|
||||||
|
case "":
|
||||||
|
options.XPaddingMethod = "repeat-x"
|
||||||
|
case "repeat-x", "tokenish":
|
||||||
|
default:
|
||||||
|
return E.New("unsupported padding method: " + options.XPaddingMethod)
|
||||||
|
}
|
||||||
|
switch options.UplinkDataPlacement {
|
||||||
|
case "":
|
||||||
|
options.UplinkDataPlacement = "body"
|
||||||
|
case "body":
|
||||||
|
case "cookie", "header":
|
||||||
|
if mode != "packet-up" {
|
||||||
|
return E.New("UplinkDataPlacement can be " + options.UplinkDataPlacement + " only in packet-up mode")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unsupported uplink data placement: " + options.UplinkDataPlacement)
|
||||||
|
}
|
||||||
|
if options.UplinkHTTPMethod == "" {
|
||||||
|
options.UplinkHTTPMethod = "POST"
|
||||||
|
}
|
||||||
|
options.UplinkHTTPMethod = strings.ToUpper(options.UplinkHTTPMethod)
|
||||||
|
if options.UplinkHTTPMethod == "GET" && mode != "packet-up" {
|
||||||
|
return E.New("uplinkHTTPMethod can be GET only in packet-up mode")
|
||||||
|
}
|
||||||
|
switch options.SessionPlacement {
|
||||||
|
case "":
|
||||||
|
options.SessionPlacement = "path"
|
||||||
|
case "path", "cookie", "header", "query":
|
||||||
|
default:
|
||||||
|
return E.New("unsupported session placement: " + options.SessionPlacement)
|
||||||
|
}
|
||||||
|
switch options.SeqPlacement {
|
||||||
|
case "":
|
||||||
|
options.SeqPlacement = "path"
|
||||||
|
case "path":
|
||||||
|
case "cookie", "header", "query":
|
||||||
|
if options.SessionPlacement == "path" {
|
||||||
|
return E.New("SeqPlacement must be path when SessionPlacement is path")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return E.New("unsupported seq placement: " + options.SeqPlacement)
|
||||||
|
}
|
||||||
|
if options.SessionPlacement != "path" && options.SessionKey == "" {
|
||||||
|
switch options.SessionPlacement {
|
||||||
|
case "cookie", "query":
|
||||||
|
options.SessionKey = "x_session"
|
||||||
|
case "header":
|
||||||
|
options.SessionKey = "X-Session"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.SeqPlacement != "path" && options.SeqKey == "" {
|
||||||
|
switch options.SeqPlacement {
|
||||||
|
case "cookie", "query":
|
||||||
|
options.SeqKey = "x_seq"
|
||||||
|
case "header":
|
||||||
|
options.SeqKey = "X-Seq"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.UplinkDataPlacement != "body" && options.UplinkDataKey == "" {
|
||||||
|
switch options.UplinkDataPlacement {
|
||||||
|
case "cookie":
|
||||||
|
options.UplinkDataKey = "x_data"
|
||||||
|
case "header":
|
||||||
|
options.UplinkDataKey = "X-Data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.UplinkChunkSize == 0 {
|
||||||
|
switch options.UplinkDataPlacement {
|
||||||
|
case "cookie":
|
||||||
|
options.UplinkChunkSize = 3 * 1024 // 3KB
|
||||||
|
case "header":
|
||||||
|
options.UplinkChunkSize = 4 * 1024 // 4KB
|
||||||
|
}
|
||||||
|
} else if options.UplinkChunkSize < 64 {
|
||||||
|
options.UplinkChunkSize = 64
|
||||||
|
}
|
||||||
|
if options.Xmux == nil {
|
||||||
|
options.Xmux = &V2RayXHTTPXmuxOptions{}
|
||||||
|
options.Xmux.MaxConcurrency.From = 1
|
||||||
|
options.Xmux.MaxConcurrency.To = 1
|
||||||
|
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("maxConnections cannot be specified together with maxConcurrency")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string {
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string {
|
||||||
pathAndQuery := strings.SplitN(c.Path, "?", 2)
|
pathAndQuery := strings.SplitN(c.Path, "?", 2)
|
||||||
path := pathAndQuery[0]
|
path := pathAndQuery[0]
|
||||||
@@ -158,19 +325,14 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedQuery() string {
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *V2RayXHTTPBaseOptions) GetRequestHeader(rawURL string) http.Header {
|
func (c *V2RayXHTTPBaseOptions) GetRequestHeader() http.Header {
|
||||||
header := http.Header{}
|
header := http.Header{}
|
||||||
for k, v := range c.Headers {
|
for k, v := range c.Headers {
|
||||||
header.Add(k, v)
|
header.Add(k, v)
|
||||||
}
|
}
|
||||||
u, _ := url.Parse(rawURL)
|
if header.Get("User-Agent") == "" {
|
||||||
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
|
header.Set("User-Agent", utils.ChromeUA)
|
||||||
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
|
}
|
||||||
// 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
|
|
||||||
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
|
|
||||||
// h3's similar QPACK feature uses the same huffman table.
|
|
||||||
u.RawQuery = "x_padding=" + strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().Rand()))
|
|
||||||
header.Set("Referer", u.String())
|
|
||||||
return header
|
return header
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,6 +346,13 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedXPaddingBytes() Xbadoption.Range {
|
|||||||
return c.XPaddingBytes
|
return c.XPaddingBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkHTTPMethod() string {
|
||||||
|
if c.UplinkHTTPMethod == "" {
|
||||||
|
return "POST"
|
||||||
|
}
|
||||||
|
return c.UplinkHTTPMethod
|
||||||
|
}
|
||||||
|
|
||||||
func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range {
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range {
|
||||||
if c.ScMaxEachPostBytes.To == 0 {
|
if c.ScMaxEachPostBytes.To == 0 {
|
||||||
return Xbadoption.Range{
|
return Xbadoption.Range{
|
||||||
@@ -222,6 +391,55 @@ func (c *V2RayXHTTPBaseOptions) GetNormalizedScStreamUpServerSecs() Xbadoption.R
|
|||||||
return c.ScStreamUpServerSecs
|
return c.ScStreamUpServerSecs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionPlacement() string {
|
||||||
|
if c.SessionPlacement == "" {
|
||||||
|
return PlacementPath
|
||||||
|
}
|
||||||
|
return c.SessionPlacement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqPlacement() string {
|
||||||
|
if c.SeqPlacement == "" {
|
||||||
|
return PlacementPath
|
||||||
|
}
|
||||||
|
return c.SeqPlacement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkDataPlacement() string {
|
||||||
|
if c.UplinkDataPlacement == "" {
|
||||||
|
return PlacementBody
|
||||||
|
}
|
||||||
|
return c.UplinkDataPlacement
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionKey() string {
|
||||||
|
if c.SessionKey != "" {
|
||||||
|
return c.SessionKey
|
||||||
|
}
|
||||||
|
switch c.GetNormalizedSessionPlacement() {
|
||||||
|
case PlacementHeader:
|
||||||
|
return "X-Session"
|
||||||
|
case PlacementCookie, PlacementQuery:
|
||||||
|
return "x_session"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqKey() string {
|
||||||
|
if c.SeqKey != "" {
|
||||||
|
return c.SeqKey
|
||||||
|
}
|
||||||
|
switch c.GetNormalizedSeqPlacement() {
|
||||||
|
case PlacementHeader:
|
||||||
|
return "X-Seq"
|
||||||
|
case PlacementCookie, PlacementQuery:
|
||||||
|
return "x_seq"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type V2RayXHTTPXmuxOptions struct {
|
type V2RayXHTTPXmuxOptions struct {
|
||||||
MaxConcurrency Xbadoption.Range `json:"max_concurrency"`
|
MaxConcurrency Xbadoption.Range `json:"max_concurrency"`
|
||||||
MaxConnections Xbadoption.Range `json:"max_connections"`
|
MaxConnections Xbadoption.Range `json:"max_connections"`
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type WireGuardEndpointOptions struct {
|
type WireGuardEndpointOptions struct {
|
||||||
System bool `json:"system,omitempty"`
|
System bool `json:"system,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
MTU uint32 `json:"mtu,omitempty"`
|
MTU uint32 `json:"mtu,omitempty"`
|
||||||
Address badoption.Listable[netip.Prefix] `json:"address"`
|
Address badoption.Listable[netip.Prefix] `json:"address"`
|
||||||
PrivateKey string `json:"private_key"`
|
PrivateKey string `json:"private_key"`
|
||||||
ListenPort uint16 `json:"listen_port,omitempty"`
|
ListenPort uint16 `json:"listen_port,omitempty"`
|
||||||
Peers []WireGuardPeer `json:"peers,omitempty"`
|
Peers []WireGuardPeer `json:"peers,omitempty"`
|
||||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||||
Workers int `json:"workers,omitempty"`
|
Workers int `json:"workers,omitempty"`
|
||||||
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
|
||||||
|
DisablePauses bool `json:"disable_pauses,omitempty"`
|
||||||
|
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
||||||
DialerOptions
|
DialerOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,13 +33,15 @@ type WireGuardPeer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WireGuardWARPEndpointOptions struct {
|
type WireGuardWARPEndpointOptions struct {
|
||||||
System bool `json:"system,omitempty"`
|
System bool `json:"system,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
ListenPort uint16 `json:"listen_port,omitempty"`
|
ListenPort uint16 `json:"listen_port,omitempty"`
|
||||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||||
Workers int `json:"workers,omitempty"`
|
Workers int `json:"workers,omitempty"`
|
||||||
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
|
||||||
Profile WARPProfile `json:"profile,omitempty"`
|
DisablePauses bool `json:"disable_pauses,omitempty"`
|
||||||
|
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
||||||
|
Profile WARPProfile `json:"profile,omitempty"`
|
||||||
DialerOptions
|
DialerOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +62,15 @@ type LegacyWireGuardOutboundOptions struct {
|
|||||||
PrivateKey string `json:"private_key"`
|
PrivateKey string `json:"private_key"`
|
||||||
Peers []LegacyWireGuardPeer `json:"peers,omitempty"`
|
Peers []LegacyWireGuardPeer `json:"peers,omitempty"`
|
||||||
ServerOptions
|
ServerOptions
|
||||||
PeerPublicKey string `json:"peer_public_key"`
|
PeerPublicKey string `json:"peer_public_key"`
|
||||||
PreSharedKey string `json:"pre_shared_key,omitempty"`
|
PreSharedKey string `json:"pre_shared_key,omitempty"`
|
||||||
Reserved []uint8 `json:"reserved,omitempty"`
|
Reserved []uint8 `json:"reserved,omitempty"`
|
||||||
Workers int `json:"workers,omitempty"`
|
Workers int `json:"workers,omitempty"`
|
||||||
MTU uint32 `json:"mtu,omitempty"`
|
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
|
||||||
Network NetworkList `json:"network,omitempty"`
|
DisablePauses bool `json:"disable_pauses,omitempty"`
|
||||||
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
MTU uint32 `json:"mtu,omitempty"`
|
||||||
|
Network NetworkList `json:"network,omitempty"`
|
||||||
|
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacyWireGuardPeer struct {
|
type LegacyWireGuardPeer struct {
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ func HandleStreamDNSRequest(ctx context.Context, router adapter.DNSRouter, conn
|
|||||||
conn.Close()
|
conn.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
responseBuffer := buf.NewPacket()
|
responseLength := response.Len()
|
||||||
|
responseBuffer := buf.NewSize(3 + responseLength)
|
||||||
defer responseBuffer.Release()
|
defer responseBuffer.Release()
|
||||||
responseBuffer.Resize(2, 0)
|
responseBuffer.Resize(2, 0)
|
||||||
n, err := response.PackBuffer(responseBuffer.FreeBytes())
|
n, err := response.PackBuffer(responseBuffer.FreeBytes())
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package naive
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
@@ -22,7 +22,11 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
sHttp "github.com/sagernet/sing/protocol/http"
|
sHttp "github.com/sagernet/sing/protocol/http"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ConfigureHTTP3ListenerFunc func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error)
|
var ConfigureHTTP3ListenerFunc func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error)
|
||||||
@@ -82,16 +86,11 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
if stage != adapter.StartStateStart {
|
if stage != adapter.StartStateStart {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var tlsConfig *tls.STDConfig
|
|
||||||
if n.tlsConfig != nil {
|
if n.tlsConfig != nil {
|
||||||
err := n.tlsConfig.Start()
|
err := n.tlsConfig.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, "create TLS config")
|
return E.Cause(err, "create TLS config")
|
||||||
}
|
}
|
||||||
tlsConfig, err = n.tlsConfig.Config()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if common.Contains(n.network, N.NetworkTCP) {
|
if common.Contains(n.network, N.NetworkTCP) {
|
||||||
tcpListener, err := n.listener.ListenTCP()
|
tcpListener, err := n.listener.ListenTCP()
|
||||||
@@ -99,20 +98,23 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
n.httpServer = &http.Server{
|
n.httpServer = &http.Server{
|
||||||
Handler: n,
|
Handler: h2c.NewHandler(n, &http2.Server{}),
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
BaseContext: func(listener net.Listener) context.Context {
|
BaseContext: func(listener net.Listener) context.Context {
|
||||||
return n.ctx
|
return n.ctx
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
var sErr error
|
listener := net.Listener(tcpListener)
|
||||||
if tlsConfig != nil {
|
if n.tlsConfig != nil {
|
||||||
sErr = n.httpServer.ServeTLS(tcpListener, "", "")
|
if len(n.tlsConfig.NextProtos()) == 0 {
|
||||||
} else {
|
n.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
||||||
sErr = n.httpServer.Serve(tcpListener)
|
} else if !common.Contains(n.tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
||||||
|
n.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, n.tlsConfig.NextProtos()...))
|
||||||
|
}
|
||||||
|
listener = aTLS.NewListener(tcpListener, n.tlsConfig)
|
||||||
}
|
}
|
||||||
if sErr != nil && !E.IsClosedOrCanceled(sErr) {
|
sErr := n.httpServer.Serve(listener)
|
||||||
|
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
|
||||||
n.logger.Error("http server serve error: ", sErr)
|
n.logger.Error("http server serve error: ", sErr)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -161,13 +163,16 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
n.badRequest(ctx, request, E.New("authorization failed"))
|
n.badRequest(ctx, request, E.New("authorization failed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writer.Header().Set("Padding", generateNaivePaddingHeader())
|
writer.Header().Set("Padding", generatePaddingHeader())
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
writer.(http.Flusher).Flush()
|
writer.(http.Flusher).Flush()
|
||||||
|
|
||||||
hostPort := request.URL.Host
|
hostPort := request.Header.Get("-connect-authority")
|
||||||
if hostPort == "" {
|
if hostPort == "" {
|
||||||
hostPort = request.Host
|
hostPort = request.URL.Host
|
||||||
|
if hostPort == "" {
|
||||||
|
hostPort = request.Host
|
||||||
|
}
|
||||||
}
|
}
|
||||||
source := sHttp.SourceAddress(request)
|
source := sHttp.SourceAddress(request)
|
||||||
destination := M.ParseSocksaddr(hostPort).Unwrap()
|
destination := M.ParseSocksaddr(hostPort).Unwrap()
|
||||||
@@ -178,9 +183,14 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
n.badRequest(ctx, request, E.New("hijack failed"))
|
n.badRequest(ctx, request, E.New("hijack failed"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination)
|
n.newConnection(ctx, false, &naiveConn{Conn: conn}, userName, source, destination)
|
||||||
} else {
|
} else {
|
||||||
n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination)
|
n.newConnection(ctx, true, &naiveH2Conn{
|
||||||
|
reader: request.Body,
|
||||||
|
writer: writer,
|
||||||
|
flusher: writer.(http.Flusher),
|
||||||
|
remoteAddress: source,
|
||||||
|
}, userName, source, destination)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,18 +246,3 @@ func rejectHTTP(writer http.ResponseWriter, statusCode int) {
|
|||||||
}
|
}
|
||||||
conn.Close()
|
conn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateNaivePaddingHeader() string {
|
|
||||||
paddingLen := rand.Intn(32) + 30
|
|
||||||
padding := make([]byte, paddingLen)
|
|
||||||
bits := rand.Uint64()
|
|
||||||
for i := 0; i < 16; i++ {
|
|
||||||
// Codes that won't be Huffman coded.
|
|
||||||
padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
|
|
||||||
bits >>= 4
|
|
||||||
}
|
|
||||||
for i := 16; i < paddingLen; i++ {
|
|
||||||
padding[i] = '~'
|
|
||||||
}
|
|
||||||
return string(padding)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,417 +7,243 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
|
"github.com/sagernet/sing/common/baderror"
|
||||||
"github.com/sagernet/sing/common/buf"
|
"github.com/sagernet/sing/common/buf"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/rw"
|
"github.com/sagernet/sing/common/rw"
|
||||||
)
|
)
|
||||||
|
|
||||||
const kFirstPaddings = 8
|
const paddingCount = 8
|
||||||
|
|
||||||
type naiveH1Conn struct {
|
func generatePaddingHeader() string {
|
||||||
net.Conn
|
paddingLen := rand.Intn(32) + 30
|
||||||
|
padding := make([]byte, paddingLen)
|
||||||
|
bits := rand.Uint64()
|
||||||
|
for i := 0; i < 16; i++ {
|
||||||
|
padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
|
||||||
|
bits >>= 4
|
||||||
|
}
|
||||||
|
for i := 16; i < paddingLen; i++ {
|
||||||
|
padding[i] = '~'
|
||||||
|
}
|
||||||
|
return string(padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
type paddingConn struct {
|
||||||
readPadding int
|
readPadding int
|
||||||
writePadding int
|
writePadding int
|
||||||
readRemaining int
|
readRemaining int
|
||||||
paddingRemaining int
|
paddingRemaining int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *naiveH1Conn) Read(p []byte) (n int, err error) {
|
func (p *paddingConn) readWithPadding(reader io.Reader, buffer []byte) (n int, err error) {
|
||||||
n, err = c.read(p)
|
if p.readRemaining > 0 {
|
||||||
return n, wrapHttpError(err)
|
if len(buffer) > p.readRemaining {
|
||||||
}
|
buffer = buffer[:p.readRemaining]
|
||||||
|
|
||||||
func (c *naiveH1Conn) read(p []byte) (n int, err error) {
|
|
||||||
if c.readRemaining > 0 {
|
|
||||||
if len(p) > c.readRemaining {
|
|
||||||
p = p[:c.readRemaining]
|
|
||||||
}
|
}
|
||||||
n, err = c.Conn.Read(p)
|
n, err = reader.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.readRemaining -= n
|
p.readRemaining -= n
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.paddingRemaining > 0 {
|
if p.paddingRemaining > 0 {
|
||||||
err = rw.SkipN(c.Conn, c.paddingRemaining)
|
err = rw.SkipN(reader, p.paddingRemaining)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.paddingRemaining = 0
|
p.paddingRemaining = 0
|
||||||
}
|
}
|
||||||
if c.readPadding < kFirstPaddings {
|
if p.readPadding < paddingCount {
|
||||||
var paddingHdr []byte
|
var paddingHeader []byte
|
||||||
if len(p) >= 3 {
|
if len(buffer) >= 3 {
|
||||||
paddingHdr = p[:3]
|
paddingHeader = buffer[:3]
|
||||||
} else {
|
} else {
|
||||||
paddingHdr = make([]byte, 3)
|
paddingHeader = make([]byte, 3)
|
||||||
}
|
}
|
||||||
_, err = io.ReadFull(c.Conn, paddingHdr)
|
_, err = io.ReadFull(reader, paddingHeader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
|
originalDataSize := int(binary.BigEndian.Uint16(paddingHeader[:2]))
|
||||||
paddingSize := int(paddingHdr[2])
|
paddingSize := int(paddingHeader[2])
|
||||||
if len(p) > originalDataSize {
|
if len(buffer) > originalDataSize {
|
||||||
p = p[:originalDataSize]
|
buffer = buffer[:originalDataSize]
|
||||||
}
|
}
|
||||||
n, err = c.Conn.Read(p)
|
n, err = reader.Read(buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.readPadding++
|
p.readPadding++
|
||||||
c.readRemaining = originalDataSize - n
|
p.readRemaining = originalDataSize - n
|
||||||
c.paddingRemaining = paddingSize
|
p.paddingRemaining = paddingSize
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return c.Conn.Read(p)
|
return reader.Read(buffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *naiveH1Conn) Write(p []byte) (n int, err error) {
|
func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, err error) {
|
||||||
for pLen := len(p); pLen > 0; {
|
if p.writePadding < paddingCount {
|
||||||
var data []byte
|
|
||||||
if pLen > 65535 {
|
|
||||||
data = p[:65535]
|
|
||||||
p = p[65535:]
|
|
||||||
pLen -= 65535
|
|
||||||
} else {
|
|
||||||
data = p
|
|
||||||
pLen = 0
|
|
||||||
}
|
|
||||||
var writeN int
|
|
||||||
writeN, err = c.write(data)
|
|
||||||
n += writeN
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return n, wrapHttpError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) write(p []byte) (n int, err error) {
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
paddingSize := rand.Intn(256)
|
paddingSize := rand.Intn(256)
|
||||||
|
buffer := buf.NewSize(3 + len(data) + paddingSize)
|
||||||
buffer := buf.NewSize(3 + len(p) + paddingSize)
|
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
header := buffer.Extend(3)
|
header := buffer.Extend(3)
|
||||||
binary.BigEndian.PutUint16(header, uint16(len(p)))
|
binary.BigEndian.PutUint16(header, uint16(len(data)))
|
||||||
header[2] = byte(paddingSize)
|
header[2] = byte(paddingSize)
|
||||||
|
common.Must1(buffer.Write(data))
|
||||||
common.Must1(buffer.Write(p))
|
buffer.Extend(paddingSize)
|
||||||
_, err = c.Conn.Write(buffer.Bytes())
|
_, err = writer.Write(buffer.Bytes())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
n = len(p)
|
n = len(data)
|
||||||
}
|
}
|
||||||
c.writePadding++
|
p.writePadding++
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return c.Conn.Write(p)
|
return writer.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *naiveH1Conn) FrontHeadroom() int {
|
func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffer) error {
|
||||||
if c.writePadding < kFirstPaddings {
|
if p.writePadding < paddingCount {
|
||||||
return 3
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) RearHeadroom() int {
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
return 255
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) WriterMTU() int {
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
return 65535
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) WriteBuffer(buffer *buf.Buffer) error {
|
|
||||||
defer buffer.Release()
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
bufferLen := buffer.Len()
|
bufferLen := buffer.Len()
|
||||||
if bufferLen > 65535 {
|
if bufferLen > 65535 {
|
||||||
return common.Error(c.Write(buffer.Bytes()))
|
_, err := p.writeChunked(writer, buffer.Bytes())
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
paddingSize := rand.Intn(256)
|
paddingSize := rand.Intn(256)
|
||||||
header := buffer.ExtendHeader(3)
|
header := buffer.ExtendHeader(3)
|
||||||
binary.BigEndian.PutUint16(header, uint16(bufferLen))
|
binary.BigEndian.PutUint16(header, uint16(bufferLen))
|
||||||
header[2] = byte(paddingSize)
|
header[2] = byte(paddingSize)
|
||||||
buffer.Extend(paddingSize)
|
buffer.Extend(paddingSize)
|
||||||
c.writePadding++
|
p.writePadding++
|
||||||
}
|
}
|
||||||
return wrapHttpError(common.Error(c.Conn.Write(buffer.Bytes())))
|
return common.Error(writer.Write(buffer.Bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME
|
func (p *paddingConn) writeChunked(writer io.Writer, data []byte) (n int, err error) {
|
||||||
/*func (c *naiveH1Conn) WriteTo(w io.Writer) (n int64, err error) {
|
for len(data) > 0 {
|
||||||
if c.readPadding < kFirstPaddings {
|
var chunk []byte
|
||||||
n, err = bufio.WriteToN(c, w, kFirstPaddings-c.readPadding)
|
if len(data) > 65535 {
|
||||||
} else {
|
chunk = data[:65535]
|
||||||
n, err = bufio.Copy(w, c.Conn)
|
data = data[65535:]
|
||||||
}
|
|
||||||
return n, wrapHttpError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) ReadFrom(r io.Reader) (n int64, err error) {
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
n, err = bufio.ReadFromN(c, r, kFirstPaddings-c.writePadding)
|
|
||||||
} else {
|
|
||||||
n, err = bufio.Copy(c.Conn, r)
|
|
||||||
}
|
|
||||||
return n, wrapHttpError(err)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) Upstream() any {
|
|
||||||
return c.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) ReaderReplaceable() bool {
|
|
||||||
return c.readPadding == kFirstPaddings
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH1Conn) WriterReplaceable() bool {
|
|
||||||
return c.writePadding == kFirstPaddings
|
|
||||||
}
|
|
||||||
|
|
||||||
type naiveH2Conn struct {
|
|
||||||
reader io.Reader
|
|
||||||
writer io.Writer
|
|
||||||
flusher http.Flusher
|
|
||||||
rAddr net.Addr
|
|
||||||
readPadding int
|
|
||||||
writePadding int
|
|
||||||
readRemaining int
|
|
||||||
paddingRemaining int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
|
|
||||||
n, err = c.read(p)
|
|
||||||
return n, wrapHttpError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) read(p []byte) (n int, err error) {
|
|
||||||
if c.readRemaining > 0 {
|
|
||||||
if len(p) > c.readRemaining {
|
|
||||||
p = p[:c.readRemaining]
|
|
||||||
}
|
|
||||||
n, err = c.reader.Read(p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.readRemaining -= n
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.paddingRemaining > 0 {
|
|
||||||
err = rw.SkipN(c.reader, c.paddingRemaining)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.paddingRemaining = 0
|
|
||||||
}
|
|
||||||
if c.readPadding < kFirstPaddings {
|
|
||||||
var paddingHdr []byte
|
|
||||||
if len(p) >= 3 {
|
|
||||||
paddingHdr = p[:3]
|
|
||||||
} else {
|
} else {
|
||||||
paddingHdr = make([]byte, 3)
|
chunk = data
|
||||||
|
data = nil
|
||||||
}
|
}
|
||||||
_, err = io.ReadFull(c.reader, paddingHdr)
|
var written int
|
||||||
|
written, err = p.writeWithPadding(writer, chunk)
|
||||||
|
n += written
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
|
|
||||||
paddingSize := int(paddingHdr[2])
|
|
||||||
if len(p) > originalDataSize {
|
|
||||||
p = p[:originalDataSize]
|
|
||||||
}
|
|
||||||
n, err = c.reader.Read(p)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.readPadding++
|
|
||||||
c.readRemaining = originalDataSize - n
|
|
||||||
c.paddingRemaining = paddingSize
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return c.reader.Read(p)
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
|
func (p *paddingConn) frontHeadroom() int {
|
||||||
for pLen := len(p); pLen > 0; {
|
if p.writePadding < paddingCount {
|
||||||
var data []byte
|
|
||||||
if pLen > 65535 {
|
|
||||||
data = p[:65535]
|
|
||||||
p = p[65535:]
|
|
||||||
pLen -= 65535
|
|
||||||
} else {
|
|
||||||
data = p
|
|
||||||
pLen = 0
|
|
||||||
}
|
|
||||||
var writeN int
|
|
||||||
writeN, err = c.write(data)
|
|
||||||
n += writeN
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == nil {
|
|
||||||
c.flusher.Flush()
|
|
||||||
}
|
|
||||||
return n, wrapHttpError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) write(p []byte) (n int, err error) {
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
paddingSize := rand.Intn(256)
|
|
||||||
|
|
||||||
buffer := buf.NewSize(3 + len(p) + paddingSize)
|
|
||||||
defer buffer.Release()
|
|
||||||
header := buffer.Extend(3)
|
|
||||||
binary.BigEndian.PutUint16(header, uint16(len(p)))
|
|
||||||
header[2] = byte(paddingSize)
|
|
||||||
|
|
||||||
common.Must1(buffer.Write(p))
|
|
||||||
_, err = c.writer.Write(buffer.Bytes())
|
|
||||||
if err == nil {
|
|
||||||
n = len(p)
|
|
||||||
}
|
|
||||||
c.writePadding++
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return c.writer.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) FrontHeadroom() int {
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
return 3
|
return 3
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *naiveH2Conn) RearHeadroom() int {
|
func (p *paddingConn) rearHeadroom() int {
|
||||||
if c.writePadding < kFirstPaddings {
|
if p.writePadding < paddingCount {
|
||||||
return 255
|
return 255
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *naiveH2Conn) WriterMTU() int {
|
func (p *paddingConn) writerMTU() int {
|
||||||
if c.writePadding < kFirstPaddings {
|
if p.writePadding < paddingCount {
|
||||||
return 65535
|
return 65535
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *paddingConn) readerReplaceable() bool {
|
||||||
|
return p.readPadding == paddingCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *paddingConn) writerReplaceable() bool {
|
||||||
|
return p.writePadding == paddingCount
|
||||||
|
}
|
||||||
|
|
||||||
|
type naiveConn struct {
|
||||||
|
net.Conn
|
||||||
|
paddingConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *naiveConn) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = c.readWithPadding(c.Conn, p)
|
||||||
|
return n, baderror.WrapH2(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *naiveConn) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = c.writeChunked(c.Conn, p)
|
||||||
|
return n, baderror.WrapH2(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error {
|
||||||
|
defer buffer.Release()
|
||||||
|
err := c.writeBufferWithPadding(c.Conn, buffer)
|
||||||
|
return baderror.WrapH2(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() }
|
||||||
|
func (c *naiveConn) RearHeadroom() int { return c.rearHeadroom() }
|
||||||
|
func (c *naiveConn) WriterMTU() int { return c.writerMTU() }
|
||||||
|
func (c *naiveConn) Upstream() any { return c.Conn }
|
||||||
|
func (c *naiveConn) ReaderReplaceable() bool { return c.readerReplaceable() }
|
||||||
|
func (c *naiveConn) WriterReplaceable() bool { return c.writerReplaceable() }
|
||||||
|
|
||||||
|
type naiveH2Conn struct {
|
||||||
|
reader io.Reader
|
||||||
|
writer io.Writer
|
||||||
|
flusher http.Flusher
|
||||||
|
remoteAddress net.Addr
|
||||||
|
paddingConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = c.readWithPadding(c.reader, p)
|
||||||
|
return n, baderror.WrapH2(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = c.writeChunked(c.writer, p)
|
||||||
|
if err == nil {
|
||||||
|
c.flusher.Flush()
|
||||||
|
}
|
||||||
|
return n, baderror.WrapH2(err)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
|
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
|
||||||
defer buffer.Release()
|
defer buffer.Release()
|
||||||
if c.writePadding < kFirstPaddings {
|
err := c.writeBufferWithPadding(c.writer, buffer)
|
||||||
bufferLen := buffer.Len()
|
|
||||||
if bufferLen > 65535 {
|
|
||||||
return common.Error(c.Write(buffer.Bytes()))
|
|
||||||
}
|
|
||||||
paddingSize := rand.Intn(256)
|
|
||||||
header := buffer.ExtendHeader(3)
|
|
||||||
binary.BigEndian.PutUint16(header, uint16(bufferLen))
|
|
||||||
header[2] = byte(paddingSize)
|
|
||||||
buffer.Extend(paddingSize)
|
|
||||||
c.writePadding++
|
|
||||||
}
|
|
||||||
err := common.Error(c.writer.Write(buffer.Bytes()))
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
c.flusher.Flush()
|
c.flusher.Flush()
|
||||||
}
|
}
|
||||||
return wrapHttpError(err)
|
return baderror.WrapH2(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME
|
|
||||||
/*func (c *naiveH2Conn) WriteTo(w io.Writer) (n int64, err error) {
|
|
||||||
if c.readPadding < kFirstPaddings {
|
|
||||||
n, err = bufio.WriteToN(c, w, kFirstPaddings-c.readPadding)
|
|
||||||
} else {
|
|
||||||
n, err = bufio.Copy(w, c.reader)
|
|
||||||
}
|
|
||||||
return n, wrapHttpError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) ReadFrom(r io.Reader) (n int64, err error) {
|
|
||||||
if c.writePadding < kFirstPaddings {
|
|
||||||
n, err = bufio.ReadFromN(c, r, kFirstPaddings-c.writePadding)
|
|
||||||
} else {
|
|
||||||
n, err = bufio.Copy(c.writer, r)
|
|
||||||
}
|
|
||||||
return n, wrapHttpError(err)
|
|
||||||
}*/
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) Close() error {
|
func (c *naiveH2Conn) Close() error {
|
||||||
return common.Close(
|
return common.Close(c.reader, c.writer)
|
||||||
c.reader,
|
|
||||||
c.writer,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *naiveH2Conn) LocalAddr() net.Addr {
|
func (c *naiveH2Conn) LocalAddr() net.Addr { return M.Socksaddr{} }
|
||||||
return M.Socksaddr{}
|
func (c *naiveH2Conn) RemoteAddr() net.Addr { return c.remoteAddress }
|
||||||
}
|
func (c *naiveH2Conn) SetDeadline(t time.Time) error { return os.ErrInvalid }
|
||||||
|
func (c *naiveH2Conn) SetReadDeadline(t time.Time) error { return os.ErrInvalid }
|
||||||
func (c *naiveH2Conn) RemoteAddr() net.Addr {
|
func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid }
|
||||||
return c.rAddr
|
func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool { return true }
|
||||||
}
|
func (c *naiveH2Conn) UpstreamReader() any { return c.reader }
|
||||||
|
func (c *naiveH2Conn) UpstreamWriter() any { return c.writer }
|
||||||
func (c *naiveH2Conn) SetDeadline(t time.Time) error {
|
func (c *naiveH2Conn) FrontHeadroom() int { return c.frontHeadroom() }
|
||||||
return os.ErrInvalid
|
func (c *naiveH2Conn) RearHeadroom() int { return c.rearHeadroom() }
|
||||||
}
|
func (c *naiveH2Conn) WriterMTU() int { return c.writerMTU() }
|
||||||
|
func (c *naiveH2Conn) ReaderReplaceable() bool { return c.readerReplaceable() }
|
||||||
func (c *naiveH2Conn) SetReadDeadline(t time.Time) error {
|
func (c *naiveH2Conn) WriterReplaceable() bool { return c.writerReplaceable() }
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error {
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) UpstreamReader() any {
|
|
||||||
return c.reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) UpstreamWriter() any {
|
|
||||||
return c.writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) ReaderReplaceable() bool {
|
|
||||||
return c.readPadding == kFirstPaddings
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *naiveH2Conn) WriterReplaceable() bool {
|
|
||||||
return c.writePadding == kFirstPaddings
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapHttpError(err error) error {
|
|
||||||
if err == nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "client disconnected") {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "body closed by handler") {
|
|
||||||
return net.ErrClosed
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "canceled with error code 268") {
|
|
||||||
return io.EOF
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
"github.com/sagernet/sing/service/filemanager"
|
"github.com/sagernet/sing/service/filemanager"
|
||||||
"github.com/sagernet/tailscale/ipn"
|
"github.com/sagernet/tailscale/ipn"
|
||||||
@@ -158,6 +159,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
},
|
},
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
RootCAs: adapter.RootPoolFromContext(ctx),
|
RootCAs: adapter.RootPoolFromContext(ctx),
|
||||||
|
Time: ntp.TimeFuncFromContext(ctx),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -339,26 +341,42 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination
|
|||||||
}
|
}
|
||||||
return N.DialSerial(ctx, t, network, destination, destinationAddresses)
|
return N.DialSerial(ctx, t, network, destination, destinationAddresses)
|
||||||
}
|
}
|
||||||
addr := tcpip.FullAddress{
|
addr4, addr6 := t.server.TailscaleIPs()
|
||||||
|
remoteAddr := tcpip.FullAddress{
|
||||||
NIC: 1,
|
NIC: 1,
|
||||||
Port: destination.Port,
|
Port: destination.Port,
|
||||||
Addr: addressFromAddr(destination.Addr),
|
Addr: addressFromAddr(destination.Addr),
|
||||||
}
|
}
|
||||||
|
var localAddr tcpip.FullAddress
|
||||||
var networkProtocol tcpip.NetworkProtocolNumber
|
var networkProtocol tcpip.NetworkProtocolNumber
|
||||||
if destination.IsIPv4() {
|
if destination.IsIPv4() {
|
||||||
|
if !addr4.IsValid() {
|
||||||
|
return nil, E.New("missing Tailscale IPv4 address")
|
||||||
|
}
|
||||||
networkProtocol = header.IPv4ProtocolNumber
|
networkProtocol = header.IPv4ProtocolNumber
|
||||||
|
localAddr = tcpip.FullAddress{
|
||||||
|
NIC: 1,
|
||||||
|
Addr: addressFromAddr(addr4),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if !addr6.IsValid() {
|
||||||
|
return nil, E.New("missing Tailscale IPv6 address")
|
||||||
|
}
|
||||||
networkProtocol = header.IPv6ProtocolNumber
|
networkProtocol = header.IPv6ProtocolNumber
|
||||||
|
localAddr = tcpip.FullAddress{
|
||||||
|
NIC: 1,
|
||||||
|
Addr: addressFromAddr(addr6),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
switch N.NetworkName(network) {
|
switch N.NetworkName(network) {
|
||||||
case N.NetworkTCP:
|
case N.NetworkTCP:
|
||||||
tcpConn, err := gonet.DialContextTCP(ctx, t.stack, addr, networkProtocol)
|
tcpConn, err := gonet.DialTCPWithBind(ctx, t.stack, localAddr, remoteAddr, networkProtocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return tcpConn, nil
|
return tcpConn, nil
|
||||||
case N.NetworkUDP:
|
case N.NetworkUDP:
|
||||||
udpConn, err := gonet.DialUDP(t.stack, nil, &addr, networkProtocol)
|
udpConn, err := gonet.DialUDP(t.stack, &localAddr, &remoteAddr, networkProtocol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -456,20 +474,20 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
|||||||
metadata.Inbound = t.Tag()
|
metadata.Inbound = t.Tag()
|
||||||
metadata.InboundType = t.Type()
|
metadata.InboundType = t.Type()
|
||||||
metadata.Source = source
|
metadata.Source = source
|
||||||
metadata.Destination = destination
|
|
||||||
addr4, addr6 := t.server.TailscaleIPs()
|
addr4, addr6 := t.server.TailscaleIPs()
|
||||||
switch destination.Addr {
|
switch destination.Addr {
|
||||||
case addr4:
|
case addr4:
|
||||||
metadata.OriginDestination = destination
|
metadata.OriginDestination = destination
|
||||||
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
|
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
|
||||||
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
|
||||||
case addr6:
|
case addr6:
|
||||||
metadata.OriginDestination = destination
|
metadata.OriginDestination = destination
|
||||||
destination.Addr = netip.IPv6Loopback()
|
destination.Addr = netip.IPv6Loopback()
|
||||||
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
|
||||||
}
|
}
|
||||||
|
metadata.Destination = destination
|
||||||
t.logger.InfoContext(ctx, "inbound packet connection from ", source)
|
t.logger.InfoContext(ctx, "inbound packet connection from ", source)
|
||||||
t.logger.InfoContext(ctx, "inbound packet connection to ", destination)
|
t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
|
||||||
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
if ruleIndex == 0 {
|
if ruleIndex == 0 {
|
||||||
ruleIndex = tun.DefaultIPRoute2RuleIndex
|
ruleIndex = tun.DefaultIPRoute2RuleIndex
|
||||||
}
|
}
|
||||||
|
autoRedirectFallbackRuleIndex := options.AutoRedirectIPRoute2FallbackRuleIndex
|
||||||
|
if autoRedirectFallbackRuleIndex == 0 {
|
||||||
|
autoRedirectFallbackRuleIndex = tun.DefaultIPRoute2AutoRedirectFallbackRuleIndex
|
||||||
|
}
|
||||||
inputMark := uint32(options.AutoRedirectInputMark)
|
inputMark := uint32(options.AutoRedirectInputMark)
|
||||||
if inputMark == 0 {
|
if inputMark == 0 {
|
||||||
inputMark = tun.DefaultAutoRedirectInputMark
|
inputMark = tun.DefaultAutoRedirectInputMark
|
||||||
@@ -192,32 +196,33 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
inboundOptions: options.InboundOptions,
|
inboundOptions: options.InboundOptions,
|
||||||
tunOptions: tun.Options{
|
tunOptions: tun.Options{
|
||||||
Name: options.InterfaceName,
|
Name: options.InterfaceName,
|
||||||
MTU: tunMTU,
|
MTU: tunMTU,
|
||||||
GSO: enableGSO,
|
GSO: enableGSO,
|
||||||
Inet4Address: inet4Address,
|
Inet4Address: inet4Address,
|
||||||
Inet6Address: inet6Address,
|
Inet6Address: inet6Address,
|
||||||
AutoRoute: options.AutoRoute,
|
AutoRoute: options.AutoRoute,
|
||||||
IPRoute2TableIndex: tableIndex,
|
IPRoute2TableIndex: tableIndex,
|
||||||
IPRoute2RuleIndex: ruleIndex,
|
IPRoute2RuleIndex: ruleIndex,
|
||||||
AutoRedirectInputMark: inputMark,
|
IPRoute2AutoRedirectFallbackRuleIndex: autoRedirectFallbackRuleIndex,
|
||||||
AutoRedirectOutputMark: outputMark,
|
AutoRedirectInputMark: inputMark,
|
||||||
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
|
AutoRedirectOutputMark: outputMark,
|
||||||
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
|
Inet4LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is4),
|
||||||
StrictRoute: options.StrictRoute,
|
Inet6LoopbackAddress: common.Filter(options.LoopbackAddress, netip.Addr.Is6),
|
||||||
IncludeInterface: options.IncludeInterface,
|
StrictRoute: options.StrictRoute,
|
||||||
ExcludeInterface: options.ExcludeInterface,
|
IncludeInterface: options.IncludeInterface,
|
||||||
Inet4RouteAddress: inet4RouteAddress,
|
ExcludeInterface: options.ExcludeInterface,
|
||||||
Inet6RouteAddress: inet6RouteAddress,
|
Inet4RouteAddress: inet4RouteAddress,
|
||||||
Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
|
Inet6RouteAddress: inet6RouteAddress,
|
||||||
Inet6RouteExcludeAddress: inet6RouteExcludeAddress,
|
Inet4RouteExcludeAddress: inet4RouteExcludeAddress,
|
||||||
IncludeUID: includeUID,
|
Inet6RouteExcludeAddress: inet6RouteExcludeAddress,
|
||||||
ExcludeUID: excludeUID,
|
IncludeUID: includeUID,
|
||||||
IncludeAndroidUser: options.IncludeAndroidUser,
|
ExcludeUID: excludeUID,
|
||||||
IncludePackage: options.IncludePackage,
|
IncludeAndroidUser: options.IncludeAndroidUser,
|
||||||
ExcludePackage: options.ExcludePackage,
|
IncludePackage: options.IncludePackage,
|
||||||
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
ExcludePackage: options.ExcludePackage,
|
||||||
EXP_MultiPendingPackets: multiPendingPackets,
|
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
||||||
|
EXP_MultiPendingPackets: multiPendingPackets,
|
||||||
},
|
},
|
||||||
udpTimeout: udpTimeout,
|
udpTimeout: udpTimeout,
|
||||||
stack: options.Stack,
|
stack: options.Stack,
|
||||||
@@ -319,7 +324,6 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
t.tunOptions.Name = tun.CalculateInterfaceName("")
|
t.tunOptions.Name = tun.CalculateInterfaceName("")
|
||||||
}
|
}
|
||||||
if t.platformInterface == nil {
|
if t.platformInterface == nil {
|
||||||
t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet)
|
|
||||||
for _, routeRuleSet := range t.routeRuleSet {
|
for _, routeRuleSet := range t.routeRuleSet {
|
||||||
ipSets := routeRuleSet.ExtractIPSet()
|
ipSets := routeRuleSet.ExtractIPSet()
|
||||||
if len(ipSets) == 0 {
|
if len(ipSets) == 0 {
|
||||||
@@ -331,7 +335,6 @@ func (t *Inbound) Start(stage adapter.StartStage) error {
|
|||||||
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
|
t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet)
|
|
||||||
for _, routeExcludeRuleSet := range t.routeExcludeRuleSet {
|
for _, routeExcludeRuleSet := range t.routeExcludeRuleSet {
|
||||||
ipSets := routeExcludeRuleSet.ExtractIPSet()
|
ipSets := routeExcludeRuleSet.ExtractIPSet()
|
||||||
if len(ipSets) == 0 {
|
if len(ipSets) == 0 {
|
||||||
|
|||||||
@@ -124,8 +124,10 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
Reserved: it.Reserved,
|
Reserved: it.Reserved,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Workers: options.Workers,
|
Workers: options.Workers,
|
||||||
Amnezia: amnezia,
|
PreallocatedBuffersPerPool: options.PreallocatedBuffersPerPool,
|
||||||
|
DisablePauses: options.DisablePauses,
|
||||||
|
Amnezia: amnezia,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -130,13 +130,15 @@ func NewWARPEndpoint(ctx context.Context, router adapter.Router, logger log.Cont
|
|||||||
logger,
|
logger,
|
||||||
tag,
|
tag,
|
||||||
option.WireGuardEndpointOptions{
|
option.WireGuardEndpointOptions{
|
||||||
System: options.System,
|
System: options.System,
|
||||||
Name: options.Name,
|
Name: options.Name,
|
||||||
ListenPort: options.ListenPort,
|
ListenPort: options.ListenPort,
|
||||||
UDPTimeout: options.UDPTimeout,
|
UDPTimeout: options.UDPTimeout,
|
||||||
Workers: options.Workers,
|
Workers: options.Workers,
|
||||||
Amnezia: options.Amnezia,
|
PreallocatedBuffersPerPool: options.PreallocatedBuffersPerPool,
|
||||||
DialerOptions: options.DialerOptions,
|
DisablePauses: options.DisablePauses,
|
||||||
|
Amnezia: options.Amnezia,
|
||||||
|
DialerOptions: options.DialerOptions,
|
||||||
|
|
||||||
Address: badoption.Listable[netip.Prefix]{
|
Address: badoption.Listable[netip.Prefix]{
|
||||||
netip.MustParsePrefix(config.Interface.Addresses.V4 + "/32"),
|
netip.MustParsePrefix(config.Interface.Addresses.V4 + "/32"),
|
||||||
|
|||||||
@@ -125,9 +125,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
}
|
}
|
||||||
return endpointAddresses[0], nil
|
return endpointAddresses[0], nil
|
||||||
},
|
},
|
||||||
Peers: peers,
|
Peers: peers,
|
||||||
Workers: options.Workers,
|
Workers: options.Workers,
|
||||||
Amnezia: amnezia,
|
PreallocatedBuffersPerPool: options.PreallocatedBuffersPerPool,
|
||||||
|
DisablePauses: options.DisablePauses,
|
||||||
|
Amnezia: amnezia,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn,
|
|||||||
} else {
|
} else {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
m.logger.DebugContext(ctx, "connection download finished")
|
m.logger.DebugContext(ctx, "connection download finished")
|
||||||
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") {
|
} else if !E.IsClosedOrCanceled(err) && !strings.Contains(err.Error(), "NO_ERROR") && !strings.Contains(err.Error(), "response body closed") {
|
||||||
m.logger.ErrorContext(ctx, "connection download closed: ", err)
|
m.logger.ErrorContext(ctx, "connection download closed: ", err)
|
||||||
} else {
|
} else {
|
||||||
m.logger.TraceContext(ctx, "connection download closed")
|
m.logger.TraceContext(ctx, "connection download closed")
|
||||||
|
|||||||
@@ -107,9 +107,7 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range r.items {
|
for _, item := range r.items {
|
||||||
if _, isRuleSet := item.(*RuleSetItem); !isRuleSet {
|
metadata.DidMatch = true
|
||||||
metadata.DidMatch = true
|
|
||||||
}
|
|
||||||
if !item.Match(metadata) {
|
if !item.Match(metadata) {
|
||||||
return r.invert
|
return r.invert
|
||||||
}
|
}
|
||||||
|
|||||||
157
route/rule/rule_abstract_test.go
Normal file
157
route/rule/rule_abstract_test.go
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go4.org/netipx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeRuleSet struct {
|
||||||
|
matched bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) Name() string {
|
||||||
|
return "fake-rule-set"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) StartContext(context.Context, *adapter.HTTPStartContext) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) PostStart() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||||
|
return adapter.RuleSetMetadata{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) ExtractIPSet() []*netipx.IPSet {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) IncRef() {}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) DecRef() {}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) Cleanup() {}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) RegisterCallback(adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) UnregisterCallback(*list.Element[adapter.RuleSetUpdateCallback]) {}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) Match(*adapter.InboundContext) bool {
|
||||||
|
return f.matched
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleSet) String() string {
|
||||||
|
return "fake-rule-set"
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeRuleItem struct {
|
||||||
|
matched bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleItem) Match(*adapter.InboundContext) bool {
|
||||||
|
return f.matched
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRuleItem) String() string {
|
||||||
|
return "fake-rule-item"
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRuleSetOnlyRule(ruleSetMatched bool, invert bool) *DefaultRule {
|
||||||
|
ruleSetItem := &RuleSetItem{
|
||||||
|
setList: []adapter.RuleSet{&fakeRuleSet{matched: ruleSetMatched}},
|
||||||
|
}
|
||||||
|
return &DefaultRule{
|
||||||
|
abstractDefaultRule: abstractDefaultRule{
|
||||||
|
items: []RuleItem{ruleSetItem},
|
||||||
|
allItems: []RuleItem{ruleSetItem},
|
||||||
|
invert: invert,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSingleItemRule(matched bool) *DefaultRule {
|
||||||
|
item := &fakeRuleItem{matched: matched}
|
||||||
|
return &DefaultRule{
|
||||||
|
abstractDefaultRule: abstractDefaultRule{
|
||||||
|
items: []RuleItem{item},
|
||||||
|
allItems: []RuleItem{item},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbstractDefaultRule_RuleSetOnly_InvertFalse(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.True(t, newRuleSetOnlyRule(true, false).Match(&adapter.InboundContext{}))
|
||||||
|
require.False(t, newRuleSetOnlyRule(false, false).Match(&adapter.InboundContext{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbstractDefaultRule_RuleSetOnly_InvertTrue(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.False(t, newRuleSetOnlyRule(true, true).Match(&adapter.InboundContext{}))
|
||||||
|
require.True(t, newRuleSetOnlyRule(false, true).Match(&adapter.InboundContext{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbstractLogicalRule_And_WithRuleSetInvert(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
aMatched bool
|
||||||
|
ruleSetBMatch bool
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "A true B true",
|
||||||
|
aMatched: true,
|
||||||
|
ruleSetBMatch: true,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "A true B false",
|
||||||
|
aMatched: true,
|
||||||
|
ruleSetBMatch: false,
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "A false B true",
|
||||||
|
aMatched: false,
|
||||||
|
ruleSetBMatch: true,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "A false B false",
|
||||||
|
aMatched: false,
|
||||||
|
ruleSetBMatch: false,
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
logicalRule := &abstractLogicalRule{
|
||||||
|
mode: C.LogicalTypeAnd,
|
||||||
|
rules: []adapter.HeadlessRule{
|
||||||
|
newSingleItemRule(testCase.aMatched),
|
||||||
|
newRuleSetOnlyRule(testCase.ruleSetBMatch, true),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Equal(t, testCase.expected, logicalRule.Match(&adapter.InboundContext{}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package derp
|
|||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
|
stdTLS "crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -31,6 +32,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
"github.com/sagernet/sing/service/filemanager"
|
"github.com/sagernet/sing/service/filemanager"
|
||||||
@@ -159,6 +161,10 @@ func (d *Service) Start(stage adapter.StartStage) error {
|
|||||||
httpClients = append(httpClients, &http.Client{
|
httpClients = append(httpClients, &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
ForceAttemptHTTP2: true,
|
ForceAttemptHTTP2: true,
|
||||||
|
TLSClientConfig: &stdTLS.Config{
|
||||||
|
RootCAs: adapter.RootPoolFromContext(d.ctx),
|
||||||
|
Time: ntp.TimeFuncFromContext(d.ctx),
|
||||||
|
},
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ func (s *Service) loadCache() error {
|
|||||||
os.RemoveAll(basePath)
|
os.RemoveAll(basePath)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
s.cacheMutex.Lock()
|
||||||
|
s.lastSavedCache = cacheBinary
|
||||||
|
s.cacheMutex.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,16 +59,30 @@ func (s *Service) saveCache() error {
|
|||||||
if s.cachePath == "" {
|
if s.cachePath == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
cacheBinary, err := s.encodeCache()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.cacheMutex.Lock()
|
||||||
|
defer s.cacheMutex.Unlock()
|
||||||
|
if bytes.Equal(s.lastSavedCache, cacheBinary) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.writeCache(cacheBinary)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) writeCache(cacheBinary []byte) error {
|
||||||
basePath := filemanager.BasePath(s.ctx, s.cachePath)
|
basePath := filemanager.BasePath(s.ctx, s.cachePath)
|
||||||
err := os.MkdirAll(filepath.Dir(basePath), 0o777)
|
err := os.MkdirAll(filepath.Dir(basePath), 0o777)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
cacheBinary, err := s.encodeCache()
|
err = os.WriteFile(basePath, cacheBinary, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return os.WriteFile(s.cachePath, cacheBinary, 0o644)
|
s.lastSavedCache = cacheBinary
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) decodeCache(cacheBinary []byte) error {
|
func (s *Service) decodeCache(cacheBinary []byte) error {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
@@ -28,21 +30,27 @@ func RegisterService(registry *boxService.Registry) {
|
|||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
boxService.Adapter
|
boxService.Adapter
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
logger log.ContextLogger
|
cancel context.CancelFunc
|
||||||
listener *listener.Listener
|
logger log.ContextLogger
|
||||||
tlsConfig tls.ServerConfig
|
listener *listener.Listener
|
||||||
httpServer *http.Server
|
tlsConfig tls.ServerConfig
|
||||||
traffics map[string]*TrafficManager
|
httpServer *http.Server
|
||||||
users map[string]*UserManager
|
traffics map[string]*TrafficManager
|
||||||
cachePath string
|
users map[string]*UserManager
|
||||||
|
cachePath string
|
||||||
|
saveTicker *time.Ticker
|
||||||
|
lastSavedCache []byte
|
||||||
|
cacheMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
|
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.SSMAPIServiceOptions) (adapter.Service, error) {
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
chiRouter := chi.NewRouter()
|
chiRouter := chi.NewRouter()
|
||||||
s := &Service{
|
s := &Service{
|
||||||
Adapter: boxService.NewAdapter(C.TypeSSMAPI, tag),
|
Adapter: boxService.NewAdapter(C.TypeSSMAPI, tag),
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
listener: listener.New(listener.Options{
|
listener: listener.New(listener.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
@@ -95,6 +103,8 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error(E.Cause(err, "load cache"))
|
s.logger.Error(E.Cause(err, "load cache"))
|
||||||
}
|
}
|
||||||
|
s.saveTicker = time.NewTicker(1 * time.Minute)
|
||||||
|
go s.loopSaveCache()
|
||||||
if s.tlsConfig != nil {
|
if s.tlsConfig != nil {
|
||||||
err = s.tlsConfig.Start()
|
err = s.tlsConfig.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,7 +130,27 @@ func (s *Service) Start(stage adapter.StartStage) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) loopSaveCache() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-s.saveTicker.C:
|
||||||
|
err := s.saveCache()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error(E.Cause(err, "save cache"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) Close() error {
|
func (s *Service) Close() error {
|
||||||
|
if s.cancel != nil {
|
||||||
|
s.cancel()
|
||||||
|
}
|
||||||
|
if s.saveTicker != nil {
|
||||||
|
s.saveTicker.Stop()
|
||||||
|
}
|
||||||
err := s.saveCache()
|
err := s.saveCache()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error(E.Cause(err, "save cache"))
|
s.logger.Error(E.Cause(err, "save cache"))
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ type StreamWrapper struct {
|
|||||||
|
|
||||||
func (s *StreamWrapper) Read(p []byte) (n int, err error) {
|
func (s *StreamWrapper) Read(p []byte) (n int, err error) {
|
||||||
n, err = s.Stream.Read(p)
|
n, err = s.Stream.Read(p)
|
||||||
|
//nolint:staticcheck
|
||||||
return n, baderror.WrapQUIC(err)
|
return n, baderror.WrapQUIC(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StreamWrapper) Write(p []byte) (n int, err error) {
|
func (s *StreamWrapper) Write(p []byte) (n int, err error) {
|
||||||
n, err = s.Stream.Write(p)
|
n, err = s.Stream.Write(p)
|
||||||
|
//nolint:staticcheck
|
||||||
return n, baderror.WrapQUIC(err)
|
return n, baderror.WrapQUIC(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import (
|
|||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/quic-go/quic-go"
|
"github.com/sagernet/quic-go"
|
||||||
"github.com/quic-go/quic-go/http3"
|
"github.com/sagernet/quic-go/http3"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
"github.com/sagernet/sing-box/common/xray/buf"
|
"github.com/sagernet/sing-box/common/xray/buf"
|
||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||||
"github.com/sagernet/sing-box/common/xray/uuid"
|
"github.com/sagernet/sing-box/common/xray/uuid"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
qtls "github.com/sagernet/sing-quic"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -34,12 +35,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
options *option.V2RayXHTTPOptions
|
options *option.V2RayXHTTPOptions
|
||||||
getRequestURL func(sessionId string) url.URL
|
baseRequestURL url.URL
|
||||||
getRequestURL2 func(sessionId string) url.URL
|
baseRequestURL2 url.URL
|
||||||
getHTTPClient func() (DialerClient, *XmuxClient)
|
getHTTPClient func() (DialerClient, *XmuxClient)
|
||||||
getHTTPClient2 func() (DialerClient, *XmuxClient)
|
getHTTPClient2 func() (DialerClient, *XmuxClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayXHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) {
|
func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayXHTTPOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) {
|
||||||
@@ -47,37 +48,22 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
|||||||
return nil, E.New("mode is not set")
|
return nil, E.New("mode is not set")
|
||||||
}
|
}
|
||||||
dest := serverAddr
|
dest := serverAddr
|
||||||
var gotlsConfig *gotls.Config
|
baseRequestURL, err := getBaseRequestURL(&options.V2RayXHTTPBaseOptions, dest, tlsConfig)
|
||||||
if tlsConfig != nil {
|
|
||||||
var err error
|
|
||||||
gotlsConfig, err = tlsConfig.Config()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
baseRequestURL, err := getBaseRequestURL(
|
|
||||||
&options.V2RayXHTTPBaseOptions, dest, tlsConfig,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
getRequestURL := func(sessionId string) url.URL {
|
|
||||||
requestURL := baseRequestURL
|
|
||||||
requestURL.Path += sessionId
|
|
||||||
return requestURL
|
|
||||||
}
|
|
||||||
var xmuxOptions option.V2RayXHTTPXmuxOptions
|
var xmuxOptions option.V2RayXHTTPXmuxOptions
|
||||||
if options.Xmux != nil {
|
if options.Xmux != nil {
|
||||||
xmuxOptions = *options.Xmux
|
xmuxOptions = *options.Xmux
|
||||||
}
|
}
|
||||||
xmuxManager := NewXmuxManager(xmuxOptions, func() XmuxConn {
|
xmuxManager := NewXmuxManager(xmuxOptions, func() XmuxConn {
|
||||||
return createHTTPClient(dest, dialer, &options.V2RayXHTTPBaseOptions, tlsConfig, gotlsConfig)
|
return createHTTPClient(dest, dialer, &options.V2RayXHTTPBaseOptions, tlsConfig)
|
||||||
})
|
})
|
||||||
getHTTPClient := func() (DialerClient, *XmuxClient) {
|
getHTTPClient := func() (DialerClient, *XmuxClient) {
|
||||||
xmuxClient := xmuxManager.GetXmuxClient(ctx)
|
xmuxClient := xmuxManager.GetXmuxClient(ctx)
|
||||||
return xmuxClient.XmuxConn.(DialerClient), xmuxClient
|
return xmuxClient.XmuxConn.(DialerClient), xmuxClient
|
||||||
}
|
}
|
||||||
getRequestURL2 := getRequestURL
|
baseRequestURL2 := baseRequestURL
|
||||||
getHTTPClient2 := getHTTPClient
|
getHTTPClient2 := getHTTPClient
|
||||||
if options.Download != nil {
|
if options.Download != nil {
|
||||||
options2 := options.Download
|
options2 := options.Download
|
||||||
@@ -91,32 +77,22 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
|||||||
}
|
}
|
||||||
dest2 := options2.ServerOptions.Build()
|
dest2 := options2.ServerOptions.Build()
|
||||||
var tlsConfig2 tls.Config
|
var tlsConfig2 tls.Config
|
||||||
var gotlsConfig2 *gotls.Config
|
|
||||||
if options2.TLS != nil {
|
if options2.TLS != nil {
|
||||||
tlsConfig2, err = tls.NewClient(ctx, options2.Server, common.PtrValueOrDefault(options2.TLS))
|
tlsConfig2, err = tls.NewClient(ctx, options2.Server, common.PtrValueOrDefault(options2.TLS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
gotlsConfig2, err = tlsConfig2.Config()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
baseRequestURL2, err := getBaseRequestURL(&options2.V2RayXHTTPBaseOptions, dest2, tlsConfig2)
|
baseRequestURL2, err = getBaseRequestURL(&options2.V2RayXHTTPBaseOptions, dest2, tlsConfig2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
getRequestURL2 = func(sessionId string) url.URL {
|
|
||||||
requestURL2 := baseRequestURL2
|
|
||||||
requestURL2.Path += sessionId
|
|
||||||
return requestURL2
|
|
||||||
}
|
|
||||||
var xmuxOptions2 option.V2RayXHTTPXmuxOptions
|
var xmuxOptions2 option.V2RayXHTTPXmuxOptions
|
||||||
if options2.Xmux != nil {
|
if options2.Xmux != nil {
|
||||||
xmuxOptions2 = *options2.Xmux
|
xmuxOptions2 = *options2.Xmux
|
||||||
}
|
}
|
||||||
xmuxManager2 := NewXmuxManager(xmuxOptions2, func() XmuxConn {
|
xmuxManager2 := NewXmuxManager(xmuxOptions2, func() XmuxConn {
|
||||||
return createHTTPClient(dest2, dialer2, &options2.V2RayXHTTPBaseOptions, tlsConfig2, gotlsConfig2)
|
return createHTTPClient(dest2, dialer2, &options2.V2RayXHTTPBaseOptions, tlsConfig2)
|
||||||
})
|
})
|
||||||
getHTTPClient2 = func() (DialerClient, *XmuxClient) {
|
getHTTPClient2 = func() (DialerClient, *XmuxClient) {
|
||||||
xmuxClient2 := xmuxManager2.GetXmuxClient(ctx)
|
xmuxClient2 := xmuxManager2.GetXmuxClient(ctx)
|
||||||
@@ -124,21 +100,25 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &Client{
|
return &Client{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
options: &options,
|
options: &options,
|
||||||
getHTTPClient: getHTTPClient,
|
getHTTPClient: getHTTPClient,
|
||||||
getHTTPClient2: getHTTPClient2,
|
getHTTPClient2: getHTTPClient2,
|
||||||
getRequestURL: getRequestURL,
|
baseRequestURL: baseRequestURL,
|
||||||
getRequestURL2: getRequestURL2,
|
baseRequestURL2: baseRequestURL2,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
||||||
options := c.options
|
options := c.options
|
||||||
mode := c.options.Mode
|
mode := c.options.Mode
|
||||||
sessionIdUuid := uuid.New()
|
sessionId := ""
|
||||||
requestURL := c.getRequestURL(sessionIdUuid.String())
|
if c.options.Mode != "stream-one" {
|
||||||
requestURL2 := c.getRequestURL2(sessionIdUuid.String())
|
sessionIdUuid := uuid.New()
|
||||||
|
sessionId = sessionIdUuid.String()
|
||||||
|
}
|
||||||
|
requestURL := c.baseRequestURL
|
||||||
|
requestURL2 := c.baseRequestURL2
|
||||||
httpClient, xmuxClient := c.getHTTPClient()
|
httpClient, xmuxClient := c.getHTTPClient()
|
||||||
httpClient2, xmuxClient2 := c.getHTTPClient2()
|
httpClient2, xmuxClient2 := c.getHTTPClient2()
|
||||||
if xmuxClient != nil {
|
if xmuxClient != nil {
|
||||||
@@ -169,7 +149,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
|||||||
if xmuxClient != nil {
|
if xmuxClient != nil {
|
||||||
xmuxClient.LeftRequests.Add(-1)
|
xmuxClient.LeftRequests.Add(-1)
|
||||||
}
|
}
|
||||||
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), reader, false)
|
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, false)
|
||||||
if err != nil { // browser dialer only
|
if err != nil { // browser dialer only
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -178,7 +158,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
|||||||
if xmuxClient2 != nil {
|
if xmuxClient2 != nil {
|
||||||
xmuxClient2.LeftRequests.Add(-1)
|
xmuxClient2.LeftRequests.Add(-1)
|
||||||
}
|
}
|
||||||
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), nil, false)
|
conn.reader, conn.remoteAddr, conn.localAddr, err = httpClient2.OpenStream(ctx, requestURL2.String(), sessionId, nil, false)
|
||||||
if err != nil { // browser dialer only
|
if err != nil { // browser dialer only
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -187,7 +167,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
|||||||
if xmuxClient != nil {
|
if xmuxClient != nil {
|
||||||
xmuxClient.LeftRequests.Add(-1)
|
xmuxClient.LeftRequests.Add(-1)
|
||||||
}
|
}
|
||||||
_, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), reader, true)
|
_, _, _, err = httpClient.OpenStream(ctx, requestURL.String(), sessionId, reader, true)
|
||||||
if err != nil { // browser dialer only
|
if err != nil { // browser dialer only
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -221,7 +201,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
|||||||
// this intentionally makes a shallow-copy of the struct so we
|
// this intentionally makes a shallow-copy of the struct so we
|
||||||
// can reassign Path (potentially concurrently)
|
// can reassign Path (potentially concurrently)
|
||||||
url := requestURL
|
url := requestURL
|
||||||
url.Path += "/" + strconv.FormatInt(seq, 10)
|
seqStr := strconv.FormatInt(seq, 10)
|
||||||
seq += 1
|
seq += 1
|
||||||
if scMinPostsIntervalMs.From > 0 {
|
if scMinPostsIntervalMs.From > 0 {
|
||||||
time.Sleep(time.Duration(scMinPostsIntervalMs.Rand())*time.Millisecond - time.Since(lastWrite))
|
time.Sleep(time.Duration(scMinPostsIntervalMs.Rand())*time.Millisecond - time.Since(lastWrite))
|
||||||
@@ -242,6 +222,8 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
|
|||||||
err := httpClient.PostPacket(
|
err := httpClient.PostPacket(
|
||||||
ctx,
|
ctx,
|
||||||
url.String(),
|
url.String(),
|
||||||
|
sessionId,
|
||||||
|
seqStr,
|
||||||
&buf.MultiBufferContainer{MultiBuffer: chunk},
|
&buf.MultiBufferContainer{MultiBuffer: chunk},
|
||||||
int64(chunk.Len()),
|
int64(chunk.Len()),
|
||||||
)
|
)
|
||||||
@@ -262,11 +244,11 @@ func (c *Client) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decideHTTPVersion(gotlsConfig *gotls.Config) string {
|
func decideHTTPVersion(tlsConfig tls.Config) string {
|
||||||
if gotlsConfig == nil || len(gotlsConfig.NextProtos) == 0 || gotlsConfig.NextProtos[0] == "http/1.1" {
|
if tlsConfig == nil || len(tlsConfig.NextProtos()) == 0 || tlsConfig.NextProtos()[0] == "http/1.1" {
|
||||||
return "1.1"
|
return "1.1"
|
||||||
}
|
}
|
||||||
if gotlsConfig.NextProtos[0] == "h3" {
|
if tlsConfig.NextProtos()[0] == "h3" {
|
||||||
return "3"
|
return "3"
|
||||||
}
|
}
|
||||||
return "2"
|
return "2"
|
||||||
@@ -298,8 +280,8 @@ func getBaseRequestURL(options *option.V2RayXHTTPBaseOptions, dest M.Socksaddr,
|
|||||||
return requestURL, nil
|
return requestURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createHTTPClient(dest M.Socksaddr, dialer N.Dialer, options *option.V2RayXHTTPBaseOptions, tlsConfig tls.Config, gotlsConfig *gotls.Config) DialerClient {
|
func createHTTPClient(dest M.Socksaddr, dialer N.Dialer, options *option.V2RayXHTTPBaseOptions, tlsConfig tls.Config) DialerClient {
|
||||||
httpVersion := decideHTTPVersion(gotlsConfig)
|
httpVersion := decideHTTPVersion(tlsConfig)
|
||||||
dialContext := func(ctxInner context.Context) (net.Conn, error) {
|
dialContext := func(ctxInner context.Context) (net.Conn, error) {
|
||||||
conn, err := dialer.DialContext(ctxInner, "tcp", dest)
|
conn, err := dialer.DialContext(ctxInner, "tcp", dest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -332,14 +314,13 @@ func createHTTPClient(dest M.Socksaddr, dialer N.Dialer, options *option.V2RayXH
|
|||||||
KeepAlivePeriod: keepAlivePeriod,
|
KeepAlivePeriod: keepAlivePeriod,
|
||||||
}
|
}
|
||||||
transport = &http3.Transport{
|
transport = &http3.Transport{
|
||||||
QUICConfig: quicConfig,
|
QUICConfig: quicConfig,
|
||||||
TLSClientConfig: gotlsConfig.Clone(),
|
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
|
||||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, dest)
|
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, dest)
|
||||||
if dErr != nil {
|
if dErr != nil {
|
||||||
return nil, dErr
|
return nil, dErr
|
||||||
}
|
}
|
||||||
return quic.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsCfg, cfg)
|
return qtls.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsConfig, cfg)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
case "2":
|
case "2":
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ package xhttp
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptrace"
|
"net/http/httptrace"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/xray"
|
common "github.com/sagernet/sing-box/common/xray"
|
||||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
)
|
)
|
||||||
@@ -19,11 +21,11 @@ import (
|
|||||||
type DialerClient interface {
|
type DialerClient interface {
|
||||||
IsClosed() bool
|
IsClosed() bool
|
||||||
|
|
||||||
// ctx, url, body, uploadOnly
|
// ctx, url, sessionId, body, uploadOnly
|
||||||
OpenStream(context.Context, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
|
OpenStream(context.Context, string, string, io.Reader, bool) (io.ReadCloser, net.Addr, net.Addr, error)
|
||||||
|
|
||||||
// ctx, url, body, contentLength
|
// ctx, url, sessionId, seqStr, body, contentLength
|
||||||
PostPacket(context.Context, string, io.Reader, int64) error
|
PostPacket(context.Context, string, string, string, io.Reader, int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// implements xhttp.DialerClient in terms of direct network connections
|
// implements xhttp.DialerClient in terms of direct network connections
|
||||||
@@ -41,7 +43,7 @@ func (c *DefaultDialerClient) IsClosed() bool {
|
|||||||
return c.closed
|
return c.closed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
|
func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, sessionId string, body io.Reader, uploadOnly bool) (wrc io.ReadCloser, remoteAddr, localAddr net.Addr, err error) {
|
||||||
// this is done when the TCP/UDP connection to the server was established,
|
// this is done when the TCP/UDP connection to the server was established,
|
||||||
// and we can unblock the Dial function and print correct net addresses in
|
// and we can unblock the Dial function and print correct net addresses in
|
||||||
// logs
|
// logs
|
||||||
@@ -55,11 +57,31 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
|
|||||||
})
|
})
|
||||||
method := "GET" // stream-down
|
method := "GET" // stream-down
|
||||||
if body != nil {
|
if body != nil {
|
||||||
method = "POST" // stream-up/one
|
method = c.options.GetNormalizedUplinkHTTPMethod() // stream-up/one
|
||||||
}
|
}
|
||||||
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
|
req, _ := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
|
||||||
req.Header = c.options.GetRequestHeader(url)
|
req.Header = c.options.GetRequestHeader()
|
||||||
if method == "POST" && !c.options.NoGRPCHeader {
|
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")
|
req.Header.Set("Content-Type", "application/grpc")
|
||||||
}
|
}
|
||||||
wrc = &WaitReadCloser{Wait: make(chan struct{})}
|
wrc = &WaitReadCloser{Wait: make(chan struct{})}
|
||||||
@@ -85,13 +107,76 @@ func (c *DefaultDialerClient) OpenStream(ctx context.Context, url string, body i
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body io.Reader, contentLength int64) error {
|
func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, sessionId string, seqStr string, body io.Reader, contentLength int64) error {
|
||||||
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), "POST", url, body)
|
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
|
||||||
|
}
|
||||||
|
method := c.options.GetNormalizedUplinkHTTPMethod()
|
||||||
|
req, err := http.NewRequestWithContext(context.WithoutCancel(ctx), method, url, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.ContentLength = contentLength
|
req.ContentLength = contentLength
|
||||||
req.Header = c.options.GetRequestHeader(url)
|
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)
|
||||||
if c.httpVersion != "1.1" {
|
if c.httpVersion != "1.1" {
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -150,7 +235,6 @@ func (c *DefaultDialerClient) PostPacket(ctx context.Context, url string, body i
|
|||||||
}
|
}
|
||||||
c.uploadRawPool.Put(uploadConn)
|
c.uploadRawPool.Put(uploadConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,3 +274,45 @@ func (w *WaitReadCloser) Close() error {
|
|||||||
close(w.Wait)
|
close(w.Wait)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ApplyMetaToRequest(options *option.V2RayXHTTPBaseOptions, req *http.Request, sessionId string, seqStr string) {
|
||||||
|
sessionPlacement := options.GetNormalizedSessionPlacement()
|
||||||
|
seqPlacement := options.GetNormalizedSeqPlacement()
|
||||||
|
sessionKey := options.GetNormalizedSessionKey()
|
||||||
|
seqKey := options.GetNormalizedSeqKey()
|
||||||
|
if sessionId != "" {
|
||||||
|
switch sessionPlacement {
|
||||||
|
case option.PlacementPath:
|
||||||
|
req.URL.Path = appendToPath(req.URL.Path, sessionId)
|
||||||
|
case option.PlacementQuery:
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set(sessionKey, sessionId)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
case option.PlacementHeader:
|
||||||
|
req.Header.Set(sessionKey, sessionId)
|
||||||
|
case option.PlacementCookie:
|
||||||
|
req.AddCookie(&http.Cookie{Name: sessionKey, Value: sessionId})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seqStr != "" {
|
||||||
|
switch seqPlacement {
|
||||||
|
case option.PlacementPath:
|
||||||
|
req.URL.Path = appendToPath(req.URL.Path, seqStr)
|
||||||
|
case option.PlacementQuery:
|
||||||
|
q := req.URL.Query()
|
||||||
|
q.Set(seqKey, seqStr)
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
case option.PlacementHeader:
|
||||||
|
req.Header.Set(seqKey, seqStr)
|
||||||
|
case option.PlacementCookie:
|
||||||
|
req.AddCookie(&http.Cookie{Name: seqKey, Value: seqStr})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendToPath(path, value string) string {
|
||||||
|
if strings.HasSuffix(path, "/") {
|
||||||
|
return path + value
|
||||||
|
}
|
||||||
|
return path + "/" + value
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ package xhttp
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -17,13 +18,11 @@ import (
|
|||||||
"github.com/sagernet/quic-go/http3"
|
"github.com/sagernet/quic-go/http3"
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
qtls "github.com/sagernet/sing-quic"
|
qtls "github.com/sagernet/sing-quic"
|
||||||
|
|
||||||
// qtls "github.com/sagernet/sing-quic"
|
|
||||||
"github.com/sagernet/sing-box/common/xray/signal/done"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
@@ -99,29 +98,31 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||||
writer.Header().Set("Access-Control-Allow-Methods", "GET, POST")
|
writer.Header().Set("Access-Control-Allow-Methods", "*")
|
||||||
writer.Header().Set("X-Padding", strings.Repeat("X", int(s.options.GetNormalizedXPaddingBytes().Rand())))
|
length := int(s.options.GetNormalizedXPaddingBytes().Rand())
|
||||||
validRange := s.options.GetNormalizedXPaddingBytes()
|
config := XPaddingConfig{Length: length}
|
||||||
paddingLength := 0
|
if s.options.XPaddingObfsMode {
|
||||||
referrer := request.Header.Get("Referer")
|
config.Placement = XPaddingPlacement{
|
||||||
if referrer != "" {
|
Placement: s.options.XPaddingPlacement,
|
||||||
if referrerURL, err := url.Parse(referrer); err == nil {
|
Key: s.options.XPaddingKey,
|
||||||
// Browser dialer cannot control the host part of referrer header, so only check the query
|
Header: s.options.XPaddingHeader,
|
||||||
paddingLength = len(referrerURL.Query().Get("x_padding"))
|
|
||||||
}
|
}
|
||||||
|
config.Method = PaddingMethod(s.options.XPaddingMethod)
|
||||||
} else {
|
} else {
|
||||||
paddingLength = len(request.URL.Query().Get("x_padding"))
|
config.Placement = XPaddingPlacement{
|
||||||
|
Placement: option.PlacementHeader,
|
||||||
|
Header: "X-Padding",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if int32(paddingLength) < validRange.From || int32(paddingLength) > validRange.To {
|
ApplyXPaddingToHeader(writer.Header(), config)
|
||||||
s.logger.ErrorContext(request.Context(), "invalid x_padding length:", int32(paddingLength))
|
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)))
|
||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sessionId := ""
|
sessionId, seqStr := ExtractMetaFromRequest(s.options, request, s.path)
|
||||||
subpath := strings.Split(request.URL.Path[len(s.path):], "/")
|
|
||||||
if len(subpath) > 0 {
|
|
||||||
sessionId = subpath[0]
|
|
||||||
}
|
|
||||||
if sessionId == "" && s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-one" && s.options.Mode != "stream-up" {
|
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")
|
s.logger.ErrorContext(request.Context(), "stream-one mode is not allowed")
|
||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
@@ -154,12 +155,25 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
currentSession = s.upsertSession(sessionId)
|
currentSession = s.upsertSession(sessionId)
|
||||||
}
|
}
|
||||||
scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To)
|
scMaxEachPostBytes := int(s.options.GetNormalizedScMaxEachPostBytes().To)
|
||||||
if request.Method == "POST" && sessionId != "" { // stream-up, packet-up
|
uplinkHTTPMethod := s.options.GetNormalizedUplinkHTTPMethod()
|
||||||
seq := ""
|
isUplinkRequest := false
|
||||||
if len(subpath) > 1 {
|
if uplinkHTTPMethod != "GET" && request.Method == uplinkHTTPMethod {
|
||||||
seq = subpath[1]
|
isUplinkRequest = true
|
||||||
|
}
|
||||||
|
uplinkDataPlacement := s.options.GetNormalizedUplinkDataPlacement()
|
||||||
|
uplinkDataKey := s.options.UplinkDataKey
|
||||||
|
switch uplinkDataPlacement {
|
||||||
|
case option.PlacementHeader:
|
||||||
|
if request.Header.Get(uplinkDataKey+"-Upstream") == "1" {
|
||||||
|
isUplinkRequest = true
|
||||||
}
|
}
|
||||||
if seq == "" {
|
case option.PlacementCookie:
|
||||||
|
if c, _ := request.Cookie(uplinkDataKey + "_upstream"); c != nil && c.Value == "1" {
|
||||||
|
isUplinkRequest = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isUplinkRequest && sessionId != "" { // stream-up, packet-up
|
||||||
|
if seqStr == "" {
|
||||||
if s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-up" {
|
if s.options.Mode != "" && s.options.Mode != "auto" && s.options.Mode != "stream-up" {
|
||||||
s.logger.ErrorContext(request.Context(), "stream-up mode is not allowed")
|
s.logger.ErrorContext(request.Context(), "stream-up mode is not allowed")
|
||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
@@ -181,6 +195,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
writer.Header().Set("Cache-Control", "no-store")
|
writer.Header().Set("Cache-Control", "no-store")
|
||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
scStreamUpServerSecs := s.options.GetNormalizedScStreamUpServerSecs()
|
scStreamUpServerSecs := s.options.GetNormalizedScStreamUpServerSecs()
|
||||||
|
referrer := request.Header.Get("Referer")
|
||||||
if referrer != "" && scStreamUpServerSecs.To > 0 {
|
if referrer != "" && scStreamUpServerSecs.To > 0 {
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
@@ -205,7 +220,55 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
writer.WriteHeader(http.StatusBadRequest)
|
writer.WriteHeader(http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
payload, err := io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
|
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, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
payload, err = io.ReadAll(io.LimitReader(request.Body, int64(scMaxEachPostBytes)+1))
|
||||||
|
}
|
||||||
if len(payload) > scMaxEachPostBytes {
|
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.")
|
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)
|
writer.WriteHeader(http.StatusRequestEntityTooLarge)
|
||||||
@@ -216,7 +279,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
writer.WriteHeader(http.StatusInternalServerError)
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
seqInt, err := strconv.ParseUint(seq, 10, 64)
|
seq, err := strconv.ParseUint(seqStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.InfoContext(request.Context(), err, "failed to upload (ParseUint)")
|
s.logger.InfoContext(request.Context(), err, "failed to upload (ParseUint)")
|
||||||
writer.WriteHeader(http.StatusInternalServerError)
|
writer.WriteHeader(http.StatusInternalServerError)
|
||||||
@@ -224,7 +287,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
|||||||
}
|
}
|
||||||
err = currentSession.uploadQueue.Push(Packet{
|
err = currentSession.uploadQueue.Push(Packet{
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
Seq: seqInt,
|
Seq: seq,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.InfoContext(request.Context(), err, "failed to upload (PushPayload)")
|
s.logger.InfoContext(request.Context(), err, "failed to upload (PushPayload)")
|
||||||
@@ -352,3 +415,41 @@ func (s *Server) upsertSession(sessionId string) *httpSession {
|
|||||||
}()
|
}()
|
||||||
return session
|
return session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExtractMetaFromRequest(options *option.V2RayXHTTPOptions, req *http.Request, path string) (sessionId string, seqStr string) {
|
||||||
|
sessionPlacement := options.GetNormalizedSessionPlacement()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
switch sessionPlacement {
|
||||||
|
case option.PlacementQuery:
|
||||||
|
sessionId = req.URL.Query().Get(sessionKey)
|
||||||
|
case option.PlacementHeader:
|
||||||
|
sessionId = req.Header.Get(sessionKey)
|
||||||
|
case option.PlacementCookie:
|
||||||
|
if cookie, e := req.Cookie(sessionKey); e == nil {
|
||||||
|
sessionId = cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch seqPlacement {
|
||||||
|
case option.PlacementQuery:
|
||||||
|
seqStr = req.URL.Query().Get(seqKey)
|
||||||
|
case option.PlacementHeader:
|
||||||
|
seqStr = req.Header.Get(seqKey)
|
||||||
|
case option.PlacementCookie:
|
||||||
|
if cookie, e := req.Cookie(seqKey); e == nil {
|
||||||
|
seqStr = cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessionId, seqStr
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package xhttp
|
package xhttp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
common "github.com/sagernet/sing-box/common/xray"
|
||||||
"github.com/sagernet/sing-box/common/xray/buf"
|
"github.com/sagernet/sing-box/common/xray/buf"
|
||||||
"github.com/sagernet/sing-box/common/xray/pipe"
|
"github.com/sagernet/sing-box/common/xray/pipe"
|
||||||
)
|
)
|
||||||
@@ -24,15 +25,17 @@ func (w uploadWriter) Write(b []byte) (int, error) {
|
|||||||
b = b[:capacity]
|
b = b[:capacity]
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
buffer := buf.New()
|
|
||||||
n, err := buffer.Write(b)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = w.WriteMultiBuffer([]*buf.Buffer{buffer})
|
buffer := buf.MultiBufferContainer{}
|
||||||
if err != nil {
|
common.Must2(buffer.Write(b))
|
||||||
return 0, err
|
|
||||||
|
var writed int
|
||||||
|
for _, buff := range buffer.MultiBuffer {
|
||||||
|
err := w.WriteMultiBuffer(buf.MultiBuffer{buff})
|
||||||
|
if err != nil {
|
||||||
|
return writed, err
|
||||||
|
}
|
||||||
|
writed += int(buff.Len())
|
||||||
}
|
}
|
||||||
return n, nil
|
return writed, nil
|
||||||
}
|
}
|
||||||
|
|||||||
268
transport/v2rayxhttp/xpadding.go
Normal file
268
transport/v2rayxhttp/xpadding.go
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
package xhttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"golang.org/x/net/http2/hpack"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaddingMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
PaddingMethodRepeatX PaddingMethod = "repeat-x"
|
||||||
|
PaddingMethodTokenish PaddingMethod = "tokenish"
|
||||||
|
)
|
||||||
|
|
||||||
|
const charsetBase62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||||
|
|
||||||
|
// Huffman encoding gives ~20% size reduction for base62 sequences
|
||||||
|
const avgHuffmanBytesPerCharBase62 = 0.8
|
||||||
|
|
||||||
|
const validationTolerance = 2
|
||||||
|
|
||||||
|
type XPaddingPlacement struct {
|
||||||
|
Placement string
|
||||||
|
Key string
|
||||||
|
Header string
|
||||||
|
RawURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
type XPaddingConfig struct {
|
||||||
|
Length int
|
||||||
|
Placement XPaddingPlacement
|
||||||
|
Method PaddingMethod
|
||||||
|
}
|
||||||
|
|
||||||
|
func randStringFromCharset(n int, charset string) (string, bool) {
|
||||||
|
if n <= 0 || len(charset) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
m := len(charset)
|
||||||
|
limit := byte(256 - (256 % m))
|
||||||
|
result := make([]byte, n)
|
||||||
|
i := 0
|
||||||
|
buf := make([]byte, 256)
|
||||||
|
for i < n {
|
||||||
|
if _, err := rand.Read(buf); err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for _, rb := range buf {
|
||||||
|
if rb >= limit {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[i] = charset[int(rb)%m]
|
||||||
|
i++
|
||||||
|
if i == n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(result), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func absInt(x int) int {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateTokenishPaddingBase62(targetHuffmanBytes int) string {
|
||||||
|
n := int(math.Ceil(float64(targetHuffmanBytes) / avgHuffmanBytesPerCharBase62))
|
||||||
|
if n < 1 {
|
||||||
|
n = 1
|
||||||
|
}
|
||||||
|
randBase62Str, ok := randStringFromCharset(n, charsetBase62)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
const maxIter = 150
|
||||||
|
adjustChar := byte('X')
|
||||||
|
// Adjust until close enough
|
||||||
|
for iter := 0; iter < maxIter; iter++ {
|
||||||
|
currentLength := int(hpack.HuffmanEncodeLength(randBase62Str))
|
||||||
|
diff := currentLength - targetHuffmanBytes
|
||||||
|
|
||||||
|
if absInt(diff) <= validationTolerance {
|
||||||
|
return randBase62Str
|
||||||
|
}
|
||||||
|
if diff < 0 {
|
||||||
|
// Too small -> append padding char(s)
|
||||||
|
randBase62Str += string(adjustChar)
|
||||||
|
// Avoid a long run of identical chars
|
||||||
|
if adjustChar == 'X' {
|
||||||
|
adjustChar = 'Z'
|
||||||
|
} else {
|
||||||
|
adjustChar = 'X'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Too big -> remove from the end
|
||||||
|
if len(randBase62Str) <= 1 {
|
||||||
|
return randBase62Str
|
||||||
|
}
|
||||||
|
randBase62Str = randBase62Str[:len(randBase62Str)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return randBase62Str
|
||||||
|
}
|
||||||
|
|
||||||
|
func GeneratePadding(method PaddingMethod, length int) string {
|
||||||
|
if length <= 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
|
||||||
|
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
|
||||||
|
// 'X' and 'Z' are assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
|
||||||
|
// h3's similar QPACK feature uses the same huffman table.
|
||||||
|
switch method {
|
||||||
|
case PaddingMethodRepeatX:
|
||||||
|
return strings.Repeat("X", length)
|
||||||
|
case PaddingMethodTokenish:
|
||||||
|
paddingValue := GenerateTokenishPaddingBase62(length)
|
||||||
|
if paddingValue == "" {
|
||||||
|
return strings.Repeat("X", length)
|
||||||
|
}
|
||||||
|
return paddingValue
|
||||||
|
default:
|
||||||
|
return strings.Repeat("X", length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyPaddingToCookie(req *http.Request, name, value string) {
|
||||||
|
if req == nil || name == "" || value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.AddCookie(&http.Cookie{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyPaddingToQuery(u *url.URL, key, value string) {
|
||||||
|
if u == nil || key == "" || value == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
q := u.Query()
|
||||||
|
q.Set(key, value)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyXPaddingToHeader(h http.Header, config XPaddingConfig) {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
paddingValue := GeneratePadding(config.Method, config.Length)
|
||||||
|
switch p := config.Placement; p.Placement {
|
||||||
|
case option.PlacementHeader:
|
||||||
|
h.Set(p.Header, paddingValue)
|
||||||
|
case option.PlacementQueryInHeader:
|
||||||
|
u, err := url.Parse(p.RawURL)
|
||||||
|
if err != nil || u == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
u.RawQuery = p.Key + "=" + paddingValue
|
||||||
|
h.Set(p.Header, u.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApplyXPaddingToRequest(req *http.Request, config XPaddingConfig) {
|
||||||
|
if req == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if req.Header == nil {
|
||||||
|
req.Header = make(http.Header)
|
||||||
|
}
|
||||||
|
placement := config.Placement.Placement
|
||||||
|
if placement == option.PlacementHeader || placement == option.PlacementQueryInHeader {
|
||||||
|
ApplyXPaddingToHeader(req.Header, config)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
paddingValue := GeneratePadding(config.Method, config.Length)
|
||||||
|
switch placement {
|
||||||
|
case option.PlacementCookie:
|
||||||
|
ApplyPaddingToCookie(req, config.Placement.Key, paddingValue)
|
||||||
|
case option.PlacementQuery:
|
||||||
|
ApplyPaddingToQuery(req.URL, config.Placement.Key, paddingValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractXPaddingFromRequest(options *option.V2RayXHTTPBaseOptions, req *http.Request, obfsMode bool) (string, string) {
|
||||||
|
if req == nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
if !obfsMode {
|
||||||
|
referrer := req.Header.Get("Referer")
|
||||||
|
if referrer != "" {
|
||||||
|
if referrerURL, err := url.Parse(referrer); err == nil {
|
||||||
|
paddingValue := referrerURL.Query().Get("x_padding")
|
||||||
|
paddingPlacement := option.PlacementQueryInHeader + "=Referer, key=x_padding"
|
||||||
|
return paddingValue, paddingPlacement
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
paddingValue := req.URL.Query().Get("x_padding")
|
||||||
|
return paddingValue, option.PlacementQuery + ", key=x_padding"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key := options.XPaddingKey
|
||||||
|
header := options.XPaddingHeader
|
||||||
|
if cookie, err := req.Cookie(key); err == nil {
|
||||||
|
if cookie != nil && cookie.Value != "" {
|
||||||
|
paddingValue := cookie.Value
|
||||||
|
paddingPlacement := option.PlacementCookie + ", key=" + key
|
||||||
|
return paddingValue, paddingPlacement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerValue := req.Header.Get(header)
|
||||||
|
if headerValue != "" {
|
||||||
|
if options.XPaddingPlacement == option.PlacementHeader {
|
||||||
|
paddingPlacement := option.PlacementHeader + "=" + header
|
||||||
|
return headerValue, paddingPlacement
|
||||||
|
}
|
||||||
|
|
||||||
|
if parsedURL, err := url.Parse(headerValue); err == nil {
|
||||||
|
paddingPlacement := option.PlacementQueryInHeader + "=" + header + ", key=" + key
|
||||||
|
|
||||||
|
return parsedURL.Query().Get(key), paddingPlacement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
queryValue := req.URL.Query().Get(key)
|
||||||
|
if queryValue != "" {
|
||||||
|
paddingPlacement := option.PlacementQuery + ", key=" + key
|
||||||
|
return queryValue, paddingPlacement
|
||||||
|
}
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsPaddingValid(options *option.V2RayXHTTPBaseOptions, paddingValue string, from, to int32, method PaddingMethod) bool {
|
||||||
|
if paddingValue == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if to <= 0 {
|
||||||
|
r := options.GetNormalizedXPaddingBytes()
|
||||||
|
from, to = r.From, r.To
|
||||||
|
}
|
||||||
|
switch method {
|
||||||
|
case PaddingMethodRepeatX:
|
||||||
|
n := int32(len(paddingValue))
|
||||||
|
return n >= from && n <= to
|
||||||
|
case PaddingMethodTokenish:
|
||||||
|
const tolerance = int32(validationTolerance)
|
||||||
|
n := int32(hpack.HuffmanEncodeLength(paddingValue))
|
||||||
|
f := from - tolerance
|
||||||
|
t := to + tolerance
|
||||||
|
if f < 0 {
|
||||||
|
f = 0
|
||||||
|
}
|
||||||
|
return n >= f && n <= t
|
||||||
|
default:
|
||||||
|
n := int32(len(paddingValue))
|
||||||
|
return n >= from && n <= to
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,7 +88,7 @@ func (w *systemDevice) Start() error {
|
|||||||
w.options.Logger.Info("started at ", w.options.Name)
|
w.options.Logger.Info("started at ", w.options.Name)
|
||||||
w.device = tunInterface
|
w.device = tunInterface
|
||||||
batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
|
batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN)
|
||||||
if isBatchTUN {
|
if isBatchTUN && batchTUN.BatchSize() > 1 {
|
||||||
w.batchDevice = batchTUN
|
w.batchDevice = batchTUN
|
||||||
}
|
}
|
||||||
w.events <- wgTun.EventUp
|
w.events <- wgTun.EventUp
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func (e *Endpoint) Start(resolve bool) error {
|
|||||||
e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...))
|
e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
wgDevice := device.NewDevice(e.options.Context, e.tunDevice, bind, logger, e.options.Workers)
|
wgDevice := device.NewDevice(e.options.Context, e.tunDevice, bind, logger, e.options.Workers, e.options.PreallocatedBuffersPerPool, e.options.DisablePauses)
|
||||||
e.tunDevice.SetDevice(wgDevice)
|
e.tunDevice.SetDevice(wgDevice)
|
||||||
ipcConf := e.ipcConf
|
ipcConf := e.ipcConf
|
||||||
if e.options.Amnezia != nil {
|
if e.options.Amnezia != nil {
|
||||||
|
|||||||
@@ -12,22 +12,24 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type EndpointOptions struct {
|
type EndpointOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Logger logger.ContextLogger
|
Logger logger.ContextLogger
|
||||||
System bool
|
System bool
|
||||||
Handler tun.Handler
|
Handler tun.Handler
|
||||||
UDPTimeout time.Duration
|
UDPTimeout time.Duration
|
||||||
Dialer N.Dialer
|
Dialer N.Dialer
|
||||||
CreateDialer func(interfaceName string) N.Dialer
|
CreateDialer func(interfaceName string) N.Dialer
|
||||||
Name string
|
Name string
|
||||||
MTU uint32
|
MTU uint32
|
||||||
Address []netip.Prefix
|
Address []netip.Prefix
|
||||||
PrivateKey string
|
PrivateKey string
|
||||||
ListenPort uint16
|
ListenPort uint16
|
||||||
ResolvePeer func(domain string) (netip.Addr, error)
|
ResolvePeer func(domain string) (netip.Addr, error)
|
||||||
Peers []PeerOptions
|
Peers []PeerOptions
|
||||||
Workers int
|
Workers int
|
||||||
Amnezia *AmneziaOptions
|
PreallocatedBuffersPerPool uint32
|
||||||
|
DisablePauses bool
|
||||||
|
Amnezia *AmneziaOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerOptions struct {
|
type PeerOptions struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user