Compare commits

..

72 Commits

Author SHA1 Message Date
Sergei Maklagin
596291567f Update AmneziaWG 2026-01-25 21:24:43 +03:00
Sergei Maklagin
a2a5f46cb6 Resolve conflicts 2026-01-18 21:53:22 +03:00
Sergei Maklagin
f6da8e52b4 Fix logger 2026-01-18 21:51:30 +03:00
世界
b27d707668 Bump version 2026-01-17 04:54:24 +08:00
世界
3ce94d50dd Update uTLS to v1.8.2 2026-01-17 04:54:18 +08:00
世界
29d56fca9c Update smux to v1.5.50 & Fix h2mux RST_STREAM on half-close 2026-01-17 04:17:14 +08:00
世界
ab18010ee1 Bump version 2026-01-12 20:38:21 +08:00
世界
e69c202c79 Fix logic issues with BBR impl 2026-01-12 20:34:04 +08:00
世界
0a812f2a46 Bump version 2026-01-07 15:13:35 +08:00
Gavin Luo
fffe9fc566 Fix reset buffer in dhcp response loop
Previously, the buffer was not reset within the response loop. If a packet
handle failed or completed, the buffer retained its state. Specifically,
if `ReadPacketFrom` returned `io.ErrShortBuffer`, the error was ignored
via `continue`, but the buffer remained full. This caused the next
read attempt to immediately fail with the same error, creating a tight
busy-wait loop that consumed 100% CPU.

Validates `buffer.Reset()` is called at the start of each iteration to
ensure a clean state for 'ReadPacketFrom'.
2026-01-05 17:46:59 +08:00
世界
6fdf27a701 Fix Tailscale endpoint using wrong source IP with advertise_routes 2026-01-04 22:14:54 +08:00
Bruce Wayne
7fa7d4f0a9 ducumentation: update Shadowsocks inbound documentation for SSM API 2026-01-02 19:18:52 +08:00
世界
f511ebc1d4 Fix lint errors 2026-01-02 19:17:53 +08:00
世界
84bbdc2eba Revert "Pin gofumpt and golangci-lint versions"
This reverts commit d9d7f7880d.
2026-01-02 19:14:13 +08:00
世界
568612fc70 Fix duplicate tag detection for empty tags
Closes https://github.com/SagerNet/sing-box/issues/3665
2026-01-02 19:14:13 +08:00
世界
d78828fd81 Fix quic sniffer 2026-01-02 19:14:13 +08:00
世界
f56d9ab945 Bump version 2025-12-25 14:47:10 +08:00
世界
86fabd6a22 Update Mozilla certificates 2025-12-25 14:42:18 +08:00
世界
24a1e7cee4 Ignore darwin IP_DONTFRAG error when not supported 2025-12-25 14:40:48 +08:00
世界
223dd8bb1a Fix TCP DNS response buffer 2025-12-22 13:51:00 +08:00
世界
68448de7d0 Fix missing RootPoolFromContext and TimeFuncFromContext in HTTP clients 2025-12-22 13:50:57 +08:00
世界
1ebff74c21 Fix DNS cache not working when domain strategy is set
The cache lookup was performed before rule matching, using the caller's
strategy (usually AsIS/0) instead of the resolved strategy. This caused
cache misses when ipv4_only was configured globally but the cache lookup
expected both A and AAAA records.

Remove LookupCache and ExchangeCache from Router, as the cache checks
inside client.Lookup and client.Exchange already handle caching correctly
after rule matching with the proper strategy and transport.
2025-12-21 16:59:10 +08:00
世界
f0cd3422c1 Bump version 2025-12-14 00:09:19 +08:00
世界
e385a98ced Update Go to 1.25.5 2025-12-13 20:11:29 +08:00
世界
670f32baee Fix naive inbound 2025-12-12 21:19:28 +08:00
Sergei Maklagin
287fe834db Update XHTTP 2025-12-11 02:46:57 +03:00
Sergei Maklagin
d7f0cea4ff Fix typo 2025-12-08 23:18:51 +03:00
Sergei Maklagin
d8b470d1ba Add new wireguard options 2025-12-08 22:32:33 +03:00
Sergei Maklagin
984fc295b3 Fix XHTTP TLS 2025-12-08 22:30:58 +03:00
世界
2747a00ba2 Fix tailscale destination 2025-12-01 15:02:04 +08:00
世界
48e76038d0 Update Go to 1.25.4 2025-11-16 09:53:10 +08:00
世界
6421252d44 release: Fix windows7 build 2025-11-16 09:09:34 +08:00
世界
216c4c8bd4 Fix adapter handler 2025-11-16 08:34:46 +08:00
世界
5841d410a1 ssm-api: Fix save cache 2025-11-04 11:00:43 +08:00
Kumiko as a Service
63c8207d7a Use --no-cache --upgrade option in apk add
No need for separate upgrade / cache cleanup steps.

Signed-off-by: Kumiko as a Service <Dreista@users.noreply.github.com>
2025-11-04 11:00:41 +08:00
Shtorm
6e4b7ed744 Merge pull request #6 from starifly/extended
fix(xhttp): use download request URL for down leg
2025-11-03 19:13:46 +03:00
starifly
855d400654 fix(xhttp): use download request URL for down leg 2025-11-03 23:18:03 +08:00
Sergei Maklagin
4c5e2c6645 Remove direct detour checking 2025-11-02 17:59:12 +03:00
Sergei Maklagin
725eccdea8 Fix Range 2025-11-02 17:55:18 +03:00
Sergei Maklagin
2ff042abd2 Resolve unnecessary logger 2025-11-02 17:41:18 +03:00
Sergei Maklagin
ffb282e47e Resolve conflicts 2025-11-02 17:39:38 +03:00
世界
54ed58499d Bump version 2025-10-27 18:04:24 +08:00
世界
b1bdc18c85 Fix socks response 2025-10-27 18:03:05 +08:00
世界
a38030cc0b Fix memory leak in hysteria2 2025-10-24 10:52:08 +08:00
世界
4626aa2cb0 Bump version 2025-10-21 21:39:28 +08:00
世界
5a40b673a4 Update dependencies 2025-10-21 21:39:28 +08:00
世界
541f63fee4 redirect: Fix compatibility with /product/bin/su 2025-10-21 21:27:15 +08:00
世界
5de6f4a14f Fix tailscale not enforcing NoLogsNoSupport 2025-10-16 22:30:08 +08:00
世界
5658830077 Fix trailing dot handling in local DNS transport 2025-10-16 21:43:12 +08:00
世界
0e50edc009 documentation: Add appreciate for Warp 2025-10-16 21:43:12 +08:00
世界
444f454810 Bump version 2025-10-14 23:43:36 +08:00
世界
d0e1fd6c7e Update Go to 1.25.3 2025-10-14 23:43:36 +08:00
世界
17b4d1e010 Update uTLS to v1.8.1 2025-10-14 23:40:19 +08:00
世界
06791470c9 Fix DNS reject panic 2025-10-14 23:40:19 +08:00
世界
ef14c8ca0e Disable TCP slow open for anytls
Fixes #3459
2025-10-14 23:40:19 +08:00
世界
36dc883c7c Fix DNS negative caching to comply with RFC 2308 2025-10-09 23:45:23 +08:00
Mahdi
6557bd7029 Fix dns cache in lookup 2025-10-09 23:45:23 +08:00
世界
41b30c91d9 Improve HTTPS DNS transport 2025-10-09 23:45:23 +08:00
世界
0f767d5ce1 Update .gitignore 2025-10-07 13:37:11 +08:00
世界
328a6de797 Bump version 2025-10-05 17:58:21 +08:00
Mahdi
886be6414d Fix dns truncate 2025-10-05 17:58:21 +08:00
世界
9362d3cab3 Attempt to fix leak in quic-go 2025-10-01 11:59:17 +08:00
世界
ced2e39dbf Update dependencies 2025-10-01 11:55:33 +08:00
anytls
2159d8877b Update anytls v0.0.11
Co-authored-by: anytls <anytls>
2025-10-01 10:23:15 +08:00
世界
cb7dba3eff release: Improve publish testflight 2025-10-01 10:22:44 +08:00
世界
d9d7f7880d Pin gofumpt and golangci-lint versions
As we don't want to remove naked returns
2025-09-23 16:33:42 +08:00
世界
a031aaf2c0 Do not reset network on sleep or wake 2025-09-23 16:17:44 +08:00
世界
4bca951773 Fix adguard matcher 2025-09-23 16:12:29 +08:00
世界
140735dbde Fix websocket log handling 2025-09-23 16:12:29 +08:00
世界
714a68bba1 Update .gitignore 2025-09-23 16:12:29 +08:00
Sergei Maklagin
91f9134379 Update README.md 2025-09-15 01:25:46 +03:00
Sergei Maklagin
5a4de7b242 Integrate Amnezia 1.5 2025-09-15 01:25:19 +03:00
72 changed files with 2011 additions and 1418 deletions

View File

@@ -1,25 +1,27 @@
#!/usr/bin/env bash
VERSION="1.23.12"
VERSION="1.25.5"
mkdir -p $HOME/go
cd $HOME/go
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_legacy
cd go_legacy
mv go go_win7
cd go_win7
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x
# that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
# this patch file only works on golang1.25.x
# that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
curl https://github.com/MetaCubeX/go/commit/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6788c4c6f9fafb56729bad6b660f7ee2272d699f.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/a5b2168bb836ed9d6601c626f95e56c07923f906.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/f56f1e23507e646c85243a71bde7b9629b2f970c.diff | patch --verbose -p 1

View File

@@ -46,7 +46,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -88,9 +88,9 @@ jobs:
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 }
- { os: windows, arch: amd64, legacy_go123: true, legacy_name: "windows-7" }
- { os: windows, arch: amd64, legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: "386" }
- { os: windows, arch: "386", legacy_go123: true, legacy_name: "windows-7" }
- { os: windows, arch: "386", legacy_win7: true, legacy_name: "windows-7" }
- { os: windows, arch: arm64 }
- { os: darwin, arch: amd64 }
@@ -107,32 +107,32 @@ jobs:
with:
fetch-depth: 0
- name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Setup Go 1.24
if: matrix.legacy_go124
uses: actions/setup-go@v5
with:
go-version: ~1.24.6
- name: Cache Go 1.23
if: matrix.legacy_go123
id: cache-legacy-go
go-version: ~1.24.10
- name: Cache Go for Windows 7
if: matrix.legacy_win7
id: cache-go-for-windows7
uses: actions/cache@v4
with:
path: |
~/go/go_legacy
key: go_legacy_12312
- name: Setup Go 1.23
if: matrix.legacy_go123 && steps.cache-legacy-go.outputs.cache-hit != 'true'
~/go/go_win7
key: go_win7_1255
- name: Setup Go for Windows 7
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |-
.github/setup_legacy_go.sh
- name: Setup Go 1.23
if: matrix.legacy_go123
.github/setup_go_for_windows7.sh
- name: Setup Go for Windows 7
if: matrix.legacy_win7
run: |-
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV
echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1
@@ -300,7 +300,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -380,7 +380,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Setup Android NDK
id: setup-ndk
uses: nttld/setup-ndk@v1
@@ -479,7 +479,7 @@ jobs:
if: matrix.if
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Set tag
if: matrix.if
run: |-

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ~1.24.6
go-version: ~1.24.10
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
with:

View File

@@ -30,7 +30,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Check input version
if: github.event_name == 'workflow_dispatch'
run: |-
@@ -71,7 +71,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ^1.25.1
go-version: ^1.25.5
- name: Setup Android NDK
if: matrix.os == 'android'
uses: nttld/setup-ndk@v1

4
.gitignore vendored
View File

@@ -15,4 +15,6 @@
.DS_Store
/config.d/
/venv/
CLAUDE.md
AGENTS.md
/.claude/

View File

@@ -20,8 +20,6 @@ RUN set -ex \
FROM --platform=$TARGETPLATFORM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN set -ex \
&& apk upgrade \
&& apk add bash tzdata ca-certificates nftables \
&& rm -rf /var/cache/apk/*
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
ENTRYPOINT ["sing-box"]

View File

@@ -4,12 +4,13 @@ Sing-box with extended features.
## Features
* Amnezia
* Amnezia 1.5
* WARP
* Tunneling
* Mieru
* XHTTP
* SDNS (DNSCrypt)
* Extended Wireguard options
* Unified delay
## Examples

View File

@@ -27,8 +27,6 @@ type DNSClient interface {
Start()
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)
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
ClearCache()
}

View File

@@ -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) {
myMetadata := ContextFrom(ctx)
_, myMetadata := ExtendContext(ctx)
if source.IsValid() {
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) {
myMetadata := ContextFrom(ctx)
_, myMetadata := ExtendContext(ctx)
if source.IsValid() {
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) {
metadata := ContextFrom(ctx)
_, metadata := ExtendContext(ctx)
if source.IsValid() {
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) {
metadata := ContextFrom(ctx)
_, metadata := ExtendContext(ctx)
if source.IsValid() {
metadata.Source = source
}

View File

@@ -134,6 +134,7 @@ func publishTestflight(ctx context.Context) error {
asc.PlatformTVOS,
}
}
waitingForProcess := false
for _, platform := range platforms {
log.Info(string(platform), " list builds")
for {
@@ -145,12 +146,13 @@ func publishTestflight(ctx context.Context) error {
return err
}
build := builds.Data[0]
if common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute {
if !waitingForProcess && (common.Contains(buildIDs, build.ID) || time.Since(build.Attributes.UploadedDate.Time) > 30*time.Minute) {
log.Info(string(platform), " ", tag, " waiting for process")
time.Sleep(15 * time.Second)
continue
}
if *build.Attributes.ProcessingState != "VALID" {
waitingForProcess = true
log.Info(string(platform), " ", tag, " waiting for process: ", *build.Attributes.ProcessingState)
time.Sleep(15 * time.Second)
continue

File diff suppressed because it is too large Load Diff

View File

@@ -52,14 +52,6 @@ func (d *DetourDialer) init() {
d.initErr = E.New("outbound detour not found: ", d.detour)
return
}
if !d.legacyDNSDialer {
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
if directDialer.IsEmpty() {
d.initErr = E.New("detour to an empty direct outbound makes no sense")
return
}
}
}
d.dialer = dialer
}

View File

@@ -303,8 +303,6 @@ find:
metadata.Protocol = C.ProtocolQUIC
fingerprint, err := ja3.Compute(buffer.Bytes())
if err != nil {
metadata.Protocol = C.ProtocolQUIC
metadata.Client = C.ClientChromium
metadata.SniffContext = fragments
return E.Cause1(ErrNeedMoreData, err)
}
@@ -334,7 +332,7 @@ find:
}
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
if maybeUQUIC(fingerprint) {
if isQUICGo(fingerprint) {
metadata.Client = C.ClientQUICGo
} else {
metadata.Client = C.ClientChromium

View File

@@ -1,21 +1,29 @@
package sniff
import (
"crypto/tls"
"github.com/sagernet/sing-box/common/ja3"
)
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
// The cronet without this behavior does not have version 115
var uQUICChrome115 = &ja3.ClientHello{
Version: tls.VersionTLS12,
CipherSuites: []uint16{4865, 4866, 4867},
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},
}
const (
// X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
x25519Kyber768Draft00 uint16 = 0x11EC // 4588
// renegotiation_info extension used by Go crypto/tls
extensionRenegotiationInfo uint16 = 0xFF01 // 65281
)
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
return !uQUICChrome115.Equals(fingerprint, true)
// isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
// 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
}

View 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")
}
}

View File

@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
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)
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
require.NoError(t, err)
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
var metadata adapter.InboundContext
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
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)
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
require.NoError(t, err)

View File

@@ -53,26 +53,48 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
return tlsConn, nil
}
type Dialer struct {
type Dialer interface {
N.Dialer
DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error)
}
type defaultDialer struct {
dialer N.Dialer
config Config
}
func NewDialer(dialer N.Dialer, config Config) N.Dialer {
return &Dialer{dialer, config}
func NewDialer(dialer N.Dialer, config Config) Dialer {
return &defaultDialer{dialer, config}
}
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if network != N.NetworkTCP {
func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if N.NetworkName(network) != N.NetworkTCP {
return nil, os.ErrInvalid
}
conn, err := d.dialer.DialContext(ctx, network, destination)
return d.DialTLSContext(ctx, destination)
}
func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
}
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
return d.dialContext(ctx, destination)
}
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
if err != nil {
return nil, err
}
return ClientHandshake(ctx, conn, d.config)
tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config)
if err != nil {
conn.Close()
return nil, err
}
return tlsConn, nil
}
func (d *Dialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
func (d *defaultDialer) Upstream() any {
return d.dialer
}

View File

@@ -13,7 +13,7 @@ const (
Size = 8192
)
var zero = [Size * 10]byte{0}
var ErrBufferFull = E.New("buffer is full")
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.
// 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 {
end := b.end + n
if end > int32(len(b.v)) {
@@ -152,7 +152,7 @@ func (b *Buffer) Extend(n int32) []byte {
}
ext := b.v[b.end:end]
b.end = end
copy(ext, zero[:])
clear(ext)
return ext
}
@@ -215,7 +215,7 @@ func (b *Buffer) Resize(from, to int32) {
b.start += from
b.Check()
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))
}
// 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.
func (b *Buffer) IsEmpty() bool {
return b.Len() == 0
@@ -258,13 +266,16 @@ func (b *Buffer) IsFull() bool {
func (b *Buffer) Write(data []byte) (int, error) {
nBytes := copy(b.v[b.end:], data)
b.end += int32(nBytes)
if nBytes < len(data) {
return nBytes, ErrBufferFull
}
return nBytes, nil
}
// WriteByte writes a single byte into the buffer.
func (b *Buffer) WriteByte(v byte) error {
if b.IsFull() {
return E.New("buffer full")
return ErrBufferFull
}
b.v[b.end] = v
b.end++

View File

@@ -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.
type TimeoutReader interface {
Reader
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
}

View File

@@ -144,7 +144,7 @@ func Compact(mb MultiBuffer) MultiBuffer {
for i := 1; i < len(mb); i++ {
curr := mb[i]
if last.Len()+curr.Len() > Size {
if curr.Len() > last.Available() {
mb2 = append(mb2, last)
last = curr
} else {

View File

@@ -75,9 +75,10 @@ func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) {
// BufferedWriter is a Writer with internal buffer.
type BufferedWriter struct {
sync.Mutex
writer Writer
buffer *Buffer
buffered bool
writer Writer
buffer *Buffer
buffered bool
flushNext bool
}
// 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
}
@@ -201,6 +208,13 @@ func (w *BufferedWriter) SetBuffered(f bool) error {
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.
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
if err := w.SetBuffered(false); err != nil {

View File

@@ -1,5 +1,7 @@
package common
import "reflect"
// Must panics if err is not nil.
func Must(err error) {
if err != nil {
@@ -17,3 +19,14 @@ func Must2(v interface{}, err error) interface{} {
func Error2(v interface{}, err error) error {
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
}

View File

@@ -9,6 +9,9 @@ func RandBetween(from int64, to int64) int64 {
if from == to {
return from
}
if from > to {
from, to = to, from
}
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
return from + bigInt.Int64()
}

View File

@@ -33,17 +33,22 @@ func (c *Range) UnmarshalJSON(content []byte) error {
if err == nil {
parts := strings.Split(stringValue, "-")
if len(parts) != 2 {
return E.New("invalid length of range parts")
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
rangeValue.From, rangeValue.To = int32(from), int32(from)
} else {
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
to, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return err
}
rangeValue.From, rangeValue.To = int32(from), int32(to)
}
from, err := strconv.ParseInt(parts[0], 10, 32)
if err != nil {
return err
}
to, err := strconv.ParseInt(parts[1], 10, 32)
if err != nil {
return err
}
rangeValue.From, rangeValue.To = int32(from), int32(to)
} else {
err := json.Unmarshal(content, &rangeValue)
if err != nil {

View File

@@ -200,16 +200,19 @@ func (p *pipe) Interrupt() {
p.Lock()
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 {
return
}
p.state = errord
if !p.data.IsEmpty() {
buf.ReleaseMulti(p.data)
p.data = nil
}
common.Must(p.done.Close())
}

View File

@@ -3,6 +3,7 @@ package signal
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/sagernet/sing-box/common/xray"
@@ -14,10 +15,12 @@ type ActivityUpdater interface {
}
type ActivityTimer struct {
sync.RWMutex
mu sync.RWMutex
updated chan struct{}
checkTask *task.Periodic
onTimeout func()
consumed atomic.Bool
once sync.Once
}
func (t *ActivityTimer) Update() {
@@ -37,39 +40,39 @@ func (t *ActivityTimer) check() error {
}
func (t *ActivityTimer) finish() {
t.Lock()
defer t.Unlock()
t.once.Do(func() {
t.consumed.Store(true)
t.mu.Lock()
defer t.mu.Unlock()
if t.onTimeout != nil {
common.CloseIfExists(t.checkTask)
t.onTimeout()
t.onTimeout = nil
}
if t.checkTask != nil {
t.checkTask.Close()
t.checkTask = nil
}
})
}
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
if t.consumed.Load() {
return
}
if timeout == 0 {
t.finish()
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,
Execute: t.check,
}
t.Lock()
if t.checkTask != nil {
t.checkTask.Close()
}
t.checkTask = checkTask
t.Unlock()
common.CloseIfExists(t.checkTask)
t.checkTask = newCheckTask
t.Update()
common.Must(checkTask.Start())
common.Must(newCheckTask.Start())
}
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {

View File

@@ -95,6 +95,20 @@ func (c *Client) Start() {
}
}
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
for _, record := range response.Ns {
if soa, isSOA := record.(*dns.SOA); isSOA {
soaTTL := soa.Header().Ttl
soaMinimum := soa.Minttl
if soaTTL < soaMinimum {
return soaTTL, true
}
return soaMinimum, true
}
}
return 0, false
}
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
if len(message.Question) == 0 {
if c.logger != nil {
@@ -214,7 +228,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
response.Answer = append(response.Answer, validResponse.Answer...)
}
}*/
disableCache = disableCache || response.Rcode != dns.RcodeSuccess || len(response.Answer) == 0
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
if responseChecker != nil {
var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses
@@ -251,10 +265,17 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
}
}
var timeToLive uint32
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
if len(response.Answer) == 0 {
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
timeToLive = soaTTL
}
}
if timeToLive == 0 {
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
for _, record := range recordList {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
timeToLive = record.Header().Ttl
}
}
}
}
@@ -332,64 +353,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)
response6, _ := c.loadResponse(dns.Question{
Name: dnsName,
Qtype: dns.TypeAAAA,
Qclass: dns.ClassINET,
}, nil)
if response4 != nil || response6 != nil {
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 {
if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...)

View File

@@ -15,8 +15,7 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
}
responseLen := response.Len()
if responseLen > maxLen {
copyResponse := *response
response = &copyResponse
response = response.Copy()
response.Truncate(maxLen)
}
buffer := buf.NewSize(headroom*2 + 1 + responseLen)

View File

@@ -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()))
var (
response *mDNS.Msg
transport adapter.DNSTransport
err error
)
response, cached := r.client.ExchangeCache(ctx, message)
if !cached {
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
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()
}
}
var metadata *adapter.InboundContext
ctx, metadata = adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
metadata.QueryType = message.Question[0].Qtype
switch metadata.QueryType {
case mDNS.TypeA:
metadata.IPVersion = 4
case mDNS.TypeAAAA:
metadata.IPVersion = 6
}
metadata.Domain = FqdnToDomain(message.Question[0].Name)
if options.Transport != nil {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
options.Strategy = legacyTransport.LegacyStrategy()
}
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 !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
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 {
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) {
var (
responseAddrs []netip.Addr
cached bool
err error
)
printResult := func() {
@@ -347,13 +344,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
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)
ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{}
@@ -386,12 +376,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
return nil, &R.RejectedError{Cause: action.Error(ctx)}
case *R.RuleActionPredefined:
if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode)

View File

@@ -243,6 +243,7 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
defer buffer.Release()
for {
buffer.Reset()
_, _, err := buffer.ReadPacketFrom(packetConn)
if err != nil {
if errors.Is(err, io.ErrShortBuffer) {

View File

@@ -25,7 +25,6 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
sHTTP "github.com/sagernet/sing/protocol/http"
mDNS "github.com/miekg/dns"
@@ -47,7 +46,7 @@ type HTTPSTransport struct {
destination *url.URL
headers http.Header
transportAccess sync.Mutex
transport *http.Transport
transport *HTTPSTransportWrapper
transportResetAt time.Time
}
@@ -62,11 +61,8 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if err != nil {
return nil, err
}
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS))
}
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
}
headers := options.Headers.Build()
host := headers.Get("Host")
@@ -124,37 +120,13 @@ func NewHTTPSRaw(
serverAddr M.Socksaddr,
tlsConfig tls.Config,
) *HTTPSTransport {
var transport *http.Transport
if tlsConfig != nil {
transport = &http.Transport{
ForceAttemptHTTP2: true,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
tcpConn, hErr := dialer.DialContext(ctx, network, serverAddr)
if hErr != nil {
return nil, hErr
}
tlsConn, hErr := aTLS.ClientHandshake(ctx, tcpConn, tlsConfig)
if hErr != nil {
tcpConn.Close()
return nil, hErr
}
return tlsConn, nil
},
}
} else {
transport = &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, network, serverAddr)
},
}
}
return &HTTPSTransport{
TransportAdapter: adapter,
logger: logger,
dialer: dialer,
destination: destination,
headers: headers,
transport: transport,
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
}
}

View File

@@ -0,0 +1,80 @@
package transport
import (
"context"
"errors"
"net"
"net/http"
"sync/atomic"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/net/http2"
)
var errFallback = E.New("fallback to HTTP/1.1")
type HTTPSTransportWrapper struct {
http2Transport *http2.Transport
httpTransport *http.Transport
fallback *atomic.Bool
}
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
var fallback atomic.Bool
return &HTTPSTransportWrapper{
http2Transport: &http2.Transport{
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
if err != nil {
return nil, err
}
state := tlsConn.ConnectionState()
if state.NegotiatedProtocol == http2.NextProtoTLS {
return tlsConn, nil
}
tlsConn.Close()
fallback.Store(true)
return nil, errFallback
},
},
httpTransport: &http.Transport{
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
return dialer.DialTLSContext(ctx, serverAddr)
},
},
fallback: &fallback,
}
}
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
if h.fallback.Load() {
return h.httpTransport.RoundTrip(request)
} else {
response, err := h.http2Transport.RoundTrip(request)
if err != nil {
if errors.Is(err, errFallback) {
return h.httpTransport.RoundTrip(request)
}
return nil, err
}
return response, nil
}
}
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
h.http2Transport.CloseIdleConnections()
h.httpTransport.CloseIdleConnections()
}
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
return &HTTPSTransportWrapper{
httpTransport: h.httpTransport,
http2Transport: &http2.Transport{
DialTLSContext: h.http2Transport.DialTLSContext,
},
fallback: h.fallback,
}
}

View File

@@ -54,18 +54,17 @@ func (t *Transport) Close() error {
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
addresses := t.hosts.Lookup(domain)
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
}
}
systemConfig := getSystemDNSConfig(t.ctx)
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) {
return t.exchangeSingleRequest(ctx, systemConfig, message, domain)
return t.exchangeSingleRequest(ctx, systemConfig, message, question.Name)
} else {
return t.exchangeParallel(ctx, systemConfig, message, domain)
return t.exchangeParallel(ctx, systemConfig, message, question.Name)
}
}

View File

@@ -67,7 +67,6 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
return f.DNSTransport.Exchange(ctx, message)
}
question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
var network string
if question.Qtype == mDNS.TypeA {
@@ -75,7 +74,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
} else {
network = "ip6"
}
addresses, err := f.resolver.LookupNetIP(ctx, network, domain)
addresses, err := f.resolver.LookupNetIP(ctx, network, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -85,7 +84,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
}
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} else if question.Qtype == mDNS.TypeNS {
records, err := f.resolver.LookupNS(ctx, domain)
records, err := f.resolver.LookupNS(ctx, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -114,7 +113,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
}
return response, nil
} else if question.Qtype == mDNS.TypeCNAME {
cname, err := f.resolver.LookupCNAME(ctx, domain)
cname, err := f.resolver.LookupCNAME(ctx, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -142,7 +141,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
},
}, nil
} else if question.Qtype == mDNS.TypeTXT {
records, err := f.resolver.LookupTXT(ctx, domain)
records, err := f.resolver.LookupTXT(ctx, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {
@@ -170,7 +169,7 @@ func (f *FallbackTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*m
},
}, nil
} else if question.Qtype == mDNS.TypeMX {
records, err := f.resolver.LookupMX(ctx, domain)
records, err := f.resolver.LookupMX(ctx, question.Name)
if err != nil {
var dnsError *net.DNSError
if errors.As(err, &dnsError) && dnsError.IsNotFound {

View File

@@ -2,6 +2,64 @@
icon: material/alert-decagram
---
#### 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
* Fixes and improvements
#### 1.12.11
* Fixes and improvements
#### 1.12.10
* Update uTLS to v1.8.1 **1**
* Fixes and improvements
**1**:
This update fixes an critical issue that could cause simulated Chrome fingerprints to be detected,
see https://github.com/refraction-networking/utls/pull/375.
#### 1.12.9
* Fixes and improvements
#### 1.12.8
* Fixes and improvements
@@ -92,7 +150,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
**7**:
@@ -154,7 +213,8 @@ See [Tun](/configuration/inbound/tun/#loopback_address).
We have significantly improved the performance of tun inbound on Apple platforms, especially in the gVisor stack.
The following data was tested using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
The following data was tested
using [tun_bench](https://github.com/SagerNet/sing-box/blob/dev-next/cmd/internal/tun_bench/main.go) on M4 MacBook pro.
| Version | Stack | MTU | Upload | Download |
|-------------|--------|-------|--------|----------|
@@ -173,8 +233,8 @@ The following data was tested using [tun_bench](https://github.com/SagerNet/sing
**18**:
We continue to experience issues updating our sing-box apps on the App Store and Play Store.
Until we rewrite and resubmit the apps, they are considered irrecoverable.
We continue to experience issues updating our sing-box apps on the App Store and Play Store.
Until we rewrite and resubmit the apps, they are considered irrecoverable.
Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15
@@ -455,7 +515,8 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
**2**:
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions, see [Route Action](/configuration/route/rule_action).
`resolve` route action now accepts `disable_cache` and other options like in DNS route actions,
see [Route Action](/configuration/route/rule_action).
**3**:
@@ -486,7 +547,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile.
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches from [MetaCubeX/go](https://github.com/MetaCubeX/go).
For Windows 7 users, legacy binaries now continue to compile with Go 1.23 and patches
from [MetaCubeX/go](https://github.com/MetaCubeX/go).
### 1.11.3

View File

@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
!!! 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
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :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 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)
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
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

View File

@@ -9,6 +9,7 @@
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==",
"managed": false,
"multiplex": {}
}
```
@@ -86,6 +87,10 @@ Both if empty.
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
| 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
See [Multiplex](/configuration/shared/multiplex#inbound) for details.

View File

@@ -9,6 +9,7 @@
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==",
"managed": false,
"multiplex": {}
}
```
@@ -86,6 +87,10 @@ See [Listen Fields](/configuration/shared/listen/) for details.
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
| other methods | 任意字符串 |
#### managed
默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。
#### multiplex
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。

View File

@@ -230,9 +230,18 @@ The path to the server private key, in PEM format.
==Client only==
!!! failure ""
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.
!!! failure "Not Recommended"
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.

View File

@@ -220,9 +220,16 @@ TLS 版本值:
==仅客户端==
!!! failure ""
!!! failure "不推荐"
没有证据表明 GFW 根据 TLS 客户端指纹检测并阻止服务器,并且,使用一个未经安全审查的不完美模拟可能带来安全隐患
uTLS 已被研究人员多次发现其指纹可被识别的漏洞
uTLS 是一个试图通过复制 ClientHello 结构来模仿浏览器 TLS 指纹的 Go 库。
然而,浏览器使用完全不同的 TLS 实现Chrome 使用 BoringSSLFirefox 使用 NSS
其实现行为无法通过简单复制握手格式来复现,其行为细节必然存在差异,使得检测成为可能。
此外,此库缺乏积极维护,且代码质量较差,不建议用于反审查场景。
如需 TLS 指纹抵抗,请改用 [NaiveProxy](/configuration/inbound/naive/)。
uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻力。

View File

@@ -4,8 +4,7 @@ icon: material/horse
# Trojan
Torjan 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.
Trojan is the most commonly used TLS proxy made in China. It can be used in various combinations.
| 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",
"tls": {
"enabled": true,
"server_name": "example.org",
"utls": {
"enabled": true,
"fingerprint": "firefox"
}
"server_name": "example.org"
},
"multiplex": {
"enabled": true
@@ -171,11 +166,7 @@ but only the combination of uTLS and multiplexing is recommended.
"tls": {
"enabled": true,
"server_name": "example.org",
"certificate_path": "/path/to/certificate.pem",
"utls": {
"enabled": true,
"fingerprint": "firefox"
}
"certificate_path": "/path/to/certificate.pem"
},
"multiplex": {
"enabled": true
@@ -198,11 +189,7 @@ but only the combination of uTLS and multiplexing is recommended.
"tls": {
"enabled": true,
"server_name": "example.org",
"insecure": true,
"utls": {
"enabled": true,
"fingerprint": "firefox"
}
"insecure": true
},
"multiplex": {
"enabled": true

View File

@@ -11,16 +11,22 @@ the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohas
![](https://nekohasekai.github.io/sponsor-images/sponsors.svg)
### Special Sponsors
## Commercial Sponsors
**Viral Tech, Inc.**
> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents.
[![](https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png)](https://go.warp.dev/sing-box)
## Special Sponsors
> Viral Tech, Inc.
Helping us re-list sing-box apps on the Apple Store.
---
[![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com)
> [JetBrains](https://www.jetbrains.com)
Free license for the amazing IDEs.
---
[![JetBrains logo](https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg)](https://www.jetbrains.com)

View File

@@ -30,10 +30,23 @@
"jc": 120,
"jmin": 23,
"jmax": 911,
"s1": 1,
"s2": 2,
"s3": 3,
"s4": 4,
"h1": 1,
"h2": 2,
"h3": 3,
"h4": 4
"h4": 4,
"i1": "<b 0xc70000000108...",
"i2": "<b 0xc70000000108...",
"i3": "<b 0xc70000000108...",
"i4": "<b 0xc70000000108...",
"i5": "<b 0xc70000000108...",
"j1": "<b 0xc70000000108...",
"j2": "<b 0xc70000000108...",
"j3": "<b 0xc70000000108...",
"itime": 50,
}
}
],

View File

@@ -27,7 +27,7 @@
},
"profile": {
"detour": "direct",
// for getting existing WARP device profile
// For getting existing WARP device profile, else sing-box will create new profile
"id": "",
"private_key": "",
"auth_token": ""
@@ -56,7 +56,7 @@
"experimental": {
"cache_file": {
"enabled": true,
"store_warp_config": true
"store_warp_config": true // For saving WARP device profiles
}
}
}

View 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
}
}

View File

@@ -48,7 +48,6 @@
"h_keep_alive_period": 60
},
"download": {
"mode": "",
"host": "example.com",
"path": "/xhttp",
"domain_strategy": "prefer_ipv4",

View File

@@ -12,9 +12,7 @@ type iOSPauseFields struct {
func (s *BoxService) Pause() {
s.pauseManager.DevicePause()
if !C.IsIos {
s.instance.Router().ResetNetwork()
} else {
if C.IsIos {
if s.endPauseTimer == nil {
s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake)
} else {
@@ -26,7 +24,6 @@ func (s *BoxService) Pause() {
func (s *BoxService) Wake() {
if !C.IsIos {
s.pauseManager.DeviceWake()
s.instance.Router().ResetNetwork()
}
}

35
go.mod
View File

@@ -1,11 +1,11 @@
module github.com/sagernet/sing-box
go 1.24.1
go 1.24.4
toolchain go1.24.3
toolchain go1.24.6
require (
github.com/anytls/sing-anytls v0.0.8
github.com/anytls/sing-anytls v0.0.11
github.com/caddyserver/certmagic v0.23.0
github.com/coder/websocket v1.8.13
github.com/cretz/bine v0.2.0
@@ -18,29 +18,28 @@ require (
github.com/libdns/alidns v1.0.5-libdns.v1.beta1
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4
github.com/metacubex/utls v1.8.0
github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0
github.com/metacubex/utls v1.8.4
github.com/mholt/acmez/v3 v3.1.2
github.com/miekg/dns v1.1.67
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/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
github.com/sagernet/cors v1.2.1
github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.8
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-beta.1
github.com/sagernet/sing v0.7.10
github.com/sagernet/sing-mux v0.3.3
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
github.com/sagernet/sing v0.7.14
github.com/sagernet/sing-mux v0.3.4
github.com/sagernet/sing-quic v0.5.2
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.7.2
github.com/sagernet/sing-tun v0.7.3
github.com/sagernet/sing-vmess v0.2.7
github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1
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/wireguard-go v0.0.1-beta.7
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.9.1
@@ -54,7 +53,6 @@ require (
golang.org/x/mod v0.27.0
golang.org/x/net v0.43.0
golang.org/x/sys v0.35.0
golang.org/x/time v0.11.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
@@ -67,7 +65,6 @@ require (
github.com/ameshkov/dnsstamps v1.0.3 // indirect
github.com/tidwall/match v1.1.1 // 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/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
@@ -134,14 +131,16 @@ require (
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
github.com/tevino/abool/v2 v2.1.0 // indirect
github.com/tevino/abool v1.2.0 // indirect
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap/exp v0.3.0 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
golang.org/x/term v0.34.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
@@ -149,7 +148,9 @@ require (
lukechampine.com/blake3 v1.4.1 // indirect
)
replace github.com/sagernet/wireguard-go => github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec
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

66
go.sum
View File

@@ -12,8 +12,8 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc=
github.com/anytls/sing-anytls v0.0.8/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
@@ -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/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.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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
@@ -49,8 +48,6 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec h1:jXekDkSozctYj5JQlV1mCsIW+qHpIkgSFhOIpgRSB1I=
github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec/go.mod h1:akc2Wh+rX9bFFNnHJGsQ8VIV3eJI1LXJYgx2Y+8lcW8=
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
@@ -130,10 +127,10 @@ github.com/mdlayher/sdnotify v1.0.0 h1:Ma9XeLVN/l0qpyx1tNeMSeTjCPH6NtuD6/N9XdTlQ
github.com/mdlayher/sdnotify v1.0.0/go.mod h1:HQUmpM4XgYkhDLtd+Uad8ZFK1T9D5+pNxnXQjCeJlGE=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.0 h1:mSYi6FMnmc5riARl5UZDmWVy710z+P5b7xuGW0lV9ac=
github.com/metacubex/utls v1.8.0/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ=
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/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
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/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
@@ -153,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/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/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/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
@@ -174,46 +169,40 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs=
github.com/sagernet/quic-go v0.52.0-beta.1/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.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.3 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA=
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
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/sing v0.7.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
github.com/sagernet/sing v0.7.14/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.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
github.com/sagernet/sing-quic v0.5.2 h1:I3vlfRImhr0uLwRS3b3ib70RMG9FcXtOKKUDz3eKRWc=
github.com/sagernet/sing-quic v0.5.2/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
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-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
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/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.7.2 h1:uJkAZM0KBqIYzrq077QGqdvj/+4i/pMOx6Pnx0jYqAs=
github.com/sagernet/sing-tun v0.7.2/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
github.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmVes=
github.com/sagernet/sing-tun v0.7.3/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
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/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4=
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1 h1:gMC0q+0VvZBotZMZ9G0R8ZMEIT/Q6KnXbw0/OgMjmdk=
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
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/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/tailscale v1.80.3-sing-box-1.12-mod.2-extended-1.0.0 h1:Yp4dIRwiwLda9JXyGMHkfYRr2r01NarkzsNd/oi10dk=
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/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
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.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.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/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
@@ -234,8 +223,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c=
github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY=
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -267,10 +256,10 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
@@ -304,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.1.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/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -340,6 +328,8 @@ gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 h1:ze1vwAdliUAr68RQ5NtufWaXaOg8WUO2OACzEV+TNdE=
gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=

View File

@@ -5,6 +5,7 @@ import (
"context"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
)
@@ -60,37 +61,40 @@ func checkOptions(options *Options) error {
func checkInbounds(inbounds []Inbound) error {
seen := make(map[string]bool)
for _, inbound := range inbounds {
if inbound.Tag == "" {
continue
for i, inbound := range inbounds {
tag := inbound.Tag
if tag == "" {
tag = F.ToString(i)
}
if seen[inbound.Tag] {
return E.New("duplicate inbound tag: ", inbound.Tag)
if seen[tag] {
return E.New("duplicate inbound tag: ", tag)
}
seen[inbound.Tag] = true
seen[tag] = true
}
return nil
}
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
seen := make(map[string]bool)
for _, outbound := range outbounds {
if outbound.Tag == "" {
continue
for i, outbound := range outbounds {
tag := outbound.Tag
if tag == "" {
tag = F.ToString(i)
}
if seen[outbound.Tag] {
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
if seen[tag] {
return E.New("duplicate outbound/endpoint tag: ", tag)
}
seen[outbound.Tag] = true
seen[tag] = true
}
for _, endpoint := range endpoints {
if endpoint.Tag == "" {
continue
for i, endpoint := range endpoints {
tag := endpoint.Tag
if tag == "" {
tag = F.ToString(i)
}
if seen[endpoint.Tag] {
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
if seen[tag] {
return E.New("duplicate outbound/endpoint tag: ", tag)
}
seen[endpoint.Tag] = true
seen[tag] = true
}
return nil
}

View File

@@ -110,7 +110,6 @@ type V2RayHTTPUpgradeOptions struct {
}
type V2RayXHTTPBaseOptions struct {
Mode string `json:"mode"`
Host string `json:"host,omitempty"`
Path string `json:"path,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
@@ -126,6 +125,7 @@ type V2RayXHTTPBaseOptions struct {
}
type V2RayXHTTPOptions struct {
Mode string `json:"mode"`
V2RayXHTTPBaseOptions
Download *V2RayXHTTPDownloadOptions `json:"download"`
}

View File

@@ -7,16 +7,18 @@ import (
)
type WireGuardEndpointOptions struct {
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address"`
PrivateKey string `json:"private_key"`
ListenPort uint16 `json:"listen_port,omitempty"`
Peers []WireGuardPeer `json:"peers,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
Workers int `json:"workers,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Address badoption.Listable[netip.Prefix] `json:"address"`
PrivateKey string `json:"private_key"`
ListenPort uint16 `json:"listen_port,omitempty"`
Peers []WireGuardPeer `json:"peers,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
DialerOptions
}
@@ -31,17 +33,19 @@ type WireGuardPeer struct {
}
type WireGuardWARPEndpointOptions struct {
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
Workers int `json:"workers,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
Profile *WireGuardCloudflareProfile `json:"profile,omitempty"`
System bool `json:"system,omitempty"`
Name string `json:"name,omitempty"`
ListenPort uint16 `json:"listen_port,omitempty"`
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
Profile WARPProfile `json:"profile,omitempty"`
DialerOptions
}
type WireGuardCloudflareProfile struct {
type WARPProfile struct {
ID string `json:"id,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
AuthToken string `json:"auth_token,omitempty"`
@@ -58,13 +62,15 @@ type LegacyWireGuardOutboundOptions struct {
PrivateKey string `json:"private_key"`
Peers []LegacyWireGuardPeer `json:"peers,omitempty"`
ServerOptions
PeerPublicKey string `json:"peer_public_key"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Network NetworkList `json:"network,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
PeerPublicKey string `json:"peer_public_key"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
PreallocatedBuffersPerPool uint32 `json:"preallocated_buffers_per_pool,omitempty"`
DisablePauses bool `json:"disable_pauses,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Network NetworkList `json:"network,omitempty"`
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
}
type LegacyWireGuardPeer struct {
@@ -76,13 +82,24 @@ type LegacyWireGuardPeer struct {
}
type WireGuardAmnezia struct {
JC int `json:"jc,omitempty"`
JMin int `json:"jmin,omitempty"`
JMax int `json:"jmax,omitempty"`
S1 int `json:"s1,omitempty"`
S2 int `json:"s2,omitempty"`
H1 uint32 `json:"h1,omitempty"`
H2 uint32 `json:"h2,omitempty"`
H3 uint32 `json:"h3,omitempty"`
H4 uint32 `json:"h4,omitempty"`
JC int `json:"jc,omitempty"`
JMin int `json:"jmin,omitempty"`
JMax int `json:"jmax,omitempty"`
S1 int `json:"s1,omitempty"`
S2 int `json:"s2,omitempty"`
S3 int `json:"s3,omitempty"`
S4 int `json:"s4,omitempty"`
H1 uint32 `json:"h1,omitempty"`
H2 uint32 `json:"h2,omitempty"`
H3 uint32 `json:"h3,omitempty"`
H4 uint32 `json:"h4,omitempty"`
I1 string `json:"i1,omitempty"`
I2 string `json:"i2,omitempty"`
I3 string `json:"i3,omitempty"`
I4 string `json:"i4,omitempty"`
I5 string `json:"i5,omitempty"`
J1 string `json:"j1,omitempty"`
J2 string `json:"j2,omitempty"`
J3 string `json:"j3,omitempty"`
ITime int64 `json:"itime,omitempty"`
}

View File

@@ -13,6 +13,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot"
@@ -43,6 +44,13 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired
}
// TCP Fast Open is incompatible with anytls because TFO creates a lazy connection
// that only establishes on first write. The lazy connection returns an empty address
// before establishment, but anytls SOCKS wrapper tries to access the remote address
// during handshake, causing a null pointer dereference crash.
if options.DialerOptions.TCPFastOpen {
return nil, E.New("tcp_fast_open is not supported with anytls outbound")
}
tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {

View File

@@ -46,7 +46,8 @@ func HandleStreamDNSRequest(ctx context.Context, router adapter.DNSRouter, conn
conn.Close()
return err
}
responseBuffer := buf.NewPacket()
responseLength := response.Len()
responseBuffer := buf.NewSize(3 + responseLength)
defer responseBuffer.Release()
responseBuffer.Resize(2, 0)
n, err := response.PackBuffer(responseBuffer.FreeBytes())

View File

@@ -2,8 +2,8 @@ package naive
import (
"context"
"errors"
"io"
"math/rand"
"net"
"net/http"
@@ -22,7 +22,11 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
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)
@@ -82,16 +86,11 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
var tlsConfig *tls.STDConfig
if n.tlsConfig != nil {
err := n.tlsConfig.Start()
if err != nil {
return E.Cause(err, "create TLS config")
}
tlsConfig, err = n.tlsConfig.Config()
if err != nil {
return err
}
}
if common.Contains(n.network, N.NetworkTCP) {
tcpListener, err := n.listener.ListenTCP()
@@ -99,20 +98,23 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
return err
}
n.httpServer = &http.Server{
Handler: n,
TLSConfig: tlsConfig,
Handler: h2c.NewHandler(n, &http2.Server{}),
BaseContext: func(listener net.Listener) context.Context {
return n.ctx
},
}
go func() {
var sErr error
if tlsConfig != nil {
sErr = n.httpServer.ServeTLS(tcpListener, "", "")
} else {
sErr = n.httpServer.Serve(tcpListener)
listener := net.Listener(tcpListener)
if n.tlsConfig != nil {
if len(n.tlsConfig.NextProtos()) == 0 {
n.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
} 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)
}
}()
@@ -161,13 +163,16 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
n.badRequest(ctx, request, E.New("authorization failed"))
return
}
writer.Header().Set("Padding", generateNaivePaddingHeader())
writer.Header().Set("Padding", generatePaddingHeader())
writer.WriteHeader(http.StatusOK)
writer.(http.Flusher).Flush()
hostPort := request.URL.Host
hostPort := request.Header.Get("-connect-authority")
if hostPort == "" {
hostPort = request.Host
hostPort = request.URL.Host
if hostPort == "" {
hostPort = request.Host
}
}
source := sHttp.SourceAddress(request)
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"))
return
}
n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination)
n.newConnection(ctx, false, &naiveConn{Conn: conn}, userName, source, destination)
} 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()
}
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)
}

View File

@@ -7,417 +7,242 @@ import (
"net"
"net/http"
"os"
"strings"
"time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/baderror"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/rw"
)
const kFirstPaddings = 8
const paddingCount = 8
type naiveH1Conn struct {
net.Conn
func generatePaddingHeader() string {
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
writePadding int
readRemaining int
paddingRemaining int
}
func (c *naiveH1Conn) Read(p []byte) (n int, err error) {
n, err = c.read(p)
return n, wrapHttpError(err)
}
func (c *naiveH1Conn) read(p []byte) (n int, err error) {
if c.readRemaining > 0 {
if len(p) > c.readRemaining {
p = p[:c.readRemaining]
func (p *paddingConn) readWithPadding(reader io.Reader, buffer []byte) (n int, err error) {
if p.readRemaining > 0 {
if len(buffer) > p.readRemaining {
buffer = buffer[:p.readRemaining]
}
n, err = c.Conn.Read(p)
n, err = reader.Read(buffer)
if err != nil {
return
}
c.readRemaining -= n
p.readRemaining -= n
return
}
if c.paddingRemaining > 0 {
err = rw.SkipN(c.Conn, c.paddingRemaining)
if p.paddingRemaining > 0 {
err = rw.SkipN(reader, p.paddingRemaining)
if err != nil {
return
}
c.paddingRemaining = 0
p.paddingRemaining = 0
}
if c.readPadding < kFirstPaddings {
var paddingHdr []byte
if len(p) >= 3 {
paddingHdr = p[:3]
if p.readPadding < paddingCount {
var paddingHeader []byte
if len(buffer) >= 3 {
paddingHeader = buffer[:3]
} else {
paddingHdr = make([]byte, 3)
paddingHeader = make([]byte, 3)
}
_, err = io.ReadFull(c.Conn, paddingHdr)
_, err = io.ReadFull(reader, paddingHeader)
if err != nil {
return
}
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
paddingSize := int(paddingHdr[2])
if len(p) > originalDataSize {
p = p[:originalDataSize]
originalDataSize := int(binary.BigEndian.Uint16(paddingHeader[:2]))
paddingSize := int(paddingHeader[2])
if len(buffer) > originalDataSize {
buffer = buffer[:originalDataSize]
}
n, err = c.Conn.Read(p)
n, err = reader.Read(buffer)
if err != nil {
return
}
c.readPadding++
c.readRemaining = originalDataSize - n
c.paddingRemaining = paddingSize
p.readPadding++
p.readRemaining = originalDataSize - n
p.paddingRemaining = paddingSize
return
}
return c.Conn.Read(p)
return reader.Read(buffer)
}
func (c *naiveH1Conn) Write(p []byte) (n int, err error) {
for pLen := len(p); pLen > 0; {
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 {
func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, err error) {
if p.writePadding < paddingCount {
paddingSize := rand.Intn(256)
buffer := buf.NewSize(3 + len(p) + paddingSize)
buffer := buf.NewSize(3 + len(data) + paddingSize)
defer buffer.Release()
header := buffer.Extend(3)
binary.BigEndian.PutUint16(header, uint16(len(p)))
binary.BigEndian.PutUint16(header, uint16(len(data)))
header[2] = byte(paddingSize)
common.Must1(buffer.Write(p))
_, err = c.Conn.Write(buffer.Bytes())
common.Must1(buffer.Write(data))
_, err = writer.Write(buffer.Bytes())
if err == nil {
n = len(p)
n = len(data)
}
c.writePadding++
p.writePadding++
return
}
return c.Conn.Write(p)
return writer.Write(data)
}
func (c *naiveH1Conn) FrontHeadroom() int {
if c.writePadding < kFirstPaddings {
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 {
func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffer) error {
if p.writePadding < paddingCount {
bufferLen := buffer.Len()
if bufferLen > 65535 {
return common.Error(c.Write(buffer.Bytes()))
_, err := p.writeChunked(writer, buffer.Bytes())
return err
}
paddingSize := rand.Intn(256)
header := buffer.ExtendHeader(3)
binary.BigEndian.PutUint16(header, uint16(bufferLen))
header[2] = byte(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 (c *naiveH1Conn) 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.Conn)
}
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]
func (p *paddingConn) writeChunked(writer io.Writer, data []byte) (n int, err error) {
for len(data) > 0 {
var chunk []byte
if len(data) > 65535 {
chunk = data[:65535]
data = data[65535:]
} 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 {
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) {
for pLen := len(p); pLen > 0; {
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 {
func (p *paddingConn) frontHeadroom() int {
if p.writePadding < paddingCount {
return 3
}
return 0
}
func (c *naiveH2Conn) RearHeadroom() int {
if c.writePadding < kFirstPaddings {
func (p *paddingConn) rearHeadroom() int {
if p.writePadding < paddingCount {
return 255
}
return 0
}
func (c *naiveH2Conn) WriterMTU() int {
if c.writePadding < kFirstPaddings {
func (p *paddingConn) writerMTU() int {
if p.writePadding < paddingCount {
return 65535
}
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 {
defer buffer.Release()
if c.writePadding < kFirstPaddings {
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()))
err := c.writeBufferWithPadding(c.writer, buffer)
if err == nil {
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 {
return common.Close(
c.reader,
c.writer,
)
return common.Close(c.reader, c.writer)
}
func (c *naiveH2Conn) LocalAddr() net.Addr {
return M.Socksaddr{}
}
func (c *naiveH2Conn) RemoteAddr() net.Addr {
return c.rAddr
}
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) 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
}
func (c *naiveH2Conn) LocalAddr() net.Addr { 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) 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) FrontHeadroom() int { return c.frontHeadroom() }
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) WriterReplaceable() bool { return c.writerReplaceable() }

View File

@@ -38,6 +38,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/tailscale/ipn"
@@ -158,6 +159,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
},
TLSClientConfig: &tls.Config{
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)
}
addr := tcpip.FullAddress{
addr4, addr6 := t.server.TailscaleIPs()
remoteAddr := tcpip.FullAddress{
NIC: 1,
Port: destination.Port,
Addr: addressFromAddr(destination.Addr),
}
var localAddr tcpip.FullAddress
var networkProtocol tcpip.NetworkProtocolNumber
if destination.IsIPv4() {
if !addr4.IsValid() {
return nil, E.New("missing Tailscale IPv4 address")
}
networkProtocol = header.IPv4ProtocolNumber
localAddr = tcpip.FullAddress{
NIC: 1,
Addr: addressFromAddr(addr4),
}
} else {
if !addr6.IsValid() {
return nil, E.New("missing Tailscale IPv6 address")
}
networkProtocol = header.IPv6ProtocolNumber
localAddr = tcpip.FullAddress{
NIC: 1,
Addr: addressFromAddr(addr6),
}
}
switch N.NetworkName(network) {
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 {
return nil, err
}
return tcpConn, nil
case N.NetworkUDP:
udpConn, err := gonet.DialUDP(t.stack, nil, &addr, networkProtocol)
udpConn, err := gonet.DialUDP(t.stack, &localAddr, &remoteAddr, networkProtocol)
if err != nil {
return nil, err
}
@@ -456,20 +474,20 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
metadata.Inbound = t.Tag()
metadata.InboundType = t.Type()
metadata.Source = source
metadata.Destination = destination
addr4, addr6 := t.server.TailscaleIPs()
switch destination.Addr {
case addr4:
metadata.OriginDestination = destination
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:
metadata.OriginDestination = destination
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 to ", destination)
t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}

View File

@@ -68,15 +68,26 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
var amnezia *wireguard.AmneziaOptions
if options.Amnezia != nil {
amnezia = &wireguard.AmneziaOptions{
JC: options.Amnezia.JC,
JMin: options.Amnezia.JMin,
JMax: options.Amnezia.JMax,
S1: options.Amnezia.S1,
S2: options.Amnezia.S2,
H1: options.Amnezia.H1,
H2: options.Amnezia.H2,
H3: options.Amnezia.H3,
H4: options.Amnezia.H4,
JC: options.Amnezia.JC,
JMin: options.Amnezia.JMin,
JMax: options.Amnezia.JMax,
S1: options.Amnezia.S1,
S2: options.Amnezia.S2,
S3: options.Amnezia.S3,
S4: options.Amnezia.S4,
H1: options.Amnezia.H1,
H2: options.Amnezia.H2,
H3: options.Amnezia.H3,
H4: options.Amnezia.H4,
I1: options.Amnezia.I1,
I2: options.Amnezia.I2,
I3: options.Amnezia.I3,
I4: options.Amnezia.I4,
I5: options.Amnezia.I5,
J1: options.Amnezia.J1,
J2: options.Amnezia.J2,
J3: options.Amnezia.J3,
ITime: options.Amnezia.ITime,
}
}
wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{
@@ -113,8 +124,10 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
Reserved: it.Reserved,
}
}),
Workers: options.Workers,
Amnezia: amnezia,
Workers: options.Workers,
PreallocatedBuffersPerPool: options.PreallocatedBuffersPerPool,
DisablePauses: options.DisablePauses,
Amnezia: amnezia,
})
if err != nil {
return nil, err

View File

@@ -16,6 +16,7 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
M "github.com/sagernet/sing/common/metadata"
@@ -41,10 +42,8 @@ func NewWARPEndpoint(ctx context.Context, router adapter.Router, logger log.Cont
if options.Detour != "" {
dependencies = append(dependencies, options.Detour)
}
if options.Profile != nil {
if options.Profile.Detour != "" {
dependencies = append(dependencies, options.Profile.Detour)
}
if options.Profile.Detour != "" {
dependencies = append(dependencies, options.Profile.Detour)
}
warpEndpoint := &WARPEndpoint{
Adapter: endpoint.NewAdapter(C.TypeWARP, tag, []string{N.NetworkTCP, N.NetworkUDP}, dependencies),
@@ -80,17 +79,15 @@ func NewWARPEndpoint(ctx context.Context, router adapter.Router, logger log.Cont
}
}
opts := make([]cloudflare.CloudflareApiOption, 0, 1)
if options.Profile != nil {
if options.Profile.Detour != "" {
detour, ok := service.FromContext[adapter.OutboundManager](ctx).Outbound(options.Profile.Detour)
if !ok {
logger.ErrorContext(ctx, E.New("outbound detour not found: ", options.Profile.Detour))
return
}
opts = append(opts, cloudflare.WithDialContext(func(ctx context.Context, network, addr string) (net.Conn, error) {
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
}))
if options.Profile.Detour != "" {
detour, ok := service.FromContext[adapter.OutboundManager](ctx).Outbound(options.Profile.Detour)
if !ok {
logger.ErrorContext(ctx, E.New("outbound detour not found: ", options.Profile.Detour))
return
}
opts = append(opts, cloudflare.WithDialContext(func(ctx context.Context, network, addr string) (net.Conn, error) {
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
}))
}
api := cloudflare.NewCloudflareApi(opts...)
var profile *cloudflare.CloudflareProfile
@@ -133,13 +130,15 @@ func NewWARPEndpoint(ctx context.Context, router adapter.Router, logger log.Cont
logger,
tag,
option.WireGuardEndpointOptions{
System: options.System,
Name: options.Name,
ListenPort: options.ListenPort,
UDPTimeout: options.UDPTimeout,
Workers: options.Workers,
Amnezia: options.Amnezia,
DialerOptions: options.DialerOptions,
System: options.System,
Name: options.Name,
ListenPort: options.ListenPort,
UDPTimeout: options.UDPTimeout,
Workers: options.Workers,
PreallocatedBuffersPerPool: options.PreallocatedBuffersPerPool,
DisablePauses: options.DisablePauses,
Amnezia: options.Amnezia,
DialerOptions: options.DialerOptions,
Address: badoption.Listable[netip.Prefix]{
netip.MustParsePrefix(config.Interface.Addresses.V4 + "/32"),
@@ -185,10 +184,7 @@ func (w *WARPEndpoint) Start(stage adapter.StartStage) error {
}
func (w *WARPEndpoint) Close() error {
if ok := w.isEndpointInitialized(); !ok {
return E.New("endpoint not initialized")
}
return w.endpoint.Close()
return common.Close(w.endpoint)
}
func (w *WARPEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {

View File

@@ -82,15 +82,26 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
var amnezia *wireguard.AmneziaOptions
if options.Amnezia != nil {
amnezia = &wireguard.AmneziaOptions{
JC: options.Amnezia.JC,
JMin: options.Amnezia.JMin,
JMax: options.Amnezia.JMax,
S1: options.Amnezia.S1,
S2: options.Amnezia.S2,
H1: options.Amnezia.H1,
H2: options.Amnezia.H2,
H3: options.Amnezia.H3,
H4: options.Amnezia.H4,
JC: options.Amnezia.JC,
JMin: options.Amnezia.JMin,
JMax: options.Amnezia.JMax,
S1: options.Amnezia.S1,
S2: options.Amnezia.S2,
S3: options.Amnezia.S3,
S4: options.Amnezia.S4,
H1: options.Amnezia.H1,
H2: options.Amnezia.H2,
H3: options.Amnezia.H3,
H4: options.Amnezia.H4,
I1: options.Amnezia.I1,
I2: options.Amnezia.I2,
I3: options.Amnezia.I3,
I4: options.Amnezia.I4,
I5: options.Amnezia.I5,
J1: options.Amnezia.J1,
J2: options.Amnezia.J2,
J3: options.Amnezia.J3,
ITime: options.Amnezia.ITime,
}
}
wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{
@@ -114,9 +125,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
}
return endpointAddresses[0], nil
},
Peers: peers,
Workers: options.Workers,
Amnezia: amnezia,
Peers: peers,
Workers: options.Workers,
PreallocatedBuffersPerPool: options.PreallocatedBuffersPerPool,
DisablePauses: options.DisablePauses,
Amnezia: amnezia,
})
if err != nil {
return nil, err

View File

@@ -303,7 +303,7 @@ func (m *ConnectionManager) connectionCopy(ctx context.Context, source net.Conn,
} else {
if err == nil {
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)
} else {
m.logger.TraceContext(ctx, "connection download closed")

View File

@@ -3,6 +3,7 @@ package derp
import (
"bufio"
"context"
stdTLS "crypto/tls"
"encoding/json"
"fmt"
"io"
@@ -31,6 +32,7 @@ import (
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
@@ -159,6 +161,10 @@ func (d *Service) Start(stage adapter.StartStage) error {
httpClients = append(httpClients, &http.Client{
Transport: &http.Transport{
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) {
return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
},

View File

@@ -49,6 +49,9 @@ func (s *Service) loadCache() error {
os.RemoveAll(basePath)
return err
}
s.cacheMutex.Lock()
s.lastSavedCache = cacheBinary
s.cacheMutex.Unlock()
return nil
}
@@ -56,16 +59,30 @@ func (s *Service) saveCache() error {
if s.cachePath == "" {
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)
err := os.MkdirAll(filepath.Dir(basePath), 0o777)
if err != nil {
return err
}
cacheBinary, err := s.encodeCache()
err = os.WriteFile(basePath, cacheBinary, 0o644)
if err != nil {
return err
}
return os.WriteFile(s.cachePath, cacheBinary, 0o644)
s.lastSavedCache = cacheBinary
return nil
}
func (s *Service) decodeCache(cacheBinary []byte) error {

View File

@@ -4,6 +4,8 @@ import (
"context"
"errors"
"net/http"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
@@ -28,21 +30,27 @@ func RegisterService(registry *boxService.Registry) {
type Service struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
httpServer *http.Server
traffics map[string]*TrafficManager
users map[string]*UserManager
cachePath string
ctx context.Context
cancel context.CancelFunc
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
httpServer *http.Server
traffics map[string]*TrafficManager
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) {
ctx, cancel := context.WithCancel(ctx)
chiRouter := chi.NewRouter()
s := &Service{
Adapter: boxService.NewAdapter(C.TypeSSMAPI, tag),
ctx: ctx,
cancel: cancel,
logger: logger,
listener: listener.New(listener.Options{
Context: ctx,
@@ -95,6 +103,8 @@ func (s *Service) Start(stage adapter.StartStage) error {
if err != nil {
s.logger.Error(E.Cause(err, "load cache"))
}
s.saveTicker = time.NewTicker(1 * time.Minute)
go s.loopSaveCache()
if s.tlsConfig != nil {
err = s.tlsConfig.Start()
if err != nil {
@@ -120,7 +130,27 @@ func (s *Service) Start(stage adapter.StartStage) error {
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 {
if s.cancel != nil {
s.cancel()
}
if s.saveTicker != nil {
s.saveTicker.Stop()
}
err := s.saveCache()
if err != nil {
s.logger.Error(E.Cause(err, "save cache"))

View File

@@ -14,11 +14,13 @@ type StreamWrapper struct {
func (s *StreamWrapper) Read(p []byte) (n int, err error) {
n, err = s.Stream.Read(p)
//nolint:staticcheck
return n, baderror.WrapQUIC(err)
}
func (s *StreamWrapper) Write(p []byte) (n int, err error) {
n, err = s.Stream.Write(p)
//nolint:staticcheck
return n, baderror.WrapQUIC(err)
}

View File

@@ -291,7 +291,7 @@ func wrapWsError(err error) error {
}
var closedErr wsutil.ClosedError
if errors.As(err, &closedErr) {
if closedErr.Code == ws.StatusNormalClosure {
if closedErr.Code == ws.StatusNormalClosure || closedErr.Code == ws.StatusNoStatusRcvd {
err = io.EOF
}
}

View File

@@ -13,8 +13,8 @@ import (
"sync/atomic"
"time"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
"github.com/sagernet/quic-go"
"github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/tls"
"github.com/sagernet/sing-box/common/xray/buf"
@@ -23,6 +23,7 @@ import (
"github.com/sagernet/sing-box/common/xray/signal/done"
"github.com/sagernet/sing-box/common/xray/uuid"
"github.com/sagernet/sing-box/option"
qtls "github.com/sagernet/sing-quic"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
@@ -47,14 +48,6 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
return nil, E.New("mode is not set")
}
dest := serverAddr
var gotlsConfig *gotls.Config
if tlsConfig != nil {
var err error
gotlsConfig, err = tlsConfig.Config()
if err != nil {
return nil, err
}
}
baseRequestURL, err := getBaseRequestURL(
&options.V2RayXHTTPBaseOptions, dest, tlsConfig,
)
@@ -71,7 +64,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
xmuxOptions = *options.Xmux
}
xmuxManager := NewXmuxManager(xmuxOptions, func() XmuxConn {
return createHTTPClient(dest, dialer, &options.V2RayXHTTPBaseOptions, tlsConfig, gotlsConfig)
return createHTTPClient(dest, dialer, &options.V2RayXHTTPBaseOptions, tlsConfig)
})
getHTTPClient := func() (DialerClient, *XmuxClient) {
xmuxClient := xmuxManager.GetXmuxClient(ctx)
@@ -91,16 +84,11 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
}
dest2 := options2.ServerOptions.Build()
var tlsConfig2 tls.Config
var gotlsConfig2 *gotls.Config
if options2.TLS != nil {
tlsConfig2, err = tls.NewClient(ctx, options2.Server, common.PtrValueOrDefault(options2.TLS))
if err != nil {
return nil, err
}
gotlsConfig2, err = tlsConfig2.Config()
if err != nil {
return nil, err
}
}
baseRequestURL2, err := getBaseRequestURL(&options2.V2RayXHTTPBaseOptions, dest2, tlsConfig2)
if err != nil {
@@ -116,7 +104,7 @@ func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, opt
xmuxOptions2 = *options2.Xmux
}
xmuxManager2 := NewXmuxManager(xmuxOptions2, func() XmuxConn {
return createHTTPClient(dest2, dialer2, &options2.V2RayXHTTPBaseOptions, tlsConfig2, gotlsConfig2)
return createHTTPClient(dest2, dialer2, &options2.V2RayXHTTPBaseOptions, tlsConfig2)
})
getHTTPClient2 = func() (DialerClient, *XmuxClient) {
xmuxClient2 := xmuxManager2.GetXmuxClient(ctx)
@@ -138,7 +126,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
mode := c.options.Mode
sessionIdUuid := uuid.New()
requestURL := c.getRequestURL(sessionIdUuid.String())
requestURL2 := c.getRequestURL(sessionIdUuid.String())
requestURL2 := c.getRequestURL2(sessionIdUuid.String())
httpClient, xmuxClient := c.getHTTPClient()
httpClient2, xmuxClient2 := c.getHTTPClient2()
if xmuxClient != nil {
@@ -262,11 +250,11 @@ func (c *Client) Close() error {
return nil
}
func decideHTTPVersion(gotlsConfig *gotls.Config) string {
if gotlsConfig == nil || len(gotlsConfig.NextProtos) == 0 || gotlsConfig.NextProtos[0] == "http/1.1" {
func decideHTTPVersion(tlsConfig tls.Config) string {
if tlsConfig == nil || len(tlsConfig.NextProtos()) == 0 || tlsConfig.NextProtos()[0] == "http/1.1" {
return "1.1"
}
if gotlsConfig.NextProtos[0] == "h3" {
if tlsConfig.NextProtos()[0] == "h3" {
return "3"
}
return "2"
@@ -298,8 +286,8 @@ func getBaseRequestURL(options *option.V2RayXHTTPBaseOptions, dest M.Socksaddr,
return requestURL, nil
}
func createHTTPClient(dest M.Socksaddr, dialer N.Dialer, options *option.V2RayXHTTPBaseOptions, tlsConfig tls.Config, gotlsConfig *gotls.Config) DialerClient {
httpVersion := decideHTTPVersion(gotlsConfig)
func createHTTPClient(dest M.Socksaddr, dialer N.Dialer, options *option.V2RayXHTTPBaseOptions, tlsConfig tls.Config) DialerClient {
httpVersion := decideHTTPVersion(tlsConfig)
dialContext := func(ctxInner context.Context) (net.Conn, error) {
conn, err := dialer.DialContext(ctxInner, "tcp", dest)
if err != nil {
@@ -332,14 +320,13 @@ func createHTTPClient(dest M.Socksaddr, dialer N.Dialer, options *option.V2RayXH
KeepAlivePeriod: keepAlivePeriod,
}
transport = &http3.Transport{
QUICConfig: quicConfig,
TLSClientConfig: gotlsConfig.Clone(),
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (*quic.Conn, error) {
QUICConfig: quicConfig,
Dial: func(ctx context.Context, addr string, tlsCfg *gotls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, dest)
if dErr != nil {
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":

View File

@@ -1,6 +1,7 @@
package xhttp
import (
common "github.com/sagernet/sing-box/common/xray"
"github.com/sagernet/sing-box/common/xray/buf"
"github.com/sagernet/sing-box/common/xray/pipe"
)
@@ -24,15 +25,17 @@ func (w uploadWriter) Write(b []byte) (int, error) {
b = b[:capacity]
}
*/
buffer := buf.New()
n, err := buffer.Write(b)
if err != nil {
return 0, err
}
err = w.WriteMultiBuffer([]*buf.Buffer{buffer})
if err != nil {
return 0, err
buffer := buf.MultiBufferContainer{}
common.Must2(buffer.Write(b))
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
}

View File

@@ -177,19 +177,70 @@ func (e *Endpoint) Start(resolve bool) error {
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)
ipcConf := e.ipcConf
if e.options.Amnezia != nil {
ipcConf += "\njc=" + strconv.Itoa(e.options.Amnezia.JC) + "\n"
ipcConf += "jmin=" + strconv.Itoa(e.options.Amnezia.JMin) + "\n"
ipcConf += "jmax=" + strconv.Itoa(e.options.Amnezia.JMax) + "\n"
ipcConf += "s1=" + strconv.Itoa(e.options.Amnezia.S1) + "\n"
ipcConf += "s2=" + strconv.Itoa(e.options.Amnezia.S2) + "\n"
ipcConf += "h1=" + strconv.FormatUint(uint64(e.options.Amnezia.H1), 10) + "\n"
ipcConf += "h2=" + strconv.FormatUint(uint64(e.options.Amnezia.H2), 10) + "\n"
ipcConf += "h3=" + strconv.FormatUint(uint64(e.options.Amnezia.H3), 10) + "\n"
ipcConf += "h4=" + strconv.FormatUint(uint64(e.options.Amnezia.H4), 10)
if e.options.Amnezia.JC > 0 {
ipcConf += "\njc=" + strconv.Itoa(e.options.Amnezia.JC)
}
if e.options.Amnezia.JMin > 0 {
ipcConf += "\njmin=" + strconv.Itoa(e.options.Amnezia.JMin)
}
if e.options.Amnezia.JMax > 0 {
ipcConf += "\njmax=" + strconv.Itoa(e.options.Amnezia.JMax)
}
if e.options.Amnezia.S1 > 0 {
ipcConf += "\ns1=" + strconv.Itoa(e.options.Amnezia.S1)
}
if e.options.Amnezia.S2 > 0 {
ipcConf += "\ns2=" + strconv.Itoa(e.options.Amnezia.S2)
}
if e.options.Amnezia.S3 > 0 {
ipcConf += "\ns3=" + strconv.Itoa(e.options.Amnezia.S3)
}
if e.options.Amnezia.S4 > 0 {
ipcConf += "\ns4=" + strconv.Itoa(e.options.Amnezia.S4)
}
if e.options.Amnezia.H1 > 0 {
ipcConf += "\nh1=" + strconv.FormatUint(uint64(e.options.Amnezia.H1), 10)
}
if e.options.Amnezia.H2 > 0 {
ipcConf += "\nh2=" + strconv.FormatUint(uint64(e.options.Amnezia.H2), 10)
}
if e.options.Amnezia.H3 > 0 {
ipcConf += "\nh3=" + strconv.FormatUint(uint64(e.options.Amnezia.H3), 10)
}
if e.options.Amnezia.H4 > 0 {
ipcConf += "\nh4=" + strconv.FormatUint(uint64(e.options.Amnezia.H4), 10)
}
if e.options.Amnezia.I1 != "" {
ipcConf += "\ni1=" + e.options.Amnezia.I1
}
if e.options.Amnezia.I2 != "" {
ipcConf += "\ni2=" + e.options.Amnezia.I2
}
if e.options.Amnezia.I3 != "" {
ipcConf += "\ni3=" + e.options.Amnezia.I3
}
if e.options.Amnezia.I4 != "" {
ipcConf += "\ni4=" + e.options.Amnezia.I4
}
if e.options.Amnezia.I5 != "" {
ipcConf += "\ni5=" + e.options.Amnezia.I5
}
if e.options.Amnezia.J1 != "" {
ipcConf += "\nj1=" + e.options.Amnezia.J1
}
if e.options.Amnezia.J2 != "" {
ipcConf += "\nj2=" + e.options.Amnezia.J2
}
if e.options.Amnezia.J3 != "" {
ipcConf += "\nj3=" + e.options.Amnezia.J3
}
if e.options.Amnezia.ITime > 0 {
ipcConf += "\nitime=" + strconv.FormatInt(e.options.Amnezia.ITime, 10)
}
}
for _, peer := range e.peers {
ipcConf += peer.GenerateIpcLines()

View File

@@ -5,29 +5,31 @@ import (
"net/netip"
"time"
"github.com/sagernet/sing-tun"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type EndpointOptions struct {
Context context.Context
Logger logger.ContextLogger
System bool
Handler tun.Handler
UDPTimeout time.Duration
Dialer N.Dialer
CreateDialer func(interfaceName string) N.Dialer
Name string
MTU uint32
Address []netip.Prefix
PrivateKey string
ListenPort uint16
ResolvePeer func(domain string) (netip.Addr, error)
Peers []PeerOptions
Workers int
Amnezia *AmneziaOptions
Context context.Context
Logger logger.ContextLogger
System bool
Handler tun.Handler
UDPTimeout time.Duration
Dialer N.Dialer
CreateDialer func(interfaceName string) N.Dialer
Name string
MTU uint32
Address []netip.Prefix
PrivateKey string
ListenPort uint16
ResolvePeer func(domain string) (netip.Addr, error)
Peers []PeerOptions
Workers int
PreallocatedBuffersPerPool uint32
DisablePauses bool
Amnezia *AmneziaOptions
}
type PeerOptions struct {
@@ -40,13 +42,24 @@ type PeerOptions struct {
}
type AmneziaOptions struct {
JC int
JMin int
JMax int
S1 int
S2 int
H1 uint32
H2 uint32
H3 uint32
H4 uint32
JC int
JMin int
JMax int
S1 int
S2 int
S3 int
S4 int
H1 uint32
H2 uint32
H3 uint32
H4 uint32
I1 string
I2 string
I3 string
I4 string
I5 string
J1 string
J2 string
J3 string
ITime int64
}