Compare commits

...

46 Commits

Author SHA1 Message Date
世界
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
世界
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
世界
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
世界
573c6179ab Bump version 2025-09-13 13:16:13 +08:00
世界
510bf05e36 Fix UDP exchange for local/dhcp DNS servers 2025-09-13 12:26:48 +08:00
38 changed files with 1272 additions and 1116 deletions

View File

@@ -1,25 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
VERSION="1.23.12" VERSION="1.25.5"
mkdir -p $HOME/go mkdir -p $HOME/go
cd $HOME/go cd $HOME/go
wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz" wget "https://dl.google.com/go/go${VERSION}.linux-amd64.tar.gz"
tar -xzf "go${VERSION}.linux-amd64.tar.gz" tar -xzf "go${VERSION}.linux-amd64.tar.gz"
mv go go_legacy mv go go_win7
cd go_legacy cd go_win7
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557 # modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x # this patch file only works on golang1.25.x
# that means after golang1.24 release it must be changed # that means after golang1.26 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/ # see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
# revert: # revert:
# 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng" # 693def151adff1af707d82d28f55dba81ceb08e1: "crypto/rand,runtime: switch RtlGenRandom for ProcessPrng"
# 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7" # 7c1157f9544922e96945196b47b95664b1e39108: "net: remove sysSocket fallback for Windows 7"
# 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround" # 48042aa09c2f878c4faa576948b07fe625c4707a: "syscall: remove Windows 7 console handle workaround"
# a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries" # a17d959debdb04cd550016a3501dd09d50cd62e7: "runtime: always use LoadLibraryEx to load system libraries"
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1 alias curl='curl -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"'
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/8cb5472d94c34b88733a81091bd328e70ee565a4.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.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 - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Check input version - name: Check input version
if: github.event_name == 'workflow_dispatch' if: github.event_name == 'workflow_dispatch'
run: |- run: |-
@@ -88,9 +88,9 @@ jobs:
- { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" } - { os: linux, arch: loong64, debian: loongarch64, rpm: loongarch64, openwrt: "loongarch64_generic" }
- { os: windows, arch: amd64 } - { 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" }
- { 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: windows, arch: arm64 }
- { os: darwin, arch: amd64 } - { os: darwin, arch: amd64 }
@@ -107,32 +107,32 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Go - name: Setup Go
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }} if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Setup Go 1.24 - name: Setup Go 1.24
if: matrix.legacy_go124 if: matrix.legacy_go124
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.24.6 go-version: ~1.24.10
- name: Cache Go 1.23 - name: Cache Go for Windows 7
if: matrix.legacy_go123 if: matrix.legacy_win7
id: cache-legacy-go id: cache-go-for-windows7
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: | path: |
~/go/go_legacy ~/go/go_win7
key: go_legacy_12312 key: go_win7_1255
- name: Setup Go 1.23 - name: Setup Go for Windows 7
if: matrix.legacy_go123 && steps.cache-legacy-go.outputs.cache-hit != 'true' if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
run: |- run: |-
.github/setup_legacy_go.sh .github/setup_go_for_windows7.sh
- name: Setup Go 1.23 - name: Setup Go for Windows 7
if: matrix.legacy_go123 if: matrix.legacy_win7
run: |- run: |-
echo "PATH=$HOME/go/go_legacy/bin:$PATH" >> $GITHUB_ENV echo "PATH=$HOME/go/go_win7/bin:$PATH" >> $GITHUB_ENV
echo "GOROOT=$HOME/go/go_legacy" >> $GITHUB_ENV echo "GOROOT=$HOME/go/go_win7" >> $GITHUB_ENV
- name: Setup Android NDK - name: Setup Android NDK
if: matrix.os == 'android' if: matrix.os == 'android'
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -300,7 +300,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -380,7 +380,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Setup Android NDK - name: Setup Android NDK
id: setup-ndk id: setup-ndk
uses: nttld/setup-ndk@v1 uses: nttld/setup-ndk@v1
@@ -479,7 +479,7 @@ jobs:
if: matrix.if if: matrix.if
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ^1.25.1 go-version: ^1.25.5
- name: Set tag - name: Set tag
if: matrix.if if: matrix.if
run: |- run: |-

View File

@@ -28,11 +28,11 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
go-version: ~1.24.6 go-version: ~1.24.10
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v8 uses: golangci/golangci-lint-action@v8
with: with:
version: latest version: v2.4.0
args: --timeout=30m args: --timeout=30m
install-mode: binary install-mode: binary
verify: false verify: false

View File

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

4
.gitignore vendored
View File

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

View File

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

View File

@@ -38,7 +38,7 @@ fmt:
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" . @gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
fmt_install: fmt_install:
go install -v mvdan.cc/gofumpt@latest go install -v mvdan.cc/gofumpt@v0.8.0
go install -v github.com/daixiang0/gci@latest go install -v github.com/daixiang0/gci@latest
lint: lint:
@@ -49,7 +49,7 @@ lint:
GOOS=freebsd golangci-lint run ./... GOOS=freebsd golangci-lint run ./...
lint_install: lint_install:
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
proto: proto:
@go run ./cmd/internal/protogen @go run ./cmd/internal/protogen

View File

@@ -1,3 +1,11 @@
> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents
<a href="https://go.warp.dev/sing-box">
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
</a>
---
# sing-box # sing-box
The universal proxy platform. The universal proxy platform.

View File

@@ -27,8 +27,6 @@ type DNSClient interface {
Start() Start()
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
ClearCache() ClearCache()
} }

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) { func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx) _, myMetadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
myMetadata.Source = source myMetadata.Source = source
} }
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
} }
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
myMetadata := ContextFrom(ctx) _, myMetadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
myMetadata.Source = source myMetadata.Source = source
} }
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
} }
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx) _, metadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
metadata.Source = source metadata.Source = source
} }
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
} }
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
metadata := ContextFrom(ctx) _, metadata := ExtendContext(ctx)
if source.IsValid() { if source.IsValid() {
metadata.Source = source metadata.Source = source
} }

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -53,26 +53,48 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
return tlsConn, nil 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 dialer N.Dialer
config Config config Config
} }
func NewDialer(dialer N.Dialer, config Config) N.Dialer { func NewDialer(dialer N.Dialer, config Config) Dialer {
return &Dialer{dialer, config} return &defaultDialer{dialer, config}
} }
func (d *Dialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { func (d *defaultDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if network != N.NetworkTCP { if N.NetworkName(network) != N.NetworkTCP {
return nil, os.ErrInvalid 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 { if err != nil {
return nil, err 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) { func (d *defaultDialer) Upstream() any {
return nil, os.ErrInvalid return d.dialer
} }

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) { 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 len(message.Question) == 0 {
if c.logger != nil { 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...) 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 { if responseChecker != nil {
var rejected bool var rejected bool
// TODO: add accept_any rule and support to check response instead of addresses // 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 var timeToLive uint32
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} { if len(response.Answer) == 0 {
for _, record := range recordList { if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive { timeToLive = soaTTL
timeToLive = record.Header().Ttl }
}
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 { func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
if strategy == C.DomainStrategyPreferIPv6 { if strategy == C.DomainStrategyPreferIPv6 {
return append(response6, response4...) return append(response6, response4...)

View File

@@ -15,8 +15,7 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
} }
responseLen := response.Len() responseLen := response.Len()
if responseLen > maxLen { if responseLen > maxLen {
copyResponse := *response response = response.Copy()
response = &copyResponse
response.Truncate(maxLen) response.Truncate(maxLen)
} }
buffer := buf.NewSize(headroom*2 + 1 + responseLen) 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())) r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
var ( var (
response *mDNS.Msg
transport adapter.DNSTransport transport adapter.DNSTransport
err error err error
) )
response, cached := r.client.ExchangeCache(ctx, message) var metadata *adapter.InboundContext
if !cached { ctx, metadata = adapter.ExtendContext(ctx)
var metadata *adapter.InboundContext metadata.Destination = M.Socksaddr{}
ctx, metadata = adapter.ExtendContext(ctx) metadata.QueryType = message.Question[0].Qtype
metadata.Destination = M.Socksaddr{} switch metadata.QueryType {
metadata.QueryType = message.Question[0].Qtype case mDNS.TypeA:
switch metadata.QueryType { metadata.IPVersion = 4
case mDNS.TypeA: case mDNS.TypeAAAA:
metadata.IPVersion = 4 metadata.IPVersion = 6
case mDNS.TypeAAAA: }
metadata.IPVersion = 6 metadata.Domain = FqdnToDomain(message.Question[0].Name)
} if options.Transport != nil {
metadata.Domain = FqdnToDomain(message.Question[0].Name) transport = options.Transport
if options.Transport != nil { if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
transport = options.Transport
if legacyTransport, isLegacy := transport.(adapter.LegacyDNSTransport); isLegacy {
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = legacyTransport.LegacyStrategy()
}
if !options.ClientSubnet.IsValid() {
options.ClientSubnet = legacyTransport.LegacyClientSubnet()
}
}
if options.Strategy == C.DomainStrategyAsIS { if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy options.Strategy = legacyTransport.LegacyStrategy()
} }
response, err = r.client.Exchange(ctx, transport, message, options, nil) if !options.ClientSubnet.IsValid() {
} else { options.ClientSubnet = legacyTransport.LegacyClientSubnet()
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
} }
} }
if options.Strategy == C.DomainStrategyAsIS {
options.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(ctx, transport, message, options, nil)
} else {
var (
rule adapter.DNSRule
ruleIndex int
)
ruleIndex = -1
for {
dnsCtx := adapter.OverrideContext(ctx)
dnsOptions := options
transport, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message), &dnsOptions)
if rule != nil {
switch action := rule.Action().(type) {
case *R.RuleActionReject:
switch action.Method {
case C.RuleActionRejectMethodDefault:
return &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
Id: message.Id,
Rcode: mDNS.RcodeRefused,
Response: true,
},
Question: []mDNS.Question{message.Question[0]},
}, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined:
return action.Response(message), nil
}
}
var responseCheck func(responseAddrs []netip.Addr) bool
if rule != nil && rule.WithAddressLimit() {
responseCheck = func(responseAddrs []netip.Addr) bool {
metadata.DestinationAddresses = responseAddrs
return rule.MatchAddressLimit(metadata)
}
}
if dnsOptions.Strategy == C.DomainStrategyAsIS {
dnsOptions.Strategy = r.defaultDomainStrategy
}
response, err = r.client.Exchange(dnsCtx, transport, message, dnsOptions, responseCheck)
var rejected bool
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())), " (cached)")
} else if errors.Is(err, ErrResponseRejected) {
rejected = true
r.logger.DebugContext(ctx, E.Cause(err, "response rejected for ", FormatQuestion(message.Question[0].String())))
} else if len(message.Question) > 0 {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for ", FormatQuestion(message.Question[0].String())))
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "exchange failed for <empty query>"))
}
}
if responseCheck != nil && rejected {
continue
}
break
}
} }
if err != nil { if err != nil {
return nil, err return nil, err
@@ -327,7 +325,6 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) { func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
var ( var (
responseAddrs []netip.Addr responseAddrs []netip.Addr
cached bool
err error err error
) )
printResult := func() { printResult := func() {
@@ -347,13 +344,6 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err = E.Cause(err, "lookup ", domain) err = E.Cause(err, "lookup ", domain)
} }
} }
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, E.New("lookup ", domain, ": empty result (cached)")
}
return responseAddrs, nil
}
r.logger.DebugContext(ctx, "lookup domain ", domain) r.logger.DebugContext(ctx, "lookup domain ", domain)
ctx, metadata := adapter.ExtendContext(ctx) ctx, metadata := adapter.ExtendContext(ctx)
metadata.Destination = M.Socksaddr{} metadata.Destination = M.Socksaddr{}
@@ -386,12 +376,7 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
if rule != nil { if rule != nil {
switch action := rule.Action().(type) { switch action := rule.Action().(type) {
case *R.RuleActionReject: case *R.RuleActionReject:
switch action.Method { return nil, &R.RejectedError{Cause: action.Error(ctx)}
case C.RuleActionRejectMethodDefault:
return nil, nil
case C.RuleActionRejectMethodDrop:
return nil, tun.ErrDrop
}
case *R.RuleActionPredefined: case *R.RuleActionPredefined:
if action.Rcode != mDNS.RcodeSuccess { if action.Rcode != mDNS.RcodeSuccess {
err = RcodeError(action.Rcode) err = RcodeError(action.Rcode)

View File

@@ -121,7 +121,7 @@ func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() { if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
conn.SetDeadline(deadline) conn.SetDeadline(deadline)
} }
buffer := buf.Get(1 + request.Len()) buffer := buf.Get(buf.UDPBufferSize)
defer buf.Put(buffer) defer buf.Put(buffer)
rawMessage, err := request.PackBuffer(buffer) rawMessage, err := request.PackBuffer(buffer)
if err != nil { if err != nil {

View File

@@ -25,7 +25,6 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
sHTTP "github.com/sagernet/sing/protocol/http" sHTTP "github.com/sagernet/sing/protocol/http"
mDNS "github.com/miekg/dns" mDNS "github.com/miekg/dns"
@@ -47,7 +46,7 @@ type HTTPSTransport struct {
destination *url.URL destination *url.URL
headers http.Header headers http.Header
transportAccess sync.Mutex transportAccess sync.Mutex
transport *http.Transport transport *HTTPSTransportWrapper
transportResetAt time.Time transportResetAt time.Time
} }
@@ -62,11 +61,8 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if err != nil { if err != nil {
return nil, err return nil, err
} }
if common.Error(tlsConfig.Config()) == nil && !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) { if len(tlsConfig.NextProtos()) == 0 {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), http2.NextProtoTLS)) tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
}
if !common.Contains(tlsConfig.NextProtos(), "http/1.1") {
tlsConfig.SetNextProtos(append(tlsConfig.NextProtos(), "http/1.1"))
} }
headers := options.Headers.Build() headers := options.Headers.Build()
host := headers.Get("Host") host := headers.Get("Host")
@@ -124,37 +120,13 @@ func NewHTTPSRaw(
serverAddr M.Socksaddr, serverAddr M.Socksaddr,
tlsConfig tls.Config, tlsConfig tls.Config,
) *HTTPSTransport { ) *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{ return &HTTPSTransport{
TransportAdapter: adapter, TransportAdapter: adapter,
logger: logger, logger: logger,
dialer: dialer, dialer: dialer,
destination: destination, destination: destination,
headers: headers, 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) { func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
question := message.Question[0] question := message.Question[0]
domain := dns.FqdnToDomain(question.Name)
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA { 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 { if len(addresses) > 0 {
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
} }
} }
systemConfig := getSystemDNSConfig(t.ctx) systemConfig := getSystemDNSConfig(t.ctx)
if systemConfig.singleRequest || !(message.Question[0].Qtype == mDNS.TypeA || message.Question[0].Qtype == mDNS.TypeAAAA) { 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 { } else {
return t.exchangeParallel(ctx, systemConfig, message, domain) return t.exchangeParallel(ctx, systemConfig, message, question.Name)
} }
} }
@@ -182,7 +181,7 @@ func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request
} }
conn.SetDeadline(deadline) conn.SetDeadline(deadline)
} }
buffer := buf.Get(1 + request.Len()) buffer := buf.Get(buf.UDPBufferSize)
defer buf.Put(buffer) defer buf.Put(buffer)
rawMessage, err := request.PackBuffer(buffer) rawMessage, err := request.PackBuffer(buffer)
if err != nil { if err != nil {

View File

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

View File

@@ -2,7 +2,44 @@
icon: material/alert-decagram icon: material/alert-decagram
--- ---
#### 1.12.7 #### 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 * Fixes and improvements
@@ -92,7 +129,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. 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**: **7**:
@@ -154,7 +192,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. 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 | | Version | Stack | MTU | Upload | Download |
|-------------|--------|-------|--------|----------| |-------------|--------|-------|--------|----------|
@@ -173,8 +212,8 @@ The following data was tested using [tun_bench](https://github.com/SagerNet/sing
**18**: **18**:
We continue to experience issues updating our sing-box apps on the App Store and Play Store. 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. 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. Therefore, after this release, we will not be repeating this notice unless there is new information.
### 1.11.15 ### 1.11.15
@@ -455,7 +494,8 @@ See [AnyTLS Inbound](/configuration/inbound/anytls/) and [AnyTLS Outbound](/conf
**2**: **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**: **3**:
@@ -486,7 +526,8 @@ See [Tailscale](/configuration/endpoint/tailscale/).
Due to maintenance difficulties, sing-box 1.12.0 requires at least Go 1.23 to compile. 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 ### 1.11.3

View File

@@ -9,7 +9,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
!!! failure "" !!! failure ""
We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected). Due to non-technical reasons, we are temporarily unable to update the sing-box app on the App Store and release the standalone version of the macOS client (TestFlight users are not affected)
## :material-graph: Requirements ## :material-graph: Requirements
@@ -18,7 +18,7 @@ platform-specific function implementation, such as TUN transparent proxy impleme
## :material-download: Download ## :material-download: Download
* [App Store](https://apps.apple.com/app/sing-box-vt/id6673731168) * ~~[App Store](https://apps.apple.com/app/sing-box-vt/id6673731168)~~
* TestFlight (Beta) * TestFlight (Beta)
TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai) TestFlight quota is only available to [sponsors](https://github.com/sponsors/nekohasekai)
@@ -26,15 +26,15 @@ TestFlight quota is only available to [sponsors](https://github.com/sponsors/nek
Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot) Once you donate, you can get an invitation by join our Telegram group for sponsors from [@yet_another_sponsor_bot](https://t.me/yet_another_sponsor_bot)
or sending us your Apple ID [via email](mailto:contact@sagernet.org). or sending us your Apple ID [via email](mailto:contact@sagernet.org).
## :material-file-download: Download (macOS standalone version) ## ~~:material-file-download: Download (macOS standalone version)~~
* [Homebrew Cask](https://formulae.brew.sh/cask/sfm) * ~~[Homebrew Cask](https://formulae.brew.sh/cask/sfm)~~
```bash ```bash
brew install sfm # brew install sfm
``` ```
* [GitHub Releases](https://github.com/SagerNet/sing-box/releases) * ~~[GitHub Releases](https://github.com/SagerNet/sing-box/releases)~~
## :material-source-repository: Source code ## :material-source-repository: Source code

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) ![](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. 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. 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

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

14
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
go 1.23.1 go 1.23.1
require ( 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/caddyserver/certmagic v0.23.0
github.com/coder/websocket v1.8.13 github.com/coder/websocket v1.8.13
github.com/cretz/bine v0.2.0 github.com/cretz/bine v0.2.0
@@ -15,8 +15,8 @@ require (
github.com/libdns/alidns v1.0.5-libdns.v1.beta1 github.com/libdns/alidns v1.0.5-libdns.v1.beta1
github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6
github.com/logrusorgru/aurora v2.0.3+incompatible github.com/logrusorgru/aurora v2.0.3+incompatible
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0
github.com/metacubex/utls v1.8.0 github.com/metacubex/utls v1.8.3
github.com/mholt/acmez/v3 v3.1.2 github.com/mholt/acmez/v3 v3.1.2
github.com/miekg/dns v1.1.67 github.com/miekg/dns v1.1.67
github.com/oschwald/maxminddb-golang v1.13.1 github.com/oschwald/maxminddb-golang v1.13.1
@@ -26,17 +26,17 @@ require (
github.com/sagernet/fswatch v0.1.1 github.com/sagernet/fswatch v0.1.1
github.com/sagernet/gomobile v0.1.8 github.com/sagernet/gomobile v0.1.8
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.52.0-beta.1 github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
github.com/sagernet/sing v0.7.10 github.com/sagernet/sing v0.7.14
github.com/sagernet/sing-mux v0.3.3 github.com/sagernet/sing-mux v0.3.3
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
github.com/sagernet/sing-shadowsocks v0.2.8 github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1 github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
github.com/sagernet/sing-tun v0.7.2 github.com/sagernet/sing-tun v0.7.3
github.com/sagernet/sing-vmess v0.2.7 github.com/sagernet/sing-vmess v0.2.7
github.com/sagernet/smux v1.5.34-mod.2 github.com/sagernet/smux v1.5.34-mod.2
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-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/wireguard-go v0.0.1-beta.7
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1

28
go.sum
View File

@@ -8,8 +8,8 @@ github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7V
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 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.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
github.com/anytls/sing-anytls v0.0.8/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8= 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 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= 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= github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
@@ -122,10 +122,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/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 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc= github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.8.0 h1:mSYi6FMnmc5riARl5UZDmWVy710z+P5b7xuGW0lV9ac= github.com/metacubex/utls v1.8.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
github.com/metacubex/utls v1.8.0/go.mod h1:FdjYzVfCtgtna19hX0ER1Xsa5uJInwdQ4IcaaI98lEQ= github.com/metacubex/utls v1.8.3/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc= github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0= github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
@@ -164,11 +164,11 @@ 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/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 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/quic-go v0.52.0-beta.1 h1:hWkojLg64zjV+MJOvJU/kOeWndm3tiEfBLx5foisszs= github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
github.com/sagernet/quic-go v0.52.0-beta.1/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4= github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
github.com/sagernet/sing v0.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.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.14 h1:5QQRDCUvYNOMyVp3LuK/hYEBAIv0VsbD3x/l9zH467s=
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.14/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 h1:YFgt9plMWzH994BMZLmyKL37PdIVaIilwP0Jg+EcLfw=
github.com/sagernet/sing-mux v0.3.3/go.mod h1:pht8iFY4c9Xltj7rhVd208npkNaeCxzyXCgulDPLUDA= 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 h1:5Wx3XeTiKrrrcrAky7Hc1bO3CGxrvho2Vu5b/adlEIM=
@@ -179,14 +179,14 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA= github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-tun v0.7.2 h1:uJkAZM0KBqIYzrq077QGqdvj/+4i/pMOx6Pnx0jYqAs= github.com/sagernet/sing-tun v0.7.3 h1:MFnAir+l24ElEyxdfwtY8mqvUUL9nPnL9TDYLkOmVes=
github.com/sagernet/sing-tun v0.7.2/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM= 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 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs= github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2WkltRQ4= github.com/sagernet/smux v1.5.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/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.2 h1:MO7s4ni2bSfAOhcan2rdQSWCztkMXmqyg6jYPZp8bEE=
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI= github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI= github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo= github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=

View File

@@ -13,6 +13,7 @@ import (
"github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/uot" "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 { if options.TLS == nil || !options.TLS.Enabled {
return nil, C.ErrTLSRequired 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)) tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil { if err != nil {

View File

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

View File

@@ -2,8 +2,8 @@ package naive
import ( import (
"context" "context"
"errors"
"io" "io"
"math/rand"
"net" "net"
"net/http" "net/http"
@@ -22,7 +22,11 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
sHttp "github.com/sagernet/sing/protocol/http" sHttp "github.com/sagernet/sing/protocol/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
) )
var ConfigureHTTP3ListenerFunc func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error) var ConfigureHTTP3ListenerFunc func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error)
@@ -82,16 +86,11 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart { if stage != adapter.StartStateStart {
return nil return nil
} }
var tlsConfig *tls.STDConfig
if n.tlsConfig != nil { if n.tlsConfig != nil {
err := n.tlsConfig.Start() err := n.tlsConfig.Start()
if err != nil { if err != nil {
return E.Cause(err, "create TLS config") return E.Cause(err, "create TLS config")
} }
tlsConfig, err = n.tlsConfig.Config()
if err != nil {
return err
}
} }
if common.Contains(n.network, N.NetworkTCP) { if common.Contains(n.network, N.NetworkTCP) {
tcpListener, err := n.listener.ListenTCP() tcpListener, err := n.listener.ListenTCP()
@@ -99,20 +98,23 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
return err return err
} }
n.httpServer = &http.Server{ n.httpServer = &http.Server{
Handler: n, Handler: h2c.NewHandler(n, &http2.Server{}),
TLSConfig: tlsConfig,
BaseContext: func(listener net.Listener) context.Context { BaseContext: func(listener net.Listener) context.Context {
return n.ctx return n.ctx
}, },
} }
go func() { go func() {
var sErr error listener := net.Listener(tcpListener)
if tlsConfig != nil { if n.tlsConfig != nil {
sErr = n.httpServer.ServeTLS(tcpListener, "", "") if len(n.tlsConfig.NextProtos()) == 0 {
} else { n.tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
sErr = n.httpServer.Serve(tcpListener) } else if !common.Contains(n.tlsConfig.NextProtos(), http2.NextProtoTLS) {
n.tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, n.tlsConfig.NextProtos()...))
}
listener = aTLS.NewListener(tcpListener, n.tlsConfig)
} }
if sErr != nil && !E.IsClosedOrCanceled(sErr) { sErr := n.httpServer.Serve(listener)
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
n.logger.Error("http server serve error: ", sErr) n.logger.Error("http server serve error: ", sErr)
} }
}() }()
@@ -161,13 +163,16 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
n.badRequest(ctx, request, E.New("authorization failed")) n.badRequest(ctx, request, E.New("authorization failed"))
return return
} }
writer.Header().Set("Padding", generateNaivePaddingHeader()) writer.Header().Set("Padding", generatePaddingHeader())
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
writer.(http.Flusher).Flush() writer.(http.Flusher).Flush()
hostPort := request.URL.Host hostPort := request.Header.Get("-connect-authority")
if hostPort == "" { if hostPort == "" {
hostPort = request.Host hostPort = request.URL.Host
if hostPort == "" {
hostPort = request.Host
}
} }
source := sHttp.SourceAddress(request) source := sHttp.SourceAddress(request)
destination := M.ParseSocksaddr(hostPort).Unwrap() destination := M.ParseSocksaddr(hostPort).Unwrap()
@@ -178,9 +183,14 @@ func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
n.badRequest(ctx, request, E.New("hijack failed")) n.badRequest(ctx, request, E.New("hijack failed"))
return return
} }
n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination) n.newConnection(ctx, false, &naiveConn{Conn: conn}, userName, source, destination)
} else { } else {
n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination) n.newConnection(ctx, true, &naiveH2Conn{
reader: request.Body,
writer: writer,
flusher: writer.(http.Flusher),
remoteAddress: source,
}, userName, source, destination)
} }
} }
@@ -236,18 +246,3 @@ func rejectHTTP(writer http.ResponseWriter, statusCode int) {
} }
conn.Close() conn.Close()
} }
func generateNaivePaddingHeader() string {
paddingLen := rand.Intn(32) + 30
padding := make([]byte, paddingLen)
bits := rand.Uint64()
for i := 0; i < 16; i++ {
// Codes that won't be Huffman coded.
padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
bits >>= 4
}
for i := 16; i < paddingLen; i++ {
padding[i] = '~'
}
return string(padding)
}

View File

@@ -7,417 +7,242 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"strings"
"time" "time"
"github.com/sagernet/sing/common" "github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/baderror"
"github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/rw" "github.com/sagernet/sing/common/rw"
) )
const kFirstPaddings = 8 const paddingCount = 8
type naiveH1Conn struct { func generatePaddingHeader() string {
net.Conn paddingLen := rand.Intn(32) + 30
padding := make([]byte, paddingLen)
bits := rand.Uint64()
for i := 0; i < 16; i++ {
padding[i] = "!#$()+<>?@[]^`{}"[bits&15]
bits >>= 4
}
for i := 16; i < paddingLen; i++ {
padding[i] = '~'
}
return string(padding)
}
type paddingConn struct {
readPadding int readPadding int
writePadding int writePadding int
readRemaining int readRemaining int
paddingRemaining int paddingRemaining int
} }
func (c *naiveH1Conn) Read(p []byte) (n int, err error) { func (p *paddingConn) readWithPadding(reader io.Reader, buffer []byte) (n int, err error) {
n, err = c.read(p) if p.readRemaining > 0 {
return n, wrapHttpError(err) if len(buffer) > p.readRemaining {
} buffer = buffer[:p.readRemaining]
func (c *naiveH1Conn) read(p []byte) (n int, err error) {
if c.readRemaining > 0 {
if len(p) > c.readRemaining {
p = p[:c.readRemaining]
} }
n, err = c.Conn.Read(p) n, err = reader.Read(buffer)
if err != nil { if err != nil {
return return
} }
c.readRemaining -= n p.readRemaining -= n
return return
} }
if c.paddingRemaining > 0 { if p.paddingRemaining > 0 {
err = rw.SkipN(c.Conn, c.paddingRemaining) err = rw.SkipN(reader, p.paddingRemaining)
if err != nil { if err != nil {
return return
} }
c.paddingRemaining = 0 p.paddingRemaining = 0
} }
if c.readPadding < kFirstPaddings { if p.readPadding < paddingCount {
var paddingHdr []byte var paddingHeader []byte
if len(p) >= 3 { if len(buffer) >= 3 {
paddingHdr = p[:3] paddingHeader = buffer[:3]
} else { } else {
paddingHdr = make([]byte, 3) paddingHeader = make([]byte, 3)
} }
_, err = io.ReadFull(c.Conn, paddingHdr) _, err = io.ReadFull(reader, paddingHeader)
if err != nil { if err != nil {
return return
} }
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2])) originalDataSize := int(binary.BigEndian.Uint16(paddingHeader[:2]))
paddingSize := int(paddingHdr[2]) paddingSize := int(paddingHeader[2])
if len(p) > originalDataSize { if len(buffer) > originalDataSize {
p = p[:originalDataSize] buffer = buffer[:originalDataSize]
} }
n, err = c.Conn.Read(p) n, err = reader.Read(buffer)
if err != nil { if err != nil {
return return
} }
c.readPadding++ p.readPadding++
c.readRemaining = originalDataSize - n p.readRemaining = originalDataSize - n
c.paddingRemaining = paddingSize p.paddingRemaining = paddingSize
return return
} }
return c.Conn.Read(p) return reader.Read(buffer)
} }
func (c *naiveH1Conn) Write(p []byte) (n int, err error) { func (p *paddingConn) writeWithPadding(writer io.Writer, data []byte) (n int, err error) {
for pLen := len(p); pLen > 0; { if p.writePadding < paddingCount {
var data []byte
if pLen > 65535 {
data = p[:65535]
p = p[65535:]
pLen -= 65535
} else {
data = p
pLen = 0
}
var writeN int
writeN, err = c.write(data)
n += writeN
if err != nil {
break
}
}
return n, wrapHttpError(err)
}
func (c *naiveH1Conn) write(p []byte) (n int, err error) {
if c.writePadding < kFirstPaddings {
paddingSize := rand.Intn(256) paddingSize := rand.Intn(256)
buffer := buf.NewSize(3 + len(data) + paddingSize)
buffer := buf.NewSize(3 + len(p) + paddingSize)
defer buffer.Release() defer buffer.Release()
header := buffer.Extend(3) header := buffer.Extend(3)
binary.BigEndian.PutUint16(header, uint16(len(p))) binary.BigEndian.PutUint16(header, uint16(len(data)))
header[2] = byte(paddingSize) header[2] = byte(paddingSize)
common.Must1(buffer.Write(data))
common.Must1(buffer.Write(p)) _, err = writer.Write(buffer.Bytes())
_, err = c.Conn.Write(buffer.Bytes())
if err == nil { if err == nil {
n = len(p) n = len(data)
} }
c.writePadding++ p.writePadding++
return return
} }
return c.Conn.Write(p) return writer.Write(data)
} }
func (c *naiveH1Conn) FrontHeadroom() int { func (p *paddingConn) writeBufferWithPadding(writer io.Writer, buffer *buf.Buffer) error {
if c.writePadding < kFirstPaddings { if p.writePadding < paddingCount {
return 3
}
return 0
}
func (c *naiveH1Conn) RearHeadroom() int {
if c.writePadding < kFirstPaddings {
return 255
}
return 0
}
func (c *naiveH1Conn) WriterMTU() int {
if c.writePadding < kFirstPaddings {
return 65535
}
return 0
}
func (c *naiveH1Conn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
if c.writePadding < kFirstPaddings {
bufferLen := buffer.Len() bufferLen := buffer.Len()
if bufferLen > 65535 { if bufferLen > 65535 {
return common.Error(c.Write(buffer.Bytes())) _, err := p.writeChunked(writer, buffer.Bytes())
return err
} }
paddingSize := rand.Intn(256) paddingSize := rand.Intn(256)
header := buffer.ExtendHeader(3) header := buffer.ExtendHeader(3)
binary.BigEndian.PutUint16(header, uint16(bufferLen)) binary.BigEndian.PutUint16(header, uint16(bufferLen))
header[2] = byte(paddingSize) header[2] = byte(paddingSize)
buffer.Extend(paddingSize) buffer.Extend(paddingSize)
c.writePadding++ p.writePadding++
} }
return wrapHttpError(common.Error(c.Conn.Write(buffer.Bytes()))) return common.Error(writer.Write(buffer.Bytes()))
} }
// FIXME func (p *paddingConn) writeChunked(writer io.Writer, data []byte) (n int, err error) {
/*func (c *naiveH1Conn) WriteTo(w io.Writer) (n int64, err error) { for len(data) > 0 {
if c.readPadding < kFirstPaddings { var chunk []byte
n, err = bufio.WriteToN(c, w, kFirstPaddings-c.readPadding) if len(data) > 65535 {
} else { chunk = data[:65535]
n, err = bufio.Copy(w, c.Conn) data = data[65535:]
}
return n, wrapHttpError(err)
}
func (c *naiveH1Conn) ReadFrom(r io.Reader) (n int64, err error) {
if c.writePadding < kFirstPaddings {
n, err = bufio.ReadFromN(c, r, kFirstPaddings-c.writePadding)
} else {
n, err = bufio.Copy(c.Conn, r)
}
return n, wrapHttpError(err)
}
*/
func (c *naiveH1Conn) Upstream() any {
return c.Conn
}
func (c *naiveH1Conn) ReaderReplaceable() bool {
return c.readPadding == kFirstPaddings
}
func (c *naiveH1Conn) WriterReplaceable() bool {
return c.writePadding == kFirstPaddings
}
type naiveH2Conn struct {
reader io.Reader
writer io.Writer
flusher http.Flusher
rAddr net.Addr
readPadding int
writePadding int
readRemaining int
paddingRemaining int
}
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
n, err = c.read(p)
return n, wrapHttpError(err)
}
func (c *naiveH2Conn) read(p []byte) (n int, err error) {
if c.readRemaining > 0 {
if len(p) > c.readRemaining {
p = p[:c.readRemaining]
}
n, err = c.reader.Read(p)
if err != nil {
return
}
c.readRemaining -= n
return
}
if c.paddingRemaining > 0 {
err = rw.SkipN(c.reader, c.paddingRemaining)
if err != nil {
return
}
c.paddingRemaining = 0
}
if c.readPadding < kFirstPaddings {
var paddingHdr []byte
if len(p) >= 3 {
paddingHdr = p[:3]
} else { } else {
paddingHdr = make([]byte, 3) chunk = data
data = nil
} }
_, err = io.ReadFull(c.reader, paddingHdr) var written int
written, err = p.writeWithPadding(writer, chunk)
n += written
if err != nil { if err != nil {
return return
} }
originalDataSize := int(binary.BigEndian.Uint16(paddingHdr[:2]))
paddingSize := int(paddingHdr[2])
if len(p) > originalDataSize {
p = p[:originalDataSize]
}
n, err = c.reader.Read(p)
if err != nil {
return
}
c.readPadding++
c.readRemaining = originalDataSize - n
c.paddingRemaining = paddingSize
return
} }
return c.reader.Read(p) return
} }
func (c *naiveH2Conn) Write(p []byte) (n int, err error) { func (p *paddingConn) frontHeadroom() int {
for pLen := len(p); pLen > 0; { if p.writePadding < paddingCount {
var data []byte
if pLen > 65535 {
data = p[:65535]
p = p[65535:]
pLen -= 65535
} else {
data = p
pLen = 0
}
var writeN int
writeN, err = c.write(data)
n += writeN
if err != nil {
break
}
}
if err == nil {
c.flusher.Flush()
}
return n, wrapHttpError(err)
}
func (c *naiveH2Conn) write(p []byte) (n int, err error) {
if c.writePadding < kFirstPaddings {
paddingSize := rand.Intn(256)
buffer := buf.NewSize(3 + len(p) + paddingSize)
defer buffer.Release()
header := buffer.Extend(3)
binary.BigEndian.PutUint16(header, uint16(len(p)))
header[2] = byte(paddingSize)
common.Must1(buffer.Write(p))
_, err = c.writer.Write(buffer.Bytes())
if err == nil {
n = len(p)
}
c.writePadding++
return
}
return c.writer.Write(p)
}
func (c *naiveH2Conn) FrontHeadroom() int {
if c.writePadding < kFirstPaddings {
return 3 return 3
} }
return 0 return 0
} }
func (c *naiveH2Conn) RearHeadroom() int { func (p *paddingConn) rearHeadroom() int {
if c.writePadding < kFirstPaddings { if p.writePadding < paddingCount {
return 255 return 255
} }
return 0 return 0
} }
func (c *naiveH2Conn) WriterMTU() int { func (p *paddingConn) writerMTU() int {
if c.writePadding < kFirstPaddings { if p.writePadding < paddingCount {
return 65535 return 65535
} }
return 0 return 0
} }
func (p *paddingConn) readerReplaceable() bool {
return p.readPadding == paddingCount
}
func (p *paddingConn) writerReplaceable() bool {
return p.writePadding == paddingCount
}
type naiveConn struct {
net.Conn
paddingConn
}
func (c *naiveConn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.Conn, p)
return n, baderror.WrapH2(err)
}
func (c *naiveConn) Write(p []byte) (n int, err error) {
n, err = c.writeChunked(c.Conn, p)
return n, baderror.WrapH2(err)
}
func (c *naiveConn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release()
err := c.writeBufferWithPadding(c.Conn, buffer)
return baderror.WrapH2(err)
}
func (c *naiveConn) FrontHeadroom() int { return c.frontHeadroom() }
func (c *naiveConn) RearHeadroom() int { return c.rearHeadroom() }
func (c *naiveConn) WriterMTU() int { return c.writerMTU() }
func (c *naiveConn) Upstream() any { return c.Conn }
func (c *naiveConn) ReaderReplaceable() bool { return c.readerReplaceable() }
func (c *naiveConn) WriterReplaceable() bool { return c.writerReplaceable() }
type naiveH2Conn struct {
reader io.Reader
writer io.Writer
flusher http.Flusher
remoteAddress net.Addr
paddingConn
}
func (c *naiveH2Conn) Read(p []byte) (n int, err error) {
n, err = c.readWithPadding(c.reader, p)
return n, baderror.WrapH2(err)
}
func (c *naiveH2Conn) Write(p []byte) (n int, err error) {
n, err = c.writeChunked(c.writer, p)
if err == nil {
c.flusher.Flush()
}
return n, baderror.WrapH2(err)
}
func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error { func (c *naiveH2Conn) WriteBuffer(buffer *buf.Buffer) error {
defer buffer.Release() defer buffer.Release()
if c.writePadding < kFirstPaddings { err := c.writeBufferWithPadding(c.writer, buffer)
bufferLen := buffer.Len()
if bufferLen > 65535 {
return common.Error(c.Write(buffer.Bytes()))
}
paddingSize := rand.Intn(256)
header := buffer.ExtendHeader(3)
binary.BigEndian.PutUint16(header, uint16(bufferLen))
header[2] = byte(paddingSize)
buffer.Extend(paddingSize)
c.writePadding++
}
err := common.Error(c.writer.Write(buffer.Bytes()))
if err == nil { if err == nil {
c.flusher.Flush() c.flusher.Flush()
} }
return wrapHttpError(err) return baderror.WrapH2(err)
} }
// FIXME
/*func (c *naiveH2Conn) WriteTo(w io.Writer) (n int64, err error) {
if c.readPadding < kFirstPaddings {
n, err = bufio.WriteToN(c, w, kFirstPaddings-c.readPadding)
} else {
n, err = bufio.Copy(w, c.reader)
}
return n, wrapHttpError(err)
}
func (c *naiveH2Conn) ReadFrom(r io.Reader) (n int64, err error) {
if c.writePadding < kFirstPaddings {
n, err = bufio.ReadFromN(c, r, kFirstPaddings-c.writePadding)
} else {
n, err = bufio.Copy(c.writer, r)
}
return n, wrapHttpError(err)
}*/
func (c *naiveH2Conn) Close() error { func (c *naiveH2Conn) Close() error {
return common.Close( return common.Close(c.reader, c.writer)
c.reader,
c.writer,
)
} }
func (c *naiveH2Conn) LocalAddr() net.Addr { func (c *naiveH2Conn) LocalAddr() net.Addr { return M.Socksaddr{} }
return M.Socksaddr{} func (c *naiveH2Conn) RemoteAddr() net.Addr { return c.remoteAddress }
} func (c *naiveH2Conn) SetDeadline(t time.Time) error { return os.ErrInvalid }
func (c *naiveH2Conn) SetReadDeadline(t time.Time) error { return os.ErrInvalid }
func (c *naiveH2Conn) RemoteAddr() net.Addr { func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error { return os.ErrInvalid }
return c.rAddr func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool { return true }
} func (c *naiveH2Conn) UpstreamReader() any { return c.reader }
func (c *naiveH2Conn) UpstreamWriter() any { return c.writer }
func (c *naiveH2Conn) SetDeadline(t time.Time) error { func (c *naiveH2Conn) FrontHeadroom() int { return c.frontHeadroom() }
return os.ErrInvalid func (c *naiveH2Conn) RearHeadroom() int { return c.rearHeadroom() }
} func (c *naiveH2Conn) WriterMTU() int { return c.writerMTU() }
func (c *naiveH2Conn) ReaderReplaceable() bool { return c.readerReplaceable() }
func (c *naiveH2Conn) SetReadDeadline(t time.Time) error { func (c *naiveH2Conn) WriterReplaceable() bool { return c.writerReplaceable() }
return os.ErrInvalid
}
func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *naiveH2Conn) UpstreamReader() any {
return c.reader
}
func (c *naiveH2Conn) UpstreamWriter() any {
return c.writer
}
func (c *naiveH2Conn) ReaderReplaceable() bool {
return c.readPadding == kFirstPaddings
}
func (c *naiveH2Conn) WriterReplaceable() bool {
return c.writePadding == kFirstPaddings
}
func wrapHttpError(err error) error {
if err == nil {
return err
}
if strings.Contains(err.Error(), "client disconnected") {
return net.ErrClosed
}
if strings.Contains(err.Error(), "body closed by handler") {
return net.ErrClosed
}
if strings.Contains(err.Error(), "canceled with error code 268") {
return io.EOF
}
return err
}

View File

@@ -38,6 +38,7 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/tailscale/ipn" "github.com/sagernet/tailscale/ipn"
@@ -158,6 +159,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
}, },
TLSClientConfig: &tls.Config{ TLSClientConfig: &tls.Config{
RootCAs: adapter.RootPoolFromContext(ctx), RootCAs: adapter.RootPoolFromContext(ctx),
Time: ntp.TimeFuncFromContext(ctx),
}, },
}, },
}, },
@@ -456,20 +458,20 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
metadata.Inbound = t.Tag() metadata.Inbound = t.Tag()
metadata.InboundType = t.Type() metadata.InboundType = t.Type()
metadata.Source = source metadata.Source = source
metadata.Destination = destination
addr4, addr6 := t.server.TailscaleIPs() addr4, addr6 := t.server.TailscaleIPs()
switch destination.Addr { switch destination.Addr {
case addr4: case addr4:
metadata.OriginDestination = destination metadata.OriginDestination = destination
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
case addr6: case addr6:
metadata.OriginDestination = destination metadata.OriginDestination = destination
destination.Addr = netip.IPv6Loopback() destination.Addr = netip.IPv6Loopback()
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
} }
metadata.Destination = destination
t.logger.InfoContext(ctx, "inbound packet connection from ", source) t.logger.InfoContext(ctx, "inbound packet connection from ", source)
t.logger.InfoContext(ctx, "inbound packet connection to ", destination) t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
} }

View File

@@ -3,6 +3,7 @@ package derp
import ( import (
"bufio" "bufio"
"context" "context"
stdTLS "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@@ -31,6 +32,7 @@ import (
"github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/logger"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network" N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/ntp"
aTLS "github.com/sagernet/sing/common/tls" aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service" "github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager" "github.com/sagernet/sing/service/filemanager"
@@ -159,6 +161,10 @@ func (d *Service) Start(stage adapter.StartStage) error {
httpClients = append(httpClients, &http.Client{ httpClients = append(httpClients, &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
ForceAttemptHTTP2: true, ForceAttemptHTTP2: true,
TLSClientConfig: &stdTLS.Config{
RootCAs: adapter.RootPoolFromContext(d.ctx),
Time: ntp.TimeFuncFromContext(d.ctx),
},
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr)) return verifyDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
}, },

View File

@@ -49,6 +49,9 @@ func (s *Service) loadCache() error {
os.RemoveAll(basePath) os.RemoveAll(basePath)
return err return err
} }
s.cacheMutex.Lock()
s.lastSavedCache = cacheBinary
s.cacheMutex.Unlock()
return nil return nil
} }
@@ -56,16 +59,30 @@ func (s *Service) saveCache() error {
if s.cachePath == "" { if s.cachePath == "" {
return nil return nil
} }
cacheBinary, err := s.encodeCache()
if err != nil {
return err
}
s.cacheMutex.Lock()
defer s.cacheMutex.Unlock()
if bytes.Equal(s.lastSavedCache, cacheBinary) {
return nil
}
return s.writeCache(cacheBinary)
}
func (s *Service) writeCache(cacheBinary []byte) error {
basePath := filemanager.BasePath(s.ctx, s.cachePath) basePath := filemanager.BasePath(s.ctx, s.cachePath)
err := os.MkdirAll(filepath.Dir(basePath), 0o777) err := os.MkdirAll(filepath.Dir(basePath), 0o777)
if err != nil { if err != nil {
return err return err
} }
cacheBinary, err := s.encodeCache() err = os.WriteFile(basePath, cacheBinary, 0o644)
if err != nil { if err != nil {
return err return err
} }
return os.WriteFile(s.cachePath, cacheBinary, 0o644) s.lastSavedCache = cacheBinary
return nil
} }
func (s *Service) decodeCache(cacheBinary []byte) error { func (s *Service) decodeCache(cacheBinary []byte) error {

View File

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

View File

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