Compare commits

...

64 commits

Author SHA1 Message Date
wwqgtxx
d5243adf89 chore: better global-client-fingerprint handle 2025-04-19 02:04:09 +08:00
wwqgtxx
6236cb1cf0 chore: cleanup trojan code 2025-04-19 01:32:55 +08:00
wwqgtxx
619c9dc0c6 chore: apply the default interface/mark of the dialer in the final stage 2025-04-18 20:16:51 +08:00
wwqgtxx
9c5067e519 action: disable MinGW's path conversion in test 2025-04-18 19:48:22 +08:00
wwqgtxx
feee9b320c chore: remove unneeded tls timeout in anytls 2025-04-18 16:59:53 +08:00
wwqgtxx
63e66f49ca chore: cleanup trojan code 2025-04-18 16:59:28 +08:00
wwqgtxx
bad61f918f fix: avoid panic in inbound test 2025-04-18 11:40:37 +08:00
wwqgtxx
69ce4d0f8c chore: speed up inbound test 2025-04-17 23:40:46 +08:00
wwqgtxx
b59f11f7ac chore: add singMux inbound test for shadowsocks/trojan/vless/vmess 2025-04-17 21:07:35 +08:00
wwqgtxx
30d90d49f0 chore: update option checks to use IsZeroOptions 2025-04-17 21:06:55 +08:00
wwqgtxx
76052b5b26 fix: grpc in trojan not apply client-fingerprint 2025-04-17 12:54:36 +08:00
wwqgtxx
7d7f5c8980 chore: add inbound test for tuic 2025-04-17 10:02:48 +08:00
wwqgtxx
e79465d306 chore: add inbound test for hysteria2 2025-04-17 09:26:12 +08:00
wwqgtxx
345d3d7052 chore: add inbound test for anytls 2025-04-17 09:01:26 +08:00
wwqgtxx
3d806b5e4c chore: add inbound test for shadowsocks/trojan 2025-04-17 01:36:14 +08:00
wwqgtxx
b5fcd1d1d1 fix: chacha8-ietf-poly1305 not work 2025-04-17 00:11:24 +08:00
wwqgtxx
b21b8ee046 fix: panic in ssr packet 2025-04-16 22:22:56 +08:00
wwqgtxx
d0d0c392d7 chore: add inbound test for vmess/vless 2025-04-16 20:44:48 +08:00
wwqgtxx
a75e570cca fix: vision conn read short buffer error 2025-04-16 20:38:10 +08:00
wwqgtxx
9e0889c02c fix: observable test 2025-04-16 13:16:11 +08:00
wwqgtxx
55cbbf7f41 fix: singledo test 2025-04-16 13:13:01 +08:00
wwqgtxx
664b134015 fix: websocket data losing 2025-04-16 13:02:50 +08:00
wwqgtxx
ba3c44a169 chore: code cleanup 2025-04-16 09:54:02 +08:00
wwqgtxx
dcb20e2824 fix: websocket server upgrade in golang1.20 2025-04-16 08:47:44 +08:00
wwqgtxx
3d2cb992fa fix: grpc outbound not apply ca fingerprint 2025-04-16 01:00:06 +08:00
wwqgtxx
984535f006 action: run tests on more platforms 2025-04-15 22:02:40 +08:00
wwqgtxx
8fa4e8122c chore: remove internal crypto/tls fork in reality server 2025-04-13 03:03:28 +08:00
wwqgtxx
7551c8a545 chore: remove unneed code 2025-04-12 23:42:57 +08:00
wwqgtxx
237e2edea4 chore: tun will add firewall rule for Profile ALL on windows system stack 2025-04-12 22:46:26 +08:00
wwqgtxx
fe01033efe chore: quic sniffer should use the exact length of crypto stream when assembling 2025-04-12 22:27:07 +08:00
wwqgtxx
84cd0ef688 chore: remove internal crypto/tls fork in shadowtls 2025-04-12 20:28:26 +08:00
wwqgtxx
cedb36df5f chore: using SetupContextForConn to reduce the DialContext cannot be cancelled 2025-04-12 11:19:03 +08:00
HiMetre
7a260f7bcf fix: udp dial support ip4p (#1377) 2025-04-11 09:20:58 +08:00
wwqgtxx
8085c68b6d chore: decrease direct using *net.TCPConn 2025-04-11 00:33:07 +08:00
wwqgtxx
dbb5b7db1c fix: SetupContextForConn should return context error to user 2025-04-11 00:03:46 +08:00
wwqgtxx
bfd06ebad0 chore: rebuild SetupContextForConn with context.AfterFunc 2025-04-10 01:29:55 +08:00
wwqgtxx
e8af058694 fix: websocketWithEarlyDataConn can't close underlay conn when is dialing or not dialed 2025-04-10 00:13:14 +08:00
wwqgtxx
487d7fa81f fix: panic under some stupid input config 2025-04-09 18:02:13 +08:00
wwqgtxx
4b15568a29 chore: cleanup metadata code 2025-04-09 18:02:13 +08:00
wwqgtxx
cac2bf72e1 chore: cleanup netip code 2025-04-09 18:02:13 +08:00
wwqgtxx
b2d2890866 chore: cleanup resolveUDPAddr code 2025-04-09 18:02:12 +08:00
anytls
8752f80595 fix: anytls stream read error (#1970)
Co-authored-by: anytls <anytls>
2025-04-09 18:02:12 +08:00
wwqgtxx
a6c0c02e0d chore: ignore interfaces not in IfOperStatusUp when fetch system dns server on windows 2025-04-09 18:02:12 +08:00
wwqgtxx
2acb0b71ee fix: tun IncludeInterface/ExcludeInterface priority 2025-04-08 19:20:29 +08:00
wwqgtxx
2a40eba0ca feat: tun add exclude-src-port,exclude-src-port-range,exclude-dst-port and exclude-dst-port-range on linux 2025-04-08 19:07:39 +08:00
okhowang
a22efd5c91
feat: add exclude port and exclude port range options (#1951)
Fixes #1769
2025-04-08 12:10:30 +08:00
wwqgtxx
9e8f4ada47 chore: better addr parsing 2025-04-06 10:43:21 +08:00
wwqgtxx
09c7ee0d12 fix: grpc server panic 2025-04-06 10:12:57 +08:00
wwqgtxx
2a08c44f51 action: fix run build on pull_request 2025-04-05 10:48:07 +08:00
wwqgtxx
190047c8c0 fix: grpc transport not apply dial timeout 2025-04-04 21:05:54 +08:00
wwqgtxx
24a9ff6d03 fix: disallow dialFunc be called after grpc transport has be closed 2025-04-04 13:33:00 +08:00
wwqgtxx
efa224373f fix: shut it down more aggressively in grpc transport closing 2025-04-04 11:54:19 +08:00
wwqgtxx
b0bd4f4caf fix: resources not released when hysteria2 verification failed 2025-04-04 11:12:08 +08:00
wwqgtxx
eaaccffcef fix: race in Single.Do 2025-04-04 10:55:16 +08:00
wwqgtxx
e81f3a97af fix: correctly implement references to proxies 2025-04-04 09:08:52 +08:00
wwqgtxx
323973f22f fix: converter judgment conditions 2025-04-04 00:22:52 +08:00
5aaee9
ed7533ca1a
fix: tproxy high cpu usage (#1957) 2025-04-03 23:52:19 +08:00
wwqgtxx
7de24e26b4 fix: StreamGunWithConn not synchronously close the incoming net.Conn 2025-04-03 23:41:24 +08:00
wwqgtxx
622d99d000 chore: rebuild outdated proxy auto close mechanism 2025-04-03 22:42:32 +08:00
wwqgtxx
7f1225b0c4 fix: grpc transport can't be closed 2025-04-03 22:41:05 +08:00
wwqgtxx
23ffe451f4 chore: using http/httptrace to get local/remoteAddr for grpc client 2025-04-03 19:47:49 +08:00
wwqgtxx
7b37fcfc8d fix: auto_redirect should only hijack DNS requests from local addresses 2025-04-02 23:47:34 +08:00
wwqgtxx
daa592c7f3 fix: converter panic 2025-04-02 21:13:46 +08:00
wwqgtxx
577f64a601 fix: X25519MLKEM768 does not work properly with reality 2025-04-02 14:39:07 +08:00
112 changed files with 2876 additions and 1175 deletions

View file

@ -14,7 +14,7 @@ on:
- Alpha - Alpha
tags: tags:
- "v*" - "v*"
pull_request_target: pull_request:
branches: branches:
- Alpha - Alpha
concurrency: concurrency:

115
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,115 @@
name: Test
on:
push:
paths-ignore:
- "docs/**"
- "README.md"
- ".github/ISSUE_TEMPLATE/**"
branches:
- Alpha
tags:
- "v*"
pull_request:
branches:
- Alpha
jobs:
test:
strategy:
matrix:
os:
- 'ubuntu-latest' # amd64 linux
- 'windows-latest' # amd64 windows
- 'macos-latest' # arm64 macos
- 'ubuntu-24.04-arm' # arm64 linux
- 'macos-13' # amd64 macos
go-version:
- '1.24'
- '1.23'
- '1.22'
- '1.21'
- '1.20'
fail-fast: false
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
env:
CGO_ENABLED: 0
GOTOOLCHAIN: local
# Fix mingw trying to be smart and converting paths https://github.com/moby/moby/issues/24029#issuecomment-250412919
MSYS_NO_PATHCONV: true
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.24.x
# that means after golang1.25 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.24/
# 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"
- name: Revert Golang1.24 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.24' }}
run: |
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/2a406dc9f1ea7323d6ca9fccb2fe9ddebb6b1cc8.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/7b1fd7d39c6be0185fbe1d929578ab372ac5c632.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/979d6d8bab3823ff572ace26767fd2ce3cf351ae.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/ac3e93c061779dfefc0dd13a5b6e6f764a25621e.diff | patch --verbose -p 1
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.23.x
# that means after golang1.24 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.23/
# 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"
- name: Revert Golang1.23 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.23' }}
run: |
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/9ac42137ef6730e8b7daca016ece831297a1d75b.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/21290de8a4c91408de7c2b5b68757b1e90af49dd.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/6a31d3fa8e47ddabc10bd97bff10d9a85f4cfb76.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/69e2eed6dd0f6d815ebf15797761c13f31213dd6.diff | patch --verbose -p 1
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
# this patch file only works on golang1.22.x
# that means after golang1.23 release it must be changed
# see: https://github.com/MetaCubeX/go/commits/release-branch.go1.22/
# 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"
- name: Revert Golang1.22 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.22' }}
run: |
cd $(go env GOROOT)
curl https://github.com/MetaCubeX/go/commit/9779155f18b6556a034f7bb79fb7fb2aad1e26a9.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/ef0606261340e608017860b423ffae5c1ce78239.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/7f83badcb925a7e743188041cb6e561fc9b5b642.diff | patch --verbose -p 1
curl https://github.com/MetaCubeX/go/commit/83ff9782e024cb328b690cbf0da4e7848a327f4f.diff | patch --verbose -p 1
# modify from https://github.com/restic/restic/issues/4636#issuecomment-1896455557
- name: Revert Golang1.21 commit for Windows7/8
if: ${{ runner.os == 'Windows' && matrix.go-version == '1.21' }}
run: |
cd $(go env GOROOT)
curl https://github.com/golang/go/commit/9e43850a3298a9b8b1162ba0033d4c53f8637571.diff | patch --verbose -R -p 1
- name: Test
run: go test ./... -v -count=1
- name: Test with tag with_gvisor
run: go test ./... -v -count=1 -tags "with_gvisor"

View file

@ -290,6 +290,7 @@ func (p *Proxy) URLTest(ctx context.Context, url string, expectedStatus utils.In
t = uint16(time.Since(start) / time.Millisecond) t = uint16(time.Since(start) / time.Millisecond)
return return
} }
func NewProxy(adapter C.ProxyAdapter) *Proxy { func NewProxy(adapter C.ProxyAdapter) *Proxy {
return &Proxy{ return &Proxy{
ProxyAdapter: adapter, ProxyAdapter: adapter,

View file

@ -7,7 +7,6 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/metacubex/mihomo/common/nnip"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/socks5"
) )
@ -21,13 +20,13 @@ func parseSocksAddr(target socks5.Addr) *C.Metadata {
metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".") metadata.Host = strings.TrimRight(string(target[2:2+target[1]]), ".")
metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1])) metadata.DstPort = uint16((int(target[2+target[1]]) << 8) | int(target[2+target[1]+1]))
case socks5.AtypIPv4: case socks5.AtypIPv4:
metadata.DstIP = nnip.IpToAddr(net.IP(target[1 : 1+net.IPv4len])) metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv4len])
metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1])) metadata.DstPort = uint16((int(target[1+net.IPv4len]) << 8) | int(target[1+net.IPv4len+1]))
case socks5.AtypIPv6: case socks5.AtypIPv6:
ip6, _ := netip.AddrFromSlice(target[1 : 1+net.IPv6len]) metadata.DstIP, _ = netip.AddrFromSlice(target[1 : 1+net.IPv6len])
metadata.DstIP = ip6.Unmap()
metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1])) metadata.DstPort = uint16((int(target[1+net.IPv6len]) << 8) | int(target[1+net.IPv6len+1]))
} }
metadata.DstIP = metadata.DstIP.Unmap()
return metadata return metadata
} }

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"net" "net"
"runtime"
"strconv" "strconv"
"time" "time"
@ -12,7 +11,6 @@ import (
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/anytls" "github.com/metacubex/mihomo/transport/anytls"
"github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/vmess"
@ -52,7 +50,7 @@ func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata, opts ...
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(CN.NewRefConn(c, t), t), nil return NewConn(c, t), nil
} }
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
@ -73,7 +71,7 @@ func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
metadata.DstIP = ip metadata.DstIP = ip
} }
destination := M.SocksaddrFromNet(metadata.UDPAddr()) destination := M.SocksaddrFromNet(metadata.UDPAddr())
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), t), nil return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
} }
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
@ -88,6 +86,11 @@ func (t *AnyTLS) ProxyInfo() C.ProxyInfo {
return info return info
} }
// Close implements C.ProxyAdapter
func (t *AnyTLS) Close() error {
return t.client.Close()
}
func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) { func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
@ -111,9 +114,6 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
if tlsConfig.Host == "" { if tlsConfig.Host == "" {
tlsConfig.Host = option.Server tlsConfig.Host = option.Server
} }
if tlsC.HaveGlobalFingerprint() && len(option.ClientFingerprint) == 0 {
tlsConfig.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
tOption.TLSConfig = tlsConfig tOption.TLSConfig = tlsConfig
outbound := &AnyTLS{ outbound := &AnyTLS{
@ -132,9 +132,6 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
option: &option, option: &option,
dialer: singDialer, dialer: singDialer,
} }
runtime.SetFinalizer(outbound, func(o *AnyTLS) {
_ = o.client.Close()
})
return outbound, nil return outbound, nil
} }

View file

@ -4,15 +4,23 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"net" "net"
"runtime"
"strings" "strings"
"sync"
"syscall" "syscall"
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"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log"
) )
type ProxyAdapter interface {
C.ProxyAdapter
DialOptions(opts ...dialer.Option) []dialer.Option
}
type Base struct { type Base struct {
name string name string
addr string addr string
@ -152,6 +160,10 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option {
return opts return opts
} }
func (b *Base) Close() error {
return nil
}
type BasicOption struct { type BasicOption struct {
TFO bool `proxy:"tfo,omitempty"` TFO bool `proxy:"tfo,omitempty"`
MPTCP bool `proxy:"mptcp,omitempty"` MPTCP bool `proxy:"mptcp,omitempty"`
@ -221,6 +233,10 @@ func (c *conn) ReaderReplaceable() bool {
return true return true
} }
func (c *conn) AddRef(ref any) {
c.ExtendedConn = N.NewRefConn(c.ExtendedConn, ref) // add ref for autoCloseProxyAdapter
}
func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn { func NewConn(c net.Conn, a C.ProxyAdapter) C.Conn {
if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn if _, ok := c.(syscall.Conn); !ok { // exclusion system conn like *net.TCPConn
c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly c = N.NewDeadlineConn(c) // most conn from outbound can't handle readDeadline correctly
@ -267,6 +283,10 @@ func (c *packetConn) ReaderReplaceable() bool {
return true return true
} }
func (c *packetConn) AddRef(ref any) {
c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter
}
func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn { func newPacketConn(pc net.PacketConn, a C.ProxyAdapter) C.PacketConn {
epc := N.NewEnhancePacketConn(pc) epc := N.NewEnhancePacketConn(pc)
if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn if _, ok := pc.(syscall.Conn); !ok { // exclusion system conn like *net.UDPConn
@ -286,3 +306,75 @@ func parseRemoteDestination(addr string) string {
} }
} }
} }
type AddRef interface {
AddRef(ref any)
}
type autoCloseProxyAdapter struct {
ProxyAdapter
closeOnce sync.Once
closeErr error
}
func (p *autoCloseProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
c, err := p.ProxyAdapter.DialContext(ctx, metadata, opts...)
if err != nil {
return nil, err
}
if c, ok := c.(AddRef); ok {
c.AddRef(p)
}
return c, nil
}
func (p *autoCloseProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
c, err := p.ProxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
if c, ok := c.(AddRef); ok {
c.AddRef(p)
}
return c, nil
}
func (p *autoCloseProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
pc, err := p.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
if err != nil {
return nil, err
}
if pc, ok := pc.(AddRef); ok {
pc.AddRef(p)
}
return pc, nil
}
func (p *autoCloseProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc, err := p.ProxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
if err != nil {
return nil, err
}
if pc, ok := pc.(AddRef); ok {
pc.AddRef(p)
}
return pc, nil
}
func (p *autoCloseProxyAdapter) Close() error {
p.closeOnce.Do(func() {
log.Debugln("Closing outdated proxy [%s]", p.Name())
runtime.SetFinalizer(p, nil)
p.closeErr = p.ProxyAdapter.Close()
})
return p.closeErr
}
func NewAutoCloseProxyAdapter(adapter ProxyAdapter) ProxyAdapter {
proxy := &autoCloseProxyAdapter{
ProxyAdapter: adapter,
}
// auto close ProxyAdapter
runtime.SetFinalizer(proxy, (*autoCloseProxyAdapter).Close)
return proxy
}

View file

@ -7,11 +7,11 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -51,7 +51,7 @@ func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Me
} }
} }
if err := h.shakeHand(metadata, c); err != nil { if err := h.shakeHandContext(ctx, c, metadata); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
@ -99,7 +99,12 @@ func (h *Http) ProxyInfo() C.ProxyInfo {
return info return info
} }
func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { func (h *Http) shakeHandContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
addr := metadata.RemoteAddress() addr := metadata.RemoteAddress()
HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n" HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n"
tempHeaders := map[string]string{ tempHeaders := map[string]string{
@ -123,13 +128,13 @@ func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
HeaderString += "\r\n" HeaderString += "\r\n"
_, err := rw.Write([]byte(HeaderString)) _, err = c.Write([]byte(HeaderString))
if err != nil { if err != nil {
return err return err
} }
resp, err := http.ReadResponse(bufio.NewReader(rw), nil) resp, err := http.ReadResponse(bufio.NewReader(c), nil)
if err != nil { if err != nil {
return err return err

View file

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"runtime"
"strconv" "strconv"
"time" "time"
@ -15,7 +14,6 @@ import (
"github.com/metacubex/quic-go/congestion" "github.com/metacubex/quic-go/congestion"
M "github.com/sagernet/sing/common/metadata" M "github.com/sagernet/sing/common/metadata"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -45,8 +43,6 @@ type Hysteria struct {
option *HysteriaOption option *HysteriaOption
client *core.Client client *core.Client
closeCh chan struct{} // for test
} }
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
@ -55,7 +51,7 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata, opts .
return nil, err return nil, err
} }
return NewConn(CN.NewRefConn(tcpConn, h), h), nil return NewConn(tcpConn, h), nil
} }
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
@ -63,7 +59,7 @@ func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newPacketConn(CN.NewRefPacketConn(&hyPacketConn{udpConn}, h), h), nil return newPacketConn(&hyPacketConn{udpConn}, h), nil
} }
func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer { func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.PacketDialer {
@ -82,7 +78,7 @@ func (h *Hysteria) genHdc(ctx context.Context, opts ...dialer.Option) utils.Pack
return cDialer.ListenPacket(ctx, network, "", rAddrPort) return cDialer.ListenPacket(ctx, network, "", rAddrPort)
}, },
remoteAddr: func(addr string) (net.Addr, error) { remoteAddr: func(addr string) (net.Addr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", addr, h.prefer) return resolveUDPAddr(ctx, "udp", addr, h.prefer)
}, },
} }
} }
@ -239,18 +235,16 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
option: &option, option: &option,
client: client, client: client,
} }
runtime.SetFinalizer(outbound, closeHysteria)
return outbound, nil return outbound, nil
} }
func closeHysteria(h *Hysteria) { // Close implements C.ProxyAdapter
func (h *Hysteria) Close() error {
if h.client != nil { if h.client != nil {
_ = h.client.Close() return h.client.Close()
}
if h.closeCh != nil {
close(h.closeCh)
} }
return nil
} }
type hyPacketConn struct { type hyPacketConn struct {

View file

@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net" "net"
"runtime"
"strconv" "strconv"
"time" "time"
@ -39,8 +38,6 @@ type Hysteria2 struct {
option *Hysteria2Option option *Hysteria2Option
client *hysteria2.Client client *hysteria2.Client
dialer proxydialer.SingDialer dialer proxydialer.SingDialer
closeCh chan struct{} // for test
} }
type Hysteria2Option struct { type Hysteria2Option struct {
@ -78,7 +75,7 @@ func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(CN.NewRefConn(c, h), h), nil return NewConn(c, h), nil
} }
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
@ -91,16 +88,15 @@ func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if pc == nil { if pc == nil {
return nil, errors.New("packetConn is nil") return nil, errors.New("packetConn is nil")
} }
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), h), h), nil return newPacketConn(CN.NewThreadSafePacketConn(pc), h), nil
} }
func closeHysteria2(h *Hysteria2) { // Close implements C.ProxyAdapter
func (h *Hysteria2) Close() error {
if h.client != nil { if h.client != nil {
_ = h.client.CloseWithError(errors.New("proxy removed")) return h.client.CloseWithError(errors.New("proxy removed"))
}
if h.closeCh != nil {
close(h.closeCh)
} }
return nil
} }
// ProxyInfo implements C.ProxyAdapter // ProxyInfo implements C.ProxyAdapter
@ -175,7 +171,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
CWND: option.CWND, CWND: option.CWND,
UdpMTU: option.UdpMTU, UdpMTU: option.UdpMTU,
ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) { ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) return resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
}, },
} }
@ -192,7 +188,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
}) })
if len(serverAddress) > 0 { if len(serverAddress) > 0 {
clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
} }
if option.HopInterval == 0 { if option.HopInterval == 0 {
@ -226,7 +222,6 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
client: client, client: client,
dialer: singDialer, dialer: singDialer,
} }
runtime.SetFinalizer(outbound, closeHysteria2)
return outbound, nil return outbound, nil
} }

View file

@ -1,38 +0,0 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteria2GC(t *testing.T) {
option := Hysteria2Option{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.HopInterval = 30
option.Password = "password"
option.Obfs = "salamander"
option.ObfsPassword = "password"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria2(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}

View file

@ -1,39 +0,0 @@
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestHysteriaGC(t *testing.T) {
option := HysteriaOption{}
option.Server = "127.0.0.1"
option.Ports = "200,204,401-429,501-503"
option.Protocol = "udp"
option.Up = "1Mbps"
option.Down = "1Mbps"
option.HopInterval = 30
option.Obfs = "salamander"
option.SNI = "example.com"
option.ALPN = []string{"h3"}
hy, err := NewHysteria(option)
if err != nil {
t.Error(err)
return
}
closeCh := make(chan struct{})
hy.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
hy = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"net" "net"
"runtime"
"strconv" "strconv"
"sync" "sync"
@ -62,7 +61,7 @@ func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
if err != nil { if err != nil {
return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err) return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err)
} }
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), m), nil return newPacketConn(CN.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
} }
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
@ -141,16 +140,17 @@ func NewMieru(option MieruOption) (*Mieru, error) {
option: &option, option: &option,
client: c, client: c,
} }
runtime.SetFinalizer(outbound, closeMieru)
return outbound, nil return outbound, nil
} }
func closeMieru(m *Mieru) { // Close implements C.ProxyAdapter
func (m *Mieru) Close() error {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
if m.client != nil && m.client.IsRunning() { if m.client != nil && m.client.IsRunning() {
m.client.Stop() return m.client.Stop()
} }
return nil
} }
func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec { func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec {

View file

@ -20,16 +20,19 @@ func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
config := new(tlsC.RealityConfig) config := new(tlsC.RealityConfig)
const x25519ScalarSize = 32 const x25519ScalarSize = 32
var publicKey [x25519ScalarSize]byte publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)
n, err := base64.RawURLEncoding.Decode(publicKey[:], []byte(o.PublicKey)) if err != nil || len(publicKey) != x25519ScalarSize {
if err != nil || n != x25519ScalarSize {
return nil, errors.New("invalid REALITY public key") return nil, errors.New("invalid REALITY public key")
} }
config.PublicKey, err = ecdh.X25519().NewPublicKey(publicKey[:]) config.PublicKey, err = ecdh.X25519().NewPublicKey(publicKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("fail to create REALITY public key: %w", err) return nil, fmt.Errorf("fail to create REALITY public key: %w", err)
} }
n := hex.DecodedLen(len(o.ShortID))
if n > tlsC.RealityMaxShortIDLen {
return nil, errors.New("invalid REALITY short id")
}
n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID)) n, err = hex.Decode(config.ShortID[:], []byte(o.ShortID))
if err != nil || n > tlsC.RealityMaxShortIDLen { if err != nil || n > tlsC.RealityMaxShortIDLen {
return nil, errors.New("invalid REALITY short ID") return nil, errors.New("invalid REALITY short ID")

View file

@ -100,7 +100,7 @@ type restlsOption struct {
} }
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
useEarly := false useEarly := false
switch ss.obfsMode { switch ss.obfsMode {
case "tls": case "tls":
@ -109,7 +109,6 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
_, port, _ := net.SplitHostPort(ss.addr) _, port, _ := net.SplitHostPort(ss.addr)
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port) c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
case "websocket": case "websocket":
var err error
if ss.v2rayOption != nil { if ss.v2rayOption != nil {
c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption) c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
} else if ss.gostOption != nil { } else if ss.gostOption != nil {
@ -121,14 +120,12 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
} }
case shadowtls.Mode: case shadowtls.Mode:
var err error
c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption) c, err = shadowtls.NewShadowTLS(ctx, c, ss.shadowTLSOption)
if err != nil { if err != nil {
return nil, err return nil, err
} }
useEarly = true useEarly = true
case restls.Mode: case restls.Mode:
var err error
c, err = restls.NewRestls(ctx, c, ss.restlsConfig) c, err = restls.NewRestls(ctx, c, ss.restlsConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err) return nil, fmt.Errorf("%s (restls) connect error: %w", ss.addr, err)
@ -136,6 +133,12 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
useEarly = true useEarly = true
} }
useEarly = useEarly || N.NeedHandshake(c) useEarly = useEarly || N.NeedHandshake(c)
if !useEarly {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
}
if metadata.NetWork == C.UDP && ss.option.UDPOverTCP { if metadata.NetWork == C.UDP && ss.option.UDPOverTCP {
uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion)) uotDestination := uot.RequestDestination(uint8(ss.option.UDPOverTCPVersion))
if useEarly { if useEarly {
@ -197,7 +200,7 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial
return nil, err return nil, err
} }
} }
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ss.addr, ss.prefer) addr, err := resolveUDPAddr(ctx, "udp", ss.addr, ss.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -42,12 +42,15 @@ type ShadowSocksROption struct {
} }
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (ssr *ShadowSocksR) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
c = ssr.obfs.StreamConn(c) c = ssr.obfs.StreamConn(c)
c = ssr.cipher.StreamConn(c) c = ssr.cipher.StreamConn(c)
var ( var (
iv []byte iv []byte
err error
) )
switch conn := c.(type) { switch conn := c.(type) {
case *shadowstream.Conn: case *shadowstream.Conn:
@ -102,7 +105,7 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
return nil, err return nil, err
} }
} }
addr, err := resolveUDPAddrWithPrefer(ctx, "udp", ssr.addr, ssr.prefer) addr, err := resolveUDPAddr(ctx, "udp", ssr.addr, ssr.prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -3,7 +3,6 @@ package outbound
import ( import (
"context" "context"
"errors" "errors"
"runtime"
CN "github.com/metacubex/mihomo/common/net" CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
@ -18,8 +17,7 @@ import (
) )
type SingMux struct { type SingMux struct {
C.ProxyAdapter ProxyAdapter
base ProxyBase
client *mux.Client client *mux.Client
dialer proxydialer.SingDialer dialer proxydialer.SingDialer
onlyTcp bool onlyTcp bool
@ -43,25 +41,21 @@ type BrutalOption struct {
Down string `proxy:"down,omitempty"` Down string `proxy:"down,omitempty"`
} }
type ProxyBase interface {
DialOptions(opts ...dialer.Option) []dialer.Option
}
func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (s *SingMux) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
options := s.base.DialOptions(opts...) options := s.ProxyAdapter.DialOptions(opts...)
s.dialer.SetDialer(dialer.NewDialer(options...)) s.dialer.SetDialer(dialer.NewDialer(options...))
c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) c, err := s.client.DialContext(ctx, "tcp", M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return NewConn(CN.NewRefConn(c, s), s.ProxyAdapter), err return NewConn(c, s), err
} }
func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
if s.onlyTcp { if s.onlyTcp {
return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...) return s.ProxyAdapter.ListenPacketContext(ctx, metadata, opts...)
} }
options := s.base.DialOptions(opts...) options := s.ProxyAdapter.DialOptions(opts...)
s.dialer.SetDialer(dialer.NewDialer(options...)) s.dialer.SetDialer(dialer.NewDialer(options...))
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr // sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr
@ -80,7 +74,7 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
if pc == nil { if pc == nil {
return nil, E.New("packetConn is nil") return nil, E.New("packetConn is nil")
} }
return newPacketConn(CN.NewRefPacketConn(CN.NewThreadSafePacketConn(pc), s), s.ProxyAdapter), nil return newPacketConn(CN.NewThreadSafePacketConn(pc), s), nil
} }
func (s *SingMux) SupportUDP() bool { func (s *SingMux) SupportUDP() bool {
@ -103,11 +97,15 @@ func (s *SingMux) ProxyInfo() C.ProxyInfo {
return info return info
} }
func closeSingMux(s *SingMux) { // Close implements C.ProxyAdapter
func (s *SingMux) Close() error {
if s.client != nil {
_ = s.client.Close() _ = s.client.Close()
} }
return s.ProxyAdapter.Close()
}
func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.ProxyAdapter, error) { func NewSingMux(option SingMuxOption, proxy ProxyAdapter) (ProxyAdapter, error) {
// TODO // TODO
// "TCP Brutal is only supported on Linux-based systems" // "TCP Brutal is only supported on Linux-based systems"
@ -131,11 +129,9 @@ func NewSingMux(option SingMuxOption, proxy C.ProxyAdapter, base ProxyBase) (C.P
} }
outbound := &SingMux{ outbound := &SingMux{
ProxyAdapter: proxy, ProxyAdapter: proxy,
base: base,
client: client, client: client,
dialer: singDialer, dialer: singDialer,
onlyTcp: option.OnlyTcp, onlyTcp: option.OnlyTcp,
} }
runtime.SetFinalizer(outbound, closeSingMux)
return outbound, nil return outbound, nil
} }

View file

@ -6,6 +6,7 @@ import (
"net" "net"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -41,7 +42,7 @@ type streamOption struct {
obfsOption *simpleObfsOption obfsOption *simpleObfsOption
} }
func streamConn(c net.Conn, option streamOption) *snell.Snell { func snellStreamConn(c net.Conn, option streamOption) *snell.Snell {
switch option.obfsOption.Mode { switch option.obfsOption.Mode {
case "tls": case "tls":
c = obfs.NewTLSObfs(c, option.obfsOption.Host) c = obfs.NewTLSObfs(c, option.obfsOption.Host)
@ -54,25 +55,35 @@ func streamConn(c net.Conn, option streamOption) *snell.Snell {
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (s *Snell) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption}) c = snellStreamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
if metadata.NetWork == C.UDP { err := s.writeHeaderContext(ctx, c, metadata)
err := snell.WriteUDPHeader(c, s.version)
return c, err return c, err
} }
err := snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
return c, err func (s *Snell) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if metadata.NetWork == C.UDP {
err = snell.WriteUDPHeader(c, s.version)
return
}
err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version)
return
} }
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (s *Snell) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
if s.version == snell.Version2 && len(opts) == 0 { if s.version == snell.Version2 && dialer.IsZeroOptions(opts) {
c, err := s.pool.Get() c, err := s.pool.Get()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = snell.WriteHeader(c, metadata.String(), uint(metadata.DstPort), s.version); err != nil { if err = s.writeHeaderContext(ctx, c, metadata); err != nil {
c.Close() _ = c.Close()
return nil, err return nil, err
} }
return NewConn(c, s), err return NewConn(c, s), err
@ -120,12 +131,8 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
if err != nil { if err != nil {
return nil, err return nil, err
} }
c = streamConn(c, streamOption{s.psk, s.version, s.addr, s.obfsOption})
err = snell.WriteUDPHeader(c, s.version) c, err = s.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, err
}
pc := snell.PacketConn(c) pc := snell.PacketConn(c)
return newPacketConn(pc, s), nil return newPacketConn(pc, s), nil
@ -212,7 +219,7 @@ func NewSnell(option SnellOption) (*Snell, error) {
return nil, err return nil, err
} }
return streamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil return snellStreamConn(c, streamOption{psk, option.Version, addr, obfsOption}), nil
}) })
} }
return s, nil return s, nil

View file

@ -10,6 +10,7 @@ import (
"net/netip" "net/netip"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -58,7 +59,7 @@ func (ss *Socks5) StreamConnContext(ctx context.Context, c net.Conn, metadata *C
Password: ss.pass, Password: ss.pass,
} }
} }
if _, err := socks5.ClientHandshake(c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil { if _, err := ss.clientHandshakeContext(ctx, c, serializesSocksAddr(metadata), socks5.CmdConnect, user); err != nil {
return nil, err return nil, err
} }
return c, nil return c, nil
@ -135,7 +136,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
} }
udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0)) udpAssocateAddr := socks5.AddrFromStdAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
bindAddr, err := socks5.ClientHandshake(c, udpAssocateAddr, socks5.CmdUDPAssociate, user) bindAddr, err := ss.clientHandshakeContext(ctx, c, udpAssocateAddr, socks5.CmdUDPAssociate, user)
if err != nil { if err != nil {
err = fmt.Errorf("client hanshake error: %w", err) err = fmt.Errorf("client hanshake error: %w", err)
return return
@ -147,7 +148,7 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
err = errors.New("invalid UDP bind address") err = errors.New("invalid UDP bind address")
return return
} else if bindUDPAddr.IP.IsUnspecified() { } else if bindUDPAddr.IP.IsUnspecified() {
serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr()) serverAddr, err := resolveUDPAddr(ctx, "udp", ss.Addr(), C.IPv4Prefer)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -178,6 +179,14 @@ func (ss *Socks5) ProxyInfo() C.ProxyInfo {
return info return info
} }
func (ss *Socks5) clientHandshakeContext(ctx context.Context, c net.Conn, addr socks5.Addr, command socks5.Command, user *socks5.User) (_ socks5.Addr, err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
return socks5.ClientHandshake(c, addr, command, user)
}
func NewSocks5(option Socks5Option) (*Socks5, error) { func NewSocks5(option Socks5Option) (*Socks5, error) {
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {

View file

@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"runtime"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -25,7 +24,10 @@ type Ssh struct {
*Base *Base
option *SshOption option *SshOption
client *sshClient // using a standalone struct to avoid its inner loop invalidate the Finalizer
config *ssh.ClientConfig
client *ssh.Client
cMutex sync.Mutex
} }
type SshOption struct { type SshOption struct {
@ -49,7 +51,7 @@ func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dia
return nil, err return nil, err
} }
} }
client, err := s.client.connect(ctx, cDialer, s.addr) client, err := s.connect(ctx, cDialer, s.addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -58,16 +60,10 @@ func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dia
return nil, err return nil, err
} }
return NewConn(N.NewRefConn(c, s), s), nil return NewConn(c, s), nil
} }
type sshClient struct { func (s *Ssh) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) {
config *ssh.ClientConfig
client *ssh.Client
cMutex sync.Mutex
}
func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) {
s.cMutex.Lock() s.cMutex.Lock()
defer s.cMutex.Unlock() defer s.cMutex.Unlock()
if s.client != nil { if s.client != nil {
@ -108,7 +104,15 @@ func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string)
return client, nil return client, nil
} }
func (s *sshClient) Close() error { // ProxyInfo implements C.ProxyAdapter
func (s *Ssh) ProxyInfo() C.ProxyInfo {
info := s.Base.ProxyInfo()
info.DialerProxy = s.option.DialerProxy
return info
}
// Close implements C.ProxyAdapter
func (s *Ssh) Close() error {
s.cMutex.Lock() s.cMutex.Lock()
defer s.cMutex.Unlock() defer s.cMutex.Unlock()
if s.client != nil { if s.client != nil {
@ -117,17 +121,6 @@ func (s *sshClient) Close() error {
return nil return nil
} }
func closeSsh(s *Ssh) {
_ = s.client.Close()
}
// ProxyInfo implements C.ProxyAdapter
func (s *Ssh) ProxyInfo() C.ProxyInfo {
info := s.Base.ProxyInfo()
info.DialerProxy = s.option.DialerProxy
return info
}
func NewSsh(option SshOption) (*Ssh, error) { func NewSsh(option SshOption) (*Ssh, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
@ -204,11 +197,8 @@ func NewSsh(option SshOption) (*Ssh, error) {
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option, option: &option,
client: &sshClient{
config: &config, config: &config,
},
} }
runtime.SetFinalizer(outbound, closeSsh)
return outbound, nil return outbound, nil
} }

View file

@ -9,6 +9,7 @@ import (
"net/http" "net/http"
"strconv" "strconv"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
@ -17,12 +18,13 @@ import (
"github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/shadowsocks/core"
"github.com/metacubex/mihomo/transport/trojan" "github.com/metacubex/mihomo/transport/trojan"
"github.com/metacubex/mihomo/transport/vmess"
) )
type Trojan struct { type Trojan struct {
*Base *Base
instance *trojan.Trojan
option *TrojanOption option *TrojanOption
hexPassword [trojan.KeyLength]byte
// for gun mux // for gun mux
gunTLSConfig *tls.Config gunTLSConfig *tls.Config
@ -60,15 +62,21 @@ type TrojanSSOption struct {
Password string `proxy:"password,omitempty"` Password string `proxy:"password,omitempty"`
} }
func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error) { // StreamConnContext implements C.ProxyAdapter
if t.option.Network == "ws" { func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
switch t.option.Network {
case "ws":
host, port, _ := net.SplitHostPort(t.addr) host, port, _ := net.SplitHostPort(t.addr)
wsOpts := &trojan.WebsocketOption{
wsOpts := &vmess.WebsocketConfig{
Host: host, Host: host,
Port: port, Port: port,
Path: t.option.WSOpts.Path, Path: t.option.WSOpts.Path,
MaxEarlyData: t.option.WSOpts.MaxEarlyData,
EarlyDataHeaderName: t.option.WSOpts.EarlyDataHeaderName,
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: t.option.ClientFingerprint,
Headers: http.Header{}, Headers: http.Header{},
} }
@ -82,57 +90,95 @@ func (t *Trojan) plainStream(ctx context.Context, c net.Conn) (net.Conn, error)
} }
} }
return t.instance.StreamWebsocketConn(ctx, c, wsOpts) alpn := trojan.DefaultWebsocketALPN
if len(t.option.ALPN) != 0 {
alpn = t.option.ALPN
} }
return t.instance.StreamConn(ctx, c) wsOpts.TLS = true
tlsConfig := &tls.Config{
NextProtos: alpn,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: t.option.SkipCertVerify,
ServerName: t.option.SNI,
} }
// StreamConnContext implements C.ProxyAdapter wsOpts.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, t.option.Fingerprint)
func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
var err error
if tlsC.HaveGlobalFingerprint() && len(t.option.ClientFingerprint) == 0 {
t.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
if t.transport != nil {
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
} else {
c, err = t.plainStream(ctx, c)
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
if t.ssCipher != nil {
c = t.ssCipher.StreamConn(c)
}
if metadata.NetWork == C.UDP {
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
return c, err
}
err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata))
return c, err
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
// gun transport
if t.transport != nil && len(opts) == 0 {
c, err := gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "grpc":
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig)
default:
// default tcp network
// handle TLS
alpn := trojan.DefaultALPN
if len(t.option.ALPN) != 0 {
alpn = t.option.ALPN
}
c, err = vmess.StreamTLSConn(ctx, c, &vmess.TLSConfig{
Host: t.option.SNI,
SkipCertVerify: t.option.SkipCertVerify,
FingerPrint: t.option.Fingerprint,
ClientFingerprint: t.option.ClientFingerprint,
NextProtos: alpn,
Reality: t.realityConfig,
})
}
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
return t.streamConnContext(ctx, c, metadata)
}
func (t *Trojan) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
if t.ssCipher != nil { if t.ssCipher != nil {
c = t.ssCipher.StreamConn(c) c = t.ssCipher.StreamConn(c)
} }
if err = t.instance.WriteHeader(c, trojan.CommandTCP, serializesSocksAddr(metadata)); err != nil { if ctx.Done() != nil {
c.Close() done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
command := trojan.CommandTCP
if metadata.NetWork == C.UDP {
command = trojan.CommandUDP
}
err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata))
return c, err
}
func (t *Trojan) writeHeaderContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
command := trojan.CommandTCP
if metadata.NetWork == C.UDP {
command = trojan.CommandUDP
}
err = trojan.WriteHeader(c, t.hexPassword, command, serializesSocksAddr(metadata))
return err
}
// DialContext implements C.ProxyAdapter
func (t *Trojan) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport
if t.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
if err != nil {
return nil, err
}
defer func(c net.Conn) {
safeConnClose(c, err)
}(c)
c, err = t.streamConnContext(ctx, c, metadata)
if err != nil {
return nil, err return nil, err
} }
@ -171,7 +217,7 @@ 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 && len(opts) == 0 { if t.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig) c, err = gun.StreamGunWithTransport(t.transport, t.gunConfig)
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)
@ -180,16 +226,12 @@ func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata,
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
if t.ssCipher != nil { c, err = t.streamConnContext(ctx, c, metadata)
c = t.ssCipher.StreamConn(c)
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc := t.instance.PacketConn(c) pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata) return t.ListenPacketWithDialer(ctx, dialer.NewDialer(t.Base.DialOptions(opts...)...), metadata)
@ -210,21 +252,12 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
defer func(c net.Conn) { defer func(c net.Conn) {
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = t.plainStream(ctx, c) c, err = t.StreamConnContext(ctx, c, metadata)
if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
}
if t.ssCipher != nil {
c = t.ssCipher.StreamConn(c)
}
err = t.instance.WriteHeader(c, trojan.CommandUDP, serializesSocksAddr(metadata))
if err != nil { if err != nil {
return nil, err return nil, err
} }
pc := t.instance.PacketConn(c) pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
@ -235,7 +268,7 @@ func (t *Trojan) SupportWithDialer() C.NetWork {
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := t.instance.PacketConn(c) pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err return newPacketConn(pc, t), err
} }
@ -251,22 +284,17 @@ func (t *Trojan) ProxyInfo() C.ProxyInfo {
return info return info
} }
// Close implements C.ProxyAdapter
func (t *Trojan) Close() error {
if t.transport != nil {
return t.transport.Close()
}
return nil
}
func NewTrojan(option TrojanOption) (*Trojan, error) { func NewTrojan(option TrojanOption) (*Trojan, error) {
addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
tOption := &trojan.Option{
Password: option.Password,
ALPN: option.ALPN,
ServerName: option.Server,
SkipCertVerify: option.SkipCertVerify,
Fingerprint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint,
}
if option.SNI != "" {
tOption.ServerName = option.SNI
}
t := &Trojan{ t := &Trojan{
Base: &Base{ Base: &Base{
name: option.Name, name: option.Name,
@ -279,8 +307,8 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
instance: trojan.New(tOption),
option: &option, option: &option,
hexPassword: trojan.Key(option.Password),
} }
var err error var err error
@ -288,7 +316,6 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
tOption.Reality = t.realityConfig
if option.SSOpts.Enabled { if option.SSOpts.Enabled {
if option.SSOpts.Password == "" { if option.SSOpts.Password == "" {
@ -305,7 +332,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
} }
if option.Network == "grpc" { if option.Network == "grpc" {
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
var err error var err error
var cDialer C.Dialer = dialer.NewDialer(t.Base.DialOptions()...) var cDialer C.Dialer = dialer.NewDialer(t.Base.DialOptions()...)
if len(t.option.DialerProxy) > 0 { if len(t.option.DialerProxy) > 0 {
@ -314,7 +341,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err return nil, err
} }
} }
c, err := cDialer.DialContext(context.Background(), "tcp", t.addr) c, err := cDialer.DialContext(ctx, "tcp", t.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error()) return nil, fmt.Errorf("%s connect error: %s", t.addr, err.Error())
} }
@ -324,8 +351,8 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
NextProtos: option.ALPN, NextProtos: option.ALPN,
MinVersion: tls.VersionTLS12, MinVersion: tls.VersionTLS12,
InsecureSkipVerify: tOption.SkipCertVerify, InsecureSkipVerify: option.SkipCertVerify,
ServerName: tOption.ServerName, ServerName: option.SNI,
} }
var err error var err error
@ -334,12 +361,13 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err return nil, err
} }
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, tOption.ClientFingerprint, t.realityConfig) t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig)
t.gunTLSConfig = tlsConfig t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{
ServiceName: option.GrpcOpts.GrpcServiceName, ServiceName: option.GrpcOpts.GrpcServiceName,
Host: tOption.ServerName, Host: option.SNI,
ClientFingerprint: option.ClientFingerprint,
} }
} }

View file

@ -130,7 +130,7 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *
return nil, nil, err return nil, nil, err
} }
} }
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", t.addr, t.prefer) udpAddr, err := resolveUDPAddr(ctx, "udp", t.addr, t.prefer)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View file

@ -3,118 +3,59 @@ package outbound
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/tls"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"regexp" "regexp"
"strconv" "strconv"
"sync"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/socks5"
) )
var (
globalClientSessionCache tls.ClientSessionCache
once sync.Once
)
func getClientSessionCache() tls.ClientSessionCache {
once.Do(func() {
globalClientSessionCache = tls.NewLRUClientSessionCache(128)
})
return globalClientSessionCache
}
func serializesSocksAddr(metadata *C.Metadata) []byte { func serializesSocksAddr(metadata *C.Metadata) []byte {
var buf [][]byte var buf [][]byte
addrType := metadata.AddrType() addrType := metadata.AddrType()
aType := uint8(addrType)
p := uint(metadata.DstPort) p := uint(metadata.DstPort)
port := []byte{uint8(p >> 8), uint8(p & 0xff)} port := []byte{uint8(p >> 8), uint8(p & 0xff)}
switch addrType { switch addrType {
case socks5.AtypDomainName: case C.AtypDomainName:
lenM := uint8(len(metadata.Host)) lenM := uint8(len(metadata.Host))
host := []byte(metadata.Host) host := []byte(metadata.Host)
buf = [][]byte{{aType, lenM}, host, port} buf = [][]byte{{socks5.AtypDomainName, lenM}, host, port}
case socks5.AtypIPv4: case C.AtypIPv4:
host := metadata.DstIP.AsSlice() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{socks5.AtypIPv4}, host, port}
case socks5.AtypIPv6: case C.AtypIPv6:
host := metadata.DstIP.AsSlice() host := metadata.DstIP.AsSlice()
buf = [][]byte{{aType}, host, port} buf = [][]byte{{socks5.AtypIPv6}, host, port}
} }
return bytes.Join(buf, nil) return bytes.Join(buf, nil)
} }
func resolveUDPAddr(ctx context.Context, network, address string) (*net.UDPAddr, error) { func resolveUDPAddr(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
ip, err := resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
if err != nil {
return nil, err
}
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
}
func resolveUDPAddrWithPrefer(ctx context.Context, network, address string, prefer C.DNSPrefer) (*net.UDPAddr, error) {
host, port, err := net.SplitHostPort(address) host, port, err := net.SplitHostPort(address)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var ip netip.Addr var ip netip.Addr
var fallback netip.Addr
switch prefer { switch prefer {
case C.IPv4Only: case C.IPv4Only:
ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver) ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
case C.IPv6Only: case C.IPv6Only:
ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver) ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
case C.IPv6Prefer: case C.IPv6Prefer:
var ips []netip.Addr ip, err = resolver.ResolveIPPrefer6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
ips, err = resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
if err == nil {
for _, addr := range ips {
if addr.Is6() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
default: default:
// C.IPv4Prefer, C.DualStack and other ip, err = resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
var ips []netip.Addr
ips, err = resolver.LookupIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
if err == nil {
for _, addr := range ips {
if addr.Is4() {
ip = addr
break
} else {
if !fallback.IsValid() {
fallback = addr
}
}
}
}
}
if !ip.IsValid() && fallback.IsValid() {
ip = fallback
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
ip, port = resolver.LookupIP4P(ip, port)
return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port)) return net.ResolveUDPAddr(network, net.JoinHostPort(ip.String(), port))
} }

View file

@ -22,7 +22,6 @@ import (
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/gun" "github.com/metacubex/mihomo/transport/gun"
"github.com/metacubex/mihomo/transport/socks5"
"github.com/metacubex/mihomo/transport/vless" "github.com/metacubex/mihomo/transport/vless"
"github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/vmess"
@ -76,13 +75,7 @@ type VlessOption struct {
ClientFingerprint string `proxy:"client-fingerprint,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
} }
func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
var err error
if tlsC.HaveGlobalFingerprint() && len(v.option.ClientFingerprint) == 0 {
v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
@ -156,7 +149,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = vmess.StreamH2Conn(c, h2Opts) c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
default: default:
@ -169,10 +162,14 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
return nil, err return nil, err
} }
return v.streamConn(c, metadata) return v.streamConnContext(ctx, c, metadata)
} }
func (v *Vless) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { func (v *Vless) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
if metadata.NetWork == C.UDP { if metadata.NetWork == C.UDP {
if v.option.PacketAddr { if v.option.PacketAddr {
metadata = &C.Metadata{ metadata = &C.Metadata{
@ -229,9 +226,10 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -239,7 +237,7 @@ func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.client.StreamConn(c, parseVlessAddr(metadata, v.option.XUDP)) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -284,7 +282,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -293,7 +291,7 @@ func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.streamConn(c, metadata) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vless client error: %v", err) return nil, fmt.Errorf("new vless client error: %v", err)
} }
@ -385,19 +383,27 @@ func (v *Vless) ProxyInfo() C.ProxyInfo {
return info return info
} }
// Close implements C.ProxyAdapter
func (v *Vless) Close() error {
if v.transport != nil {
return v.transport.Close()
}
return nil
}
func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr { func parseVlessAddr(metadata *C.Metadata, xudp bool) *vless.DstAddr {
var addrType byte var addrType byte
var addr []byte var addr []byte
switch metadata.AddrType() { switch metadata.AddrType() {
case socks5.AtypIPv4: case C.AtypIPv4:
addrType = vless.AtypIPv4 addrType = vless.AtypIPv4
addr = make([]byte, net.IPv4len) addr = make([]byte, net.IPv4len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypIPv6: case C.AtypIPv6:
addrType = vless.AtypIPv6 addrType = vless.AtypIPv6
addr = make([]byte, net.IPv6len) addr = make([]byte, net.IPv6len)
copy(addr[:], metadata.DstIP.AsSlice()) copy(addr[:], metadata.DstIP.AsSlice())
case socks5.AtypDomainName: case C.AtypDomainName:
addrType = vless.AtypDomainName addrType = vless.AtypDomainName
addr = make([]byte, len(metadata.Host)+1) addr = make([]byte, len(metadata.Host)+1)
addr[0] = byte(len(metadata.Host)) addr[0] = byte(len(metadata.Host))
@ -563,7 +569,7 @@ func NewVless(option VlessOption) (*Vless, error) {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
} }
case "grpc": case "grpc":
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
var err error var err error
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...) var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
if len(v.option.DialerProxy) > 0 { if len(v.option.DialerProxy) > 0 {
@ -572,7 +578,7 @@ func NewVless(option VlessOption) (*Vless, error) {
return nil, err return nil, err
} }
} }
c, err := cDialer.DialContext(context.Background(), "tcp", v.addr) c, err := cDialer.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())
} }
@ -589,10 +595,13 @@ func NewVless(option VlessOption) (*Vless, error) {
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) }, v.option.Fingerprint)
if err != nil {
return nil, err
}
if option.ServerName == "" { if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host tlsConfig.ServerName = host

View file

@ -96,13 +96,7 @@ type WSOptions struct {
} }
// StreamConnContext implements C.ProxyAdapter // StreamConnContext implements C.ProxyAdapter
func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) { func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ net.Conn, err error) {
var err error
if tlsC.HaveGlobalFingerprint() && (len(v.option.ClientFingerprint) == 0) {
v.option.ClientFingerprint = tlsC.GetGlobalFingerprint()
}
switch v.option.Network { switch v.option.Network {
case "ws": case "ws":
host, port, _ := net.SplitHostPort(v.addr) host, port, _ := net.SplitHostPort(v.addr)
@ -199,7 +193,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Path: v.option.HTTP2Opts.Path, Path: v.option.HTTP2Opts.Path,
} }
c, err = mihomoVMess.StreamH2Conn(c, h2Opts) c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig)
default: default:
@ -226,17 +220,24 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
if err != nil { if err != nil {
return nil, err return nil, err
} }
return v.streamConn(c, metadata) return v.streamConnContext(ctx, c, metadata)
} }
func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) { func (v *Vmess) streamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (conn net.Conn, err error) {
useEarly := N.NeedHandshake(c)
if !useEarly {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, c)
defer done(&err)
}
}
if metadata.NetWork == C.UDP { if metadata.NetWork == C.UDP {
if v.option.XUDP { if v.option.XUDP {
var globalID [8]byte var globalID [8]byte
if metadata.SourceValid() { if metadata.SourceValid() {
globalID = utils.GlobalID(metadata.SourceAddress()) globalID = utils.GlobalID(metadata.SourceAddress())
} }
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyXUDPPacketConn(c, conn = v.client.DialEarlyXUDPPacketConn(c,
globalID, globalID,
M.SocksaddrFromNet(metadata.UDPAddr())) M.SocksaddrFromNet(metadata.UDPAddr()))
@ -246,7 +247,7 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
M.SocksaddrFromNet(metadata.UDPAddr())) M.SocksaddrFromNet(metadata.UDPAddr()))
} }
} else if v.option.PacketAddr { } else if v.option.PacketAddr {
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyPacketConn(c, conn = v.client.DialEarlyPacketConn(c,
M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443)) M.ParseSocksaddrHostPort(packetaddr.SeqPacketMagicAddress, 443))
} else { } else {
@ -255,7 +256,7 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
} }
conn = packetaddr.NewBindConn(conn) conn = packetaddr.NewBindConn(conn)
} else { } else {
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyPacketConn(c, conn = v.client.DialEarlyPacketConn(c,
M.SocksaddrFromNet(metadata.UDPAddr())) M.SocksaddrFromNet(metadata.UDPAddr()))
} else { } else {
@ -264,7 +265,7 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
} }
} }
} else { } else {
if N.NeedHandshake(c) { if useEarly {
conn = v.client.DialEarlyConn(c, conn = v.client.DialEarlyConn(c,
M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort))
} else { } else {
@ -280,9 +281,10 @@ func (v *Vmess) streamConn(c net.Conn, metadata *C.Metadata) (conn net.Conn, err
// DialContext implements C.ProxyAdapter // DialContext implements C.ProxyAdapter
func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -290,7 +292,7 @@ func (v *Vmess) DialContext(ctx context.Context, metadata *C.Metadata, opts ...d
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.client.DialConn(c, M.ParseSocksaddrHostPort(metadata.String(), metadata.DstPort)) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -332,7 +334,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
if v.transport != nil && len(opts) == 0 { if v.transport != nil && dialer.IsZeroOptions(opts) {
c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@ -341,7 +343,7 @@ func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata, o
safeConnClose(c, err) safeConnClose(c, err)
}(c) }(c)
c, err = v.streamConn(c, metadata) c, err = v.streamConnContext(ctx, c, metadata)
if err != nil { if err != nil {
return nil, fmt.Errorf("new vmess client error: %v", err) return nil, fmt.Errorf("new vmess client error: %v", err)
} }
@ -395,6 +397,14 @@ func (v *Vmess) ProxyInfo() C.ProxyInfo {
return info return info
} }
// Close implements C.ProxyAdapter
func (v *Vmess) Close() error {
if v.transport != nil {
return v.transport.Close()
}
return nil
}
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr // vmess use stream-oriented udp with a special address, so we need a net.UDPAddr
@ -470,7 +480,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com")
} }
case "grpc": case "grpc":
dialFn := func(network, addr string) (net.Conn, error) { dialFn := func(ctx context.Context, network, addr string) (net.Conn, error) {
var err error var err error
var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...) var cDialer C.Dialer = dialer.NewDialer(v.Base.DialOptions()...)
if len(v.option.DialerProxy) > 0 { if len(v.option.DialerProxy) > 0 {
@ -479,7 +489,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return nil, err return nil, err
} }
} }
c, err := cDialer.DialContext(context.Background(), "tcp", v.addr) c, err := cDialer.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())
} }
@ -496,10 +506,13 @@ func NewVmess(option VmessOption) (*Vmess, error) {
} }
var tlsConfig *tls.Config var tlsConfig *tls.Config
if option.TLS { if option.TLS {
tlsConfig = ca.GetGlobalTLSConfig(&tls.Config{ tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
InsecureSkipVerify: v.option.SkipCertVerify, InsecureSkipVerify: v.option.SkipCertVerify,
ServerName: v.option.ServerName, ServerName: v.option.ServerName,
}) }, v.option.Fingerprint)
if err != nil {
return nil, err
}
if option.ServerName == "" { if option.ServerName == "" {
host, _, _ := net.SplitHostPort(v.addr) host, _, _ := net.SplitHostPort(v.addr)
tlsConfig.ServerName = host tlsConfig.ServerName = host

View file

@ -8,14 +8,12 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"runtime"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/atomic"
CN "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
@ -45,7 +43,6 @@ type WireGuard struct {
tunDevice wireguard.Device tunDevice wireguard.Device
dialer proxydialer.SingDialer dialer proxydialer.SingDialer
resolver resolver.Resolver resolver resolver.Resolver
refP *refProxyAdapter
initOk atomic.Bool initOk atomic.Bool
initMutex sync.Mutex initMutex sync.Mutex
@ -57,8 +54,6 @@ type WireGuard struct {
serverAddrMap map[M.Socksaddr]netip.AddrPort serverAddrMap map[M.Socksaddr]netip.AddrPort
serverAddrTime atomic.TypedValue[time.Time] serverAddrTime atomic.TypedValue[time.Time]
serverAddrMutex sync.Mutex serverAddrMutex sync.Mutex
closeCh chan struct{} // for test
} }
type WireGuardOption struct { type WireGuardOption struct {
@ -173,7 +168,6 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
}, },
dialer: proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), slowdown.New()), dialer: proxydialer.NewSlowDownSingDialer(proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()), slowdown.New()),
} }
runtime.SetFinalizer(outbound, closeWireGuard)
var reserved [3]uint8 var reserved [3]uint8
if len(option.Reserved) > 0 { if len(option.Reserved) > 0 {
@ -286,15 +280,13 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
} }
} }
refP := &refProxyAdapter{}
outbound.refP = refP
if option.RemoteDnsResolve && len(option.Dns) > 0 { if option.RemoteDnsResolve && len(option.Dns) > 0 {
nss, err := dns.ParseNameServer(option.Dns) nss, err := dns.ParseNameServer(option.Dns)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for i := range nss { for i := range nss {
nss[i].ProxyAdapter = refP nss[i].ProxyAdapter = outbound
} }
outbound.resolver = dns.NewResolver(dns.Config{ outbound.resolver = dns.NewResolver(dns.Config{
Main: nss, Main: nss,
@ -309,7 +301,7 @@ func (w *WireGuard) resolve(ctx context.Context, address M.Socksaddr) (netip.Add
if address.Addr.IsValid() { if address.Addr.IsValid() {
return address.AddrPort(), nil return address.AddrPort(), nil
} }
udpAddr, err := resolveUDPAddrWithPrefer(ctx, "udp", address.String(), w.prefer) udpAddr, err := resolveUDPAddr(ctx, "udp", address.String(), w.prefer)
if err != nil { if err != nil {
return netip.AddrPort{}, err return netip.AddrPort{}, err
} }
@ -488,13 +480,12 @@ func (w *WireGuard) genIpcConf(ctx context.Context, updateOnly bool) (string, er
return ipcConf, nil return ipcConf, nil
} }
func closeWireGuard(w *WireGuard) { // Close implements C.ProxyAdapter
func (w *WireGuard) Close() error {
if w.device != nil { if w.device != nil {
w.device.Close() w.device.Close()
} }
if w.closeCh != nil { return nil
close(w.closeCh)
}
} }
func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
@ -507,8 +498,6 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
if !metadata.Resolved() || w.resolver != nil { if !metadata.Resolved() || w.resolver != nil {
r := resolver.DefaultResolver r := resolver.DefaultResolver
if w.resolver != nil { if w.resolver != nil {
w.refP.SetProxyAdapter(w)
defer w.refP.ClearProxyAdapter()
r = w.resolver r = w.resolver
} }
options = append(options, dialer.WithResolver(r)) options = append(options, dialer.WithResolver(r))
@ -523,7 +512,7 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts
if conn == nil { if conn == nil {
return nil, E.New("conn is nil") return nil, E.New("conn is nil")
} }
return NewConn(CN.NewRefConn(conn, w), w), nil return NewConn(conn, w), nil
} }
func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
@ -536,8 +525,6 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver r := resolver.DefaultResolver
if w.resolver != nil { if w.resolver != nil {
w.refP.SetProxyAdapter(w)
defer w.refP.ClearProxyAdapter()
r = w.resolver r = w.resolver
} }
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
@ -553,139 +540,10 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if pc == nil { if pc == nil {
return nil, E.New("packetConn is nil") return nil, E.New("packetConn is nil")
} }
return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil return newPacketConn(pc, w), nil
} }
// IsL3Protocol implements C.ProxyAdapter // IsL3Protocol implements C.ProxyAdapter
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool { func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
return true return true
} }
type refProxyAdapter struct {
proxyAdapter C.ProxyAdapter
count int
mutex sync.Mutex
}
func (r *refProxyAdapter) SetProxyAdapter(proxyAdapter C.ProxyAdapter) {
r.mutex.Lock()
defer r.mutex.Unlock()
r.proxyAdapter = proxyAdapter
r.count++
}
func (r *refProxyAdapter) ClearProxyAdapter() {
r.mutex.Lock()
defer r.mutex.Unlock()
r.count--
if r.count == 0 {
r.proxyAdapter = nil
}
}
func (r *refProxyAdapter) Name() string {
if r.proxyAdapter != nil {
return r.proxyAdapter.Name()
}
return ""
}
func (r *refProxyAdapter) Type() C.AdapterType {
if r.proxyAdapter != nil {
return r.proxyAdapter.Type()
}
return C.AdapterType(0)
}
func (r *refProxyAdapter) Addr() string {
if r.proxyAdapter != nil {
return r.proxyAdapter.Addr()
}
return ""
}
func (r *refProxyAdapter) SupportUDP() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportUDP()
}
return false
}
func (r *refProxyAdapter) ProxyInfo() C.ProxyInfo {
if r.proxyAdapter != nil {
return r.proxyAdapter.ProxyInfo()
}
return C.ProxyInfo{}
}
func (r *refProxyAdapter) MarshalJSON() ([]byte, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.MarshalJSON()
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.StreamConnContext(ctx, c, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.DialContext(ctx, metadata, opts...)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.ListenPacketContext(ctx, metadata, opts...)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) SupportUOT() bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportUOT()
}
return false
}
func (r *refProxyAdapter) SupportWithDialer() C.NetWork {
if r.proxyAdapter != nil {
return r.proxyAdapter.SupportWithDialer()
}
return C.InvalidNet
}
func (r *refProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
if r.proxyAdapter != nil {
return r.proxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
}
return nil, C.ErrNotSupport
}
func (r *refProxyAdapter) IsL3Protocol(metadata *C.Metadata) bool {
if r.proxyAdapter != nil {
return r.proxyAdapter.IsL3Protocol(metadata)
}
return false
}
func (r *refProxyAdapter) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
if r.proxyAdapter != nil {
return r.proxyAdapter.Unwrap(metadata, touch)
}
return nil
}
var _ C.ProxyAdapter = (*refProxyAdapter)(nil)

View file

@ -1,45 +0,0 @@
//go:build with_gvisor
package outbound
import (
"context"
"runtime"
"testing"
"time"
)
func TestWireGuardGC(t *testing.T) {
option := WireGuardOption{}
option.Server = "162.159.192.1"
option.Port = 2408
option.PrivateKey = "iOx7749AdqH3IqluG7+0YbGKd0m1mcEXAfGRzpy9rG8="
option.PublicKey = "bmXOC+F1FxEMF9dyiK2H5/1SUtzH0JuVo51h2wPfgyo="
option.Ip = "172.16.0.2"
option.Ipv6 = "2606:4700:110:8d29:be92:3a6a:f4:c437"
option.Reserved = []uint8{51, 69, 125}
wg, err := NewWireGuard(option)
if err != nil {
t.Error(err)
}
closeCh := make(chan struct{})
wg.closeCh = closeCh
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
err = wg.init(ctx)
if err != nil {
t.Error(err)
return
}
// must do a small sleep before test GC
// because it maybe deadlocks if w.device.Close call too fast after w.device.Start
time.Sleep(10 * time.Millisecond)
wg = nil
runtime.GC()
select {
case <-closeCh:
return
case <-ctx.Done():
t.Error("timeout not GC")
}
}

View file

@ -3,8 +3,6 @@ package adapter
import ( import (
"fmt" "fmt"
tlsC "github.com/metacubex/mihomo/component/tls"
"github.com/metacubex/mihomo/adapter/outbound" "github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/common/structure" "github.com/metacubex/mihomo/common/structure"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@ -18,12 +16,12 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
} }
var ( var (
proxy C.ProxyAdapter proxy outbound.ProxyAdapter
err error err error
) )
switch proxyType { switch proxyType {
case "ss": case "ss":
ssOption := &outbound.ShadowSocksOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} ssOption := &outbound.ShadowSocksOption{}
err = decoder.Decode(mapping, ssOption) err = decoder.Decode(mapping, ssOption)
if err != nil { if err != nil {
break break
@ -56,7 +54,6 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
Method: "GET", Method: "GET",
Path: []string{"/"}, Path: []string{"/"},
}, },
ClientFingerprint: tlsC.GetGlobalFingerprint(),
} }
err = decoder.Decode(mapping, vmessOption) err = decoder.Decode(mapping, vmessOption)
@ -65,7 +62,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
} }
proxy, err = outbound.NewVmess(*vmessOption) proxy, err = outbound.NewVmess(*vmessOption)
case "vless": case "vless":
vlessOption := &outbound.VlessOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} vlessOption := &outbound.VlessOption{}
err = decoder.Decode(mapping, vlessOption) err = decoder.Decode(mapping, vlessOption)
if err != nil { if err != nil {
break break
@ -79,7 +76,7 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
} }
proxy, err = outbound.NewSnell(*snellOption) proxy, err = outbound.NewSnell(*snellOption)
case "trojan": case "trojan":
trojanOption := &outbound.TrojanOption{ClientFingerprint: tlsC.GetGlobalFingerprint()} trojanOption := &outbound.TrojanOption{}
err = decoder.Decode(mapping, trojanOption) err = decoder.Decode(mapping, trojanOption)
if err != nil { if err != nil {
break break
@ -170,12 +167,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) {
return nil, err return nil, err
} }
if muxOption.Enabled { if muxOption.Enabled {
proxy, err = outbound.NewSingMux(*muxOption, proxy, proxy.(outbound.ProxyBase)) proxy, err = outbound.NewSingMux(*muxOption, proxy)
if err != nil { if err != nil {
return nil, err return nil, err
} }
} }
} }
proxy = outbound.NewAutoCloseProxyAdapter(proxy)
return NewProxy(proxy), nil return NewProxy(proxy), nil
} }

View file

@ -0,0 +1,31 @@
package contextutils
import (
"context"
"sync"
)
func afterFunc(ctx context.Context, f func()) (stop func() bool) {
stopc := make(chan struct{})
once := sync.Once{} // either starts running f or stops f from running
if ctx.Done() != nil {
go func() {
select {
case <-ctx.Done():
once.Do(func() {
go f()
})
case <-stopc:
}
}()
}
return func() bool {
stopped := false
once.Do(func() {
stopped = true
close(stopc)
})
return stopped
}
}

View file

@ -0,0 +1,11 @@
//go:build !go1.21
package contextutils
import (
"context"
)
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
return afterFunc(ctx, f)
}

View file

@ -0,0 +1,9 @@
//go:build go1.21
package contextutils
import "context"
func AfterFunc(ctx context.Context, f func()) (stop func() bool) {
return context.AfterFunc(ctx, f)
}

View file

@ -0,0 +1,100 @@
package contextutils
import (
"context"
"testing"
"time"
)
const (
shortDuration = 1 * time.Millisecond // a reasonable duration to block in a test
veryLongDuration = 1000 * time.Hour // an arbitrary upper bound on the test's running time
)
func TestAfterFuncCalledAfterCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
donec := make(chan struct{})
stop := afterFunc(ctx, func() {
close(donec)
})
select {
case <-donec:
t.Fatalf("AfterFunc called before context is done")
case <-time.After(shortDuration):
}
cancel()
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called after context is canceled")
}
if stop() {
t.Fatalf("stop() = true, want false")
}
}
func TestAfterFuncCalledAfterTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
defer cancel()
donec := make(chan struct{})
afterFunc(ctx, func() {
close(donec)
})
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called after context is canceled")
}
}
func TestAfterFuncCalledImmediately(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
donec := make(chan struct{})
afterFunc(ctx, func() {
close(donec)
})
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called for already-canceled context")
}
}
func TestAfterFuncNotCalledAfterStop(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
donec := make(chan struct{})
stop := afterFunc(ctx, func() {
close(donec)
})
if !stop() {
t.Fatalf("stop() = false, want true")
}
cancel()
select {
case <-donec:
t.Fatalf("AfterFunc called for already-canceled context")
case <-time.After(shortDuration):
}
if stop() {
t.Fatalf("stop() = true, want false")
}
}
// This test verifies that canceling a context does not block waiting for AfterFuncs to finish.
func TestAfterFuncCalledAsynchronously(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
donec := make(chan struct{})
stop := afterFunc(ctx, func() {
// The channel send blocks until donec is read from.
donec <- struct{}{}
})
defer stop()
cancel()
// After cancel returns, read from donec and unblock the AfterFunc.
select {
case <-donec:
case <-time.After(veryLongDuration):
t.Fatalf("AfterFunc not called after context is canceled")
}
}

View file

@ -275,15 +275,16 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
vmess["skip-cert-verify"] = false vmess["skip-cert-verify"] = false
vmess["cipher"] = "auto" vmess["cipher"] = "auto"
if cipher, ok := values["scy"]; ok && cipher != "" { if cipher, ok := values["scy"].(string); ok && cipher != "" {
vmess["cipher"] = cipher vmess["cipher"] = cipher
} }
if sni, ok := values["sni"]; ok && sni != "" { if sni, ok := values["sni"].(string); ok && sni != "" {
vmess["servername"] = sni vmess["servername"] = sni
} }
network, _ := values["net"].(string) network, ok := values["net"].(string)
if ok {
network = strings.ToLower(network) network = strings.ToLower(network)
if values["type"] == "http" { if values["type"] == "http" {
network = "http" network = "http"
@ -291,6 +292,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
network = "h2" network = "h2"
} }
vmess["network"] = network vmess["network"] = network
}
tls, ok := values["tls"].(string) tls, ok := values["tls"].(string)
if ok { if ok {
@ -307,12 +309,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "http": case "http":
headers := make(map[string]any) headers := make(map[string]any)
httpOpts := make(map[string]any) httpOpts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" { if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = []string{host.(string)} headers["Host"] = []string{host}
} }
httpOpts["path"] = []string{"/"} httpOpts["path"] = []string{"/"}
if path, ok := values["path"]; ok && path != "" { if path, ok := values["path"].(string); ok && path != "" {
httpOpts["path"] = []string{path.(string)} httpOpts["path"] = []string{path}
} }
httpOpts["headers"] = headers httpOpts["headers"] = headers
@ -321,8 +323,8 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
case "h2": case "h2":
headers := make(map[string]any) headers := make(map[string]any)
h2Opts := make(map[string]any) h2Opts := make(map[string]any)
if host, ok := values["host"]; ok && host != "" { if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = []string{host.(string)} headers["Host"] = []string{host}
} }
h2Opts["path"] = values["path"] h2Opts["path"] = values["path"]
@ -334,11 +336,11 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
headers := make(map[string]any) headers := make(map[string]any)
wsOpts := make(map[string]any) wsOpts := make(map[string]any)
wsOpts["path"] = "/" wsOpts["path"] = "/"
if host, ok := values["host"]; ok && host != "" { if host, ok := values["host"].(string); ok && host != "" {
headers["Host"] = host.(string) headers["Host"] = host
} }
if path, ok := values["path"]; ok && path != "" { if path, ok := values["path"].(string); ok && path != "" {
path := path.(string) path := path
pathURL, err := url.Parse(path) pathURL, err := url.Parse(path)
if err == nil { if err == nil {
query := pathURL.Query() query := pathURL.Query()

View file

@ -3,29 +3,37 @@ package net
import ( import (
"context" "context"
"net" "net"
"github.com/metacubex/mihomo/common/contextutils"
) )
// SetupContextForConn is a helper function that starts connection I/O interrupter goroutine. // SetupContextForConn is a helper function that starts connection I/O interrupter.
// if ctx be canceled before done called, it will close the connection.
// should use like this:
//
// func streamConn(ctx context.Context, conn net.Conn) (_ net.Conn, err error) {
// if ctx.Done() != nil {
// done := N.SetupContextForConn(ctx, conn)
// defer done(&err)
// }
// conn, err := xxx
// return conn, err
// }
func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) { func SetupContextForConn(ctx context.Context, conn net.Conn) (done func(*error)) {
var ( stopc := make(chan struct{})
quit = make(chan struct{}) stop := contextutils.AfterFunc(ctx, func() {
interrupt = make(chan error, 1)
)
go func() {
select {
case <-quit:
interrupt <- nil
case <-ctx.Done():
// Close the connection, discarding the error // Close the connection, discarding the error
_ = conn.Close() _ = conn.Close()
interrupt <- ctx.Err() close(stopc)
} })
}()
return func(inputErr *error) { return func(inputErr *error) {
close(quit) if !stop() {
if ctxErr := <-interrupt; ctxErr != nil && inputErr != nil { // The AfterFunc was started, wait for it to complete.
<-stopc
if ctxErr := ctx.Err(); ctxErr != nil && inputErr != nil {
// Return context error to user. // Return context error to user.
inputErr = &ctxErr *inputErr = ctxErr
}
} }
} }
} }

103
common/net/context_test.go Normal file
View file

@ -0,0 +1,103 @@
package net_test
import (
"context"
"errors"
"net"
"testing"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/stretchr/testify/assert"
)
func testRead(ctx context.Context, conn net.Conn) (err error) {
if ctx.Done() != nil {
done := N.SetupContextForConn(ctx, conn)
defer done(&err)
}
_, err = conn.Read(make([]byte, 1))
return err
}
func TestSetupContextForConnWithCancel(t *testing.T) {
t.Parallel()
c1, c2 := N.Pipe()
defer c1.Close()
defer c2.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
errc := make(chan error)
go func() {
errc <- testRead(ctx, c1)
}()
select {
case <-errc:
t.Fatal("conn closed before cancel")
case <-time.After(100 * time.Millisecond):
cancel()
}
select {
case err := <-errc:
assert.ErrorIs(t, err, context.Canceled)
case <-time.After(100 * time.Millisecond):
t.Fatal("conn not be canceled")
}
}
func TestSetupContextForConnWithTimeout1(t *testing.T) {
t.Parallel()
c1, c2 := N.Pipe()
defer c1.Close()
defer c2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
errc := make(chan error)
go func() {
errc <- testRead(ctx, c1)
}()
select {
case err := <-errc:
if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Fatal("conn closed before timeout")
}
assert.ErrorIs(t, err, context.DeadlineExceeded)
case <-time.After(200 * time.Millisecond):
t.Fatal("conn not be canceled")
}
}
func TestSetupContextForConnWithTimeout2(t *testing.T) {
t.Parallel()
c1, c2 := N.Pipe()
defer c1.Close()
defer c2.Close()
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
errc := make(chan error)
go func() {
errc <- testRead(ctx, c1)
}()
select {
case <-errc:
t.Fatal("conn closed before cancel")
case <-time.After(100 * time.Millisecond):
c2.Write(make([]byte, 1))
}
select {
case err := <-errc:
assert.Nil(t, ctx.Err())
assert.Nil(t, err)
case <-time.After(200 * time.Millisecond):
t.Fatal("conn not be canceled")
}
}

View file

@ -77,6 +77,6 @@ func (c *refConn) WriterReplaceable() bool { // Relay() will handle reference
var _ ExtendedConn = (*refConn)(nil) var _ ExtendedConn = (*refConn)(nil)
func NewRefConn(conn net.Conn, ref any) net.Conn { func NewRefConn(conn net.Conn, ref any) ExtendedConn {
return &refConn{conn: NewExtendedConn(conn), ref: ref} return &refConn{conn: NewExtendedConn(conn), ref: ref}
} }

View file

@ -3,8 +3,10 @@ package net
import ( import (
"crypto/rand" "crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/sha256"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/hex"
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"math/big" "math/big"
@ -16,7 +18,11 @@ type Path interface {
func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) { func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) {
if certificate == "" && privateKey == "" { if certificate == "" && privateKey == "" {
return newRandomTLSKeyPair() var err error
certificate, privateKey, _, err = NewRandomTLSKeyPair()
if err != nil {
return tls.Certificate{}, err
}
} }
cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey))
if painTextErr == nil { if painTextErr == nil {
@ -32,10 +38,10 @@ func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, erro
return cert, nil return cert, nil
} }
func newRandomTLSKeyPair() (tls.Certificate, error) { func NewRandomTLSKeyPair() (certificate string, privateKey string, fingerprint string, err error) {
key, err := rsa.GenerateKey(rand.Reader, 2048) key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil { if err != nil {
return tls.Certificate{}, err return
} }
template := x509.Certificate{SerialNumber: big.NewInt(1)} template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate( certDER, err := x509.CreateCertificate(
@ -45,14 +51,15 @@ func newRandomTLSKeyPair() (tls.Certificate, error) {
&key.PublicKey, &key.PublicKey,
key) key)
if err != nil { if err != nil {
return tls.Certificate{}, err return
} }
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}) cert, err := x509.ParseCertificate(certDER)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil { if err != nil {
return tls.Certificate{}, err return
} }
return tlsCert, nil hash := sha256.Sum256(cert.Raw)
fingerprint = hex.EncodeToString(hash[:])
privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)}))
certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER}))
return
} }

View file

@ -1,73 +0,0 @@
package nnip
import (
"encoding/binary"
"net"
"net/netip"
)
// IpToAddr converts the net.IP to netip.Addr.
// If slice's length is not 4 or 16, IpToAddr returns netip.Addr{}
func IpToAddr(slice net.IP) netip.Addr {
ip := slice
if len(ip) != 4 {
if ip = slice.To4(); ip == nil {
ip = slice
}
}
if addr, ok := netip.AddrFromSlice(ip); ok {
return addr
}
return netip.Addr{}
}
// UnMasked returns p's last IP address.
// If p is invalid, UnMasked returns netip.Addr{}
func UnMasked(p netip.Prefix) netip.Addr {
if !p.IsValid() {
return netip.Addr{}
}
buf := p.Addr().As16()
hi := binary.BigEndian.Uint64(buf[:8])
lo := binary.BigEndian.Uint64(buf[8:])
bits := p.Bits()
if bits <= 32 {
bits += 96
}
hi = hi | ^uint64(0)>>bits
lo = lo | ^(^uint64(0) << (128 - bits))
binary.BigEndian.PutUint64(buf[:8], hi)
binary.BigEndian.PutUint64(buf[8:], lo)
addr := netip.AddrFrom16(buf)
if p.Addr().Is4() {
return addr.Unmap()
}
return addr
}
// PrefixCompare returns an integer comparing two prefixes.
// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2.
// modify from https://github.com/golang/go/issues/61642#issuecomment-1848587909
func PrefixCompare(p, p2 netip.Prefix) int {
// compare by validity, address family and prefix base address
if c := p.Masked().Addr().Compare(p2.Masked().Addr()); c != 0 {
return c
}
// compare by prefix length
f1, f2 := p.Bits(), p2.Bits()
if f1 < f2 {
return -1
}
if f1 > f2 {
return 1
}
// compare by prefix address
return p.Addr().Compare(p2.Addr())
}

View file

@ -10,6 +10,7 @@ type Observable[T any] struct {
listener map[Subscription[T]]*Subscriber[T] listener map[Subscription[T]]*Subscriber[T]
mux sync.Mutex mux sync.Mutex
done bool done bool
stopCh chan struct{}
} }
func (o *Observable[T]) process() { func (o *Observable[T]) process() {
@ -31,6 +32,7 @@ func (o *Observable[T]) close() {
for _, sub := range o.listener { for _, sub := range o.listener {
sub.Close() sub.Close()
} }
close(o.stopCh)
} }
func (o *Observable[T]) Subscribe() (Subscription[T], error) { func (o *Observable[T]) Subscribe() (Subscription[T], error) {
@ -59,6 +61,7 @@ func NewObservable[T any](iter Iterable[T]) *Observable[T] {
observable := &Observable[T]{ observable := &Observable[T]{
iterable: iter, iterable: iter,
listener: map[Subscription[T]]*Subscriber[T]{}, listener: map[Subscription[T]]*Subscriber[T]{},
stopCh: make(chan struct{}),
} }
go observable.process() go observable.process()
return observable return observable

View file

@ -70,9 +70,11 @@ func TestObservable_SubscribeClosedSource(t *testing.T) {
src := NewObservable[int](iter) src := NewObservable[int](iter)
data, _ := src.Subscribe() data, _ := src.Subscribe()
<-data <-data
select {
_, closed := src.Subscribe() case <-src.stopCh:
assert.NotNil(t, closed) case <-time.After(time.Second):
assert.Fail(t, "timeout not stop")
}
} }
func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) { func TestObservable_UnSubscribeWithNotExistSubscription(t *testing.T) {

View file

@ -22,9 +22,10 @@ func sleepAndSend[T any](ctx context.Context, delay int, input T) func() (T, err
} }
func TestPicker_Basic(t *testing.T) { func TestPicker_Basic(t *testing.T) {
t.Parallel()
picker, ctx := WithContext[int](context.Background()) picker, ctx := WithContext[int](context.Background())
picker.Go(sleepAndSend(ctx, 30, 2)) picker.Go(sleepAndSend(ctx, 200, 2))
picker.Go(sleepAndSend(ctx, 20, 1)) picker.Go(sleepAndSend(ctx, 100, 1))
number := picker.Wait() number := picker.Wait()
assert.NotNil(t, number) assert.NotNil(t, number)
@ -32,8 +33,9 @@ func TestPicker_Basic(t *testing.T) {
} }
func TestPicker_Timeout(t *testing.T) { func TestPicker_Timeout(t *testing.T) {
t.Parallel()
picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5) picker, ctx := WithTimeout[int](context.Background(), time.Millisecond*5)
picker.Go(sleepAndSend(ctx, 20, 1)) picker.Go(sleepAndSend(ctx, 100, 1))
number := picker.Wait() number := picker.Wait()
assert.Equal(t, number, lo.Empty[int]()) assert.Equal(t, number, lo.Empty[int]())

View file

@ -13,7 +13,6 @@ type call[T any] struct {
type Single[T any] struct { type Single[T any] struct {
mux sync.Mutex mux sync.Mutex
last time.Time
wait time.Duration wait time.Duration
call *call[T] call *call[T]
result *Result[T] result *Result[T]
@ -22,16 +21,18 @@ type Single[T any] struct {
type Result[T any] struct { type Result[T any] struct {
Val T Val T
Err error Err error
Time time.Time
} }
// Do single.Do likes sync.singleFlight // Do single.Do likes sync.singleFlight
func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) { func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
s.mux.Lock() s.mux.Lock()
now := time.Now() result := s.result
if now.Before(s.last.Add(s.wait)) { if result != nil && time.Since(result.Time) < s.wait {
s.mux.Unlock() s.mux.Unlock()
return s.result.Val, s.result.Err, true return result.Val, result.Err, true
} }
s.result = nil // The result has expired, clear it
if callM := s.call; callM != nil { if callM := s.call; callM != nil {
s.mux.Unlock() s.mux.Unlock()
@ -47,15 +48,19 @@ func (s *Single[T]) Do(fn func() (T, error)) (v T, err error, shared bool) {
callM.wg.Done() callM.wg.Done()
s.mux.Lock() s.mux.Lock()
if s.call == callM { // maybe reset when fn is running
s.call = nil s.call = nil
s.result = &Result[T]{callM.val, callM.err} s.result = &Result[T]{callM.val, callM.err, time.Now()}
s.last = now }
s.mux.Unlock() s.mux.Unlock()
return callM.val, callM.err, false return callM.val, callM.err, false
} }
func (s *Single[T]) Reset() { func (s *Single[T]) Reset() {
s.last = time.Time{} s.mux.Lock()
s.call = nil
s.result = nil
s.mux.Unlock()
} }
func NewSingle[T any](wait time.Duration) *Single[T] { func NewSingle[T any](wait time.Duration) *Single[T] {

View file

@ -11,12 +11,13 @@ import (
) )
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
single := NewSingle[int](time.Millisecond * 30) t.Parallel()
single := NewSingle[int](time.Millisecond * 200)
foo := 0 foo := 0
shardCount := atomic.NewInt32(0) shardCount := atomic.NewInt32(0)
call := func() (int, error) { call := func() (int, error) {
foo++ foo++
time.Sleep(time.Millisecond * 5) time.Sleep(time.Millisecond * 20)
return 0, nil return 0, nil
} }
@ -39,7 +40,8 @@ func TestBasic(t *testing.T) {
} }
func TestTimer(t *testing.T) { func TestTimer(t *testing.T) {
single := NewSingle[int](time.Millisecond * 30) t.Parallel()
single := NewSingle[int](time.Millisecond * 200)
foo := 0 foo := 0
callM := func() (int, error) { callM := func() (int, error) {
foo++ foo++
@ -47,7 +49,7 @@ func TestTimer(t *testing.T) {
} }
_, _, _ = single.Do(callM) _, _, _ = single.Do(callM)
time.Sleep(10 * time.Millisecond) time.Sleep(100 * time.Millisecond)
_, _, shard := single.Do(callM) _, _, shard := single.Do(callM)
assert.Equal(t, 1, foo) assert.Equal(t, 1, foo)
@ -55,7 +57,8 @@ func TestTimer(t *testing.T) {
} }
func TestReset(t *testing.T) { func TestReset(t *testing.T) {
single := NewSingle[int](time.Millisecond * 30) t.Parallel()
single := NewSingle[int](time.Millisecond * 200)
foo := 0 foo := 0
callM := func() (int, error) { callM := func() (int, error) {
foo++ foo++

View file

@ -141,7 +141,6 @@ func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, cu
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConfig = GetGlobalTLSConfig(tlsConfig)
tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes) tlsConfig.VerifyPeerCertificate = verifyFingerprint(fingerprintBytes)
tlsConfig.InsecureSkipVerify = true tlsConfig.InsecureSkipVerify = true
} }

View file

@ -6,7 +6,6 @@ import (
"net" "net"
"net/netip" "net/netip"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/component/iface" "github.com/metacubex/mihomo/component/iface"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
@ -86,12 +85,14 @@ func receiveOffer(conn net.PacketConn, id dhcpv4.TransactionID, result chan<- []
return return
} }
dnsAddr := make([]netip.Addr, l) results := make([]netip.Addr, 0, len(dns))
for i := 0; i < l; i++ { for _, ip := range dns {
dnsAddr[i] = nnip.IpToAddr(dns[i]) if addr, ok := netip.AddrFromSlice(ip); ok {
results = append(results, addr.Unmap())
}
} }
result <- dnsAddr result <- results
return return
} }

View file

@ -7,14 +7,12 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/metacubex/mihomo/component/keepalive" "github.com/metacubex/mihomo/component/keepalive"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
"github.com/metacubex/mihomo/log"
) )
const ( const (
@ -22,34 +20,16 @@ const (
DefaultUDPTimeout = DefaultTCPTimeout DefaultUDPTimeout = DefaultTCPTimeout
) )
type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error)
var ( var (
dialMux sync.Mutex dialMux sync.Mutex
IP4PEnable bool
actualSingleStackDialContext = serialSingleStackDialContext actualSingleStackDialContext = serialSingleStackDialContext
actualDualStackDialContext = serialDualStackDialContext actualDualStackDialContext = serialDualStackDialContext
tcpConcurrent = false tcpConcurrent = false
fallbackTimeout = 300 * time.Millisecond fallbackTimeout = 300 * time.Millisecond
) )
func applyOptions(options ...Option) *option {
opt := &option{
interfaceName: DefaultInterface.Load(),
routingMark: int(DefaultRoutingMark.Load()),
}
for _, o := range DefaultOptions {
o(opt)
}
for _, o := range options {
o(opt)
}
return opt
}
func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) { func DialContext(ctx context.Context, network, address string, options ...Option) (net.Conn, error) {
opt := applyOptions(options...) opt := applyOptions(options...)
@ -79,38 +59,43 @@ func DialContext(ctx context.Context, network, address string, options ...Option
} }
func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) { func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.AddrPort, options ...Option) (net.PacketConn, error) {
cfg := applyOptions(options...) opt := applyOptions(options...)
lc := &net.ListenConfig{} lc := &net.ListenConfig{}
if cfg.addrReuse { if opt.addrReuse {
addrReuseToListenConfig(lc) addrReuseToListenConfig(lc)
} }
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA) if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA)
socketHookToListenConfig(lc) socketHookToListenConfig(lc)
} else { } else {
interfaceName := cfg.interfaceName if opt.interfaceName == "" {
if interfaceName == "" { opt.interfaceName = DefaultInterface.Load()
}
if opt.interfaceName == "" {
if finder := DefaultInterfaceFinder.Load(); finder != nil { if finder := DefaultInterfaceFinder.Load(); finder != nil {
interfaceName = finder.FindInterfaceName(rAddrPort.Addr()) opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
} }
} }
if rAddrPort.Addr().Unmap().IsLoopback() { if rAddrPort.Addr().Unmap().IsLoopback() {
// avoid "The requested address is not valid in its context." // avoid "The requested address is not valid in its context."
interfaceName = "" opt.interfaceName = ""
} }
if interfaceName != "" { if opt.interfaceName != "" {
bind := bindIfaceToListenConfig bind := bindIfaceToListenConfig
if cfg.fallbackBind { if opt.fallbackBind {
bind = fallbackBindIfaceToListenConfig bind = fallbackBindIfaceToListenConfig
} }
addr, err := bind(interfaceName, lc, network, address, rAddrPort) addr, err := bind(opt.interfaceName, lc, network, address, rAddrPort)
if err != nil { if err != nil {
return nil, err return nil, err
} }
address = addr address = addr
} }
if cfg.routingMark != 0 { if opt.routingMark == 0 {
bindMarkToListenConfig(cfg.routingMark, lc, network, address) opt.routingMark = int(DefaultRoutingMark.Load())
}
if opt.routingMark != 0 {
bindMarkToListenConfig(opt.routingMark, lc, network, address)
} }
} }
@ -136,11 +121,9 @@ func GetTcpConcurrent() bool {
return tcpConcurrent return tcpConcurrent
} }
func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt *option) (net.Conn, error) { func dialContext(ctx context.Context, network string, destination netip.Addr, port string, opt option) (net.Conn, error) {
var address string var address string
if IP4PEnable { destination, port = resolver.LookupIP4P(destination, port)
destination, port = lookupIP4P(destination, port)
}
address = net.JoinHostPort(destination.String(), port) address = net.JoinHostPort(destination.String(), port)
netDialer := opt.netDialer netDialer := opt.netDialer
@ -163,21 +146,26 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA) if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
socketHookToToDialer(dialer) socketHookToToDialer(dialer)
} else { } else {
interfaceName := opt.interfaceName // don't change the "opt", it's a pointer if opt.interfaceName == "" {
if interfaceName == "" { opt.interfaceName = DefaultInterface.Load()
}
if opt.interfaceName == "" {
if finder := DefaultInterfaceFinder.Load(); finder != nil { if finder := DefaultInterfaceFinder.Load(); finder != nil {
interfaceName = finder.FindInterfaceName(destination) opt.interfaceName = finder.FindInterfaceName(destination)
} }
} }
if interfaceName != "" { if opt.interfaceName != "" {
bind := bindIfaceToDialer bind := bindIfaceToDialer
if opt.fallbackBind { if opt.fallbackBind {
bind = fallbackBindIfaceToDialer bind = fallbackBindIfaceToDialer
} }
if err := bind(interfaceName, dialer, network, destination); err != nil { if err := bind(opt.interfaceName, dialer, network, destination); err != nil {
return nil, err return nil, err
} }
} }
if opt.routingMark == 0 {
opt.routingMark = int(DefaultRoutingMark.Load())
}
if opt.routingMark != 0 { if opt.routingMark != 0 {
bindMarkToDialer(opt.routingMark, dialer, network, destination) bindMarkToDialer(opt.routingMark, dialer, network, destination)
} }
@ -189,26 +177,26 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
return dialer.DialContext(ctx, network, address) return dialer.DialContext(ctx, network, address)
} }
func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func serialSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
return serialDialContext(ctx, network, ips, port, opt) return serialDialContext(ctx, network, ips, port, opt)
} }
func serialDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func serialDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt) return dualStackDialContext(ctx, serialDialContext, network, ips, port, opt)
} }
func concurrentSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func concurrentSingleStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
return parallelDialContext(ctx, network, ips, port, opt) return parallelDialContext(ctx, network, ips, port, opt)
} }
func concurrentDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func concurrentDualStackDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
if opt.prefer != 4 && opt.prefer != 6 { if opt.prefer != 4 && opt.prefer != 6 {
return parallelDialContext(ctx, network, ips, port, opt) return parallelDialContext(ctx, network, ips, port, opt)
} }
return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt) return dualStackDialContext(ctx, parallelDialContext, network, ips, port, opt)
} }
func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func dualStackDialContext(ctx context.Context, dialFn dialFunc, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
ipv4s, ipv6s := resolver.SortationAddr(ips) ipv4s, ipv6s := resolver.SortationAddr(ips)
if len(ipv4s) == 0 && len(ipv6s) == 0 { if len(ipv4s) == 0 && len(ipv6s) == 0 {
return nil, ErrorNoIpAddress return nil, ErrorNoIpAddress
@ -289,7 +277,7 @@ loop:
return nil, errors.Join(errs...) return nil, errors.Join(errs...)
} }
func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func parallelDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
if len(ips) == 0 { if len(ips) == 0 {
return nil, ErrorNoIpAddress return nil, ErrorNoIpAddress
} }
@ -328,7 +316,7 @@ func parallelDialContext(ctx context.Context, network string, ips []netip.Addr,
return nil, os.ErrDeadlineExceeded return nil, os.ErrDeadlineExceeded
} }
func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt *option) (net.Conn, error) { func serialDialContext(ctx context.Context, network string, ips []netip.Addr, port string, opt option) (net.Conn, error) {
if len(ips) == 0 { if len(ips) == 0 {
return nil, ErrorNoIpAddress return nil, ErrorNoIpAddress
} }
@ -394,23 +382,5 @@ func (d Dialer) ListenPacket(ctx context.Context, network, address string, rAddr
func NewDialer(options ...Option) Dialer { func NewDialer(options ...Option) Dialer {
opt := applyOptions(options...) opt := applyOptions(options...)
return Dialer{Opt: *opt} return Dialer{Opt: opt}
}
func GetIP4PEnable(enableIP4PConvert bool) {
IP4PEnable = enableIP4PConvert
}
// kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go
func lookupIP4P(addr netip.Addr, port string) (netip.Addr, string) {
ip := addr.AsSlice()
if ip[0] == 0x20 && ip[1] == 0x01 &&
ip[2] == 0x00 && ip[3] == 0x00 {
addr = netip.AddrFrom4([4]byte{ip[12], ip[13], ip[14], ip[15]})
port = strconv.Itoa(int(ip[10])<<8 + int(ip[11]))
log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr.String(), port))
return addr, port
}
return addr, port
} }

View file

@ -10,7 +10,6 @@ import (
) )
var ( var (
DefaultOptions []Option
DefaultInterface = atomic.NewTypedValue[string]("") DefaultInterface = atomic.NewTypedValue[string]("")
DefaultRoutingMark = atomic.NewInt32(0) DefaultRoutingMark = atomic.NewInt32(0)
@ -115,3 +114,15 @@ func WithOption(o option) Option {
*opt = o *opt = o
} }
} }
func IsZeroOptions(opts []Option) bool {
return applyOptions(opts...) == option{}
}
func applyOptions(options ...Option) option {
opt := option{}
for _, o := range options {
o(&opt)
}
return opt
}

View file

@ -6,9 +6,10 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/component/profile/cachefile" "github.com/metacubex/mihomo/component/profile/cachefile"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"go4.org/netipx"
) )
const ( const (
@ -183,7 +184,7 @@ func New(options Options) (*Pool, error) {
hostAddr = options.IPNet.Masked().Addr() hostAddr = options.IPNet.Masked().Addr()
gateway = hostAddr.Next() gateway = hostAddr.Next()
first = gateway.Next().Next().Next() // default start with 198.18.0.4 first = gateway.Next().Next().Next() // default start with 198.18.0.4
last = nnip.UnMasked(options.IPNet) last = netipx.PrefixLastIP(options.IPNet)
) )
if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) { if !options.IPNet.IsValid() || !first.IsValid() || !first.Less(last) {

View file

@ -69,7 +69,7 @@ func HttpRequestWithProxy(ctx context.Context, url, method string, header map[st
TLSHandshakeTimeout: 10 * time.Second, TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second, ExpectContinueTimeout: 1 * time.Second,
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
if conn, err := inner.HandleTcp(address, specialProxy); err == nil { if conn, err := inner.HandleTcp(inner.GetTunnel(), address, specialProxy); err == nil {
return conn, nil return conn, nil
} else { } else {
return dialer.DialContext(ctx, network, address) return dialer.DialContext(ctx, network, address)

View file

@ -59,7 +59,7 @@ func SetNetListenConfig(lc *net.ListenConfig) {
} }
func TCPKeepAlive(c net.Conn) { func TCPKeepAlive(c net.Conn) {
if tcp, ok := c.(*net.TCPConn); ok && tcp != nil { if tcp, ok := c.(TCPConn); ok && tcp != nil {
tcpKeepAlive(tcp) tcpKeepAlive(tcp)
} }
} }

View file

@ -2,9 +2,18 @@
package keepalive package keepalive
import "net" import (
"net"
"time"
)
func tcpKeepAlive(tcp *net.TCPConn) { type TCPConn interface {
net.Conn
SetKeepAlive(keepalive bool) error
SetKeepAlivePeriod(d time.Duration) error
}
func tcpKeepAlive(tcp TCPConn) {
if DisableKeepAlive() { if DisableKeepAlive() {
_ = tcp.SetKeepAlive(false) _ = tcp.SetKeepAlive(false)
} else { } else {

View file

@ -4,6 +4,12 @@ package keepalive
import "net" import "net"
type TCPConn interface {
net.Conn
SetKeepAlive(keepalive bool) error
SetKeepAliveConfig(config net.KeepAliveConfig) error
}
func keepAliveConfig() net.KeepAliveConfig { func keepAliveConfig() net.KeepAliveConfig {
config := net.KeepAliveConfig{ config := net.KeepAliveConfig{
Enable: true, Enable: true,
@ -18,7 +24,7 @@ func keepAliveConfig() net.KeepAliveConfig {
return config return config
} }
func tcpKeepAlive(tcp *net.TCPConn) { func tcpKeepAlive(tcp TCPConn) {
if DisableKeepAlive() { if DisableKeepAlive() {
_ = tcp.SetKeepAlive(false) _ = tcp.SetKeepAlive(false)
} else { } else {

View file

@ -87,6 +87,7 @@ func findProcessName(network string, ip netip.Addr, port int) (uint32, string, e
default: default:
continue continue
} }
srcIP = srcIP.Unmap()
if ip == srcIP { if ip == srcIP {
// xsocket_n.so_last_pid // xsocket_n.so_last_pid

View file

@ -10,7 +10,6 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
) )
@ -136,13 +135,14 @@ func (s *searcher) Search(buf []byte, ip netip.Addr, port uint16, isTCP bool) (u
switch { switch {
case flag&0x1 > 0 && isIPv4: case flag&0x1 > 0 && isIPv4:
// ipv4 // ipv4
srcIP = nnip.IpToAddr(buf[inp+s.ip : inp+s.ip+4]) srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip : inp+s.ip+4])
case flag&0x2 > 0 && !isIPv4: case flag&0x2 > 0 && !isIPv4:
// ipv6 // ipv6
srcIP = nnip.IpToAddr(buf[inp+s.ip-12 : inp+s.ip+4]) srcIP, _ = netip.AddrFromSlice(buf[inp+s.ip-12 : inp+s.ip+4])
default: default:
continue continue
} }
srcIP = srcIP.Unmap()
if ip != srcIP { if ip != srcIP {
continue continue

View file

@ -7,7 +7,6 @@ import (
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/sys/windows" "golang.org/x/sys/windows"
@ -137,7 +136,8 @@ func (s *searcher) Search(b []byte, ip netip.Addr, port uint16) (uint32, error)
continue continue
} }
srcIP := nnip.IpToAddr(row[s.ip : s.ip+s.ipSize]) srcIP, _ := netip.AddrFromSlice(row[s.ip : s.ip+s.ipSize])
srcIP = srcIP.Unmap()
// windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto // windows binds an unbound udp socket to 0.0.0.0/[::] while first sendto
if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) { if ip != srcIP && (!srcIP.IsUnspecified() || s.tcpState != -1) {
continue continue

View file

@ -0,0 +1,37 @@
package resolver
import (
"net"
"net/netip"
"strconv"
"github.com/metacubex/mihomo/log"
)
var (
ip4PEnable bool
)
func GetIP4PEnable() bool {
return ip4PEnable
}
func SetIP4PEnable(enableIP4PConvert bool) {
ip4PEnable = enableIP4PConvert
}
// kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go
func LookupIP4P(addr netip.Addr, port string) (netip.Addr, string) {
if ip4PEnable {
ip := addr.AsSlice()
if ip[0] == 0x20 && ip[1] == 0x01 &&
ip[2] == 0x00 && ip[3] == 0x00 {
addr = netip.AddrFrom4([4]byte{ip[12], ip[13], ip[14], ip[15]})
port = strconv.Itoa(int(ip[10])<<8 + int(ip[11]))
log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr.String(), port))
return addr, port
}
}
return addr, port
}

View file

@ -196,6 +196,26 @@ func ResolveIP(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPWithResolver(ctx, host, DefaultResolver) return ResolveIPWithResolver(ctx, host, DefaultResolver)
} }
// ResolveIPPrefer6WithResolver same as ResolveIP, but with a resolver
func ResolveIPPrefer6WithResolver(ctx context.Context, host string, r Resolver) (netip.Addr, error) {
ips, err := LookupIPWithResolver(ctx, host, r)
if err != nil {
return netip.Addr{}, err
} else if len(ips) == 0 {
return netip.Addr{}, fmt.Errorf("%w: %s", ErrIPNotFound, host)
}
ipv4s, ipv6s := SortationAddr(ips)
if len(ipv6s) > 0 {
return ipv6s[randv2.IntN(len(ipv6s))], nil
}
return ipv4s[randv2.IntN(len(ipv4s))], nil
}
// ResolveIPPrefer6 with a host, return ip and priority return TypeAAAA
func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver)
}
func ResetConnection() { func ResetConnection() {
if DefaultResolver != nil { if DefaultResolver != nil {
go DefaultResolver.ResetConnection() go DefaultResolver.ResetConnection()

View file

@ -399,9 +399,7 @@ func (q *quicPacketSender) readQuicData(b []byte) error {
} }
} }
_ = q.tryAssemble() return q.tryAssemble()
return nil
} }
func (q *quicPacketSender) tryAssemble() error { func (q *quicPacketSender) tryAssemble() error {
@ -415,7 +413,17 @@ func (q *quicPacketSender) tryAssemble() error {
if len(q.ranges) != 1 || q.ranges[0].Start() != 0 || q.ranges[0].End() != uint64(len(q.buffer)) { if len(q.ranges) != 1 || q.ranges[0].Start() != 0 || q.ranges[0].End() != uint64(len(q.buffer)) {
q.lock.RUnlock() q.lock.RUnlock()
return ErrNoClue // incomplete fragment, just return
return nil
}
if len(q.buffer) <= 4 ||
// Handshake Type (1) + uint24 Length (3) + ClientHello body
// maxCryptoStreamOffset is in the valid range of uint16 so just ignore the q.buffer[1]
int(binary.BigEndian.Uint16([]byte{q.buffer[2], q.buffer[3]})+4) != len(q.buffer) {
q.lock.RUnlock()
// end of segment not reached, just return
return nil
} }
domain, err := ReadClientHello(q.buffer) domain, err := ReadClientHello(q.buffer)

View file

@ -26,6 +26,7 @@ import (
utls "github.com/metacubex/utls" utls "github.com/metacubex/utls"
"golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf" "golang.org/x/crypto/hkdf"
"golang.org/x/exp/slices"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -36,9 +37,9 @@ type RealityConfig struct {
ShortID [RealityMaxShortIDLen]byte ShortID [RealityMaxShortIDLen]byte
} }
func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { func GetRealityConn(ctx context.Context, conn net.Conn, clientFingerprint string, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) {
retry := 0 retry := 0
for fingerprint, exists := GetFingerprint(ClientFingerprint); exists; retry++ { for fingerprint, exists := GetFingerprint(clientFingerprint); exists; retry++ {
verifier := &realityVerifier{ verifier := &realityVerifier{
serverName: tlsConfig.ServerName, serverName: tlsConfig.ServerName,
} }
@ -60,6 +61,27 @@ func GetRealityConn(ctx context.Context, conn net.Conn, ClientFingerprint string
return nil, err return nil, err
} }
// ------for X25519MLKEM768 does not work properly with reality-------
// Iterate over extensions and check
for _, extension := range uConn.Extensions {
if ce, ok := extension.(*utls.SupportedCurvesExtension); ok {
ce.Curves = slices.DeleteFunc(ce.Curves, func(curveID utls.CurveID) bool {
return curveID == utls.X25519MLKEM768
})
}
if ks, ok := extension.(*utls.KeyShareExtension); ok {
ks.KeyShares = slices.DeleteFunc(ks.KeyShares, func(share utls.KeyShare) bool {
return share.Group == utls.X25519MLKEM768
})
}
}
// Rebuild the client hello
err = uConn.BuildHandshakeState()
if err != nil {
return nil, err
}
// --------------------------------------------------------------------
hello := uConn.HandshakeState.Hello hello := uConn.HandshakeState.Hello
rawSessionID := hello.Raw[39 : 39+32] // the location of session ID rawSessionID := hello.Raw[39 : 39+32] // the location of session ID
for i := range rawSessionID { // https://github.com/golang/go/issues/5373 for i := range rawSessionID { // https://github.com/golang/go/issues/5373

View file

@ -279,6 +279,10 @@ type RawTun struct {
IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"`
ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"`
ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"`
ExcludeSrcPort []uint16 `yaml:"exclude-src-port" json:"exclude-src-port,omitempty"`
ExcludeSrcPortRange []string `yaml:"exclude-src-port-range" json:"exclude-src-port-range,omitempty"`
ExcludeDstPort []uint16 `yaml:"exclude-dst-port" json:"exclude-dst-port,omitempty"`
ExcludeDstPortRange []string `yaml:"exclude-dst-port-range" json:"exclude-dst-port-range,omitempty"`
IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"`
IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"`
ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"`
@ -1560,6 +1564,10 @@ func parseTun(rawTun RawTun, general *General) error {
IncludeUIDRange: rawTun.IncludeUIDRange, IncludeUIDRange: rawTun.IncludeUIDRange,
ExcludeUID: rawTun.ExcludeUID, ExcludeUID: rawTun.ExcludeUID,
ExcludeUIDRange: rawTun.ExcludeUIDRange, ExcludeUIDRange: rawTun.ExcludeUIDRange,
ExcludeSrcPort: rawTun.ExcludeSrcPort,
ExcludeSrcPortRange: rawTun.ExcludeSrcPortRange,
ExcludeDstPort: rawTun.ExcludeDstPort,
ExcludeDstPortRange: rawTun.ExcludeDstPortRange,
IncludeAndroidUser: rawTun.IncludeAndroidUser, IncludeAndroidUser: rawTun.IncludeAndroidUser,
IncludePackage: rawTun.IncludePackage, IncludePackage: rawTun.IncludePackage,
ExcludePackage: rawTun.ExcludePackage, ExcludePackage: rawTun.ExcludePackage,

View file

@ -149,6 +149,9 @@ type ProxyAdapter interface {
// Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract. // Unwrap extracts the proxy from a proxy-group. It returns nil when nothing to extract.
Unwrap(metadata *Metadata, touch bool) Proxy Unwrap(metadata *Metadata, touch bool) Proxy
// Close releasing associated resources
Close() error
} }
type Group interface { type Group interface {

View file

@ -6,11 +6,15 @@ import (
"net" "net"
"net/netip" "net/netip"
"strconv" "strconv"
"github.com/metacubex/mihomo/transport/socks5"
) )
// Socks addr type // SOCKS address types as defined in RFC 1928 section 5.
const (
AtypIPv4 AddrType = 1
AtypDomainName AddrType = 3
AtypIPv6 AddrType = 4
)
const ( const (
TCP NetWork = iota TCP NetWork = iota
UDP UDP
@ -37,6 +41,21 @@ const (
INNER INNER
) )
type AddrType byte
func (a AddrType) String() string {
switch a {
case AtypIPv4:
return "IPv4"
case AtypDomainName:
return "DomainName"
case AtypIPv6:
return "IPv6"
default:
return "Unknown"
}
}
type NetWork int type NetWork int
func (n NetWork) String() string { func (n NetWork) String() string {
@ -207,14 +226,14 @@ func (m *Metadata) SourceValid() bool {
return m.SrcPort != 0 && m.SrcIP.IsValid() return m.SrcPort != 0 && m.SrcIP.IsValid()
} }
func (m *Metadata) AddrType() int { func (m *Metadata) AddrType() AddrType {
switch true { switch true {
case m.Host != "" || !m.DstIP.IsValid(): case m.Host != "" || !m.DstIP.IsValid():
return socks5.AtypDomainName return AtypDomainName
case m.DstIP.Is4(): case m.DstIP.Is4():
return socks5.AtypIPv4 return AtypIPv4
default: default:
return socks5.AtypIPv6 return AtypIPv6
} }
} }

View file

@ -6,7 +6,6 @@ import (
"time" "time"
"github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/common/lru"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/fakeip"
R "github.com/metacubex/mihomo/component/resolver" R "github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@ -120,14 +119,21 @@ func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware {
switch a := ans.(type) { switch a := ans.(type) {
case *D.A: case *D.A:
ip = nnip.IpToAddr(a.A) ip, _ = netip.AddrFromSlice(a.A)
ttl = a.Hdr.Ttl ttl = a.Hdr.Ttl
case *D.AAAA: case *D.AAAA:
ip = nnip.IpToAddr(a.AAAA) ip, _ = netip.AddrFromSlice(a.AAAA)
ttl = a.Hdr.Ttl ttl = a.Hdr.Ttl
default: default:
continue continue
} }
if !ip.IsValid() {
continue
}
if !ip.IsGlobalUnicast() {
continue
}
ip = ip.Unmap()
if ttl < 1 { if ttl < 1 {
ttl = 1 ttl = 1

View file

@ -19,6 +19,16 @@ func dnsReadConfig() (servers []string, err error) {
return return
} }
for _, aa := range aas { for _, aa := range aas {
// Only take interfaces whose OperStatus is IfOperStatusUp(0x01) into DNS configs.
if aa.OperStatus != windows.IfOperStatusUp {
continue
}
// Only take interfaces which have at least one gateway
if aa.FirstGatewayAddress == nil {
continue
}
for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next { for dns := aa.FirstDnsServerAddress; dns != nil; dns = dns.Next {
sa, err := dns.Address.Sockaddr.Sockaddr() sa, err := dns.Address.Sockaddr.Sockaddr()
if err != nil { if err != nil {
@ -63,7 +73,8 @@ func adapterAddresses() ([]*windows.IpAdapterAddresses, error) {
l := uint32(15000) // recommended initial size l := uint32(15000) // recommended initial size
for { for {
b = make([]byte, l) b = make([]byte, l)
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) const flags = windows.GAA_FLAG_INCLUDE_PREFIX | windows.GAA_FLAG_INCLUDE_GATEWAYS
err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, flags, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l)
if err == nil { if err == nil {
if l == 0 { if l == 0 {
return nil, nil return nil, nil

View file

@ -10,7 +10,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/metacubex/mihomo/common/nnip"
"github.com/metacubex/mihomo/common/picker" "github.com/metacubex/mihomo/common/picker"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
@ -150,19 +149,24 @@ func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg {
return msg return msg
} }
func msgToIP(msg *D.Msg) []netip.Addr { func msgToIP(msg *D.Msg) (ips []netip.Addr) {
ips := []netip.Addr{}
for _, answer := range msg.Answer { for _, answer := range msg.Answer {
var ip netip.Addr
switch ans := answer.(type) { switch ans := answer.(type) {
case *D.AAAA: case *D.AAAA:
ips = append(ips, nnip.IpToAddr(ans.AAAA)) ip, _ = netip.AddrFromSlice(ans.AAAA)
case *D.A: case *D.A:
ips = append(ips, nnip.IpToAddr(ans.A)) ip, _ = netip.AddrFromSlice(ans.A)
default:
continue
} }
if !ip.IsValid() {
continue
} }
ip = ip.Unmap()
return ips ips = append(ips, ip)
}
return
} }
func msgToDomain(msg *D.Msg) string { func msgToDomain(msg *D.Msg) string {

View file

@ -1298,6 +1298,31 @@ listeners:
# password: "example" # password: "example"
### 注意对于trojan listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “ss-option” 的其中一项 ### ### 注意对于trojan listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 或 “ss-option” 的其中一项 ###
- name: hysteria2-in-1
type: hysteria2
port: 10820 # 支持使用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:
00000000-0000-0000-0000-000000000000: PASSWORD_0
00000000-0000-0000-0000-000000000001: PASSWORD_1
# certificate: ./server.crt
# private-key: ./server.key
## up 和 down 均不写或为 0 则使用 BBR 流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps
# obfs: salamander # 默认为空,如果填写则开启 obfs目前仅支持 salamander
# obfs-password: yourpassword
# max-idle-time: 15000
# alpn:
# - h3
# ignore-client-bandwidth: false
# HTTP3 服务器认证失败时的行为 URL 字符串配置),如果 masquerade 未配置,则返回 404 页
# masquerade: file:///var/www # 作为文件服务器
# masquerade: http://127.0.0.1:8080 #作为反向代理
# masquerade: https://127.0.0.1:8080 #作为反向代理
- name: tun-in-1 - name: tun-in-1
type: tun type: tun
# rule: sub-rule-name1 # 默认使用 rules如果未找到 sub-rule 则直接使用 rules # rule: sub-rule-name1 # 默认使用 rules如果未找到 sub-rule 则直接使用 rules

11
go.mod
View file

@ -20,19 +20,19 @@ require (
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab
github.com/metacubex/bart v0.19.0 github.com/metacubex/bart v0.19.0
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
github.com/metacubex/chacha v0.1.1 github.com/metacubex/chacha v0.1.2
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629 github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925
github.com/metacubex/sing-shadowsocks v0.2.8 github.com/metacubex/sing-shadowsocks v0.2.8
github.com/metacubex/sing-shadowsocks2 v0.2.2 github.com/metacubex/sing-shadowsocks2 v0.2.2
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
github.com/metacubex/utls v1.6.8-alpha.4 github.com/metacubex/utls v1.6.8-alpha.6
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181
github.com/miekg/dns v1.1.63 github.com/miekg/dns v1.1.63
github.com/mroth/weightedrand/v2 v2.1.0 github.com/mroth/weightedrand/v2 v2.1.0
@ -44,7 +44,6 @@ require (
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
github.com/sagernet/sing v0.5.2 github.com/sagernet/sing v0.5.2
github.com/sagernet/sing-mux v0.2.1 github.com/sagernet/sing-mux v0.2.1
github.com/sagernet/sing-shadowtls v0.1.5
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/samber/lo v1.49.1 github.com/samber/lo v1.49.1
github.com/shirou/gopsutil/v4 v4.25.1 // lastest version compatible with golang1.20 github.com/shirou/gopsutil/v4 v4.25.1 // lastest version compatible with golang1.20

22
go.sum
View file

@ -101,8 +101,8 @@ github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro= github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c= github.com/metacubex/chacha v0.1.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc=
github.com/metacubex/chacha v0.1.1/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
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=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM9MQ2ySuqu2aeBqcA8rtfWUYLZ8RtI=
@ -111,26 +111,26 @@ github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996 h1:B+AP/Pj2/j
github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY= github.com/metacubex/quic-go v0.49.1-0.20250212162123-c135a4412996/go.mod h1:ExVjGyEwTUjCFqx+5uxgV7MOoA3fZI+th4D40H35xmY=
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/reality v0.0.0-20250219003814-74e8d7850629 h1:aHsYiTvubfgMa3JMTDY//hDXVvFWrHg6ARckR52ttZs=
github.com/metacubex/reality v0.0.0-20250219003814-74e8d7850629/go.mod h1:TTeIOZLdGmzc07Oedn++vWUUfkZoXLF4sEMxWuhBFr8=
github.com/metacubex/sing v0.0.0-20250228041610-d94509dc612a h1:xjPXdDTlIKq4U/KnKpoCtkxD03T8GimtQrvHy/3dN00= github.com/metacubex/sing v0.0.0-20250228041610-d94509dc612a h1:xjPXdDTlIKq4U/KnKpoCtkxD03T8GimtQrvHy/3dN00=
github.com/metacubex/sing v0.0.0-20250228041610-d94509dc612a/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/metacubex/sing v0.0.0-20250228041610-d94509dc612a/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA= github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c h1:OB3WmMA8YPJjE36RjD9X8xlrWGJ4orxbf2R/KAE28b0=
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-quic v0.0.0-20250404030904-b2cc8aab562c/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4=
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg= github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63 h1:vy/8ZYYtWUXYnOnw/NF8ThG1W/RqM/h5rkun+OXZMH0=
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= github.com/metacubex/sing-shadowtls v0.0.0-20250412122235-0e9005731a63/go.mod h1:eDZ2JpkSkewGmUlCoLSn2MRFn1D0jKPIys/6aogFx7U=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5 h1:hcsz5e5lqhBxn3iQQDIF60FLZ8PQT542GTQZ+1wcIGo=
github.com/metacubex/sing-tun v0.4.6-0.20250412144348-c426cb167db5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ= github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.6.8-alpha.4 h1:5EvsCHxDNneaOtAyc8CztoNSpmonLvkvuGs01lIeeEI= github.com/metacubex/utls v1.6.8-alpha.6 h1:5ZdZNiZFkKKgEcuPOOROIc8bA4dX2VJHoY3gajSnSaU=
github.com/metacubex/utls v1.6.8-alpha.4/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk= github.com/metacubex/utls v1.6.8-alpha.6/go.mod h1:MEZ5WO/VLKYs/s/dOzEK/mlXOQxc04ESeLzRgjmLYtk=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 h1:hJLQviGySBuaynlCwf/oYgIxbVbGRUIKZCxdya9YrbQ=
github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y= github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181/go.mod h1:phewKljNYiTVT31Gcif8RiCKnTUOgVWFJjccqYM8s+Y=
github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
@ -174,8 +174,6 @@ github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNen
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo= github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo=
github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE= github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE=
github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo=
github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=

View file

@ -221,7 +221,7 @@ func updateExperimental(c *config.Experimental) {
if c.QUICGoDisableECN { if c.QUICGoDisableECN {
_ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true)) _ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true))
} }
dialer.GetIP4PEnable(c.IP4PEnable) resolver.SetIP4PEnable(c.IP4PEnable)
} }
func updateNTP(c *config.NTP) { func updateNTP(c *config.NTP) {

View file

@ -86,6 +86,11 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig)
} else {
return nil, errors.New("disallow using AnyTLS without certificates config")
}
sl.listeners = append(sl.listeners, l) sl.listeners = append(sl.listeners, l)
go func() { go func() {
@ -130,8 +135,6 @@ func (l *Listener) AddrList() (addrList []net.Addr) {
func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) { func (l *Listener) HandleConn(conn net.Conn, h *sing.ListenerHandler) {
ctx := context.TODO() ctx := context.TODO()
conn = tls.Server(conn, l.tlsConfig)
defer conn.Close() defer conn.Close()
b := buf.NewPacket() b := buf.NewPacket()

View file

@ -3,9 +3,9 @@ package config
import ( import (
"net/netip" "net/netip"
"github.com/metacubex/mihomo/common/nnip"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"go4.org/netipx"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -50,6 +50,10 @@ type Tun struct {
IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"` IncludeUIDRange []string `yaml:"include-uid-range" json:"include-uid-range,omitempty"`
ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"` ExcludeUID []uint32 `yaml:"exclude-uid" json:"exclude-uid,omitempty"`
ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"` ExcludeUIDRange []string `yaml:"exclude-uid-range" json:"exclude-uid-range,omitempty"`
ExcludeSrcPort []uint16 `yaml:"exclude-src-port" json:"exclude-src-port,omitempty"`
ExcludeSrcPortRange []string `yaml:"exclude-src-port-range" json:"exclude-src-port-range,omitempty"`
ExcludeDstPort []uint16 `yaml:"exclude-dst-port" json:"exclude-dst-port,omitempty"`
ExcludeDstPortRange []string `yaml:"exclude-dst-port-range" json:"exclude-dst-port-range,omitempty"`
IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"` IncludeAndroidUser []int `yaml:"include-android-user" json:"include-android-user,omitempty"`
IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"` IncludePackage []string `yaml:"include-package" json:"include-package,omitempty"`
ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"` ExcludePackage []string `yaml:"exclude-package" json:"exclude-package,omitempty"`
@ -66,11 +70,11 @@ type Tun struct {
func (t *Tun) Sort() { func (t *Tun) Sort() {
slices.Sort(t.DNSHijack) slices.Sort(t.DNSHijack)
slices.SortFunc(t.Inet4Address, nnip.PrefixCompare) slices.SortFunc(t.Inet4Address, netipx.ComparePrefix)
slices.SortFunc(t.Inet6Address, nnip.PrefixCompare) slices.SortFunc(t.Inet6Address, netipx.ComparePrefix)
slices.SortFunc(t.RouteAddress, nnip.PrefixCompare) slices.SortFunc(t.RouteAddress, netipx.ComparePrefix)
slices.Sort(t.RouteAddressSet) slices.Sort(t.RouteAddressSet)
slices.SortFunc(t.RouteExcludeAddress, nnip.PrefixCompare) slices.SortFunc(t.RouteExcludeAddress, netipx.ComparePrefix)
slices.Sort(t.RouteExcludeAddressSet) slices.Sort(t.RouteExcludeAddressSet)
slices.Sort(t.IncludeInterface) slices.Sort(t.IncludeInterface)
slices.Sort(t.ExcludeInterface) slices.Sort(t.ExcludeInterface)
@ -82,10 +86,10 @@ func (t *Tun) Sort() {
slices.Sort(t.IncludePackage) slices.Sort(t.IncludePackage)
slices.Sort(t.ExcludePackage) slices.Sort(t.ExcludePackage)
slices.SortFunc(t.Inet4RouteAddress, nnip.PrefixCompare) slices.SortFunc(t.Inet4RouteAddress, netipx.ComparePrefix)
slices.SortFunc(t.Inet6RouteAddress, nnip.PrefixCompare) slices.SortFunc(t.Inet6RouteAddress, netipx.ComparePrefix)
slices.SortFunc(t.Inet4RouteExcludeAddress, nnip.PrefixCompare) slices.SortFunc(t.Inet4RouteExcludeAddress, netipx.ComparePrefix)
slices.SortFunc(t.Inet6RouteExcludeAddress, nnip.PrefixCompare) slices.SortFunc(t.Inet6RouteExcludeAddress, netipx.ComparePrefix)
} }
func (t *Tun) Equal(other Tun) bool { func (t *Tun) Equal(other Tun) bool {

View file

@ -78,7 +78,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if tlsConfig.Certificates != nil { if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality") return nil, errors.New("certificate is unavailable in reality")
} }
realityBuilder, err = config.RealityConfig.Build() realityBuilder, err = config.RealityConfig.Build(tunnel)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,63 @@
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 testInboundAnyTLS(t *testing.T, inboundOptions inbound.AnyTLSOption, outboundOptions outbound.AnyTLSOption) {
t.Parallel()
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "anytls_inbound",
Listen: "127.0.0.1",
Port: "0",
}
inboundOptions.Users = map[string]string{"test": userUUID}
in, err := inbound.NewAnyTLS(&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 = "anytls_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
outboundOptions.Password = userUUID
out, err := outbound.NewAnyTLS(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
}
func TestInboundAnyTLS_TLS(t *testing.T) {
inboundOptions := inbound.AnyTLSOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
}
outboundOptions := outbound.AnyTLSOption{
Fingerprint: tlsFingerprint,
}
testInboundAnyTLS(t, inboundOptions, outboundOptions)
}

View file

@ -0,0 +1,251 @@
package inbound_test
import (
"context"
"crypto/rand"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"sync"
"testing"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/generater"
C "github.com/metacubex/mihomo/constant"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/stretchr/testify/assert"
)
var httpPath = "/inbound_test"
var httpData = make([]byte, 10240)
var remoteAddr = netip.MustParseAddr("1.2.3.4")
var userUUID = utils.NewUUIDV4().String()
var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = N.NewRandomTLSKeyPair()
var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey))
var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}}
var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "")
var realityPrivateKey, realityPublickey string
var realityDest = "itunes.apple.com"
var realityShortid = "10f897e26c4b9478"
func init() {
rand.Read(httpData)
privateKey, err := generater.GeneratePrivateKey()
if err != nil {
panic(err)
}
publicKey := privateKey.PublicKey()
realityPrivateKey = base64.RawURLEncoding.EncodeToString(privateKey[:])
realityPublickey = base64.RawURLEncoding.EncodeToString(publicKey[:])
}
type TestTunnel struct {
HandleTCPConnFn func(conn net.Conn, metadata *C.Metadata)
HandleUDPPacketFn func(packet C.UDPPacket, metadata *C.Metadata)
NatTableFn func() C.NatTable
CloseFn func() error
DoTestFn func(t *testing.T, proxy C.ProxyAdapter)
}
func (tt *TestTunnel) HandleTCPConn(conn net.Conn, metadata *C.Metadata) {
tt.HandleTCPConnFn(conn, metadata)
}
func (tt *TestTunnel) HandleUDPPacket(packet C.UDPPacket, metadata *C.Metadata) {
tt.HandleUDPPacketFn(packet, metadata)
}
func (tt *TestTunnel) NatTable() C.NatTable {
return tt.NatTableFn()
}
func (tt *TestTunnel) Close() error {
return tt.CloseFn()
}
func (tt *TestTunnel) DoTest(t *testing.T, proxy C.ProxyAdapter) {
tt.DoTestFn(t, proxy)
}
type TestTunnelListener struct {
ch chan net.Conn
ctx context.Context
cancel context.CancelFunc
addr net.Addr
}
func (t *TestTunnelListener) Accept() (net.Conn, error) {
select {
case conn, ok := <-t.ch:
if !ok {
return nil, net.ErrClosed
}
return conn, nil
case <-t.ctx.Done():
return nil, t.ctx.Err()
}
}
func (t *TestTunnelListener) Close() error {
t.cancel()
return nil
}
func (t *TestTunnelListener) Addr() net.Addr {
return t.addr
}
type WaitCloseConn struct {
net.Conn
ch chan struct{}
once sync.Once
}
func (c *WaitCloseConn) Close() error {
err := c.Conn.Close()
c.once.Do(func() {
close(c.ch)
})
return err
}
var _ C.Tunnel = (*TestTunnel)(nil)
var _ net.Listener = (*TestTunnelListener)(nil)
func NewHttpTestTunnel() *TestTunnel {
ctx, cancel := context.WithCancel(context.Background())
ln := &TestTunnelListener{ch: make(chan net.Conn), ctx: ctx, cancel: cancel, addr: net.TCPAddrFromAddrPort(netip.AddrPortFrom(remoteAddr, 0))}
r := chi.NewRouter()
r.Get(httpPath, func(w http.ResponseWriter, r *http.Request) {
render.Data(w, r, httpData)
})
go http.Serve(ln, r)
testFn := func(t *testing.T, proxy C.ProxyAdapter, proto string) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s://%s%s", proto, remoteAddr, httpPath), nil)
if !assert.NoError(t, err) {
return
}
req = req.WithContext(ctx)
var dstPort uint16 = 80
if proto == "https" {
dstPort = 443
}
metadata := &C.Metadata{
NetWork: C.TCP,
DstIP: remoteAddr,
DstPort: dstPort,
}
instance, err := proxy.DialContext(ctx, metadata)
if !assert.NoError(t, err) {
return
}
defer instance.Close()
transport := &http.Transport{
DialContext: func(context.Context, string, string) (net.Conn, error) {
return instance, nil
},
// from http.DefaultTransport
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
// for our self-signed cert
TLSClientConfig: tlsClientConfig.Clone(),
// open http2
ForceAttemptHTTP2: true,
}
client := http.Client{
Timeout: 30 * time.Second,
Transport: transport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
defer client.CloseIdleConnections()
resp, err := client.Do(req)
if !assert.NoError(t, err) {
return
}
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
data, err := io.ReadAll(resp.Body)
if !assert.NoError(t, err) {
return
}
assert.Equal(t, httpData, data)
}
tunnel := &TestTunnel{
HandleTCPConnFn: func(conn net.Conn, metadata *C.Metadata) {
defer conn.Close()
if metadata.DstIP != remoteAddr && metadata.Host != realityDest {
return // not match, just return
}
c := &WaitCloseConn{
Conn: conn,
ch: make(chan struct{}),
}
if metadata.DstPort == 443 {
tlsConn := tls.Server(c, tlsConfig.Clone())
if metadata.Host == realityDest { // ignore the tls handshake error for realityDest
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
if err := tlsConn.HandshakeContext(ctx); err != nil {
return
}
}
ln.ch <- tlsConn
} else {
ln.ch <- c
}
<-c.ch
},
CloseFn: ln.Close,
DoTestFn: func(t *testing.T, proxy C.ProxyAdapter) {
// Sequential testing for debugging
t.Run("Sequential", func(t *testing.T) {
testFn(t, proxy, "http")
testFn(t, proxy, "https")
})
// Concurrent testing to detect stress
t.Run("Concurrent", func(t *testing.T) {
wg := sync.WaitGroup{}
const num = 50
for i := 0; i < num; i++ {
wg.Add(1)
go func() {
testFn(t, proxy, "https")
defer wg.Done()
}()
}
for i := 0; i < num; i++ {
wg.Add(1)
go func() {
testFn(t, proxy, "http")
defer wg.Done()
}()
}
wg.Wait()
})
},
}
return tunnel
}

View file

@ -0,0 +1,93 @@
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 testInboundHysteria2(t *testing.T, inboundOptions inbound.Hysteria2Option, outboundOptions outbound.Hysteria2Option) {
t.Parallel()
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "hysteria2_inbound",
Listen: "127.0.0.1",
Port: "0",
}
inboundOptions.Users = map[string]string{"test": userUUID}
in, err := inbound.NewHysteria2(&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 = "hysteria2_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
outboundOptions.Password = userUUID
out, err := outbound.NewHysteria2(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
}
func TestInboundHysteria2_TLS(t *testing.T) {
inboundOptions := inbound.Hysteria2Option{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
}
outboundOptions := outbound.Hysteria2Option{
Fingerprint: tlsFingerprint,
}
testInboundHysteria2(t, inboundOptions, outboundOptions)
}
func TestInboundHysteria2_Salamander(t *testing.T) {
inboundOptions := inbound.Hysteria2Option{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
Obfs: "salamander",
ObfsPassword: userUUID,
}
outboundOptions := outbound.Hysteria2Option{
Fingerprint: tlsFingerprint,
Obfs: "salamander",
ObfsPassword: userUUID,
}
testInboundHysteria2(t, inboundOptions, outboundOptions)
}
func TestInboundHysteria2_Brutal(t *testing.T) {
inboundOptions := inbound.Hysteria2Option{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
Up: "30 Mbps",
Down: "200 Mbps",
}
outboundOptions := outbound.Hysteria2Option{
Fingerprint: tlsFingerprint,
Up: "30 Mbps",
Down: "200 Mbps",
}
testInboundHysteria2(t, inboundOptions, outboundOptions)
}

View file

@ -0,0 +1,44 @@
package inbound_test
import (
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/stretchr/testify/assert"
)
var singMuxProtocolList = []string{"h2mux", "smux"} // don't test "yamux" because it has some confused bugs
// notCloseProxyAdapter is a proxy adapter that does not close the underlying outbound.ProxyAdapter.
// The outbound.SingMux will close the underlying outbound.ProxyAdapter when it is closed, but we don't want to close it.
// The underlying outbound.ProxyAdapter should only be closed by the caller of testSingMux.
type notCloseProxyAdapter struct {
outbound.ProxyAdapter
}
func (n *notCloseProxyAdapter) Close() error {
return nil
}
func testSingMux(t *testing.T, tunnel *TestTunnel, out outbound.ProxyAdapter) {
t.Run("singmux", func(t *testing.T) {
for _, protocol := range singMuxProtocolList {
protocol := protocol
t.Run(protocol, func(t *testing.T) {
t.Parallel()
singMuxOption := outbound.SingMuxOption{
Enabled: true,
Protocol: protocol,
}
out, err := outbound.NewSingMux(singMuxOption, &notCloseProxyAdapter{out})
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
})
}
})
}

View file

@ -0,0 +1,98 @@
package inbound_test
import (
"crypto/rand"
"encoding/base64"
"net/netip"
"strings"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
shadowsocks "github.com/metacubex/sing-shadowsocks"
"github.com/metacubex/sing-shadowsocks/shadowaead"
"github.com/metacubex/sing-shadowsocks/shadowaead_2022"
"github.com/metacubex/sing-shadowsocks/shadowstream"
"github.com/stretchr/testify/assert"
)
var shadowsocksCipherList = []string{shadowsocks.MethodNone}
var shadowsocksPassword32 string
var shadowsocksPassword16 string
func init() {
shadowsocksCipherList = append(shadowsocksCipherList, shadowaead.List...)
shadowsocksCipherList = append(shadowsocksCipherList, shadowaead_2022.List...)
shadowsocksCipherList = append(shadowsocksCipherList, shadowstream.List...)
passwordBytes := make([]byte, 32)
rand.Read(passwordBytes)
shadowsocksPassword32 = base64.StdEncoding.EncodeToString(passwordBytes)
shadowsocksPassword16 = base64.StdEncoding.EncodeToString(passwordBytes[:16])
}
func testInboundShadowSocks(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) {
t.Parallel()
for _, cipher := range shadowsocksCipherList {
cipher := cipher
t.Run(cipher, func(t *testing.T) {
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
inboundOptions.Cipher = cipher
outboundOptions.Cipher = cipher
testInboundShadowSocks0(t, inboundOptions, outboundOptions)
})
}
}
func testInboundShadowSocks0(t *testing.T, inboundOptions inbound.ShadowSocksOption, outboundOptions outbound.ShadowSocksOption) {
t.Parallel()
password := shadowsocksPassword32
if strings.Contains(inboundOptions.Cipher, "-128-") {
password = shadowsocksPassword16
}
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "shadowsocks_inbound",
Listen: "127.0.0.1",
Port: "0",
}
inboundOptions.Password = password
in, err := inbound.NewShadowSocks(&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 = "shadowsocks_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
outboundOptions.Password = password
out, err := outbound.NewShadowSocks(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
testSingMux(t, tunnel, out)
}
func TestInboundShadowSocks_Basic(t *testing.T) {
inboundOptions := inbound.ShadowSocksOption{}
outboundOptions := outbound.ShadowSocksOption{}
testInboundShadowSocks(t, inboundOptions, outboundOptions)
}

View file

@ -0,0 +1,219 @@
package inbound_test
import (
"net"
"net/netip"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/stretchr/testify/assert"
)
func testInboundTrojan(t *testing.T, inboundOptions inbound.TrojanOption, outboundOptions outbound.TrojanOption) {
t.Parallel()
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "trojan_inbound",
Listen: "127.0.0.1",
Port: "0",
}
inboundOptions.Users = []inbound.TrojanUser{
{Username: "test", Password: userUUID},
}
in, err := inbound.NewTrojan(&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 = "trojan_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
outboundOptions.Password = userUUID
out, err := outbound.NewTrojan(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
testSingMux(t, tunnel, out)
}
func TestInboundTrojan_TLS(t *testing.T) {
inboundOptions := inbound.TrojanOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
}
outboundOptions := outbound.TrojanOption{
Fingerprint: tlsFingerprint,
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_Wss1(t *testing.T) {
inboundOptions := inbound.TrojanOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
}
outboundOptions := outbound.TrojanOption{
Fingerprint: tlsFingerprint,
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_Wss2(t *testing.T) {
inboundOptions := inbound.TrojanOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
GrpcServiceName: "GunService",
}
outboundOptions := outbound.TrojanOption{
Fingerprint: tlsFingerprint,
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_Grpc1(t *testing.T) {
inboundOptions := inbound.TrojanOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
GrpcServiceName: "GunService",
}
outboundOptions := outbound.TrojanOption{
Fingerprint: tlsFingerprint,
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_Grpc2(t *testing.T) {
inboundOptions := inbound.TrojanOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
GrpcServiceName: "GunService",
}
outboundOptions := outbound.TrojanOption{
Fingerprint: tlsFingerprint,
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_Reality(t *testing.T) {
inboundOptions := inbound.TrojanOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
}
outboundOptions := outbound.TrojanOption{
SNI: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_Reality_Grpc(t *testing.T) {
inboundOptions := inbound.TrojanOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
GrpcServiceName: "GunService",
}
outboundOptions := outbound.TrojanOption{
SNI: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_TLS_TrojanSS(t *testing.T) {
inboundOptions := inbound.TrojanOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
SSOption: inbound.TrojanSSOption{
Enabled: true,
Method: "",
Password: "password",
},
}
outboundOptions := outbound.TrojanOption{
Fingerprint: tlsFingerprint,
SSOpts: outbound.TrojanSSOption{
Enabled: true,
Method: "",
Password: "password",
},
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}
func TestInboundTrojan_Wss_TrojanSS(t *testing.T) {
inboundOptions := inbound.TrojanOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
SSOption: inbound.TrojanSSOption{
Enabled: true,
Method: "",
Password: "password",
},
WsPath: "/ws",
}
outboundOptions := outbound.TrojanOption{
Fingerprint: tlsFingerprint,
SSOpts: outbound.TrojanSSOption{
Enabled: true,
Method: "",
Password: "password",
},
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundTrojan(t, inboundOptions, outboundOptions)
}

View file

@ -0,0 +1,92 @@
package inbound_test
import (
"net/netip"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/stretchr/testify/assert"
)
var tuicCCs = []string{"cubic", "new_reno", "bbr"}
func testInboundTuic(t *testing.T, inboundOptions inbound.TuicOption, outboundOptions outbound.TuicOption) {
t.Parallel()
inboundOptions.Users = map[string]string{userUUID: userUUID}
inboundOptions.Token = []string{userUUID}
for _, tuicCC := range tuicCCs {
tuicCC := tuicCC
t.Run(tuicCC, func(t *testing.T) {
t.Parallel()
t.Run("v4", func(t *testing.T) {
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
outboundOptions.Token = userUUID
outboundOptions.CongestionController = tuicCC
inboundOptions.CongestionController = tuicCC
testInboundTuic0(t, inboundOptions, outboundOptions)
})
t.Run("v5", func(t *testing.T) {
inboundOptions, outboundOptions := inboundOptions, outboundOptions // don't modify outside options value
outboundOptions.UUID = userUUID
outboundOptions.Password = userUUID
outboundOptions.CongestionController = tuicCC
inboundOptions.CongestionController = tuicCC
testInboundTuic0(t, inboundOptions, outboundOptions)
})
})
}
}
func testInboundTuic0(t *testing.T, inboundOptions inbound.TuicOption, outboundOptions outbound.TuicOption) {
t.Parallel()
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "tuic_inbound",
Listen: "127.0.0.1",
Port: "0",
}
in, err := inbound.NewTuic(&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 = "tuic_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
out, err := outbound.NewTuic(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
}
func TestInboundTuic_TLS(t *testing.T) {
inboundOptions := inbound.TuicOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
AuthenticationTimeout: 5000,
}
outboundOptions := outbound.TuicOption{
Fingerprint: tlsFingerprint,
}
testInboundTuic(t, inboundOptions, outboundOptions)
}

View file

@ -39,6 +39,10 @@ type TunOption struct {
IncludeUIDRange []string `inbound:"include-uid-range,omitempty"` IncludeUIDRange []string `inbound:"include-uid-range,omitempty"`
ExcludeUID []uint32 `inbound:"exclude-uid,omitempty"` ExcludeUID []uint32 `inbound:"exclude-uid,omitempty"`
ExcludeUIDRange []string `inbound:"exclude-uid-range,omitempty"` ExcludeUIDRange []string `inbound:"exclude-uid-range,omitempty"`
ExcludeSrcPort []uint16 `inbound:"exclude-src-port,omitempty"`
ExcludeSrcPortRange []string `inbound:"exclude-src-port-range,omitempty"`
ExcludeDstPort []uint16 `inbound:"exclude-dst-port,omitempty"`
ExcludeDstPortRange []string `inbound:"exclude-dst-port-range,omitempty"`
IncludeAndroidUser []int `inbound:"include-android-user,omitempty"` IncludeAndroidUser []int `inbound:"include-android-user,omitempty"`
IncludePackage []string `inbound:"include-package,omitempty"` IncludePackage []string `inbound:"include-package,omitempty"`
ExcludePackage []string `inbound:"exclude-package,omitempty"` ExcludePackage []string `inbound:"exclude-package,omitempty"`
@ -137,6 +141,10 @@ func NewTun(options *TunOption) (*Tun, error) {
IncludeUIDRange: options.IncludeUIDRange, IncludeUIDRange: options.IncludeUIDRange,
ExcludeUID: options.ExcludeUID, ExcludeUID: options.ExcludeUID,
ExcludeUIDRange: options.ExcludeUIDRange, ExcludeUIDRange: options.ExcludeUIDRange,
ExcludeSrcPort: options.ExcludeSrcPort,
ExcludeSrcPortRange: options.ExcludeSrcPortRange,
ExcludeDstPort: options.ExcludeDstPort,
ExcludeDstPortRange: options.ExcludeDstPortRange,
IncludeAndroidUser: options.IncludeAndroidUser, IncludeAndroidUser: options.IncludeAndroidUser,
IncludePackage: options.IncludePackage, IncludePackage: options.IncludePackage,
ExcludePackage: options.ExcludePackage, ExcludePackage: options.ExcludePackage,

View file

@ -0,0 +1,195 @@
package inbound_test
import (
"net"
"net/netip"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/stretchr/testify/assert"
)
func testInboundVless(t *testing.T, inboundOptions inbound.VlessOption, outboundOptions outbound.VlessOption) {
t.Parallel()
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "vless_inbound",
Listen: "127.0.0.1",
Port: "0",
}
inboundOptions.Users = []inbound.VlessUser{
{Username: "test", UUID: userUUID, Flow: "xtls-rprx-vision"},
}
in, err := inbound.NewVless(&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 = "vless_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
outboundOptions.UUID = userUUID
out, err := outbound.NewVless(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
testSingMux(t, tunnel, out)
}
func TestInboundVless_TLS(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
}
func TestInboundVless_Wss1(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
}
func TestInboundVless_Wss2(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
}
func TestInboundVless_Grpc1(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVless(t, inboundOptions, outboundOptions)
}
func TestInboundVless_Grpc2(t *testing.T) {
inboundOptions := inbound.VlessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VlessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVless(t, inboundOptions, outboundOptions)
}
func TestInboundVless_Reality(t *testing.T) {
inboundOptions := inbound.VlessOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
}
outboundOptions := outbound.VlessOption{
TLS: true,
ServerName: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {
outboundOptions.Flow = "xtls-rprx-vision"
testInboundVless(t, inboundOptions, outboundOptions)
})
}
func TestInboundVless_Reality_Grpc(t *testing.T) {
inboundOptions := inbound.VlessOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VlessOption{
TLS: true,
ServerName: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVless(t, inboundOptions, outboundOptions)
}

View file

@ -0,0 +1,257 @@
package inbound_test
import (
"net"
"net/netip"
"testing"
"github.com/metacubex/mihomo/adapter/outbound"
"github.com/metacubex/mihomo/listener/inbound"
"github.com/stretchr/testify/assert"
)
func testInboundVMess(t *testing.T, inboundOptions inbound.VmessOption, outboundOptions outbound.VmessOption) {
t.Parallel()
inboundOptions.BaseOption = inbound.BaseOption{
NameStr: "vmess_inbound",
Listen: "127.0.0.1",
Port: "0",
}
inboundOptions.Users = []inbound.VmessUser{
{Username: "test", UUID: userUUID, AlterID: 0},
}
in, err := inbound.NewVmess(&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 = "vmess_outbound"
outboundOptions.Server = addrPort.Addr().String()
outboundOptions.Port = int(addrPort.Port())
outboundOptions.UUID = userUUID
outboundOptions.AlterID = 0
outboundOptions.Cipher = "auto"
out, err := outbound.NewVmess(outboundOptions)
if !assert.NoError(t, err) {
return
}
defer out.Close()
tunnel.DoTest(t, out)
testSingMux(t, tunnel, out)
}
func TestInboundVMess_Basic(t *testing.T) {
inboundOptions := inbound.VmessOption{}
outboundOptions := outbound.VmessOption{}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_TLS(t *testing.T) {
inboundOptions := inbound.VmessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
}
outboundOptions := outbound.VmessOption{
TLS: true,
Fingerprint: tlsFingerprint,
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Ws(t *testing.T) {
inboundOptions := inbound.VmessOption{
WsPath: "/ws",
}
outboundOptions := outbound.VmessOption{
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Ws_ed1(t *testing.T) {
inboundOptions := inbound.VmessOption{
WsPath: "/ws",
}
outboundOptions := outbound.VmessOption{
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws?ed=2048",
},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Ws_ed2(t *testing.T) {
inboundOptions := inbound.VmessOption{
WsPath: "/ws",
}
outboundOptions := outbound.VmessOption{
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
MaxEarlyData: 2048,
EarlyDataHeaderName: "Sec-WebSocket-Protocol",
},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Ws_Upgrade1(t *testing.T) {
inboundOptions := inbound.VmessOption{
WsPath: "/ws",
}
outboundOptions := outbound.VmessOption{
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
V2rayHttpUpgrade: true,
},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Ws_Upgrade2(t *testing.T) {
inboundOptions := inbound.VmessOption{
WsPath: "/ws",
}
outboundOptions := outbound.VmessOption{
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
V2rayHttpUpgrade: true,
V2rayHttpUpgradeFastOpen: true,
},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Wss1(t *testing.T) {
inboundOptions := inbound.VmessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
}
outboundOptions := outbound.VmessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Wss2(t *testing.T) {
inboundOptions := inbound.VmessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VmessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "ws",
WSOpts: outbound.WSOptions{
Path: "/ws",
},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Grpc1(t *testing.T) {
inboundOptions := inbound.VmessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VmessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Grpc2(t *testing.T) {
inboundOptions := inbound.VmessOption{
Certificate: tlsCertificate,
PrivateKey: tlsPrivateKey,
WsPath: "/ws",
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VmessOption{
TLS: true,
Fingerprint: tlsFingerprint,
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Reality(t *testing.T) {
inboundOptions := inbound.VmessOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
}
outboundOptions := outbound.VmessOption{
TLS: true,
ServerName: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
}
testInboundVMess(t, inboundOptions, outboundOptions)
}
func TestInboundVMess_Reality_Grpc(t *testing.T) {
inboundOptions := inbound.VmessOption{
RealityConfig: inbound.RealityConfig{
Dest: net.JoinHostPort(realityDest, "443"),
PrivateKey: realityPrivateKey,
ShortID: []string{realityShortid},
ServerNames: []string{realityDest},
},
GrpcServiceName: "GunService",
}
outboundOptions := outbound.VmessOption{
TLS: true,
ServerName: realityDest,
RealityOpts: outbound.RealityOptions{
PublicKey: realityPublickey,
ShortID: realityShortid,
},
ClientFingerprint: "chrome",
Network: "grpc",
GrpcOpts: outbound.GrpcOptions{GrpcServiceName: "GunService"},
}
testInboundVMess(t, inboundOptions, outboundOptions)
}

View file

@ -3,8 +3,6 @@ package inner
import ( import (
"errors" "errors"
"net" "net"
"net/netip"
"strconv"
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"
@ -16,9 +14,13 @@ func New(t C.Tunnel) {
tunnel = t tunnel = t
} }
func HandleTcp(address string, proxy string) (conn net.Conn, err error) { func GetTunnel() C.Tunnel {
return tunnel
}
func HandleTcp(tunnel C.Tunnel, address string, proxy string) (conn net.Conn, err error) {
if tunnel == nil { if tunnel == nil {
return nil, errors.New("tcp uninitialized") return nil, errors.New("tunnel uninitialized")
} }
// executor Parsed // executor Parsed
conn1, conn2 := N.Pipe() conn1, conn2 := N.Pipe()
@ -31,15 +33,8 @@ func HandleTcp(address string, proxy string) (conn net.Conn, err error) {
if proxy != "" { if proxy != "" {
metadata.SpecialProxy = proxy metadata.SpecialProxy = proxy
} }
if h, port, err := net.SplitHostPort(address); err == nil { if err = metadata.SetRemoteAddress(address); err != nil {
if port, err := strconv.ParseUint(port, 10, 16); err == nil { return nil, err
metadata.DstPort = uint16(port)
}
if ip, err := netip.ParseAddr(h); err == nil {
metadata.DstIP = ip
} else {
metadata.Host = h
}
} }
go tunnel.HandleTCPConn(conn2, metadata) go tunnel.HandleTCPConn(conn2, metadata)

View file

@ -73,7 +73,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if tlsConfig.Certificates != nil { if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality") return nil, errors.New("certificate is unavailable in reality")
} }
realityBuilder, err = config.RealityConfig.Build() realityBuilder, err = config.RealityConfig.Build(tunnel)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -9,13 +9,15 @@ import (
"net" "net"
"time" "time"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/listener/inner" "github.com/metacubex/mihomo/listener/inner"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/ntp" "github.com/metacubex/mihomo/ntp"
"github.com/metacubex/reality" utls "github.com/metacubex/utls"
) )
type Conn = reality.Conn type Conn = utls.Conn
type Config struct { type Config struct {
Dest string Dest string
@ -26,13 +28,14 @@ type Config struct {
Proxy string Proxy string
} }
func (c Config) Build() (*Builder, error) { func (c Config) Build(tunnel C.Tunnel) (*Builder, error) {
realityConfig := &reality.Config{} realityConfig := &utls.RealityConfig{}
realityConfig.SessionTicketsDisabled = true realityConfig.SessionTicketsDisabled = true
realityConfig.Type = "tcp" realityConfig.Type = "tcp"
realityConfig.Dest = c.Dest realityConfig.Dest = c.Dest
realityConfig.Time = ntp.Now realityConfig.Time = ntp.Now
realityConfig.ServerNames = make(map[string]bool) realityConfig.ServerNames = make(map[string]bool)
realityConfig.Log = log.Debugln
for _, it := range c.ServerNames { for _, it := range c.ServerNames {
realityConfig.ServerNames[it] = true realityConfig.ServerNames[it] = true
} }
@ -50,7 +53,11 @@ func (c Config) Build() (*Builder, error) {
realityConfig.ShortIds = make(map[[8]byte]bool) realityConfig.ShortIds = make(map[[8]byte]bool)
for i, shortIDString := range c.ShortID { for i, shortIDString := range c.ShortID {
var shortID [8]byte var shortID [8]byte
decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString)) decodedLen := hex.DecodedLen(len(shortIDString))
if decodedLen > 8 {
return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString)
}
decodedLen, err = hex.Decode(shortID[:], []byte(shortIDString))
if err != nil { if err != nil {
return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err) return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err)
} }
@ -61,18 +68,18 @@ func (c Config) Build() (*Builder, error) {
} }
realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) {
return inner.HandleTcp(address, c.Proxy) return inner.HandleTcp(tunnel, address, c.Proxy)
} }
return &Builder{realityConfig}, nil return &Builder{realityConfig}, nil
} }
type Builder struct { type Builder struct {
realityConfig *reality.Config realityConfig *utls.RealityConfig
} }
func (b Builder) NewListener(l net.Listener) net.Listener { func (b Builder) NewListener(l net.Listener) net.Listener {
l = reality.NewListener(l, b.realityConfig) l = utls.NewRealityListener(l, b.realityConfig)
// Due to low implementation quality, the reality server intercepted half close and caused memory leaks. // Due to low implementation quality, the reality server intercepted half close and caused memory leaks.
// We fixed it by calling Close() directly. // We fixed it by calling Close() directly.
l = realityListenerWrapper{l} l = realityListenerWrapper{l}
@ -80,7 +87,7 @@ func (b Builder) NewListener(l net.Listener) net.Listener {
} }
type realityConnWrapper struct { type realityConnWrapper struct {
*reality.Conn *utls.Conn
} }
func (c realityConnWrapper) Upstream() any { func (c realityConnWrapper) Upstream() any {
@ -100,5 +107,5 @@ func (l realityListenerWrapper) Accept() (net.Conn, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return realityConnWrapper{c.(*reality.Conn)}, nil return realityConnWrapper{c.(*utls.Conn)}, nil
} }

View file

@ -72,7 +72,7 @@ func NewListenerHandler(lc ListenerConfig) (h *ListenerHandler, err error) {
NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context {
return ctx return ctx
}, },
Logger: log.SingLogger, Logger: log.SingInfoToDebugLogger, // convert sing-mux info log to debug
Handler: h, Handler: h,
Padding: lc.MuxOption.Padding, Padding: lc.MuxOption.Padding,
Brutal: mux.BrutalOptions{ Brutal: mux.BrutalOptions{

View file

@ -196,7 +196,7 @@ func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
ctx := sing.WithAdditions(context.TODO(), additions...) ctx := sing.WithAdditions(context.TODO(), additions...)
err := l.service.NewConnection(ctx, conn, M.Metadata{ err := l.service.NewConnection(ctx, conn, M.Metadata{
Protocol: "shadowsocks", Protocol: "shadowsocks",
Source: M.ParseSocksaddr(conn.RemoteAddr().String()), Source: M.SocksaddrFromNet(conn.RemoteAddr()),
}) })
if err != nil { if err != nil {
_ = conn.Close() _ = conn.Close()

View file

@ -21,6 +21,7 @@ import (
LC "github.com/metacubex/mihomo/listener/config" LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/listener/sing"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"golang.org/x/exp/constraints"
tun "github.com/metacubex/sing-tun" tun "github.com/metacubex/sing-tun"
"github.com/metacubex/sing-tun/control" "github.com/metacubex/sing-tun/control"
@ -211,6 +212,22 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
return nil, E.Cause(err, "parse exclude_uid_range") return nil, E.Cause(err, "parse exclude_uid_range")
} }
} }
excludeSrcPort := uidToRange(options.ExcludeSrcPort)
if len(options.ExcludeSrcPortRange) > 0 {
var err error
excludeSrcPort, err = parseRange(excludeSrcPort, options.ExcludeSrcPortRange)
if err != nil {
return nil, E.Cause(err, "parse exclude_src_port_range")
}
}
excludeDstPort := uidToRange(options.ExcludeDstPort)
if len(options.ExcludeDstPortRange) > 0 {
var err error
excludeDstPort, err = parseRange(excludeDstPort, options.ExcludeDstPortRange)
if err != nil {
return nil, E.Cause(err, "parse exclude_dst_port_range")
}
}
var dnsAdds []netip.AddrPort var dnsAdds []netip.AddrPort
@ -339,6 +356,8 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
ExcludeInterface: options.ExcludeInterface, ExcludeInterface: options.ExcludeInterface,
IncludeUID: includeUID, IncludeUID: includeUID,
ExcludeUID: excludeUID, ExcludeUID: excludeUID,
ExcludeSrcPort: excludeSrcPort,
ExcludeDstPort: excludeDstPort,
IncludeAndroidUser: options.IncludeAndroidUser, IncludeAndroidUser: options.IncludeAndroidUser,
IncludePackage: options.IncludePackage, IncludePackage: options.IncludePackage,
ExcludePackage: options.ExcludePackage, ExcludePackage: options.ExcludePackage,
@ -566,13 +585,13 @@ func (d *cDialerInterfaceFinder) FindInterfaceName(destination netip.Addr) strin
return "<invalid>" return "<invalid>"
} }
func uidToRange(uidList []uint32) []ranges.Range[uint32] { func uidToRange[T constraints.Integer](uidList []T) []ranges.Range[T] {
return common.Map(uidList, func(uid uint32) ranges.Range[uint32] { return common.Map(uidList, func(uid T) ranges.Range[T] {
return ranges.NewSingle(uid) return ranges.NewSingle(uid)
}) })
} }
func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.Range[uint32], error) { func parseRange[T constraints.Integer](uidRanges []ranges.Range[T], rangeList []string) ([]ranges.Range[T], error) {
for _, uidRange := range rangeList { for _, uidRange := range rangeList {
if !strings.Contains(uidRange, ":") { if !strings.Contains(uidRange, ":") {
return nil, E.New("missing ':' in range: ", uidRange) return nil, E.New("missing ':' in range: ", uidRange)
@ -593,7 +612,7 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges.
if err != nil { if err != nil {
return nil, E.Cause(err, "parse range end") return nil, E.Cause(err, "parse range end")
} }
uidRanges = append(uidRanges, ranges.New(uint32(start), uint32(end))) uidRanges = append(uidRanges, ranges.New(T(start), T(end)))
} }
return uidRanges, nil return uidRanges, nil
} }

View file

@ -106,7 +106,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition)
if tlsConfig.Certificates != nil { if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality") return nil, errors.New("certificate is unavailable in reality")
} }
realityBuilder, err = config.RealityConfig.Build() realityBuilder, err = config.RealityConfig.Build(tunnel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -201,7 +201,7 @@ func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
ctx := sing.WithAdditions(context.TODO(), additions...) ctx := sing.WithAdditions(context.TODO(), additions...)
err := l.service.NewConnection(ctx, conn, metadata.Metadata{ err := l.service.NewConnection(ctx, conn, metadata.Metadata{
Protocol: "vless", Protocol: "vless",
Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), Source: metadata.SocksaddrFromNet(conn.RemoteAddr()),
}) })
if err != nil { if err != nil {
_ = conn.Close() _ = conn.Close()

View file

@ -90,7 +90,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition)
if tlsConfig.Certificates != nil { if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality") return nil, errors.New("certificate is unavailable in reality")
} }
realityBuilder, err = config.RealityConfig.Build() realityBuilder, err = config.RealityConfig.Build(tunnel)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -187,7 +187,7 @@ func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou
ctx := sing.WithAdditions(context.TODO(), additions...) ctx := sing.WithAdditions(context.TODO(), additions...)
err := l.service.NewConnection(ctx, conn, metadata.Metadata{ err := l.service.NewConnection(ctx, conn, metadata.Metadata{
Protocol: "vmess", Protocol: "vmess",
Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), Source: metadata.SocksaddrFromNet(conn.RemoteAddr()),
}) })
if err != nil { if err != nil {
_ = conn.Close() _ = conn.Close()

View file

@ -72,7 +72,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A
if tlsConfig.Certificates != nil { if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality") return nil, errors.New("certificate is unavailable in reality")
} }
realityBuilder, err = config.RealityConfig.Build() realityBuilder, err = config.RealityConfig.Build(tunnel)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -81,7 +81,7 @@ func NewUDP(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPLi
} }
dscp, _ := getDSCP(oob[:oobn]) dscp, _ := getDSCP(oob[:oobn])
additions = append(additions, inbound.WithDSCP(dscp)) additions := append(additions, inbound.WithDSCP(dscp)) // don't change outside additions
if rAddr.Addr().Is4() { if rAddr.Addr().Is4() {
// try to unmap 4in6 address // try to unmap 4in6 address

View file

@ -84,7 +84,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition)
if tlsConfig.Certificates != nil { if tlsConfig.Certificates != nil {
return nil, errors.New("certificate is unavailable in reality") return nil, errors.New("certificate is unavailable in reality")
} }
realityBuilder, err = config.RealityConfig.Build() realityBuilder, err = config.RealityConfig.Build(tunnel)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -60,6 +60,14 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) (
} else { } else {
tlsConfig.NextProtos = []string{"h3"} tlsConfig.NextProtos = []string{"h3"}
} }
if config.MaxIdleTime == 0 {
config.MaxIdleTime = 15000
}
if config.AuthenticationTimeout == 0 {
config.AuthenticationTimeout = 1000
}
quicConfig := &quic.Config{ quicConfig := &quic.Config{
MaxIdleTimeout: time.Duration(config.MaxIdleTime) * time.Millisecond, MaxIdleTimeout: time.Duration(config.MaxIdleTime) * time.Millisecond,
MaxIncomingStreams: ServerMaxIncomingStreams, MaxIncomingStreams: ServerMaxIncomingStreams,

View file

@ -65,4 +65,17 @@ func (l singLogger) Panic(args ...any) {
Fatalln(fmt.Sprint(args...)) Fatalln(fmt.Sprint(args...))
} }
type singInfoToDebugLogger struct {
singLogger
}
func (l singInfoToDebugLogger) InfoContext(ctx context.Context, args ...any) {
Debugln(fmt.Sprint(args...))
}
func (l singInfoToDebugLogger) Info(args ...any) {
Debugln(fmt.Sprint(args...))
}
var SingLogger L.ContextLogger = singLogger{} var SingLogger L.ContextLogger = singLogger{}
var SingInfoToDebugLogger L.ContextLogger = singInfoToDebugLogger{}

View file

@ -9,7 +9,6 @@ import (
"github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/atomic"
"github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/buf"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/transport/anytls/padding" "github.com/metacubex/mihomo/transport/anytls/padding"
"github.com/metacubex/mihomo/transport/anytls/session" "github.com/metacubex/mihomo/transport/anytls/session"
"github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/mihomo/transport/vmess"
@ -83,12 +82,7 @@ func (c *Client) CreateOutboundTLSConnection(ctx context.Context) (net.Conn, err
b.WriteZeroN(paddingLen) b.WriteZeroN(paddingLen)
} }
getTlsConn := func() (net.Conn, error) { tlsConn, err := vmess.StreamTLSConn(ctx, conn, c.tlsConfig)
ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
return vmess.StreamTLSConn(ctx, conn, c.tlsConfig)
}
tlsConn, err := getTlsConn()
if err != nil { if err != nil {
conn.Close() conn.Close()
return nil, err return nil, err

View file

@ -40,7 +40,7 @@ func newStream(id uint32, sess *Session) *Stream {
// Read implements net.Conn // Read implements net.Conn
func (s *Stream) Read(b []byte) (n int, err error) { func (s *Stream) Read(b []byte) (n int, err error) {
n, err = s.pipeR.Read(b) n, err = s.pipeR.Read(b)
if s.dieErr != nil { if n == 0 && s.dieErr != nil {
err = s.dieErr err = s.dieErr
} }
return return

View file

@ -13,6 +13,7 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/http/httptrace"
"net/url" "net/url"
"sync" "sync"
"time" "time"
@ -21,6 +22,7 @@ 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"
tlsC "github.com/metacubex/mihomo/component/tls" tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant"
"golang.org/x/net/http2" "golang.org/x/net/http2"
) )
@ -35,17 +37,17 @@ var defaultHeader = http.Header{
"user-agent": []string{"grpc-go/1.36.0"}, "user-agent": []string{"grpc-go/1.36.0"},
} }
type DialFn = func(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, error) initFn func() (io.ReadCloser, netAddr, error)
writer io.Writer writer io.Writer
flusher http.Flusher closer io.Closer
netAddr netAddr
reader io.ReadCloser reader io.ReadCloser
once sync.Once once sync.Once
close atomic.Bool closed atomic.Bool
err error err error
remain int remain int
br *bufio.Reader br *bufio.Reader
@ -60,7 +62,7 @@ type Config struct {
} }
func (g *Conn) initReader() { func (g *Conn) initReader() {
reader, err := g.initFn() reader, addr, err := g.initFn()
if err != nil { if err != nil {
g.err = err g.err = err
if closer, ok := g.writer.(io.Closer); ok { if closer, ok := g.writer.(io.Closer); ok {
@ -68,8 +70,9 @@ func (g *Conn) initReader() {
} }
return return
} }
g.netAddr = addr
if !g.close.Load() { if !g.closed.Load() {
g.reader = reader g.reader = reader
g.br = bufio.NewReader(reader) g.br = bufio.NewReader(reader)
} else { } else {
@ -147,8 +150,8 @@ func (g *Conn) Write(b []byte) (n int, err error) {
err = g.err err = g.err
} }
if g.flusher != nil { if flusher, ok := g.writer.(http.Flusher); ok {
g.flusher.Flush() flusher.Flush()
} }
return len(b), err return len(b), err
@ -170,8 +173,8 @@ func (g *Conn) WriteBuffer(buffer *buf.Buffer) error {
err = g.err err = g.err
} }
if g.flusher != nil { if flusher, ok := g.writer.(http.Flusher); ok {
g.flusher.Flush() flusher.Flush()
} }
return err return err
@ -182,15 +185,28 @@ func (g *Conn) FrontHeadroom() int {
} }
func (g *Conn) Close() error { func (g *Conn) Close() error {
g.close.Store(true) g.closed.Store(true)
var errorArr []error
if reader := g.reader; reader != nil { if reader := g.reader; reader != nil {
reader.Close() if err := reader.Close(); err != nil {
errorArr = append(errorArr, err)
}
} }
if closer, ok := g.writer.(io.Closer); ok { if closer, ok := g.writer.(io.Closer); ok {
return closer.Close() if err := closer.Close(); err != nil {
errorArr = append(errorArr, err)
} }
return nil }
if closer := g.closer; closer != nil {
if err := closer.Close(); err != nil {
errorArr = append(errorArr, err)
}
}
return errors.Join(errorArr...)
} }
func (g *Conn) SetReadDeadline(t time.Time) error { return g.SetDeadline(t) } func (g *Conn) SetReadDeadline(t time.Time) error { return g.SetDeadline(t) }
@ -208,24 +224,26 @@ func (g *Conn) SetDeadline(t time.Time) error {
return nil return nil
} }
func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string, realityConfig *tlsC.RealityConfig) *TransportWrap { func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, clientFingerprint string, realityConfig *tlsC.RealityConfig) *TransportWrap {
wrap := 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) {
pconn, err := dialFn(network, addr) ctx, cancel := context.WithTimeout(ctx, C.DefaultTLSTimeout)
defer cancel()
pconn, err := dialFn(ctx, network, addr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
wrap.remoteAddr = pconn.RemoteAddr()
wrap.localAddr = pconn.LocalAddr()
if tlsConfig == nil { if tlsConfig == nil {
return pconn, nil return pconn, nil
} }
if len(Fingerprint) != 0 { clientFingerprint := clientFingerprint
if tlsC.HaveGlobalFingerprint() && len(clientFingerprint) == 0 {
clientFingerprint = tlsC.GetGlobalFingerprint()
}
if len(clientFingerprint) != 0 {
if realityConfig == nil { if realityConfig == nil {
if fingerprint, exists := tlsC.GetFingerprint(Fingerprint); exists { if fingerprint, exists := tlsC.GetFingerprint(clientFingerprint); exists {
utlsConn := tlsC.UClient(pconn, cfg, fingerprint) utlsConn := tlsC.UClient(pconn, cfg, fingerprint)
if err := utlsConn.HandshakeContext(ctx); err != nil { if err := utlsConn.HandshakeContext(ctx); err != nil {
pconn.Close() pconn.Close()
@ -239,7 +257,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string, re
return utlsConn, nil return utlsConn, nil
} }
} else { } else {
realityConn, err := tlsC.GetRealityConn(ctx, pconn, Fingerprint, cfg, realityConfig) realityConn, err := tlsC.GetRealityConn(ctx, pconn, clientFingerprint, cfg, realityConfig)
if err != nil { if err != nil {
pconn.Close() pconn.Close()
return nil, err return nil, err
@ -269,7 +287,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string, re
return conn, nil return conn, nil
} }
wrap.Transport = &http2.Transport{ transport := &http2.Transport{
DialTLSContext: dialFunc, DialTLSContext: dialFunc,
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
AllowHTTP: false, AllowHTTP: false,
@ -277,7 +295,13 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string, re
PingTimeout: 0, PingTimeout: 0,
} }
return &wrap ctx, cancel := context.WithCancel(context.Background())
wrap := &TransportWrap{
Transport: transport,
ctx: ctx,
cancel: cancel,
}
return wrap
} }
func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, error) { func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, error) {
@ -302,17 +326,25 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
ProtoMinor: 0, ProtoMinor: 0,
Header: defaultHeader, Header: defaultHeader,
} }
request = request.WithContext(transport.ctx)
conn := &Conn{ conn := &Conn{
initFn: func() (io.ReadCloser, error) { initFn: func() (io.ReadCloser, netAddr, error) {
nAddr := netAddr{}
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
nAddr.localAddr = connInfo.Conn.LocalAddr()
nAddr.remoteAddr = connInfo.Conn.RemoteAddr()
},
}
request = request.WithContext(httptrace.WithClientTrace(request.Context(), trace))
response, err := transport.RoundTrip(request) response, err := transport.RoundTrip(request)
if err != nil { if err != nil {
return nil, err return nil, nAddr, err
} }
return response.Body, nil return response.Body, nAddr, nil
}, },
writer: writer, writer: writer,
netAddr: transport.netAddr,
} }
go conn.Init() go conn.Init()
@ -320,10 +352,17 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er
} }
func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) { func StreamGunWithConn(conn net.Conn, tlsConfig *tls.Config, cfg *Config, realityConfig *tlsC.RealityConfig) (net.Conn, error) {
dialFn := func(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, realityConfig) transport := NewHTTP2Client(dialFn, tlsConfig, cfg.ClientFingerprint, realityConfig)
return StreamGunWithTransport(transport, cfg) c, err := StreamGunWithTransport(transport, cfg)
if err != nil {
return nil, err
}
if c, ok := c.(*Conn); ok { // The incoming net.Conn should be closed synchronously with the generated gun.Conn
c.closer = conn
}
return c, nil
} }

View file

@ -43,21 +43,22 @@ func NewServerHandler(options ServerOption) http.Handler {
writer.WriteHeader(http.StatusOK) writer.WriteHeader(http.StatusOK)
conn := &Conn{ conn := &Conn{
initFn: func() (io.ReadCloser, error) { initFn: func() (io.ReadCloser, netAddr, error) {
return request.Body, nil nAddr := netAddr{}
},
writer: writer,
flusher: writer.(http.Flusher),
}
if request.RemoteAddr != "" { if request.RemoteAddr != "" {
metadata := C.Metadata{} metadata := C.Metadata{}
if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil { if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil {
conn.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort()) nAddr.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort())
} }
} }
if addr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { if addr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
conn.localAddr = addr nAddr.localAddr = addr
} }
return request.Body, nAddr, nil
},
writer: writer,
}
_ = conn.Init()
wrapper := &h2ConnWrapper{ wrapper := &h2ConnWrapper{
// gun.Conn can't correct handle ReadDeadline // gun.Conn can't correct handle ReadDeadline

View file

@ -1,21 +1,26 @@
package gun package gun
import ( import (
"golang.org/x/net/http2" "context"
"net" "net"
"sync"
"golang.org/x/net/http2"
) )
type TransportWrap struct { type TransportWrap struct {
*http2.Transport *http2.Transport
netAddr ctx context.Context
cancel context.CancelFunc
closeOnce sync.Once
} }
func (tw *TransportWrap) RemoteAddr() net.Addr { func (tw *TransportWrap) Close() error {
return tw.remoteAddr tw.closeOnce.Do(func() {
} tw.cancel()
closeTransport(tw.Transport)
func (tw *TransportWrap) LocalAddr() net.Addr { })
return tw.localAddr return nil
} }
type netAddr struct { type netAddr struct {
@ -23,10 +28,10 @@ type netAddr struct {
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
} }

View file

@ -0,0 +1,64 @@
package gun
import (
"net"
"net/http"
"sync"
"time"
"unsafe"
"golang.org/x/net/http2"
)
type clientConnPool struct {
t *http2.Transport
mu sync.Mutex
conns map[string][]*http2.ClientConn // key is host:port
dialing map[string]unsafe.Pointer // currently in-flight dials
keys map[*http2.ClientConn][]string
addConnCalls map[string]unsafe.Pointer // in-flight addConnIfNeeded calls
}
type clientConn struct {
t *http.Transport
tconn net.Conn // usually *tls.Conn, except specialized impls
}
type efaceWords struct {
typ unsafe.Pointer
data unsafe.Pointer
}
type tlsConn interface {
net.Conn
NetConn() net.Conn
}
func closeClientConn(cc *http2.ClientConn) { // like forceCloseConn() in http2.ClientConn but also apply for tls-like conn
if conn, ok := (*clientConn)(unsafe.Pointer(cc)).tconn.(tlsConn); ok {
t := time.AfterFunc(time.Second, func() {
_ = conn.NetConn().Close()
})
defer t.Stop()
}
_ = cc.Close()
}
func closeTransport(tr *http2.Transport) {
connPool := transportConnPool(tr)
p := (*clientConnPool)((*efaceWords)(unsafe.Pointer(&connPool)).data)
p.mu.Lock()
defer p.mu.Unlock()
for _, vv := range p.conns {
for _, cc := range vv {
closeClientConn(cc)
}
}
// cleanup
p.conns = make(map[string][]*http2.ClientConn)
p.keys = make(map[*http2.ClientConn][]string)
}
//go:linkname transportConnPool golang.org/x/net/http2.(*Transport).connPool
func transportConnPool(t *http2.Transport) http2.ClientConnPool

Some files were not shown because too many files have changed in this diff Show more