mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-03-01 10:09:54 +00:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9033717190 | ||
|
|
dda1d525c1 | ||
|
|
3035ae89e3 | ||
|
|
c251e411e5 | ||
|
|
f6722ab79b | ||
|
|
4ca515896b | ||
|
|
836c972c54 | ||
|
|
43509da1a9 | ||
|
|
3752cb044f | ||
|
|
30391b40c4 | ||
|
|
05fbf552ec | ||
|
|
e4143cf1ad | ||
|
|
5eaf5d16ce | ||
|
|
9dee264f13 | ||
|
|
50480406cf | ||
|
|
6eb27ac3dc | ||
|
|
a949ad883c | ||
|
|
20bf57c117 | ||
|
|
60a9312057 | ||
|
|
9fda032a28 | ||
|
|
c3399fd346 | ||
|
|
445083b624 |
42
.github/workflows/build.yml
vendored
42
.github/workflows/build.yml
vendored
@@ -59,8 +59,8 @@ jobs:
|
|||||||
- { goos: linux, goarch: s390x, output: s390x, debian: s390x, rpm: s390x }
|
- { goos: linux, goarch: s390x, output: s390x, debian: s390x, rpm: s390x }
|
||||||
- { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le }
|
- { goos: linux, goarch: ppc64le, output: ppc64le, debian: ppc64el, rpm: ppc64le }
|
||||||
|
|
||||||
# Go 1.25 with special patch can work on Windows 7
|
# Go 1.26 with special patch can work on Windows 7
|
||||||
# https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
# https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
|
||||||
- { goos: windows, goarch: '386', output: '386' }
|
- { goos: windows, goarch: '386', output: '386' }
|
||||||
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
|
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible } # old style file name will be removed in next released
|
||||||
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
|
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64 }
|
||||||
@@ -82,6 +82,13 @@ jobs:
|
|||||||
- { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 }
|
- { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 }
|
||||||
- { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 }
|
- { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 }
|
||||||
|
|
||||||
|
# Go 1.25 with special patch can work on Windows 7
|
||||||
|
# https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||||
|
- { goos: windows, goarch: '386', output: '386-go125', goversion: '1.25' }
|
||||||
|
- { goos: windows, goarch: amd64, goamd64: v1, output: amd64-v1-go125, goversion: '1.25' }
|
||||||
|
- { goos: windows, goarch: amd64, goamd64: v2, output: amd64-v2-go125, goversion: '1.25' }
|
||||||
|
- { goos: windows, goarch: amd64, goamd64: v3, output: amd64-v3-go125, goversion: '1.25' }
|
||||||
|
|
||||||
# Go 1.24 with special patch can work on Windows 7
|
# Go 1.24 with special patch can work on Windows 7
|
||||||
# https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
|
# https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
|
||||||
- { goos: windows, goarch: '386', output: '386-go124', goversion: '1.24' }
|
- { goos: windows, goarch: '386', output: '386-go124', goversion: '1.24' }
|
||||||
@@ -154,7 +161,7 @@ jobs:
|
|||||||
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }}
|
if: ${{ matrix.jobs.goversion == '' && matrix.jobs.abi != '1' }}
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.26'
|
||||||
check-latest: true # Always check for the latest patch release
|
check-latest: true # Always check for the latest patch release
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
@@ -164,14 +171,30 @@ jobs:
|
|||||||
go-version: ${{ matrix.jobs.goversion }}
|
go-version: ${{ matrix.jobs.goversion }}
|
||||||
check-latest: true # Always check for the latest patch release
|
check-latest: true # Always check for the latest patch release
|
||||||
|
|
||||||
- name: Set up Go1.24 loongarch abi1
|
- name: Set up Go1.25 loongarch abi1
|
||||||
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }}
|
||||||
run: |
|
run: |
|
||||||
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.24.0/go1.24.0.linux-amd64-abi1.tar.gz
|
wget -q https://github.com/MetaCubeX/loongarch64-golang/releases/download/1.25.5/go1.25.5.linux-amd64-abi1.tar.gz
|
||||||
sudo tar zxf go1.24.0.linux-amd64-abi1.tar.gz -C /usr/local
|
sudo tar zxf go1.25.5.linux-amd64-abi1.tar.gz -C /usr/local
|
||||||
echo "/usr/local/go/bin" >> $GITHUB_PATH
|
echo "/usr/local/go/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
# this patch file only works on golang1.26.x
|
||||||
|
# that means after golang1.27 release it must be changed
|
||||||
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.26/
|
||||||
|
# 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"
|
||||||
|
# f0894a00f4b756d4b9b4078af2e686b359493583: "os: remove 5ms sleep on Windows in (*Process).Wait"
|
||||||
|
# sepical fix:
|
||||||
|
# - os.RemoveAll not working on Windows7
|
||||||
|
- name: Revert Golang1.26 commit for Windows7/8
|
||||||
|
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
|
||||||
|
run: |
|
||||||
|
cd $(go env GOROOT)
|
||||||
|
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.26.patch
|
||||||
|
|
||||||
# this patch file only works on golang1.25.x
|
# this patch file only works on golang1.25.x
|
||||||
# that means after golang1.26 release it must be changed
|
# that means after golang1.26 release it must be changed
|
||||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.25/
|
||||||
@@ -184,12 +207,11 @@ jobs:
|
|||||||
# sepical fix:
|
# sepical fix:
|
||||||
# - os.RemoveAll not working on Windows7
|
# - os.RemoveAll not working on Windows7
|
||||||
- name: Revert Golang1.25 commit for Windows7/8
|
- name: Revert Golang1.25 commit for Windows7/8
|
||||||
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '' }}
|
if: ${{ matrix.jobs.goos == 'windows' && matrix.jobs.goversion == '1.25' }}
|
||||||
run: |
|
run: |
|
||||||
cd $(go env GOROOT)
|
cd $(go env GOROOT)
|
||||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.25.patch
|
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.25.patch
|
||||||
|
|
||||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
|
||||||
# this patch file only works on golang1.24.x
|
# this patch file only works on golang1.24.x
|
||||||
# that means after golang1.25 release it must be changed
|
# that means after golang1.25 release it must be changed
|
||||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
|
||||||
@@ -204,7 +226,6 @@ jobs:
|
|||||||
cd $(go env GOROOT)
|
cd $(go env GOROOT)
|
||||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.24.patch
|
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.24.patch
|
||||||
|
|
||||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
|
||||||
# this patch file only works on golang1.23.x
|
# this patch file only works on golang1.23.x
|
||||||
# that means after golang1.24 release it must be changed
|
# that means after golang1.24 release it must be changed
|
||||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
|
||||||
@@ -219,7 +240,6 @@ jobs:
|
|||||||
cd $(go env GOROOT)
|
cd $(go env GOROOT)
|
||||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.23.patch
|
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.23.patch
|
||||||
|
|
||||||
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
|
|
||||||
# this patch file only works on golang1.22.x
|
# this patch file only works on golang1.22.x
|
||||||
# that means after golang1.23 release it must be changed
|
# that means after golang1.23 release it must be changed
|
||||||
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
|
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
|
||||||
|
|||||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- 'ubuntu-24.04-arm' # arm64 linux
|
- 'ubuntu-24.04-arm' # arm64 linux
|
||||||
- 'macos-15-intel' # amd64 macos
|
- 'macos-15-intel' # amd64 macos
|
||||||
go-version:
|
go-version:
|
||||||
- '1.26.0-rc.3'
|
- '1.26'
|
||||||
- '1.25'
|
- '1.25'
|
||||||
- '1.24'
|
- '1.24'
|
||||||
- '1.23'
|
- '1.23'
|
||||||
@@ -51,17 +51,11 @@ jobs:
|
|||||||
check-latest: true # Always check for the latest patch release
|
check-latest: true # Always check for the latest patch release
|
||||||
|
|
||||||
- name: Revert Golang commit for Windows7/8
|
- name: Revert Golang commit for Windows7/8
|
||||||
if: ${{ runner.os == 'Windows' && matrix.go-version != '1.20' && matrix.go-version != '1.26.0-rc.3' }}
|
if: ${{ runner.os == 'Windows' && matrix.go-version != '1.20' }}
|
||||||
run: |
|
run: |
|
||||||
cd $(go env GOROOT)
|
cd $(go env GOROOT)
|
||||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go${{matrix.go-version}}.patch
|
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go${{matrix.go-version}}.patch
|
||||||
|
|
||||||
- name: Revert Golang commit for Windows7/8
|
|
||||||
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.26.0-rc.3' }}
|
|
||||||
run: |
|
|
||||||
cd $(go env GOROOT)
|
|
||||||
patch --verbose -p 1 < $GITHUB_WORKSPACE/.github/patch/go1.26.patch
|
|
||||||
|
|
||||||
- name: Remove inbound test for macOS
|
- name: Remove inbound test for macOS
|
||||||
if: ${{ runner.os == 'macOS' }}
|
if: ${{ runner.os == 'macOS' }}
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
||||||
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||||
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||||
|
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@@ -28,16 +29,17 @@ type Mieru struct {
|
|||||||
|
|
||||||
type MieruOption struct {
|
type MieruOption struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port,omitempty"`
|
Port int `proxy:"port,omitempty"`
|
||||||
PortRange string `proxy:"port-range,omitempty"`
|
PortRange string `proxy:"port-range,omitempty"`
|
||||||
Transport string `proxy:"transport"`
|
Transport string `proxy:"transport"`
|
||||||
UDP bool `proxy:"udp,omitempty"`
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
UserName string `proxy:"username"`
|
UserName string `proxy:"username"`
|
||||||
Password string `proxy:"password"`
|
Password string `proxy:"password"`
|
||||||
Multiplexing string `proxy:"multiplexing,omitempty"`
|
Multiplexing string `proxy:"multiplexing,omitempty"`
|
||||||
HandshakeMode string `proxy:"handshake-mode,omitempty"`
|
HandshakeMode string `proxy:"handshake-mode,omitempty"`
|
||||||
|
TrafficPattern string `proxy:"traffic-pattern,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type mieruPacketDialer struct {
|
type mieruPacketDialer struct {
|
||||||
@@ -291,6 +293,10 @@ func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, erro
|
|||||||
if handshakeMode, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; ok {
|
if handshakeMode, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; ok {
|
||||||
config.Profile.HandshakeMode = (*mierupb.HandshakeMode)(&handshakeMode)
|
config.Profile.HandshakeMode = (*mierupb.HandshakeMode)(&handshakeMode)
|
||||||
}
|
}
|
||||||
|
if option.TrafficPattern != "" {
|
||||||
|
trafficPattern, _ := mierutp.Decode(option.TrafficPattern)
|
||||||
|
config.Profile.TrafficPattern = trafficPattern
|
||||||
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,6 +351,15 @@ func validateMieruOption(option MieruOption) error {
|
|||||||
return fmt.Errorf("invalid handshake mode: %s", option.HandshakeMode)
|
return fmt.Errorf("invalid handshake mode: %s", option.HandshakeMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if option.TrafficPattern != "" {
|
||||||
|
trafficPattern, err := mierutp.Decode(option.TrafficPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode traffic pattern %q: %w", option.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||||
|
return fmt.Errorf("invalid traffic pattern %q: %w", option.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,13 @@ func TestNewMieru(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
option: MieruOption{
|
option: MieruOption{
|
||||||
Name: "test",
|
Name: "test",
|
||||||
Server: "example.com",
|
Server: "example.com",
|
||||||
Port: 10003,
|
Port: 10003,
|
||||||
Transport: "UDP",
|
Transport: "UDP",
|
||||||
UserName: "test",
|
UserName: "test",
|
||||||
Password: "test",
|
Password: "test",
|
||||||
|
TrafficPattern: "GgQIARAK",
|
||||||
},
|
},
|
||||||
wantBaseAddr: "example.com:10003",
|
wantBaseAddr: "example.com:10003",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ import (
|
|||||||
|
|
||||||
type RealityOptions struct {
|
type RealityOptions struct {
|
||||||
PublicKey string `proxy:"public-key"`
|
PublicKey string `proxy:"public-key"`
|
||||||
ShortID string `proxy:"short-id"`
|
ShortID string `proxy:"short-id,omitempty"`
|
||||||
|
|
||||||
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
|
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
|
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
"github.com/metacubex/mihomo/transport/sudoku"
|
"github.com/metacubex/mihomo/transport/sudoku"
|
||||||
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Sudoku struct {
|
type Sudoku struct {
|
||||||
@@ -18,32 +19,43 @@ type Sudoku struct {
|
|||||||
option *SudokuOption
|
option *SudokuOption
|
||||||
baseConf sudoku.ProtocolConfig
|
baseConf sudoku.ProtocolConfig
|
||||||
|
|
||||||
httpMaskMu sync.Mutex
|
|
||||||
httpMaskClient *sudoku.HTTPMaskTunnelClient
|
|
||||||
|
|
||||||
muxMu sync.Mutex
|
muxMu sync.Mutex
|
||||||
muxClient *sudoku.MultiplexClient
|
muxClient *sudoku.MultiplexClient
|
||||||
|
|
||||||
|
httpMaskMu sync.Mutex
|
||||||
|
httpMaskClient *httpmask.TunnelClient
|
||||||
|
httpMaskKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
type SudokuOption struct {
|
type SudokuOption struct {
|
||||||
BasicOption
|
BasicOption
|
||||||
Name string `proxy:"name"`
|
Name string `proxy:"name"`
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port"`
|
||||||
Key string `proxy:"key"`
|
Key string `proxy:"key"`
|
||||||
AEADMethod string `proxy:"aead-method,omitempty"`
|
AEADMethod string `proxy:"aead-method,omitempty"`
|
||||||
PaddingMin *int `proxy:"padding-min,omitempty"`
|
PaddingMin *int `proxy:"padding-min,omitempty"`
|
||||||
PaddingMax *int `proxy:"padding-max,omitempty"`
|
PaddingMax *int `proxy:"padding-max,omitempty"`
|
||||||
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
TableType string `proxy:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
||||||
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
|
EnablePureDownlink *bool `proxy:"enable-pure-downlink,omitempty"`
|
||||||
HTTPMask bool `proxy:"http-mask,omitempty"`
|
HTTPMask *bool `proxy:"http-mask,omitempty"`
|
||||||
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
|
HTTPMaskMode string `proxy:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto", "ws"
|
||||||
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
|
HTTPMaskTLS bool `proxy:"http-mask-tls,omitempty"` // only for http-mask-mode stream/poll/auto
|
||||||
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
|
HTTPMaskHost string `proxy:"http-mask-host,omitempty"` // optional Host/SNI override (domain or domain:port)
|
||||||
PathRoot string `proxy:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
|
PathRoot string `proxy:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
|
||||||
HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto" (reuse h1/h2), "on" (single tunnel, multi-target)
|
HTTPMaskMultiplex string `proxy:"http-mask-multiplex,omitempty"` // "off" (default), "auto" (reuse h1/h2), "on" (single tunnel, multi-target)
|
||||||
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
HTTPMaskOptions *SudokuHTTPMaskOptions `proxy:"httpmask,omitempty"`
|
||||||
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
|
CustomTable string `proxy:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
||||||
|
CustomTables []string `proxy:"custom-tables,omitempty"` // optional table rotation patterns, overrides custom-table when non-empty
|
||||||
|
}
|
||||||
|
|
||||||
|
type SudokuHTTPMaskOptions struct {
|
||||||
|
Disable bool `proxy:"disable,omitempty"`
|
||||||
|
Mode string `proxy:"mode,omitempty"`
|
||||||
|
TLS bool `proxy:"tls,omitempty"`
|
||||||
|
Host string `proxy:"host,omitempty"`
|
||||||
|
PathRoot string `proxy:"path_root,omitempty"`
|
||||||
|
Multiplex string `proxy:"multiplex,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DialContext implements C.ProxyAdapter
|
// DialContext implements C.ProxyAdapter
|
||||||
@@ -73,7 +85,7 @@ func (s *Sudoku) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
|
|||||||
return nil, fmt.Errorf("encode target address failed: %w", err)
|
return nil, fmt.Errorf("encode target address failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = c.Write(addrBuf); err != nil {
|
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
return nil, fmt.Errorf("send target address failed: %w", err)
|
return nil, fmt.Errorf("send target address failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,9 +108,9 @@ func (s *Sudoku) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = sudoku.WritePreface(c); err != nil {
|
if err = sudoku.WriteKIPMessage(c, sudoku.KIPTypeStartUoT, nil); err != nil {
|
||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
return nil, fmt.Errorf("send uot preface failed: %w", err)
|
return nil, fmt.Errorf("start uot failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPacketConn(N.NewThreadSafePacketConn(sudoku.NewUoTPacketConn(c)), s), nil
|
return newPacketConn(N.NewThreadSafePacketConn(sudoku.NewUoTPacketConn(c)), s), nil
|
||||||
@@ -141,32 +153,45 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
|||||||
return nil, fmt.Errorf("key is required")
|
return nil, fmt.Errorf("key is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
tableType := strings.ToLower(option.TableType)
|
defaultConf := sudoku.DefaultConfig()
|
||||||
if tableType == "" {
|
tableType, err := sudoku.NormalizeTableType(option.TableType)
|
||||||
tableType = "prefer_ascii"
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if tableType != "prefer_ascii" && tableType != "prefer_entropy" {
|
paddingMin, paddingMax := sudoku.ResolvePadding(option.PaddingMin, option.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax)
|
||||||
return nil, fmt.Errorf("table-type must be prefer_ascii or prefer_entropy")
|
enablePureDownlink := sudoku.DerefBool(option.EnablePureDownlink, defaultConf.EnablePureDownlink)
|
||||||
|
|
||||||
|
disableHTTPMask := defaultConf.DisableHTTPMask
|
||||||
|
if option.HTTPMask != nil {
|
||||||
|
disableHTTPMask = !*option.HTTPMask
|
||||||
|
}
|
||||||
|
httpMaskMode := defaultConf.HTTPMaskMode
|
||||||
|
if option.HTTPMaskMode != "" {
|
||||||
|
httpMaskMode = option.HTTPMaskMode
|
||||||
|
}
|
||||||
|
httpMaskTLS := option.HTTPMaskTLS
|
||||||
|
httpMaskHost := option.HTTPMaskHost
|
||||||
|
pathRoot := strings.TrimSpace(option.PathRoot)
|
||||||
|
httpMaskMultiplex := defaultConf.HTTPMaskMultiplex
|
||||||
|
if option.HTTPMaskMultiplex != "" {
|
||||||
|
httpMaskMultiplex = option.HTTPMaskMultiplex
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConf := sudoku.DefaultConfig()
|
if hm := option.HTTPMaskOptions; hm != nil {
|
||||||
paddingMin := defaultConf.PaddingMin
|
disableHTTPMask = hm.Disable
|
||||||
paddingMax := defaultConf.PaddingMax
|
if hm.Mode != "" {
|
||||||
if option.PaddingMin != nil {
|
httpMaskMode = hm.Mode
|
||||||
paddingMin = *option.PaddingMin
|
}
|
||||||
}
|
httpMaskTLS = hm.TLS
|
||||||
if option.PaddingMax != nil {
|
httpMaskHost = hm.Host
|
||||||
paddingMax = *option.PaddingMax
|
if pr := strings.TrimSpace(hm.PathRoot); pr != "" {
|
||||||
}
|
pathRoot = pr
|
||||||
if option.PaddingMin == nil && option.PaddingMax != nil && paddingMax < paddingMin {
|
}
|
||||||
paddingMin = paddingMax
|
if mux := strings.TrimSpace(hm.Multiplex); mux != "" {
|
||||||
}
|
httpMaskMultiplex = mux
|
||||||
if option.PaddingMax == nil && option.PaddingMin != nil && paddingMax < paddingMin {
|
} else {
|
||||||
paddingMax = paddingMin
|
httpMaskMultiplex = defaultConf.HTTPMaskMultiplex
|
||||||
}
|
}
|
||||||
enablePureDownlink := defaultConf.EnablePureDownlink
|
|
||||||
if option.EnablePureDownlink != nil {
|
|
||||||
enablePureDownlink = *option.EnablePureDownlink
|
|
||||||
}
|
}
|
||||||
|
|
||||||
baseConf := sudoku.ProtocolConfig{
|
baseConf := sudoku.ProtocolConfig{
|
||||||
@@ -177,20 +202,14 @@ func NewSudoku(option SudokuOption) (*Sudoku, error) {
|
|||||||
PaddingMax: paddingMax,
|
PaddingMax: paddingMax,
|
||||||
EnablePureDownlink: enablePureDownlink,
|
EnablePureDownlink: enablePureDownlink,
|
||||||
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
|
HandshakeTimeoutSeconds: defaultConf.HandshakeTimeoutSeconds,
|
||||||
DisableHTTPMask: !option.HTTPMask,
|
DisableHTTPMask: disableHTTPMask,
|
||||||
HTTPMaskMode: defaultConf.HTTPMaskMode,
|
HTTPMaskMode: httpMaskMode,
|
||||||
HTTPMaskTLSEnabled: option.HTTPMaskTLS,
|
HTTPMaskTLSEnabled: httpMaskTLS,
|
||||||
HTTPMaskHost: option.HTTPMaskHost,
|
HTTPMaskHost: httpMaskHost,
|
||||||
HTTPMaskPathRoot: strings.TrimSpace(option.PathRoot),
|
HTTPMaskPathRoot: pathRoot,
|
||||||
HTTPMaskMultiplex: defaultConf.HTTPMaskMultiplex,
|
HTTPMaskMultiplex: httpMaskMultiplex,
|
||||||
}
|
}
|
||||||
if option.HTTPMaskMode != "" {
|
tables, err := sudoku.NewClientTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
|
||||||
baseConf.HTTPMaskMode = option.HTTPMaskMode
|
|
||||||
}
|
|
||||||
if option.HTTPMaskMultiplex != "" {
|
|
||||||
baseConf.HTTPMaskMultiplex = option.HTTPMaskMultiplex
|
|
||||||
}
|
|
||||||
tables, err := sudoku.NewTablesWithCustomPatterns(sudoku.ClientAEADSeed(option.Key), tableType, option.CustomTable, option.CustomTables)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("build table(s) failed: %w", err)
|
return nil, fmt.Errorf("build table(s) failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -244,7 +263,7 @@ func normalizeHTTPMaskMultiplex(mode string) string {
|
|||||||
|
|
||||||
func httpTunnelModeEnabled(mode string) bool {
|
func httpTunnelModeEnabled(mode string) bool {
|
||||||
switch strings.ToLower(strings.TrimSpace(mode)) {
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||||
case "stream", "poll", "auto":
|
case "stream", "poll", "auto", "ws":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
@@ -271,14 +290,24 @@ func (s *Sudoku) dialAndHandshake(ctx context.Context, cfg *sudoku.ProtocolConfi
|
|||||||
)
|
)
|
||||||
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
if !cfg.DisableHTTPMask && httpTunnelModeEnabled(cfg.HTTPMaskMode) {
|
||||||
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
muxMode := normalizeHTTPMaskMultiplex(cfg.HTTPMaskMultiplex)
|
||||||
switch muxMode {
|
if muxMode == "auto" && strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) != "ws" {
|
||||||
case "auto", "on":
|
if client, cerr := s.getOrCreateHTTPMaskClient(cfg); cerr == nil && client != nil {
|
||||||
client, errX := s.getOrCreateHTTPMaskClient(cfg)
|
c, err = client.DialTunnel(ctx, httpmask.TunnelDialOptions{
|
||||||
if errX != nil {
|
Mode: cfg.HTTPMaskMode,
|
||||||
return nil, errX
|
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
||||||
|
HostOverride: cfg.HTTPMaskHost,
|
||||||
|
PathRoot: cfg.HTTPMaskPathRoot,
|
||||||
|
AuthKey: sudoku.ClientAEADSeed(cfg.Key),
|
||||||
|
Upgrade: upgrade,
|
||||||
|
Multiplex: cfg.HTTPMaskMultiplex,
|
||||||
|
DialContext: s.dialer.DialContext,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
s.resetHTTPMaskClient()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c, err = client.Dial(ctx, upgrade)
|
}
|
||||||
default:
|
if c == nil && err == nil {
|
||||||
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext, upgrade)
|
c, err = sudoku.DialHTTPMaskTunnel(ctx, cfg.ServerAddress, cfg, s.dialer.DialContext, upgrade)
|
||||||
}
|
}
|
||||||
if err == nil && c != nil {
|
if err == nil && c != nil {
|
||||||
@@ -372,34 +401,51 @@ func (s *Sudoku) resetMuxClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Sudoku) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*sudoku.HTTPMaskTunnelClient, error) {
|
|
||||||
if s == nil {
|
|
||||||
return nil, fmt.Errorf("nil adapter")
|
|
||||||
}
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("config is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.httpMaskMu.Lock()
|
|
||||||
defer s.httpMaskMu.Unlock()
|
|
||||||
|
|
||||||
if s.httpMaskClient != nil {
|
|
||||||
return s.httpMaskClient, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := sudoku.NewHTTPMaskTunnelClient(cfg.ServerAddress, cfg, s.dialer.DialContext)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.httpMaskClient = c
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Sudoku) resetHTTPMaskClient() {
|
func (s *Sudoku) resetHTTPMaskClient() {
|
||||||
s.httpMaskMu.Lock()
|
s.httpMaskMu.Lock()
|
||||||
defer s.httpMaskMu.Unlock()
|
defer s.httpMaskMu.Unlock()
|
||||||
if s.httpMaskClient != nil {
|
if s.httpMaskClient != nil {
|
||||||
s.httpMaskClient.CloseIdleConnections()
|
s.httpMaskClient.CloseIdleConnections()
|
||||||
s.httpMaskClient = nil
|
s.httpMaskClient = nil
|
||||||
|
s.httpMaskKey = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Sudoku) getOrCreateHTTPMaskClient(cfg *sudoku.ProtocolConfig) (*httpmask.TunnelClient, error) {
|
||||||
|
if s == nil || cfg == nil {
|
||||||
|
return nil, fmt.Errorf("nil adapter or config")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := cfg.ServerAddress + "|" + strconv.FormatBool(cfg.HTTPMaskTLSEnabled) + "|" + strings.TrimSpace(cfg.HTTPMaskHost)
|
||||||
|
|
||||||
|
s.httpMaskMu.Lock()
|
||||||
|
if s.httpMaskClient != nil && s.httpMaskKey == key {
|
||||||
|
client := s.httpMaskClient
|
||||||
|
s.httpMaskMu.Unlock()
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
s.httpMaskMu.Unlock()
|
||||||
|
|
||||||
|
client, err := httpmask.NewTunnelClient(cfg.ServerAddress, httpmask.TunnelClientOptions{
|
||||||
|
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
||||||
|
HostOverride: cfg.HTTPMaskHost,
|
||||||
|
DialContext: s.dialer.DialContext,
|
||||||
|
MaxIdleConns: 32,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.httpMaskMu.Lock()
|
||||||
|
defer s.httpMaskMu.Unlock()
|
||||||
|
if s.httpMaskClient != nil && s.httpMaskKey == key {
|
||||||
|
client.CloseIdleConnections()
|
||||||
|
return s.httpMaskClient, nil
|
||||||
|
}
|
||||||
|
if s.httpMaskClient != nil {
|
||||||
|
s.httpMaskClient.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
s.httpMaskClient = client
|
||||||
|
s.httpMaskKey = key
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,9 +27,8 @@ type Trojan struct {
|
|||||||
hexPassword [trojan.KeyLength]byte
|
hexPassword [trojan.KeyLength]byte
|
||||||
|
|
||||||
// for gun mux
|
// for gun mux
|
||||||
gunTLSConfig *tls.Config
|
|
||||||
gunConfig *gun.Config
|
gunConfig *gun.Config
|
||||||
transport *gun.TransportWrap
|
gunTransport *gun.TransportWrap
|
||||||
|
|
||||||
realityConfig *tlsC.RealityConfig
|
realityConfig *tlsC.RealityConfig
|
||||||
echConfig *ech.Config
|
echConfig *ech.Config
|
||||||
@@ -66,7 +65,6 @@ type TrojanSSOption struct {
|
|||||||
Password string `proxy:"password,omitempty"`
|
Password string `proxy:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamConnContext implements C.ProxyAdapter
|
|
||||||
func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
|
func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
|
||||||
switch t.option.Network {
|
switch t.option.Network {
|
||||||
case "ws":
|
case "ws":
|
||||||
@@ -118,7 +116,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
|
|||||||
|
|
||||||
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
|
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
|
||||||
case "grpc":
|
case "grpc":
|
||||||
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig)
|
break // already handle in gun transport
|
||||||
default:
|
default:
|
||||||
// default tcp network
|
// default tcp network
|
||||||
// handle TLS
|
// handle TLS
|
||||||
@@ -179,27 +177,14 @@ func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C
|
|||||||
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if t.transport != nil {
|
if t.gunTransport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig)
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
|
||||||
}
|
|
||||||
defer func(c net.Conn) {
|
|
||||||
safeConnClose(c, err)
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
c, err = t.streamConnContext(ctx, c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewConn(c, t), nil
|
|
||||||
}
|
}
|
||||||
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func(c net.Conn) {
|
defer func(c net.Conn) {
|
||||||
safeConnClose(c, err)
|
safeConnClose(c, err)
|
||||||
}(c)
|
}(c)
|
||||||
@@ -219,35 +204,19 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
|
|||||||
}
|
}
|
||||||
|
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
|
|
||||||
// grpc transport
|
// grpc transport
|
||||||
if t.transport != nil {
|
if t.gunTransport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
|
c, err = gun.StreamGunWithTransport(t.gunTransport, t.gunConfig)
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
|
||||||
}
|
|
||||||
defer func(c net.Conn) {
|
|
||||||
safeConnClose(c, err)
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
c, err = t.streamConnContext(ctx, c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pc := trojan.NewPacketConn(c)
|
|
||||||
return newPacketConn(pc, t), err
|
|
||||||
}
|
}
|
||||||
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c, err = t.dialer.DialContext(ctx, "tcp", t.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
|
||||||
}
|
}
|
||||||
defer func(c net.Conn) {
|
defer func(c net.Conn) {
|
||||||
safeConnClose(c, err)
|
safeConnClose(c, err)
|
||||||
}(c)
|
}(c)
|
||||||
|
|
||||||
c, err = t.StreamConnContext(ctx, c, metadata)
|
c, err = t.StreamConnContext(ctx, c, metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -271,8 +240,8 @@ func (t *Trojan) ProxyInfo() C.ProxyInfo {
|
|||||||
|
|
||||||
// Close implements C.ProxyAdapter
|
// Close implements C.ProxyAdapter
|
||||||
func (t *Trojan) Close() error {
|
func (t *Trojan) Close() error {
|
||||||
if t.transport != nil {
|
if t.gunTransport != nil {
|
||||||
return t.transport.Close()
|
return t.gunTransport.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -336,29 +305,24 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConfig, err := ca.GetTLSConfig(ca.Option{
|
tlsConfig := &vmess.TLSConfig{
|
||||||
TLSConfig: &tls.Config{
|
Host: option.SNI,
|
||||||
NextProtos: option.ALPN,
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
MinVersion: tls.VersionTLS12,
|
FingerPrint: option.Fingerprint,
|
||||||
InsecureSkipVerify: option.SkipCertVerify,
|
Certificate: option.Certificate,
|
||||||
ServerName: option.SNI,
|
PrivateKey: option.PrivateKey,
|
||||||
},
|
ClientFingerprint: option.ClientFingerprint,
|
||||||
Fingerprint: option.Fingerprint,
|
NextProtos: []string{"h2"},
|
||||||
Certificate: option.Certificate,
|
ECH: t.echConfig,
|
||||||
PrivateKey: option.PrivateKey,
|
Reality: t.realityConfig,
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig)
|
t.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
|
|
||||||
t.gunTLSConfig = tlsConfig
|
|
||||||
t.gunConfig = &gun.Config{
|
t.gunConfig = &gun.Config{
|
||||||
ServiceName: option.GrpcOpts.GrpcServiceName,
|
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||||
UserAgent: option.GrpcOpts.GrpcUserAgent,
|
UserAgent: option.GrpcOpts.GrpcUserAgent,
|
||||||
Host: option.SNI,
|
Host: option.SNI,
|
||||||
ClientFingerprint: option.ClientFingerprint,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
144
adapter/outbound/trusttunnel.go
Normal file
144
adapter/outbound/trusttunnel.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package outbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
"github.com/metacubex/mihomo/transport/trusttunnel"
|
||||||
|
"github.com/metacubex/mihomo/transport/vmess"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrustTunnel struct {
|
||||||
|
*Base
|
||||||
|
client *trusttunnel.Client
|
||||||
|
option *TrustTunnelOption
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrustTunnelOption struct {
|
||||||
|
BasicOption
|
||||||
|
Name string `proxy:"name"`
|
||||||
|
Server string `proxy:"server"`
|
||||||
|
Port int `proxy:"port"`
|
||||||
|
UserName string `proxy:"username,omitempty"`
|
||||||
|
Password string `proxy:"password,omitempty"`
|
||||||
|
ALPN []string `proxy:"alpn,omitempty"`
|
||||||
|
SNI string `proxy:"sni,omitempty"`
|
||||||
|
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
|
||||||
|
ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
|
||||||
|
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||||
|
Fingerprint string `proxy:"fingerprint,omitempty"`
|
||||||
|
Certificate string `proxy:"certificate,omitempty"`
|
||||||
|
PrivateKey string `proxy:"private-key,omitempty"`
|
||||||
|
UDP bool `proxy:"udp,omitempty"`
|
||||||
|
HealthCheck bool `proxy:"health-check,omitempty"`
|
||||||
|
|
||||||
|
Quic bool `proxy:"quic,omitempty"`
|
||||||
|
CongestionController string `proxy:"congestion-controller,omitempty"`
|
||||||
|
CWND int `proxy:"cwnd,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TrustTunnel) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
|
c, err := t.client.Dial(ctx, metadata.RemoteAddress())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewConn(c, t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TrustTunnel) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
|
||||||
|
if err = t.ResolveUDP(ctx, metadata); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pc, err := t.client.ListenPacket(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPacketConn(N.NewThreadSafePacketConn(pc), t), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportUOT implements C.ProxyAdapter
|
||||||
|
func (t *TrustTunnel) SupportUOT() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyInfo implements C.ProxyAdapter
|
||||||
|
func (t *TrustTunnel) ProxyInfo() C.ProxyInfo {
|
||||||
|
info := t.Base.ProxyInfo()
|
||||||
|
info.DialerProxy = t.option.DialerProxy
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements C.ProxyAdapter
|
||||||
|
func (t *TrustTunnel) Close() error {
|
||||||
|
return t.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrustTunnel(option TrustTunnelOption) (*TrustTunnel, error) {
|
||||||
|
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
|
||||||
|
outbound := &TrustTunnel{
|
||||||
|
Base: &Base{
|
||||||
|
name: option.Name,
|
||||||
|
addr: addr,
|
||||||
|
tp: C.TrustTunnel,
|
||||||
|
pdName: option.ProviderName,
|
||||||
|
udp: option.UDP,
|
||||||
|
tfo: option.TFO,
|
||||||
|
mpTcp: option.MPTCP,
|
||||||
|
iface: option.Interface,
|
||||||
|
rmark: option.RoutingMark,
|
||||||
|
prefer: option.IPVersion,
|
||||||
|
},
|
||||||
|
option: &option,
|
||||||
|
}
|
||||||
|
outbound.dialer = option.NewDialer(outbound.DialOptions())
|
||||||
|
|
||||||
|
tOption := trusttunnel.ClientOptions{
|
||||||
|
Dialer: outbound.dialer,
|
||||||
|
ResolvUDP: func(ctx context.Context, server string) (netip.AddrPort, error) {
|
||||||
|
udpAddr, err := resolveUDPAddr(ctx, "udp", server, option.IPVersion)
|
||||||
|
if err != nil {
|
||||||
|
return netip.AddrPort{}, err
|
||||||
|
}
|
||||||
|
return udpAddr.AddrPort(), nil
|
||||||
|
},
|
||||||
|
Server: addr,
|
||||||
|
Username: option.UserName,
|
||||||
|
Password: option.Password,
|
||||||
|
QUIC: option.Quic,
|
||||||
|
QUICCongestionControl: option.CongestionController,
|
||||||
|
QUICCwnd: option.CWND,
|
||||||
|
HealthCheck: option.HealthCheck,
|
||||||
|
}
|
||||||
|
echConfig, err := option.ECHOpts.Parse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig := &vmess.TLSConfig{
|
||||||
|
Host: option.SNI,
|
||||||
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
|
NextProtos: option.ALPN,
|
||||||
|
FingerPrint: option.Fingerprint,
|
||||||
|
Certificate: option.Certificate,
|
||||||
|
PrivateKey: option.PrivateKey,
|
||||||
|
ClientFingerprint: option.ClientFingerprint,
|
||||||
|
ECH: echConfig,
|
||||||
|
}
|
||||||
|
if tlsConfig.Host == "" {
|
||||||
|
tlsConfig.Host = option.Server
|
||||||
|
}
|
||||||
|
tOption.TLSConfig = tlsConfig
|
||||||
|
|
||||||
|
client, err := trusttunnel.NewClient(context.TODO(), tOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outbound.client = client
|
||||||
|
|
||||||
|
return outbound, nil
|
||||||
|
}
|
||||||
@@ -33,9 +33,8 @@ type Vless struct {
|
|||||||
encryption *encryption.ClientInstance
|
encryption *encryption.ClientInstance
|
||||||
|
|
||||||
// for gun mux
|
// for gun mux
|
||||||
gunTLSConfig *tls.Config
|
|
||||||
gunConfig *gun.Config
|
gunConfig *gun.Config
|
||||||
transport *gun.TransportWrap
|
gunTransport *gun.TransportWrap
|
||||||
|
|
||||||
realityConfig *tlsC.RealityConfig
|
realityConfig *tlsC.RealityConfig
|
||||||
echConfig *ech.Config
|
echConfig *ech.Config
|
||||||
@@ -151,7 +150,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
|
|
||||||
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
|
c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
|
||||||
case "grpc":
|
case "grpc":
|
||||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
|
break // already handle in gun transport
|
||||||
default:
|
default:
|
||||||
// default tcp network
|
// default tcp network
|
||||||
// handle TLS
|
// handle TLS
|
||||||
@@ -234,23 +233,11 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
|
|||||||
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil {
|
if v.gunTransport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
}
|
|
||||||
defer func(c net.Conn) {
|
|
||||||
safeConnClose(c, err)
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
c, err = v.streamConnContext(ctx, c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewConn(c, v), nil
|
|
||||||
}
|
}
|
||||||
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
@@ -272,28 +259,11 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
|
|||||||
}
|
}
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil {
|
if v.gunTransport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
}
|
|
||||||
defer func(c net.Conn) {
|
|
||||||
safeConnClose(c, err)
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
c, err = v.streamConnContext(ctx, c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("new vless client error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return v.ListenPacketOnStreamConn(ctx, c, metadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
@@ -348,8 +318,8 @@ func (v *Vless) ProxyInfo() C.ProxyInfo {
|
|||||||
|
|
||||||
// Close implements C.ProxyAdapter
|
// Close implements C.ProxyAdapter
|
||||||
func (v *Vless) Close() error {
|
func (v *Vless) Close() error {
|
||||||
if v.transport != nil {
|
if v.gunTransport != nil {
|
||||||
return v.transport.Close()
|
return v.gunTransport.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -461,38 +431,35 @@ func NewVless(option VlessOption) (*Vless, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gunConfig := &gun.Config{
|
gunConfig := &gun.Config{
|
||||||
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||||
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
|
UserAgent: option.GrpcOpts.GrpcUserAgent,
|
||||||
Host: v.option.ServerName,
|
Host: option.ServerName,
|
||||||
ClientFingerprint: v.option.ClientFingerprint,
|
|
||||||
}
|
}
|
||||||
if option.ServerName == "" {
|
if option.ServerName == "" {
|
||||||
gunConfig.Host = v.addr
|
gunConfig.Host = v.addr
|
||||||
}
|
}
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *vmess.TLSConfig
|
||||||
if option.TLS {
|
if option.TLS {
|
||||||
tlsConfig, err = ca.GetTLSConfig(ca.Option{
|
tlsConfig = &vmess.TLSConfig{
|
||||||
TLSConfig: &tls.Config{
|
Host: option.ServerName,
|
||||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
ServerName: v.option.ServerName,
|
FingerPrint: option.Fingerprint,
|
||||||
},
|
Certificate: option.Certificate,
|
||||||
Fingerprint: v.option.Fingerprint,
|
PrivateKey: option.PrivateKey,
|
||||||
Certificate: v.option.Certificate,
|
ClientFingerprint: option.ClientFingerprint,
|
||||||
PrivateKey: v.option.PrivateKey,
|
NextProtos: []string{"h2"},
|
||||||
})
|
ECH: v.echConfig,
|
||||||
if err != nil {
|
Reality: v.realityConfig,
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if option.ServerName == "" {
|
if option.ServerName == "" {
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
tlsConfig.ServerName = host
|
tlsConfig.Host = host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.gunTLSConfig = tlsConfig
|
|
||||||
v.gunConfig = gunConfig
|
v.gunConfig = gunConfig
|
||||||
|
|
||||||
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
|
v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
|
|||||||
@@ -34,9 +34,8 @@ type Vmess struct {
|
|||||||
option *VmessOption
|
option *VmessOption
|
||||||
|
|
||||||
// for gun mux
|
// for gun mux
|
||||||
gunTLSConfig *tls.Config
|
|
||||||
gunConfig *gun.Config
|
gunConfig *gun.Config
|
||||||
transport *gun.TransportWrap
|
gunTransport *gun.TransportWrap
|
||||||
|
|
||||||
realityConfig *tlsC.RealityConfig
|
realityConfig *tlsC.RealityConfig
|
||||||
echConfig *ech.Config
|
echConfig *ech.Config
|
||||||
@@ -98,7 +97,6 @@ type WSOptions struct {
|
|||||||
V2rayHttpUpgradeFastOpen bool `proxy:"v2ray-http-upgrade-fast-open,omitempty"`
|
V2rayHttpUpgradeFastOpen bool `proxy:"v2ray-http-upgrade-fast-open,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StreamConnContext implements C.ProxyAdapter
|
|
||||||
func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
|
func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
|
||||||
switch v.option.Network {
|
switch v.option.Network {
|
||||||
case "ws":
|
case "ws":
|
||||||
@@ -205,7 +203,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
|
|
||||||
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
|
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
|
||||||
case "grpc":
|
case "grpc":
|
||||||
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
|
break // already handle in gun transport
|
||||||
default:
|
default:
|
||||||
// handle TLS
|
// handle TLS
|
||||||
if v.option.TLS {
|
if v.option.TLS {
|
||||||
@@ -296,23 +294,11 @@ func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.M
|
|||||||
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil {
|
if v.gunTransport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
}
|
|
||||||
defer func(c net.Conn) {
|
|
||||||
safeConnClose(c, err)
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
c, err = v.streamConnContext(ctx, c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return NewConn(c, v), nil
|
|
||||||
}
|
}
|
||||||
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
@@ -331,27 +317,11 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (
|
|||||||
}
|
}
|
||||||
var c net.Conn
|
var c net.Conn
|
||||||
// gun transport
|
// gun transport
|
||||||
if v.transport != nil {
|
if v.gunTransport != nil {
|
||||||
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
|
c, err = gun.StreamGunWithTransport(v.gunTransport, v.gunConfig)
|
||||||
if err != nil {
|
} else {
|
||||||
return nil, err
|
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
||||||
}
|
|
||||||
defer func(c net.Conn) {
|
|
||||||
safeConnClose(c, err)
|
|
||||||
}(c)
|
|
||||||
|
|
||||||
c, err = v.streamConnContext(ctx, c, metadata)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("new vmess client error: %v", err)
|
|
||||||
}
|
|
||||||
return v.ListenPacketOnStreamConn(ctx, c, metadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = v.ResolveUDP(ctx, metadata); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err = v.dialer.DialContext(ctx, "tcp", v.addr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error())
|
||||||
}
|
}
|
||||||
@@ -375,8 +345,8 @@ func (v *Vmess) ProxyInfo() C.ProxyInfo {
|
|||||||
|
|
||||||
// Close implements C.ProxyAdapter
|
// Close implements C.ProxyAdapter
|
||||||
func (v *Vmess) Close() error {
|
func (v *Vmess) Close() error {
|
||||||
if v.transport != nil {
|
if v.gunTransport != nil {
|
||||||
return v.transport.Close()
|
return v.gunTransport.Close()
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -467,38 +437,35 @@ func NewVmess(option VmessOption) (*Vmess, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gunConfig := &gun.Config{
|
gunConfig := &gun.Config{
|
||||||
ServiceName: v.option.GrpcOpts.GrpcServiceName,
|
ServiceName: option.GrpcOpts.GrpcServiceName,
|
||||||
UserAgent: v.option.GrpcOpts.GrpcUserAgent,
|
UserAgent: option.GrpcOpts.GrpcUserAgent,
|
||||||
Host: v.option.ServerName,
|
Host: option.ServerName,
|
||||||
ClientFingerprint: v.option.ClientFingerprint,
|
|
||||||
}
|
}
|
||||||
if option.ServerName == "" {
|
if option.ServerName == "" {
|
||||||
gunConfig.Host = v.addr
|
gunConfig.Host = v.addr
|
||||||
}
|
}
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *mihomoVMess.TLSConfig
|
||||||
if option.TLS {
|
if option.TLS {
|
||||||
tlsConfig, err = ca.GetTLSConfig(ca.Option{
|
tlsConfig = &mihomoVMess.TLSConfig{
|
||||||
TLSConfig: &tls.Config{
|
Host: option.ServerName,
|
||||||
InsecureSkipVerify: v.option.SkipCertVerify,
|
SkipCertVerify: option.SkipCertVerify,
|
||||||
ServerName: v.option.ServerName,
|
FingerPrint: option.Fingerprint,
|
||||||
},
|
Certificate: option.Certificate,
|
||||||
Fingerprint: v.option.Fingerprint,
|
PrivateKey: option.PrivateKey,
|
||||||
Certificate: v.option.Certificate,
|
ClientFingerprint: option.ClientFingerprint,
|
||||||
PrivateKey: v.option.PrivateKey,
|
NextProtos: []string{"h2"},
|
||||||
})
|
ECH: v.echConfig,
|
||||||
if err != nil {
|
Reality: v.realityConfig,
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
if option.ServerName == "" {
|
if option.ServerName == "" {
|
||||||
host, _, _ := net.SplitHostPort(v.addr)
|
host, _, _ := net.SplitHostPort(v.addr)
|
||||||
tlsConfig.ServerName = host
|
tlsConfig.Host = host
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
v.gunTLSConfig = tlsConfig
|
|
||||||
v.gunConfig = gunConfig
|
v.gunConfig = gunConfig
|
||||||
|
|
||||||
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
|
v.gunTransport = gun.NewHTTP2Client(dialFn, tlsConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
return v, nil
|
return v, nil
|
||||||
|
|||||||
@@ -77,8 +77,8 @@ type WireGuardOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WireGuardPeerOption struct {
|
type WireGuardPeerOption struct {
|
||||||
Server string `proxy:"server"`
|
Server string `proxy:"server,omitempty"`
|
||||||
Port int `proxy:"port"`
|
Port int `proxy:"port,omitempty"`
|
||||||
PublicKey string `proxy:"public-key,omitempty"`
|
PublicKey string `proxy:"public-key,omitempty"`
|
||||||
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
|
PreSharedKey string `proxy:"pre-shared-key,omitempty"`
|
||||||
Reserved []uint8 `proxy:"reserved,omitempty"`
|
Reserved []uint8 `proxy:"reserved,omitempty"`
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error, fn func(
|
|||||||
|
|
||||||
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
|
log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes)
|
||||||
if gb.failedTimes >= gb.maxFailedTimes {
|
if gb.failedTimes >= gb.maxFailedTimes {
|
||||||
log.Warnln("because %s failed multiple times, active health check", gb.Name())
|
log.Warnln("because %s failed multiple times, activate health check", gb.Name())
|
||||||
fn()
|
fn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ type GroupCommonOption struct {
|
|||||||
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
|
IncludeAllProviders bool `group:"include-all-providers,omitempty"`
|
||||||
Hidden bool `group:"hidden,omitempty"`
|
Hidden bool `group:"hidden,omitempty"`
|
||||||
Icon string `group:"icon,omitempty"`
|
Icon string `group:"icon,omitempty"`
|
||||||
|
|
||||||
// removed configs, only for error logging
|
|
||||||
Interface string `group:"interface-name,omitempty"`
|
|
||||||
RoutingMark int `group:"routing-mark,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]P.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
|
func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, providersMap map[string]P.ProxyProvider, AllProxies []string, AllProviders []string) (C.ProxyAdapter, error) {
|
||||||
@@ -62,12 +58,15 @@ func ParseProxyGroup(config map[string]any, proxyMap map[string]C.Proxy, provide
|
|||||||
return nil, errFormat
|
return nil, errFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
if groupOption.RoutingMark != 0 {
|
if _, ok := config["routing-mark"]; ok {
|
||||||
log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
log.Errorln("The group [%s] with routing-mark configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
||||||
}
|
}
|
||||||
if groupOption.Interface != "" {
|
if _, ok := config["interface-name"]; ok {
|
||||||
log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
log.Errorln("The group [%s] with interface-name configuration was removed, please set it directly on the proxy instead", groupOption.Name)
|
||||||
}
|
}
|
||||||
|
if _, ok := config["dialer-proxy"]; ok {
|
||||||
|
log.Errorln("The group [%s] with dialer-proxy configuration is not allowed, please set it directly on the proxy instead", groupOption.Name)
|
||||||
|
}
|
||||||
|
|
||||||
groupName := groupOption.Name
|
groupName := groupOption.Name
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,13 @@ func ParseProxy(mapping map[string]any, options ...ProxyOption) (C.Proxy, error)
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
proxy, err = outbound.NewMasque(*masqueOption)
|
proxy, err = outbound.NewMasque(*masqueOption)
|
||||||
|
case "trusttunnel":
|
||||||
|
trustTunnelOption := &outbound.TrustTunnelOption{BasicOption: basicOption}
|
||||||
|
err = decoder.Decode(mapping, trustTunnelOption)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
proxy, err = outbound.NewTrustTunnel(*trustTunnelOption)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (o *overrideSchema) Apply(mapping map[string]any) error {
|
|||||||
mapping["skip-cert-verify"] = *o.SkipCertVerify
|
mapping["skip-cert-verify"] = *o.SkipCertVerify
|
||||||
}
|
}
|
||||||
if o.Interface != nil {
|
if o.Interface != nil {
|
||||||
mapping["interface"] = *o.Interface
|
mapping["interface-name"] = *o.Interface
|
||||||
}
|
}
|
||||||
if o.RoutingMark != nil {
|
if o.RoutingMark != nil {
|
||||||
mapping["routing-mark"] = *o.RoutingMark
|
mapping["routing-mark"] = *o.RoutingMark
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package buf
|
package buf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/metacubex/sing/common"
|
|
||||||
"github.com/metacubex/sing/common/buf"
|
"github.com/metacubex/sing/common/buf"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -9,14 +8,52 @@ const BufferSize = buf.BufferSize
|
|||||||
|
|
||||||
type Buffer = buf.Buffer
|
type Buffer = buf.Buffer
|
||||||
|
|
||||||
var New = buf.New
|
func New() *Buffer {
|
||||||
var NewPacket = buf.NewPacket
|
return buf.New()
|
||||||
var NewSize = buf.NewSize
|
}
|
||||||
var With = buf.With
|
|
||||||
var As = buf.As
|
|
||||||
var ReleaseMulti = buf.ReleaseMulti
|
|
||||||
|
|
||||||
var (
|
func NewPacket() *Buffer {
|
||||||
Must = common.Must
|
return buf.NewPacket()
|
||||||
Error = common.Error
|
}
|
||||||
)
|
|
||||||
|
func NewSize(size int) *Buffer {
|
||||||
|
return buf.NewSize(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func With(data []byte) *Buffer {
|
||||||
|
return buf.With(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func As(data []byte) *Buffer {
|
||||||
|
return buf.As(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReleaseMulti(buffers []*Buffer) {
|
||||||
|
buf.ReleaseMulti(buffers)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(_ any, err error) error {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func Must(errs ...error) {
|
||||||
|
for _, err := range errs {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Must1[T any](result T, err error) T {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func Must2[T any, T2 any](result T, result2 T2, err error) (T, T2) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return result, result2
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -38,58 +39,7 @@ func (d *Decoder) Decode(src map[string]any, dst any) error {
|
|||||||
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
if reflect.TypeOf(dst).Kind() != reflect.Ptr {
|
||||||
return fmt.Errorf("decode must recive a ptr struct")
|
return fmt.Errorf("decode must recive a ptr struct")
|
||||||
}
|
}
|
||||||
t := reflect.TypeOf(dst).Elem()
|
return d.decode("", src, reflect.ValueOf(dst).Elem())
|
||||||
v := reflect.ValueOf(dst).Elem()
|
|
||||||
for idx := 0; idx < v.NumField(); idx++ {
|
|
||||||
field := t.Field(idx)
|
|
||||||
if field.Anonymous {
|
|
||||||
if err := d.decodeStruct(field.Name, src, v.Field(idx)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tag := field.Tag.Get(d.option.TagName)
|
|
||||||
key, omitKey, found := strings.Cut(tag, ",")
|
|
||||||
omitempty := found && omitKey == "omitempty"
|
|
||||||
|
|
||||||
// As a special case, if the field tag is "-", the field is always omitted.
|
|
||||||
// Note that a field with name "-" can still be generated using the tag "-,".
|
|
||||||
if key == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
value, ok := src[key]
|
|
||||||
if !ok {
|
|
||||||
if d.option.KeyReplacer != nil {
|
|
||||||
key = d.option.KeyReplacer.Replace(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _strKey := range src {
|
|
||||||
strKey := _strKey
|
|
||||||
if d.option.KeyReplacer != nil {
|
|
||||||
strKey = d.option.KeyReplacer.Replace(strKey)
|
|
||||||
}
|
|
||||||
if strings.EqualFold(key, strKey) {
|
|
||||||
value = src[_strKey]
|
|
||||||
ok = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !ok || value == nil {
|
|
||||||
if omitempty {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return fmt.Errorf("key '%s' missing", key)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := d.decode(key, value, v.Field(idx))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isNil returns true if the input is nil or a typed nil pointer.
|
// isNil returns true if the input is nil or a typed nil pointer.
|
||||||
@@ -456,6 +406,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
|
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
targetValKeysUnused := make(map[any]struct{})
|
||||||
errors := make([]string, 0)
|
errors := make([]string, 0)
|
||||||
|
|
||||||
// This slice will keep track of all the structs we'll be decoding.
|
// This slice will keep track of all the structs we'll be decoding.
|
||||||
@@ -470,6 +421,11 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
field reflect.StructField
|
field reflect.StructField
|
||||||
val reflect.Value
|
val reflect.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remainField is set to a valid field set with the "remain" tag if
|
||||||
|
// we are keeping track of remaining values.
|
||||||
|
var remainField *field
|
||||||
|
|
||||||
var fields []field
|
var fields []field
|
||||||
for len(structs) > 0 {
|
for len(structs) > 0 {
|
||||||
structVal := structs[0]
|
structVal := structs[0]
|
||||||
@@ -479,30 +435,47 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
|
|
||||||
for i := 0; i < structType.NumField(); i++ {
|
for i := 0; i < structType.NumField(); i++ {
|
||||||
fieldType := structType.Field(i)
|
fieldType := structType.Field(i)
|
||||||
fieldKind := fieldType.Type.Kind()
|
fieldVal := structVal.Field(i)
|
||||||
|
if fieldVal.Kind() == reflect.Ptr && fieldVal.Elem().Kind() == reflect.Struct {
|
||||||
|
// Handle embedded struct pointers as embedded structs.
|
||||||
|
fieldVal = fieldVal.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
// If "squash" is specified in the tag, we squash the field down.
|
// If "squash" is specified in the tag, we squash the field down.
|
||||||
squash := false
|
squash := fieldVal.Kind() == reflect.Struct && fieldType.Anonymous
|
||||||
|
remain := false
|
||||||
|
|
||||||
|
// We always parse the tags cause we're looking for other tags too
|
||||||
tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",")
|
tagParts := strings.Split(fieldType.Tag.Get(d.option.TagName), ",")
|
||||||
for _, tag := range tagParts[1:] {
|
for _, tag := range tagParts[1:] {
|
||||||
if tag == "squash" {
|
if tag == "squash" {
|
||||||
squash = true
|
squash = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tag == "remain" {
|
||||||
|
remain = true
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if squash {
|
if squash {
|
||||||
if fieldKind != reflect.Struct {
|
if fieldVal.Kind() != reflect.Struct {
|
||||||
errors = append(errors,
|
errors = append(errors,
|
||||||
fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldKind).Error())
|
fmt.Errorf("%s: unsupported type for squash: %s", fieldType.Name, fieldVal.Kind()).Error())
|
||||||
} else {
|
} else {
|
||||||
structs = append(structs, structVal.FieldByName(fieldType.Name))
|
structs = append(structs, fieldVal)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal struct field, store it away
|
// Build our field
|
||||||
fields = append(fields, field{fieldType, structVal.Field(i)})
|
if remain {
|
||||||
|
remainField = &field{fieldType, fieldVal}
|
||||||
|
} else {
|
||||||
|
// Normal struct field, store it away
|
||||||
|
fields = append(fields, field{fieldType, fieldVal})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -511,8 +484,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
field, fieldValue := f.field, f.val
|
field, fieldValue := f.field, f.val
|
||||||
fieldName := field.Name
|
fieldName := field.Name
|
||||||
|
|
||||||
tagValue := field.Tag.Get(d.option.TagName)
|
tagParts := strings.Split(field.Tag.Get(d.option.TagName), ",")
|
||||||
tagValue = strings.SplitN(tagValue, ",", 2)[0]
|
tagValue := tagParts[0]
|
||||||
if tagValue != "" {
|
if tagValue != "" {
|
||||||
fieldName = tagValue
|
fieldName = tagValue
|
||||||
}
|
}
|
||||||
@@ -521,6 +494,13 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
omitempty := false
|
||||||
|
for _, tag := range tagParts[1:] {
|
||||||
|
if tag == "omitempty" {
|
||||||
|
omitempty = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rawMapKey := reflect.ValueOf(fieldName)
|
rawMapKey := reflect.ValueOf(fieldName)
|
||||||
rawMapVal := dataVal.MapIndex(rawMapKey)
|
rawMapVal := dataVal.MapIndex(rawMapKey)
|
||||||
if !rawMapVal.IsValid() {
|
if !rawMapVal.IsValid() {
|
||||||
@@ -548,7 +528,10 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
|
|
||||||
if !rawMapVal.IsValid() {
|
if !rawMapVal.IsValid() {
|
||||||
// There was no matching key in the map for the value in
|
// There was no matching key in the map for the value in
|
||||||
// the struct. Just ignore.
|
// the struct. Remember it for potential errors and metadata.
|
||||||
|
if !omitempty {
|
||||||
|
targetValKeysUnused[fieldName] = struct{}{}
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -570,7 +553,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
// If the name is empty string, then we're at the root, and we
|
// If the name is empty string, then we're at the root, and we
|
||||||
// don't dot-join the fields.
|
// don't dot-join the fields.
|
||||||
if name != "" {
|
if name != "" {
|
||||||
fieldName = fmt.Sprintf("%s.%s", name, fieldName)
|
fieldName = name + "." + fieldName
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil {
|
if err := d.decode(fieldName, rawMapVal.Interface(), fieldValue); err != nil {
|
||||||
@@ -578,6 +561,36 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we have a "remain"-tagged field and we have unused keys then
|
||||||
|
// we put the unused keys directly into the remain field.
|
||||||
|
if remainField != nil && len(dataValKeysUnused) > 0 {
|
||||||
|
// Build a map of only the unused values
|
||||||
|
remain := map[interface{}]interface{}{}
|
||||||
|
for key := range dataValKeysUnused {
|
||||||
|
remain[key] = dataVal.MapIndex(reflect.ValueOf(key)).Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode it as-if we were just decoding this map onto our map.
|
||||||
|
if err := d.decodeMap(name, remain, remainField.val); err != nil {
|
||||||
|
errors = append(errors, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the map to nil so we have none so that the next check will
|
||||||
|
// not error (ErrorUnused)
|
||||||
|
dataValKeysUnused = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(targetValKeysUnused) > 0 {
|
||||||
|
keys := make([]string, 0, len(targetValKeysUnused))
|
||||||
|
for rawKey := range targetValKeysUnused {
|
||||||
|
keys = append(keys, rawKey.(string))
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", "))
|
||||||
|
errors = append(errors, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
if len(errors) > 0 {
|
if len(errors) > 0 {
|
||||||
return fmt.Errorf(strings.Join(errors, ","))
|
return fmt.Errorf(strings.Join(errors, ","))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,6 +139,49 @@ func TestStructure_Nest(t *testing.T) {
|
|||||||
assert.Equal(t, s.BazOptional, goal)
|
assert.Equal(t, s.BazOptional, goal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructure_DoubleNest(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"bar": map[string]any{
|
||||||
|
"foo": 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
goal := BazOptional{
|
||||||
|
Foo: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &struct {
|
||||||
|
Bar struct {
|
||||||
|
BazOptional
|
||||||
|
} `test:"bar"`
|
||||||
|
}{}
|
||||||
|
err := decoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, s.Bar.BazOptional, goal)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStructure_Remain(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"foo": 1,
|
||||||
|
"bar": "test",
|
||||||
|
"extra": false,
|
||||||
|
}
|
||||||
|
|
||||||
|
goal := &Baz{
|
||||||
|
Foo: 1,
|
||||||
|
Bar: "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &struct {
|
||||||
|
Baz
|
||||||
|
Remain map[string]any `test:",remain"`
|
||||||
|
}{}
|
||||||
|
err := decoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, *goal, s.Baz)
|
||||||
|
assert.Equal(t, map[string]any{"extra": false}, s.Remain)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStructure_SliceNilValue(t *testing.T) {
|
func TestStructure_SliceNilValue(t *testing.T) {
|
||||||
rawMap := map[string]any{
|
rawMap := map[string]any{
|
||||||
"foo": 1,
|
"foo": 1,
|
||||||
@@ -228,6 +271,23 @@ func TestStructure_Pointer(t *testing.T) {
|
|||||||
assert.Nil(t, s.Bar)
|
assert.Nil(t, s.Bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStructure_PointerStruct(t *testing.T) {
|
||||||
|
rawMap := map[string]any{
|
||||||
|
"foo": "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &struct {
|
||||||
|
Foo *string `test:"foo,omitempty"`
|
||||||
|
Bar *Baz `test:"bar,omitempty"`
|
||||||
|
}{}
|
||||||
|
|
||||||
|
err := decoder.Decode(rawMap, s)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, s.Foo)
|
||||||
|
assert.Equal(t, "foo", *s.Foo)
|
||||||
|
assert.Nil(t, s.Bar)
|
||||||
|
}
|
||||||
|
|
||||||
type num struct {
|
type num struct {
|
||||||
a int
|
a int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func HttpRequest(ctx context.Context, url, method string, header map[string][]st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := header["User-Agent"]; !ok {
|
if req.Header.Get("User-Agent") == "" {
|
||||||
req.Header.Set("User-Agent", UA())
|
req.Header.Set("User-Agent", UA())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,8 @@ func UEncryptedClientHelloKey(it tls.EncryptedClientHelloKey) utls.EncryptedClie
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConnectionState = utls.ConnectionState
|
||||||
|
|
||||||
type Config = utls.Config
|
type Config = utls.Config
|
||||||
|
|
||||||
var tlsCertificateRequestInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.CertificateRequestInfo)(nil)).Elem().FieldByName("ctx")).Offset
|
var tlsCertificateRequestInfoCtxOffset = utils.MustOK(reflect.TypeOf((*tls.CertificateRequestInfo)(nil)).Elem().FieldByName("ctx")).Offset
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ const (
|
|||||||
AnyTLS
|
AnyTLS
|
||||||
Sudoku
|
Sudoku
|
||||||
Masque
|
Masque
|
||||||
|
TrustTunnel
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -215,6 +216,8 @@ func (at AdapterType) String() string {
|
|||||||
return "Sudoku"
|
return "Sudoku"
|
||||||
case Masque:
|
case Masque:
|
||||||
return "Masque"
|
return "Masque"
|
||||||
|
case TrustTunnel:
|
||||||
|
return "TrustTunnel"
|
||||||
case Relay:
|
case Relay:
|
||||||
return "Relay"
|
return "Relay"
|
||||||
case Selector:
|
case Selector:
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ const (
|
|||||||
ANYTLS
|
ANYTLS
|
||||||
MIERU
|
MIERU
|
||||||
SUDOKU
|
SUDOKU
|
||||||
|
TRUSTTUNNEL
|
||||||
INNER
|
INNER
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -115,6 +116,8 @@ func (t Type) String() string {
|
|||||||
return "Mieru"
|
return "Mieru"
|
||||||
case SUDOKU:
|
case SUDOKU:
|
||||||
return "Sudoku"
|
return "Sudoku"
|
||||||
|
case TRUSTTUNNEL:
|
||||||
|
return "TrustTunnel"
|
||||||
case INNER:
|
case INNER:
|
||||||
return "Inner"
|
return "Inner"
|
||||||
default:
|
default:
|
||||||
@@ -159,6 +162,8 @@ func ParseType(t string) (*Type, error) {
|
|||||||
res = MIERU
|
res = MIERU
|
||||||
case "SUDOKU":
|
case "SUDOKU":
|
||||||
res = SUDOKU
|
res = SUDOKU
|
||||||
|
case "TRUSTTUNNEL":
|
||||||
|
res = TRUSTTUNNEL
|
||||||
case "INNER":
|
case "INNER":
|
||||||
res = INNER
|
res = INNER
|
||||||
default:
|
default:
|
||||||
|
|||||||
42
dns/dot.go
42
dns/dot.go
@@ -23,6 +23,7 @@ type dnsOverTLS struct {
|
|||||||
host string
|
host string
|
||||||
dialer *dnsDialer
|
dialer *dnsDialer
|
||||||
skipCertVerify bool
|
skipCertVerify bool
|
||||||
|
disableReuse bool
|
||||||
|
|
||||||
access sync.Mutex
|
access sync.Mutex
|
||||||
connections deque.Deque[net.Conn] // LIFO
|
connections deque.Deque[net.Conn] // LIFO
|
||||||
@@ -57,11 +58,13 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
|
|||||||
var conn net.Conn
|
var conn net.Conn
|
||||||
isOldConn := true
|
isOldConn := true
|
||||||
|
|
||||||
t.access.Lock()
|
if !t.disableReuse {
|
||||||
if t.connections.Len() > 0 {
|
t.access.Lock()
|
||||||
conn = t.connections.PopBack()
|
if t.connections.Len() > 0 {
|
||||||
|
conn = t.connections.PopBack()
|
||||||
|
}
|
||||||
|
t.access.Unlock()
|
||||||
}
|
}
|
||||||
t.access.Unlock()
|
|
||||||
|
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
conn, err = t.dialContext(ctx)
|
conn, err = t.dialContext(ctx)
|
||||||
@@ -90,13 +93,17 @@ func (t *dnsOverTLS) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, err
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.access.Lock()
|
if !t.disableReuse {
|
||||||
if t.connections.Len() >= maxOldDotConns {
|
t.access.Lock()
|
||||||
oldConn := t.connections.PopFront()
|
if t.connections.Len() >= maxOldDotConns {
|
||||||
go oldConn.Close() // close in a new goroutine, not blocking the current task
|
oldConn := t.connections.PopFront()
|
||||||
|
go oldConn.Close() // close in a new goroutine, not blocking the current task
|
||||||
|
}
|
||||||
|
t.connections.PushBack(conn)
|
||||||
|
t.access.Unlock()
|
||||||
|
} else {
|
||||||
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
t.connections.PushBack(conn)
|
|
||||||
t.access.Unlock()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -134,12 +141,14 @@ func (t *dnsOverTLS) dialContext(ctx context.Context) (net.Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *dnsOverTLS) ResetConnection() {
|
func (t *dnsOverTLS) ResetConnection() {
|
||||||
t.access.Lock()
|
if !t.disableReuse {
|
||||||
for t.connections.Len() > 0 {
|
t.access.Lock()
|
||||||
oldConn := t.connections.PopFront()
|
for t.connections.Len() > 0 {
|
||||||
go oldConn.Close() // close in a new goroutine, not blocking the current task
|
oldConn := t.connections.PopFront()
|
||||||
|
go oldConn.Close() // close in a new goroutine, not blocking the current task
|
||||||
|
}
|
||||||
|
t.access.Unlock()
|
||||||
}
|
}
|
||||||
t.access.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *dnsOverTLS) Close() error {
|
func (t *dnsOverTLS) Close() error {
|
||||||
@@ -159,6 +168,9 @@ func newDoTClient(addr string, resolver *Resolver, params map[string]string, pro
|
|||||||
if params["skip-cert-verify"] == "true" {
|
if params["skip-cert-verify"] == "true" {
|
||||||
c.skipCertVerify = true
|
c.skipCertVerify = true
|
||||||
}
|
}
|
||||||
|
if params["disable-reuse"] == "true" {
|
||||||
|
c.disableReuse = true
|
||||||
|
}
|
||||||
runtime.SetFinalizer(c, (*dnsOverTLS).Close)
|
runtime.SetFinalizer(c, (*dnsOverTLS).Close)
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1077,6 +1077,8 @@ proxies: # socks5
|
|||||||
# multiplexing: MULTIPLEXING_LOW
|
# multiplexing: MULTIPLEXING_LOW
|
||||||
# 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT,否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD
|
# 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT,否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD
|
||||||
# handshake-mode: HANDSHAKE_STANDARD
|
# handshake-mode: HANDSHAKE_STANDARD
|
||||||
|
# 一个 base64 字符串用于微调网络行为
|
||||||
|
# traffic-pattern: ""
|
||||||
|
|
||||||
# sudoku
|
# sudoku
|
||||||
- name: sudoku
|
- name: sudoku
|
||||||
@@ -1090,12 +1092,22 @@ proxies: # socks5
|
|||||||
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
|
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
|
||||||
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
|
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
|
||||||
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
||||||
http-mask: true # 是否启用http掩码
|
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
|
||||||
# http-mask-mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll);stream/poll/auto 支持走 CDN/反代
|
httpmask:
|
||||||
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 https;false 强制 http(不会根据端口自动推断)
|
disable: false # true 禁用所有 HTTP 伪装/隧道
|
||||||
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
|
mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||||
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
|
# tls: true # 可选:仅在 mode 为 stream/poll/auto/ws 时生效;true 强制 https/wss;false 强制 http/ws(不会根据端口自动推断)
|
||||||
# http-mask-multiplex: off # 可选:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 http-mask-mode=stream/poll/auto 生效)
|
# host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 mode 为 stream/poll/auto/ws 时生效
|
||||||
|
# path_root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
|
||||||
|
# multiplex: off # 可选:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 mode=stream/poll/auto 生效;ws 强制 off)
|
||||||
|
#
|
||||||
|
# 向后兼容旧写法:
|
||||||
|
# http-mask: true # 是否启用 http 掩码
|
||||||
|
# http-mask-mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||||
|
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto/ws 时生效;true 强制 https/wss;false 强制 http/ws
|
||||||
|
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto/ws 时生效
|
||||||
|
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致)
|
||||||
|
# http-mask-multiplex: off # 可选:off(默认)、auto(复用底层 HTTP 连接)、on(Sudoku mux 单隧道多目标;ws 强制 off)
|
||||||
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
|
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
|
||||||
|
|
||||||
# anytls
|
# anytls
|
||||||
@@ -1115,6 +1127,23 @@ proxies: # socks5
|
|||||||
# - http/1.1
|
# - http/1.1
|
||||||
# skip-cert-verify: true
|
# skip-cert-verify: true
|
||||||
|
|
||||||
|
# trusttunnel
|
||||||
|
- name: trusttunnel
|
||||||
|
type: trusttunnel
|
||||||
|
server: 1.2.3.4
|
||||||
|
port: 443
|
||||||
|
username: username
|
||||||
|
password: password
|
||||||
|
# client-fingerprint: chrome
|
||||||
|
health-check: true
|
||||||
|
udp: true
|
||||||
|
# sni: "example.com"
|
||||||
|
# alpn:
|
||||||
|
# - h2
|
||||||
|
# skip-cert-verify: true
|
||||||
|
# quic: true # 默认为false
|
||||||
|
# congestion-controller: bbr
|
||||||
|
|
||||||
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
|
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
|
||||||
- name: "dns-out"
|
- name: "dns-out"
|
||||||
type: dns
|
type: dns
|
||||||
@@ -1628,6 +1657,8 @@ listeners:
|
|||||||
users:
|
users:
|
||||||
username1: password1
|
username1: password1
|
||||||
username2: password2
|
username2: password2
|
||||||
|
# 一个 base64 字符串用于微调网络行为
|
||||||
|
# traffic-pattern: ""
|
||||||
|
|
||||||
- name: sudoku-in-1
|
- name: sudoku-in-1
|
||||||
type: sudoku
|
type: sudoku
|
||||||
@@ -1642,9 +1673,19 @@ listeners:
|
|||||||
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
|
||||||
handshake-timeout: 5 # 可选(秒)
|
handshake-timeout: 5 # 可选(秒)
|
||||||
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
|
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
|
||||||
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false)
|
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
|
||||||
# http-mask-mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll);stream/poll/auto 支持走 CDN/反代
|
httpmask:
|
||||||
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
|
disable: false # true 禁用所有 HTTP 伪装/隧道
|
||||||
|
mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||||
|
# path_root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
|
||||||
|
#
|
||||||
|
# 可选:当启用 HTTPMask 且识别到“像 HTTP 但不符合 tunnel/auth”的请求时,将原始字节透传给 fallback(常用于与其他服务共端口):
|
||||||
|
# fallback: "127.0.0.1:80"
|
||||||
|
#
|
||||||
|
# 向后兼容旧写法:
|
||||||
|
# disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false)
|
||||||
|
# http-mask-mode: legacy # 可选:legacy(默认)、stream(split-stream)、poll、auto(先 stream 再 poll)、ws(WebSocket 隧道)
|
||||||
|
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1731,6 +1772,30 @@ listeners:
|
|||||||
# masquerade: http://127.0.0.1:8080 #作为反向代理
|
# masquerade: http://127.0.0.1:8080 #作为反向代理
|
||||||
# masquerade: https://127.0.0.1:8080 #作为反向代理
|
# masquerade: https://127.0.0.1:8080 #作为反向代理
|
||||||
|
|
||||||
|
- name: trusttunnel-in-1
|
||||||
|
type: trusttunnel
|
||||||
|
port: 10821 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
|
||||||
|
listen: 0.0.0.0
|
||||||
|
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
|
||||||
|
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
|
||||||
|
users:
|
||||||
|
- username: 1
|
||||||
|
password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
|
||||||
|
certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径
|
||||||
|
private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径
|
||||||
|
network: ["tcp", "udp"] # http2+http3
|
||||||
|
congestion-controller: bbr
|
||||||
|
# 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空
|
||||||
|
# client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify"
|
||||||
|
# client-auth-cert: string # 证书 PEM 格式,或者 证书的路径
|
||||||
|
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
|
||||||
|
# ech-key: |
|
||||||
|
# -----BEGIN ECH KEYS-----
|
||||||
|
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
|
||||||
|
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
|
||||||
|
# dC5jb20AAA==
|
||||||
|
# -----END ECH KEYS-----
|
||||||
|
|
||||||
# 注意,listeners中的tun仅提供给高级用户使用,普通用户应使用顶层配置中的tun
|
# 注意,listeners中的tun仅提供给高级用户使用,普通用户应使用顶层配置中的tun
|
||||||
- name: tun-in-1
|
- name: tun-in-1
|
||||||
type: tun
|
type: tun
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -3,11 +3,10 @@ module github.com/metacubex/mihomo
|
|||||||
go 1.20
|
go 1.20
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.1.0
|
|
||||||
github.com/bahlo/generic-list-go v0.2.0
|
github.com/bahlo/generic-list-go v0.2.0
|
||||||
github.com/coreos/go-iptables v0.8.0
|
github.com/coreos/go-iptables v0.8.0
|
||||||
github.com/dlclark/regexp2 v1.11.5
|
github.com/dlclark/regexp2 v1.11.5
|
||||||
github.com/enfein/mieru/v3 v3.26.2
|
github.com/enfein/mieru/v3 v3.28.0
|
||||||
github.com/gobwas/ws v1.4.0
|
github.com/gobwas/ws v1.4.0
|
||||||
github.com/gofrs/uuid/v5 v5.4.0
|
github.com/gofrs/uuid/v5 v5.4.0
|
||||||
github.com/golang/snappy v1.0.0
|
github.com/golang/snappy v1.0.0
|
||||||
@@ -19,12 +18,13 @@ require (
|
|||||||
github.com/metacubex/chi v0.1.0
|
github.com/metacubex/chi v0.1.0
|
||||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727
|
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727
|
||||||
github.com/metacubex/cpu v0.1.0
|
github.com/metacubex/cpu v0.1.0
|
||||||
|
github.com/metacubex/edwards25519 v1.2.0
|
||||||
github.com/metacubex/fswatch v0.1.1
|
github.com/metacubex/fswatch v0.1.1
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||||
github.com/metacubex/http v0.1.0
|
github.com/metacubex/http v0.1.0
|
||||||
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
|
github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604
|
||||||
github.com/metacubex/mlkem v0.1.0
|
github.com/metacubex/mlkem v0.1.0
|
||||||
github.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af
|
github.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56
|
||||||
github.com/metacubex/randv2 v0.2.0
|
github.com/metacubex/randv2 v0.2.0
|
||||||
github.com/metacubex/restls-client-go v0.1.7
|
github.com/metacubex/restls-client-go v0.1.7
|
||||||
github.com/metacubex/sing v0.5.7
|
github.com/metacubex/sing v0.5.7
|
||||||
@@ -33,7 +33,7 @@ require (
|
|||||||
github.com/metacubex/sing-shadowsocks v0.2.12
|
github.com/metacubex/sing-shadowsocks v0.2.12
|
||||||
github.com/metacubex/sing-shadowsocks2 v0.2.7
|
github.com/metacubex/sing-shadowsocks2 v0.2.7
|
||||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
|
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
|
||||||
github.com/metacubex/sing-tun v0.4.15
|
github.com/metacubex/sing-tun v0.4.16-0.20260213034958-b09e3f5bbae6
|
||||||
github.com/metacubex/sing-vmess v0.2.5
|
github.com/metacubex/sing-vmess v0.2.5
|
||||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
|
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
|
||||||
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
|
github.com/metacubex/smux v0.0.0-20260105030934-d0c8756d3141
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,5 +1,3 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
|
||||||
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
|
github.com/RyuaNerin/go-krypto v1.3.0 h1:smavTzSMAx8iuVlGb4pEwl9MD2qicqMzuXR2QWp2/Pg=
|
||||||
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
|
github.com/RyuaNerin/go-krypto v1.3.0/go.mod h1:9R9TU936laAIqAmjcHo/LsaXYOZlymudOAxjaBf62UM=
|
||||||
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
|
github.com/RyuaNerin/testingutil v0.1.0 h1:IYT6JL57RV3U2ml3dLHZsVtPOP6yNK7WUVdzzlpNrss=
|
||||||
@@ -22,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
|||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
|
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
|
||||||
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
||||||
github.com/enfein/mieru/v3 v3.26.2 h1:U/2XJc+3vrJD9r815FoFdwToQFEcqSOzzzWIPPhjfEU=
|
github.com/enfein/mieru/v3 v3.28.0 h1:4OsFPUIjKfQ6ymfyX1Laqz7h+zB8TxuK1m0isnYJ8ww=
|
||||||
github.com/enfein/mieru/v3 v3.26.2/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
github.com/enfein/mieru/v3 v3.28.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||||
@@ -93,6 +91,8 @@ github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727 h1:qbZQ0sO
|
|||||||
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=
|
github.com/metacubex/connect-ip-go v0.0.0-20260128031117-1cad62060727/go.mod h1:xYC8Ik7/rN6no+vTRuWMEziGwm3brA0wNM/zZP9qhOQ=
|
||||||
github.com/metacubex/cpu v0.1.0 h1:8PeTdV9j6UKbN1K5Jvtbi/Jock7dknvzyYuLb8Conmk=
|
github.com/metacubex/cpu v0.1.0 h1:8PeTdV9j6UKbN1K5Jvtbi/Jock7dknvzyYuLb8Conmk=
|
||||||
github.com/metacubex/cpu v0.1.0/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=
|
github.com/metacubex/cpu v0.1.0/go.mod h1:09VEt4dSRLR+bOA8l4w4NDuzGZ8n5dkMv7e8axgEeTU=
|
||||||
|
github.com/metacubex/edwards25519 v1.2.0 h1:pIQZLBsjQgg3Nl/c86YYFEUAbL5qQRnPq4LrgIw0KK4=
|
||||||
|
github.com/metacubex/edwards25519 v1.2.0/go.mod h1:NCQF3J/Ki7382FJuokwsywEIIEI/gro/3smyXgQJsx0=
|
||||||
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
|
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
|
||||||
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
|
||||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
|
||||||
@@ -113,8 +113,8 @@ github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3Dmy
|
|||||||
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
|
||||||
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
github.com/metacubex/qpack v0.6.0 h1:YqClGIMOpiRYLjV1qOs483Od08MdPgRnHjt90FuaAKw=
|
||||||
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
|
github.com/metacubex/qpack v0.6.0/go.mod h1:lKGSi7Xk94IMvHGOmxS9eIei3bvIqpOAImEBsaOwTkA=
|
||||||
github.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af h1:do5o1rzn64NEN5oGswo7VruDkbz2055fhVT3rXehA8E=
|
github.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56 h1:7yfF31COW2hiCovb5+3uSxRl3UKWOXjpS0j4N5U0qZ8=
|
||||||
github.com/metacubex/quic-go v0.59.1-0.20260128071132-0f3233b973af/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
|
github.com/metacubex/quic-go v0.59.1-0.20260213014310-4df8f0de5b56/go.mod h1:oNzMrmylS897M3zSMuapIdwSwfq6F2qW01Z3NhVRJhk=
|
||||||
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiLs=
|
||||||
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY=
|
||||||
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
github.com/metacubex/restls-client-go v0.1.7 h1:eCwiXCTQb5WJu9IlgYvDBA1OgrINv58dEe7hcN5H15k=
|
||||||
@@ -131,8 +131,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.7 h1:hSuuc0YpsfiqYqt1o+fP4m34BQz4e6w
|
|||||||
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
|
github.com/metacubex/sing-shadowsocks2 v0.2.7/go.mod h1:vOEbfKC60txi0ca+yUlqEwOGc3Obl6cnSgx9Gf45KjE=
|
||||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
|
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
|
||||||
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
|
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
|
||||||
github.com/metacubex/sing-tun v0.4.15 h1:0uOO8kCpodgs4Op8L7sn+C4J6a/lQagmeRTrzHxn+mo=
|
github.com/metacubex/sing-tun v0.4.16-0.20260213034958-b09e3f5bbae6 h1:3yeZyDHGBmI/1XLsWBhr1sLhifWHkJa5J5Kf8djIbqs=
|
||||||
github.com/metacubex/sing-tun v0.4.15/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
|
github.com/metacubex/sing-tun v0.4.16-0.20260213034958-b09e3f5bbae6/go.mod h1:L/TjQY5JEGy8nvsuYmy/XgMFMCPiF0+AWSFCYfS6r9w=
|
||||||
github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE=
|
github.com/metacubex/sing-vmess v0.2.5 h1:m9Zt5I27lB9fmLMZfism9sH2LcnAfShZfwSkf6/KJoE=
|
||||||
github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q=
|
github.com/metacubex/sing-vmess v0.2.5/go.mod h1:AwtlzUgf8COe9tRYAKqWZ+leDH7p5U98a0ZUpYehl8Q=
|
||||||
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
|
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type SudokuServer struct {
|
|||||||
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
|
DisableHTTPMask bool `json:"disable-http-mask,omitempty"`
|
||||||
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
|
HTTPMaskMode string `json:"http-mask-mode,omitempty"`
|
||||||
PathRoot string `json:"path-root,omitempty"`
|
PathRoot string `json:"path-root,omitempty"`
|
||||||
|
Fallback string `json:"fallback,omitempty"`
|
||||||
|
|
||||||
// mihomo private extension (not the part of standard Sudoku protocol)
|
// mihomo private extension (not the part of standard Sudoku protocol)
|
||||||
MuxOption sing.MuxOption `json:"mux-option,omitempty"`
|
MuxOption sing.MuxOption `json:"mux-option,omitempty"`
|
||||||
|
|||||||
24
listener/config/trusttunnel.go
Normal file
24
listener/config/trusttunnel.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrustTunnelServer struct {
|
||||||
|
Enable bool `yaml:"enable" json:"enable"`
|
||||||
|
Listen string `yaml:"listen" json:"listen"`
|
||||||
|
Users map[string]string `yaml:"users" json:"users,omitempty"`
|
||||||
|
Certificate string `yaml:"certificate" json:"certificate"`
|
||||||
|
PrivateKey string `yaml:"private-key" json:"private-key"`
|
||||||
|
ClientAuthType string `yaml:"client-auth-type" json:"client-auth-type,omitempty"`
|
||||||
|
ClientAuthCert string `yaml:"client-auth-cert" json:"client-auth-cert,omitempty"`
|
||||||
|
EchKey string `yaml:"ech-key" json:"ech-key"`
|
||||||
|
Network []string `yaml:"network" json:"network,omitempty"`
|
||||||
|
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
|
||||||
|
CWND int `yaml:"cwnd" json:"cwnd,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TrustTunnelServer) String() string {
|
||||||
|
b, _ := json.Marshal(t)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
mieruserver "github.com/enfein/mieru/v3/apis/server"
|
mieruserver "github.com/enfein/mieru/v3/apis/server"
|
||||||
|
mierutp "github.com/enfein/mieru/v3/apis/trafficpattern"
|
||||||
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,8 +27,9 @@ type Mieru struct {
|
|||||||
|
|
||||||
type MieruOption struct {
|
type MieruOption struct {
|
||||||
BaseOption
|
BaseOption
|
||||||
Transport string `inbound:"transport"`
|
Transport string `inbound:"transport"`
|
||||||
Users map[string]string `inbound:"users"`
|
Users map[string]string `inbound:"users"`
|
||||||
|
TrafficPattern string `inbound:"traffic-pattern,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type mieruListenerFactory struct{}
|
type mieruListenerFactory struct{}
|
||||||
@@ -154,10 +156,13 @@ func buildMieruServerConfig(option *MieruOption, ports utils.IntRanges[uint16])
|
|||||||
Password: proto.String(password),
|
Password: proto.String(password),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
var trafficPattern *mierupb.TrafficPattern
|
||||||
|
trafficPattern, _ = mierutp.Decode(option.TrafficPattern)
|
||||||
return &mieruserver.ServerConfig{
|
return &mieruserver.ServerConfig{
|
||||||
Config: &mierupb.ServerConfig{
|
Config: &mierupb.ServerConfig{
|
||||||
PortBindings: portBindings,
|
PortBindings: portBindings,
|
||||||
Users: users,
|
Users: users,
|
||||||
|
TrafficPattern: trafficPattern,
|
||||||
},
|
},
|
||||||
StreamListenerFactory: mieruListenerFactory{},
|
StreamListenerFactory: mieruListenerFactory{},
|
||||||
PacketListenerFactory: mieruListenerFactory{},
|
PacketListenerFactory: mieruListenerFactory{},
|
||||||
@@ -179,5 +184,14 @@ func validateMieruOption(option *MieruOption) error {
|
|||||||
return fmt.Errorf("password is empty")
|
return fmt.Errorf("password is empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if option.TrafficPattern != "" {
|
||||||
|
trafficPattern, err := mierutp.Decode(option.TrafficPattern)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to decode traffic pattern %q: %w", option.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
if err := mierutp.Validate(trafficPattern); err != nil {
|
||||||
|
return fmt.Errorf("invalid traffic pattern %q: %w", option.TrafficPattern, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,20 @@ func TestNewMieru(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "valid traffic pattern",
|
||||||
|
args: args{
|
||||||
|
option: &inbound.MieruOption{
|
||||||
|
BaseOption: inbound.BaseOption{
|
||||||
|
Port: "8080",
|
||||||
|
},
|
||||||
|
Transport: "TCP",
|
||||||
|
Users: map[string]string{"user": "pass"},
|
||||||
|
TrafficPattern: "GgQIARAK",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "invalid - no port",
|
name: "invalid - no port",
|
||||||
args: args{
|
args: args{
|
||||||
@@ -135,6 +149,20 @@ func TestNewMieru(t *testing.T) {
|
|||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid traffic pattern",
|
||||||
|
args: args{
|
||||||
|
option: &inbound.MieruOption{
|
||||||
|
BaseOption: inbound.BaseOption{
|
||||||
|
Port: "8080",
|
||||||
|
},
|
||||||
|
Transport: "TCP",
|
||||||
|
Users: map[string]string{"user": "pass"},
|
||||||
|
TrafficPattern: "1212ababXYYX",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
|||||||
@@ -13,23 +13,31 @@ import (
|
|||||||
|
|
||||||
type SudokuOption struct {
|
type SudokuOption struct {
|
||||||
BaseOption
|
BaseOption
|
||||||
Key string `inbound:"key"`
|
Key string `inbound:"key"`
|
||||||
AEADMethod string `inbound:"aead-method,omitempty"`
|
AEADMethod string `inbound:"aead-method,omitempty"`
|
||||||
PaddingMin *int `inbound:"padding-min,omitempty"`
|
PaddingMin *int `inbound:"padding-min,omitempty"`
|
||||||
PaddingMax *int `inbound:"padding-max,omitempty"`
|
PaddingMax *int `inbound:"padding-max,omitempty"`
|
||||||
TableType string `inbound:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
TableType string `inbound:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
||||||
HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"`
|
HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"`
|
||||||
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
|
EnablePureDownlink *bool `inbound:"enable-pure-downlink,omitempty"`
|
||||||
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
CustomTable string `inbound:"custom-table,omitempty"` // optional custom byte layout, e.g. xpxvvpvv
|
||||||
CustomTables []string `inbound:"custom-tables,omitempty"`
|
CustomTables []string `inbound:"custom-tables,omitempty"`
|
||||||
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
|
DisableHTTPMask bool `inbound:"disable-http-mask,omitempty"`
|
||||||
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
|
HTTPMaskMode string `inbound:"http-mask-mode,omitempty"` // "legacy" (default), "stream", "poll", "auto"
|
||||||
PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
|
PathRoot string `inbound:"path-root,omitempty"` // optional first-level path prefix for HTTP tunnel endpoints
|
||||||
|
Fallback string `inbound:"fallback,omitempty"`
|
||||||
|
HTTPMaskOptions *SudokuHTTPMaskOptions `inbound:"httpmask,omitempty"`
|
||||||
|
|
||||||
// mihomo private extension (not the part of standard Sudoku protocol)
|
// mihomo private extension (not the part of standard Sudoku protocol)
|
||||||
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
MuxOption MuxOption `inbound:"mux-option,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SudokuHTTPMaskOptions struct {
|
||||||
|
Disable bool `inbound:"disable,omitempty"`
|
||||||
|
Mode string `inbound:"mode,omitempty"`
|
||||||
|
PathRoot string `inbound:"path_root,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func (o SudokuOption) Equal(config C.InboundConfig) bool {
|
func (o SudokuOption) Equal(config C.InboundConfig) bool {
|
||||||
return optionToString(o) == optionToString(config)
|
return optionToString(o) == optionToString(config)
|
||||||
}
|
}
|
||||||
@@ -65,6 +73,16 @@ func NewSudoku(options *SudokuOption) (*Sudoku, error) {
|
|||||||
DisableHTTPMask: options.DisableHTTPMask,
|
DisableHTTPMask: options.DisableHTTPMask,
|
||||||
HTTPMaskMode: options.HTTPMaskMode,
|
HTTPMaskMode: options.HTTPMaskMode,
|
||||||
PathRoot: strings.TrimSpace(options.PathRoot),
|
PathRoot: strings.TrimSpace(options.PathRoot),
|
||||||
|
Fallback: strings.TrimSpace(options.Fallback),
|
||||||
|
}
|
||||||
|
if hm := options.HTTPMaskOptions; hm != nil {
|
||||||
|
serverConf.DisableHTTPMask = hm.Disable
|
||||||
|
if hm.Mode != "" {
|
||||||
|
serverConf.HTTPMaskMode = hm.Mode
|
||||||
|
}
|
||||||
|
if pr := strings.TrimSpace(hm.PathRoot); pr != "" {
|
||||||
|
serverConf.PathRoot = pr
|
||||||
|
}
|
||||||
}
|
}
|
||||||
serverConf.MuxOption = options.MuxOption.Build()
|
serverConf.MuxOption = options.MuxOption.Build()
|
||||||
|
|
||||||
|
|||||||
@@ -168,16 +168,17 @@ func TestInboundSudoku_CustomTable(t *testing.T) {
|
|||||||
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
|
func TestInboundSudoku_HTTPMaskMode(t *testing.T) {
|
||||||
key := "test_key_http_mask_mode"
|
key := "test_key_http_mask_mode"
|
||||||
|
|
||||||
for _, mode := range []string{"legacy", "stream", "poll", "auto"} {
|
for _, mode := range []string{"ws", "stream", "poll", "auto"} {
|
||||||
mode := mode
|
mode := mode
|
||||||
t.Run(mode, func(t *testing.T) {
|
t.Run(mode, func(t *testing.T) {
|
||||||
inboundOptions := inbound.SudokuOption{
|
inboundOptions := inbound.SudokuOption{
|
||||||
Key: key,
|
Key: key,
|
||||||
HTTPMaskMode: mode,
|
HTTPMaskMode: mode,
|
||||||
}
|
}
|
||||||
|
httpMask := true
|
||||||
outboundOptions := outbound.SudokuOption{
|
outboundOptions := outbound.SudokuOption{
|
||||||
Key: key,
|
Key: key,
|
||||||
HTTPMask: true,
|
HTTPMask: &httpMask,
|
||||||
HTTPMaskMode: mode,
|
HTTPMaskMode: mode,
|
||||||
}
|
}
|
||||||
testInboundSudoku(t, inboundOptions, outboundOptions)
|
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||||
|
|||||||
96
listener/inbound/trusttunnel.go
Normal file
96
listener/inbound/trusttunnel.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package inbound
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
LC "github.com/metacubex/mihomo/listener/config"
|
||||||
|
"github.com/metacubex/mihomo/listener/trusttunnel"
|
||||||
|
"github.com/metacubex/mihomo/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TrustTunnelOption struct {
|
||||||
|
BaseOption
|
||||||
|
Users AuthUsers `inbound:"users,omitempty"`
|
||||||
|
Certificate string `inbound:"certificate"`
|
||||||
|
PrivateKey string `inbound:"private-key"`
|
||||||
|
ClientAuthType string `inbound:"client-auth-type,omitempty"`
|
||||||
|
ClientAuthCert string `inbound:"client-auth-cert,omitempty"`
|
||||||
|
EchKey string `inbound:"ech-key,omitempty"`
|
||||||
|
Network []string `inbound:"network,omitempty"`
|
||||||
|
CongestionController string `inbound:"congestion-controller,omitempty"`
|
||||||
|
CWND int `inbound:"cwnd,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o TrustTunnelOption) Equal(config C.InboundConfig) bool {
|
||||||
|
return optionToString(o) == optionToString(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TrustTunnel struct {
|
||||||
|
*Base
|
||||||
|
config *TrustTunnelOption
|
||||||
|
l C.MultiAddrListener
|
||||||
|
vs LC.TrustTunnelServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTrustTunnel(options *TrustTunnelOption) (*TrustTunnel, error) {
|
||||||
|
base, err := NewBase(&options.BaseOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
users := make(map[string]string)
|
||||||
|
for _, user := range options.Users {
|
||||||
|
users[user.Username] = user.Password
|
||||||
|
}
|
||||||
|
return &TrustTunnel{
|
||||||
|
Base: base,
|
||||||
|
config: options,
|
||||||
|
vs: LC.TrustTunnelServer{
|
||||||
|
Enable: true,
|
||||||
|
Listen: base.RawAddress(),
|
||||||
|
Users: users,
|
||||||
|
Certificate: options.Certificate,
|
||||||
|
PrivateKey: options.PrivateKey,
|
||||||
|
ClientAuthType: options.ClientAuthType,
|
||||||
|
ClientAuthCert: options.ClientAuthCert,
|
||||||
|
EchKey: options.EchKey,
|
||||||
|
Network: options.Network,
|
||||||
|
CongestionController: options.CongestionController,
|
||||||
|
CWND: options.CWND,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config implements constant.InboundListener
|
||||||
|
func (v *TrustTunnel) Config() C.InboundConfig {
|
||||||
|
return v.config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address implements constant.InboundListener
|
||||||
|
func (v *TrustTunnel) Address() string {
|
||||||
|
var addrList []string
|
||||||
|
if v.l != nil {
|
||||||
|
for _, addr := range v.l.AddrList() {
|
||||||
|
addrList = append(addrList, addr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(addrList, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen implements constant.InboundListener
|
||||||
|
func (v *TrustTunnel) Listen(tunnel C.Tunnel) error {
|
||||||
|
var err error
|
||||||
|
v.l, err = trusttunnel.New(v.vs, tunnel, v.Additions()...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Infoln("TrustTunnel[%s] proxy listening at: %s", v.Name(), v.Address())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements constant.InboundListener
|
||||||
|
func (v *TrustTunnel) Close() error {
|
||||||
|
return v.l.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ C.InboundListener = (*TrustTunnel)(nil)
|
||||||
109
listener/inbound/trusttunnel_test.go
Normal file
109
listener/inbound/trusttunnel_test.go
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package inbound_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/adapter/outbound"
|
||||||
|
"github.com/metacubex/mihomo/listener/inbound"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testInboundTrustTunnel(t *testing.T, inboundOptions inbound.TrustTunnelOption, outboundOptions outbound.TrustTunnelOption) {
|
||||||
|
t.Parallel()
|
||||||
|
inboundOptions.BaseOption = inbound.BaseOption{
|
||||||
|
NameStr: "trusttunnel_inbound",
|
||||||
|
Listen: "127.0.0.1",
|
||||||
|
Port: "0",
|
||||||
|
}
|
||||||
|
inboundOptions.Users = []inbound.AuthUser{{Username: "test", Password: userUUID}}
|
||||||
|
in, err := inbound.NewTrustTunnel(&inboundOptions)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := NewHttpTestTunnel()
|
||||||
|
defer tunnel.Close()
|
||||||
|
|
||||||
|
err = in.Listen(tunnel)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
addrPort, err := netip.ParseAddrPort(in.Address())
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundOptions.Name = "trusttunnel_outbound"
|
||||||
|
outboundOptions.Server = addrPort.Addr().String()
|
||||||
|
outboundOptions.Port = int(addrPort.Port())
|
||||||
|
outboundOptions.UserName = "test"
|
||||||
|
outboundOptions.Password = userUUID
|
||||||
|
|
||||||
|
out, err := outbound.NewTrustTunnel(outboundOptions)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
tunnel.DoTest(t, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInboundTrustTunnelTLS(t *testing.T, quic bool) {
|
||||||
|
inboundOptions := inbound.TrustTunnelOption{
|
||||||
|
Certificate: tlsCertificate,
|
||||||
|
PrivateKey: tlsPrivateKey,
|
||||||
|
}
|
||||||
|
outboundOptions := outbound.TrustTunnelOption{
|
||||||
|
Fingerprint: tlsFingerprint,
|
||||||
|
HealthCheck: true,
|
||||||
|
}
|
||||||
|
if quic {
|
||||||
|
inboundOptions.Network = []string{"udp"}
|
||||||
|
inboundOptions.CongestionController = "bbr"
|
||||||
|
outboundOptions.Quic = true
|
||||||
|
}
|
||||||
|
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
|
||||||
|
t.Run("ECH", func(t *testing.T) {
|
||||||
|
inboundOptions := inboundOptions
|
||||||
|
outboundOptions := outboundOptions
|
||||||
|
inboundOptions.EchKey = echKeyPem
|
||||||
|
outboundOptions.ECHOpts = outbound.ECHOptions{
|
||||||
|
Enable: true,
|
||||||
|
Config: echConfigBase64,
|
||||||
|
}
|
||||||
|
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
|
||||||
|
})
|
||||||
|
t.Run("mTLS", func(t *testing.T) {
|
||||||
|
inboundOptions := inboundOptions
|
||||||
|
outboundOptions := outboundOptions
|
||||||
|
inboundOptions.ClientAuthCert = tlsAuthCertificate
|
||||||
|
outboundOptions.Certificate = tlsAuthCertificate
|
||||||
|
outboundOptions.PrivateKey = tlsAuthPrivateKey
|
||||||
|
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
|
||||||
|
})
|
||||||
|
t.Run("mTLS+ECH", func(t *testing.T) {
|
||||||
|
inboundOptions := inboundOptions
|
||||||
|
outboundOptions := outboundOptions
|
||||||
|
inboundOptions.ClientAuthCert = tlsAuthCertificate
|
||||||
|
outboundOptions.Certificate = tlsAuthCertificate
|
||||||
|
outboundOptions.PrivateKey = tlsAuthPrivateKey
|
||||||
|
inboundOptions.EchKey = echKeyPem
|
||||||
|
outboundOptions.ECHOpts = outbound.ECHOptions{
|
||||||
|
Enable: true,
|
||||||
|
Config: echConfigBase64,
|
||||||
|
}
|
||||||
|
testInboundTrustTunnel(t, inboundOptions, outboundOptions)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInboundTrustTunnel_H2(t *testing.T) {
|
||||||
|
testInboundTrustTunnelTLS(t, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInboundTrustTunnel_QUIC(t *testing.T) {
|
||||||
|
testInboundTrustTunnelTLS(t, true)
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ type VmessOption struct {
|
|||||||
type VmessUser struct {
|
type VmessUser struct {
|
||||||
Username string `inbound:"username,omitempty"`
|
Username string `inbound:"username,omitempty"`
|
||||||
UUID string `inbound:"uuid"`
|
UUID string `inbound:"uuid"`
|
||||||
AlterID int `inbound:"alterId"`
|
AlterID int `inbound:"alterId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o VmessOption) Equal(config C.InboundConfig) bool {
|
func (o VmessOption) Equal(config C.InboundConfig) bool {
|
||||||
|
|||||||
@@ -141,6 +141,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
listener, err = IN.NewSudoku(sudokuOption)
|
listener, err = IN.NewSudoku(sudokuOption)
|
||||||
|
case "trusttunnel":
|
||||||
|
trusttunnelOption := &IN.TrustTunnelOption{}
|
||||||
|
err = decoder.Decode(mapping, trusttunnelOption)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
listener, err = IN.NewTrustTunnel(trusttunnelOption)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
package sudoku
|
package sudoku
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/adapter/inbound"
|
"github.com/metacubex/mihomo/adapter/inbound"
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
"github.com/metacubex/mihomo/common/utils"
|
"github.com/metacubex/mihomo/common/utils"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
LC "github.com/metacubex/mihomo/listener/config"
|
LC "github.com/metacubex/mihomo/listener/config"
|
||||||
|
"github.com/metacubex/mihomo/listener/inner"
|
||||||
"github.com/metacubex/mihomo/listener/sing"
|
"github.com/metacubex/mihomo/listener/sing"
|
||||||
"github.com/metacubex/mihomo/log"
|
"github.com/metacubex/mihomo/log"
|
||||||
"github.com/metacubex/mihomo/transport/socks5"
|
"github.com/metacubex/mihomo/transport/socks5"
|
||||||
@@ -23,6 +26,7 @@ type Listener struct {
|
|||||||
closed bool
|
closed bool
|
||||||
protoConf sudoku.ProtocolConfig
|
protoConf sudoku.ProtocolConfig
|
||||||
tunnelSrv *sudoku.HTTPMaskTunnelServer
|
tunnelSrv *sudoku.HTTPMaskTunnelServer
|
||||||
|
fallback string
|
||||||
handler *sing.ListenerHandler
|
handler *sing.ListenerHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,12 +53,19 @@ func (l *Listener) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||||
|
log.Debugln("[Sudoku] accepted %s", conn.RemoteAddr())
|
||||||
handshakeConn := conn
|
handshakeConn := conn
|
||||||
handshakeCfg := &l.protoConf
|
handshakeCfg := &l.protoConf
|
||||||
|
closeConns := func() {
|
||||||
|
_ = handshakeConn.Close()
|
||||||
|
if handshakeConn != conn {
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
if l.tunnelSrv != nil {
|
if l.tunnelSrv != nil {
|
||||||
c, cfg, done, err := l.tunnelSrv.WrapConn(conn)
|
c, cfg, done, err := l.tunnelSrv.WrapConn(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = conn.Close()
|
closeConns()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if done {
|
if done {
|
||||||
@@ -68,9 +79,43 @@ func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
|
if l.fallback != "" {
|
||||||
|
if r, ok := handshakeConn.(interface{ IsHTTPMaskRejected() bool }); ok && r.IsHTTPMaskRejected() {
|
||||||
|
fb, err := inner.HandleTcp(tunnel, l.fallback, "")
|
||||||
|
if err != nil {
|
||||||
|
closeConns()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
N.Relay(handshakeConn, fb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cConn, meta, err := sudoku.ServerHandshake(handshakeConn, handshakeCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = handshakeConn.Close()
|
fallbackAddr := l.fallback
|
||||||
|
var susp *sudoku.SuspiciousError
|
||||||
|
isSuspicious := errors.As(err, &susp) && susp != nil && susp.Conn != nil
|
||||||
|
if isSuspicious {
|
||||||
|
log.Warnln("[Sudoku] suspicious handshake from %s: %v", conn.RemoteAddr(), err)
|
||||||
|
if fallbackAddr != "" {
|
||||||
|
fb, err := inner.HandleTcp(tunnel, fallbackAddr, "")
|
||||||
|
if err == nil {
|
||||||
|
relayToFallback(susp.Conn, conn, fb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debugln("[Sudoku] handshake failed from %s: %v", conn.RemoteAddr(), err)
|
||||||
|
}
|
||||||
|
closeConns()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err := sudoku.ReadServerSession(cConn, meta)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnln("[Sudoku] read session failed from %s: %v", conn.RemoteAddr(), err)
|
||||||
|
_ = cConn.Close()
|
||||||
if handshakeConn != conn {
|
if handshakeConn != conn {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
}
|
}
|
||||||
@@ -103,6 +148,7 @@ func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
|
|||||||
default:
|
default:
|
||||||
targetAddr := socks5.ParseAddr(session.Target)
|
targetAddr := socks5.ParseAddr(session.Target)
|
||||||
if targetAddr == nil {
|
if targetAddr == nil {
|
||||||
|
log.Warnln("[Sudoku] invalid target from %s: %q", conn.RemoteAddr(), session.Target)
|
||||||
_ = session.Conn.Close()
|
_ = session.Conn.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -164,6 +210,24 @@ func (p *uotPacket) LocalAddr() net.Addr {
|
|||||||
return p.rAddr
|
return p.rAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func relayToFallback(wrapper net.Conn, rawConn net.Conn, fallback net.Conn) {
|
||||||
|
if wrapper != nil {
|
||||||
|
if recorder, ok := wrapper.(interface{ GetBufferedAndRecorded() []byte }); ok {
|
||||||
|
badData := recorder.GetBufferedAndRecorded()
|
||||||
|
if len(badData) > 0 {
|
||||||
|
_ = fallback.SetWriteDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
if _, err := io.Copy(fallback, bytes.NewReader(badData)); err != nil {
|
||||||
|
_ = fallback.Close()
|
||||||
|
_ = rawConn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = fallback.SetWriteDeadline(time.Time{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
N.Relay(rawConn, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
|
func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
|
||||||
if len(additions) == 0 {
|
if len(additions) == 0 {
|
||||||
additions = []inbound.Addition{
|
additions = []inbound.Addition{
|
||||||
@@ -188,42 +252,24 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tableType := strings.ToLower(config.TableType)
|
tableType, err := sudoku.NormalizeTableType(config.TableType)
|
||||||
if tableType == "" {
|
|
||||||
tableType = "prefer_ascii"
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConf := sudoku.DefaultConfig()
|
|
||||||
paddingMin := defaultConf.PaddingMin
|
|
||||||
paddingMax := defaultConf.PaddingMax
|
|
||||||
if config.PaddingMin != nil {
|
|
||||||
paddingMin = *config.PaddingMin
|
|
||||||
}
|
|
||||||
if config.PaddingMax != nil {
|
|
||||||
paddingMax = *config.PaddingMax
|
|
||||||
}
|
|
||||||
if config.PaddingMin == nil && config.PaddingMax != nil && paddingMax < paddingMin {
|
|
||||||
paddingMin = paddingMax
|
|
||||||
}
|
|
||||||
if config.PaddingMax == nil && config.PaddingMin != nil && paddingMax < paddingMin {
|
|
||||||
paddingMax = paddingMin
|
|
||||||
}
|
|
||||||
enablePureDownlink := defaultConf.EnablePureDownlink
|
|
||||||
if config.EnablePureDownlink != nil {
|
|
||||||
enablePureDownlink = *config.EnablePureDownlink
|
|
||||||
}
|
|
||||||
|
|
||||||
tables, err := sudoku.NewTablesWithCustomPatterns(config.Key, tableType, config.CustomTable, config.CustomTables)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = l.Close()
|
_ = l.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeTimeout := defaultConf.HandshakeTimeoutSeconds
|
defaultConf := sudoku.DefaultConfig()
|
||||||
if config.HandshakeTimeoutSecond != nil {
|
paddingMin, paddingMax := sudoku.ResolvePadding(config.PaddingMin, config.PaddingMax, defaultConf.PaddingMin, defaultConf.PaddingMax)
|
||||||
handshakeTimeout = *config.HandshakeTimeoutSecond
|
enablePureDownlink := sudoku.DerefBool(config.EnablePureDownlink, defaultConf.EnablePureDownlink)
|
||||||
|
|
||||||
|
tables, err := sudoku.NewServerTablesWithCustomPatterns(sudoku.ServerAEADSeed(config.Key), tableType, config.CustomTable, config.CustomTables)
|
||||||
|
if err != nil {
|
||||||
|
_ = l.Close()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handshakeTimeout := sudoku.DerefInt(config.HandshakeTimeoutSecond, defaultConf.HandshakeTimeoutSeconds)
|
||||||
|
|
||||||
protoConf := sudoku.ProtocolConfig{
|
protoConf := sudoku.ProtocolConfig{
|
||||||
Key: config.Key,
|
Key: config.Key,
|
||||||
AEADMethod: defaultConf.AEADMethod,
|
AEADMethod: defaultConf.AEADMethod,
|
||||||
@@ -249,8 +295,13 @@ func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition)
|
|||||||
addr: config.Listen,
|
addr: config.Listen,
|
||||||
protoConf: protoConf,
|
protoConf: protoConf,
|
||||||
handler: h,
|
handler: h,
|
||||||
|
fallback: strings.TrimSpace(config.Fallback),
|
||||||
|
}
|
||||||
|
if sl.fallback != "" {
|
||||||
|
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServerWithFallback(&sl.protoConf)
|
||||||
|
} else {
|
||||||
|
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
|
||||||
}
|
}
|
||||||
sl.tunnelSrv = sudoku.NewHTTPMaskTunnelServer(&sl.protoConf)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
|
|||||||
188
listener/trusttunnel/server.go
Normal file
188
listener/trusttunnel/server.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/adapter/inbound"
|
||||||
|
"github.com/metacubex/mihomo/common/sockopt"
|
||||||
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
|
"github.com/metacubex/mihomo/component/ech"
|
||||||
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
LC "github.com/metacubex/mihomo/listener/config"
|
||||||
|
"github.com/metacubex/mihomo/listener/sing"
|
||||||
|
"github.com/metacubex/mihomo/log"
|
||||||
|
"github.com/metacubex/mihomo/ntp"
|
||||||
|
"github.com/metacubex/mihomo/transport/trusttunnel"
|
||||||
|
|
||||||
|
"github.com/metacubex/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Listener struct {
|
||||||
|
closed bool
|
||||||
|
config LC.TrustTunnelServer
|
||||||
|
listeners []net.Listener
|
||||||
|
udpListeners []net.PacketConn
|
||||||
|
tlsConfig *tls.Config
|
||||||
|
services []*trusttunnel.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config LC.TrustTunnelServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) {
|
||||||
|
if len(additions) == 0 {
|
||||||
|
additions = []inbound.Addition{
|
||||||
|
inbound.WithInName("DEFAULT-TRUSTTUNNEL"),
|
||||||
|
inbound.WithSpecialRules(""),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConfig := &tls.Config{Time: ntp.Now}
|
||||||
|
if config.Certificate != "" && config.PrivateKey != "" {
|
||||||
|
certLoader, err := ca.NewTLSKeyPairLoader(config.Certificate, config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig.GetCertificate = func(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
return certLoader()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.EchKey != "" {
|
||||||
|
err = ech.LoadECHKey(config.EchKey, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tlsConfig.ClientAuth = ca.ClientAuthTypeFromString(config.ClientAuthType)
|
||||||
|
if len(config.ClientAuthCert) > 0 {
|
||||||
|
if tlsConfig.ClientAuth == tls.NoClientCert {
|
||||||
|
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
|
||||||
|
pool, err := ca.LoadCertificates(config.ClientAuthCert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig.ClientCAs = pool
|
||||||
|
}
|
||||||
|
|
||||||
|
sl = &Listener{
|
||||||
|
config: config,
|
||||||
|
tlsConfig: tlsConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
h, err := sing.NewListenerHandler(sing.ListenerConfig{
|
||||||
|
Tunnel: tunnel,
|
||||||
|
Type: C.TRUSTTUNNEL,
|
||||||
|
Additions: additions,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tlsConfig.GetCertificate == nil {
|
||||||
|
return nil, errors.New("disallow using TrustTunnel without certificates config")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(config.Network) == 0 {
|
||||||
|
config.Network = []string{"tcp"}
|
||||||
|
}
|
||||||
|
listenTCP, listenUDP := false, false
|
||||||
|
for _, network := range config.Network {
|
||||||
|
network = strings.ToLower(network)
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(network, "tcp"):
|
||||||
|
listenTCP = true
|
||||||
|
case strings.HasPrefix(network, "udp"):
|
||||||
|
listenUDP = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range strings.Split(config.Listen, ",") {
|
||||||
|
addr := addr
|
||||||
|
|
||||||
|
var (
|
||||||
|
tcpListener net.Listener
|
||||||
|
udpConn net.PacketConn
|
||||||
|
)
|
||||||
|
if listenTCP {
|
||||||
|
tcpListener, err = inbound.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
_ = sl.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sl.listeners = append(sl.listeners, tcpListener)
|
||||||
|
}
|
||||||
|
if listenUDP {
|
||||||
|
udpConn, err = inbound.ListenPacket("udp", addr)
|
||||||
|
if err != nil {
|
||||||
|
_ = sl.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sockopt.UDPReuseaddr(udpConn); err != nil {
|
||||||
|
log.Warnln("Failed to Reuse UDP Address: %s", err)
|
||||||
|
}
|
||||||
|
sl.udpListeners = append(sl.udpListeners, udpConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := trusttunnel.NewService(trusttunnel.ServiceOptions{
|
||||||
|
Ctx: context.Background(),
|
||||||
|
Logger: log.SingLogger,
|
||||||
|
Handler: h,
|
||||||
|
ICMPHandler: nil,
|
||||||
|
QUICCongestionControl: config.CongestionController,
|
||||||
|
QUICCwnd: config.CWND,
|
||||||
|
})
|
||||||
|
service.UpdateUsers(config.Users)
|
||||||
|
err = service.Start(tcpListener, udpConn, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
_ = sl.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sl.services = append(sl.services, service)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Close() error {
|
||||||
|
l.closed = true
|
||||||
|
var retErr error
|
||||||
|
for _, lis := range l.services {
|
||||||
|
err := lis.Close()
|
||||||
|
if err != nil {
|
||||||
|
retErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, lis := range l.listeners {
|
||||||
|
err := lis.Close()
|
||||||
|
if err != nil {
|
||||||
|
retErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, lis := range l.udpListeners {
|
||||||
|
err := lis.Close()
|
||||||
|
if err != nil {
|
||||||
|
retErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) Config() string {
|
||||||
|
return l.config.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Listener) AddrList() (addrList []net.Addr) {
|
||||||
|
for _, lis := range l.listeners {
|
||||||
|
addrList = append(addrList, lis.Addr())
|
||||||
|
}
|
||||||
|
for _, lis := range l.udpListeners {
|
||||||
|
addrList = append(addrList, lis.LocalAddr())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -18,9 +18,9 @@ import (
|
|||||||
|
|
||||||
"github.com/metacubex/mihomo/common/buf"
|
"github.com/metacubex/mihomo/common/buf"
|
||||||
"github.com/metacubex/mihomo/common/pool"
|
"github.com/metacubex/mihomo/common/pool"
|
||||||
"github.com/metacubex/mihomo/component/ech"
|
|
||||||
tlsC "github.com/metacubex/mihomo/component/tls"
|
tlsC "github.com/metacubex/mihomo/component/tls"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
"github.com/metacubex/mihomo/transport/vmess"
|
||||||
|
|
||||||
"github.com/metacubex/http"
|
"github.com/metacubex/http"
|
||||||
"github.com/metacubex/http/httptrace"
|
"github.com/metacubex/http/httptrace"
|
||||||
@@ -40,10 +40,10 @@ var defaultHeader = http.Header{
|
|||||||
type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error)
|
type DialFn = func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
initFn func() (io.ReadCloser, netAddr, error)
|
initFn func() (io.ReadCloser, NetAddr, error)
|
||||||
writer io.Writer // writer must not nil
|
writer io.Writer // writer must not nil
|
||||||
closer io.Closer
|
closer io.Closer
|
||||||
netAddr
|
NetAddr
|
||||||
|
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
initErr error
|
initErr error
|
||||||
@@ -59,10 +59,9 @@ type Conn struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ServiceName string
|
ServiceName string
|
||||||
UserAgent string
|
UserAgent string
|
||||||
Host string
|
Host string
|
||||||
ClientFingerprint string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Conn) initReader() {
|
func (g *Conn) initReader() {
|
||||||
@@ -74,7 +73,7 @@ func (g *Conn) initReader() {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g.netAddr = addr
|
g.NetAddr = addr
|
||||||
|
|
||||||
g.closeMutex.Lock()
|
g.closeMutex.Lock()
|
||||||
defer g.closeMutex.Unlock()
|
defer g.closeMutex.Unlock()
|
||||||
@@ -247,7 +246,7 @@ func (g *Conn) SetDeadline(t time.Time) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint string, echConfig *ech.Config, realityConfig *tlsC.RealityConfig) *TransportWrap {
|
func NewHTTP2Client(dialFn DialFn, tlsConfig *vmess.TLSConfig) *TransportWrap {
|
||||||
dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
dialFunc := func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
|
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -260,65 +259,33 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint stri
|
|||||||
return pconn, nil
|
return pconn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if clientFingerprint, ok := tlsC.GetFingerprint(clientFingerprint); ok {
|
conn, err := vmess.StreamTLSConn(ctx, pconn, tlsConfig)
|
||||||
if realityConfig == nil {
|
if err != nil {
|
||||||
tlsConfig := tlsC.UConfig(cfg)
|
_ = pconn.Close()
|
||||||
err := echConfig.ClientHandleUTLS(ctx, tlsConfig)
|
return nil, err
|
||||||
if err != nil {
|
}
|
||||||
pconn.Close()
|
|
||||||
return nil, err
|
if tlsConfig.Reality == nil { // reality doesn't return the negotiated ALPN
|
||||||
}
|
switch tlsConn := conn.(type) {
|
||||||
tlsConn := tlsC.UClient(pconn, tlsConfig, clientFingerprint)
|
case interface{ ConnectionState() tls.ConnectionState }:
|
||||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
|
||||||
pconn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
state := tlsConn.ConnectionState()
|
state := tlsConn.ConnectionState()
|
||||||
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
|
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
|
||||||
tlsConn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
|
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
|
||||||
}
|
}
|
||||||
return tlsConn, nil
|
case interface{ ConnectionState() tlsC.ConnectionState }:
|
||||||
} else {
|
state := tlsConn.ConnectionState()
|
||||||
realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, cfg.ServerName, realityConfig)
|
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
|
||||||
if err != nil {
|
_ = conn.Close()
|
||||||
pconn.Close()
|
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
//state := realityConn.(*utls.UConn).ConnectionState()
|
|
||||||
//if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
|
|
||||||
// realityConn.Close()
|
|
||||||
// return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
|
|
||||||
//}
|
|
||||||
return realityConn, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if realityConfig != nil {
|
|
||||||
return nil, errors.New("REALITY is based on uTLS, please set a client-fingerprint")
|
|
||||||
}
|
|
||||||
|
|
||||||
err = echConfig.ClientHandle(ctx, cfg)
|
|
||||||
if err != nil {
|
|
||||||
pconn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn := tls.Client(pconn, cfg)
|
|
||||||
if err := conn.HandshakeContext(ctx); err != nil {
|
|
||||||
pconn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
state := conn.ConnectionState()
|
|
||||||
if p := state.NegotiatedProtocol; p != http.Http2NextProtoTLS {
|
|
||||||
conn.Close()
|
|
||||||
return nil, fmt.Errorf("http2: unexpected ALPN protocol %s, want %s", p, http.Http2NextProtoTLS)
|
|
||||||
}
|
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
transport := &http.Http2Transport{
|
transport := &http.Http2Transport{
|
||||||
DialTLSContext: dialFunc,
|
DialTLSContext: dialFunc,
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
AllowHTTP: false,
|
AllowHTTP: false,
|
||||||
DisableCompression: true,
|
DisableCompression: true,
|
||||||
PingTimeout: 0,
|
PingTimeout: 0,
|
||||||
@@ -351,7 +318,7 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
|
|||||||
|
|
||||||
header := defaultHeader.Clone()
|
header := defaultHeader.Clone()
|
||||||
if cfg.UserAgent != "" {
|
if cfg.UserAgent != "" {
|
||||||
header["user-agent"] = []string{cfg.UserAgent}
|
header.Set("User-Agent", cfg.UserAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
request := &http.Request{
|
request := &http.Request{
|
||||||
@@ -372,12 +339,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
|
|||||||
request = request.WithContext(transport.ctx)
|
request = request.WithContext(transport.ctx)
|
||||||
|
|
||||||
conn := &Conn{
|
conn := &Conn{
|
||||||
initFn: func() (io.ReadCloser, netAddr, error) {
|
initFn: func() (io.ReadCloser, NetAddr, error) {
|
||||||
nAddr := netAddr{}
|
nAddr := NetAddr{}
|
||||||
trace := &httptrace.ClientTrace{
|
trace := &httptrace.ClientTrace{
|
||||||
GotConn: func(connInfo httptrace.GotConnInfo) {
|
GotConn: func(connInfo httptrace.GotConnInfo) {
|
||||||
nAddr.localAddr = connInfo.Conn.LocalAddr()
|
nAddr.SetLocalAddr(connInfo.Conn.LocalAddr())
|
||||||
nAddr.remoteAddr = connInfo.Conn.RemoteAddr()
|
nAddr.SetRemoteAddr(connInfo.Conn.RemoteAddr())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace))
|
request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace))
|
||||||
@@ -394,12 +361,12 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
|
|||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, echConfig *ech.Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) {
|
func StreamGunWithConn(conn net.Conn, tlsConfig *vmess.TLSConfig, cfg *Config) (net.Conn, error) {
|
||||||
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return conn, nil
|
return conn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, echConfig, realityConfig)
|
transport := NewHTTP2Client(dialFn, tlsConfig)
|
||||||
c, err := StreamGunWithTransport(transport, cfg)
|
c, err := StreamGunWithTransport(transport, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/metacubex/mihomo/common/buf"
|
"github.com/metacubex/mihomo/common/buf"
|
||||||
N "github.com/metacubex/mihomo/common/net"
|
N "github.com/metacubex/mihomo/common/net"
|
||||||
C "github.com/metacubex/mihomo/constant"
|
|
||||||
|
|
||||||
"github.com/metacubex/http"
|
"github.com/metacubex/http"
|
||||||
"github.com/metacubex/http/h2c"
|
"github.com/metacubex/http/h2c"
|
||||||
@@ -42,17 +41,9 @@ func NewServerHandler(options ServerOption) http.Handler {
|
|||||||
writer.WriteHeader(http.StatusOK)
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
|
||||||
conn := &Conn{
|
conn := &Conn{
|
||||||
initFn: func() (io.ReadCloser, netAddr, error) {
|
initFn: func() (io.ReadCloser, NetAddr, error) {
|
||||||
nAddr := netAddr{}
|
nAddr := NetAddr{}
|
||||||
if request.RemoteAddr != "" {
|
nAddr.SetAddrFromRequest(request)
|
||||||
metadata := C.Metadata{}
|
|
||||||
if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil {
|
|
||||||
nAddr.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if addr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
|
|
||||||
nAddr.localAddr = addr
|
|
||||||
}
|
|
||||||
return request.Body, nAddr, nil
|
return request.Body, nAddr, nil
|
||||||
},
|
},
|
||||||
writer: writer,
|
writer: writer,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
|
||||||
"github.com/metacubex/http"
|
"github.com/metacubex/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,20 +20,40 @@ type TransportWrap struct {
|
|||||||
func (tw *TransportWrap) Close() error {
|
func (tw *TransportWrap) Close() error {
|
||||||
tw.closeOnce.Do(func() {
|
tw.closeOnce.Do(func() {
|
||||||
tw.cancel()
|
tw.cancel()
|
||||||
closeTransport(tw.Http2Transport)
|
CloseTransport(tw.Http2Transport)
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type netAddr struct {
|
type NetAddr struct {
|
||||||
remoteAddr net.Addr
|
remoteAddr net.Addr
|
||||||
localAddr net.Addr
|
localAddr net.Addr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (addr netAddr) RemoteAddr() net.Addr {
|
func (addr NetAddr) RemoteAddr() net.Addr {
|
||||||
return addr.remoteAddr
|
return addr.remoteAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (addr netAddr) LocalAddr() net.Addr {
|
func (addr NetAddr) LocalAddr() net.Addr {
|
||||||
return addr.localAddr
|
return addr.localAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (addr *NetAddr) SetAddrFromRequest(request *http.Request) {
|
||||||
|
if request.RemoteAddr != "" {
|
||||||
|
metadata := C.Metadata{}
|
||||||
|
if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil {
|
||||||
|
addr.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if netAddr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
|
||||||
|
addr.localAddr = netAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (addr *NetAddr) SetRemoteAddr(remoteAddr net.Addr) {
|
||||||
|
addr.remoteAddr = remoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (addr *NetAddr) SetLocalAddr(localAddr net.Addr) {
|
||||||
|
addr.localAddr = localAddr
|
||||||
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ func closeClientConn(cc *http.Http2ClientConn) { // like forceCloseConn() in htt
|
|||||||
_ = cc.Close()
|
_ = cc.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeTransport(tr *http.Http2Transport) {
|
func CloseTransport(tr *http.Http2Transport) {
|
||||||
connPool := transportConnPool(tr)
|
connPool := transportConnPool(tr)
|
||||||
p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data)
|
p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data)
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func EncodeAddress(rawAddr string) ([]byte, error) {
|
func EncodeAddress(rawAddr string) ([]byte, error) {
|
||||||
@@ -20,13 +21,21 @@ func EncodeAddress(rawAddr string) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf []byte
|
var buf []byte
|
||||||
|
if i := strings.IndexByte(host, '%'); i >= 0 {
|
||||||
|
// Zone identifiers are not representable in SOCKS5 IPv6 address encoding.
|
||||||
|
host = host[:i]
|
||||||
|
}
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
if ip4 := ip.To4(); ip4 != nil {
|
if ip4 := ip.To4(); ip4 != nil {
|
||||||
buf = append(buf, 0x01) // IPv4
|
buf = append(buf, 0x01) // IPv4
|
||||||
buf = append(buf, ip4...)
|
buf = append(buf, ip4...)
|
||||||
} else {
|
} else {
|
||||||
buf = append(buf, 0x04) // IPv6
|
buf = append(buf, 0x04) // IPv6
|
||||||
buf = append(buf, ip...)
|
ip16 := ip.To16()
|
||||||
|
if ip16 == nil {
|
||||||
|
return nil, fmt.Errorf("invalid ipv6: %q", host)
|
||||||
|
}
|
||||||
|
buf = append(buf, ip16...)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if len(host) > 255 {
|
if len(host) > 255 {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type ProtocolConfig struct {
|
|||||||
// - "stream": real HTTP tunnel (split-stream), CDN-compatible
|
// - "stream": real HTTP tunnel (split-stream), CDN-compatible
|
||||||
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
|
// - "poll": plain HTTP tunnel (authorize/push/pull), strong restricted-network pass-through
|
||||||
// - "auto": try stream then fall back to poll
|
// - "auto": try stream then fall back to poll
|
||||||
|
// - "ws": WebSocket tunnel (GET upgrade), CDN-friendly
|
||||||
HTTPMaskMode string
|
HTTPMaskMode string
|
||||||
|
|
||||||
// HTTPMaskTLSEnabled enables HTTPS for HTTP tunnel modes (client-side).
|
// HTTPMaskTLSEnabled enables HTTPS for HTTP tunnel modes (client-side).
|
||||||
@@ -109,9 +110,9 @@ func (c *ProtocolConfig) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMode)) {
|
switch strings.ToLower(strings.TrimSpace(c.HTTPMaskMode)) {
|
||||||
case "", "legacy", "stream", "poll", "auto":
|
case "", "legacy", "stream", "poll", "auto", "ws":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto", c.HTTPMaskMode)
|
return fmt.Errorf("invalid http-mask-mode: %s, must be one of: legacy, stream, poll, auto, ws", c.HTTPMaskMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" {
|
if v := strings.TrimSpace(c.HTTPMaskPathRoot); v != "" {
|
||||||
@@ -166,6 +167,44 @@ func DefaultConfig() *ProtocolConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DerefInt(v *int, def int) int {
|
||||||
|
if v == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
|
||||||
|
func DerefBool(v *bool, def bool) bool {
|
||||||
|
if v == nil {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return *v
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvePadding applies defaults and keeps min/max consistent when only one side is provided.
|
||||||
|
func ResolvePadding(min, max *int, defMin, defMax int) (int, int) {
|
||||||
|
paddingMin := DerefInt(min, defMin)
|
||||||
|
paddingMax := DerefInt(max, defMax)
|
||||||
|
switch {
|
||||||
|
case min == nil && max != nil && paddingMax < paddingMin:
|
||||||
|
paddingMin = paddingMax
|
||||||
|
case max == nil && min != nil && paddingMax < paddingMin:
|
||||||
|
paddingMax = paddingMin
|
||||||
|
}
|
||||||
|
return paddingMin, paddingMax
|
||||||
|
}
|
||||||
|
|
||||||
|
func NormalizeTableType(tableType string) (string, error) {
|
||||||
|
switch t := strings.ToLower(strings.TrimSpace(tableType)); t {
|
||||||
|
case "", "prefer_ascii":
|
||||||
|
return "prefer_ascii", nil
|
||||||
|
case "prefer_entropy":
|
||||||
|
return "prefer_entropy", nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("table-type must be prefer_ascii or prefer_entropy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *ProtocolConfig) tableCandidates() []*sudoku.Table {
|
func (c *ProtocolConfig) tableCandidates() []*sudoku.Table {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"filippo.io/edwards25519"
|
"github.com/metacubex/edwards25519"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyPair holds the scalar private key and point public key
|
// KeyPair holds the scalar private key and point public key
|
||||||
@@ -80,8 +80,8 @@ func RecoverPublicKey(keyHex string) (*edwards25519.Point, error) {
|
|||||||
return nil, fmt.Errorf("invalid scalar: %w", err)
|
return nil, fmt.Errorf("invalid scalar: %w", err)
|
||||||
}
|
}
|
||||||
return new(edwards25519.Point).ScalarBaseMult(x), nil
|
return new(edwards25519.Point).ScalarBaseMult(x), nil
|
||||||
|
}
|
||||||
} else if len(keyBytes) == 64 {
|
if len(keyBytes) == 64 {
|
||||||
// Split Key r || k
|
// Split Key r || k
|
||||||
rBytes := keyBytes[:32]
|
rBytes := keyBytes[:32]
|
||||||
kBytes := keyBytes[32:]
|
kBytes := keyBytes[32:]
|
||||||
|
|||||||
374
transport/sudoku/crypto/record_conn.go
Normal file
374
transport/sudoku/crypto/record_conn.go
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
)
|
||||||
|
|
||||||
|
// KeyUpdateAfterBytes controls automatic key rotation based on plaintext bytes.
|
||||||
|
// It is a package var (not config) to enable targeted tests with smaller thresholds.
|
||||||
|
var KeyUpdateAfterBytes int64 = 32 << 20 // 32 MiB
|
||||||
|
|
||||||
|
const (
|
||||||
|
recordHeaderSize = 12 // epoch(uint32) + seq(uint64) - also used as nonce+AAD.
|
||||||
|
maxFrameBodySize = 65535
|
||||||
|
)
|
||||||
|
|
||||||
|
type recordKeys struct {
|
||||||
|
baseSend []byte
|
||||||
|
baseRecv []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecordConn is a framed AEAD net.Conn with:
|
||||||
|
// - deterministic per-record nonce (epoch+seq)
|
||||||
|
// - per-direction key rotation (epoch), driven by plaintext byte counters
|
||||||
|
// - replay/out-of-order protection within the connection (strict seq check)
|
||||||
|
//
|
||||||
|
// Wire format per record:
|
||||||
|
// - uint16 bodyLen
|
||||||
|
// - header[12] = epoch(uint32 BE) || seq(uint64 BE) (plaintext)
|
||||||
|
// - ciphertext = AEAD(header as nonce, plaintext, header as AAD)
|
||||||
|
type RecordConn struct {
|
||||||
|
net.Conn
|
||||||
|
method string
|
||||||
|
|
||||||
|
writeMu sync.Mutex
|
||||||
|
readMu sync.Mutex
|
||||||
|
|
||||||
|
keys recordKeys
|
||||||
|
|
||||||
|
sendAEAD cipher.AEAD
|
||||||
|
sendAEADEpoch uint32
|
||||||
|
|
||||||
|
recvAEAD cipher.AEAD
|
||||||
|
recvAEADEpoch uint32
|
||||||
|
|
||||||
|
// Send direction state.
|
||||||
|
sendEpoch uint32
|
||||||
|
sendSeq uint64
|
||||||
|
sendBytes int64
|
||||||
|
|
||||||
|
// Receive direction state.
|
||||||
|
recvEpoch uint32
|
||||||
|
recvSeq uint64
|
||||||
|
|
||||||
|
readBuf bytes.Buffer
|
||||||
|
|
||||||
|
// writeFrame is a reusable buffer for [len||header||ciphertext] on the wire.
|
||||||
|
// Guarded by writeMu.
|
||||||
|
writeFrame []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RecordConn) CloseWrite() error {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
|
||||||
|
return cw.CloseWrite()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RecordConn) CloseRead() error {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cr, ok := c.Conn.(interface{ CloseRead() error }); ok {
|
||||||
|
return cr.CloseRead()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRecordConn(conn net.Conn, method string, baseSend, baseRecv []byte) (*RecordConn, error) {
|
||||||
|
if conn == nil {
|
||||||
|
return nil, fmt.Errorf("nil conn")
|
||||||
|
}
|
||||||
|
method = normalizeAEADMethod(method)
|
||||||
|
if method != "none" {
|
||||||
|
if err := validateBaseKey(baseSend); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid send base key: %w", err)
|
||||||
|
}
|
||||||
|
if err := validateBaseKey(baseRecv); err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid recv base key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rc := &RecordConn{Conn: conn, method: method}
|
||||||
|
rc.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)}
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RecordConn) Rekey(baseSend, baseRecv []byte) error {
|
||||||
|
if c == nil {
|
||||||
|
return fmt.Errorf("nil conn")
|
||||||
|
}
|
||||||
|
if c.method != "none" {
|
||||||
|
if err := validateBaseKey(baseSend); err != nil {
|
||||||
|
return fmt.Errorf("invalid send base key: %w", err)
|
||||||
|
}
|
||||||
|
if err := validateBaseKey(baseRecv); err != nil {
|
||||||
|
return fmt.Errorf("invalid recv base key: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.writeMu.Lock()
|
||||||
|
c.readMu.Lock()
|
||||||
|
defer c.readMu.Unlock()
|
||||||
|
defer c.writeMu.Unlock()
|
||||||
|
|
||||||
|
c.keys = recordKeys{baseSend: cloneBytes(baseSend), baseRecv: cloneBytes(baseRecv)}
|
||||||
|
c.sendEpoch = 0
|
||||||
|
c.sendSeq = 0
|
||||||
|
c.sendBytes = 0
|
||||||
|
c.recvEpoch = 0
|
||||||
|
c.recvSeq = 0
|
||||||
|
c.readBuf.Reset()
|
||||||
|
|
||||||
|
c.sendAEAD = nil
|
||||||
|
c.recvAEAD = nil
|
||||||
|
c.sendAEADEpoch = 0
|
||||||
|
c.recvAEADEpoch = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAEADMethod(method string) string {
|
||||||
|
switch method {
|
||||||
|
case "", "chacha20-poly1305":
|
||||||
|
return "chacha20-poly1305"
|
||||||
|
case "aes-128-gcm", "none":
|
||||||
|
return method
|
||||||
|
default:
|
||||||
|
return method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateBaseKey(b []byte) error {
|
||||||
|
if len(b) < 32 {
|
||||||
|
return fmt.Errorf("need at least 32 bytes, got %d", len(b))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cloneBytes(b []byte) []byte {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return append([]byte(nil), b...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RecordConn) newAEADFor(base []byte, epoch uint32) (cipher.AEAD, error) {
|
||||||
|
if c.method == "none" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
key := deriveEpochKey(base, epoch, c.method)
|
||||||
|
switch c.method {
|
||||||
|
case "aes-128-gcm":
|
||||||
|
block, err := aes.NewCipher(key[:16])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
a, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if a.NonceSize() != recordHeaderSize {
|
||||||
|
return nil, fmt.Errorf("unexpected gcm nonce size: %d", a.NonceSize())
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
case "chacha20-poly1305":
|
||||||
|
a, err := chacha20poly1305.New(key[:32])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if a.NonceSize() != recordHeaderSize {
|
||||||
|
return nil, fmt.Errorf("unexpected chacha nonce size: %d", a.NonceSize())
|
||||||
|
}
|
||||||
|
return a, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported cipher: %s", c.method)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveEpochKey(base []byte, epoch uint32, method string) []byte {
|
||||||
|
var b [4]byte
|
||||||
|
binary.BigEndian.PutUint32(b[:], epoch)
|
||||||
|
mac := hmac.New(sha256.New, base)
|
||||||
|
_, _ = mac.Write([]byte("sudoku-record:"))
|
||||||
|
_, _ = mac.Write([]byte(method))
|
||||||
|
_, _ = mac.Write(b[:])
|
||||||
|
return mac.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RecordConn) maybeBumpSendEpochLocked(addedPlain int) {
|
||||||
|
if KeyUpdateAfterBytes <= 0 || c.method == "none" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.sendBytes += int64(addedPlain)
|
||||||
|
threshold := KeyUpdateAfterBytes * int64(c.sendEpoch+1)
|
||||||
|
if c.sendBytes < threshold {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.sendEpoch++
|
||||||
|
c.sendSeq = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RecordConn) Write(p []byte) (int, error) {
|
||||||
|
if c == nil || c.Conn == nil {
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
}
|
||||||
|
if c.method == "none" {
|
||||||
|
return c.Conn.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.writeMu.Lock()
|
||||||
|
defer c.writeMu.Unlock()
|
||||||
|
|
||||||
|
total := 0
|
||||||
|
for len(p) > 0 {
|
||||||
|
if c.sendAEAD == nil || c.sendAEADEpoch != c.sendEpoch {
|
||||||
|
a, err := c.newAEADFor(c.keys.baseSend, c.sendEpoch)
|
||||||
|
if err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
c.sendAEAD = a
|
||||||
|
c.sendAEADEpoch = c.sendEpoch
|
||||||
|
}
|
||||||
|
aead := c.sendAEAD
|
||||||
|
|
||||||
|
maxPlain := maxFrameBodySize - recordHeaderSize - aead.Overhead()
|
||||||
|
if maxPlain <= 0 {
|
||||||
|
return total, errors.New("frame size too small")
|
||||||
|
}
|
||||||
|
n := len(p)
|
||||||
|
if n > maxPlain {
|
||||||
|
n = maxPlain
|
||||||
|
}
|
||||||
|
chunk := p[:n]
|
||||||
|
p = p[n:]
|
||||||
|
|
||||||
|
var header [recordHeaderSize]byte
|
||||||
|
binary.BigEndian.PutUint32(header[:4], c.sendEpoch)
|
||||||
|
binary.BigEndian.PutUint64(header[4:], c.sendSeq)
|
||||||
|
c.sendSeq++
|
||||||
|
|
||||||
|
cipherLen := n + aead.Overhead()
|
||||||
|
bodyLen := recordHeaderSize + cipherLen
|
||||||
|
frameLen := 2 + bodyLen
|
||||||
|
if bodyLen > maxFrameBodySize {
|
||||||
|
return total, errors.New("frame too large")
|
||||||
|
}
|
||||||
|
if cap(c.writeFrame) < frameLen {
|
||||||
|
c.writeFrame = make([]byte, frameLen)
|
||||||
|
}
|
||||||
|
frame := c.writeFrame[:frameLen]
|
||||||
|
binary.BigEndian.PutUint16(frame[:2], uint16(bodyLen))
|
||||||
|
copy(frame[2:2+recordHeaderSize], header[:])
|
||||||
|
|
||||||
|
dst := frame[2+recordHeaderSize : 2+recordHeaderSize : frameLen]
|
||||||
|
_ = aead.Seal(dst[:0], header[:], chunk, header[:])
|
||||||
|
|
||||||
|
if err := writeFull(c.Conn, frame); err != nil {
|
||||||
|
return total, err
|
||||||
|
}
|
||||||
|
|
||||||
|
total += n
|
||||||
|
c.maybeBumpSendEpochLocked(n)
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *RecordConn) Read(p []byte) (int, error) {
|
||||||
|
if c == nil || c.Conn == nil {
|
||||||
|
return 0, net.ErrClosed
|
||||||
|
}
|
||||||
|
if c.method == "none" {
|
||||||
|
return c.Conn.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.readMu.Lock()
|
||||||
|
defer c.readMu.Unlock()
|
||||||
|
|
||||||
|
if c.readBuf.Len() > 0 {
|
||||||
|
return c.readBuf.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lenBuf [2]byte
|
||||||
|
if _, err := io.ReadFull(c.Conn, lenBuf[:]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
bodyLen := int(binary.BigEndian.Uint16(lenBuf[:]))
|
||||||
|
if bodyLen < recordHeaderSize {
|
||||||
|
return 0, errors.New("frame too short")
|
||||||
|
}
|
||||||
|
if bodyLen > maxFrameBodySize {
|
||||||
|
return 0, errors.New("frame too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
body := make([]byte, bodyLen)
|
||||||
|
if _, err := io.ReadFull(c.Conn, body); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
header := body[:recordHeaderSize]
|
||||||
|
ciphertext := body[recordHeaderSize:]
|
||||||
|
|
||||||
|
epoch := binary.BigEndian.Uint32(header[:4])
|
||||||
|
seq := binary.BigEndian.Uint64(header[4:])
|
||||||
|
|
||||||
|
if epoch < c.recvEpoch {
|
||||||
|
return 0, fmt.Errorf("replayed epoch: got %d want >=%d", epoch, c.recvEpoch)
|
||||||
|
}
|
||||||
|
if epoch == c.recvEpoch && seq != c.recvSeq {
|
||||||
|
return 0, fmt.Errorf("out of order: epoch=%d got=%d want=%d", epoch, seq, c.recvSeq)
|
||||||
|
}
|
||||||
|
if epoch > c.recvEpoch {
|
||||||
|
const maxJump = 8
|
||||||
|
if epoch-c.recvEpoch > maxJump {
|
||||||
|
return 0, fmt.Errorf("epoch jump too large: got=%d want<=%d", epoch-c.recvEpoch, maxJump)
|
||||||
|
}
|
||||||
|
c.recvEpoch = epoch
|
||||||
|
c.recvSeq = 0
|
||||||
|
if seq != 0 {
|
||||||
|
return 0, fmt.Errorf("out of order: epoch advanced to %d but seq=%d", epoch, seq)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.recvAEAD == nil || c.recvAEADEpoch != c.recvEpoch {
|
||||||
|
a, err := c.newAEADFor(c.keys.baseRecv, c.recvEpoch)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
c.recvAEAD = a
|
||||||
|
c.recvAEADEpoch = c.recvEpoch
|
||||||
|
}
|
||||||
|
aead := c.recvAEAD
|
||||||
|
|
||||||
|
plaintext, err := aead.Open(nil, header, ciphertext, header)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("decryption failed: epoch=%d seq=%d: %w", epoch, seq, err)
|
||||||
|
}
|
||||||
|
c.recvSeq++
|
||||||
|
|
||||||
|
c.readBuf.Write(plaintext)
|
||||||
|
return c.readBuf.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFull(w io.Writer, b []byte) error {
|
||||||
|
for len(b) > 0 {
|
||||||
|
n, err := w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b = b[n:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -43,12 +43,17 @@ func TestCustomTablesRotation_ProbedByServer(t *testing.T) {
|
|||||||
go func() {
|
go func() {
|
||||||
defer close(errCh)
|
defer close(errCh)
|
||||||
defer serverConn.Close()
|
defer serverConn.Close()
|
||||||
session, err := ServerHandshake(serverConn, serverCfg)
|
c, meta, err := ServerHandshake(serverConn, serverCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer session.Conn.Close()
|
session, err := ReadServerSession(c, meta)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer c.Close()
|
||||||
if session.Type != SessionTypeTCP {
|
if session.Type != SessionTypeTCP {
|
||||||
errCh <- io.ErrUnexpectedEOF
|
errCh <- io.ErrUnexpectedEOF
|
||||||
return
|
return
|
||||||
@@ -69,7 +74,7 @@ func TestCustomTablesRotation_ProbedByServer(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("encode addr: %v", err)
|
t.Fatalf("encode addr: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := cConn.Write(addrBuf); err != nil {
|
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
t.Fatalf("write addr: %v", err)
|
t.Fatalf("write addr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,19 +2,20 @@ package sudoku
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"crypto/sha256"
|
"bytes"
|
||||||
"encoding/binary"
|
"crypto/ecdh"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
||||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/httpmask"
|
||||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SessionType int
|
type SessionType int
|
||||||
@@ -30,18 +31,96 @@ type ServerSession struct {
|
|||||||
Type SessionType
|
Type SessionType
|
||||||
Target string
|
Target string
|
||||||
|
|
||||||
// UserHash is a stable per-key identifier derived from the handshake payload.
|
// UserHash is a stable per-key identifier derived from the client hello payload.
|
||||||
// It is primarily useful for debugging / user attribution when table rotation is enabled.
|
|
||||||
UserHash string
|
UserHash string
|
||||||
}
|
}
|
||||||
|
|
||||||
type bufferedConn struct {
|
type HandshakeMeta struct {
|
||||||
net.Conn
|
UserHash string
|
||||||
r *bufio.Reader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bc *bufferedConn) Read(p []byte) (int, error) {
|
// SuspiciousError indicates a potential probing attempt or protocol violation.
|
||||||
return bc.r.Read(p)
|
// When returned, Conn (if non-nil) should contain all bytes already consumed/buffered so the caller
|
||||||
|
// can perform a best-effort fallback relay (e.g. to a local web server) without losing the request.
|
||||||
|
type SuspiciousError struct {
|
||||||
|
Err error
|
||||||
|
Conn net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SuspiciousError) Error() string {
|
||||||
|
if e == nil || e.Err == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *SuspiciousError) Unwrap() error { return e.Err }
|
||||||
|
|
||||||
|
type recordedConn struct {
|
||||||
|
net.Conn
|
||||||
|
recorded []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *recordedConn) GetBufferedAndRecorded() []byte { return rc.recorded }
|
||||||
|
|
||||||
|
type prefixedRecorderConn struct {
|
||||||
|
net.Conn
|
||||||
|
prefix []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *prefixedRecorderConn) GetBufferedAndRecorded() []byte {
|
||||||
|
var rest []byte
|
||||||
|
if r, ok := pc.Conn.(interface{ GetBufferedAndRecorded() []byte }); ok {
|
||||||
|
rest = r.GetBufferedAndRecorded()
|
||||||
|
}
|
||||||
|
out := make([]byte, 0, len(pc.prefix)+len(rest))
|
||||||
|
out = append(out, pc.prefix...)
|
||||||
|
out = append(out, rest...)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// bufferedRecorderConn wraps a net.Conn and a shared bufio.Reader so we can expose buffered bytes.
|
||||||
|
// This is used for legacy HTTP mask parsing errors so callers can fall back to a real HTTP server.
|
||||||
|
type bufferedRecorderConn struct {
|
||||||
|
net.Conn
|
||||||
|
r *bufio.Reader
|
||||||
|
recorder *bytes.Buffer
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *bufferedRecorderConn) Read(p []byte) (n int, err error) {
|
||||||
|
n, err = bc.r.Read(p)
|
||||||
|
if n > 0 && bc.recorder != nil {
|
||||||
|
bc.mu.Lock()
|
||||||
|
bc.recorder.Write(p[:n])
|
||||||
|
bc.mu.Unlock()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bc *bufferedRecorderConn) GetBufferedAndRecorded() []byte {
|
||||||
|
if bc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
bc.mu.Lock()
|
||||||
|
defer bc.mu.Unlock()
|
||||||
|
|
||||||
|
var recorded []byte
|
||||||
|
if bc.recorder != nil {
|
||||||
|
recorded = bc.recorder.Bytes()
|
||||||
|
}
|
||||||
|
buffered := 0
|
||||||
|
if bc.r != nil {
|
||||||
|
buffered = bc.r.Buffered()
|
||||||
|
}
|
||||||
|
if buffered <= 0 {
|
||||||
|
return recorded
|
||||||
|
}
|
||||||
|
peeked, _ := bc.r.Peek(buffered)
|
||||||
|
full := make([]byte, len(recorded)+len(peeked))
|
||||||
|
copy(full, recorded)
|
||||||
|
copy(full[len(recorded):], peeked)
|
||||||
|
return full
|
||||||
}
|
}
|
||||||
|
|
||||||
type preBufferedConn struct {
|
type preBufferedConn struct {
|
||||||
@@ -61,6 +140,26 @@ func (p *preBufferedConn) Read(b []byte) (int, error) {
|
|||||||
return p.Conn.Read(b)
|
return p.Conn.Read(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *preBufferedConn) CloseWrite() error {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cw, ok := p.Conn.(interface{ CloseWrite() error }); ok {
|
||||||
|
return cw.CloseWrite()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *preBufferedConn) CloseRead() error {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cr, ok := p.Conn.(interface{ CloseRead() error }); ok {
|
||||||
|
return cr.CloseRead()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type directionalConn struct {
|
type directionalConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
reader io.Reader
|
reader io.Reader
|
||||||
@@ -101,6 +200,26 @@ func (c *directionalConn) Close() error {
|
|||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *directionalConn) CloseWrite() error {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cw, ok := c.Conn.(interface{ CloseWrite() error }); ok {
|
||||||
|
return cw.CloseWrite()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *directionalConn) CloseRead() error {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cr, ok := c.Conn.(interface{ CloseRead() error }); ok {
|
||||||
|
return cr.CloseRead()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func absInt64(v int64) int64 {
|
func absInt64(v int64) int64 {
|
||||||
if v < 0 {
|
if v < 0 {
|
||||||
return -v
|
return -v
|
||||||
@@ -108,18 +227,6 @@ func absInt64(v int64) int64 {
|
|||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
downlinkModePure byte = 0x01
|
|
||||||
downlinkModePacked byte = 0x02
|
|
||||||
)
|
|
||||||
|
|
||||||
func downlinkMode(cfg *ProtocolConfig) byte {
|
|
||||||
if cfg.EnablePureDownlink {
|
|
||||||
return downlinkModePure
|
|
||||||
}
|
|
||||||
return downlinkModePacked
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
|
func buildClientObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table) net.Conn {
|
||||||
baseSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
|
baseSudoku := sudoku.NewConn(raw, table, cfg.PaddingMin, cfg.PaddingMax, false)
|
||||||
if cfg.EnablePureDownlink {
|
if cfg.EnablePureDownlink {
|
||||||
@@ -138,50 +245,16 @@ func buildServerObfsConn(raw net.Conn, cfg *ProtocolConfig, table *sudoku.Table,
|
|||||||
return uplinkSudoku, newDirectionalConn(raw, uplinkSudoku, packed, packed.Flush)
|
return uplinkSudoku, newDirectionalConn(raw, uplinkSudoku, packed, packed.Flush)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildHandshakePayload(key string) [16]byte {
|
func isLegacyHTTPMaskMode(mode string) bool {
|
||||||
var payload [16]byte
|
switch strings.ToLower(strings.TrimSpace(mode)) {
|
||||||
binary.BigEndian.PutUint64(payload[:8], uint64(time.Now().Unix()))
|
case "", "legacy":
|
||||||
|
return true
|
||||||
// Align with upstream: only decode hex bytes when this key is an ED25519 key material.
|
default:
|
||||||
// For plain UUID/strings (even if they look like hex), hash the string bytes as-is.
|
return false
|
||||||
src := []byte(key)
|
|
||||||
if _, err := crypto.RecoverPublicKey(key); err == nil {
|
|
||||||
if keyBytes, decErr := hex.DecodeString(key); decErr == nil && len(keyBytes) > 0 {
|
|
||||||
src = keyBytes
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := sha256.Sum256(src)
|
|
||||||
copy(payload[8:], hash[:8])
|
|
||||||
return payload
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTable(key string, tableType string) *sudoku.Table {
|
// ClientHandshake performs the client-side Sudoku handshake (no target request).
|
||||||
table, err := NewTableWithCustom(key, tableType, "")
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("[Sudoku] failed to init tables: %v", err))
|
|
||||||
}
|
|
||||||
return table
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTableWithCustom(key string, tableType string, customTable string) (*sudoku.Table, error) {
|
|
||||||
start := time.Now()
|
|
||||||
table, err := sudoku.NewTableWithCustom(key, tableType, customTable)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Infoln("[Sudoku] Tables initialized (%s, custom=%v) in %v", tableType, customTable != "", time.Since(start))
|
|
||||||
return table, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClientAEADSeed(key string) string {
|
|
||||||
if recovered, err := crypto.RecoverPublicKey(key); err == nil {
|
|
||||||
return crypto.EncodePoint(recovered)
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientHandshake performs the client-side Sudoku handshake (without sending target address).
|
|
||||||
func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
|
func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("config is required")
|
return nil, fmt.Errorf("config is required")
|
||||||
@@ -190,7 +263,7 @@ func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
|
|||||||
return nil, fmt.Errorf("invalid config: %w", err)
|
return nil, fmt.Errorf("invalid config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.DisableHTTPMask {
|
if !cfg.DisableHTTPMask && isLegacyHTTPMaskMode(cfg.HTTPMaskMode) {
|
||||||
if err := httpmask.WriteRandomRequestHeaderWithPathRoot(rawConn, cfg.ServerAddress, cfg.HTTPMaskPathRoot); err != nil {
|
if err := httpmask.WriteRandomRequestHeaderWithPathRoot(rawConn, cfg.ServerAddress, cfg.HTTPMaskPathRoot); err != nil {
|
||||||
return nil, fmt.Errorf("write http mask failed: %w", err)
|
return nil, fmt.Errorf("write http mask failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -201,32 +274,68 @@ func ClientHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
seed := ClientAEADSeed(cfg.Key)
|
||||||
obfsConn := buildClientObfsConn(rawConn, cfg, table)
|
obfsConn := buildClientObfsConn(rawConn, cfg, table)
|
||||||
cConn, err := crypto.NewAEADConn(obfsConn, ClientAEADSeed(cfg.Key), cfg.AEADMethod)
|
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
|
||||||
|
rc, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskC2S, pskS2C)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("setup crypto failed: %w", err)
|
return nil, fmt.Errorf("setup crypto failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handshake := buildHandshakePayload(cfg.Key)
|
if _, err := kipHandshakeClient(rc, seed, kipUserHashFromKey(cfg.Key), KIPFeatAll); err != nil {
|
||||||
if _, err := cConn.Write(handshake[:]); err != nil {
|
_ = rc.Close()
|
||||||
cConn.Close()
|
return nil, err
|
||||||
return nil, fmt.Errorf("send handshake failed: %w", err)
|
|
||||||
}
|
|
||||||
if _, err := cConn.Write([]byte{downlinkMode(cfg)}); err != nil {
|
|
||||||
cConn.Close()
|
|
||||||
return nil, fmt.Errorf("send downlink mode failed: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return cConn, nil
|
return rc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerHandshake performs Sudoku server-side handshake and detects UoT preface.
|
func readFirstSessionMessage(conn net.Conn) (*KIPMessage, error) {
|
||||||
func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, error) {
|
for {
|
||||||
|
msg, err := ReadKIPMessage(conn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if msg.Type == KIPTypeKeepAlive {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func maybeConsumeLegacyHTTPMask(rawConn net.Conn, r *bufio.Reader, cfg *ProtocolConfig) ([]byte, *SuspiciousError) {
|
||||||
|
if rawConn == nil || r == nil || cfg == nil || cfg.DisableHTTPMask || !isLegacyHTTPMaskMode(cfg.HTTPMaskMode) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
peekBytes, _ := r.Peek(4) // ignore error; subsequent read will handle it
|
||||||
|
if !httpmask.LooksLikeHTTPRequestStart(peekBytes) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
consumed, err := httpmask.ConsumeHeader(r)
|
||||||
|
if err == nil {
|
||||||
|
return consumed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder := new(bytes.Buffer)
|
||||||
|
if len(consumed) > 0 {
|
||||||
|
recorder.Write(consumed)
|
||||||
|
}
|
||||||
|
badConn := &bufferedRecorderConn{Conn: rawConn, r: r, recorder: recorder}
|
||||||
|
return consumed, &SuspiciousError{Err: fmt.Errorf("invalid http header: %w", err), Conn: badConn}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerHandshake performs the server-side KIP handshake.
|
||||||
|
func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (net.Conn, *HandshakeMeta, error) {
|
||||||
|
if rawConn == nil {
|
||||||
|
return nil, nil, fmt.Errorf("nil conn")
|
||||||
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("config is required")
|
return nil, nil, fmt.Errorf("config is required")
|
||||||
}
|
}
|
||||||
if err := cfg.Validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
return nil, fmt.Errorf("invalid config: %w", err)
|
return nil, nil, fmt.Errorf("invalid config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second
|
handshakeTimeout := time.Duration(cfg.HandshakeTimeoutSeconds) * time.Second
|
||||||
@@ -234,116 +343,113 @@ func ServerHandshake(rawConn net.Conn, cfg *ProtocolConfig) (*ServerSession, err
|
|||||||
handshakeTimeout = 5 * time.Second
|
handshakeTimeout = 5 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
rawConn.SetReadDeadline(time.Now().Add(handshakeTimeout))
|
|
||||||
|
|
||||||
bufReader := bufio.NewReader(rawConn)
|
bufReader := bufio.NewReader(rawConn)
|
||||||
if !cfg.DisableHTTPMask {
|
_ = rawConn.SetReadDeadline(time.Now().Add(handshakeTimeout))
|
||||||
if peek, err := bufReader.Peek(4); err == nil && httpmask.LooksLikeHTTPRequestStart(peek) {
|
defer func() { _ = rawConn.SetReadDeadline(time.Time{}) }()
|
||||||
if _, err := httpmask.ConsumeHeader(bufReader); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid http header: %w", err)
|
httpHeaderData, susp := maybeConsumeLegacyHTTPMask(rawConn, bufReader, cfg)
|
||||||
}
|
if susp != nil {
|
||||||
}
|
return nil, nil, susp
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates())
|
selectedTable, preRead, err := selectTableByProbe(bufReader, cfg, cfg.tableCandidates())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
combined := make([]byte, 0, len(httpHeaderData)+len(preRead))
|
||||||
|
combined = append(combined, httpHeaderData...)
|
||||||
|
combined = append(combined, preRead...)
|
||||||
|
return nil, nil, &SuspiciousError{Err: err, Conn: &recordedConn{Conn: rawConn, recorded: combined}}
|
||||||
}
|
}
|
||||||
|
|
||||||
baseConn := &preBufferedConn{Conn: rawConn, buf: preRead}
|
baseConn := &preBufferedConn{Conn: rawConn, buf: preRead}
|
||||||
bConn := &bufferedConn{Conn: baseConn, r: bufio.NewReader(baseConn)}
|
sConn, obfsConn := buildServerObfsConn(baseConn, cfg, selectedTable, true)
|
||||||
sConn, obfsConn := buildServerObfsConn(bConn, cfg, selectedTable, true)
|
|
||||||
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
|
seed := ServerAEADSeed(cfg.Key)
|
||||||
|
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
|
||||||
|
// Server side: recv is client->server, send is server->client.
|
||||||
|
rc, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskS2C, pskC2S)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("crypto setup failed: %w", err)
|
return nil, nil, fmt.Errorf("setup crypto failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var handshakeBuf [16]byte
|
msg, err := ReadKIPMessage(rc)
|
||||||
if _, err := io.ReadFull(cConn, handshakeBuf[:]); err != nil {
|
if err != nil {
|
||||||
cConn.Close()
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("handshake read failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
return nil, fmt.Errorf("read handshake failed: %w", err)
|
}
|
||||||
|
if msg.Type != KIPTypeClientHello {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("unexpected handshake message: %d", msg.Type), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
|
}
|
||||||
|
ch, err := DecodeKIPClientHelloPayload(msg.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("decode client hello failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
|
}
|
||||||
|
if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(kipHandshakeSkew.Seconds()) {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("time skew/replay"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
}
|
}
|
||||||
|
|
||||||
ts := int64(binary.BigEndian.Uint64(handshakeBuf[:8]))
|
userHashHex := hex.EncodeToString(ch.UserHash[:])
|
||||||
if absInt64(time.Now().Unix()-ts) > 60 {
|
if !globalHandshakeReplay.allow(userHashHex, ch.Nonce, time.Now()) {
|
||||||
cConn.Close()
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("replay"), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
return nil, fmt.Errorf("timestamp skew detected")
|
}
|
||||||
|
|
||||||
|
curve := ecdh.X25519()
|
||||||
|
serverEphemeral, err := curve.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("ecdh generate failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
|
}
|
||||||
|
shared, err := x25519SharedSecret(serverEphemeral, ch.ClientPub[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("ecdh failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
|
}
|
||||||
|
sessC2S, sessS2C, err := deriveSessionDirectionalBases(seed, shared, ch.Nonce)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("derive session keys failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverPub [kipHelloPubSize]byte
|
||||||
|
copy(serverPub[:], serverEphemeral.PublicKey().Bytes())
|
||||||
|
sh := &KIPServerHello{
|
||||||
|
Nonce: ch.Nonce,
|
||||||
|
ServerPub: serverPub,
|
||||||
|
SelectedFeats: ch.Features & KIPFeatAll,
|
||||||
|
}
|
||||||
|
if err := WriteKIPMessage(rc, KIPTypeServerHello, sh.EncodePayload()); err != nil {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("write server hello failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
|
}
|
||||||
|
if err := rc.Rekey(sessS2C, sessC2S); err != nil {
|
||||||
|
return nil, nil, &SuspiciousError{Err: fmt.Errorf("rekey failed: %w", err), Conn: &prefixedRecorderConn{Conn: sConn, prefix: httpHeaderData}}
|
||||||
}
|
}
|
||||||
|
|
||||||
userHash := userHashFromHandshake(handshakeBuf[:])
|
|
||||||
sConn.StopRecording()
|
sConn.StopRecording()
|
||||||
|
return rc, &HandshakeMeta{UserHash: userHashHex}, nil
|
||||||
|
}
|
||||||
|
|
||||||
modeBuf := []byte{0}
|
// ReadServerSession consumes the first post-handshake KIP control message and returns the session intent.
|
||||||
if _, err := io.ReadFull(cConn, modeBuf); err != nil {
|
func ReadServerSession(conn net.Conn, meta *HandshakeMeta) (*ServerSession, error) {
|
||||||
cConn.Close()
|
if conn == nil {
|
||||||
return nil, fmt.Errorf("read downlink mode failed: %w", err)
|
return nil, fmt.Errorf("nil conn")
|
||||||
}
|
}
|
||||||
if modeBuf[0] != downlinkMode(cfg) {
|
userHash := ""
|
||||||
cConn.Close()
|
if meta != nil {
|
||||||
return nil, fmt.Errorf("downlink mode mismatch: client=%d server=%d", modeBuf[0], downlinkMode(cfg))
|
userHash = meta.UserHash
|
||||||
}
|
}
|
||||||
|
|
||||||
firstByte := make([]byte, 1)
|
first, err := readFirstSessionMessage(conn)
|
||||||
if _, err := io.ReadFull(cConn, firstByte); err != nil {
|
if err != nil {
|
||||||
cConn.Close()
|
return nil, err
|
||||||
return nil, fmt.Errorf("read first byte failed: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if firstByte[0] == MultiplexMagicByte {
|
switch first.Type {
|
||||||
rawConn.SetReadDeadline(time.Time{})
|
case KIPTypeStartUoT:
|
||||||
return &ServerSession{Conn: cConn, Type: SessionTypeMultiplex, UserHash: userHash}, nil
|
return &ServerSession{Conn: conn, Type: SessionTypeUoT, UserHash: userHash}, nil
|
||||||
}
|
case KIPTypeStartMux:
|
||||||
|
return &ServerSession{Conn: conn, Type: SessionTypeMultiplex, UserHash: userHash}, nil
|
||||||
if firstByte[0] == UoTMagicByte {
|
case KIPTypeOpenTCP:
|
||||||
version := make([]byte, 1)
|
target, err := DecodeAddress(bytes.NewReader(first.Payload))
|
||||||
if _, err := io.ReadFull(cConn, version); err != nil {
|
if err != nil {
|
||||||
cConn.Close()
|
return nil, fmt.Errorf("decode target address failed: %w", err)
|
||||||
return nil, fmt.Errorf("read uot version failed: %w", err)
|
|
||||||
}
|
}
|
||||||
if version[0] != uotVersion {
|
return &ServerSession{Conn: conn, Type: SessionTypeTCP, Target: target, UserHash: userHash}, nil
|
||||||
cConn.Close()
|
default:
|
||||||
return nil, fmt.Errorf("unsupported uot version: %d", version[0])
|
return nil, fmt.Errorf("unknown kip message: %d", first.Type)
|
||||||
}
|
|
||||||
rawConn.SetReadDeadline(time.Time{})
|
|
||||||
return &ServerSession{Conn: cConn, Type: SessionTypeUoT, UserHash: userHash}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prefixed := &preBufferedConn{Conn: cConn, buf: firstByte}
|
|
||||||
target, err := DecodeAddress(prefixed)
|
|
||||||
if err != nil {
|
|
||||||
cConn.Close()
|
|
||||||
return nil, fmt.Errorf("read target address failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rawConn.SetReadDeadline(time.Time{})
|
|
||||||
log.Debugln("[Sudoku] incoming TCP session target: %s", target)
|
|
||||||
return &ServerSession{
|
|
||||||
Conn: prefixed,
|
|
||||||
Type: SessionTypeTCP,
|
|
||||||
Target: target,
|
|
||||||
UserHash: userHash,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenKeyPair() (privateKey, publicKey string, err error) {
|
|
||||||
// Generate Master Key
|
|
||||||
pair, err := crypto.GenerateMasterKey()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Split the master private key to get Available Private Key
|
|
||||||
availablePrivateKey, err := crypto.SplitPrivateKey(pair.Private)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
privateKey = availablePrivateKey // Available Private Key for client
|
|
||||||
publicKey = crypto.EncodePoint(pair.Public) // Master Public Key for server
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func userHashFromHandshake(handshakeBuf []byte) string {
|
|
||||||
if len(handshakeBuf) < 16 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(handshakeBuf[8:16])
|
|
||||||
}
|
}
|
||||||
|
|||||||
73
transport/sudoku/handshake_kip.go
Normal file
73
transport/sudoku/handshake_kip.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const kipHandshakeSkew = 60 * time.Second
|
||||||
|
|
||||||
|
func kipHandshakeClient(rc *crypto.RecordConn, seed string, userHash [kipHelloUserHashSize]byte, feats uint32) (uint32, error) {
|
||||||
|
if rc == nil {
|
||||||
|
return 0, fmt.Errorf("nil conn")
|
||||||
|
}
|
||||||
|
|
||||||
|
curve := ecdh.X25519()
|
||||||
|
ephemeral, err := curve.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ecdh generate failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonce [kipHelloNonceSize]byte
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||||
|
return 0, fmt.Errorf("nonce generate failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientPub [kipHelloPubSize]byte
|
||||||
|
copy(clientPub[:], ephemeral.PublicKey().Bytes())
|
||||||
|
|
||||||
|
ch := &KIPClientHello{
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
UserHash: userHash,
|
||||||
|
Nonce: nonce,
|
||||||
|
ClientPub: clientPub,
|
||||||
|
Features: feats,
|
||||||
|
}
|
||||||
|
if err := WriteKIPMessage(rc, KIPTypeClientHello, ch.EncodePayload()); err != nil {
|
||||||
|
return 0, fmt.Errorf("write client hello failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := ReadKIPMessage(rc)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("read server hello failed: %w", err)
|
||||||
|
}
|
||||||
|
if msg.Type != KIPTypeServerHello {
|
||||||
|
return 0, fmt.Errorf("unexpected handshake message: %d", msg.Type)
|
||||||
|
}
|
||||||
|
sh, err := DecodeKIPServerHelloPayload(msg.Payload)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("decode server hello failed: %w", err)
|
||||||
|
}
|
||||||
|
if sh.Nonce != nonce {
|
||||||
|
return 0, fmt.Errorf("handshake nonce mismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
shared, err := x25519SharedSecret(ephemeral, sh.ServerPub[:])
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("ecdh failed: %w", err)
|
||||||
|
}
|
||||||
|
sessC2S, sessS2C, err := deriveSessionDirectionalBases(seed, shared, nonce)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("derive session keys failed: %w", err)
|
||||||
|
}
|
||||||
|
if err := rc.Rekey(sessC2S, sessS2C); err != nil {
|
||||||
|
return 0, fmt.Errorf("rekey failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sh.SelectedFeats, nil
|
||||||
|
}
|
||||||
@@ -124,13 +124,18 @@ func runPackedTCPSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
|||||||
|
|
||||||
// Server side
|
// Server side
|
||||||
go func() {
|
go func() {
|
||||||
session, err := ServerHandshake(serverConn, cfg)
|
c, meta, err := ServerHandshake(serverConn, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- fmt.Errorf("server handshake tcp: %w", err)
|
errCh <- fmt.Errorf("server handshake tcp: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer session.Conn.Close()
|
defer c.Close()
|
||||||
|
|
||||||
|
session, err := ReadServerSession(c, meta)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- fmt.Errorf("server read session tcp: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if session.Type != SessionTypeTCP {
|
if session.Type != SessionTypeTCP {
|
||||||
errCh <- fmt.Errorf("unexpected session type: %v", session.Type)
|
errCh <- fmt.Errorf("unexpected session type: %v", session.Type)
|
||||||
return
|
return
|
||||||
@@ -159,8 +164,8 @@ func runPackedTCPSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
|||||||
errCh <- fmt.Errorf("encode address: %w", err)
|
errCh <- fmt.Errorf("encode address: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := cConn.Write(addrBuf); err != nil {
|
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
errCh <- fmt.Errorf("client send addr: %w", err)
|
errCh <- fmt.Errorf("client send open tcp: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,13 +187,18 @@ func runPackedUoTSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
|||||||
|
|
||||||
// Server side
|
// Server side
|
||||||
go func() {
|
go func() {
|
||||||
session, err := ServerHandshake(serverConn, cfg)
|
c, meta, err := ServerHandshake(serverConn, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- fmt.Errorf("server handshake uot: %w", err)
|
errCh <- fmt.Errorf("server handshake uot: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer session.Conn.Close()
|
defer c.Close()
|
||||||
|
|
||||||
|
session, err := ReadServerSession(c, meta)
|
||||||
|
if err != nil {
|
||||||
|
errCh <- fmt.Errorf("server read session uot: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
if session.Type != SessionTypeUoT {
|
if session.Type != SessionTypeUoT {
|
||||||
errCh <- fmt.Errorf("unexpected session type: %v", session.Type)
|
errCh <- fmt.Errorf("unexpected session type: %v", session.Type)
|
||||||
return
|
return
|
||||||
@@ -208,8 +218,8 @@ func runPackedUoTSession(id int, cfg *ProtocolConfig, errCh chan<- error) {
|
|||||||
}
|
}
|
||||||
defer cConn.Close()
|
defer cConn.Close()
|
||||||
|
|
||||||
if err := WritePreface(cConn); err != nil {
|
if err := WriteKIPMessage(cConn, KIPTypeStartUoT, nil); err != nil {
|
||||||
errCh <- fmt.Errorf("client write preface: %w", err)
|
errCh <- fmt.Errorf("client start uot: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ type HTTPMaskTunnelServer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
|
func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
|
||||||
|
return newHTTPMaskTunnelServer(cfg, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPMaskTunnelServerWithFallback(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
|
||||||
|
return newHTTPMaskTunnelServer(cfg, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHTTPMaskTunnelServer(cfg *ProtocolConfig, passThroughOnReject bool) *HTTPMaskTunnelServer {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return &HTTPMaskTunnelServer{}
|
return &HTTPMaskTunnelServer{}
|
||||||
}
|
}
|
||||||
@@ -22,11 +30,13 @@ func NewHTTPMaskTunnelServer(cfg *ProtocolConfig) *HTTPMaskTunnelServer {
|
|||||||
var ts *httpmask.TunnelServer
|
var ts *httpmask.TunnelServer
|
||||||
if !cfg.DisableHTTPMask {
|
if !cfg.DisableHTTPMask {
|
||||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||||
case "stream", "poll", "auto":
|
case "stream", "poll", "auto", "ws":
|
||||||
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
|
ts = httpmask.NewTunnelServer(httpmask.TunnelServerOptions{
|
||||||
Mode: cfg.HTTPMaskMode,
|
Mode: cfg.HTTPMaskMode,
|
||||||
PathRoot: cfg.HTTPMaskPathRoot,
|
PathRoot: cfg.HTTPMaskPathRoot,
|
||||||
AuthKey: ClientAEADSeed(cfg.Key),
|
AuthKey: ServerAEADSeed(cfg.Key),
|
||||||
|
// When upstream fallback is enabled, preserve rejected HTTP requests for the caller.
|
||||||
|
PassThroughOnReject: passThroughOnReject,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,6 +72,14 @@ func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Con
|
|||||||
case httpmask.HandleStartTunnel:
|
case httpmask.HandleStartTunnel:
|
||||||
inner := *s.cfg
|
inner := *s.cfg
|
||||||
inner.DisableHTTPMask = true
|
inner.DisableHTTPMask = true
|
||||||
|
// HTTPMask tunnel modes (stream/poll/auto/ws) add extra round trips before the first
|
||||||
|
// handshake bytes can reach ServerHandshake, especially under high concurrency.
|
||||||
|
// Bump the handshake timeout for tunneled conns to avoid flaky timeouts while keeping
|
||||||
|
// the default strict for raw TCP handshakes.
|
||||||
|
const minTunneledHandshakeTimeoutSeconds = 15
|
||||||
|
if inner.HandshakeTimeoutSeconds <= 0 || inner.HandshakeTimeoutSeconds < minTunneledHandshakeTimeoutSeconds {
|
||||||
|
inner.HandshakeTimeoutSeconds = minTunneledHandshakeTimeoutSeconds
|
||||||
|
}
|
||||||
return c, &inner, false, nil
|
return c, &inner, false, nil
|
||||||
default:
|
default:
|
||||||
return nil, nil, true, nil
|
return nil, nil, true, nil
|
||||||
@@ -70,7 +88,7 @@ func (s *HTTPMaskTunnelServer) WrapConn(rawConn net.Conn) (handshakeConn net.Con
|
|||||||
|
|
||||||
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
type TunnelDialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
|
|
||||||
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto) and returns a stream carrying raw Sudoku bytes.
|
// DialHTTPMaskTunnel dials a CDN-capable HTTP tunnel (stream/poll/auto/ws) and returns a stream carrying raw Sudoku bytes.
|
||||||
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
|
func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *ProtocolConfig, dial TunnelDialer, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("config is required")
|
return nil, fmt.Errorf("config is required")
|
||||||
@@ -79,7 +97,7 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol
|
|||||||
return nil, fmt.Errorf("http mask is disabled")
|
return nil, fmt.Errorf("http mask is disabled")
|
||||||
}
|
}
|
||||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
||||||
case "stream", "poll", "auto":
|
case "stream", "poll", "auto", "ws":
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
|
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
|
||||||
}
|
}
|
||||||
@@ -94,64 +112,3 @@ func DialHTTPMaskTunnel(ctx context.Context, serverAddress string, cfg *Protocol
|
|||||||
DialContext: dial,
|
DialContext: dial,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPMaskTunnelClient struct {
|
|
||||||
mode string
|
|
||||||
pathRoot string
|
|
||||||
authKey string
|
|
||||||
client *httpmask.TunnelClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPMaskTunnelClient(serverAddress string, cfg *ProtocolConfig, dial TunnelDialer) (*HTTPMaskTunnelClient, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("config is required")
|
|
||||||
}
|
|
||||||
if cfg.DisableHTTPMask {
|
|
||||||
return nil, fmt.Errorf("http mask is disabled")
|
|
||||||
}
|
|
||||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMode)) {
|
|
||||||
case "stream", "poll", "auto":
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("http-mask-mode=%q does not use http tunnel", cfg.HTTPMaskMode)
|
|
||||||
}
|
|
||||||
switch strings.ToLower(strings.TrimSpace(cfg.HTTPMaskMultiplex)) {
|
|
||||||
case "auto", "on":
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("http-mask-multiplex=%q does not enable reuse", cfg.HTTPMaskMultiplex)
|
|
||||||
}
|
|
||||||
|
|
||||||
c, err := httpmask.NewTunnelClient(serverAddress, httpmask.TunnelClientOptions{
|
|
||||||
TLSEnabled: cfg.HTTPMaskTLSEnabled,
|
|
||||||
HostOverride: cfg.HTTPMaskHost,
|
|
||||||
DialContext: dial,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &HTTPMaskTunnelClient{
|
|
||||||
mode: cfg.HTTPMaskMode,
|
|
||||||
pathRoot: cfg.HTTPMaskPathRoot,
|
|
||||||
authKey: ClientAEADSeed(cfg.Key),
|
|
||||||
client: c,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *HTTPMaskTunnelClient) Dial(ctx context.Context, upgrade func(net.Conn) (net.Conn, error)) (net.Conn, error) {
|
|
||||||
if c == nil || c.client == nil {
|
|
||||||
return nil, fmt.Errorf("nil httpmask tunnel client")
|
|
||||||
}
|
|
||||||
return c.client.DialTunnel(ctx, httpmask.TunnelDialOptions{
|
|
||||||
Mode: c.mode,
|
|
||||||
PathRoot: c.pathRoot,
|
|
||||||
AuthKey: c.authKey,
|
|
||||||
Upgrade: upgrade,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *HTTPMaskTunnelClient) CloseIdleConnections() {
|
|
||||||
if c == nil || c.client == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.client.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func startTunnelServer(t *testing.T, cfg *ProtocolConfig, handle func(*ServerSes
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
session, err := ServerHandshake(handshakeConn, handshakeCfg)
|
cConn, meta, err := ServerHandshake(handshakeConn, handshakeCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = handshakeConn.Close()
|
_ = handshakeConn.Close()
|
||||||
if handshakeConn != conn {
|
if handshakeConn != conn {
|
||||||
@@ -70,8 +70,13 @@ func startTunnelServer(t *testing.T, cfg *ProtocolConfig, handle func(*ServerSes
|
|||||||
errC <- err
|
errC <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer session.Conn.Close()
|
defer cConn.Close()
|
||||||
|
|
||||||
|
session, err := ReadServerSession(cConn, meta)
|
||||||
|
if err != nil {
|
||||||
|
errC <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
if handleErr := handle(session); handleErr != nil {
|
if handleErr := handle(session); handleErr != nil {
|
||||||
errC <- handleErr
|
errC <- handleErr
|
||||||
}
|
}
|
||||||
@@ -172,7 +177,7 @@ func TestHTTPMaskTunnel_Stream_TCPRoundTrip(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("encode addr: %v", err)
|
t.Fatalf("encode addr: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := cConn.Write(addrBuf); err != nil {
|
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
t.Fatalf("write addr: %v", err)
|
t.Fatalf("write addr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,8 +244,8 @@ func TestHTTPMaskTunnel_Poll_UoTRoundTrip(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer cConn.Close()
|
defer cConn.Close()
|
||||||
|
|
||||||
if err := WritePreface(cConn); err != nil {
|
if err := WriteKIPMessage(cConn, KIPTypeStartUoT, nil); err != nil {
|
||||||
t.Fatalf("write preface: %v", err)
|
t.Fatalf("start uot: %v", err)
|
||||||
}
|
}
|
||||||
if err := WriteDatagram(cConn, target, payload); err != nil {
|
if err := WriteDatagram(cConn, target, payload); err != nil {
|
||||||
t.Fatalf("write datagram: %v", err)
|
t.Fatalf("write datagram: %v", err)
|
||||||
@@ -305,7 +310,68 @@ func TestHTTPMaskTunnel_Auto_TCPRoundTrip(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("encode addr: %v", err)
|
t.Fatalf("encode addr: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := cConn.Write(addrBuf); err != nil {
|
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
|
t.Fatalf("write addr: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := make([]byte, 2)
|
||||||
|
if _, err := io.ReadFull(cConn, buf); err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
if string(buf) != "ok" {
|
||||||
|
t.Fatalf("unexpected payload: %q", buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
stop()
|
||||||
|
for err := range errCh {
|
||||||
|
t.Fatalf("server error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHTTPMaskTunnel_WS_TCPRoundTrip(t *testing.T) {
|
||||||
|
key := "tunnel-ws-key"
|
||||||
|
target := "1.1.1.1:80"
|
||||||
|
|
||||||
|
serverCfg := newTunnelTestTable(t, key)
|
||||||
|
serverCfg.HTTPMaskMode = "ws"
|
||||||
|
|
||||||
|
addr, stop, errCh := startTunnelServer(t, serverCfg, func(s *ServerSession) error {
|
||||||
|
if s.Type != SessionTypeTCP {
|
||||||
|
return fmt.Errorf("unexpected session type: %v", s.Type)
|
||||||
|
}
|
||||||
|
if s.Target != target {
|
||||||
|
return fmt.Errorf("target mismatch: %s", s.Target)
|
||||||
|
}
|
||||||
|
_, _ = s.Conn.Write([]byte("ok"))
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
clientCfg := *serverCfg
|
||||||
|
clientCfg.ServerAddress = addr
|
||||||
|
|
||||||
|
tunnelConn, err := DialHTTPMaskTunnel(ctx, clientCfg.ServerAddress, &clientCfg, (&net.Dialer{}).DialContext, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dial tunnel: %v", err)
|
||||||
|
}
|
||||||
|
defer tunnelConn.Close()
|
||||||
|
|
||||||
|
handshakeCfg := clientCfg
|
||||||
|
handshakeCfg.DisableHTTPMask = true
|
||||||
|
cConn, err := ClientHandshake(tunnelConn, &handshakeCfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client handshake: %v", err)
|
||||||
|
}
|
||||||
|
defer cConn.Close()
|
||||||
|
|
||||||
|
addrBuf, err := EncodeAddress(target)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encode addr: %v", err)
|
||||||
|
}
|
||||||
|
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
t.Fatalf("write addr: %v", err)
|
t.Fatalf("write addr: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +472,7 @@ func TestHTTPMaskTunnel_Soak_Concurrent(t *testing.T) {
|
|||||||
runErr <- fmt.Errorf("encode addr: %w", err)
|
runErr <- fmt.Errorf("encode addr: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, err := cConn.Write(addrBuf); err != nil {
|
if err := WriteKIPMessage(cConn, KIPTypeOpenTCP, addrBuf); err != nil {
|
||||||
runErr <- fmt.Errorf("write addr: %w", err)
|
runErr <- fmt.Errorf("write addr: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
97
transport/sudoku/init.go
Normal file
97
transport/sudoku/init.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/metacubex/edwards25519"
|
||||||
|
"github.com/metacubex/mihomo/transport/sudoku/crypto"
|
||||||
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewTable(key string, tableType string) *sudoku.Table {
|
||||||
|
table, err := NewTableWithCustom(key, tableType, "")
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("[Sudoku] failed to init tables: %v", err))
|
||||||
|
}
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTableWithCustom(key string, tableType string, customTable string) (*sudoku.Table, error) {
|
||||||
|
table, err := sudoku.NewTableWithCustom(key, tableType, customTable)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientAEADSeed returns a canonical "seed" that is stable between client private key material and server public key.
|
||||||
|
func ClientAEADSeed(key string) string {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := hex.DecodeString(key)
|
||||||
|
if err != nil {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-side key material can be:
|
||||||
|
// - split private key: 64 bytes hex (r||k)
|
||||||
|
// - master private scalar: 32 bytes hex (x)
|
||||||
|
// - PSK string: non-hex
|
||||||
|
//
|
||||||
|
// We intentionally do NOT treat a 32-byte hex as a public key here; the client is expected
|
||||||
|
// to carry private material. Server-side should use ServerAEADSeed for public keys.
|
||||||
|
switch len(b) {
|
||||||
|
case 64:
|
||||||
|
case 32:
|
||||||
|
default:
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
if recovered, err := crypto.RecoverPublicKey(key); err == nil {
|
||||||
|
return crypto.EncodePoint(recovered)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerAEADSeed returns a canonical seed for server-side configuration.
|
||||||
|
//
|
||||||
|
// When key is a public key (32-byte compressed point, hex), it returns the canonical point encoding.
|
||||||
|
// When key is private key material (split/master scalar), it derives and returns the public key.
|
||||||
|
func ServerAEADSeed(key string) string {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := hex.DecodeString(key)
|
||||||
|
if err != nil {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer interpreting 32-byte hex as a public key point, to avoid accidental scalar parsing.
|
||||||
|
if len(b) == 32 {
|
||||||
|
if p, err := new(edwards25519.Point).SetBytes(b); err == nil {
|
||||||
|
return hex.EncodeToString(p.Bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to client-side rules for private key materials / other formats.
|
||||||
|
return ClientAEADSeed(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenKeyPair generates a client "available private key" and the corresponding server public key.
|
||||||
|
func GenKeyPair() (privateKey, publicKey string, err error) {
|
||||||
|
pair, err := crypto.GenerateMasterKey()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
availablePrivateKey, err := crypto.SplitPrivateKey(pair.Private)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return availablePrivateKey, crypto.EncodePoint(pair.Public), nil
|
||||||
|
}
|
||||||
44
transport/sudoku/init_test.go
Normal file
44
transport/sudoku/init_test.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/metacubex/edwards25519"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientAEADSeed_IsStableForPrivAndPub(t *testing.T) {
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
priv, pub, err := GenKeyPair()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, pub, ClientAEADSeed(priv))
|
||||||
|
require.Equal(t, pub, ServerAEADSeed(pub))
|
||||||
|
require.Equal(t, pub, ServerAEADSeed(priv))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientAEADSeed_Supports32ByteMasterScalar(t *testing.T) {
|
||||||
|
var seed [64]byte
|
||||||
|
_, err := rand.Read(seed[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
s, err := edwards25519.NewScalar().SetUniformBytes(seed[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keyHex := hex.EncodeToString(s.Bytes())
|
||||||
|
require.Len(t, keyHex, 64)
|
||||||
|
require.NotEqual(t, keyHex, ClientAEADSeed(keyHex))
|
||||||
|
require.Equal(t, ClientAEADSeed(keyHex), ServerAEADSeed(ClientAEADSeed(keyHex)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServerAEADSeed_LeavesPublicKeyAsIs(t *testing.T) {
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
priv, pub, err := GenKeyPair()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, pub, ServerAEADSeed(pub))
|
||||||
|
require.Equal(t, pub, ServerAEADSeed(priv))
|
||||||
|
}
|
||||||
|
}
|
||||||
206
transport/sudoku/kip.go
Normal file
206
transport/sudoku/kip.go
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
kipMagic = "kip"
|
||||||
|
|
||||||
|
KIPTypeClientHello byte = 0x01
|
||||||
|
KIPTypeServerHello byte = 0x02
|
||||||
|
|
||||||
|
KIPTypeOpenTCP byte = 0x10
|
||||||
|
KIPTypeStartMux byte = 0x11
|
||||||
|
KIPTypeStartUoT byte = 0x12
|
||||||
|
KIPTypeKeepAlive byte = 0x14
|
||||||
|
)
|
||||||
|
|
||||||
|
// KIP feature bits are advisory capability flags negotiated during the handshake.
|
||||||
|
// They represent control-plane message families.
|
||||||
|
const (
|
||||||
|
KIPFeatOpenTCP uint32 = 1 << 0
|
||||||
|
KIPFeatMux uint32 = 1 << 1
|
||||||
|
KIPFeatUoT uint32 = 1 << 2
|
||||||
|
KIPFeatKeepAlive uint32 = 1 << 4
|
||||||
|
|
||||||
|
KIPFeatAll = KIPFeatOpenTCP | KIPFeatMux | KIPFeatUoT | KIPFeatKeepAlive
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
kipHelloUserHashSize = 8
|
||||||
|
kipHelloNonceSize = 16
|
||||||
|
kipHelloPubSize = 32
|
||||||
|
kipMaxPayload = 64 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
var errKIP = errors.New("kip protocol error")
|
||||||
|
|
||||||
|
type KIPMessage struct {
|
||||||
|
Type byte
|
||||||
|
Payload []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteKIPMessage(w io.Writer, typ byte, payload []byte) error {
|
||||||
|
if w == nil {
|
||||||
|
return fmt.Errorf("%w: nil writer", errKIP)
|
||||||
|
}
|
||||||
|
if len(payload) > kipMaxPayload {
|
||||||
|
return fmt.Errorf("%w: payload too large: %d", errKIP, len(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
var hdr [3 + 1 + 2]byte
|
||||||
|
copy(hdr[:3], []byte(kipMagic))
|
||||||
|
hdr[3] = typ
|
||||||
|
binary.BigEndian.PutUint16(hdr[4:], uint16(len(payload)))
|
||||||
|
|
||||||
|
if err := writeFull(w, hdr[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return writeFull(w, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadKIPMessage(r io.Reader) (*KIPMessage, error) {
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("%w: nil reader", errKIP)
|
||||||
|
}
|
||||||
|
var hdr [3 + 1 + 2]byte
|
||||||
|
if _, err := io.ReadFull(r, hdr[:]); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if string(hdr[:3]) != kipMagic {
|
||||||
|
return nil, fmt.Errorf("%w: bad magic", errKIP)
|
||||||
|
}
|
||||||
|
typ := hdr[3]
|
||||||
|
n := int(binary.BigEndian.Uint16(hdr[4:]))
|
||||||
|
if n < 0 || n > kipMaxPayload {
|
||||||
|
return nil, fmt.Errorf("%w: invalid payload length: %d", errKIP, n)
|
||||||
|
}
|
||||||
|
var payload []byte
|
||||||
|
if n > 0 {
|
||||||
|
payload = make([]byte, n)
|
||||||
|
if _, err := io.ReadFull(r, payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &KIPMessage{Type: typ, Payload: payload}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type KIPClientHello struct {
|
||||||
|
Timestamp time.Time
|
||||||
|
UserHash [kipHelloUserHashSize]byte
|
||||||
|
Nonce [kipHelloNonceSize]byte
|
||||||
|
ClientPub [kipHelloPubSize]byte
|
||||||
|
Features uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type KIPServerHello struct {
|
||||||
|
Nonce [kipHelloNonceSize]byte
|
||||||
|
ServerPub [kipHelloPubSize]byte
|
||||||
|
SelectedFeats uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func kipUserHashFromKey(psk string) [kipHelloUserHashSize]byte {
|
||||||
|
var out [kipHelloUserHashSize]byte
|
||||||
|
psk = strings.TrimSpace(psk)
|
||||||
|
if psk == "" {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Align with upstream: when the client carries private key material (or even just a public key),
|
||||||
|
// prefer hashing the raw hex bytes so different split/master keys can be distinguished.
|
||||||
|
if keyBytes, err := hex.DecodeString(psk); err == nil && len(keyBytes) > 0 {
|
||||||
|
sum := sha256.Sum256(keyBytes)
|
||||||
|
copy(out[:], sum[:kipHelloUserHashSize])
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := sha256.Sum256([]byte(psk))
|
||||||
|
copy(out[:], sum[:kipHelloUserHashSize])
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func KIPUserHashHexFromKey(psk string) string {
|
||||||
|
uh := kipUserHashFromKey(psk)
|
||||||
|
return hex.EncodeToString(uh[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KIPClientHello) EncodePayload() []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
var tmp [8]byte
|
||||||
|
binary.BigEndian.PutUint64(tmp[:], uint64(m.Timestamp.Unix()))
|
||||||
|
b.Write(tmp[:])
|
||||||
|
b.Write(m.UserHash[:])
|
||||||
|
b.Write(m.Nonce[:])
|
||||||
|
b.Write(m.ClientPub[:])
|
||||||
|
var f [4]byte
|
||||||
|
binary.BigEndian.PutUint32(f[:], m.Features)
|
||||||
|
b.Write(f[:])
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeKIPClientHelloPayload(payload []byte) (*KIPClientHello, error) {
|
||||||
|
const minLen = 8 + kipHelloUserHashSize + kipHelloNonceSize + kipHelloPubSize + 4
|
||||||
|
if len(payload) < minLen {
|
||||||
|
return nil, fmt.Errorf("%w: client hello too short", errKIP)
|
||||||
|
}
|
||||||
|
var h KIPClientHello
|
||||||
|
ts := int64(binary.BigEndian.Uint64(payload[:8]))
|
||||||
|
h.Timestamp = time.Unix(ts, 0)
|
||||||
|
off := 8
|
||||||
|
copy(h.UserHash[:], payload[off:off+kipHelloUserHashSize])
|
||||||
|
off += kipHelloUserHashSize
|
||||||
|
copy(h.Nonce[:], payload[off:off+kipHelloNonceSize])
|
||||||
|
off += kipHelloNonceSize
|
||||||
|
copy(h.ClientPub[:], payload[off:off+kipHelloPubSize])
|
||||||
|
off += kipHelloPubSize
|
||||||
|
h.Features = binary.BigEndian.Uint32(payload[off : off+4])
|
||||||
|
return &h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *KIPServerHello) EncodePayload() []byte {
|
||||||
|
var b bytes.Buffer
|
||||||
|
b.Write(m.Nonce[:])
|
||||||
|
b.Write(m.ServerPub[:])
|
||||||
|
var f [4]byte
|
||||||
|
binary.BigEndian.PutUint32(f[:], m.SelectedFeats)
|
||||||
|
b.Write(f[:])
|
||||||
|
return b.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeKIPServerHelloPayload(payload []byte) (*KIPServerHello, error) {
|
||||||
|
const want = kipHelloNonceSize + kipHelloPubSize + 4
|
||||||
|
if len(payload) != want {
|
||||||
|
return nil, fmt.Errorf("%w: server hello bad len: %d", errKIP, len(payload))
|
||||||
|
}
|
||||||
|
var h KIPServerHello
|
||||||
|
off := 0
|
||||||
|
copy(h.Nonce[:], payload[off:off+kipHelloNonceSize])
|
||||||
|
off += kipHelloNonceSize
|
||||||
|
copy(h.ServerPub[:], payload[off:off+kipHelloPubSize])
|
||||||
|
off += kipHelloPubSize
|
||||||
|
h.SelectedFeats = binary.BigEndian.Uint32(payload[off : off+4])
|
||||||
|
return &h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeFull(w io.Writer, b []byte) error {
|
||||||
|
for len(b) > 0 {
|
||||||
|
n, err := w.Write(b)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
b = b[n:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -10,19 +10,14 @@ import (
|
|||||||
"github.com/metacubex/mihomo/transport/sudoku/multiplex"
|
"github.com/metacubex/mihomo/transport/sudoku/multiplex"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// StartMultiplexClient upgrades an already-handshaked Sudoku tunnel into a multiplex session.
|
||||||
MultiplexMagicByte byte = multiplex.MagicByte
|
|
||||||
MultiplexVersion byte = multiplex.Version
|
|
||||||
)
|
|
||||||
|
|
||||||
// StartMultiplexClient writes the multiplex preface and upgrades an already-handshaked Sudoku tunnel into a multiplex session.
|
|
||||||
func StartMultiplexClient(conn net.Conn) (*MultiplexClient, error) {
|
func StartMultiplexClient(conn net.Conn) (*MultiplexClient, error) {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return nil, fmt.Errorf("nil conn")
|
return nil, fmt.Errorf("nil conn")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := multiplex.WritePreface(conn); err != nil {
|
if err := WriteKIPMessage(conn, KIPTypeStartMux, nil); err != nil {
|
||||||
return nil, fmt.Errorf("write multiplex preface failed: %w", err)
|
return nil, fmt.Errorf("write mux start failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sess, err := multiplex.NewClientSession(conn)
|
sess, err := multiplex.NewClientSession(conn)
|
||||||
@@ -77,20 +72,10 @@ func (c *MultiplexClient) IsClosed() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AcceptMultiplexServer upgrades a server-side, already-handshaked Sudoku connection into a multiplex session.
|
// AcceptMultiplexServer upgrades a server-side, already-handshaked Sudoku connection into a multiplex session.
|
||||||
//
|
|
||||||
// The caller must have already consumed the multiplex magic byte (MultiplexMagicByte). This function consumes the
|
|
||||||
// multiplex version byte and starts the session.
|
|
||||||
func AcceptMultiplexServer(conn net.Conn) (*MultiplexServer, error) {
|
func AcceptMultiplexServer(conn net.Conn) (*MultiplexServer, error) {
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
return nil, fmt.Errorf("nil conn")
|
return nil, fmt.Errorf("nil conn")
|
||||||
}
|
}
|
||||||
v, err := multiplex.ReadVersion(conn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := multiplex.ValidateVersion(v); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sess, err := multiplex.NewServerSession(conn)
|
sess, err := multiplex.NewServerSession(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
package multiplex
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// MagicByte marks a Sudoku tunnel connection that will switch into multiplex mode.
|
|
||||||
// It is sent after the Sudoku handshake + downlink mode byte.
|
|
||||||
//
|
|
||||||
// Keep it distinct from UoTMagicByte and address type bytes.
|
|
||||||
MagicByte byte = 0xED
|
|
||||||
Version byte = 0x01
|
|
||||||
)
|
|
||||||
|
|
||||||
func WritePreface(w io.Writer) error {
|
|
||||||
if w == nil {
|
|
||||||
return fmt.Errorf("nil writer")
|
|
||||||
}
|
|
||||||
_, err := w.Write([]byte{MagicByte, Version})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadVersion(r io.Reader) (byte, error) {
|
|
||||||
var b [1]byte
|
|
||||||
if _, err := io.ReadFull(r, b[:]); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return b[0], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateVersion(v byte) error {
|
|
||||||
if v != Version {
|
|
||||||
return fmt.Errorf("unsupported multiplex version: %d", v)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -18,7 +18,6 @@ func TestUserHash_StableAcrossTableRotation(t *testing.T) {
|
|||||||
sudokuobfs.NewTable("seed-b", "prefer_ascii"),
|
sudokuobfs.NewTable("seed-b", "prefer_ascii"),
|
||||||
}
|
}
|
||||||
key := "userhash-stability-key"
|
key := "userhash-stability-key"
|
||||||
target := "example.com:80"
|
|
||||||
|
|
||||||
serverCfg := DefaultConfig()
|
serverCfg := DefaultConfig()
|
||||||
serverCfg.Key = key
|
serverCfg.Key = key
|
||||||
@@ -48,13 +47,16 @@ func TestUserHash_StableAcrossTableRotation(t *testing.T) {
|
|||||||
}
|
}
|
||||||
go func(conn net.Conn) {
|
go func(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
session, err := ServerHandshake(conn, serverCfg)
|
_, meta, err := ServerHandshake(conn, serverCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errCh <- err
|
errCh <- err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer session.Conn.Close()
|
if meta == nil || meta.UserHash == "" {
|
||||||
hashCh <- session.UserHash
|
errCh <- io.ErrUnexpectedEOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hashCh <- meta.UserHash
|
||||||
}(c)
|
}(c)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -77,15 +79,6 @@ func TestUserHash_StableAcrossTableRotation(t *testing.T) {
|
|||||||
t.Fatalf("handshake %d: %v", i, err)
|
t.Fatalf("handshake %d: %v", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
addrBuf, err := EncodeAddress(target)
|
|
||||||
if err != nil {
|
|
||||||
_ = cConn.Close()
|
|
||||||
t.Fatalf("encode addr %d: %v", i, err)
|
|
||||||
}
|
|
||||||
if _, err := cConn.Write(addrBuf); err != nil {
|
|
||||||
_ = cConn.Close()
|
|
||||||
t.Fatalf("write addr %d: %v", i, err)
|
|
||||||
}
|
|
||||||
_ = cConn.Close()
|
_ = cConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,18 +138,22 @@ func TestMultiplex_TCP_Echo(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer raw.Close()
|
defer raw.Close()
|
||||||
|
|
||||||
session, err := ServerHandshake(raw, serverCfg)
|
c, meta, err := ServerHandshake(raw, serverCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
atomic.AddInt64(&handshakes, 1)
|
atomic.AddInt64(&handshakes, 1)
|
||||||
|
|
||||||
|
session, err := ReadServerSession(c, meta)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if session.Type != SessionTypeMultiplex {
|
if session.Type != SessionTypeMultiplex {
|
||||||
_ = session.Conn.Close()
|
_ = c.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mux, err := AcceptMultiplexServer(session.Conn)
|
mux, err := AcceptMultiplexServer(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -240,21 +237,3 @@ func TestMultiplex_TCP_Echo(t *testing.T) {
|
|||||||
t.Fatalf("unexpected stream count: %d", got)
|
t.Fatalf("unexpected stream count: %d", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMultiplex_Boundary_InvalidVersion(t *testing.T) {
|
|
||||||
client, server := net.Pipe()
|
|
||||||
t.Cleanup(func() { _ = client.Close() })
|
|
||||||
t.Cleanup(func() { _ = server.Close() })
|
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
_, err := AcceptMultiplexServer(server)
|
|
||||||
errCh <- err
|
|
||||||
}()
|
|
||||||
|
|
||||||
// AcceptMultiplexServer expects the magic byte to have been consumed already; write a bad version byte.
|
|
||||||
_, _ = client.Write([]byte{0xFF})
|
|
||||||
if err := <-errCh; err == nil {
|
|
||||||
t.Fatalf("expected error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import (
|
|||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"github.com/metacubex/http"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/metacubex/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/metacubex/mihomo/component/ca"
|
"github.com/metacubex/mihomo/component/ca"
|
||||||
@@ -32,6 +33,7 @@ const (
|
|||||||
TunnelModeStream TunnelMode = "stream"
|
TunnelModeStream TunnelMode = "stream"
|
||||||
TunnelModePoll TunnelMode = "poll"
|
TunnelModePoll TunnelMode = "poll"
|
||||||
TunnelModeAuto TunnelMode = "auto"
|
TunnelModeAuto TunnelMode = "auto"
|
||||||
|
TunnelModeWS TunnelMode = "ws"
|
||||||
)
|
)
|
||||||
|
|
||||||
func normalizeTunnelMode(mode string) TunnelMode {
|
func normalizeTunnelMode(mode string) TunnelMode {
|
||||||
@@ -44,6 +46,8 @@ func normalizeTunnelMode(mode string) TunnelMode {
|
|||||||
return TunnelModePoll
|
return TunnelModePoll
|
||||||
case string(TunnelModeAuto):
|
case string(TunnelModeAuto):
|
||||||
return TunnelModeAuto
|
return TunnelModeAuto
|
||||||
|
case string(TunnelModeWS):
|
||||||
|
return TunnelModeWS
|
||||||
default:
|
default:
|
||||||
// Be conservative: unknown => legacy
|
// Be conservative: unknown => legacy
|
||||||
return TunnelModeLegacy
|
return TunnelModeLegacy
|
||||||
@@ -88,7 +92,6 @@ type TunnelClientOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TunnelClient struct {
|
type TunnelClient struct {
|
||||||
client *http.Client
|
|
||||||
transport *http.Transport
|
transport *http.Transport
|
||||||
target httpClientTarget
|
target httpClientTarget
|
||||||
}
|
}
|
||||||
@@ -105,7 +108,6 @@ func NewTunnelClient(serverAddress string, opts TunnelClientOptions) (*TunnelCli
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &TunnelClient{
|
return &TunnelClient{
|
||||||
client: &http.Client{Transport: transport},
|
|
||||||
transport: transport,
|
transport: transport,
|
||||||
target: target,
|
target: target,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -119,7 +121,7 @@ func (c *TunnelClient) CloseIdleConnections() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TunnelClient) DialTunnel(ctx context.Context, opts TunnelDialOptions) (net.Conn, error) {
|
func (c *TunnelClient) DialTunnel(ctx context.Context, opts TunnelDialOptions) (net.Conn, error) {
|
||||||
if c == nil || c.client == nil {
|
if c == nil || c.transport == nil {
|
||||||
return nil, fmt.Errorf("nil tunnel client")
|
return nil, fmt.Errorf("nil tunnel client")
|
||||||
}
|
}
|
||||||
tm := normalizeTunnelMode(opts.Mode)
|
tm := normalizeTunnelMode(opts.Mode)
|
||||||
@@ -127,25 +129,31 @@ func (c *TunnelClient) DialTunnel(ctx context.Context, opts TunnelDialOptions) (
|
|||||||
return nil, fmt.Errorf("legacy mode does not use http tunnel")
|
return nil, fmt.Errorf("legacy mode does not use http tunnel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a per-dial client while sharing the underlying Transport for connection reuse.
|
||||||
|
// This matches upstream behavior and avoids potential client-level concurrency pitfalls.
|
||||||
|
client := &http.Client{Transport: c.transport}
|
||||||
|
|
||||||
switch tm {
|
switch tm {
|
||||||
case TunnelModeStream:
|
case TunnelModeStream:
|
||||||
return dialStreamWithClient(ctx, c.client, c.target, opts)
|
return dialStreamWithClient(ctx, client, c.target, opts)
|
||||||
case TunnelModePoll:
|
case TunnelModePoll:
|
||||||
return dialPollWithClient(ctx, c.client, c.target, opts)
|
return dialPollWithClient(ctx, client, c.target, opts)
|
||||||
|
case TunnelModeWS:
|
||||||
|
return nil, fmt.Errorf("ws mode does not support TunnelClient reuse")
|
||||||
case TunnelModeAuto:
|
case TunnelModeAuto:
|
||||||
streamCtx, cancelX := context.WithTimeout(ctx, 3*time.Second)
|
streamCtx, cancelX := context.WithTimeout(ctx, 3*time.Second)
|
||||||
c1, errX := dialStreamWithClient(streamCtx, c.client, c.target, opts)
|
c1, errX := dialStreamWithClient(streamCtx, client, c.target, opts)
|
||||||
cancelX()
|
cancelX()
|
||||||
if errX == nil {
|
if errX == nil {
|
||||||
return c1, nil
|
return c1, nil
|
||||||
}
|
}
|
||||||
c2, errP := dialPollWithClient(ctx, c.client, c.target, opts)
|
c2, errP := dialPollWithClient(ctx, client, c.target, opts)
|
||||||
if errP == nil {
|
if errP == nil {
|
||||||
return c2, nil
|
return c2, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("auto tunnel failed: stream: %v; poll: %w", errX, errP)
|
return nil, fmt.Errorf("auto tunnel failed: stream: %v; poll: %w", errX, errP)
|
||||||
default:
|
default:
|
||||||
return dialStreamWithClient(ctx, c.client, c.target, opts)
|
return dialStreamWithClient(ctx, client, c.target, opts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +174,8 @@ func DialTunnel(ctx context.Context, serverAddress string, opts TunnelDialOption
|
|||||||
return dialStreamFn(ctx, serverAddress, opts)
|
return dialStreamFn(ctx, serverAddress, opts)
|
||||||
case TunnelModePoll:
|
case TunnelModePoll:
|
||||||
return dialPollFn(ctx, serverAddress, opts)
|
return dialPollFn(ctx, serverAddress, opts)
|
||||||
|
case TunnelModeWS:
|
||||||
|
return dialWS(ctx, serverAddress, opts)
|
||||||
case TunnelModeAuto:
|
case TunnelModeAuto:
|
||||||
// "stream" can hang on some CDNs that buffer uploads until request body completes.
|
// "stream" can hang on some CDNs that buffer uploads until request body completes.
|
||||||
// Keep it on a short leash so we can fall back to poll within the caller's deadline.
|
// Keep it on a short leash so we can fall back to poll within the caller's deadline.
|
||||||
@@ -306,6 +316,36 @@ type sessionDialInfo struct {
|
|||||||
auth *tunnelAuth
|
auth *tunnelAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type httpStatusError struct {
|
||||||
|
code int
|
||||||
|
status string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *httpStatusError) Error() string {
|
||||||
|
if e == nil {
|
||||||
|
return "bad status"
|
||||||
|
}
|
||||||
|
if e.status != "" {
|
||||||
|
return "bad status: " + e.status
|
||||||
|
}
|
||||||
|
return "bad status"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryableStatusCode(code int) bool {
|
||||||
|
return code == http.StatusRequestTimeout || code == http.StatusTooManyRequests || code >= 500
|
||||||
|
}
|
||||||
|
|
||||||
|
type idleConnCloser interface{ CloseIdleConnections() }
|
||||||
|
|
||||||
|
func closeIdleConnections(client *http.Client) {
|
||||||
|
if client == nil || client.Transport == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c, ok := client.Transport.(idleConnCloser); ok {
|
||||||
|
c.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func dialSessionWithClient(ctx context.Context, client *http.Client, target httpClientTarget, mode TunnelMode, opts TunnelDialOptions) (*sessionDialInfo, error) {
|
func dialSessionWithClient(ctx context.Context, client *http.Client, target httpClientTarget, mode TunnelMode, opts TunnelDialOptions) (*sessionDialInfo, error) {
|
||||||
if client == nil {
|
if client == nil {
|
||||||
return nil, fmt.Errorf("nil http client")
|
return nil, fmt.Errorf("nil http client")
|
||||||
@@ -313,25 +353,61 @@ func dialSessionWithClient(ctx context.Context, client *http.Client, target http
|
|||||||
|
|
||||||
auth := newTunnelAuth(opts.AuthKey, 0)
|
auth := newTunnelAuth(opts.AuthKey, 0)
|
||||||
authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/session")}).String()
|
authorizeURL := (&url.URL{Scheme: target.scheme, Host: target.urlHost, Path: joinPathRoot(opts.PathRoot, "/session")}).String()
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Host = target.headerHost
|
|
||||||
applyTunnelHeaders(req.Header, target.headerHost, mode)
|
|
||||||
applyTunnelAuth(req, auth, mode, http.MethodGet, "/session")
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
var bodyBytes []byte
|
||||||
if err != nil {
|
for attempt := 0; ; attempt++ {
|
||||||
return nil, err
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, authorizeURL, nil)
|
||||||
}
|
if err != nil {
|
||||||
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, 4*1024))
|
return nil, err
|
||||||
_ = resp.Body.Close()
|
}
|
||||||
if err != nil {
|
req.Host = target.headerHost
|
||||||
return nil, err
|
applyTunnelHeaders(req.Header, target.headerHost, mode)
|
||||||
}
|
applyTunnelAuth(req, auth, mode, http.MethodGet, "/session")
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("%s authorize bad status: %s (%s)", mode, resp.Status, strings.TrimSpace(string(bodyBytes)))
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// Transient failure on reused keep-alive conns (multiplex=auto). Retry a few times.
|
||||||
|
if attempt < 2 && (isDialError(err) || isRetryableRequestError(err)) {
|
||||||
|
closeIdleConnections(client)
|
||||||
|
select {
|
||||||
|
case <-time.After(25 * time.Millisecond):
|
||||||
|
continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyBytes, err = io.ReadAll(io.LimitReader(resp.Body, 4*1024))
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
if attempt < 2 && isRetryableRequestError(err) {
|
||||||
|
closeIdleConnections(client)
|
||||||
|
select {
|
||||||
|
case <-time.After(25 * time.Millisecond):
|
||||||
|
continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// Retry some transient proxy/CDN errors.
|
||||||
|
if attempt < 2 && resp.StatusCode >= 500 {
|
||||||
|
closeIdleConnections(client)
|
||||||
|
select {
|
||||||
|
case <-time.After(25 * time.Millisecond):
|
||||||
|
continue
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, fmt.Errorf("%s authorize bad status: %s (%s)", mode, resp.Status, strings.TrimSpace(string(bodyBytes)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s authorize bad status: %s (%s)", mode, resp.Status, strings.TrimSpace(string(bodyBytes)))
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := parseTunnelToken(bodyBytes)
|
token, err := parseTunnelToken(bodyBytes)
|
||||||
@@ -544,9 +620,8 @@ type streamSplitConn struct {
|
|||||||
auth *tunnelAuth
|
auth *tunnelAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *streamSplitConn) Close() error {
|
func (c *streamSplitConn) closeWithError(err error) error {
|
||||||
_ = c.closeWithError(io.ErrClosedPipe)
|
_ = c.queuedConn.closeWithError(err)
|
||||||
|
|
||||||
if c.cancel != nil {
|
if c.cancel != nil {
|
||||||
c.cancel()
|
c.cancel()
|
||||||
}
|
}
|
||||||
@@ -554,6 +629,8 @@ func (c *streamSplitConn) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *streamSplitConn) Close() error { return c.closeWithError(io.ErrClosedPipe) }
|
||||||
|
|
||||||
func newStreamSplitConnFromInfo(info *sessionDialInfo) *streamSplitConn {
|
func newStreamSplitConnFromInfo(info *sessionDialInfo) *streamSplitConn {
|
||||||
if info == nil {
|
if info == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -659,7 +736,7 @@ func (c *streamSplitConn) pullLoop() {
|
|||||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, c.pullURL, nil)
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, c.pullURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
_ = c.Close()
|
_ = c.closeWithError(fmt.Errorf("stream pull build request failed: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
req.Host = c.headerHost
|
req.Host = c.headerHost
|
||||||
@@ -669,8 +746,9 @@ func (c *streamSplitConn) pullLoop() {
|
|||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
if isDialError(err) && dialRetry < maxDialRetry {
|
if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry {
|
||||||
dialRetry++
|
dialRetry++
|
||||||
|
closeIdleConnections(c.client)
|
||||||
select {
|
select {
|
||||||
case <-time.After(backoff):
|
case <-time.After(backoff):
|
||||||
case <-c.closed:
|
case <-c.closed:
|
||||||
@@ -682,16 +760,33 @@ func (c *streamSplitConn) pullLoop() {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_ = c.Close()
|
_ = c.closeWithError(fmt.Errorf("stream pull request failed: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dialRetry = 0
|
dialRetry = 0
|
||||||
backoff = minBackoff
|
backoff = minBackoff
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if isRetryableStatusCode(resp.StatusCode) && dialRetry < maxDialRetry {
|
||||||
|
dialRetry++
|
||||||
|
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024))
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
cancel()
|
||||||
|
closeIdleConnections(c.client)
|
||||||
|
select {
|
||||||
|
case <-time.After(backoff):
|
||||||
|
case <-c.closed:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
cancel()
|
cancel()
|
||||||
_ = c.Close()
|
_ = c.closeWithError(fmt.Errorf("stream pull bad status: %s", resp.Status))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +812,12 @@ func (c *streamSplitConn) pullLoop() {
|
|||||||
// Long-poll ended; retry.
|
// Long-poll ended; retry.
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
_ = c.Close()
|
// Some environments may sporadically reset the HTTP connection under load; treat
|
||||||
|
// it as an ended long-poll and retry instead of tearing down the whole tunnel.
|
||||||
|
if errors.Is(rerr, io.ErrUnexpectedEOF) || isRetryableRequestError(rerr) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
_ = c.closeWithError(fmt.Errorf("stream pull read failed: %w", rerr))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -735,8 +835,13 @@ func (c *streamSplitConn) pullLoop() {
|
|||||||
|
|
||||||
func (c *streamSplitConn) pushLoop() {
|
func (c *streamSplitConn) pushLoop() {
|
||||||
const (
|
const (
|
||||||
maxBatchBytes = 256 * 1024
|
// Batching is critical for stability under high concurrency: every flush is a new TCP
|
||||||
flushInterval = 5 * time.Millisecond
|
// connection in HTTP/1.1, and too many tiny uploads can overwhelm the accept backlog,
|
||||||
|
// causing sporadic RSTs (connection reset by peer).
|
||||||
|
//
|
||||||
|
// Keep this below the server-side maxUploadBytes limit in streamPush().
|
||||||
|
maxBatchBytes = 512 * 1024
|
||||||
|
flushInterval = 25 * time.Millisecond
|
||||||
requestTimeout = 20 * time.Second
|
requestTimeout = 20 * time.Second
|
||||||
maxDialRetry = 12
|
maxDialRetry = 12
|
||||||
minBackoff = 10 * time.Millisecond
|
minBackoff = 10 * time.Millisecond
|
||||||
@@ -754,12 +859,18 @@ func (c *streamSplitConn) pushLoop() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload := buf.Bytes()
|
||||||
reqCtx, cancel := context.WithTimeout(c.ctx, requestTimeout)
|
reqCtx, cancel := context.WithTimeout(c.ctx, requestTimeout)
|
||||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.pushURL, bytes.NewReader(buf.Bytes()))
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.pushURL, bytes.NewReader(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// Be explicit: some http client forks won't auto-populate GetBody, which makes POST retries on stale
|
||||||
|
// keep-alive connections flaky under multiplex=auto.
|
||||||
|
req.GetBody = func() (io.ReadCloser, error) {
|
||||||
|
return io.NopCloser(bytes.NewReader(payload)), nil
|
||||||
|
}
|
||||||
req.Host = c.headerHost
|
req.Host = c.headerHost
|
||||||
applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream)
|
applyTunnelHeaders(req.Header, c.headerHost, TunnelModeStream)
|
||||||
applyTunnelAuth(req, c.auth, TunnelModeStream, http.MethodPost, "/api/v1/upload")
|
applyTunnelAuth(req, c.auth, TunnelModeStream, http.MethodPost, "/api/v1/upload")
|
||||||
@@ -774,7 +885,7 @@ func (c *streamSplitConn) pushLoop() {
|
|||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
cancel()
|
cancel()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("bad status: %s", resp.Status)
|
return &httpStatusError{code: resp.StatusCode, status: resp.Status}
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
@@ -787,8 +898,22 @@ func (c *streamSplitConn) pushLoop() {
|
|||||||
for {
|
for {
|
||||||
if err := flush(); err == nil {
|
if err := flush(); err == nil {
|
||||||
return nil
|
return nil
|
||||||
} else if isDialError(err) && dialRetry < maxDialRetry {
|
} else if se := (*httpStatusError)(nil); errors.As(err, &se) && isRetryableStatusCode(se.code) && dialRetry < maxDialRetry {
|
||||||
dialRetry++
|
dialRetry++
|
||||||
|
closeIdleConnections(c.client)
|
||||||
|
select {
|
||||||
|
case <-time.After(backoff):
|
||||||
|
case <-c.closed:
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry {
|
||||||
|
dialRetry++
|
||||||
|
closeIdleConnections(c.client)
|
||||||
select {
|
select {
|
||||||
case <-time.After(backoff):
|
case <-time.After(backoff):
|
||||||
case <-c.closed:
|
case <-c.closed:
|
||||||
@@ -829,7 +954,7 @@ func (c *streamSplitConn) pushLoop() {
|
|||||||
}
|
}
|
||||||
if buf.Len()+len(b) > maxBatchBytes {
|
if buf.Len()+len(b) > maxBatchBytes {
|
||||||
if err := flushWithRetry(); err != nil {
|
if err := flushWithRetry(); err != nil {
|
||||||
_ = c.Close()
|
_ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resetTimer()
|
resetTimer()
|
||||||
@@ -837,14 +962,14 @@ func (c *streamSplitConn) pushLoop() {
|
|||||||
_, _ = buf.Write(b)
|
_, _ = buf.Write(b)
|
||||||
if buf.Len() >= maxBatchBytes {
|
if buf.Len() >= maxBatchBytes {
|
||||||
if err := flushWithRetry(); err != nil {
|
if err := flushWithRetry(); err != nil {
|
||||||
_ = c.Close()
|
_ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resetTimer()
|
resetTimer()
|
||||||
}
|
}
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
if err := flushWithRetry(); err != nil {
|
if err := flushWithRetry(); err != nil {
|
||||||
_ = c.Close()
|
_ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resetTimer()
|
resetTimer()
|
||||||
@@ -858,7 +983,7 @@ func (c *streamSplitConn) pushLoop() {
|
|||||||
}
|
}
|
||||||
if buf.Len()+len(b) > maxBatchBytes {
|
if buf.Len()+len(b) > maxBatchBytes {
|
||||||
if err := flushWithRetry(); err != nil {
|
if err := flushWithRetry(); err != nil {
|
||||||
_ = c.Close()
|
_ = c.closeWithError(fmt.Errorf("stream push flush failed: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -905,6 +1030,43 @@ func isDialError(err error) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isRetryableRequestError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// net/http may return this when reusing a keep-alive conn that the peer already closed.
|
||||||
|
// Treat it as retryable: callers already implement bounded backoff retries.
|
||||||
|
if strings.Contains(strings.ToLower(err.Error()), "server closed idle connection") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unwrap common wrappers.
|
||||||
|
var urlErr *url.Error
|
||||||
|
if errors.As(err, &urlErr) {
|
||||||
|
return isRetryableRequestError(urlErr.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection-level transient failures.
|
||||||
|
if errors.Is(err, syscall.EPIPE) || errors.Is(err, syscall.ECONNRESET) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
var netErr net.Error
|
||||||
|
if errors.As(err, &netErr) {
|
||||||
|
return netErr.Timeout() || netErr.Temporary()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (c *pollConn) closeWithError(err error) error {
|
func (c *pollConn) closeWithError(err error) error {
|
||||||
_ = c.queuedConn.closeWithError(err)
|
_ = c.queuedConn.closeWithError(err)
|
||||||
if c.cancel != nil {
|
if c.cancel != nil {
|
||||||
@@ -1012,8 +1174,10 @@ func (c *pollConn) pullLoop() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(c.ctx, http.MethodGet, c.pullURL, nil)
|
reqCtx, cancel := context.WithTimeout(c.ctx, 30*time.Second)
|
||||||
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, c.pullURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cancel()
|
||||||
_ = c.Close()
|
_ = c.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1023,8 +1187,10 @@ func (c *pollConn) pullLoop() {
|
|||||||
|
|
||||||
resp, err := c.client.Do(req)
|
resp, err := c.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if isDialError(err) && dialRetry < maxDialRetry {
|
cancel()
|
||||||
|
if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry {
|
||||||
dialRetry++
|
dialRetry++
|
||||||
|
closeIdleConnections(c.client)
|
||||||
select {
|
select {
|
||||||
case <-time.After(backoff):
|
case <-time.After(backoff):
|
||||||
case <-c.closed:
|
case <-c.closed:
|
||||||
@@ -1043,7 +1209,25 @@ func (c *pollConn) pullLoop() {
|
|||||||
backoff = minBackoff
|
backoff = minBackoff
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if isRetryableStatusCode(resp.StatusCode) && dialRetry < maxDialRetry {
|
||||||
|
dialRetry++
|
||||||
|
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 4*1024))
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
cancel()
|
||||||
|
closeIdleConnections(c.client)
|
||||||
|
select {
|
||||||
|
case <-time.After(backoff):
|
||||||
|
case <-c.closed:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
cancel()
|
||||||
_ = c.closeWithError(fmt.Errorf("poll pull bad status: %s", resp.Status))
|
_ = c.closeWithError(fmt.Errorf("poll pull bad status: %s", resp.Status))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1068,7 +1252,12 @@ func (c *pollConn) pullLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
|
cancel()
|
||||||
if err := scanner.Err(); err != nil {
|
if err := scanner.Err(); err != nil {
|
||||||
|
// Treat transient stream breaks (RST/EOF) as an ended long-poll and retry.
|
||||||
|
if errors.Is(err, io.ErrUnexpectedEOF) || isRetryableRequestError(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
_ = c.closeWithError(fmt.Errorf("poll pull scan failed: %w", err))
|
_ = c.closeWithError(fmt.Errorf("poll pull scan failed: %w", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1077,8 +1266,8 @@ func (c *pollConn) pullLoop() {
|
|||||||
|
|
||||||
func (c *pollConn) pushLoop() {
|
func (c *pollConn) pushLoop() {
|
||||||
const (
|
const (
|
||||||
maxBatchBytes = 64 * 1024
|
maxBatchBytes = 512 * 1024
|
||||||
flushInterval = 5 * time.Millisecond
|
flushInterval = 50 * time.Millisecond
|
||||||
maxLineRawBytes = 16 * 1024
|
maxLineRawBytes = 16 * 1024
|
||||||
maxDialRetry = 12
|
maxDialRetry = 12
|
||||||
minBackoff = 10 * time.Millisecond
|
minBackoff = 10 * time.Millisecond
|
||||||
@@ -1097,12 +1286,16 @@ func (c *pollConn) pushLoop() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload := buf.Bytes()
|
||||||
reqCtx, cancel := context.WithTimeout(c.ctx, 20*time.Second)
|
reqCtx, cancel := context.WithTimeout(c.ctx, 20*time.Second)
|
||||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.pushURL, bytes.NewReader(buf.Bytes()))
|
req, err := http.NewRequestWithContext(reqCtx, http.MethodPost, c.pushURL, bytes.NewReader(payload))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
req.GetBody = func() (io.ReadCloser, error) {
|
||||||
|
return io.NopCloser(bytes.NewReader(payload)), nil
|
||||||
|
}
|
||||||
req.Host = c.headerHost
|
req.Host = c.headerHost
|
||||||
applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll)
|
applyTunnelHeaders(req.Header, c.headerHost, TunnelModePoll)
|
||||||
applyTunnelAuth(req, c.auth, TunnelModePoll, http.MethodPost, "/api/v1/upload")
|
applyTunnelAuth(req, c.auth, TunnelModePoll, http.MethodPost, "/api/v1/upload")
|
||||||
@@ -1117,7 +1310,7 @@ func (c *pollConn) pushLoop() {
|
|||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
cancel()
|
cancel()
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return fmt.Errorf("bad status: %s", resp.Status)
|
return &httpStatusError{code: resp.StatusCode, status: resp.Status}
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
@@ -1131,8 +1324,22 @@ func (c *pollConn) pushLoop() {
|
|||||||
for {
|
for {
|
||||||
if err := flush(); err == nil {
|
if err := flush(); err == nil {
|
||||||
return nil
|
return nil
|
||||||
} else if isDialError(err) && dialRetry < maxDialRetry {
|
} else if se := (*httpStatusError)(nil); errors.As(err, &se) && isRetryableStatusCode(se.code) && dialRetry < maxDialRetry {
|
||||||
dialRetry++
|
dialRetry++
|
||||||
|
closeIdleConnections(c.client)
|
||||||
|
select {
|
||||||
|
case <-time.After(backoff):
|
||||||
|
case <-c.closed:
|
||||||
|
return c.closedErr()
|
||||||
|
}
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > maxBackoff {
|
||||||
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
} else if (isDialError(err) || isRetryableRequestError(err)) && dialRetry < maxDialRetry {
|
||||||
|
dialRetry++
|
||||||
|
closeIdleConnections(c.client)
|
||||||
select {
|
select {
|
||||||
case <-time.After(backoff):
|
case <-time.After(backoff):
|
||||||
case <-c.closed:
|
case <-c.closed:
|
||||||
@@ -1482,6 +1689,16 @@ func (s *TunnelServer) HandleConn(rawConn net.Conn) (HandleResult, net.Conn, err
|
|||||||
return HandleDone, nil, nil
|
return HandleDone, nil, nil
|
||||||
}
|
}
|
||||||
return s.handlePoll(rawConn, req, headerBytes, buffered)
|
return s.handlePoll(rawConn, req, headerBytes, buffered)
|
||||||
|
case TunnelModeWS:
|
||||||
|
if s.mode != TunnelModeWS && s.mode != TunnelModeAuto {
|
||||||
|
if s.passThroughOnReject {
|
||||||
|
return reject()
|
||||||
|
}
|
||||||
|
_ = writeSimpleHTTPResponse(rawConn, http.StatusNotFound, "not found")
|
||||||
|
_ = rawConn.Close()
|
||||||
|
return HandleDone, nil, nil
|
||||||
|
}
|
||||||
|
return s.handleWS(rawConn, req, headerBytes, buffered)
|
||||||
default:
|
default:
|
||||||
if s.passThroughOnReject {
|
if s.passThroughOnReject {
|
||||||
return reject()
|
return reject()
|
||||||
|
|||||||
176
transport/sudoku/obfs/httpmask/tunnel_ws.go
Normal file
176
transport/sudoku/obfs/httpmask/tunnel_ws.go
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
package httpmask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
mrand "math/rand"
|
||||||
|
"net"
|
||||||
|
stdhttp "net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gobwas/ws"
|
||||||
|
"github.com/metacubex/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalizeWSSchemeFromAddress(serverAddress string, tlsEnabled bool) (string, string) {
|
||||||
|
addr := strings.TrimSpace(serverAddress)
|
||||||
|
if strings.Contains(addr, "://") {
|
||||||
|
if u, err := url.Parse(addr); err == nil && u != nil {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(u.Scheme)) {
|
||||||
|
case "ws":
|
||||||
|
return "ws", u.Host
|
||||||
|
case "wss":
|
||||||
|
return "wss", u.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tlsEnabled {
|
||||||
|
return "wss", addr
|
||||||
|
}
|
||||||
|
return "ws", addr
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeWSDialTarget(serverAddress string, tlsEnabled bool, hostOverride string) (scheme, urlHost, dialAddr, serverName string, err error) {
|
||||||
|
scheme, addr := normalizeWSSchemeFromAddress(serverAddress, tlsEnabled)
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort(addr)
|
||||||
|
if err != nil {
|
||||||
|
// Allow ws(s)://host without port.
|
||||||
|
if strings.Contains(addr, ":") {
|
||||||
|
return "", "", "", "", fmt.Errorf("invalid server address %q: %w", serverAddress, err)
|
||||||
|
}
|
||||||
|
switch scheme {
|
||||||
|
case "wss":
|
||||||
|
port = "443"
|
||||||
|
default:
|
||||||
|
port = "80"
|
||||||
|
}
|
||||||
|
host = addr
|
||||||
|
}
|
||||||
|
|
||||||
|
if hostOverride != "" {
|
||||||
|
// Allow "example.com" or "example.com:443"
|
||||||
|
if h, p, splitErr := net.SplitHostPort(hostOverride); splitErr == nil {
|
||||||
|
if h != "" {
|
||||||
|
hostOverride = h
|
||||||
|
}
|
||||||
|
if p != "" {
|
||||||
|
port = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serverName = hostOverride
|
||||||
|
urlHost = net.JoinHostPort(hostOverride, port)
|
||||||
|
} else {
|
||||||
|
serverName = host
|
||||||
|
urlHost = net.JoinHostPort(host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialAddr = net.JoinHostPort(host, port)
|
||||||
|
return scheme, urlHost, dialAddr, trimPortForHost(serverName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyWSHeaders(h stdhttp.Header, host string) {
|
||||||
|
if h == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r := rngPool.Get().(*mrand.Rand)
|
||||||
|
ua := userAgents[r.Intn(len(userAgents))]
|
||||||
|
accept := accepts[r.Intn(len(accepts))]
|
||||||
|
lang := acceptLanguages[r.Intn(len(acceptLanguages))]
|
||||||
|
enc := acceptEncodings[r.Intn(len(acceptEncodings))]
|
||||||
|
rngPool.Put(r)
|
||||||
|
|
||||||
|
h.Set("User-Agent", ua)
|
||||||
|
h.Set("Accept", accept)
|
||||||
|
h.Set("Accept-Language", lang)
|
||||||
|
h.Set("Accept-Encoding", enc)
|
||||||
|
h.Set("Cache-Control", "no-cache")
|
||||||
|
h.Set("Pragma", "no-cache")
|
||||||
|
h.Set("X-Sudoku-Tunnel", string(TunnelModeWS))
|
||||||
|
h.Set("X-Sudoku-Version", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialWS(ctx context.Context, serverAddress string, opts TunnelDialOptions) (net.Conn, error) {
|
||||||
|
if opts.DialContext == nil {
|
||||||
|
panic("httpmask: DialContext is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
scheme, urlHost, dialAddr, serverName, err := normalizeWSDialTarget(serverAddress, opts.TLSEnabled, opts.HostOverride)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpScheme := "http"
|
||||||
|
if scheme == "wss" {
|
||||||
|
httpScheme = "https"
|
||||||
|
}
|
||||||
|
headerHost := canonicalHeaderHost(urlHost, httpScheme)
|
||||||
|
auth := newTunnelAuth(opts.AuthKey, 0)
|
||||||
|
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: scheme,
|
||||||
|
Host: urlHost,
|
||||||
|
Path: joinPathRoot(opts.PathRoot, "/ws"),
|
||||||
|
}
|
||||||
|
|
||||||
|
header := make(stdhttp.Header)
|
||||||
|
applyWSHeaders(header, headerHost)
|
||||||
|
|
||||||
|
if auth != nil {
|
||||||
|
token := auth.token(TunnelModeWS, stdhttp.MethodGet, "/ws", time.Now())
|
||||||
|
if token != "" {
|
||||||
|
header.Set("Authorization", "Bearer "+token)
|
||||||
|
q := u.Query()
|
||||||
|
q.Set(tunnelAuthQueryKey, token)
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d := ws.Dialer{
|
||||||
|
Host: headerHost,
|
||||||
|
Header: ws.HandshakeHeaderHTTP(header),
|
||||||
|
NetDial: func(dialCtx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
if addr == urlHost {
|
||||||
|
addr = dialAddr
|
||||||
|
}
|
||||||
|
return opts.DialContext(dialCtx, network, addr)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if scheme == "wss" {
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ServerName: serverName,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
|
d.TLSClient = func(conn net.Conn, hostname string) net.Conn {
|
||||||
|
return tls.Client(conn, tlsConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, br, _, err := d.Dial(ctx, u.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if br != nil && br.Buffered() > 0 {
|
||||||
|
pre := make([]byte, br.Buffered())
|
||||||
|
_, _ = io.ReadFull(br, pre)
|
||||||
|
conn = newPreBufferedConn(conn, pre)
|
||||||
|
}
|
||||||
|
|
||||||
|
wsConn := newWSStreamConn(conn, ws.StateClientSide)
|
||||||
|
if opts.Upgrade == nil {
|
||||||
|
return wsConn, nil
|
||||||
|
}
|
||||||
|
upgraded, err := opts.Upgrade(wsConn)
|
||||||
|
if err != nil {
|
||||||
|
_ = wsConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if upgraded != nil {
|
||||||
|
return upgraded, nil
|
||||||
|
}
|
||||||
|
return wsConn, nil
|
||||||
|
}
|
||||||
77
transport/sudoku/obfs/httpmask/tunnel_ws_server.go
Normal file
77
transport/sudoku/obfs/httpmask/tunnel_ws_server.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package httpmask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gobwas/ws"
|
||||||
|
)
|
||||||
|
|
||||||
|
func looksLikeWebSocketUpgrade(headers map[string]string) bool {
|
||||||
|
if headers == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !strings.EqualFold(strings.TrimSpace(headers["upgrade"]), "websocket") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
conn := headers["connection"]
|
||||||
|
for _, part := range strings.Split(conn, ",") {
|
||||||
|
if strings.EqualFold(strings.TrimSpace(part), "upgrade") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TunnelServer) handleWS(rawConn net.Conn, req *httpRequestHeader, headerBytes []byte, buffered []byte) (HandleResult, net.Conn, error) {
|
||||||
|
rejectOrReply := func(code int, body string) (HandleResult, net.Conn, error) {
|
||||||
|
if s.passThroughOnReject {
|
||||||
|
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
|
||||||
|
prefix = append(prefix, headerBytes...)
|
||||||
|
prefix = append(prefix, buffered...)
|
||||||
|
return HandlePassThrough, newRejectedPreBufferedConn(rawConn, prefix), nil
|
||||||
|
}
|
||||||
|
_ = writeSimpleHTTPResponse(rawConn, code, body)
|
||||||
|
_ = rawConn.Close()
|
||||||
|
return HandleDone, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.ParseRequestURI(req.target)
|
||||||
|
if err != nil {
|
||||||
|
return rejectOrReply(http.StatusBadRequest, "bad request")
|
||||||
|
}
|
||||||
|
|
||||||
|
path, ok := stripPathRoot(s.pathRoot, u.Path)
|
||||||
|
if !ok || path != "/ws" {
|
||||||
|
return rejectOrReply(http.StatusNotFound, "not found")
|
||||||
|
}
|
||||||
|
if strings.ToUpper(strings.TrimSpace(req.method)) != http.MethodGet {
|
||||||
|
return rejectOrReply(http.StatusBadRequest, "bad request")
|
||||||
|
}
|
||||||
|
if !looksLikeWebSocketUpgrade(req.headers) {
|
||||||
|
return rejectOrReply(http.StatusBadRequest, "bad request")
|
||||||
|
}
|
||||||
|
|
||||||
|
authVal := req.headers["authorization"]
|
||||||
|
if authVal == "" {
|
||||||
|
authVal = u.Query().Get(tunnelAuthQueryKey)
|
||||||
|
}
|
||||||
|
if !s.auth.verifyValue(authVal, TunnelModeWS, req.method, path, time.Now()) {
|
||||||
|
return rejectOrReply(http.StatusNotFound, "not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
prefix := make([]byte, 0, len(headerBytes)+len(buffered))
|
||||||
|
prefix = append(prefix, headerBytes...)
|
||||||
|
prefix = append(prefix, buffered...)
|
||||||
|
wsConnRaw := newPreBufferedConn(rawConn, prefix)
|
||||||
|
|
||||||
|
if _, err := ws.Upgrade(wsConnRaw); err != nil {
|
||||||
|
_ = rawConn.Close()
|
||||||
|
return HandleDone, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return HandleStartTunnel, newWSStreamConn(wsConnRaw, ws.StateServerSide), nil
|
||||||
|
}
|
||||||
78
transport/sudoku/obfs/httpmask/ws_stream_conn.go
Normal file
78
transport/sudoku/obfs/httpmask/ws_stream_conn.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package httpmask
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/gobwas/ws"
|
||||||
|
"github.com/gobwas/ws/wsutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
type wsStreamConn struct {
|
||||||
|
net.Conn
|
||||||
|
state ws.State
|
||||||
|
reader *wsutil.Reader
|
||||||
|
controlHandler wsutil.FrameHandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWSStreamConn(conn net.Conn, state ws.State) net.Conn {
|
||||||
|
controlHandler := wsutil.ControlFrameHandler(conn, state)
|
||||||
|
return &wsStreamConn{
|
||||||
|
Conn: conn,
|
||||||
|
state: state,
|
||||||
|
reader: &wsutil.Reader{
|
||||||
|
Source: conn,
|
||||||
|
State: state,
|
||||||
|
},
|
||||||
|
controlHandler: controlHandler,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wsStreamConn) Read(b []byte) (n int, err error) {
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
err = fmt.Errorf("websocket error: %v", v)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err = c.reader.Read(b)
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
if !errors.Is(err, wsutil.ErrNoFrameAdvance) {
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hdr, err2 := c.reader.NextFrame()
|
||||||
|
if err2 != nil {
|
||||||
|
return 0, err2
|
||||||
|
}
|
||||||
|
if hdr.OpCode.IsControl() {
|
||||||
|
if err := c.controlHandler(hdr, c.reader); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if hdr.OpCode&(ws.OpBinary|ws.OpText) == 0 {
|
||||||
|
if err := c.reader.Discard(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wsStreamConn) Write(b []byte) (int, error) {
|
||||||
|
if err := wsutil.WriteMessage(c.Conn, c.state, ws.OpBinary, b); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *wsStreamConn) Close() error {
|
||||||
|
_ = wsutil.WriteMessage(c.Conn, c.state, ws.OpClose, ws.NewCloseFrameBody(ws.StatusNormalClosure, ""))
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
@@ -4,9 +4,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"log"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -26,7 +24,7 @@ type Table struct {
|
|||||||
func NewTable(key string, mode string) *Table {
|
func NewTable(key string, mode string) *Table {
|
||||||
t, err := NewTableWithCustom(key, mode, "")
|
t, err := NewTableWithCustom(key, mode, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Panicf("failed to build table: %v", err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
@@ -35,8 +33,6 @@ func NewTable(key string, mode string) *Table {
|
|||||||
// mode: "prefer_ascii" or "prefer_entropy". If a custom pattern is provided, ASCII mode still takes precedence.
|
// mode: "prefer_ascii" or "prefer_entropy". If a custom pattern is provided, ASCII mode still takes precedence.
|
||||||
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
|
// The customPattern must contain 8 characters with exactly 2 x, 2 p, and 4 v (case-insensitive).
|
||||||
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
|
func NewTableWithCustom(key string, mode string, customPattern string) (*Table, error) {
|
||||||
start := time.Now()
|
|
||||||
|
|
||||||
layout, err := resolveLayout(mode, customPattern)
|
layout, err := resolveLayout(mode, customPattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -126,7 +122,6 @@ func NewTableWithCustom(key string, mode string, customPattern string) (*Table,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("[Init] Sudoku Tables initialized (%s) in %v", layout.name, time.Since(start))
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
74
transport/sudoku/replay.go
Normal file
74
transport/sudoku/replay.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var handshakeReplayTTL = 60 * time.Second
|
||||||
|
|
||||||
|
type nonceSet struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
m map[[kipHelloNonceSize]byte]time.Time
|
||||||
|
maxEntries int
|
||||||
|
lastPrune time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNonceSet(maxEntries int) *nonceSet {
|
||||||
|
if maxEntries <= 0 {
|
||||||
|
maxEntries = 4096
|
||||||
|
}
|
||||||
|
return &nonceSet{
|
||||||
|
m: make(map[[kipHelloNonceSize]byte]time.Time),
|
||||||
|
maxEntries: maxEntries,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *nonceSet) allow(nonce [kipHelloNonceSize]byte, now time.Time, ttl time.Duration) bool {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = 60 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if now.Sub(s.lastPrune) > ttl/2 || len(s.m) > s.maxEntries {
|
||||||
|
for k, exp := range s.m {
|
||||||
|
if !now.Before(exp) {
|
||||||
|
delete(s.m, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.lastPrune = now
|
||||||
|
for len(s.m) > s.maxEntries {
|
||||||
|
for k := range s.m {
|
||||||
|
delete(s.m, k)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if exp, ok := s.m[nonce]; ok && now.Before(exp) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
s.m[nonce] = now.Add(ttl)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type handshakeReplayProtector struct {
|
||||||
|
users sync.Map // map[userHash string]*nonceSet
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *handshakeReplayProtector) allow(userHash string, nonce [kipHelloNonceSize]byte, now time.Time) bool {
|
||||||
|
if userHash == "" {
|
||||||
|
userHash = "_"
|
||||||
|
}
|
||||||
|
val, _ := p.users.LoadOrStore(userHash, newNonceSet(4096))
|
||||||
|
set, ok := val.(*nonceSet)
|
||||||
|
if !ok || set == nil {
|
||||||
|
set = newNonceSet(4096)
|
||||||
|
p.users.Store(userHash, set)
|
||||||
|
}
|
||||||
|
return set.allow(nonce, now, handshakeReplayTTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalHandshakeReplay = &handshakeReplayProtector{}
|
||||||
58
transport/sudoku/session_keys.go
Normal file
58
transport/sudoku/session_keys.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package sudoku
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdh"
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func derivePSKDirectionalBases(psk string) (c2s, s2c []byte) {
|
||||||
|
sum := sha256.Sum256([]byte(psk))
|
||||||
|
c2sKey := make([]byte, 32)
|
||||||
|
s2cKey := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(sha256.New, sum[:], []byte("sudoku-psk-c2s")), c2sKey); err != nil {
|
||||||
|
panic("sudoku: hkdf expand failed")
|
||||||
|
}
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(sha256.New, sum[:], []byte("sudoku-psk-s2c")), s2cKey); err != nil {
|
||||||
|
panic("sudoku: hkdf expand failed")
|
||||||
|
}
|
||||||
|
return c2sKey, s2cKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveSessionDirectionalBases(psk string, shared []byte, nonce [kipHelloNonceSize]byte) (c2s, s2c []byte, err error) {
|
||||||
|
sum := sha256.Sum256([]byte(psk))
|
||||||
|
ikm := make([]byte, 0, len(shared)+len(nonce))
|
||||||
|
ikm = append(ikm, shared...)
|
||||||
|
ikm = append(ikm, nonce[:]...)
|
||||||
|
|
||||||
|
prk := hkdf.Extract(sha256.New, ikm, sum[:])
|
||||||
|
|
||||||
|
c2sKey := make([]byte, 32)
|
||||||
|
s2cKey := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(sha256.New, prk, []byte("sudoku-session-c2s")), c2sKey); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("hkdf expand c2s: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(sha256.New, prk, []byte("sudoku-session-s2c")), s2cKey); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("hkdf expand s2c: %w", err)
|
||||||
|
}
|
||||||
|
return c2sKey, s2cKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func x25519SharedSecret(priv *ecdh.PrivateKey, peerPub []byte) ([]byte, error) {
|
||||||
|
if priv == nil {
|
||||||
|
return nil, fmt.Errorf("nil priv")
|
||||||
|
}
|
||||||
|
curve := ecdh.X25519()
|
||||||
|
pk, err := curve.NewPublicKey(peerPub)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse peer pub: %w", err)
|
||||||
|
}
|
||||||
|
secret, err := priv.ECDH(pk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ecdh: %w", err)
|
||||||
|
}
|
||||||
|
return secret, nil
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
"bytes"
|
||||||
crand "crypto/rand"
|
crand "crypto/rand"
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -56,26 +55,27 @@ func drainBuffered(r *bufio.Reader) ([]byte, error) {
|
|||||||
func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error {
|
func probeHandshakeBytes(probe []byte, cfg *ProtocolConfig, table *sudoku.Table) error {
|
||||||
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
|
rc := &readOnlyConn{Reader: bytes.NewReader(probe)}
|
||||||
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
|
_, obfsConn := buildServerObfsConn(rc, cfg, table, false)
|
||||||
cConn, err := crypto.NewAEADConn(obfsConn, cfg.Key, cfg.AEADMethod)
|
seed := ServerAEADSeed(cfg.Key)
|
||||||
|
pskC2S, pskS2C := derivePSKDirectionalBases(seed)
|
||||||
|
// Server side: recv is client->server, send is server->client.
|
||||||
|
cConn, err := crypto.NewRecordConn(obfsConn, cfg.AEADMethod, pskS2C, pskC2S)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var handshakeBuf [16]byte
|
msg, err := ReadKIPMessage(cConn)
|
||||||
if _, err := io.ReadFull(cConn, handshakeBuf[:]); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
ts := int64(binary.BigEndian.Uint64(handshakeBuf[:8]))
|
if msg.Type != KIPTypeClientHello {
|
||||||
if absInt64(time.Now().Unix()-ts) > 60 {
|
return fmt.Errorf("unexpected handshake message: %d", msg.Type)
|
||||||
return fmt.Errorf("timestamp skew/replay detected")
|
|
||||||
}
|
}
|
||||||
|
ch, err := DecodeKIPClientHelloPayload(msg.Payload)
|
||||||
modeBuf := []byte{0}
|
if err != nil {
|
||||||
if _, err := io.ReadFull(cConn, modeBuf); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if modeBuf[0] != downlinkMode(cfg) {
|
if absInt64(time.Now().Unix()-ch.Timestamp.Unix()) > int64(kipHandshakeSkew.Seconds()) {
|
||||||
return fmt.Errorf("downlink mode mismatch")
|
return fmt.Errorf("time skew/replay")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -93,6 +93,17 @@ func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.T
|
|||||||
return nil, nil, fmt.Errorf("too many table candidates: %d", len(tables))
|
return nil, nil, fmt.Errorf("too many table candidates: %d", len(tables))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy so we can prune candidates without mutating the caller slice.
|
||||||
|
candidates := make([]*sudoku.Table, 0, len(tables))
|
||||||
|
for _, t := range tables {
|
||||||
|
if t != nil {
|
||||||
|
candidates = append(candidates, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return nil, nil, fmt.Errorf("no table candidates")
|
||||||
|
}
|
||||||
|
|
||||||
probe, err := drainBuffered(r)
|
probe, err := drainBuffered(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err)
|
return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err)
|
||||||
@@ -100,17 +111,18 @@ func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.T
|
|||||||
|
|
||||||
tmp := make([]byte, readChunk)
|
tmp := make([]byte, readChunk)
|
||||||
for {
|
for {
|
||||||
if len(tables) == 1 {
|
if len(candidates) == 1 {
|
||||||
tail, err := drainBuffered(r)
|
tail, err := drainBuffered(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err)
|
return nil, nil, fmt.Errorf("drain buffered bytes failed: %w", err)
|
||||||
}
|
}
|
||||||
probe = append(probe, tail...)
|
probe = append(probe, tail...)
|
||||||
return tables[0], probe, nil
|
return candidates[0], probe, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
needMore := false
|
needMore := false
|
||||||
for _, table := range tables {
|
next := candidates[:0]
|
||||||
|
for _, table := range candidates {
|
||||||
err := probeHandshakeBytes(probe, cfg, table)
|
err := probeHandshakeBytes(probe, cfg, table)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tail, err := drainBuffered(r)
|
tail, err := drainBuffered(r)
|
||||||
@@ -122,10 +134,13 @@ func selectTableByProbe(r *bufio.Reader, cfg *ProtocolConfig, tables []*sudoku.T
|
|||||||
}
|
}
|
||||||
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
|
||||||
needMore = true
|
needMore = true
|
||||||
|
next = append(next, table)
|
||||||
}
|
}
|
||||||
|
// Definitive mismatch: drop table.
|
||||||
}
|
}
|
||||||
|
candidates = next
|
||||||
|
|
||||||
if !needMore {
|
if len(candidates) == 0 || !needMore {
|
||||||
return nil, probe, fmt.Errorf("handshake table selection failed")
|
return nil, probe, fmt.Errorf("handshake table selection failed")
|
||||||
}
|
}
|
||||||
if len(probe) >= maxProbeBytes {
|
if len(probe) >= maxProbeBytes {
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import (
|
|||||||
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
"github.com/metacubex/mihomo/transport/sudoku/obfs/sudoku"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.
|
func normalizeCustomPatterns(customTable string, customTables []string) []string {
|
||||||
// When customTables is non-empty it overrides customTable (matching upstream Sudoku behavior).
|
|
||||||
func NewTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
|
|
||||||
patterns := customTables
|
patterns := customTables
|
||||||
if len(patterns) == 0 && strings.TrimSpace(customTable) != "" {
|
if len(patterns) == 0 && strings.TrimSpace(customTable) != "" {
|
||||||
patterns = []string{customTable}
|
patterns = []string{customTable}
|
||||||
@@ -16,7 +14,15 @@ func NewTablesWithCustomPatterns(key string, tableType string, customTable strin
|
|||||||
if len(patterns) == 0 {
|
if len(patterns) == 0 {
|
||||||
patterns = []string{""}
|
patterns = []string{""}
|
||||||
}
|
}
|
||||||
|
return patterns
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTablesWithCustomPatterns builds one or more obfuscation tables from x/v/p custom patterns.
|
||||||
|
// When customTables is non-empty it overrides customTable (matching upstream Sudoku behavior).
|
||||||
|
//
|
||||||
|
// Deprecated-ish: prefer NewClientTablesWithCustomPatterns / NewServerTablesWithCustomPatterns.
|
||||||
|
func NewTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
|
||||||
|
patterns := normalizeCustomPatterns(customTable, customTables)
|
||||||
tables := make([]*sudoku.Table, 0, len(patterns))
|
tables := make([]*sudoku.Table, 0, len(patterns))
|
||||||
for _, pattern := range patterns {
|
for _, pattern := range patterns {
|
||||||
pattern = strings.TrimSpace(pattern)
|
pattern = strings.TrimSpace(pattern)
|
||||||
@@ -28,3 +34,17 @@ func NewTablesWithCustomPatterns(key string, tableType string, customTable strin
|
|||||||
}
|
}
|
||||||
return tables, nil
|
return tables, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewClientTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
|
||||||
|
return NewTablesWithCustomPatterns(key, tableType, customTable, customTables)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServerTablesWithCustomPatterns matches upstream server behavior: when custom table rotation is enabled,
|
||||||
|
// also accept the default table to avoid forcing clients to update in lockstep.
|
||||||
|
func NewServerTablesWithCustomPatterns(key string, tableType string, customTable string, customTables []string) ([]*sudoku.Table, error) {
|
||||||
|
patterns := normalizeCustomPatterns(customTable, customTables)
|
||||||
|
if len(patterns) > 0 && strings.TrimSpace(patterns[0]) != "" {
|
||||||
|
patterns = append([]string{""}, patterns...)
|
||||||
|
}
|
||||||
|
return NewTablesWithCustomPatterns(key, tableType, "", patterns)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,17 +16,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
UoTMagicByte byte = 0xEE
|
maxUoTPayload = 64 * 1024
|
||||||
uotVersion = 0x01
|
|
||||||
maxUoTPayload = 64 * 1024
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WritePreface writes the UDP-over-TCP marker and version.
|
|
||||||
func WritePreface(w io.Writer) error {
|
|
||||||
_, err := w.Write([]byte{UoTMagicByte, uotVersion})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteDatagram sends a single UDP datagram frame over a reliable stream.
|
// WriteDatagram sends a single UDP datagram frame over a reliable stream.
|
||||||
func WriteDatagram(w io.Writer, addr string, payload []byte) error {
|
func WriteDatagram(w io.Writer, addr string, payload []byte) error {
|
||||||
addrBuf, err := EncodeAddress(addr)
|
addrBuf, err := EncodeAddress(addr)
|
||||||
@@ -45,14 +37,13 @@ func WriteDatagram(w io.Writer, addr string, payload []byte) error {
|
|||||||
binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf)))
|
binary.BigEndian.PutUint16(header[:2], uint16(len(addrBuf)))
|
||||||
binary.BigEndian.PutUint16(header[2:], uint16(len(payload)))
|
binary.BigEndian.PutUint16(header[2:], uint16(len(payload)))
|
||||||
|
|
||||||
if _, err := w.Write(header[:]); err != nil {
|
if err := writeFull(w, header[:]); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := w.Write(addrBuf); err != nil {
|
if err := writeFull(w, addrBuf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = w.Write(payload)
|
return writeFull(w, payload)
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadDatagram parses a single UDP datagram frame from the reliable stream.
|
// ReadDatagram parses a single UDP datagram frame from the reliable stream.
|
||||||
|
|||||||
269
transport/trusttunnel/client.go
Normal file
269
transport/trusttunnel/client.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
"github.com/metacubex/mihomo/transport/vmess"
|
||||||
|
|
||||||
|
"github.com/metacubex/http"
|
||||||
|
"github.com/metacubex/http/httptrace"
|
||||||
|
"github.com/metacubex/tls"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RoundTripper interface {
|
||||||
|
http.RoundTripper
|
||||||
|
CloseIdleConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResolvUDPFunc func(ctx context.Context, server string) (netip.AddrPort, error)
|
||||||
|
|
||||||
|
type ClientOptions struct {
|
||||||
|
Dialer C.Dialer
|
||||||
|
ResolvUDP ResolvUDPFunc
|
||||||
|
Server string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
TLSConfig *vmess.TLSConfig
|
||||||
|
QUIC bool
|
||||||
|
QUICCongestionControl string
|
||||||
|
QUICCwnd int
|
||||||
|
HealthCheck bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
ctx context.Context
|
||||||
|
dialer C.Dialer
|
||||||
|
resolv ResolvUDPFunc
|
||||||
|
server string
|
||||||
|
auth string
|
||||||
|
roundTripper RoundTripper
|
||||||
|
startOnce sync.Once
|
||||||
|
healthCheck bool
|
||||||
|
healthCheckTimer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(ctx context.Context, options ClientOptions) (client *Client, err error) {
|
||||||
|
client = &Client{
|
||||||
|
ctx: ctx,
|
||||||
|
dialer: options.Dialer,
|
||||||
|
resolv: options.ResolvUDP,
|
||||||
|
server: options.Server,
|
||||||
|
auth: buildAuth(options.Username, options.Password),
|
||||||
|
}
|
||||||
|
if options.QUIC {
|
||||||
|
if len(options.TLSConfig.NextProtos) == 0 {
|
||||||
|
options.TLSConfig.NextProtos = []string{"h3"}
|
||||||
|
} else if !slices.Contains(options.TLSConfig.NextProtos, "h3") {
|
||||||
|
return nil, errors.New("require alpn h3")
|
||||||
|
}
|
||||||
|
err = client.quicRoundTripper(options.TLSConfig, options.QUICCongestionControl, options.QUICCwnd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if len(options.TLSConfig.NextProtos) == 0 {
|
||||||
|
options.TLSConfig.NextProtos = []string{"h2"}
|
||||||
|
} else if !slices.Contains(options.TLSConfig.NextProtos, "h2") {
|
||||||
|
return nil, errors.New("require alpn h2")
|
||||||
|
}
|
||||||
|
client.h2RoundTripper(options.TLSConfig)
|
||||||
|
}
|
||||||
|
if options.HealthCheck {
|
||||||
|
client.healthCheck = true
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) h2RoundTripper(tlsConfig *vmess.TLSConfig) {
|
||||||
|
c.roundTripper = &http.Http2Transport{
|
||||||
|
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||||
|
conn, err := c.dialer.DialContext(ctx, network, c.server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConn, err := vmess.StreamTLSConn(ctx, conn, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return tlsConn, nil
|
||||||
|
},
|
||||||
|
AllowHTTP: false,
|
||||||
|
IdleConnTimeout: DefaultSessionTimeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) start() {
|
||||||
|
if c.healthCheck {
|
||||||
|
c.healthCheckTimer = time.NewTimer(DefaultHealthCheckTimeout)
|
||||||
|
go c.loopHealthCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) loopHealthCheck() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.healthCheckTimer.C:
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
c.healthCheckTimer.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(c.ctx, DefaultHealthCheckTimeout)
|
||||||
|
_ = c.HealthCheck(ctx)
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) resetHealthCheckTimer() {
|
||||||
|
if c.healthCheckTimer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.healthCheckTimer.Reset(DefaultHealthCheckTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) dial(ctx context.Context, request *http.Request, conn *httpConn, pipeReader *io.PipeReader, pipeWriter *io.PipeWriter) {
|
||||||
|
c.startOnce.Do(c.start)
|
||||||
|
trace := &httptrace.ClientTrace{
|
||||||
|
GotConn: func(connInfo httptrace.GotConnInfo) {
|
||||||
|
conn.SetLocalAddr(connInfo.Conn.LocalAddr())
|
||||||
|
conn.SetRemoteAddr(connInfo.Conn.RemoteAddr())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
request = request.WithContext(httptrace.WithClientTrace(ctx, trace))
|
||||||
|
response, err := c.roundTripper.RoundTrip(request)
|
||||||
|
if err != nil {
|
||||||
|
_ = pipeWriter.CloseWithError(err)
|
||||||
|
_ = pipeReader.CloseWithError(err)
|
||||||
|
conn.setUp(nil, err)
|
||||||
|
} else if response.StatusCode != http.StatusOK {
|
||||||
|
_ = response.Body.Close()
|
||||||
|
err = fmt.Errorf("unexpected status code: %d", response.StatusCode)
|
||||||
|
_ = pipeWriter.CloseWithError(err)
|
||||||
|
_ = pipeReader.CloseWithError(err)
|
||||||
|
conn.setUp(nil, err)
|
||||||
|
} else {
|
||||||
|
c.resetHealthCheckTimer()
|
||||||
|
conn.setUp(response.Body, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Dial(ctx context.Context, host string) (net.Conn, error) {
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
request := &http.Request{
|
||||||
|
Method: http.MethodConnect,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: host,
|
||||||
|
},
|
||||||
|
Header: make(http.Header),
|
||||||
|
Body: pipeReader,
|
||||||
|
Host: host,
|
||||||
|
}
|
||||||
|
request.Header.Add("User-Agent", TCPUserAgent)
|
||||||
|
request.Header.Add("Proxy-Authorization", c.auth)
|
||||||
|
conn := &tcpConn{
|
||||||
|
httpConn: httpConn{
|
||||||
|
writer: pipeWriter,
|
||||||
|
created: make(chan struct{}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
go c.dial(ctx, request, &conn.httpConn, pipeReader, pipeWriter)
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListenPacket(ctx context.Context) (net.PacketConn, error) {
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
request := &http.Request{
|
||||||
|
Method: http.MethodConnect,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: UDPMagicAddress,
|
||||||
|
},
|
||||||
|
Header: make(http.Header),
|
||||||
|
Body: pipeReader,
|
||||||
|
Host: UDPMagicAddress,
|
||||||
|
}
|
||||||
|
request.Header.Add("User-Agent", UDPUserAgent)
|
||||||
|
request.Header.Add("Proxy-Authorization", c.auth)
|
||||||
|
conn := &clientPacketConn{
|
||||||
|
packetConn: packetConn{
|
||||||
|
httpConn: httpConn{
|
||||||
|
writer: pipeWriter,
|
||||||
|
created: make(chan struct{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
go c.dial(ctx, request, &conn.httpConn, pipeReader, pipeWriter)
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListenICMP(ctx context.Context) (*IcmpConn, error) {
|
||||||
|
pipeReader, pipeWriter := io.Pipe()
|
||||||
|
request := &http.Request{
|
||||||
|
Method: http.MethodConnect,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: ICMPMagicAddress,
|
||||||
|
},
|
||||||
|
Header: make(http.Header),
|
||||||
|
Body: pipeReader,
|
||||||
|
Host: ICMPMagicAddress,
|
||||||
|
}
|
||||||
|
request.Header.Add("User-Agent", ICMPUserAgent)
|
||||||
|
request.Header.Add("Proxy-Authorization", c.auth)
|
||||||
|
conn := &IcmpConn{
|
||||||
|
httpConn{
|
||||||
|
writer: pipeWriter,
|
||||||
|
created: make(chan struct{}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
go c.dial(ctx, request, &conn.httpConn, pipeReader, pipeWriter)
|
||||||
|
return conn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
forceCloseAllConnections(c.roundTripper)
|
||||||
|
if c.healthCheckTimer != nil {
|
||||||
|
c.healthCheckTimer.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ResetConnections() {
|
||||||
|
forceCloseAllConnections(c.roundTripper)
|
||||||
|
c.resetHealthCheckTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) HealthCheck(ctx context.Context) error {
|
||||||
|
defer c.resetHealthCheckTimer()
|
||||||
|
request := &http.Request{
|
||||||
|
Method: http.MethodConnect,
|
||||||
|
URL: &url.URL{
|
||||||
|
Scheme: "https",
|
||||||
|
Host: HealthCheckMagicAddress,
|
||||||
|
},
|
||||||
|
Header: make(http.Header),
|
||||||
|
Host: HealthCheckMagicAddress,
|
||||||
|
}
|
||||||
|
request.Header.Add("User-Agent", HealthCheckUserAgent)
|
||||||
|
request.Header.Add("Proxy-Authorization", c.auth)
|
||||||
|
response, err := c.roundTripper.RoundTrip(request.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("unexpected status code: %d", response.StatusCode)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
4
transport/trusttunnel/doc.go
Normal file
4
transport/trusttunnel/doc.go
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Package trusttunnel copy and modify from:
|
||||||
|
// https://github.com/xchacha20-poly1305/sing-trusttunnel/tree/v0.1.1
|
||||||
|
// adopt for mihomo
|
||||||
|
package trusttunnel
|
||||||
18
transport/trusttunnel/force_close.go
Normal file
18
transport/trusttunnel/force_close.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/metacubex/mihomo/transport/gun"
|
||||||
|
|
||||||
|
"github.com/metacubex/http"
|
||||||
|
"github.com/metacubex/quic-go/http3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func forceCloseAllConnections(roundTripper RoundTripper) {
|
||||||
|
roundTripper.CloseIdleConnections()
|
||||||
|
switch tr := roundTripper.(type) {
|
||||||
|
case *http.Http2Transport:
|
||||||
|
gun.CloseTransport(tr)
|
||||||
|
case *http3.Transport:
|
||||||
|
_ = tr.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
82
transport/trusttunnel/icmp.go
Normal file
82
transport/trusttunnel/icmp.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/common/buf"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IcmpConn struct {
|
||||||
|
httpConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IcmpConn) WritePing(id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16) error {
|
||||||
|
request := buf.NewSize(2 + 16 + 2 + 1 + 2)
|
||||||
|
defer request.Release()
|
||||||
|
buf.Must(binary.Write(request, binary.BigEndian, id))
|
||||||
|
destinationAddress := buildPaddingIP(destination)
|
||||||
|
buf.Must1(request.Write(destinationAddress[:]))
|
||||||
|
buf.Must(binary.Write(request, binary.BigEndian, sequenceNumber))
|
||||||
|
buf.Must(binary.Write(request, binary.BigEndian, ttl))
|
||||||
|
buf.Must(binary.Write(request, binary.BigEndian, size))
|
||||||
|
return buf.Error(i.writeFlush(request.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IcmpConn) ReadPing() (id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16, err error) {
|
||||||
|
err = i.waitCreated()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response := buf.NewSize(2 + 16 + 1 + 1 + 2)
|
||||||
|
defer response.Release()
|
||||||
|
_, err = response.ReadFullFrom(i.body, response.Cap())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.Must(binary.Read(response, binary.BigEndian, &id))
|
||||||
|
var sourceAddressBuffer [16]byte
|
||||||
|
buf.Must1(response.Read(sourceAddressBuffer[:]))
|
||||||
|
sourceAddress = parse16BytesIP(sourceAddressBuffer)
|
||||||
|
buf.Must(binary.Read(response, binary.BigEndian, &icmpType))
|
||||||
|
buf.Must(binary.Read(response, binary.BigEndian, &code))
|
||||||
|
buf.Must(binary.Read(response, binary.BigEndian, &sequenceNumber))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IcmpConn) Close() error {
|
||||||
|
return i.httpConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IcmpConn) ReadPingRequest() (id uint16, destination netip.Addr, sequenceNumber uint16, ttl uint8, size uint16, err error) {
|
||||||
|
err = i.waitCreated()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request := buf.NewSize(2 + 16 + 2 + 1 + 2)
|
||||||
|
defer request.Release()
|
||||||
|
_, err = request.ReadFullFrom(i.body, request.Cap())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf.Must(binary.Read(request, binary.BigEndian, &id))
|
||||||
|
var destinationAddressBuffer [16]byte
|
||||||
|
buf.Must1(request.Read(destinationAddressBuffer[:]))
|
||||||
|
destination = parse16BytesIP(destinationAddressBuffer)
|
||||||
|
buf.Must(binary.Read(request, binary.BigEndian, &sequenceNumber))
|
||||||
|
buf.Must(binary.Read(request, binary.BigEndian, &ttl))
|
||||||
|
buf.Must(binary.Read(request, binary.BigEndian, &size))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IcmpConn) WritePingResponse(id uint16, sourceAddress netip.Addr, icmpType uint8, code uint8, sequenceNumber uint16) error {
|
||||||
|
response := buf.NewSize(2 + 16 + 1 + 1 + 2)
|
||||||
|
defer response.Release()
|
||||||
|
buf.Must(binary.Write(response, binary.BigEndian, id))
|
||||||
|
sourceAddressBytes := buildPaddingIP(sourceAddress)
|
||||||
|
buf.Must1(response.Write(sourceAddressBytes[:]))
|
||||||
|
buf.Must(binary.Write(response, binary.BigEndian, icmpType))
|
||||||
|
buf.Must(binary.Write(response, binary.BigEndian, code))
|
||||||
|
buf.Must(binary.Write(response, binary.BigEndian, sequenceNumber))
|
||||||
|
return buf.Error(i.writeFlush(response.Bytes()))
|
||||||
|
}
|
||||||
280
transport/trusttunnel/packet.go
Normal file
280
transport/trusttunnel/packet.go
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/metacubex/sing/common"
|
||||||
|
"github.com/metacubex/sing/common/buf"
|
||||||
|
E "github.com/metacubex/sing/common/exceptions"
|
||||||
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
|
N "github.com/metacubex/sing/common/network"
|
||||||
|
"github.com/metacubex/sing/common/rw"
|
||||||
|
)
|
||||||
|
|
||||||
|
type packetConn struct {
|
||||||
|
httpConn
|
||||||
|
readWaitOptions N.ReadWaitOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *packetConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
|
||||||
|
c.readWaitOptions = options
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ N.NetPacketConn = (*clientPacketConn)(nil)
|
||||||
|
_ N.FrontHeadroom = (*clientPacketConn)(nil)
|
||||||
|
_ N.PacketReadWaiter = (*clientPacketConn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type clientPacketConn struct {
|
||||||
|
packetConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) FrontHeadroom() int {
|
||||||
|
return 4 + 16 + 2 + 16 + 2 + 1 + math.MaxUint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
|
||||||
|
buffer = u.readWaitOptions.NewPacketBuffer()
|
||||||
|
destination, err = u.ReadPacket(buffer)
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
return nil, M.Socksaddr{}, err
|
||||||
|
}
|
||||||
|
u.readWaitOptions.PostReturn(buffer)
|
||||||
|
return buffer, destination, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
err = u.waitCreated()
|
||||||
|
if err != nil {
|
||||||
|
return M.Socksaddr{}, err
|
||||||
|
}
|
||||||
|
return u.readPacketFromServer(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
buffer := buf.With(p)
|
||||||
|
destination, err := u.ReadPacket(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
return buffer.Len(), destination.UDPAddr(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
return u.writePacketToServer(buffer, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) readPacketFromServer(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
header := buf.NewSize(4 + 16 + 2 + 16 + 2)
|
||||||
|
defer header.Release()
|
||||||
|
_, err = header.ReadFullFrom(u.body, header.Cap())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var length uint32
|
||||||
|
common.Must(binary.Read(header, binary.BigEndian, &length))
|
||||||
|
var sourceAddressBuffer [16]byte
|
||||||
|
common.Must1(header.Read(sourceAddressBuffer[:]))
|
||||||
|
destination.Addr = parse16BytesIP(sourceAddressBuffer)
|
||||||
|
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
|
||||||
|
common.Must(rw.SkipN(header, 16+2)) // To local address:port
|
||||||
|
payloadLen := int(length) - (16 + 2 + 16 + 2)
|
||||||
|
if payloadLen < 0 {
|
||||||
|
return M.Socksaddr{}, E.New("invalid udp length: ", length)
|
||||||
|
}
|
||||||
|
_, err = buffer.ReadFullFrom(u.body, payloadLen)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *clientPacketConn) writePacketToServer(buffer *buf.Buffer, source M.Socksaddr) error {
|
||||||
|
defer buffer.Release()
|
||||||
|
if !source.IsIP() {
|
||||||
|
return E.New("only support IP")
|
||||||
|
}
|
||||||
|
appName := AppName
|
||||||
|
if len(appName) > math.MaxUint8 {
|
||||||
|
appName = appName[:math.MaxUint8]
|
||||||
|
}
|
||||||
|
payloadLen := buffer.Len()
|
||||||
|
headerLen := 4 + 16 + 2 + 16 + 2 + 1 + len(appName)
|
||||||
|
lengthField := uint32(16 + 2 + 16 + 2 + 1 + len(appName) + payloadLen)
|
||||||
|
destinationAddress := buildPaddingIP(source.Addr)
|
||||||
|
|
||||||
|
var (
|
||||||
|
header *buf.Buffer
|
||||||
|
headerInBuffer bool
|
||||||
|
)
|
||||||
|
if buffer.Start() >= headerLen {
|
||||||
|
headerBytes := buffer.ExtendHeader(headerLen)
|
||||||
|
header = buf.With(headerBytes)
|
||||||
|
headerInBuffer = true
|
||||||
|
} else {
|
||||||
|
header = buf.NewSize(headerLen)
|
||||||
|
defer header.Release()
|
||||||
|
}
|
||||||
|
common.Must(binary.Write(header, binary.BigEndian, lengthField))
|
||||||
|
common.Must(header.WriteZeroN(16 + 2)) // Source address:port (unknown)
|
||||||
|
common.Must1(header.Write(destinationAddress[:]))
|
||||||
|
common.Must(binary.Write(header, binary.BigEndian, source.Port))
|
||||||
|
common.Must(binary.Write(header, binary.BigEndian, uint8(len(appName))))
|
||||||
|
common.Must1(header.WriteString(appName))
|
||||||
|
if !headerInBuffer {
|
||||||
|
_, err := u.writer.Write(header.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := u.writer.Write(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.flusher != nil {
|
||||||
|
u.flusher.Flush()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ N.NetPacketConn = (*serverPacketConn)(nil)
|
||||||
|
_ N.FrontHeadroom = (*serverPacketConn)(nil)
|
||||||
|
_ N.PacketReadWaiter = (*serverPacketConn)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type serverPacketConn struct {
|
||||||
|
packetConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) FrontHeadroom() int {
|
||||||
|
return 4 + 16 + 2 + 16 + 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) WaitReadPacket() (buffer *buf.Buffer, destination M.Socksaddr, err error) {
|
||||||
|
buffer = u.readWaitOptions.NewPacketBuffer()
|
||||||
|
destination, err = u.ReadPacket(buffer)
|
||||||
|
if err != nil {
|
||||||
|
buffer.Release()
|
||||||
|
return nil, M.Socksaddr{}, err
|
||||||
|
}
|
||||||
|
u.readWaitOptions.PostReturn(buffer)
|
||||||
|
return buffer, destination, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
err = u.waitCreated()
|
||||||
|
if err != nil {
|
||||||
|
return M.Socksaddr{}, err
|
||||||
|
}
|
||||||
|
return u.readPacketFromClient(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||||
|
buffer := buf.With(p)
|
||||||
|
destination, err := u.ReadPacket(buffer)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
return buffer.Len(), destination.UDPAddr(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
return u.writePacketToClient(buffer, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||||
|
err = u.WritePacket(buf.As(p), M.SocksaddrFromNet(addr))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) readPacketFromClient(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
header := buf.NewSize(4 + 16 + 2 + 16 + 2 + 1)
|
||||||
|
defer header.Release()
|
||||||
|
_, err = header.ReadFullFrom(u.body, header.Cap())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var length uint32
|
||||||
|
common.Must(binary.Read(header, binary.BigEndian, &length))
|
||||||
|
var sourceAddressBuffer [16]byte
|
||||||
|
common.Must1(header.Read(sourceAddressBuffer[:]))
|
||||||
|
var sourcePort uint16
|
||||||
|
common.Must(binary.Read(header, binary.BigEndian, &sourcePort))
|
||||||
|
_ = sourcePort
|
||||||
|
var destinationAddressBuffer [16]byte
|
||||||
|
common.Must1(header.Read(destinationAddressBuffer[:]))
|
||||||
|
destination.Addr = parse16BytesIP(destinationAddressBuffer)
|
||||||
|
common.Must(binary.Read(header, binary.BigEndian, &destination.Port))
|
||||||
|
var appNameLen uint8
|
||||||
|
common.Must(binary.Read(header, binary.BigEndian, &appNameLen))
|
||||||
|
if appNameLen > 0 {
|
||||||
|
err = rw.SkipN(u.body, int(appNameLen))
|
||||||
|
if err != nil {
|
||||||
|
return M.Socksaddr{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payloadLen := int(length) - (16 + 2 + 16 + 2 + 1 + int(appNameLen))
|
||||||
|
if payloadLen < 0 {
|
||||||
|
return M.Socksaddr{}, E.New("invalid udp length: ", length)
|
||||||
|
}
|
||||||
|
_, err = buffer.ReadFullFrom(u.body, payloadLen)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *serverPacketConn) writePacketToClient(buffer *buf.Buffer, source M.Socksaddr) error {
|
||||||
|
defer buffer.Release()
|
||||||
|
if !source.IsIP() {
|
||||||
|
return E.New("only support IP")
|
||||||
|
}
|
||||||
|
payloadLen := buffer.Len()
|
||||||
|
headerLen := 4 + 16 + 2 + 16 + 2
|
||||||
|
lengthField := uint32(16 + 2 + 16 + 2 + payloadLen)
|
||||||
|
sourceAddress := buildPaddingIP(source.Addr)
|
||||||
|
var destinationAddress [16]byte
|
||||||
|
var destinationPort uint16
|
||||||
|
var (
|
||||||
|
header *buf.Buffer
|
||||||
|
headerInBuffer bool
|
||||||
|
)
|
||||||
|
if buffer.Start() >= headerLen {
|
||||||
|
headerBytes := buffer.ExtendHeader(headerLen)
|
||||||
|
header = buf.With(headerBytes)
|
||||||
|
headerInBuffer = true
|
||||||
|
} else {
|
||||||
|
header = buf.NewSize(headerLen)
|
||||||
|
defer header.Release()
|
||||||
|
}
|
||||||
|
common.Must(binary.Write(header, binary.BigEndian, lengthField))
|
||||||
|
common.Must1(header.Write(sourceAddress[:]))
|
||||||
|
common.Must(binary.Write(header, binary.BigEndian, source.Port))
|
||||||
|
common.Must1(header.Write(destinationAddress[:]))
|
||||||
|
common.Must(binary.Write(header, binary.BigEndian, destinationPort))
|
||||||
|
if !headerInBuffer {
|
||||||
|
_, err := u.writer.Write(header.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, err := u.writer.Write(buffer.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if u.flusher != nil {
|
||||||
|
u.flusher.Flush()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
178
transport/trusttunnel/protocol.go
Normal file
178
transport/trusttunnel/protocol.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
C "github.com/metacubex/mihomo/constant"
|
||||||
|
"github.com/metacubex/mihomo/transport/gun"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
UDPMagicAddress = "_udp2"
|
||||||
|
ICMPMagicAddress = "_icmp"
|
||||||
|
HealthCheckMagicAddress = "_check"
|
||||||
|
|
||||||
|
DefaultQuicStreamReceiveWindow = 131072 // Chrome's default
|
||||||
|
DefaultConnectionTimeout = 30 * time.Second
|
||||||
|
DefaultHealthCheckTimeout = 7 * time.Second
|
||||||
|
DefaultQuicMaxIdleTimeout = 2 * (DefaultConnectionTimeout + DefaultHealthCheckTimeout)
|
||||||
|
DefaultSessionTimeout = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
AppName = C.Name
|
||||||
|
Version = C.Version
|
||||||
|
|
||||||
|
// TCPUserAgent is user-agent for TCP connections.
|
||||||
|
// Format: <platform> <app_name>
|
||||||
|
TCPUserAgent = runtime.GOOS + " " + AppName + "/" + Version
|
||||||
|
|
||||||
|
// UDPUserAgent is user-agent for UDP multiplexinh.
|
||||||
|
// Format: <platform> _udp2
|
||||||
|
UDPUserAgent = runtime.GOOS + " " + UDPMagicAddress
|
||||||
|
|
||||||
|
// ICMPUserAgent is user-agent for ICMP multiplexinh.
|
||||||
|
// Format: <platform> _icmp
|
||||||
|
ICMPUserAgent = runtime.GOOS + " " + ICMPMagicAddress
|
||||||
|
|
||||||
|
HealthCheckUserAgent = runtime.GOOS
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildAuth(username string, password string) string {
|
||||||
|
return "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBasicAuth parses an HTTP Basic Authentication strinh.
|
||||||
|
// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
||||||
|
func parseBasicAuth(auth string) (username, password string, ok bool) {
|
||||||
|
const prefix = "Basic "
|
||||||
|
// Case insensitive prefix match. See Issue 22736.
|
||||||
|
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
|
||||||
|
if err != nil {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
cs := string(c)
|
||||||
|
username, password, ok = strings.Cut(cs, ":")
|
||||||
|
if !ok {
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
return username, password, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse16BytesIP(buffer [16]byte) netip.Addr {
|
||||||
|
var zeroPrefix [12]byte
|
||||||
|
isIPv4 := bytes.HasPrefix(buffer[:], zeroPrefix[:])
|
||||||
|
// Special: check ::1
|
||||||
|
isIPv4 = isIPv4 && !(buffer[12] == 0 && buffer[13] == 0 && buffer[14] == 0 && buffer[15] == 1)
|
||||||
|
if isIPv4 {
|
||||||
|
return netip.AddrFrom4([4]byte(buffer[12:16]))
|
||||||
|
}
|
||||||
|
return netip.AddrFrom16(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPaddingIP(addr netip.Addr) (buffer [16]byte) {
|
||||||
|
if addr.Is6() {
|
||||||
|
return addr.As16()
|
||||||
|
}
|
||||||
|
ipv4 := addr.As4()
|
||||||
|
copy(buffer[12:16], ipv4[:])
|
||||||
|
return buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
type httpConn struct {
|
||||||
|
writer io.Writer
|
||||||
|
flusher http.Flusher
|
||||||
|
body io.ReadCloser
|
||||||
|
created chan struct{}
|
||||||
|
createErr error
|
||||||
|
gun.NetAddr
|
||||||
|
|
||||||
|
// deadlines
|
||||||
|
deadline *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpConn) setUp(body io.ReadCloser, err error) {
|
||||||
|
h.body = body
|
||||||
|
h.createErr = err
|
||||||
|
close(h.created)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpConn) waitCreated() error {
|
||||||
|
if h.body != nil || h.createErr != nil {
|
||||||
|
return h.createErr
|
||||||
|
}
|
||||||
|
<-h.created
|
||||||
|
return h.createErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpConn) Close() error {
|
||||||
|
var errorArr []error
|
||||||
|
if closer, ok := h.writer.(io.Closer); ok {
|
||||||
|
errorArr = append(errorArr, closer.Close())
|
||||||
|
}
|
||||||
|
if h.body != nil {
|
||||||
|
errorArr = append(errorArr, h.body.Close())
|
||||||
|
}
|
||||||
|
return errors.Join(errorArr...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpConn) writeFlush(p []byte) (n int, err error) {
|
||||||
|
n, err = h.writer.Write(p)
|
||||||
|
if h.flusher != nil {
|
||||||
|
h.flusher.Flush()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *httpConn) SetReadDeadline(t time.Time) error { return h.SetDeadline(t) }
|
||||||
|
func (h *httpConn) SetWriteDeadline(t time.Time) error { return h.SetDeadline(t) }
|
||||||
|
|
||||||
|
func (h *httpConn) SetDeadline(t time.Time) error {
|
||||||
|
if t.IsZero() {
|
||||||
|
if h.deadline != nil {
|
||||||
|
h.deadline.Stop()
|
||||||
|
h.deadline = nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
d := time.Until(t)
|
||||||
|
if h.deadline != nil {
|
||||||
|
h.deadline.Reset(d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
h.deadline = time.AfterFunc(d, func() {
|
||||||
|
h.Close()
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ net.Conn = (*tcpConn)(nil)
|
||||||
|
|
||||||
|
type tcpConn struct {
|
||||||
|
httpConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tcpConn) Read(b []byte) (n int, err error) {
|
||||||
|
err = t.waitCreated()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
n, err = t.body.Read(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *tcpConn) Write(b []byte) (int, error) {
|
||||||
|
return t.writeFlush(b)
|
||||||
|
}
|
||||||
85
transport/trusttunnel/quic.go
Normal file
85
transport/trusttunnel/quic.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/metacubex/mihomo/transport/tuic/common"
|
||||||
|
"github.com/metacubex/mihomo/transport/vmess"
|
||||||
|
|
||||||
|
"github.com/metacubex/http"
|
||||||
|
"github.com/metacubex/quic-go"
|
||||||
|
"github.com/metacubex/quic-go/http3"
|
||||||
|
"github.com/metacubex/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlName string, cwnd int) error {
|
||||||
|
stdConfig, err := tlsConfig.ToStdConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.roundTripper = &http3.Transport{
|
||||||
|
TLSClientConfig: stdConfig,
|
||||||
|
QUICConfig: &quic.Config{
|
||||||
|
Versions: []quic.Version{quic.Version1},
|
||||||
|
MaxIdleTimeout: DefaultQuicMaxIdleTimeout,
|
||||||
|
InitialStreamReceiveWindow: DefaultQuicStreamReceiveWindow,
|
||||||
|
DisablePathMTUDiscovery: !(runtime.GOOS == "windows" || runtime.GOOS == "linux" || runtime.GOOS == "android" || runtime.GOOS == "darwin"),
|
||||||
|
Allow0RTT: false,
|
||||||
|
},
|
||||||
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||||
|
addrPort, err := c.resolv(ctx, c.server)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tlsConfig.ECH.ClientHandle(ctx, tlsCfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
packetConn, err := c.dialer.ListenPacket(ctx, "udp", "", addrPort)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
quicConn, err := quic.DialEarly(ctx, packetConn, net.UDPAddrFromAddrPort(addrPort), tlsCfg, cfg)
|
||||||
|
if err != nil {
|
||||||
|
_ = packetConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
common.SetCongestionController(quicConn, congestionControlName, cwnd)
|
||||||
|
return quicConn, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) configHTTP3Server(tlsConfig *tls.Config, udpConn net.PacketConn) error {
|
||||||
|
tlsConfig = http3.ConfigureTLSConfig(tlsConfig)
|
||||||
|
quicListener, err := quic.ListenEarly(udpConn, tlsConfig, &quic.Config{
|
||||||
|
Versions: []quic.Version{quic.Version1},
|
||||||
|
MaxIdleTimeout: DefaultQuicMaxIdleTimeout,
|
||||||
|
MaxIncomingStreams: 1 << 60,
|
||||||
|
Allow0RTT: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h3Server := &http3.Server{
|
||||||
|
Handler: s,
|
||||||
|
IdleTimeout: DefaultSessionTimeout,
|
||||||
|
ConnContext: func(ctx context.Context, conn *quic.Conn) context.Context {
|
||||||
|
common.SetCongestionController(conn, s.quicCongestionControl, s.quicCwnd)
|
||||||
|
return ctx
|
||||||
|
},
|
||||||
|
}
|
||||||
|
s.h3Server = h3Server
|
||||||
|
s.udpConn = udpConn
|
||||||
|
go func() {
|
||||||
|
sErr := h3Server.ServeListener(quicListener)
|
||||||
|
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
|
||||||
|
s.logger.ErrorContext(s.ctx, "HTTP3 server close: ", sErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
250
transport/trusttunnel/service.go
Normal file
250
transport/trusttunnel/service.go
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
package trusttunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/metacubex/http"
|
||||||
|
"github.com/metacubex/http/h2c"
|
||||||
|
"github.com/metacubex/quic-go/http3"
|
||||||
|
"github.com/metacubex/sing/common"
|
||||||
|
"github.com/metacubex/sing/common/auth"
|
||||||
|
"github.com/metacubex/sing/common/buf"
|
||||||
|
"github.com/metacubex/sing/common/bufio"
|
||||||
|
E "github.com/metacubex/sing/common/exceptions"
|
||||||
|
"github.com/metacubex/sing/common/logger"
|
||||||
|
M "github.com/metacubex/sing/common/metadata"
|
||||||
|
N "github.com/metacubex/sing/common/network"
|
||||||
|
"github.com/metacubex/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
N.TCPConnectionHandler
|
||||||
|
N.UDPConnectionHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
type ICMPHandler interface {
|
||||||
|
NewICMPConnection(ctx context.Context, conn *IcmpConn)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceOptions struct {
|
||||||
|
Ctx context.Context
|
||||||
|
Logger logger.ContextLogger
|
||||||
|
Handler Handler
|
||||||
|
ICMPHandler ICMPHandler
|
||||||
|
QUICCongestionControl string
|
||||||
|
QUICCwnd int
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
ctx context.Context
|
||||||
|
logger logger.ContextLogger
|
||||||
|
users map[string]string
|
||||||
|
handler Handler
|
||||||
|
icmpHandler ICMPHandler
|
||||||
|
quicCongestionControl string
|
||||||
|
quicCwnd int
|
||||||
|
httpServer *http.Server
|
||||||
|
h2Server *http.Http2Server
|
||||||
|
h3Server *http3.Server
|
||||||
|
tcpListener net.Listener
|
||||||
|
tlsListener net.Listener
|
||||||
|
udpConn net.PacketConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(options ServiceOptions) *Service {
|
||||||
|
return &Service{
|
||||||
|
ctx: options.Ctx,
|
||||||
|
logger: options.Logger,
|
||||||
|
handler: options.Handler,
|
||||||
|
icmpHandler: options.ICMPHandler,
|
||||||
|
quicCongestionControl: options.QUICCongestionControl,
|
||||||
|
quicCwnd: options.QUICCwnd,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Start(tcpListener net.Listener, udpConn net.PacketConn, tlsConfig *tls.Config) error {
|
||||||
|
if tcpListener != nil {
|
||||||
|
h2Server := &http.Http2Server{}
|
||||||
|
s.httpServer = &http.Server{
|
||||||
|
Handler: h2c.NewHandler(s, h2Server),
|
||||||
|
IdleTimeout: DefaultSessionTimeout,
|
||||||
|
BaseContext: func(net.Listener) context.Context {
|
||||||
|
return s.ctx
|
||||||
|
},
|
||||||
|
}
|
||||||
|
err := http.Http2ConfigureServer(s.httpServer, h2Server)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.h2Server = h2Server
|
||||||
|
listener := tcpListener
|
||||||
|
s.tcpListener = tcpListener
|
||||||
|
if tlsConfig != nil {
|
||||||
|
listener = tls.NewListener(listener, tlsConfig)
|
||||||
|
s.tlsListener = listener
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
sErr := s.httpServer.Serve(listener)
|
||||||
|
if sErr != nil && !errors.Is(sErr, http.ErrServerClosed) {
|
||||||
|
s.logger.ErrorContext(s.ctx, "HTTP server close: ", sErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
if udpConn != nil {
|
||||||
|
err := s.configHTTP3Server(tlsConfig, udpConn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateUsers(users map[string]string) {
|
||||||
|
s.users = users
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Close() error {
|
||||||
|
var shutdownErr error
|
||||||
|
if s.httpServer != nil {
|
||||||
|
const shutdownTimeout = 5 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(s.ctx, shutdownTimeout)
|
||||||
|
shutdownErr = s.httpServer.Shutdown(ctx)
|
||||||
|
cancel()
|
||||||
|
if errors.Is(shutdownErr, http.ErrServerClosed) {
|
||||||
|
shutdownErr = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closeErr := common.Close(
|
||||||
|
common.PtrOrNil(s.httpServer),
|
||||||
|
s.tlsListener,
|
||||||
|
s.tcpListener,
|
||||||
|
common.PtrOrNil(s.h3Server),
|
||||||
|
s.udpConn,
|
||||||
|
)
|
||||||
|
return E.Errors(shutdownErr, closeErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
authorization := request.Header.Get("Proxy-Authorization")
|
||||||
|
username, loaded := s.verify(authorization)
|
||||||
|
if !loaded {
|
||||||
|
writer.WriteHeader(http.StatusProxyAuthRequired)
|
||||||
|
s.badRequest(request.Context(), request, E.New("authorization failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if request.Method != http.MethodConnect {
|
||||||
|
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
s.badRequest(request.Context(), request, E.New("unexpected HTTP method ", request.Method))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx := request.Context()
|
||||||
|
ctx = auth.ContextWithUser(ctx, username)
|
||||||
|
s.logger.DebugContext(ctx, "[", username, "] ", "request from ", request.RemoteAddr)
|
||||||
|
s.logger.DebugContext(ctx, "[", username, "] ", "request to ", request.Host)
|
||||||
|
switch request.Host {
|
||||||
|
case UDPMagicAddress:
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
flusher, isFlusher := writer.(http.Flusher)
|
||||||
|
if isFlusher {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
conn := &serverPacketConn{
|
||||||
|
packetConn: packetConn{
|
||||||
|
httpConn: httpConn{
|
||||||
|
writer: writer,
|
||||||
|
flusher: flusher,
|
||||||
|
created: make(chan struct{}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
conn.SetAddrFromRequest(request)
|
||||||
|
conn.setUp(request.Body, nil)
|
||||||
|
firstPacket := buf.NewPacket()
|
||||||
|
destination, err := conn.ReadPacket(firstPacket)
|
||||||
|
if err != nil {
|
||||||
|
firstPacket.Release()
|
||||||
|
_ = conn.Close()
|
||||||
|
s.logger.ErrorContext(ctx, E.Cause(err, "read first packet of ", request.RemoteAddr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
destination = destination.Unwrap()
|
||||||
|
cachedConn := bufio.NewCachedPacketConn(conn, firstPacket, destination)
|
||||||
|
_ = s.handler.NewPacketConnection(ctx, cachedConn, M.Metadata{
|
||||||
|
Protocol: "trusttunnel",
|
||||||
|
Source: M.ParseSocksaddr(request.RemoteAddr),
|
||||||
|
Destination: destination,
|
||||||
|
})
|
||||||
|
case ICMPMagicAddress:
|
||||||
|
flusher, isFlusher := writer.(http.Flusher)
|
||||||
|
if s.icmpHandler == nil {
|
||||||
|
writer.WriteHeader(http.StatusNotImplemented)
|
||||||
|
if isFlusher {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
_ = request.Body.Close()
|
||||||
|
} else {
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
if isFlusher {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
conn := &IcmpConn{
|
||||||
|
httpConn{
|
||||||
|
writer: writer,
|
||||||
|
flusher: flusher,
|
||||||
|
created: make(chan struct{}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
conn.SetAddrFromRequest(request)
|
||||||
|
conn.setUp(request.Body, nil)
|
||||||
|
s.icmpHandler.NewICMPConnection(ctx, conn)
|
||||||
|
}
|
||||||
|
case HealthCheckMagicAddress:
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
if flusher, isFlusher := writer.(http.Flusher); isFlusher {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
_ = request.Body.Close()
|
||||||
|
default:
|
||||||
|
writer.WriteHeader(http.StatusOK)
|
||||||
|
flusher, isFlusher := writer.(http.Flusher)
|
||||||
|
if isFlusher {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
conn := &tcpConn{
|
||||||
|
httpConn{
|
||||||
|
writer: writer,
|
||||||
|
flusher: flusher,
|
||||||
|
created: make(chan struct{}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
conn.SetAddrFromRequest(request)
|
||||||
|
conn.setUp(request.Body, nil)
|
||||||
|
_ = s.handler.NewConnection(ctx, conn, M.Metadata{
|
||||||
|
Protocol: "trusttunnel",
|
||||||
|
Source: M.ParseSocksaddr(request.RemoteAddr),
|
||||||
|
Destination: M.ParseSocksaddr(request.Host).Unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) verify(authorization string) (username string, loaded bool) {
|
||||||
|
username, password, loaded := parseBasicAuth(authorization)
|
||||||
|
if !loaded {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
recordedPassword, loaded := s.users[username]
|
||||||
|
if !loaded {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if password != recordedPassword {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return username, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) badRequest(ctx context.Context, request *http.Request, err error) {
|
||||||
|
s.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr))
|
||||||
|
}
|
||||||
@@ -24,12 +24,8 @@ type TLSConfig struct {
|
|||||||
Reality *tlsC.RealityConfig
|
Reality *tlsC.RealityConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ECHConfig struct {
|
func (cfg *TLSConfig) ToStdConfig() (*tls.Config, error) {
|
||||||
Enable bool
|
return ca.GetTLSConfig(ca.Option{
|
||||||
}
|
|
||||||
|
|
||||||
func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
|
|
||||||
tlsConfig, err := ca.GetTLSConfig(ca.Option{
|
|
||||||
TLSConfig: &tls.Config{
|
TLSConfig: &tls.Config{
|
||||||
ServerName: cfg.Host,
|
ServerName: cfg.Host,
|
||||||
InsecureSkipVerify: cfg.SkipCertVerify,
|
InsecureSkipVerify: cfg.SkipCertVerify,
|
||||||
@@ -39,6 +35,10 @@ func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn
|
|||||||
Certificate: cfg.Certificate,
|
Certificate: cfg.Certificate,
|
||||||
PrivateKey: cfg.PrivateKey,
|
PrivateKey: cfg.PrivateKey,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func StreamTLSConn(ctx context.Context, conn net.Conn, cfg *TLSConfig) (net.Conn, error) {
|
||||||
|
tlsConfig, err := cfg.ToStdConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user