mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a812f2a46 | ||
|
|
fffe9fc566 | ||
|
|
6fdf27a701 | ||
|
|
7fa7d4f0a9 | ||
|
|
f511ebc1d4 | ||
|
|
84bbdc2eba | ||
|
|
568612fc70 | ||
|
|
d78828fd81 | ||
|
|
f56d9ab945 | ||
|
|
86fabd6a22 | ||
|
|
24a1e7cee4 | ||
|
|
223dd8bb1a | ||
|
|
68448de7d0 | ||
|
|
1ebff74c21 | ||
|
|
f0cd3422c1 | ||
|
|
e385a98ced | ||
|
|
670f32baee | ||
|
|
2747a00ba2 | ||
|
|
48e76038d0 | ||
|
|
6421252d44 | ||
|
|
216c4c8bd4 | ||
|
|
5841d410a1 | ||
|
|
63c8207d7a |
2
.github/setup_go_for_windows7.sh
vendored
2
.github/setup_go_for_windows7.sh
vendored
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION="1.25.3"
|
||||
VERSION="1.25.5"
|
||||
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
|
||||
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.3
|
||||
go-version: ^1.25.5
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -107,15 +107,15 @@ 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.3
|
||||
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
|
||||
go-version: ~1.24.10
|
||||
- name: Cache Go for Windows 7
|
||||
if: matrix.legacy_win7
|
||||
id: cache-go-for-windows7
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/go/go_win7
|
||||
key: go_win7_1253
|
||||
key: go_win7_1255
|
||||
- name: Setup Go for Windows 7
|
||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||
run: |-
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.3
|
||||
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.3
|
||||
go-version: ^1.25.5
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -479,7 +479,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ^1.25.3
|
||||
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: v2.4.0
|
||||
version: latest
|
||||
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.3
|
||||
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.3
|
||||
go-version: ^1.25.5
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
|
||||
@@ -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"]
|
||||
|
||||
4
Makefile
4
Makefile
@@ -38,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@v0.8.0
|
||||
go install -v mvdan.cc/gofumpt@latest
|
||||
go install -v github.com/daixiang0/gci@latest
|
||||
|
||||
lint:
|
||||
@@ -49,7 +49,7 @@ lint:
|
||||
GOOS=freebsd golangci-lint run ./...
|
||||
|
||||
lint_install:
|
||||
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
|
||||
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
|
||||
proto:
|
||||
@go run ./cmd/internal/protogen
|
||||
|
||||
@@ -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: a4e3c00f0f...863cd1ce4f
Submodule clients/apple updated: 84d8cf1757...532c140f05
File diff suppressed because it is too large
Load Diff
@@ -303,8 +303,6 @@ find:
|
||||
metadata.Protocol = C.ProtocolQUIC
|
||||
fingerprint, err := ja3.Compute(buffer.Bytes())
|
||||
if err != nil {
|
||||
metadata.Protocol = C.ProtocolQUIC
|
||||
metadata.Client = C.ClientChromium
|
||||
metadata.SniffContext = fragments
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
@@ -334,7 +332,7 @@ find:
|
||||
}
|
||||
|
||||
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
||||
if maybeUQUIC(fingerprint) {
|
||||
if isQUICGo(fingerprint) {
|
||||
metadata.Client = C.ClientQUICGo
|
||||
} else {
|
||||
metadata.Client = C.ClientChromium
|
||||
|
||||
@@ -1,21 +1,29 @@
|
||||
package sniff
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/common/ja3"
|
||||
)
|
||||
|
||||
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
|
||||
// The cronet without this behavior does not have version 115
|
||||
var uQUICChrome115 = &ja3.ClientHello{
|
||||
Version: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{4865, 4866, 4867},
|
||||
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
|
||||
EllipticCurves: []uint16{29, 23, 24},
|
||||
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
|
||||
}
|
||||
const (
|
||||
// X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
|
||||
x25519Kyber768Draft00 uint16 = 0x11EC // 4588
|
||||
// renegotiation_info extension used by Go crypto/tls
|
||||
extensionRenegotiationInfo uint16 = 0xFF01 // 65281
|
||||
)
|
||||
|
||||
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
|
||||
return !uQUICChrome115.Equals(fingerprint, true)
|
||||
// isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
|
||||
// Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium
|
||||
// since it uses the same TLS fingerprint, so it will be identified as Chromium.
|
||||
func isQUICGo(fingerprint *ja3.ClientHello) bool {
|
||||
for _, curve := range fingerprint.EllipticCurves {
|
||||
if curve == x25519Kyber768Draft00 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, ext := range fingerprint.Extensions {
|
||||
if ext == extensionRenegotiationInfo {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
188
common/sniff/quic_capture_test.go
Normal file
188
common/sniff/quic_capture_test.go
Normal file
@@ -0,0 +1,188 @@
|
||||
package sniff_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/sniff"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
|
||||
t.Parallel()
|
||||
const testSNI = "test.example.com"
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer udpConn.Close()
|
||||
|
||||
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||
packetsChan := make(chan [][]byte, 1)
|
||||
|
||||
go func() {
|
||||
var packets [][]byte
|
||||
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
for i := 0; i < 10; i++ {
|
||||
buf := make([]byte, 2048)
|
||||
n, _, err := udpConn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
packets = append(packets, buf[:n])
|
||||
}
|
||||
packetsChan <- packets
|
||||
}()
|
||||
|
||||
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer clientConn.Close()
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: testSNI,
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h3"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||
|
||||
select {
|
||||
case packets := <-packetsChan:
|
||||
t.Logf("Captured %d packets", len(packets))
|
||||
|
||||
var metadata adapter.InboundContext
|
||||
for i, pkt := range packets {
|
||||
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
|
||||
if metadata.Domain != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
|
||||
t.Logf("Domain: %s", metadata.Domain)
|
||||
t.Logf("Client: %s", metadata.Client)
|
||||
t.Logf("Protocol: %s", metadata.Protocol)
|
||||
|
||||
// The client should be identified as quic-go, not chromium
|
||||
// Current issue: it's being identified as chromium
|
||||
if metadata.Client == "chromium" {
|
||||
t.Log("WARNING: quic-go is being misidentified as chromium!")
|
||||
}
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const testSNI = "test.example.com"
|
||||
|
||||
// Create UDP listener to capture ALL initial packets
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer udpConn.Close()
|
||||
|
||||
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||
|
||||
// Channel to receive captured packets
|
||||
packetsChan := make(chan [][]byte, 1)
|
||||
|
||||
// Start goroutine to capture packets
|
||||
go func() {
|
||||
var packets [][]byte
|
||||
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
for i := 0; i < 5; i++ { // Capture up to 5 packets
|
||||
buf := make([]byte, 2048)
|
||||
n, _, err := udpConn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
packets = append(packets, buf[:n])
|
||||
}
|
||||
packetsChan <- packets
|
||||
}()
|
||||
|
||||
// Create QUIC client connection (will fail but we capture the initial packet)
|
||||
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer clientConn.Close()
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: testSNI,
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h3"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// This will fail (no server) but sends initial packet
|
||||
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||
|
||||
// Wait for captured packets
|
||||
select {
|
||||
case packets := <-packetsChan:
|
||||
t.Logf("Captured %d QUIC packets", len(packets))
|
||||
|
||||
for i, packet := range packets {
|
||||
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
|
||||
}
|
||||
|
||||
// Test sniffer with first packet
|
||||
if len(packets) > 0 {
|
||||
var metadata adapter.InboundContext
|
||||
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
|
||||
|
||||
t.Logf("First packet sniff error: %v", err)
|
||||
t.Logf("Protocol: %s", metadata.Protocol)
|
||||
t.Logf("Domain: %s", metadata.Domain)
|
||||
t.Logf("Client: %s", metadata.Client)
|
||||
|
||||
// If first packet needs more data, try with subsequent packets
|
||||
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
|
||||
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
|
||||
t.Log("First packet needs more data, trying subsequent packets with shared context...")
|
||||
for i := 1; i < len(packets); i++ {
|
||||
// Reuse same metadata to accumulate fragments
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
|
||||
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
|
||||
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print hex dump for debugging
|
||||
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
|
||||
|
||||
// Log final results
|
||||
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
|
||||
|
||||
// Verify SNI extraction
|
||||
if metadata.Domain == "" {
|
||||
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
|
||||
} else {
|
||||
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
|
||||
}
|
||||
|
||||
// Check client identification - quic-go should be identified as quic-go, not chromium
|
||||
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
|
||||
}
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timeout waiting for QUIC packets")
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||
require.Empty(t, metadata.Client)
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||
require.NoError(t, err)
|
||||
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||
require.Empty(t, metadata.Client)
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -353,68 +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)
|
||||
if response4 == nil {
|
||||
return nil, false
|
||||
}
|
||||
response6, _ := c.loadResponse(dns.Question{
|
||||
Name: dnsName,
|
||||
Qtype: dns.TypeAAAA,
|
||||
Qclass: dns.ClassINET,
|
||||
}, nil)
|
||||
if response6 == nil {
|
||||
return nil, false
|
||||
}
|
||||
return sortAddresses(MessageToAddresses(response4), MessageToAddresses(response6), strategy), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *Client) ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool) {
|
||||
if c.disableCache || c.independentCache || len(message.Question) != 1 {
|
||||
return nil, false
|
||||
}
|
||||
question := message.Question[0]
|
||||
response, ttl := c.loadResponse(question, nil)
|
||||
if response == nil {
|
||||
return nil, false
|
||||
}
|
||||
logCachedResponse(c.logger, ctx, response, ttl)
|
||||
response.Id = message.Id
|
||||
return response, true
|
||||
}
|
||||
|
||||
func sortAddresses(response4 []netip.Addr, response6 []netip.Addr, strategy C.DomainStrategy) []netip.Addr {
|
||||
if strategy == C.DomainStrategyPreferIPv6 {
|
||||
return append(response6, response4...)
|
||||
|
||||
174
dns/router.go
174
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{}
|
||||
|
||||
@@ -243,6 +243,7 @@ func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn ne
|
||||
defer buffer.Release()
|
||||
|
||||
for {
|
||||
buffer.Reset()
|
||||
_, _, err := buffer.ReadPacketFrom(packetConn)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.ErrShortBuffer) {
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.15
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.14
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.12.13
|
||||
|
||||
* Fix naive inbound
|
||||
* Fixes and improvements
|
||||
|
||||
__Unfortunately, for non-technical reasons, we are currently unable to notarize the standalone version of the macOS client:
|
||||
because system extensions require signatures to function, we have had to temporarily halt its release.__
|
||||
|
||||
__We plan to fix the App Store release issue and launch a new standalone desktop client, but until then,
|
||||
only clients on TestFlight will be available (unless you have an Apple Developer Program and compile from source code).__
|
||||
|
||||
#### 1.12.12
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
"method": "2022-blake3-aes-128-gcm",
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||
"managed": false,
|
||||
"multiplex": {}
|
||||
}
|
||||
```
|
||||
@@ -86,6 +87,10 @@ Both if empty.
|
||||
| 2022 methods | `sing-box generate rand --base64 <Key Length>` |
|
||||
| other methods | any string |
|
||||
|
||||
#### managed
|
||||
|
||||
Defaults to `false`. Enable this when the inbound is managed by the [SSM API](/configuration/service/ssm-api) for dynamic user.
|
||||
|
||||
#### multiplex
|
||||
|
||||
See [Multiplex](/configuration/shared/multiplex#inbound) for details.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
"method": "2022-blake3-aes-128-gcm",
|
||||
"password": "8JCsPssfgS8tiRwiMlhARg==",
|
||||
"managed": false,
|
||||
"multiplex": {}
|
||||
}
|
||||
```
|
||||
@@ -86,6 +87,10 @@ See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
| 2022 methods | `sing-box generate rand --base64 <密钥长度>` |
|
||||
| other methods | 任意字符串 |
|
||||
|
||||
#### managed
|
||||
|
||||
默认为 `false`。当该入站需要由 [SSM API](/zh/configuration/service/ssm-api) 管理用户时必须启用此字段。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
|
||||
2
go.mod
2
go.mod
@@ -27,7 +27,7 @@ require (
|
||||
github.com/sagernet/gomobile v0.1.8
|
||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
|
||||
github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
|
||||
github.com/sagernet/sing v0.7.13
|
||||
github.com/sagernet/sing v0.7.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
|
||||
|
||||
4
go.sum
4
go.sum
@@ -167,8 +167,8 @@ github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/l
|
||||
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.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
|
||||
github.com/sagernet/sing v0.7.13/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=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
)
|
||||
|
||||
@@ -60,37 +61,40 @@ func checkOptions(options *Options) error {
|
||||
|
||||
func checkInbounds(inbounds []Inbound) error {
|
||||
seen := make(map[string]bool)
|
||||
for _, inbound := range inbounds {
|
||||
if inbound.Tag == "" {
|
||||
continue
|
||||
for i, inbound := range inbounds {
|
||||
tag := inbound.Tag
|
||||
if tag == "" {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
if seen[inbound.Tag] {
|
||||
return E.New("duplicate inbound tag: ", inbound.Tag)
|
||||
if seen[tag] {
|
||||
return E.New("duplicate inbound tag: ", tag)
|
||||
}
|
||||
seen[inbound.Tag] = true
|
||||
seen[tag] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkOutbounds(outbounds []Outbound, endpoints []Endpoint) error {
|
||||
seen := make(map[string]bool)
|
||||
for _, outbound := range outbounds {
|
||||
if outbound.Tag == "" {
|
||||
continue
|
||||
for i, outbound := range outbounds {
|
||||
tag := outbound.Tag
|
||||
if tag == "" {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
if seen[outbound.Tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", outbound.Tag)
|
||||
if seen[tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", tag)
|
||||
}
|
||||
seen[outbound.Tag] = true
|
||||
seen[tag] = true
|
||||
}
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Tag == "" {
|
||||
continue
|
||||
for i, endpoint := range endpoints {
|
||||
tag := endpoint.Tag
|
||||
if tag == "" {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
if seen[endpoint.Tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", endpoint.Tag)
|
||||
if seen[tag] {
|
||||
return E.New("duplicate outbound/endpoint tag: ", tag)
|
||||
}
|
||||
seen[endpoint.Tag] = true
|
||||
seen[tag] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -339,26 +341,42 @@ func (t *Endpoint) DialContext(ctx context.Context, network string, destination
|
||||
}
|
||||
return N.DialSerial(ctx, t, network, destination, destinationAddresses)
|
||||
}
|
||||
addr := tcpip.FullAddress{
|
||||
addr4, addr6 := t.server.TailscaleIPs()
|
||||
remoteAddr := tcpip.FullAddress{
|
||||
NIC: 1,
|
||||
Port: destination.Port,
|
||||
Addr: addressFromAddr(destination.Addr),
|
||||
}
|
||||
var localAddr tcpip.FullAddress
|
||||
var networkProtocol tcpip.NetworkProtocolNumber
|
||||
if destination.IsIPv4() {
|
||||
if !addr4.IsValid() {
|
||||
return nil, E.New("missing Tailscale IPv4 address")
|
||||
}
|
||||
networkProtocol = header.IPv4ProtocolNumber
|
||||
localAddr = tcpip.FullAddress{
|
||||
NIC: 1,
|
||||
Addr: addressFromAddr(addr4),
|
||||
}
|
||||
} else {
|
||||
if !addr6.IsValid() {
|
||||
return nil, E.New("missing Tailscale IPv6 address")
|
||||
}
|
||||
networkProtocol = header.IPv6ProtocolNumber
|
||||
localAddr = tcpip.FullAddress{
|
||||
NIC: 1,
|
||||
Addr: addressFromAddr(addr6),
|
||||
}
|
||||
}
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP:
|
||||
tcpConn, err := gonet.DialContextTCP(ctx, t.stack, addr, networkProtocol)
|
||||
tcpConn, err := gonet.DialTCPWithBind(ctx, t.stack, localAddr, remoteAddr, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcpConn, nil
|
||||
case N.NetworkUDP:
|
||||
udpConn, err := gonet.DialUDP(t.stack, nil, &addr, networkProtocol)
|
||||
udpConn, err := gonet.DialUDP(t.stack, &localAddr, &remoteAddr, networkProtocol)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -456,20 +474,20 @@ func (t *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn,
|
||||
metadata.Inbound = t.Tag()
|
||||
metadata.InboundType = t.Type()
|
||||
metadata.Source = source
|
||||
metadata.Destination = destination
|
||||
addr4, addr6 := t.server.TailscaleIPs()
|
||||
switch destination.Addr {
|
||||
case addr4:
|
||||
metadata.OriginDestination = destination
|
||||
destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1})
|
||||
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
||||
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
|
||||
case addr6:
|
||||
metadata.OriginDestination = destination
|
||||
destination.Addr = netip.IPv6Loopback()
|
||||
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination)
|
||||
conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, destination)
|
||||
}
|
||||
metadata.Destination = destination
|
||||
t.logger.InfoContext(ctx, "inbound packet connection from ", source)
|
||||
t.logger.InfoContext(ctx, "inbound packet connection to ", destination)
|
||||
t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination)
|
||||
t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -14,11 +14,13 @@ type StreamWrapper struct {
|
||||
|
||||
func (s *StreamWrapper) Read(p []byte) (n int, err error) {
|
||||
n, err = s.Stream.Read(p)
|
||||
//nolint:staticcheck
|
||||
return n, baderror.WrapQUIC(err)
|
||||
}
|
||||
|
||||
func (s *StreamWrapper) Write(p []byte) (n int, err error) {
|
||||
n, err = s.Stream.Write(p)
|
||||
//nolint:staticcheck
|
||||
return n, baderror.WrapQUIC(err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user