mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 18:17:18 +10:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f56d9ab945 | ||
|
|
86fabd6a22 | ||
|
|
24a1e7cee4 | ||
|
|
223dd8bb1a | ||
|
|
68448de7d0 | ||
|
|
1ebff74c21 | ||
|
|
f0cd3422c1 | ||
|
|
e385a98ced | ||
|
|
670f32baee | ||
|
|
2747a00ba2 | ||
|
|
48e76038d0 | ||
|
|
6421252d44 | ||
|
|
216c4c8bd4 | ||
|
|
5841d410a1 | ||
|
|
63c8207d7a | ||
|
|
54ed58499d | ||
|
|
b1bdc18c85 | ||
|
|
a38030cc0b | ||
|
|
4626aa2cb0 | ||
|
|
5a40b673a4 | ||
|
|
541f63fee4 | ||
|
|
5de6f4a14f | ||
|
|
5658830077 | ||
|
|
0e50edc009 | ||
|
|
444f454810 | ||
|
|
d0e1fd6c7e | ||
|
|
17b4d1e010 | ||
|
|
06791470c9 | ||
|
|
ef14c8ca0e | ||
|
|
36dc883c7c | ||
|
|
6557bd7029 | ||
|
|
41b30c91d9 | ||
|
|
0f767d5ce1 | ||
|
|
328a6de797 | ||
|
|
886be6414d | ||
|
|
9362d3cab3 | ||
|
|
ced2e39dbf | ||
|
|
2159d8877b | ||
|
|
cb7dba3eff | ||
|
|
d9d7f7880d | ||
|
|
a031aaf2c0 | ||
|
|
4bca951773 | ||
|
|
140735dbde | ||
|
|
714a68bba1 | ||
|
|
573c6179ab | ||
|
|
510bf05e36 | ||
|
|
ae852e0be4 | ||
|
|
1955002ed8 | ||
|
|
44559fb7b9 | ||
|
|
0977c5cf73 | ||
|
|
07697bf931 | ||
|
|
5d1d1a1456 | ||
|
|
146383499e | ||
|
|
e81a76fdf9 | ||
|
|
de13137418 | ||
|
|
e42b818c2a |
@@ -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
|
||||
53
.github/workflows/build.yml
vendored
53
.github/workflows/build.yml
vendored
@@ -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
|
||||
@@ -432,7 +432,8 @@ jobs:
|
||||
SERVICE_ACCOUNT_CREDENTIALS: ${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}
|
||||
build_apple:
|
||||
name: Build Apple clients
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
if: false
|
||||
needs:
|
||||
- calculate_version
|
||||
strategy:
|
||||
@@ -478,15 +479,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.1
|
||||
- name: Setup Xcode stable
|
||||
if: matrix.if && github.ref == 'refs/heads/main-next'
|
||||
run: |-
|
||||
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
- name: Setup Xcode beta
|
||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
go-version: ^1.25.5
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@@ -28,11 +28,11 @@ 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:
|
||||
version: latest
|
||||
version: v2.4.0
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
verify: false
|
||||
|
||||
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -15,4 +15,6 @@
|
||||
.DS_Store
|
||||
/config.d/
|
||||
/venv/
|
||||
|
||||
CLAUDE.md
|
||||
AGENTS.md
|
||||
/.claude/
|
||||
|
||||
@@ -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"]
|
||||
|
||||
8
Makefile
8
Makefile
@@ -17,6 +17,10 @@ build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
race:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build -race $(MAIN_PARAMS) $(MAIN)
|
||||
|
||||
ci_build:
|
||||
export GOTOOLCHAIN=local && \
|
||||
go build $(PARAMS) $(MAIN) && \
|
||||
@@ -34,7 +38,7 @@ fmt:
|
||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||
|
||||
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
|
||||
|
||||
lint:
|
||||
@@ -45,7 +49,7 @@ lint:
|
||||
GOOS=freebsd golangci-lint run ./...
|
||||
|
||||
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:
|
||||
@go run ./cmd/internal/protogen
|
||||
|
||||
@@ -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
|
||||
|
||||
The universal proxy platform.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Submodule clients/android updated: 6295dde5b3...fe128a6cd7
Submodule clients/apple updated: c5734677bd...532c140f05
@@ -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
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
var _ adapter.CertificateStore = (*Store)(nil)
|
||||
|
||||
type Store struct {
|
||||
access sync.RWMutex
|
||||
systemPool *x509.CertPool
|
||||
currentPool *x509.CertPool
|
||||
certificate string
|
||||
@@ -115,10 +117,14 @@ func (s *Store) Close() error {
|
||||
}
|
||||
|
||||
func (s *Store) Pool() *x509.CertPool {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return s.currentPool
|
||||
}
|
||||
|
||||
func (s *Store) update() error {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
var currentPool *x509.CertPool
|
||||
if s.systemPool == nil {
|
||||
currentPool = x509.NewCertPool()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -69,11 +69,7 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
||||
} else {
|
||||
return E.New("missing ECH keys")
|
||||
}
|
||||
block, rest := pem.Decode(echKey)
|
||||
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
|
||||
return E.New("invalid ECH keys pem")
|
||||
}
|
||||
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||
echKeys, err := parseECHKeys(echKey)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse ECH keys")
|
||||
}
|
||||
@@ -85,21 +81,29 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
||||
return nil
|
||||
}
|
||||
|
||||
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||
echKey, err := os.ReadFile(echKeyPath)
|
||||
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
|
||||
echKeys, err := parseECHKeys(echKey)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload ECH keys from ", echKeyPath)
|
||||
return err
|
||||
}
|
||||
c.access.Lock()
|
||||
config := c.config.Clone()
|
||||
config.EncryptedClientHelloKeys = echKeys
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseECHKeys(echKey []byte) ([]tls.EncryptedClientHelloKey, error) {
|
||||
block, _ := pem.Decode(echKey)
|
||||
if block == nil || block.Type != "ECH KEYS" {
|
||||
return E.New("invalid ECH keys pem")
|
||||
return nil, E.New("invalid ECH keys pem")
|
||||
}
|
||||
echKeys, err := UnmarshalECHKeys(block.Bytes)
|
||||
if err != nil {
|
||||
return E.Cause(err, "parse ECH keys")
|
||||
return nil, E.Cause(err, "parse ECH keys")
|
||||
}
|
||||
tlsConfig.EncryptedClientHelloKeys = echKeys
|
||||
return nil
|
||||
return echKeys, nil
|
||||
}
|
||||
|
||||
type ECHClientConfig struct {
|
||||
|
||||
@@ -18,6 +18,6 @@ func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions,
|
||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||
}
|
||||
|
||||
func reloadECHKeys(echKeyPath string, tlsConfig *tls.Config) error {
|
||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/fswatch"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
var errInsecureUnused = E.New("tls: insecure unused")
|
||||
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
logger log.Logger
|
||||
acmeService adapter.SimpleLifecycle
|
||||
@@ -32,14 +34,22 @@ type STDServerConfig struct {
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) ServerName() string {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
return c.config.ServerName
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetServerName(serverName string) {
|
||||
c.config.ServerName = serverName
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
config := c.config.Clone()
|
||||
config.ServerName = serverName
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) NextProtos() []string {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||
return c.config.NextProtos[1:]
|
||||
} else {
|
||||
@@ -48,11 +58,15 @@ func (c *STDServerConfig) NextProtos() []string {
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
config := c.config.Clone()
|
||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||
c.config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||
} else {
|
||||
c.config.NextProtos = nextProto
|
||||
config.NextProtos = nextProto
|
||||
}
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
||||
@@ -77,9 +91,6 @@ func (c *STDServerConfig) Start() error {
|
||||
if c.acmeService != nil {
|
||||
return c.acmeService.Start()
|
||||
} else {
|
||||
if c.certificatePath == "" && c.keyPath == "" {
|
||||
return nil
|
||||
}
|
||||
err := c.startWatcher()
|
||||
if err != nil {
|
||||
c.logger.Warn("create fsnotify watcher: ", err)
|
||||
@@ -99,6 +110,9 @@ func (c *STDServerConfig) startWatcher() error {
|
||||
if c.echKeyPath != "" {
|
||||
watchPath = append(watchPath, c.echKeyPath)
|
||||
}
|
||||
if len(watchPath) == 0 {
|
||||
return nil
|
||||
}
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
Path: watchPath,
|
||||
Callback: func(path string) {
|
||||
@@ -138,10 +152,18 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload key pair")
|
||||
}
|
||||
c.config.Certificates = []tls.Certificate{keyPair}
|
||||
c.access.Lock()
|
||||
config := c.config.Clone()
|
||||
config.Certificates = []tls.Certificate{keyPair}
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
c.logger.Info("reloaded TLS certificate")
|
||||
} else if path == c.echKeyPath {
|
||||
err := reloadECHKeys(c.echKeyPath, c.config)
|
||||
echKey, err := os.ReadFile(c.echKeyPath)
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload ECH keys from ", c.echKeyPath)
|
||||
}
|
||||
err = c.setECHServerConfig(echKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -262,7 +284,7 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &STDServerConfig{
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
logger: logger,
|
||||
acmeService: acmeService,
|
||||
@@ -271,5 +293,11 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
certificatePath: options.CertificatePath,
|
||||
keyPath: options.KeyPath,
|
||||
echKeyPath: echKeyPath,
|
||||
}, nil
|
||||
}
|
||||
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
serverConfig.access.Lock()
|
||||
defer serverConfig.access.Unlock()
|
||||
return serverConfig.config, nil
|
||||
}
|
||||
return serverConfig, nil
|
||||
}
|
||||
|
||||
@@ -47,15 +47,15 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *adapter.URLTestHistory
|
||||
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
||||
s.access.Lock()
|
||||
delete(s.delayHistory, tag)
|
||||
s.access.Unlock()
|
||||
s.notifyUpdated()
|
||||
s.access.Unlock()
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *adapter.URLTestHistory) {
|
||||
s.access.Lock()
|
||||
s.delayHistory[tag] = history
|
||||
s.access.Unlock()
|
||||
s.notifyUpdated()
|
||||
s.access.Unlock()
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) notifyUpdated() {
|
||||
@@ -69,6 +69,8 @@ func (s *HistoryStorage) notifyUpdated() {
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) Close() error {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
s.updateHook = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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,6 +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 && response.Rcode != dns.RcodeNameError)
|
||||
if responseChecker != nil {
|
||||
var rejected bool
|
||||
// TODO: add accept_any rule and support to check response instead of addresses
|
||||
@@ -250,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -280,7 +302,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
}
|
||||
}
|
||||
logExchangedResponse(c.logger, ctx, response, timeToLive)
|
||||
return response, err
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||
@@ -331,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...)
|
||||
|
||||
@@ -15,8 +15,7 @@ func TruncateDNSMessage(request *dns.Msg, response *dns.Msg, headroom int) (*buf
|
||||
}
|
||||
responseLen := response.Len()
|
||||
if responseLen > maxLen {
|
||||
copyResponse := *response
|
||||
response = ©Response
|
||||
response = response.Copy()
|
||||
response.Truncate(maxLen)
|
||||
}
|
||||
buffer := buf.NewSize(headroom*2 + 1 + responseLen)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||
|
||||
181
dns/router.go
181
dns/router.go
@@ -214,97 +214,95 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
}
|
||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||
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)
|
||||
|
||||
@@ -2,10 +2,13 @@ package dhcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@@ -195,7 +198,17 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface)
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "android" {
|
||||
listenAddr = "255.255.255.255:68"
|
||||
}
|
||||
packetConn, err := listener.ListenPacket(t.ctx, "udp4", listenAddr)
|
||||
var (
|
||||
packetConn net.PacketConn
|
||||
err error
|
||||
)
|
||||
for i := 0; i < 5; i++ {
|
||||
packetConn, err = listener.ListenPacket(t.ctx, "udp4", listenAddr)
|
||||
if err == nil || !errors.Is(err, syscall.EADDRINUSE) {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -232,6 +245,9 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
||||
for {
|
||||
_, _, err := buffer.ReadPacketFrom(packetConn)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.ErrShortBuffer) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,13 @@ package dhcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
"syscall"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
@@ -43,7 +44,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
err = dns.RcodeSuccess
|
||||
}
|
||||
}
|
||||
select {
|
||||
@@ -83,7 +84,7 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
|
||||
server := servers[j]
|
||||
question := message.Question[0]
|
||||
question.Name = fqdn
|
||||
response, err := t.exchangeOne(ctx, server, question, C.DNSTimeout, false, true)
|
||||
response, err := t.exchangeOne(ctx, server, question)
|
||||
if err != nil {
|
||||
lastErr = err
|
||||
continue
|
||||
@@ -94,62 +95,77 @@ func (t *Transport) tryOneName(ctx context.Context, servers []M.Socksaddr, fqdn
|
||||
return nil, E.Cause(lastErr, fqdn)
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question, timeout time.Duration, useTCP, ad bool) (*mDNS.Msg, error) {
|
||||
func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, question mDNS.Question) (*mDNS.Msg, error) {
|
||||
if server.Port == 0 {
|
||||
server.Port = 53
|
||||
}
|
||||
var networks []string
|
||||
if useTCP {
|
||||
networks = []string{N.NetworkTCP}
|
||||
} else {
|
||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||
}
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: uint16(rand.Uint32()),
|
||||
RecursionDesired: true,
|
||||
AuthenticatedData: ad,
|
||||
AuthenticatedData: true,
|
||||
},
|
||||
Question: []mDNS.Question{question},
|
||||
Compress: true,
|
||||
}
|
||||
request.SetEdns0(buf.UDPBufferSize, false)
|
||||
return t.exchangeUDP(ctx, server, request)
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
buffer := buf.Get(buf.UDPBufferSize)
|
||||
defer buf.Put(buffer)
|
||||
for _, network := range networks {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||
defer cancel()
|
||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated && network == N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
panic("unexpected")
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EMSGSIZE) {
|
||||
return t.exchangeTCP(ctx, server, request)
|
||||
}
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EMSGSIZE) {
|
||||
return t.exchangeTCP(ctx, server, request)
|
||||
}
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated {
|
||||
return t.exchangeTCP(ctx, server, request)
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
err = transport.WriteMessage(conn, 0, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.ReadMessage(conn)
|
||||
}
|
||||
|
||||
func (t *Transport) nameList(name string) []string {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
80
dns/transport/https_transport.go
Normal file
80
dns/transport/https_transport.go
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -53,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
err = dns.RcodeSuccess
|
||||
}
|
||||
}
|
||||
select {
|
||||
@@ -151,12 +151,6 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
|
||||
if server.Port == 0 {
|
||||
server.Port = 53
|
||||
}
|
||||
var networks []string
|
||||
if useTCP {
|
||||
networks = []string{N.NetworkTCP}
|
||||
} else {
|
||||
networks = []string{N.NetworkUDP, N.NetworkTCP}
|
||||
}
|
||||
request := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
Id: uint16(rand.Uint32()),
|
||||
@@ -167,43 +161,73 @@ func (t *Transport) exchangeOne(ctx context.Context, server M.Socksaddr, questio
|
||||
Compress: true,
|
||||
}
|
||||
request.SetEdns0(buf.UDPBufferSize, false)
|
||||
if !useTCP {
|
||||
return t.exchangeUDP(ctx, server, request, timeout)
|
||||
} else {
|
||||
return t.exchangeTCP(ctx, server, request, timeout)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeUDP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
|
||||
conn, err := t.dialer.DialContext(ctx, N.NetworkUDP, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
newDeadline := time.Now().Add(timeout)
|
||||
if deadline.After(newDeadline) {
|
||||
deadline = newDeadline
|
||||
}
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
buffer := buf.Get(buf.UDPBufferSize)
|
||||
defer buf.Put(buffer)
|
||||
for _, network := range networks {
|
||||
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
|
||||
defer cancel()
|
||||
conn, err := t.dialer.DialContext(ctx, network, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EMSGSIZE) && network == N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated && network == N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
rawMessage, err := request.PackBuffer(buffer)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "pack request")
|
||||
}
|
||||
panic("unexpected")
|
||||
_, err = conn.Write(rawMessage)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EMSGSIZE) {
|
||||
return t.exchangeTCP(ctx, server, request, timeout)
|
||||
}
|
||||
return nil, E.Cause(err, "write request")
|
||||
}
|
||||
n, err := conn.Read(buffer)
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EMSGSIZE) {
|
||||
return t.exchangeTCP(ctx, server, request, timeout)
|
||||
}
|
||||
return nil, E.Cause(err, "read response")
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err = response.Unpack(buffer[:n])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack response")
|
||||
}
|
||||
if response.Truncated {
|
||||
return t.exchangeTCP(ctx, server, request, timeout)
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
|
||||
func (t *Transport) exchangeTCP(ctx context.Context, server M.Socksaddr, request *mDNS.Msg, timeout time.Duration) (*mDNS.Msg, error) {
|
||||
conn, err := t.dialer.DialContext(ctx, N.NetworkTCP, server)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
if deadline, loaded := ctx.Deadline(); loaded && !deadline.IsZero() {
|
||||
newDeadline := time.Now().Add(timeout)
|
||||
if deadline.After(newDeadline) {
|
||||
deadline = newDeadline
|
||||
}
|
||||
conn.SetDeadline(deadline)
|
||||
}
|
||||
err = transport.WriteMessage(conn, 0, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return transport.ReadMessage(conn)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -2,6 +2,47 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 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
|
||||
|
||||
#### 1.12.5
|
||||
|
||||
* Fixes and improvements
|
||||
@@ -88,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.
|
||||
|
||||
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**:
|
||||
|
||||
@@ -150,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.
|
||||
|
||||
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 |
|
||||
|-------------|--------|-------|--------|----------|
|
||||
@@ -169,8 +212,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
|
||||
@@ -451,7 +494,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**:
|
||||
|
||||
@@ -482,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.
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,16 +11,22 @@ the project maintainer via [GitHub Sponsors](https://github.com/sponsors/nekohas
|
||||
|
||||

|
||||
|
||||
### Special Sponsors
|
||||
## Commercial Sponsors
|
||||
|
||||
**Viral Tech, Inc.**
|
||||
> [Warp](https://go.warp.dev/sing-box), Built for coding with multiple AI agents.
|
||||
|
||||
[](https://go.warp.dev/sing-box)
|
||||
|
||||
## Special Sponsors
|
||||
|
||||
> Viral Tech, Inc.
|
||||
|
||||
Helping us re-list sing-box apps on the Apple Store.
|
||||
|
||||
---
|
||||
|
||||
[](https://www.jetbrains.com)
|
||||
> [JetBrains](https://www.jetbrains.com)
|
||||
|
||||
Free license for the amazing IDEs.
|
||||
|
||||
---
|
||||
[](https://www.jetbrains.com)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
go.mod
14
go.mod
@@ -3,7 +3,7 @@ module github.com/sagernet/sing-box
|
||||
go 1.23.1
|
||||
|
||||
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
|
||||
@@ -15,8 +15,8 @@ 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.3
|
||||
github.com/mholt/acmez/v3 v3.1.2
|
||||
github.com/miekg/dns v1.1.67
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
@@ -26,17 +26,17 @@ require (
|
||||
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.8
|
||||
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.3
|
||||
github.com/sagernet/sing-quic v0.5.2-0.20250909083218-00a55617c0fb
|
||||
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.1-0.20250909064831-29d619807240
|
||||
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/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
|
||||
|
||||
28
go.sum
28
go.sum
@@ -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/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=
|
||||
@@ -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/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.3 h1:0m/yCxm3SK6kWve2lKiFb1pue1wHitJ8sQQD4Ikqde4=
|
||||
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/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
|
||||
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/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/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.6.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.7.8 h1:i3JBTzeEOqMRtYcyNV17LKvxkb3mr2Y/omM5ldvhCYo=
|
||||
github.com/sagernet/sing v0.7.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
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.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=
|
||||
@@ -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-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.1-0.20250909064831-29d619807240 h1:QJrYOLJB4A0ONEl1dmZtcyY9NmY6EOKAx3CblLOb+Y8=
|
||||
github.com/sagernet/sing-tun v0.7.1-0.20250909064831-29d619807240/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/tailscale v1.80.3-sing-box-1.12-mod.2 h1:MO7s4ni2bSfAOhcan2rdQSWCztkMXmqyg6jYPZp8bEE=
|
||||
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.2/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||
github.com/sagernet/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/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -456,20 +458,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,16 +27,16 @@ import (
|
||||
var _ adapter.RuleSet = (*LocalRuleSet)(nil)
|
||||
|
||||
type LocalRuleSet struct {
|
||||
ctx context.Context
|
||||
logger logger.Logger
|
||||
tag string
|
||||
rules []adapter.HeadlessRule
|
||||
metadata adapter.RuleSetMetadata
|
||||
fileFormat string
|
||||
watcher *fswatch.Watcher
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
refs atomic.Int32
|
||||
ctx context.Context
|
||||
logger logger.Logger
|
||||
tag string
|
||||
access sync.RWMutex
|
||||
rules []adapter.HeadlessRule
|
||||
metadata adapter.RuleSetMetadata
|
||||
fileFormat string
|
||||
watcher *fswatch.Watcher
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
refs atomic.Int32
|
||||
}
|
||||
|
||||
func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) {
|
||||
@@ -141,11 +141,11 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error {
|
||||
metadata.ContainsProcessRule = hasHeadlessRule(headlessRules, isProcessHeadlessRule)
|
||||
metadata.ContainsWIFIRule = hasHeadlessRule(headlessRules, isWIFIHeadlessRule)
|
||||
metadata.ContainsIPCIDRRule = hasHeadlessRule(headlessRules, isIPCIDRHeadlessRule)
|
||||
s.access.Lock()
|
||||
s.rules = rules
|
||||
s.metadata = metadata
|
||||
s.callbackAccess.Lock()
|
||||
callbacks := s.callbacks.Array()
|
||||
s.callbackAccess.Unlock()
|
||||
s.access.Unlock()
|
||||
for _, callback := range callbacks {
|
||||
callback(s)
|
||||
}
|
||||
@@ -157,10 +157,14 @@ func (s *LocalRuleSet) PostStart() error {
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) ExtractIPSet() []*netipx.IPSet {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return common.FlatMap(s.rules, extractIPSetFromRule)
|
||||
}
|
||||
|
||||
@@ -181,14 +185,14 @@ func (s *LocalRuleSet) Cleanup() {
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
|
||||
s.callbackAccess.Lock()
|
||||
defer s.callbackAccess.Unlock()
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
return s.callbacks.PushBack(callback)
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
|
||||
s.callbackAccess.Lock()
|
||||
defer s.callbackAccess.Unlock()
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
s.callbacks.Remove(element)
|
||||
}
|
||||
|
||||
|
||||
@@ -40,16 +40,16 @@ type RemoteRuleSet struct {
|
||||
logger logger.ContextLogger
|
||||
outbound adapter.OutboundManager
|
||||
options option.RuleSet
|
||||
metadata adapter.RuleSetMetadata
|
||||
updateInterval time.Duration
|
||||
dialer N.Dialer
|
||||
access sync.RWMutex
|
||||
rules []adapter.HeadlessRule
|
||||
metadata adapter.RuleSetMetadata
|
||||
lastUpdated time.Time
|
||||
lastEtag string
|
||||
updateTicker *time.Ticker
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
callbackAccess sync.Mutex
|
||||
callbacks list.List[adapter.RuleSetUpdateCallback]
|
||||
refs atomic.Int32
|
||||
}
|
||||
@@ -120,10 +120,14 @@ func (s *RemoteRuleSet) PostStart() error {
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) ExtractIPSet() []*netipx.IPSet {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
return common.FlatMap(s.rules, extractIPSetFromRule)
|
||||
}
|
||||
|
||||
@@ -144,14 +148,14 @@ func (s *RemoteRuleSet) Cleanup() {
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) RegisterCallback(callback adapter.RuleSetUpdateCallback) *list.Element[adapter.RuleSetUpdateCallback] {
|
||||
s.callbackAccess.Lock()
|
||||
defer s.callbackAccess.Unlock()
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
return s.callbacks.PushBack(callback)
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) UnregisterCallback(element *list.Element[adapter.RuleSetUpdateCallback]) {
|
||||
s.callbackAccess.Lock()
|
||||
defer s.callbackAccess.Unlock()
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
s.callbacks.Remove(element)
|
||||
}
|
||||
|
||||
@@ -185,13 +189,13 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error {
|
||||
return E.Cause(err, "parse rule_set.rules.[", i, "]")
|
||||
}
|
||||
}
|
||||
s.access.Lock()
|
||||
s.metadata.ContainsProcessRule = hasHeadlessRule(plainRuleSet.Rules, isProcessHeadlessRule)
|
||||
s.metadata.ContainsWIFIRule = hasHeadlessRule(plainRuleSet.Rules, isWIFIHeadlessRule)
|
||||
s.metadata.ContainsIPCIDRRule = hasHeadlessRule(plainRuleSet.Rules, isIPCIDRHeadlessRule)
|
||||
s.rules = rules
|
||||
s.callbackAccess.Lock()
|
||||
callbacks := s.callbacks.Array()
|
||||
s.callbackAccess.Unlock()
|
||||
s.access.Unlock()
|
||||
for _, callback := range callbacks {
|
||||
callback(s)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user