diff --git a/constant/proxy.go b/constant/proxy.go index 86a89d171d..749006e4df 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -46,6 +46,7 @@ const ( TypeAwg = "awg" //H TypeBalancer = "balancer" //H TypeDNSTT = "dnstt" //H + TypeMasterDNS = "masterdns" //H TypeGooseRelay = "gooserelay" //H TypeSmartDNSPool = "smart_dns_pool" //H — local recursive-resolver pool with AIMD throttling + recovery probing (github.com/hiddify/hmrd_multi_resolver_dns) ) @@ -133,6 +134,8 @@ func ProxyDisplayName(proxyType string) string { return "Balancer" case TypeDNSTT: return "DNSTT" + case TypeMasterDNS: + return "MasterDNS" case TypeGooseRelay: return "GooseRelay" default: diff --git a/go.mod b/go.mod index 074ec8814f..fb309b2c43 100644 --- a/go.mod +++ b/go.mod @@ -55,7 +55,7 @@ require ( github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 github.com/sagernet/smux v1.5.50-sing-box-mod.1 github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 - github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c + github.com/sagernet/wireguard-go v0.0.4 github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 github.com/spf13/cobra v1.10.2 github.com/stretchr/testify v1.11.1 @@ -63,17 +63,22 @@ require ( github.com/vishvananda/netns v0.0.5 go.uber.org/zap v1.27.1 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.48.0 + golang.org/x/crypto v0.49.0 golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 golang.org/x/mod v0.33.0 - golang.org/x/net v0.50.0 - golang.org/x/sys v0.41.0 + golang.org/x/net v0.51.0 + golang.org/x/sys v0.42.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 howett.net/plist v1.0.1 ) +require ( + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect + github.com/hiddifydeveloper/MasterDnsVPN v0.0.0-20260725182012-6d0aba10b2f0 +) + require ( github.com/AdguardTeam/golibs v0.32.7 // indirect github.com/ameshkov/dnscrypt/v2 v2.4.0 @@ -82,8 +87,8 @@ require ( github.com/pires/go-proxyproto v0.8.1 github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/text v0.35.0 // indirect golang.org/x/tools v0.42.0 // indirect ) @@ -150,7 +155,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/kamstrup/intmap v0.5.2 // indirect - github.com/klauspost/compress v1.18.3 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/reedsolomon v1.13.0 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect @@ -160,7 +165,7 @@ require ( github.com/mroth/weightedrand v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/philhofer/fwd v1.2.0 // indirect - github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pion/datachannel v1.5.5 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/ice/v2 v2.3.24 // indirect @@ -245,7 +250,7 @@ require ( go.uber.org/zap/exp v0.3.0 // indirect go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/term v0.40.0 // indirect + golang.org/x/term v0.41.0 // indirect golang.org/x/time v0.14.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect diff --git a/go.sum b/go.sum index 6427ab01e7..7c9c179425 100644 --- a/go.sum +++ b/go.sum @@ -222,6 +222,8 @@ github.com/hiddify/hmrd_multi_resolver_dns v0.0.0-20260429114007-8d809dc33d0e h1 github.com/hiddify/hmrd_multi_resolver_dns v0.0.0-20260429114007-8d809dc33d0e/go.mod h1:7u9Ece+I3gvMFzcpqhROWaykNUyoE21g/fYQMBasAWY= github.com/hiddify/vaydns v0.0.0-20260401180616-890dc987a6a9 h1:KXnaABX8hHmkcL0jbL769hEIGI5+z/DajCrlO+Bkzcc= github.com/hiddify/vaydns v0.0.0-20260401180616-890dc987a6a9/go.mod h1:+8kEfQsZJn7/4aIppVekrSuqhrKjGBIgnacTJkdAlS8= +github.com/hiddifydeveloper/MasterDnsVPN v0.0.0-20260725182012-6d0aba10b2f0 h1:kio+bC7vcUrRb+LBLsoifW+DB6LV06TjUc9rBE4yChs= +github.com/hiddifydeveloper/MasterDnsVPN v0.0.0-20260725182012-6d0aba10b2f0/go.mod h1:gvB0gnK4JxXlrcRJ8+Wrf54mhrBYlc1etxlIDF+467I= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY= @@ -235,8 +237,8 @@ github.com/kamstrup/intmap v0.5.2 h1:qnwBm1mh4XAnW9W9Ue9tZtTff8pS6+s6iKF6JRIV2Dk github.com/kamstrup/intmap v0.5.2/go.mod h1:gWUVWHKzWj8xpJVFf5GC0O26bWmv3GqdnIX/LMT6Aq4= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= -github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/reedsolomon v1.13.0 h1:E0Cmgf2kMuhZTj6eefnvpKC4/Q4jhCi9YIjcZjK4arc= @@ -306,8 +308,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= -github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= -github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY= +github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= @@ -628,8 +630,8 @@ golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0= golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU= @@ -659,8 +661,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= @@ -670,8 +672,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -696,8 +698,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -708,8 +710,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= -golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -720,8 +722,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/include/registry.go b/include/registry.go index 0a010ef95f..ab4bcc0850 100644 --- a/include/registry.go +++ b/include/registry.go @@ -27,6 +27,7 @@ import ( "github.com/sagernet/sing-box/protocol/hiddify/dnstt" "github.com/sagernet/sing-box/protocol/hiddify/gooserelay" "github.com/sagernet/sing-box/protocol/hiddify/hinvalid" + "github.com/sagernet/sing-box/protocol/hiddify/masterdns" "github.com/sagernet/sing-box/protocol/hiddify/xray" "github.com/sagernet/sing-box/protocol/http" @@ -111,6 +112,7 @@ func OutboundRegistry() *outbound.Registry { hinvalid.RegisterOutbound(registry) xray.RegisterOutbound(registry) dnstt.RegisterOutbound(registry) + masterdns.RegisterOutbound(registry) gooserelay.RegisterOutbound(registry) balancer.RegisterLoadBalance(registry) diff --git a/option/masterdns.go b/option/masterdns.go new file mode 100644 index 0000000000..a5d21c8a32 --- /dev/null +++ b/option/masterdns.go @@ -0,0 +1,30 @@ +package option + +import ( + "github.com/sagernet/sing/common/json/badoption" +) + +type MasterDnsOptions struct { + DialerOptions + + // ProtocolType is the local proxy protocol MasterDnsVPN's client + // listener speaks, e.g. "socks5". Defaults to "socks5" when empty. + ProtocolType string `json:"protocol_type,omitempty"` + + Domains []string `json:"domains,omitempty"` + Resolvers []string `json:"resolvers,omitempty"` + + SOCKS5Auth bool `json:"socks5_auth,omitempty"` + SOCKS5User string `json:"socks5_user,omitempty"` + SOCKS5Pass string `json:"socks5_pass,omitempty"` + + EncryptionKey string `json:"encryption_key,omitempty"` + DataEncryptionMethod *int `json:"data_encryption_method,omitempty"` + UploadCompressionType *int `json:"upload_compression_type,omitempty"` + + PacketDuplicationCount *int `json:"packet_duplication_count,omitempty"` + MinUploadMTU *int `json:"min_upload_mtu,omitempty"` + MaxUploadMTU *int `json:"max_upload_mtu,omitempty"` + + ConnectTimeout *badoption.Duration `json:"connect_timeout,omitempty"` +} diff --git a/protocol/hiddify/masterdns/outbound.go b/protocol/hiddify/masterdns/outbound.go new file mode 100644 index 0000000000..474851aea4 --- /dev/null +++ b/protocol/hiddify/masterdns/outbound.go @@ -0,0 +1,228 @@ +package masterdns + +import ( + "context" + "encoding/base64" + "encoding/json" + "net" + "os" + "path/filepath" + "strings" + "sync" + + mdconfig "github.com/hiddifydeveloper/MasterDnsVPN/pkg/config" + mdclient "github.com/hiddifydeveloper/MasterDnsVPN/pkg/client" + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/socks" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.MasterDnsOptions](registry, C.TypeMasterDNS, NewOutbound) +} + +var _ adapter.Outbound = (*Outbound)(nil) + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger + ctx context.Context + + options option.MasterDnsOptions + + mu sync.Mutex + tunnel *mdclient.Client + client *socks.Client + started bool + stopErr error +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MasterDnsOptions) (adapter.Outbound, error) { + if len(options.Domains) == 0 { + return nil, E.New("at least one domain is required") + } + if len(options.Resolvers) == 0 { + return nil, E.New("at least one resolver is required") + } + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMasterDNS, tag, []string{N.NetworkTCP}, options.DialerOptions), + ctx: ctx, + logger: logger, + options: options, + }, nil +} + +func (h *Outbound) PreStart() error { + return nil +} + +func (h *Outbound) PostStart() error { + return h.startTunnel() +} + +func (h *Outbound) startTunnel() error { + h.mu.Lock() + defer h.mu.Unlock() + if h.started { + return nil + } + + stateDir, err := os.MkdirTemp("", "hiddify-masterdns-"+h.Tag()) + if err != nil { + return E.Cause(err, "create masterdns state dir") + } + + resolversPath := filepath.Join(stateDir, "resolvers.txt") + if err := os.WriteFile(resolversPath, []byte(strings.Join(h.options.Resolvers, "\n")+"\n"), 0o600); err != nil { + return E.Cause(err, "write masterdns resolvers file") + } + + listenPort, err := freePort() + if err != nil { + return E.Cause(err, "allocate masterdns local port") + } + + cfgJSON := h.buildConfigJSON(listenPort) + encoded := base64.StdEncoding.EncodeToString(cfgJSON) + resolversFilePath := resolversPath + cfg, err := mdconfig.LoadClientConfigFromJSONBase64WithOverrides(encoded, mdconfig.ClientConfigOverrides{ + ResolversFilePath: &resolversFilePath, + }) + if err != nil { + return E.Cause(err, "build masterdns client config") + } + + tunnel, err := mdclient.BootstrapLoadedConfig(cfg, "") + if err != nil { + return E.Cause(err, "bootstrap masterdns client") + } + + runCtx, cancel := context.WithCancel(h.ctx) + go func() { + err := tunnel.Run(runCtx) + h.mu.Lock() + h.stopErr = err + h.mu.Unlock() + }() + _ = cancel // cancellation happens via Outbound.Close -> h.ctx cancellation from the router + + dialOptions := h.options.DialerOptions + dialOptions.Detour = "" + outboundDialer, err := dialer.New(h.ctx, dialOptions, false) + if err != nil { + return E.Cause(err, "create masterdns local dialer") + } + + socksAddr := option.ServerOptions{Server: "127.0.0.1", ServerPort: uint16(listenPort)} + user, pass := h.options.SOCKS5User, h.options.SOCKS5Pass + if !h.options.SOCKS5Auth { + user, pass = "", "" + } + h.client = socks.NewClient(outboundDialer, socksAddr.Build(), socks.Version5, user, pass) + h.tunnel = tunnel + h.started = true + return nil +} + +// buildConfigJSON renders the subset of MasterDnsVPN's ClientConfig fields +// this outbound controls into the JSON shape LoadClientConfigFromJSONBase64 +// expects (keys match the upstream TOML tags, see pkg/config/client.go). +func (h *Outbound) buildConfigJSON(listenPort int) []byte { + doc := map[string]any{ + "DOMAINS": h.options.Domains, + "LISTEN_IP": "127.0.0.1", + "LISTEN_PORT": listenPort, + } + if h.options.ProtocolType != "" { + doc["PROTOCOL_TYPE"] = h.options.ProtocolType + } + if h.options.SOCKS5Auth { + doc["SOCKS5_AUTH"] = true + doc["SOCKS5_USER"] = h.options.SOCKS5User + doc["SOCKS5_PASS"] = h.options.SOCKS5Pass + } + if h.options.EncryptionKey != "" { + doc["ENCRYPTION_KEY"] = h.options.EncryptionKey + } + if h.options.DataEncryptionMethod != nil { + doc["DATA_ENCRYPTION_METHOD"] = *h.options.DataEncryptionMethod + } + if h.options.UploadCompressionType != nil { + doc["UPLOAD_COMPRESSION_TYPE"] = *h.options.UploadCompressionType + } + if h.options.PacketDuplicationCount != nil { + doc["PACKET_DUPLICATION_COUNT"] = *h.options.PacketDuplicationCount + } + if h.options.MinUploadMTU != nil { + doc["MIN_UPLOAD_MTU"] = *h.options.MinUploadMTU + } + if h.options.MaxUploadMTU != nil { + doc["MAX_UPLOAD_MTU"] = *h.options.MaxUploadMTU + } + raw, _ := json.Marshal(doc) + return raw +} + +func freePort() (int, error) { + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +func (h *Outbound) IsReady() bool { + h.mu.Lock() + defer h.mu.Unlock() + return h.started +} + +func (h *Outbound) DisplayType() string { + str := C.ProxyDisplayName(h.Type()) + if !h.IsReady() { + str += " ⚠️ Connecting..." + } else { + str += " ✔️" + } + return str +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if !h.IsReady() { + return nil, E.New("outbound is not started") + } + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + h.logger.InfoContext(ctx, "outbound connection to ", destination) + return h.client.DialContext(ctx, network, destination) +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if !h.IsReady() { + return nil, E.New("outbound is not started") + } + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + return h.client.ListenPacket(ctx, destination) +} + +func (h *Outbound) Close() error { + h.mu.Lock() + defer h.mu.Unlock() + if h.tunnel != nil { + h.tunnel.StopAsyncRuntime() + } + return h.stopErr +}