Compare commits

...

80 Commits

Author SHA1 Message Date
wwqgtxx
5b975275f5 fix: incorrect checking of strings.Split return value
strings.Split will never return a slice of length 0 if sep is not empty, so any code that checks if the return value is of length 0 is incorrect and useless.
2025-06-25 16:20:37 +08:00
ayanamist
166392fe17 chore: sniffer replace domain only if domain is valid (#2122) 2025-06-24 21:44:26 +08:00
ayanamist
5c6aa433ca chore: unconditionally allow clients with passwords for password-free socks5 inbound (#2123) 2025-06-24 19:01:12 +08:00
xishang0128
2c55dc2557 action: fix run build on pull_request 2025-06-24 19:01:03 +08:00
wwqgtxx
56c0b088e8 doc: update path doc 2025-06-21 22:46:55 +08:00
Restia-Ashbell
5344e869a8 fix: ssr uri decode (#2116) 2025-06-21 12:19:13 +08:00
wwqgtxx
6cfaf15cbf fix: missing error return 2025-06-21 12:08:41 +08:00
wwqgtxx
31f0060b30 fix: chacha20 counter overflow
the implement it's a not safe chacha20 using but for compatible
2025-06-21 10:42:14 +08:00
wwqgtxx
c60750d549 chore: allow tun to skip the system ipv6 check when starting by environment variable SKIP_SYSTEM_IPV6_CHECK 2025-06-14 15:57:54 +08:00
wwqgtxx
ebf5918e94 fix: v2ray-plugin mux maybe not close underlay connection 2025-06-14 12:32:45 +08:00
riolurs
93ca18517c chore: converter support fingerprint for anytls 2025-06-13 23:05:06 +08:00
beck
32d447ce99 fix: convert https (#2102) 2025-06-12 17:10:09 +08:00
beck
617fef84ae feat: converter support anytls/socks/http (#2100) 2025-06-12 16:17:25 +08:00
wwqgtxx
d19199322d action: don't trigger cmfa update on pull request 2025-06-12 15:33:19 +08:00
wwqgtxx
87795e3a07 chore: add yaml marshal for common/atomic 2025-06-12 15:24:29 +08:00
wwqgtxx
85bb40aaf8 chore: add Int32Enum for common/atomic 2025-06-12 15:24:29 +08:00
wwqgtxx
082bcec281 chore: apply find process mode in direct/global mode 2025-06-12 00:27:51 +08:00
wwqgtxx
9283cb0f5f feat: add loopback-address support for tun 2025-06-11 17:45:28 +08:00
wwqgtxx
ae7967f662 chore: the resolve and findProcess behaviors of Logic and SubRules follow the order and needs of the internal rules 2025-06-10 20:11:50 +08:00
wwqgtxx
01f8f2db2f chore: cleanup allocator code 2025-06-10 10:54:08 +08:00
wwqgtxx
255ff5e977 chore: add rate limiting support for reality listener 2025-06-10 10:40:26 +08:00
wwqgtxx
939e4109d7 chore: write dns reply in single syscall 2025-06-07 00:38:39 +08:00
wwqgtxx
40587b62b8 feat: all dns client support skip-cert-verify params 2025-06-06 00:52:12 +08:00
wwqgtxx
85e6d25de5 feat: all dns client support ecs and ecs-override params 2025-06-06 00:45:58 +08:00
wwqgtxx
29a37f4f4b feat: all dns client support disable-ipv4 and disable-ipv6 params 2025-06-06 00:24:57 +08:00
wwqgtxx
2f9a3b3469 chore: cleanup code 2025-06-05 21:20:38 +08:00
wwqgtxx
40ea0ba098 fix: correct constructor for 2022-blake3-chacha8-poly1305 2025-06-05 13:47:26 +08:00
wwqgtxx
8d7f947a80 fix: TypedValue.CompareAndSwap
84aa7ff3bb
2025-06-05 13:43:30 +08:00
wwqgtxx
71a8705636 fix: remote dst parse 2025-05-31 22:57:05 +08:00
wwqgtxx
c0f452b540 chore: more unmap for 4in6 address 2025-05-29 10:14:06 +08:00
wwqgtxx
6c9abe16cc fix: vmess listener error 2025-05-28 21:33:44 +08:00
wwqgtxx
213d80c1e2 fix: quic sniffer should consider skipDomain 2025-05-28 10:06:53 +08:00
wwqgtxx
1db89da122 fix: quic sniffer should not replace domain when no valid host is read 2025-05-28 09:22:28 +08:00
wwqgtxx
689c58f661 chore: clear dstIP when overrideDest in sniffer 2025-05-27 22:47:21 +08:00
wwqgtxx
33590c4066 fix: destination should unmap before find interface 2025-05-27 18:26:35 +08:00
wwqgtxx
60ae9dce56 chore: recover log leval for preHandleMetadata 2025-05-27 18:10:44 +08:00
wwqgtxx
4741ac6702 fix: in-port not work with shadowsocks listener 2025-05-27 16:32:42 +08:00
wwqgtxx
ef3d7e4dd7 chore: remove unneeded dns resolve when proxydialer dial udp 2025-05-27 15:04:01 +08:00
wwqgtxx
a1c7881229 chore: rebuild udp dns resolve
The DNS resolution of the overall UDP part has been delayed to the connection initiation stage. During the rule matching process, it will only be triggered when the IP rule without no-resolve is matched.

For direct and wireguard outbound, the same logic as the TCP part will be followed, that is, when direct-nameserver (or DNS configured by wireguard) exists, the result of the matching process will be discarded and the domain name will be re-resolved. This re-resolution logic is only effective for fakeip.

For reject and DNS outbound, no resolution is required.

For other outbound, resolution will still be performed when the connection is initiated, and the domain name will not be sent directly to the remote server at present.
2025-05-27 10:45:26 +08:00
wwqgtxx
12e3952b74 chore: code cleanup 2025-05-26 12:33:24 +08:00
wwqgtxx
88419cbd12 chore: better parse remote dst 2025-05-26 01:12:35 +08:00
wwqgtxx
4ed830330e chore: remove confused code 2025-05-25 22:22:23 +08:00
wwqgtxx
3ed6ff9402 chore: export pipeDeadline 2025-05-25 22:07:29 +08:00
wwqgtxx
34de62d21d chore: better get localAddr 2025-05-24 23:19:38 +08:00
wwqgtxx
d2e255f257 fix: some error in tun 2025-05-24 22:23:10 +08:00
wwqgtxx
a0c46bb4b7 chore: remove the redundant layer of udpnat in sing-tun to reduce resource usage when processing udp 2025-05-24 15:57:49 +08:00
wwqgtxx
9e3bf14b1a chore: handle two interfaces have the same prefix but different address 2025-05-24 11:32:36 +08:00
wwqgtxx
28c387a9b6 chore: restore break change in sing-tun 2025-05-23 20:19:18 +08:00
wwqgtxx
15eda703b4 fix: hysteria2 panic 2025-05-23 20:12:38 +08:00
wwqgtxx
b1d12a15db chore: proxy's ech should fetch from proxy-nameserver 2025-05-22 17:42:40 +08:00
wwqgtxx
5a21bf3642 fix: listener close panic 2025-05-22 17:01:24 +08:00
wwqgtxx
199fb8fd5d chore: update quic-go to 0.52.0 2025-05-22 10:28:10 +08:00
wwqgtxx
fd959feff2 chore: update dependencies 2025-05-21 21:37:20 +08:00
wwqgtxx
d5a03901d2 fix: race in close grpc transport 2025-05-20 16:15:04 +08:00
wwqgtxx
257fead538 docs: update config.yaml follow 5cf0f18c 2025-05-20 11:08:42 +08:00
wwqgtxx
c489c5260b fix: hysteria2 hop ports init
https://github.com/MetaCubeX/mihomo/issues/2056
2025-05-20 10:56:14 +08:00
wwqgtxx
8f92b1de13 chore: simplify the single root decompression process 2025-05-20 09:48:05 +08:00
wwqgtxx
9f7a2a36c1 chore: unpack externalUI in a separate temporary directory to avoid malicious compressed packages from polluting workdir 2025-05-20 01:58:25 +08:00
wwqgtxx
a93479124c chore: stricter path checking when unpacking zip/tgz 2025-05-20 00:00:30 +08:00
wwqgtxx
ed42c4feb8 chore: disallow symlink in unzip 2025-05-19 23:42:39 +08:00
wwqgtxx
608ddb1b44 fix: external-ui-name must in local 2025-05-19 23:11:52 +08:00
wwqgtxx
d036d98128 fix: http server does not handle http2 logic correctly 2025-05-18 23:05:00 +08:00
wwqgtxx
d900c71214 fix: shadowtls v2 not work with X25519MLKEM768 2025-05-18 23:03:07 +08:00
wwqgtxx
1672750c47 chore: simplifying the old fingerprint processing method 2025-05-18 23:03:07 +08:00
wwqgtxx
41b57afb3f fix: grpc deadline implement 2025-05-18 23:03:07 +08:00
wwqgtxx
188372cb04 feat: add tls.ech-key for external-controller-tls 2025-05-17 21:21:02 +08:00
wwqgtxx
a1350d4985 feat: add ech-key for listeners 2025-05-17 20:50:21 +08:00
wwqgtxx
dc958e6a39 feat: add ech-opts for hysteria/hysteria2/tuic outbound 2025-05-17 18:41:39 +08:00
wwqgtxx
8a5f3b8909 chore: simplify port hop costs 2025-05-17 17:06:38 +08:00
wwqgtxx
c6d7ef8cb8 feat: add ech-opts for anytls/shadowsocks/trojan/vmess/vless outbound 2025-05-17 13:53:21 +08:00
wwqgtxx
bb8c47d83d fix: error typo 2025-05-15 18:07:55 +08:00
wwqgtxx
5cf0f18c29 feat: reality add support-x25519mlkem768, it only works with new version server 2025-05-15 14:54:43 +08:00
wwqgtxx
83213d493e chore: adjust min backoff from 1s to 10s 2025-05-14 21:51:18 +08:00
wwqgtxx
90ed01ed53 fix: backoff not reset when the file unchanged 2025-05-14 21:45:12 +08:00
wwqgtxx
f91a586da8 fix: inline proxy provider's healthcheck not work 2025-05-13 19:00:32 +08:00
wwqgtxx
266fb03838 chore: update dependencies 2025-05-13 12:09:38 +08:00
wwqgtxx
76e9607fd7 chore: move start healthcheck.process() from New to Initial in provider
avoid panic cause by build-in proxy have not set to tunnel
2025-05-13 01:12:06 +08:00
wwqgtxx
23e2d3a132 chore: rebuild provider load 2025-05-12 22:19:49 +08:00
wwqgtxx
6e35cf9399 fix: truncated UDP response in system dns
https://github.com/MetaCubeX/mihomo/issues/2031
2025-05-12 12:34:22 +08:00
wwqgtxx
2116640886 chore: the updateConfigs api also adds a check for SAFE_PATHS 2025-05-12 11:28:15 +08:00
180 changed files with 3047 additions and 1899 deletions

View File

@@ -198,10 +198,11 @@ jobs:
- name: Set variables - name: Set variables
run: | run: |
VERSION="${GITHUB_REF_NAME,,}-$(git rev-parse --short HEAD)" VERSION="${GITHUB_REF_NAME,,}-$(git rev-parse --short HEAD)"
VERSION="${VERSION//\//-}"
PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}" PackageVersion="$(curl -s "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" | jq -r '.tag_name' | sed 's/v//g' | awk -F '.' '{$NF = $NF + 1; print}' OFS='.').${VERSION/-/.}"
if [ -n "${{ github.event.inputs.version }}" ]; then if [ -n "${{ github.event.inputs.version }}" ]; then
VERSION=${{ github.event.inputs.version }} VERSION=${{ github.event.inputs.version }}
PackageVersion="${VERSION#v}" >> $GITHUB_ENV PackageVersion="${VERSION#v}"
fi fi
echo "VERSION=${VERSION}" >> $GITHUB_ENV echo "VERSION=${VERSION}" >> $GITHUB_ENV
echo "PackageVersion=${PackageVersion}" >> $GITHUB_ENV echo "PackageVersion=${PackageVersion}" >> $GITHUB_ENV

View File

@@ -10,9 +10,6 @@ on:
- Alpha - Alpha
tags: tags:
- "v*" - "v*"
pull_request_target:
branches:
- Alpha
jobs: jobs:
# Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies # Send "core-updated" to MetaCubeX/ClashMetaForAndroid to trigger update-dependencies

View File

@@ -7,9 +7,7 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"strconv"
"strings" "strings"
"time" "time"
@@ -316,15 +314,7 @@ func urlToMetadata(rawURL string) (addr C.Metadata, err error) {
return return
} }
} }
uintPort, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return
}
addr = C.Metadata{ err = addr.SetRemoteAddress(net.JoinHostPort(u.Hostname(), port))
Host: u.Hostname(),
DstIP: netip.Addr{},
DstPort: uint16(uintPort),
}
return return
} }

View File

@@ -4,7 +4,6 @@ import (
"net" "net"
"net/http" "net/http"
"net/netip" "net/netip"
"strconv"
"strings" "strings"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
@@ -41,23 +40,8 @@ func parseHTTPAddr(request *http.Request) *C.Metadata {
// trim FQDN (#737) // trim FQDN (#737)
host = strings.TrimRight(host, ".") host = strings.TrimRight(host, ".")
var uint16Port uint16 metadata := &C.Metadata{}
if port, err := strconv.ParseUint(port, 10, 16); err == nil { _ = metadata.SetRemoteAddress(net.JoinHostPort(host, port))
uint16Port = uint16(port)
}
metadata := &C.Metadata{
NetWork: C.TCP,
Host: host,
DstIP: netip.Addr{},
DstPort: uint16Port,
}
ip, err := netip.ParseAddr(host)
if err == nil {
metadata.DstIP = ip
}
return metadata return metadata
} }

View File

@@ -2,7 +2,6 @@ package outbound
import ( import (
"context" "context"
"errors"
"net" "net"
"strconv" "strconv"
"time" "time"
@@ -10,7 +9,6 @@ import (
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"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
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"
@@ -28,19 +26,20 @@ type AnyTLS struct {
type AnyTLSOption struct { type AnyTLSOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port"` Port int `proxy:"port"`
Password string `proxy:"password"` Password string `proxy:"password"`
ALPN []string `proxy:"alpn,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
ClientFingerprint string `proxy:"client-fingerprint,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` ClientFingerprint string `proxy:"client-fingerprint,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
UDP bool `proxy:"udp,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"` UDP bool `proxy:"udp,omitempty"`
IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"` IdleSessionCheckInterval int `proxy:"idle-session-check-interval,omitempty"`
MinIdleSession int `proxy:"min-idle-session,omitempty"` IdleSessionTimeout int `proxy:"idle-session-timeout,omitempty"`
MinIdleSession int `proxy:"min-idle-session,omitempty"`
} }
func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) { func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Conn, err error) {
@@ -52,6 +51,10 @@ func (t *AnyTLS) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.Con
} }
func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
// create tcp // create tcp
c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2)) c, err := t.client.CreateProxy(ctx, uot.RequestDestination(2))
if err != nil { if err != nil {
@@ -59,13 +62,6 @@ func (t *AnyTLS) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
} }
// create uot on tcp // create uot on tcp
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr()) destination := M.SocksaddrFromNet(metadata.UDPAddr())
return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil return newPacketConn(CN.NewThreadSafePacketConn(uot.NewLazyConn(c, uot.Request{Destination: destination})), t), nil
} }
@@ -115,12 +111,17 @@ func NewAnyTLS(option AnyTLSOption) (*AnyTLS, error) {
IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second, IdleSessionTimeout: time.Duration(option.IdleSessionTimeout) * time.Second,
MinIdleSession: option.MinIdleSession, MinIdleSession: option.MinIdleSession,
} }
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
tlsConfig := &vmess.TLSConfig{ tlsConfig := &vmess.TLSConfig{
Host: option.SNI, Host: option.SNI,
SkipCertVerify: option.SkipCertVerify, SkipCertVerify: option.SkipCertVerify,
NextProtos: option.ALPN, NextProtos: option.ALPN,
FingerPrint: option.Fingerprint, FingerPrint: option.Fingerprint,
ClientFingerprint: option.ClientFingerprint, ClientFingerprint: option.ClientFingerprint,
ECH: echConfig,
} }
if tlsConfig.Host == "" { if tlsConfig.Host == "" {
tlsConfig.Host = option.Server tlsConfig.Host = option.Server

View File

@@ -3,15 +3,16 @@ package outbound
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"runtime" "runtime"
"strings"
"sync" "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"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
) )
@@ -19,6 +20,7 @@ import (
type ProxyAdapter interface { type ProxyAdapter interface {
C.ProxyAdapter C.ProxyAdapter
DialOptions() []dialer.Option DialOptions() []dialer.Option
ResolveUDP(ctx context.Context, metadata *C.Metadata) error
} }
type Base struct { type Base struct {
@@ -160,6 +162,17 @@ func (b *Base) DialOptions() (opts []dialer.Option) {
return opts return opts
} }
func (b *Base) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
func (b *Base) Close() error { func (b *Base) Close() error {
return nil return nil
} }
@@ -203,12 +216,21 @@ func NewBase(opt BaseOption) *Base {
type conn struct { type conn struct {
N.ExtendedConn N.ExtendedConn
chain C.Chain chain C.Chain
actualRemoteDestination string adapterAddr string
} }
func (c *conn) RemoteDestination() string { func (c *conn) RemoteDestination() string {
return c.actualRemoteDestination if remoteAddr := c.RemoteAddr(); remoteAddr != nil {
m := C.Metadata{}
if err := m.SetRemoteAddr(remoteAddr); err == nil {
if m.Valid() {
return m.String()
}
}
}
host, _, _ := net.SplitHostPort(c.adapterAddr)
return host
} }
// Chains implements C.Connection // Chains implements C.Connection
@@ -241,19 +263,25 @@ 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
} }
return &conn{N.NewExtendedConn(c), []string{a.Name()}, parseRemoteDestination(a.Addr())} return &conn{N.NewExtendedConn(c), []string{a.Name()}, a.Addr()}
} }
type packetConn struct { type packetConn struct {
N.EnhancePacketConn N.EnhancePacketConn
chain C.Chain chain C.Chain
adapterName string adapterName string
connID string connID string
actualRemoteDestination string adapterAddr string
resolveUDP func(ctx context.Context, metadata *C.Metadata) error
}
func (c *packetConn) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
return c.resolveUDP(ctx, metadata)
} }
func (c *packetConn) RemoteDestination() string { func (c *packetConn) RemoteDestination() string {
return c.actualRemoteDestination host, _, _ := net.SplitHostPort(c.adapterAddr)
return host
} }
// Chains implements C.Connection // Chains implements C.Connection
@@ -287,24 +315,12 @@ func (c *packetConn) AddRef(ref any) {
c.EnhancePacketConn = N.NewRefPacketConn(c.EnhancePacketConn, ref) // add ref for autoCloseProxyAdapter 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 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
epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly epc = N.NewDeadlineEnhancePacketConn(epc) // most conn from outbound can't handle readDeadline correctly
} }
return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), parseRemoteDestination(a.Addr())} return &packetConn{epc, []string{a.Name()}, a.Name(), utils.NewUUIDV4().String(), a.Addr(), a.ResolveUDP}
}
func parseRemoteDestination(addr string) string {
if dst, _, err := net.SplitHostPort(addr); err == nil {
return dst
} else {
if addrError, ok := err.(*net.AddrError); ok && strings.Contains(addrError.Err, "missing port") {
return dst
} else {
return ""
}
}
} }
type AddRef interface { type AddRef interface {

View File

@@ -2,7 +2,8 @@ package outbound
import ( import (
"context" "context"
"errors" "fmt"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/loopback" "github.com/metacubex/mihomo/component/loopback"
"github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/component/resolver"
@@ -38,13 +39,8 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if err := d.loopBack.CheckPacketConn(metadata); err != nil { if err := d.loopBack.CheckPacketConn(metadata); err != nil {
return nil, err return nil, err
} }
// net.UDPConn.WriteTo only working with *net.UDPAddr, so we need a net.UDPAddr if err := d.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort()) pc, err := dialer.NewDialer(d.DialOptions()...).ListenPacket(ctx, "udp", "", metadata.AddrPort())
if err != nil { if err != nil {
@@ -53,6 +49,17 @@ func (d *Direct) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil return d.loopBack.NewPacketConn(newPacketConn(pc, d)), nil
} }
func (d *Direct) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if (!metadata.Resolved() || resolver.DirectHostResolver != resolver.DefaultResolver) && metadata.Host != "" {
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, resolver.DirectHostResolver)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return nil
}
func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool { func (d *Direct) IsL3Protocol(metadata *C.Metadata) bool {
return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver return true // tell DNSDialer don't send domain to DialContext, avoid lookback to DefaultResolver
} }

View File

@@ -3,6 +3,7 @@ package outbound
import ( import (
"context" "context"
"net" "net"
"net/netip"
"time" "time"
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
@@ -31,6 +32,9 @@ func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, er
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort()) log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort())
if err := d.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@@ -41,6 +45,13 @@ func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.
}, d), nil }, d), nil
} }
func (d *Dns) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
metadata.DstIP = netip.AddrFrom4([4]byte{127, 0, 0, 2})
}
return nil
}
type dnsPacket struct { type dnsPacket struct {
data []byte data []byte
put func() put func()

36
adapter/outbound/ech.go Normal file
View File

@@ -0,0 +1,36 @@
package outbound
import (
"context"
"encoding/base64"
"fmt"
"github.com/metacubex/mihomo/component/ech"
"github.com/metacubex/mihomo/component/resolver"
)
type ECHOptions struct {
Enable bool `proxy:"enable,omitempty" obfs:"enable,omitempty"`
Config string `proxy:"config,omitempty" obfs:"config,omitempty"`
}
func (o ECHOptions) Parse() (*ech.Config, error) {
if !o.Enable {
return nil, nil
}
echConfig := &ech.Config{}
if o.Config != "" {
list, err := base64.StdEncoding.DecodeString(o.Config)
if err != nil {
return nil, fmt.Errorf("base64 decode ech config string failed: %v", err)
}
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
return list, nil
}
} else {
echConfig.GetEncryptedClientHelloConfigList = func(ctx context.Context, serverName string) ([]byte, error) {
return resolver.ResolveECHWithResolver(ctx, serverName, resolver.ProxyServerHostResolver)
}
}
return echConfig, nil
}

View File

@@ -12,6 +12,7 @@ import (
"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/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
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"
@@ -44,6 +45,9 @@ type Hysteria struct {
option *HysteriaOption option *HysteriaOption
client *core.Client client *core.Client
tlsConfig *tlsC.Config
echConfig *ech.Config
} }
func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
@@ -56,6 +60,9 @@ func (h *Hysteria) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
} }
func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (h *Hysteria) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
if err := h.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
udpConn, err := h.client.DialUDP(h.genHdc(ctx)) udpConn, err := h.client.DialUDP(h.genHdc(ctx))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -79,7 +86,15 @@ func (h *Hysteria) genHdc(ctx context.Context) utils.PacketDialer {
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 resolveUDPAddr(ctx, "udp", addr, h.prefer) udpAddr, err := resolveUDPAddr(ctx, "udp", addr, h.prefer)
if err != nil {
return nil, err
}
err = h.echConfig.ClientHandle(ctx, h.tlsConfig)
if err != nil {
return nil, err
}
return udpAddr, nil
}, },
} }
} }
@@ -93,30 +108,31 @@ func (h *Hysteria) ProxyInfo() C.ProxyInfo {
type HysteriaOption struct { type HysteriaOption struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port,omitempty"` Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"` Ports string `proxy:"ports,omitempty"`
Protocol string `proxy:"protocol,omitempty"` Protocol string `proxy:"protocol,omitempty"`
ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash ObfsProtocol string `proxy:"obfs-protocol,omitempty"` // compatible with Stash
Up string `proxy:"up"` Up string `proxy:"up"`
UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash UpSpeed int `proxy:"up-speed,omitempty"` // compatible with Stash
Down string `proxy:"down"` Down string `proxy:"down"`
DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash DownSpeed int `proxy:"down-speed,omitempty"` // compatible with Stash
Auth string `proxy:"auth,omitempty"` Auth string `proxy:"auth,omitempty"`
AuthString string `proxy:"auth-str,omitempty"` AuthString string `proxy:"auth-str,omitempty"`
Obfs string `proxy:"obfs,omitempty"` Obfs string `proxy:"obfs,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"` CustomCA string `proxy:"ca,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"` FastOpen bool `proxy:"fast-open,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"`
} }
func (c *HysteriaOption) Speed() (uint64, uint64, error) { func (c *HysteriaOption) Speed() (uint64, uint64, error) {
@@ -161,6 +177,13 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
} else { } else {
tlsConfig.NextProtos = []string{DefaultALPN} tlsConfig.NextProtos = []string{DefaultALPN}
} }
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
tlsClientConfig := tlsC.UConfig(tlsConfig)
quicConfig := &quic.Config{ quicConfig := &quic.Config{
InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn), InitialStreamReceiveWindow: uint64(option.ReceiveWindowConn),
MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn), MaxStreamReceiveWindow: uint64(option.ReceiveWindowConn),
@@ -215,7 +238,7 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
down = uint64(option.DownSpeed * mbpsToBps) down = uint64(option.DownSpeed * mbpsToBps)
} }
client, err := core.NewClient( client, err := core.NewClient(
addr, ports, option.Protocol, auth, tlsC.UConfig(tlsConfig), quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl { addr, ports, option.Protocol, auth, tlsClientConfig, quicConfig, clientTransport, up, down, func(refBPS uint64) congestion.CongestionControl {
return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS)) return hyCongestion.NewBrutalSender(congestion.ByteCount(refBPS))
}, obfuscator, hopInterval, option.FastOpen, }, obfuscator, hopInterval, option.FastOpen,
) )
@@ -233,8 +256,10 @@ func NewHysteria(option HysteriaOption) (*Hysteria, error) {
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option, option: &option,
client: client, client: client,
tlsConfig: tlsClientConfig,
echConfig: echConfig,
} }
return outbound, nil return outbound, nil

View File

@@ -20,7 +20,6 @@ import (
tuicCommon "github.com/metacubex/mihomo/transport/tuic/common" tuicCommon "github.com/metacubex/mihomo/transport/tuic/common"
"github.com/metacubex/quic-go" "github.com/metacubex/quic-go"
"github.com/metacubex/randv2"
"github.com/metacubex/sing-quic/hysteria2" "github.com/metacubex/sing-quic/hysteria2"
M "github.com/metacubex/sing/common/metadata" M "github.com/metacubex/sing/common/metadata"
) )
@@ -42,24 +41,25 @@ type Hysteria2 struct {
type Hysteria2Option struct { type Hysteria2Option struct {
BasicOption BasicOption
Name string `proxy:"name"` Name string `proxy:"name"`
Server string `proxy:"server"` Server string `proxy:"server"`
Port int `proxy:"port,omitempty"` Port int `proxy:"port,omitempty"`
Ports string `proxy:"ports,omitempty"` Ports string `proxy:"ports,omitempty"`
HopInterval int `proxy:"hop-interval,omitempty"` HopInterval int `proxy:"hop-interval,omitempty"`
Up string `proxy:"up,omitempty"` Up string `proxy:"up,omitempty"`
Down string `proxy:"down,omitempty"` Down string `proxy:"down,omitempty"`
Password string `proxy:"password,omitempty"` Password string `proxy:"password,omitempty"`
Obfs string `proxy:"obfs,omitempty"` Obfs string `proxy:"obfs,omitempty"`
ObfsPassword string `proxy:"obfs-password,omitempty"` ObfsPassword string `proxy:"obfs-password,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
ALPN []string `proxy:"alpn,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"` ALPN []string `proxy:"alpn,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"` CustomCA string `proxy:"ca,omitempty"`
CWND int `proxy:"cwnd,omitempty"` CustomCAString string `proxy:"ca-str,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"` CWND int `proxy:"cwnd,omitempty"`
UdpMTU int `proxy:"udp-mtu,omitempty"`
// quic-go special config // quic-go special config
InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"` InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"`
@@ -77,6 +77,9 @@ func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata) (_ C.
} }
func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (h *Hysteria2) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = h.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
pc, err := h.client.ListenPacket(ctx) pc, err := h.client.ListenPacket(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -154,6 +157,12 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
tlsConfig.NextProtos = option.ALPN tlsConfig.NextProtos = option.ALPN
} }
tlsClientConfig := tlsC.UConfig(tlsConfig)
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
if option.UdpMTU == 0 { if option.UdpMTU == 0 {
// "1200" from quic-go's MaxDatagramSize // "1200" from quic-go's MaxDatagramSize
// "-3" from quic-go's DatagramFrame.MaxDataLen // "-3" from quic-go's DatagramFrame.MaxDataLen
@@ -175,41 +184,46 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) {
ReceiveBPS: StringToBps(option.Down), ReceiveBPS: StringToBps(option.Down),
SalamanderPassword: salamanderPassword, SalamanderPassword: salamanderPassword,
Password: option.Password, Password: option.Password,
TLSConfig: tlsC.UConfig(tlsConfig), TLSConfig: tlsClientConfig,
QUICConfig: quicConfig, QUICConfig: quicConfig,
UDPDisabled: false, UDPDisabled: false,
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 resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) udpAddr, err := resolveUDPAddr(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion))
if err != nil {
return nil, err
}
err = echConfig.ClientHandle(ctx, tlsClientConfig)
if err != nil {
return nil, err
}
return udpAddr, nil
}, },
} }
var ranges utils.IntRanges[uint16] var ranges utils.IntRanges[uint16]
var serverAddress []string var serverPorts []uint16
if option.Ports != "" { if option.Ports != "" {
ranges, err = utils.NewUnsignedRanges[uint16](option.Ports) ranges, err = utils.NewUnsignedRanges[uint16](option.Ports)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ranges.Range(func(port uint16) bool { ranges.Range(func(port uint16) bool {
serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port)))) serverPorts = append(serverPorts, port)
return true return true
}) })
if len(serverAddress) > 0 { if len(serverPorts) > 0 {
clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) {
return resolveUDPAddr(ctx, "udp", serverAddress[randv2.IntN(len(serverAddress))], C.NewDNSPrefer(option.IPVersion))
}
if option.HopInterval == 0 { if option.HopInterval == 0 {
option.HopInterval = defaultHopInterval option.HopInterval = defaultHopInterval
} else if option.HopInterval < minHopInterval { } else if option.HopInterval < minHopInterval {
option.HopInterval = minHopInterval option.HopInterval = minHopInterval
} }
clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second
clientOptions.ServerPorts = serverPorts
} }
} }
if option.Port == 0 && len(serverAddress) == 0 { if option.Port == 0 && len(serverPorts) == 0 {
return nil, errors.New("invalid port") return nil, errors.New("invalid port")
} }

View File

@@ -54,6 +54,9 @@ func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = m.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if err := m.ensureClientIsRunning(); err != nil { if err := m.ensureClientIsRunning(); err != nil {
return nil, err return nil, err
} }

View File

@@ -13,11 +13,14 @@ import (
type RealityOptions struct { type RealityOptions struct {
PublicKey string `proxy:"public-key"` PublicKey string `proxy:"public-key"`
ShortID string `proxy:"short-id"` ShortID string `proxy:"short-id"`
SupportX25519MLKEM768 bool `proxy:"support-x25519mlkem768"`
} }
func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) { func (o RealityOptions) Parse() (*tlsC.RealityConfig, error) {
if o.PublicKey != "" { if o.PublicKey != "" {
config := new(tlsC.RealityConfig) config := new(tlsC.RealityConfig)
config.SupportX25519MLKEM768 = o.SupportX25519MLKEM768
const x25519ScalarSize = 32 const x25519ScalarSize = 32
publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey) publicKey, err := base64.RawURLEncoding.DecodeString(o.PublicKey)

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"io" "io"
"net" "net"
"net/netip"
"time" "time"
"github.com/metacubex/mihomo/common/buf" "github.com/metacubex/mihomo/common/buf"
@@ -29,9 +30,19 @@ func (r *Reject) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) { func (r *Reject) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (C.PacketConn, error) {
if err := r.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
return newPacketConn(&nopPacketConn{}, r), nil return newPacketConn(&nopPacketConn{}, r), nil
} }
func (r *Reject) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if !metadata.Resolved() {
metadata.DstIP = netip.IPv4Unspecified()
}
return nil
}
func NewRejectWithOption(option RejectOption) *Reject { func NewRejectWithOption(option RejectOption) *Reject {
return &Reject{ return &Reject{
Base: &Base{ Base: &Base{

View File

@@ -2,7 +2,6 @@ package outbound
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"strconv" "strconv"
@@ -11,7 +10,6 @@ import (
"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"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
gost "github.com/metacubex/mihomo/transport/gost-plugin" gost "github.com/metacubex/mihomo/transport/gost-plugin"
"github.com/metacubex/mihomo/transport/restls" "github.com/metacubex/mihomo/transport/restls"
@@ -64,6 +62,7 @@ type v2rayObfsOption struct {
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@@ -77,6 +76,7 @@ type gostObfsOption struct {
Host string `obfs:"host,omitempty"` Host string `obfs:"host,omitempty"`
Path string `obfs:"path,omitempty"` Path string `obfs:"path,omitempty"`
TLS bool `obfs:"tls,omitempty"` TLS bool `obfs:"tls,omitempty"`
ECHOpts ECHOptions `obfs:"ech-opts,omitempty"`
Fingerprint string `obfs:"fingerprint,omitempty"` Fingerprint string `obfs:"fingerprint,omitempty"`
Headers map[string]string `obfs:"headers,omitempty"` Headers map[string]string `obfs:"headers,omitempty"`
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"` SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
@@ -200,6 +200,9 @@ func (ss *ShadowSocks) ListenPacketWithDialer(ctx context.Context, dialer C.Dial
return nil, err return nil, err
} }
} }
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
addr, err := resolveUDPAddr(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
@@ -228,15 +231,9 @@ func (ss *ShadowSocks) ProxyInfo() C.ProxyInfo {
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (ss *ShadowSocks) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
if ss.option.UDPOverTCP { if ss.option.UDPOverTCP {
// ss uot use stream-oriented udp with a special address, so we need a net.UDPAddr if err = ss.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
destination := M.SocksaddrFromNet(metadata.UDPAddr()) destination := M.SocksaddrFromNet(metadata.UDPAddr())
if ss.option.UDPOverTCPVersion == uot.LegacyVersion { if ss.option.UDPOverTCPVersion == uot.LegacyVersion {
return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil return newPacketConn(N.NewThreadSafePacketConn(uot.NewConn(c, uot.Request{Destination: destination})), ss), nil
@@ -303,6 +300,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
v2rayOption.TLS = true v2rayOption.TLS = true
v2rayOption.SkipCertVerify = opts.SkipCertVerify v2rayOption.SkipCertVerify = opts.SkipCertVerify
v2rayOption.Fingerprint = opts.Fingerprint v2rayOption.Fingerprint = opts.Fingerprint
echConfig, err := opts.ECHOpts.Parse()
if err != nil {
return nil, fmt.Errorf("ss %s initialize v2ray-plugin error: %w", addr, err)
}
v2rayOption.ECHConfig = echConfig
} }
} else if option.Plugin == "gost-plugin" { } else if option.Plugin == "gost-plugin" {
opts := gostObfsOption{Host: "bing.com", Mux: true} opts := gostObfsOption{Host: "bing.com", Mux: true}
@@ -325,6 +328,12 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
gostOption.TLS = true gostOption.TLS = true
gostOption.SkipCertVerify = opts.SkipCertVerify gostOption.SkipCertVerify = opts.SkipCertVerify
gostOption.Fingerprint = opts.Fingerprint gostOption.Fingerprint = opts.Fingerprint
echConfig, err := opts.ECHOpts.Parse()
if err != nil {
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
}
gostOption.ECHConfig = echConfig
} }
} else if option.Plugin == shadowtls.Mode { } else if option.Plugin == shadowtls.Mode {
obfsMode = shadowtls.Mode obfsMode = shadowtls.Mode

View File

@@ -105,6 +105,9 @@ func (ssr *ShadowSocksR) ListenPacketWithDialer(ctx context.Context, dialer C.Di
return nil, err return nil, err
} }
} }
if err = ssr.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
addr, err := resolveUDPAddr(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

@@ -2,12 +2,10 @@ package outbound
import ( import (
"context" "context"
"errors"
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"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
@@ -53,16 +51,9 @@ func (s *SingMux) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
if s.onlyTcp { if s.onlyTcp {
return s.ProxyAdapter.ListenPacketContext(ctx, metadata) return s.ProxyAdapter.ListenPacketContext(ctx, metadata)
} }
if err = s.ProxyAdapter.ResolveUDP(ctx, metadata); err != nil {
// sing-mux use stream-oriented udp with a special address, so we need a net.UDPAddr return nil, err
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr())) pc, err := s.client.ListenPacket(ctx, M.SocksaddrFromNet(metadata.UDPAddr()))
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -127,6 +127,9 @@ func (s *Snell) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
return nil, err return nil, err
} }
} }
if err = s.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", s.addr) c, err := dialer.DialContext(ctx, "tcp", s.addr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -109,6 +109,9 @@ func (ss *Socks5) ListenPacketContext(ctx context.Context, metadata *C.Metadata)
return nil, err return nil, err
} }
} }
if err = ss.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := cDialer.DialContext(ctx, "tcp", ss.addr) c, err := cDialer.DialContext(ctx, "tcp", ss.addr)
if err != nil { if err != nil {
err = fmt.Errorf("%s connect error: %w", ss.addr, err) err = fmt.Errorf("%s connect error: %w", ss.addr, err)

View File

@@ -138,7 +138,7 @@ func NewSsh(option SshOption) (*Ssh, error) {
} else { } else {
path := C.Path.Resolve(option.PrivateKey) path := C.Path.Resolve(option.PrivateKey)
if !C.Path.IsSafePath(path) { if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path) return nil, C.Path.ErrNotSafePath(path)
} }
b, err = os.ReadFile(path) b, err = os.ReadFile(path)
if err != nil { if err != nil {

View File

@@ -12,6 +12,7 @@ import (
N "github.com/metacubex/mihomo/common/net" 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/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
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"
@@ -32,6 +33,7 @@ type Trojan struct {
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig realityConfig *tlsC.RealityConfig
echConfig *ech.Config
ssCipher core.Cipher ssCipher core.Cipher
} }
@@ -48,6 +50,7 @@ type TrojanOption struct {
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
UDP bool `proxy:"udp,omitempty"` UDP bool `proxy:"udp,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"`
WSOpts WSOptions `proxy:"ws-opts,omitempty"` WSOpts WSOptions `proxy:"ws-opts,omitempty"`
@@ -77,6 +80,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgrade: t.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: t.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: t.option.ClientFingerprint, ClientFingerprint: t.option.ClientFingerprint,
ECHConfig: t.echConfig,
Headers: http.Header{}, Headers: http.Header{},
} }
@@ -110,7 +114,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts) c, err = vmess.StreamWebsocketConn(ctx, c, wsOpts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.realityConfig) c, err = gun.StreamGunWithConn(c, t.gunTLSConfig, t.gunConfig, t.echConfig, t.realityConfig)
default: default:
// default tcp network // default tcp network
// handle TLS // handle TLS
@@ -124,6 +128,7 @@ func (t *Trojan) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.
FingerPrint: t.option.Fingerprint, FingerPrint: t.option.Fingerprint,
ClientFingerprint: t.option.ClientFingerprint, ClientFingerprint: t.option.ClientFingerprint,
NextProtos: alpn, NextProtos: alpn,
ECH: t.echConfig,
Reality: t.realityConfig, Reality: t.realityConfig,
}) })
} }
@@ -214,6 +219,10 @@ func (t *Trojan) DialContextWithDialer(ctx context.Context, dialer C.Dialer, met
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Trojan) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
var c net.Conn var c net.Conn
// grpc transport // grpc transport
@@ -245,6 +254,9 @@ func (t *Trojan) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, me
return nil, err return nil, err
} }
} }
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
c, err := dialer.DialContext(ctx, "tcp", t.addr) c, err := dialer.DialContext(ctx, "tcp", t.addr)
if err != nil { if err != nil {
return nil, fmt.Errorf("%s connect error: %w", t.addr, err) return nil, fmt.Errorf("%s connect error: %w", t.addr, err)
@@ -266,12 +278,6 @@ func (t *Trojan) SupportWithDialer() C.NetWork {
return C.ALLNet return C.ALLNet
} }
// ListenPacketOnStreamConn implements C.ProxyAdapter
func (t *Trojan) ListenPacketOnStreamConn(c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
pc := trojan.NewPacketConn(c)
return newPacketConn(pc, t), err
}
// SupportUOT implements C.ProxyAdapter // SupportUOT implements C.ProxyAdapter
func (t *Trojan) SupportUOT() bool { func (t *Trojan) SupportUOT() bool {
return true return true
@@ -321,6 +327,11 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err return nil, err
} }
t.echConfig, err = option.ECHOpts.Parse()
if err != nil {
return nil, err
}
if option.SSOpts.Enabled { if option.SSOpts.Enabled {
if option.SSOpts.Password == "" { if option.SSOpts.Password == "" {
return nil, errors.New("empty password") return nil, errors.New("empty password")
@@ -365,7 +376,7 @@ func NewTrojan(option TrojanOption) (*Trojan, error) {
return nil, err return nil, err
} }
t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.realityConfig) t.transport = gun.NewHTTP2Client(dialFn, tlsConfig, option.ClientFingerprint, t.echConfig, t.realityConfig)
t.gunTLSConfig = tlsConfig t.gunTLSConfig = tlsConfig
t.gunConfig = &gun.Config{ t.gunConfig = &gun.Config{

View File

@@ -3,7 +3,6 @@ package outbound
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"math" "math"
"net" "net"
@@ -12,8 +11,8 @@ import (
"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/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
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/tuic" "github.com/metacubex/mihomo/transport/tuic"
@@ -28,6 +27,9 @@ type Tuic struct {
*Base *Base
option *TuicOption option *TuicOption
client *tuic.PoolClient client *tuic.PoolClient
tlsConfig *tlsC.Config
echConfig *ech.Config
} }
type TuicOption struct { type TuicOption struct {
@@ -48,18 +50,19 @@ type TuicOption struct {
DisableSni bool `proxy:"disable-sni,omitempty"` DisableSni bool `proxy:"disable-sni,omitempty"`
MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"` MaxUdpRelayPacketSize int `proxy:"max-udp-relay-packet-size,omitempty"`
FastOpen bool `proxy:"fast-open,omitempty"` FastOpen bool `proxy:"fast-open,omitempty"`
MaxOpenStreams int `proxy:"max-open-streams,omitempty"` MaxOpenStreams int `proxy:"max-open-streams,omitempty"`
CWND int `proxy:"cwnd,omitempty"` CWND int `proxy:"cwnd,omitempty"`
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
CustomCA string `proxy:"ca,omitempty"` CustomCA string `proxy:"ca,omitempty"`
CustomCAString string `proxy:"ca-str,omitempty"` CustomCAString string `proxy:"ca-str,omitempty"`
ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"` ReceiveWindowConn int `proxy:"recv-window-conn,omitempty"`
ReceiveWindow int `proxy:"recv-window,omitempty"` ReceiveWindow int `proxy:"recv-window,omitempty"`
DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"` DisableMTUDiscovery bool `proxy:"disable-mtu-discovery,omitempty"`
MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"` MaxDatagramFrameSize int `proxy:"max-datagram-frame-size,omitempty"`
SNI string `proxy:"sni,omitempty"` SNI string `proxy:"sni,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
UDPOverStream bool `proxy:"udp-over-stream,omitempty"` UDPOverStream bool `proxy:"udp-over-stream,omitempty"`
UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"` UDPOverStreamVersion int `proxy:"udp-over-stream-version,omitempty"`
@@ -86,6 +89,10 @@ func (t *Tuic) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_
// ListenPacketWithDialer implements C.ProxyAdapter // ListenPacketWithDialer implements C.ProxyAdapter
func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) { func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.PacketConn, err error) {
if err = t.ResolveUDP(ctx, metadata); err != nil {
return nil, err
}
if t.option.UDPOverStream { if t.option.UDPOverStream {
uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion)) uotDestination := uot.RequestDestination(uint8(t.option.UDPOverStreamVersion))
uotMetadata := *metadata uotMetadata := *metadata
@@ -97,13 +104,6 @@ func (t *Tuic) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, meta
} }
// tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr // tuic uos use stream-oriented udp with a special address, so we need a net.UDPAddr
if !metadata.Resolved() {
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
}
destination := M.SocksaddrFromNet(metadata.UDPAddr()) destination := M.SocksaddrFromNet(metadata.UDPAddr())
if t.option.UDPOverStreamVersion == uot.LegacyVersion { if t.option.UDPOverStreamVersion == uot.LegacyVersion {
@@ -135,6 +135,10 @@ func (t *Tuic) dialWithDialer(ctx context.Context, dialer C.Dialer) (transport *
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
err = t.echConfig.ClientHandle(ctx, t.tlsConfig)
if err != nil {
return nil, nil, err
}
addr = udpAddr addr = udpAddr
var pc net.PacketConn var pc net.PacketConn
pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort()) pc, err = dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
@@ -249,6 +253,12 @@ func NewTuic(option TuicOption) (*Tuic, error) {
tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config tlsConfig.InsecureSkipVerify = true // tls: either ServerName or InsecureSkipVerify must be specified in the tls.Config
} }
tlsClientConfig := tlsC.UConfig(tlsConfig)
echConfig, err := option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.UDPOverStreamVersion { switch option.UDPOverStreamVersion {
case uot.Version, uot.LegacyVersion: case uot.Version, uot.LegacyVersion:
case 0: case 0:
@@ -268,7 +278,9 @@ func NewTuic(option TuicOption) (*Tuic, error) {
rmark: option.RoutingMark, rmark: option.RoutingMark,
prefer: C.NewDNSPrefer(option.IPVersion), prefer: C.NewDNSPrefer(option.IPVersion),
}, },
option: &option, option: &option,
tlsConfig: tlsClientConfig,
echConfig: echConfig,
} }
clientMaxOpenStreams := int64(option.MaxOpenStreams) clientMaxOpenStreams := int64(option.MaxOpenStreams)
@@ -285,7 +297,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
if len(option.Token) > 0 { if len(option.Token) > 0 {
tkn := tuic.GenTKN(option.Token) tkn := tuic.GenTKN(option.Token)
clientOption := &tuic.ClientOptionV4{ clientOption := &tuic.ClientOptionV4{
TlsConfig: tlsC.UConfig(tlsConfig), TlsConfig: tlsClientConfig,
QuicConfig: quicConfig, QuicConfig: quicConfig,
Token: tkn, Token: tkn,
UdpRelayMode: udpRelayMode, UdpRelayMode: udpRelayMode,
@@ -305,7 +317,7 @@ func NewTuic(option TuicOption) (*Tuic, error) {
maxUdpRelayPacketSize = tuic.MaxFragSizeV5 maxUdpRelayPacketSize = tuic.MaxFragSizeV5
} }
clientOption := &tuic.ClientOptionV5{ clientOption := &tuic.ClientOptionV5{
TlsConfig: tlsC.UConfig(tlsConfig), TlsConfig: tlsClientConfig,
QuicConfig: quicConfig, QuicConfig: quicConfig,
Uuid: uuid.FromStringOrNil(option.UUID), Uuid: uuid.FromStringOrNil(option.UUID),
Password: option.Password, Password: option.Password,

View File

@@ -17,8 +17,8 @@ import (
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"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/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
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"
@@ -46,6 +46,7 @@ type Vless struct {
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig realityConfig *tlsC.RealityConfig
echConfig *ech.Config
} }
type VlessOption struct { type VlessOption struct {
@@ -62,6 +63,7 @@ type VlessOption struct {
XUDP bool `proxy:"xudp,omitempty"` XUDP bool `proxy:"xudp,omitempty"`
PacketEncoding string `proxy:"packet-encoding,omitempty"` PacketEncoding string `proxy:"packet-encoding,omitempty"`
Network string `proxy:"network,omitempty"` Network string `proxy:"network,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
@@ -88,6 +90,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{}, Headers: http.Header{},
} }
@@ -151,7 +154,7 @@ func (v *Vless) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = vmess.StreamH2Conn(ctx, c, h2Opts) c, err = vmess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
default: default:
// default tcp network // default tcp network
// handle TLS // handle TLS
@@ -206,6 +209,7 @@ func (v *Vless) streamTLSConn(ctx context.Context, conn net.Conn, isH2 bool) (ne
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint, FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN, NextProtos: v.option.ALPN,
} }
@@ -272,13 +276,8 @@ func (v *Vless) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr if err = v.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
@@ -310,13 +309,8 @@ func (v *Vless) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
} }
} }
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr if err = v.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
c, err := dialer.DialContext(ctx, "tcp", v.addr) c, err := dialer.DialContext(ctx, "tcp", v.addr)
@@ -342,13 +336,8 @@ func (v *Vless) SupportWithDialer() C.NetWork {
// ListenPacketOnStreamConn implements C.ProxyAdapter // ListenPacketOnStreamConn implements C.ProxyAdapter
func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vless use stream-oriented udp with a special address, so we need a net.UDPAddr if err = v.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
if v.option.XUDP { if v.option.XUDP {
@@ -563,6 +552,11 @@ func NewVless(option VlessOption) (*Vless, error) {
return nil, err return nil, err
} }
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network { switch option.Network {
case "h2": case "h2":
if len(option.HTTP2Opts.Host) == 0 { if len(option.HTTP2Opts.Host) == 0 {
@@ -611,7 +605,7 @@ func NewVless(option VlessOption) (*Vless, error) {
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
} }
return v, nil return v, nil

View File

@@ -15,8 +15,8 @@ import (
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"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/ech"
"github.com/metacubex/mihomo/component/proxydialer" "github.com/metacubex/mihomo/component/proxydialer"
"github.com/metacubex/mihomo/component/resolver"
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/ntp" "github.com/metacubex/mihomo/ntp"
@@ -41,6 +41,7 @@ type Vmess struct {
transport *gun.TransportWrap transport *gun.TransportWrap
realityConfig *tlsC.RealityConfig realityConfig *tlsC.RealityConfig
echConfig *ech.Config
} }
type VmessOption struct { type VmessOption struct {
@@ -58,6 +59,7 @@ type VmessOption struct {
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
Fingerprint string `proxy:"fingerprint,omitempty"` Fingerprint string `proxy:"fingerprint,omitempty"`
ServerName string `proxy:"servername,omitempty"` ServerName string `proxy:"servername,omitempty"`
ECHOpts ECHOptions `proxy:"ech-opts,omitempty"`
RealityOpts RealityOptions `proxy:"reality-opts,omitempty"` RealityOpts RealityOptions `proxy:"reality-opts,omitempty"`
HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"`
HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"`
@@ -109,6 +111,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade, V2rayHttpUpgrade: v.option.WSOpts.V2rayHttpUpgrade,
V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen, V2rayHttpUpgradeFastOpen: v.option.WSOpts.V2rayHttpUpgradeFastOpen,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECHConfig: v.echConfig,
Headers: http.Header{}, Headers: http.Header{},
} }
@@ -146,6 +149,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
Host: host, Host: host,
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN, NextProtos: v.option.ALPN,
} }
@@ -195,7 +199,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts) c, err = mihomoVMess.StreamH2Conn(ctx, c, h2Opts)
case "grpc": case "grpc":
c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.realityConfig) c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig, v.echConfig, v.realityConfig)
default: default:
// handle TLS // handle TLS
if v.option.TLS { if v.option.TLS {
@@ -205,6 +209,7 @@ func (v *Vmess) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.M
SkipCertVerify: v.option.SkipCertVerify, SkipCertVerify: v.option.SkipCertVerify,
FingerPrint: v.option.Fingerprint, FingerPrint: v.option.Fingerprint,
ClientFingerprint: v.option.ClientFingerprint, ClientFingerprint: v.option.ClientFingerprint,
ECH: v.echConfig,
Reality: v.realityConfig, Reality: v.realityConfig,
NextProtos: v.option.ALPN, NextProtos: v.option.ALPN,
} }
@@ -324,13 +329,8 @@ func (v *Vmess) DialContextWithDialer(ctx context.Context, dialer C.Dialer, meta
// ListenPacketContext implements C.ProxyAdapter // ListenPacketContext implements C.ProxyAdapter
func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) { func (v *Vmess) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr if err = v.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
var c net.Conn var c net.Conn
// gun transport // gun transport
@@ -361,13 +361,8 @@ func (v *Vmess) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, met
} }
} }
// vmess use stream-oriented udp with a special address, so we need a net.UDPAddr if err = v.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
c, err := dialer.DialContext(ctx, "tcp", v.addr) c, err := dialer.DialContext(ctx, "tcp", v.addr)
@@ -407,13 +402,8 @@ func (v *Vmess) Close() error {
// 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 if err = v.ResolveUDP(ctx, metadata); err != nil {
if !metadata.Resolved() { return nil, err
ip, err := resolver.ResolveIP(ctx, metadata.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
if pc, ok := c.(net.PacketConn); ok { if pc, ok := c.(net.PacketConn); ok {
@@ -474,6 +464,11 @@ func NewVmess(option VmessOption) (*Vmess, error) {
return nil, err return nil, err
} }
v.echConfig, err = v.option.ECHOpts.Parse()
if err != nil {
return nil, err
}
switch option.Network { switch option.Network {
case "h2": case "h2":
if len(option.HTTP2Opts.Host) == 0 { if len(option.HTTP2Opts.Host) == 0 {
@@ -522,7 +517,7 @@ func NewVmess(option VmessOption) (*Vmess, error) {
v.gunTLSConfig = tlsConfig v.gunTLSConfig = tlsConfig
v.gunConfig = gunConfig v.gunConfig = gunConfig
v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.realityConfig) v.transport = gun.NewHTTP2Client(dialFn, tlsConfig, v.option.ClientFingerprint, v.echConfig, v.realityConfig)
} }
return v, nil return v, nil

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
@@ -520,16 +519,8 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
if err = w.init(ctx); err != nil { if err = w.init(ctx); err != nil {
return nil, err return nil, err
} }
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { if err = w.ResolveUDP(ctx, metadata); err != nil {
r := resolver.DefaultResolver return nil, err
if w.resolver != nil {
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return nil, errors.New("can't resolve ip")
}
metadata.DstIP = ip
} }
pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap()) pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
if err != nil { if err != nil {
@@ -541,6 +532,21 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat
return newPacketConn(pc, w), nil return newPacketConn(pc, w), nil
} }
func (w *WireGuard) ResolveUDP(ctx context.Context, metadata *C.Metadata) error {
if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
r := resolver.DefaultResolver
if w.resolver != nil {
r = w.resolver
}
ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
if err != nil {
return fmt.Errorf("can't resolve ip: %w", err)
}
metadata.DstIP = ip
}
return 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

View File

@@ -32,7 +32,6 @@ type HealthCheck struct {
url string url string
extra map[string]*extraOption extra map[string]*extraOption
mu sync.Mutex mu sync.Mutex
started atomic.Bool
proxies []C.Proxy proxies []C.Proxy
interval time.Duration interval time.Duration
lazy bool lazy bool
@@ -43,13 +42,8 @@ type HealthCheck struct {
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
if hc.started.Load() {
log.Warnln("Skip start health check timer due to it's started")
return
}
ticker := time.NewTicker(hc.interval) ticker := time.NewTicker(hc.interval)
hc.start() go hc.check()
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
@@ -62,13 +56,12 @@ func (hc *HealthCheck) process() {
} }
case <-hc.ctx.Done(): case <-hc.ctx.Done():
ticker.Stop() ticker.Stop()
hc.stop()
return return
} }
} }
} }
func (hc *HealthCheck) setProxy(proxies []C.Proxy) { func (hc *HealthCheck) setProxies(proxies []C.Proxy) {
hc.proxies = proxies hc.proxies = proxies
} }
@@ -105,10 +98,6 @@ func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.
option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus} option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
splitAndAddFiltersToExtra(filter, option) splitAndAddFiltersToExtra(filter, option)
hc.extra[url] = option hc.extra[url] = option
if hc.auto() && !hc.started.Load() {
go hc.process()
}
} }
func splitAndAddFiltersToExtra(filter string, option *extraOption) { func splitAndAddFiltersToExtra(filter string, option *extraOption) {
@@ -131,14 +120,6 @@ func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now()) hc.lastTouch.Store(time.Now())
} }
func (hc *HealthCheck) start() {
hc.started.Store(true)
}
func (hc *HealthCheck) stop() {
hc.started.Store(false)
}
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
if len(hc.proxies) == 0 { if len(hc.proxies) == 0 {
return return

View File

@@ -17,7 +17,6 @@ import (
var ( var (
errVehicleType = errors.New("unsupport vehicle type") errVehicleType = errors.New("unsupport vehicle type")
errSubPath = errors.New("path is not subpath of home directory")
) )
type healthCheckSchema struct { type healthCheckSchema struct {
@@ -115,7 +114,7 @@ func ParseProxyProvider(name string, mapping map[string]any) (types.ProxyProvide
if schema.Path != "" { if schema.Path != "" {
path = C.Path.Resolve(schema.Path) path = C.Path.Resolve(schema.Path)
if !C.Path.IsSafePath(path) { if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("%w: %s", errSubPath, path) return nil, C.Path.ErrNotSafePath(path)
} }
} }
vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit) vehicle = resource.NewHTTPVehicle(schema.URL, path, schema.Proxy, schema.Header, resource.DefaultHttpTimeout, schema.SizeLimit)

View File

@@ -57,6 +57,13 @@ func (bp *baseProvider) Version() uint32 {
return bp.version return bp.version
} }
func (bp *baseProvider) Initial() error {
if bp.healthCheck.auto() {
go bp.healthCheck.process()
}
return nil
}
func (bp *baseProvider) HealthCheck() { func (bp *baseProvider) HealthCheck() {
bp.healthCheck.check() bp.healthCheck.check()
} }
@@ -88,7 +95,7 @@ func (bp *baseProvider) RegisterHealthCheckTask(url string, expectedStatus utils
func (bp *baseProvider) setProxies(proxies []C.Proxy) { func (bp *baseProvider) setProxies(proxies []C.Proxy) {
bp.proxies = proxies bp.proxies = proxies
bp.version += 1 bp.version += 1
bp.healthCheck.setProxy(proxies) bp.healthCheck.setProxies(proxies)
if bp.healthCheck.auto() { if bp.healthCheck.auto() {
go bp.healthCheck.check() go bp.healthCheck.check()
} }
@@ -133,6 +140,9 @@ func (pp *proxySetProvider) Update() error {
} }
func (pp *proxySetProvider) Initial() error { func (pp *proxySetProvider) Initial() error {
if err := pp.baseProvider.Initial(); err != nil {
return err
}
_, err := pp.Fetcher.Initial() _, err := pp.Fetcher.Initial()
if err != nil { if err != nil {
return err return err
@@ -162,10 +172,6 @@ func (pp *proxySetProvider) Close() error {
} }
func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) { func NewProxySetProvider(name string, interval time.Duration, payload []map[string]any, parser resource.Parser[[]C.Proxy], vehicle types.Vehicle, hc *HealthCheck) (*ProxySetProvider, error) {
if hc.auto() {
go hc.process()
}
pd := &proxySetProvider{ pd := &proxySetProvider{
baseProvider: baseProvider{ baseProvider: baseProvider{
name: name, name: name,
@@ -185,6 +191,8 @@ func NewProxySetProvider(name string, interval time.Duration, payload []map[stri
return nil, err return nil, err
} }
pd.proxies = proxies pd.proxies = proxies
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
hc.setProxies(proxies)
} }
fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies) fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, pd.setProxies)
@@ -234,10 +242,6 @@ func (ip *inlineProvider) VehicleType() types.VehicleType {
return types.Inline return types.Inline
} }
func (ip *inlineProvider) Initial() error {
return nil
}
func (ip *inlineProvider) Update() error { func (ip *inlineProvider) Update() error {
// make api update happy // make api update happy
ip.updateAt = time.Now() ip.updateAt = time.Now()
@@ -245,10 +249,6 @@ func (ip *inlineProvider) Update() error {
} }
func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) { func NewInlineProvider(name string, payload []map[string]any, parser resource.Parser[[]C.Proxy], hc *HealthCheck) (*InlineProvider, error) {
if hc.auto() {
go hc.process()
}
ps := ProxySchema{Proxies: payload} ps := ProxySchema{Proxies: payload}
buf, err := yaml.Marshal(ps) buf, err := yaml.Marshal(ps)
if err != nil { if err != nil {
@@ -258,6 +258,8 @@ func NewInlineProvider(name string, payload []map[string]any, parser resource.Pa
if err != nil { if err != nil {
return nil, err return nil, err
} }
// direct call setProxies on hc to avoid starting a health check process immediately, it should be done by Initial()
hc.setProxies(proxies)
ip := &inlineProvider{ ip := &inlineProvider{
baseProvider: baseProvider{ baseProvider: baseProvider{
@@ -301,13 +303,6 @@ func (cp *compatibleProvider) Update() error {
return nil return nil
} }
func (cp *compatibleProvider) Initial() error {
if cp.healthCheck.interval != 0 && cp.healthCheck.url != "" {
cp.HealthCheck()
}
return nil
}
func (cp *compatibleProvider) VehicleType() types.VehicleType { func (cp *compatibleProvider) VehicleType() types.VehicleType {
return types.Compatible return types.Compatible
} }
@@ -317,10 +312,6 @@ func NewCompatibleProvider(name string, proxies []C.Proxy, hc *HealthCheck) (*Co
return nil, errors.New("provider need one proxy at least") return nil, errors.New("provider need one proxy at least")
} }
if hc.auto() {
go hc.process()
}
pd := &compatibleProvider{ pd := &compatibleProvider{
baseProvider: baseProvider{ baseProvider: baseProvider{
name: name, name: name,

63
common/atomic/enum.go Normal file
View File

@@ -0,0 +1,63 @@
package atomic
import (
"encoding/json"
"fmt"
"sync/atomic"
)
type Int32Enum[T ~int32] struct {
value atomic.Int32
}
func (i *Int32Enum[T]) MarshalJSON() ([]byte, error) {
return json.Marshal(i.Load())
}
func (i *Int32Enum[T]) UnmarshalJSON(b []byte) error {
var v T
if err := json.Unmarshal(b, &v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int32Enum[T]) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Int32Enum[T]) UnmarshalYAML(unmarshal func(any) error) error {
var v T
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int32Enum[T]) String() string {
return fmt.Sprint(i.Load())
}
func (i *Int32Enum[T]) Store(v T) {
i.value.Store(int32(v))
}
func (i *Int32Enum[T]) Load() T {
return T(i.value.Load())
}
func (i *Int32Enum[T]) Swap(new T) T {
return T(i.value.Swap(int32(new)))
}
func (i *Int32Enum[T]) CompareAndSwap(old, new T) bool {
return i.value.CompareAndSwap(int32(old), int32(new))
}
func NewInt32Enum[T ~int32](v T) *Int32Enum[T] {
a := &Int32Enum[T]{}
a.Store(v)
return a
}

View File

@@ -29,6 +29,19 @@ func (i *Bool) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (i *Bool) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Bool) UnmarshalYAML(unmarshal func(any) error) error {
var v bool
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Bool) String() string { func (i *Bool) String() string {
v := i.Load() v := i.Load()
return strconv.FormatBool(v) return strconv.FormatBool(v)
@@ -58,6 +71,19 @@ func (p *Pointer[T]) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (p *Pointer[T]) MarshalYAML() (any, error) {
return p.Load(), nil
}
func (p *Pointer[T]) UnmarshalYAML(unmarshal func(any) error) error {
var v *T
if err := unmarshal(&v); err != nil {
return err
}
p.Store(v)
return nil
}
func (p *Pointer[T]) String() string { func (p *Pointer[T]) String() string {
return fmt.Sprint(p.Load()) return fmt.Sprint(p.Load())
} }
@@ -84,6 +110,19 @@ func (i *Int32) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (i *Int32) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Int32) UnmarshalYAML(unmarshal func(any) error) error {
var v int32
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int32) String() string { func (i *Int32) String() string {
v := i.Load() v := i.Load()
return strconv.FormatInt(int64(v), 10) return strconv.FormatInt(int64(v), 10)
@@ -111,6 +150,19 @@ func (i *Int64) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (i *Int64) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Int64) UnmarshalYAML(unmarshal func(any) error) error {
var v int64
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Int64) String() string { func (i *Int64) String() string {
v := i.Load() v := i.Load()
return strconv.FormatInt(int64(v), 10) return strconv.FormatInt(int64(v), 10)
@@ -138,6 +190,19 @@ func (i *Uint32) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (i *Uint32) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Uint32) UnmarshalYAML(unmarshal func(any) error) error {
var v uint32
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Uint32) String() string { func (i *Uint32) String() string {
v := i.Load() v := i.Load()
return strconv.FormatUint(uint64(v), 10) return strconv.FormatUint(uint64(v), 10)
@@ -165,6 +230,19 @@ func (i *Uint64) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (i *Uint64) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Uint64) UnmarshalYAML(unmarshal func(any) error) error {
var v uint64
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Uint64) String() string { func (i *Uint64) String() string {
v := i.Load() v := i.Load()
return strconv.FormatUint(uint64(v), 10) return strconv.FormatUint(uint64(v), 10)
@@ -192,6 +270,19 @@ func (i *Uintptr) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (i *Uintptr) MarshalYAML() (any, error) {
return i.Load(), nil
}
func (i *Uintptr) UnmarshalYAML(unmarshal func(any) error) error {
var v uintptr
if err := unmarshal(&v); err != nil {
return err
}
i.Store(v)
return nil
}
func (i *Uintptr) String() string { func (i *Uintptr) String() string {
v := i.Load() v := i.Load()
return strconv.FormatUint(uint64(v), 10) return strconv.FormatUint(uint64(v), 10)

View File

@@ -27,11 +27,16 @@ type tValue[T any] struct {
} }
func (t *TypedValue[T]) Load() T { func (t *TypedValue[T]) Load() T {
value, _ := t.LoadOk()
return value
}
func (t *TypedValue[T]) LoadOk() (_ T, ok bool) {
value := t.value.Load() value := t.value.Load()
if value == nil { if value == nil {
return DefaultValue[T]() return DefaultValue[T](), false
} }
return value.(tValue[T]).value return value.(tValue[T]).value, true
} }
func (t *TypedValue[T]) Store(value T) { func (t *TypedValue[T]) Store(value T) {
@@ -47,7 +52,11 @@ func (t *TypedValue[T]) Swap(new T) T {
} }
func (t *TypedValue[T]) CompareAndSwap(old, new T) bool { func (t *TypedValue[T]) CompareAndSwap(old, new T) bool {
return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) return t.value.CompareAndSwap(tValue[T]{old}, tValue[T]{new}) ||
// In the edge-case where [atomic.Value.Store] is uninitialized
// and trying to compare with the zero value of T,
// then compare-and-swap with the nil any value.
(any(old) == any(DefaultValue[T]()) && t.value.CompareAndSwap(any(nil), tValue[T]{new}))
} }
func (t *TypedValue[T]) MarshalJSON() ([]byte, error) { func (t *TypedValue[T]) MarshalJSON() ([]byte, error) {
@@ -63,6 +72,19 @@ func (t *TypedValue[T]) UnmarshalJSON(b []byte) error {
return nil return nil
} }
func (t *TypedValue[T]) MarshalYAML() (any, error) {
return t.Load(), nil
}
func (t *TypedValue[T]) UnmarshalYAML(unmarshal func(any) error) error {
var v T
if err := unmarshal(&v); err != nil {
return err
}
t.Store(v)
return nil
}
func NewTypedValue[T any](t T) (v TypedValue[T]) { func NewTypedValue[T any](t T) (v TypedValue[T]) {
v.Store(t) v.Store(t)
return return

View File

@@ -0,0 +1,77 @@
package atomic
import (
"io"
"os"
"testing"
)
func TestTypedValue(t *testing.T) {
{
// Always wrapping should not allocate for simple values
// because tValue[T] has the same memory layout as T.
var v TypedValue[bool]
bools := []bool{true, false}
if n := int(testing.AllocsPerRun(1000, func() {
for _, b := range bools {
v.Store(b)
}
})); n != 0 {
t.Errorf("AllocsPerRun = %d, want 0", n)
}
}
{
var v TypedValue[int]
got, gotOk := v.LoadOk()
if got != 0 || gotOk {
t.Fatalf("LoadOk = (%v, %v), want (0, false)", got, gotOk)
}
v.Store(1)
got, gotOk = v.LoadOk()
if got != 1 || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (1, true)", got, gotOk)
}
}
{
var v TypedValue[error]
got, gotOk := v.LoadOk()
if got != nil || gotOk {
t.Fatalf("LoadOk = (%v, %v), want (nil, false)", got, gotOk)
}
v.Store(io.EOF)
got, gotOk = v.LoadOk()
if got != io.EOF || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (EOF, true)", got, gotOk)
}
err := &os.PathError{}
v.Store(err)
got, gotOk = v.LoadOk()
if got != err || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (%v, true)", got, gotOk, err)
}
v.Store(nil)
got, gotOk = v.LoadOk()
if got != nil || !gotOk {
t.Fatalf("LoadOk = (%v, %v), want (nil, true)", got, gotOk)
}
}
{
c1, c2, c3 := make(chan struct{}), make(chan struct{}), make(chan struct{})
var v TypedValue[chan struct{}]
if v.CompareAndSwap(c1, c2) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if v.CompareAndSwap(nil, c1) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
if v.CompareAndSwap(c2, c3) != false {
t.Fatalf("CompareAndSwap = true, want false")
}
if v.CompareAndSwap(c1, c2) != true {
t.Fatalf("CompareAndSwap = false, want true")
}
}
}

View File

@@ -2,6 +2,7 @@ package convert
import ( import (
"encoding/base64" "encoding/base64"
"fmt"
"strings" "strings"
) )
@@ -43,3 +44,22 @@ func decodeUrlSafe(data string) string {
} }
return string(dcBuf) return string(dcBuf)
} }
func TryDecodeBase64(s string) (decoded []byte, err error) {
if len(s)%4 == 0 {
if decoded, err = base64.StdEncoding.DecodeString(s); err == nil {
return
}
if decoded, err = base64.URLEncoding.DecodeString(s); err == nil {
return
}
} else {
if decoded, err = base64.RawStdEncoding.DecodeString(s); err == nil {
return
}
if decoded, err = base64.RawURLEncoding.DecodeString(s); err == nil {
return
}
}
return nil, fmt.Errorf("invalid base64-encoded string")
}

View File

@@ -456,12 +456,12 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
proxies = append(proxies, ss) proxies = append(proxies, ss)
case "ssr": case "ssr":
dcBuf, err := encRaw.DecodeString(body) dcBuf, err := TryDecodeBase64(body)
if err != nil { if err != nil {
continue continue
} }
// ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64&protoparam=&remarks=urlsafebase64&group=urlsafebase64&udpport=0&uot=1 // ssr://host:port:protocol:method:obfs:urlsafebase64pass/?obfsparam=urlsafebase64param&protoparam=urlsafebase64param&remarks=urlsafebase64remarks&group=urlsafebase64group&udpport=0&uot=1
before, after, ok := strings.Cut(string(dcBuf), "/?") before, after, ok := strings.Cut(string(dcBuf), "/?")
if !ok { if !ok {
@@ -490,7 +490,7 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
name := uniqueName(names, remarks) name := uniqueName(names, remarks)
obfsParam := decodeUrlSafe(query.Get("obfsparam")) obfsParam := decodeUrlSafe(query.Get("obfsparam"))
protocolParam := query.Get("protoparam") protocolParam := decodeUrlSafe(query.Get("protoparam"))
ssr := make(map[string]any, 20) ssr := make(map[string]any, 20)
@@ -513,6 +513,101 @@ func ConvertsV2Ray(buf []byte) ([]map[string]any, error) {
} }
proxies = append(proxies, ssr) proxies = append(proxies, ssr)
case "socks", "socks5", "socks5h", "http", "https":
link, err := url.Parse(line)
if err != nil {
continue
}
server := link.Hostname()
if server == "" {
continue
}
portStr := link.Port()
if portStr == "" {
continue
}
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
name := uniqueName(names, remarks)
encodeStr := link.User.String()
var username, password string
if encodeStr != "" {
decodeStr := string(DecodeBase64([]byte(encodeStr)))
splitStr := strings.Split(decodeStr, ":")
// todo: should use url.QueryUnescape ?
username = splitStr[0]
if len(splitStr) == 2 {
password = splitStr[1]
}
}
socks := make(map[string]any, 10)
socks["name"] = name
socks["type"] = func() string {
switch scheme {
case "socks", "socks5", "socks5h":
return "socks5"
case "http", "https":
return "http"
}
return scheme
}()
socks["server"] = server
socks["port"] = portStr
socks["username"] = username
socks["password"] = password
socks["skip-cert-verify"] = true
if scheme == "https" {
socks["tls"] = true
}
proxies = append(proxies, socks)
case "anytls":
// https://github.com/anytls/anytls-go/blob/main/docs/uri_scheme.md
link, err := url.Parse(line)
if err != nil {
continue
}
username := link.User.Username()
password, exist := link.User.Password()
if !exist {
password = username
}
query := link.Query()
server := link.Hostname()
if server == "" {
continue
}
portStr := link.Port()
if portStr == "" {
continue
}
insecure, sni := query.Get("insecure"), query.Get("sni")
insecureBool := insecure == "1"
fingerprint := query.Get("hpkp")
remarks := link.Fragment
if remarks == "" {
remarks = fmt.Sprintf("%s:%s", server, portStr)
}
name := uniqueName(names, remarks)
anytls := make(map[string]any, 10)
anytls["name"] = name
anytls["type"] = "anytls"
anytls["server"] = server
anytls["port"] = portStr
anytls["username"] = username
anytls["password"] = password
anytls["sni"] = sni
anytls["fingerprint"] = fingerprint
anytls["skip-cert-verify"] = insecureBool
anytls["udp"] = true
proxies = append(proxies, anytls)
} }
} }

View File

@@ -20,7 +20,7 @@ type connReadResult struct {
type Conn struct { type Conn struct {
network.ExtendedConn network.ExtendedConn
deadline atomic.TypedValue[time.Time] deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline pipeDeadline PipeDeadline
disablePipe atomic.Bool disablePipe atomic.Bool
inRead atomic.Bool inRead atomic.Bool
resultCh chan *connReadResult resultCh chan *connReadResult
@@ -34,7 +34,7 @@ func IsConn(conn any) bool {
func NewConn(conn net.Conn) *Conn { func NewConn(conn net.Conn) *Conn {
c := &Conn{ c := &Conn{
ExtendedConn: bufio.NewExtendedConn(conn), ExtendedConn: bufio.NewExtendedConn(conn),
pipeDeadline: makePipeDeadline(), pipeDeadline: MakePipeDeadline(),
resultCh: make(chan *connReadResult, 1), resultCh: make(chan *connReadResult, 1),
} }
c.resultCh <- nil c.resultCh <- nil
@@ -58,7 +58,7 @@ func (c *Conn) Read(p []byte) (n int, err error) {
c.resultCh <- nil c.resultCh <- nil
break break
} }
case <-c.pipeDeadline.wait(): case <-c.pipeDeadline.Wait():
return 0, os.ErrDeadlineExceeded return 0, os.ErrDeadlineExceeded
} }
@@ -104,7 +104,7 @@ func (c *Conn) ReadBuffer(buffer *buf.Buffer) (err error) {
c.resultCh <- nil c.resultCh <- nil
break break
} }
case <-c.pipeDeadline.wait(): case <-c.pipeDeadline.Wait():
return os.ErrDeadlineExceeded return os.ErrDeadlineExceeded
} }
@@ -130,7 +130,7 @@ func (c *Conn) SetReadDeadline(t time.Time) error {
return c.ExtendedConn.SetReadDeadline(t) return c.ExtendedConn.SetReadDeadline(t)
} }
c.deadline.Store(t) c.deadline.Store(t)
c.pipeDeadline.set(t) c.pipeDeadline.Set(t)
return nil return nil
} }

View File

@@ -19,7 +19,7 @@ type readResult struct {
type NetPacketConn struct { type NetPacketConn struct {
net.PacketConn net.PacketConn
deadline atomic.TypedValue[time.Time] deadline atomic.TypedValue[time.Time]
pipeDeadline pipeDeadline pipeDeadline PipeDeadline
disablePipe atomic.Bool disablePipe atomic.Bool
inRead atomic.Bool inRead atomic.Bool
resultCh chan any resultCh chan any
@@ -28,7 +28,7 @@ type NetPacketConn struct {
func NewNetPacketConn(pc net.PacketConn) net.PacketConn { func NewNetPacketConn(pc net.PacketConn) net.PacketConn {
npc := &NetPacketConn{ npc := &NetPacketConn{
PacketConn: pc, PacketConn: pc,
pipeDeadline: makePipeDeadline(), pipeDeadline: MakePipeDeadline(),
resultCh: make(chan any, 1), resultCh: make(chan any, 1),
} }
npc.resultCh <- nil npc.resultCh <- nil
@@ -83,7 +83,7 @@ FOR:
c.resultCh <- nil c.resultCh <- nil
break FOR break FOR
} }
case <-c.pipeDeadline.wait(): case <-c.pipeDeadline.Wait():
return 0, nil, os.ErrDeadlineExceeded return 0, nil, os.ErrDeadlineExceeded
} }
} }
@@ -122,7 +122,7 @@ func (c *NetPacketConn) SetReadDeadline(t time.Time) error {
return c.PacketConn.SetReadDeadline(t) return c.PacketConn.SetReadDeadline(t)
} }
c.deadline.Store(t) c.deadline.Store(t)
c.pipeDeadline.set(t) c.pipeDeadline.Set(t)
return nil return nil
} }

View File

@@ -52,7 +52,7 @@ FOR:
c.netPacketConn.resultCh <- nil c.netPacketConn.resultCh <- nil
break FOR break FOR
} }
case <-c.netPacketConn.pipeDeadline.wait(): case <-c.netPacketConn.pipeDeadline.Wait():
return nil, nil, nil, os.ErrDeadlineExceeded return nil, nil, nil, os.ErrDeadlineExceeded
} }
} }

View File

@@ -69,7 +69,7 @@ FOR:
c.netPacketConn.resultCh <- nil c.netPacketConn.resultCh <- nil
break FOR break FOR
} }
case <-c.netPacketConn.pipeDeadline.wait(): case <-c.netPacketConn.pipeDeadline.Wait():
return M.Socksaddr{}, os.ErrDeadlineExceeded return M.Socksaddr{}, os.ErrDeadlineExceeded
} }
} }
@@ -146,7 +146,7 @@ FOR:
c.netPacketConn.resultCh <- nil c.netPacketConn.resultCh <- nil
break FOR break FOR
} }
case <-c.netPacketConn.pipeDeadline.wait(): case <-c.netPacketConn.pipeDeadline.Wait():
return nil, M.Socksaddr{}, os.ErrDeadlineExceeded return nil, M.Socksaddr{}, os.ErrDeadlineExceeded
} }
} }

View File

@@ -9,24 +9,24 @@ import (
"time" "time"
) )
// pipeDeadline is an abstraction for handling timeouts. // PipeDeadline is an abstraction for handling timeouts.
type pipeDeadline struct { type PipeDeadline struct {
mu sync.Mutex // Guards timer and cancel mu sync.Mutex // Guards timer and cancel
timer *time.Timer timer *time.Timer
cancel chan struct{} // Must be non-nil cancel chan struct{} // Must be non-nil
} }
func makePipeDeadline() pipeDeadline { func MakePipeDeadline() PipeDeadline {
return pipeDeadline{cancel: make(chan struct{})} return PipeDeadline{cancel: make(chan struct{})}
} }
// set sets the point in time when the deadline will time out. // Set sets the point in time when the deadline will time out.
// A timeout event is signaled by closing the channel returned by waiter. // A timeout event is signaled by closing the channel returned by waiter.
// Once a timeout has occurred, the deadline can be refreshed by specifying a // Once a timeout has occurred, the deadline can be refreshed by specifying a
// t value in the future. // t value in the future.
// //
// A zero value for t prevents timeout. // A zero value for t prevents timeout.
func (d *pipeDeadline) set(t time.Time) { func (d *PipeDeadline) Set(t time.Time) {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
@@ -61,8 +61,8 @@ func (d *pipeDeadline) set(t time.Time) {
} }
} }
// wait returns a channel that is closed when the deadline is exceeded. // Wait returns a channel that is closed when the deadline is exceeded.
func (d *pipeDeadline) wait() chan struct{} { func (d *PipeDeadline) Wait() chan struct{} {
d.mu.Lock() d.mu.Lock()
defer d.mu.Unlock() defer d.mu.Unlock()
return d.cancel return d.cancel

View File

@@ -33,8 +33,8 @@ type pipe struct {
localDone chan struct{} localDone chan struct{}
remoteDone <-chan struct{} remoteDone <-chan struct{}
readDeadline pipeDeadline readDeadline PipeDeadline
writeDeadline pipeDeadline writeDeadline PipeDeadline
readWaitOptions N.ReadWaitOptions readWaitOptions N.ReadWaitOptions
} }
@@ -56,15 +56,15 @@ func Pipe() (net.Conn, net.Conn) {
rdRx: cb1, rdTx: cn1, rdRx: cb1, rdTx: cn1,
wrTx: cb2, wrRx: cn2, wrTx: cb2, wrRx: cn2,
localDone: done1, remoteDone: done2, localDone: done1, remoteDone: done2,
readDeadline: makePipeDeadline(), readDeadline: MakePipeDeadline(),
writeDeadline: makePipeDeadline(), writeDeadline: MakePipeDeadline(),
} }
p2 := &pipe{ p2 := &pipe{
rdRx: cb2, rdTx: cn2, rdRx: cb2, rdTx: cn2,
wrTx: cb1, wrRx: cn1, wrTx: cb1, wrRx: cn1,
localDone: done2, remoteDone: done1, localDone: done2, remoteDone: done1,
readDeadline: makePipeDeadline(), readDeadline: MakePipeDeadline(),
writeDeadline: makePipeDeadline(), writeDeadline: MakePipeDeadline(),
} }
return p1, p2 return p1, p2
} }
@@ -86,7 +86,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
case isClosedChan(p.remoteDone): case isClosedChan(p.remoteDone):
return 0, io.EOF return 0, io.EOF
case isClosedChan(p.readDeadline.wait()): case isClosedChan(p.readDeadline.Wait()):
return 0, os.ErrDeadlineExceeded return 0, os.ErrDeadlineExceeded
} }
@@ -99,7 +99,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
case <-p.remoteDone: case <-p.remoteDone:
return 0, io.EOF return 0, io.EOF
case <-p.readDeadline.wait(): case <-p.readDeadline.Wait():
return 0, os.ErrDeadlineExceeded return 0, os.ErrDeadlineExceeded
} }
} }
@@ -118,7 +118,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
case isClosedChan(p.remoteDone): case isClosedChan(p.remoteDone):
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
case isClosedChan(p.writeDeadline.wait()): case isClosedChan(p.writeDeadline.Wait()):
return 0, os.ErrDeadlineExceeded return 0, os.ErrDeadlineExceeded
} }
@@ -134,7 +134,7 @@ func (p *pipe) write(b []byte) (n int, err error) {
return n, io.ErrClosedPipe return n, io.ErrClosedPipe
case <-p.remoteDone: case <-p.remoteDone:
return n, io.ErrClosedPipe return n, io.ErrClosedPipe
case <-p.writeDeadline.wait(): case <-p.writeDeadline.Wait():
return n, os.ErrDeadlineExceeded return n, os.ErrDeadlineExceeded
} }
} }
@@ -145,8 +145,8 @@ func (p *pipe) SetDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe return io.ErrClosedPipe
} }
p.readDeadline.set(t) p.readDeadline.Set(t)
p.writeDeadline.set(t) p.writeDeadline.Set(t)
return nil return nil
} }
@@ -154,7 +154,7 @@ func (p *pipe) SetReadDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe return io.ErrClosedPipe
} }
p.readDeadline.set(t) p.readDeadline.Set(t)
return nil return nil
} }
@@ -162,7 +162,7 @@ func (p *pipe) SetWriteDeadline(t time.Time) error {
if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) { if isClosedChan(p.localDone) || isClosedChan(p.remoteDone) {
return io.ErrClosedPipe return io.ErrClosedPipe
} }
p.writeDeadline.set(t) p.writeDeadline.Set(t)
return nil return nil
} }
@@ -192,7 +192,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
return nil, io.ErrClosedPipe return nil, io.ErrClosedPipe
case isClosedChan(p.remoteDone): case isClosedChan(p.remoteDone):
return nil, io.EOF return nil, io.EOF
case isClosedChan(p.readDeadline.wait()): case isClosedChan(p.readDeadline.Wait()):
return nil, os.ErrDeadlineExceeded return nil, os.ErrDeadlineExceeded
} }
select { select {
@@ -211,7 +211,7 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) {
return nil, io.ErrClosedPipe return nil, io.ErrClosedPipe
case <-p.remoteDone: case <-p.remoteDone:
return nil, io.EOF return nil, io.EOF
case <-p.readDeadline.wait(): case <-p.readDeadline.Wait():
return nil, os.ErrDeadlineExceeded return nil, os.ErrDeadlineExceeded
} }
} }

View File

@@ -34,8 +34,8 @@ func (l *handleContextListener) init() {
} }
} }
}() }()
if c, err := l.handle(l.ctx, c); err == nil { if conn, err := l.handle(l.ctx, c); err == nil {
l.conns <- c l.conns <- conn
} else { } else {
// handle failed, close the underlying connection. // handle failed, close the underlying connection.
_ = c.Close() _ = c.Close()

View File

@@ -8,18 +8,23 @@ import (
"sync" "sync"
) )
var defaultAllocator = NewAllocator() var DefaultAllocator = NewAllocator()
// Allocator for incoming frames, optimized to prevent overwriting after zeroing type Allocator interface {
type Allocator struct { Get(size int) []byte
Put(buf []byte) error
}
// defaultAllocator for incoming frames, optimized to prevent overwriting after zeroing
type defaultAllocator struct {
buffers [11]sync.Pool buffers [11]sync.Pool
} }
// NewAllocator initiates a []byte allocator for frames less than 65536 bytes, // NewAllocator initiates a []byte allocator for frames less than 65536 bytes,
// the waste(memory fragmentation) of space allocation is guaranteed to be // the waste(memory fragmentation) of space allocation is guaranteed to be
// no more than 50%. // no more than 50%.
func NewAllocator() *Allocator { func NewAllocator() Allocator {
return &Allocator{ return &defaultAllocator{
buffers: [...]sync.Pool{ // 64B -> 64K buffers: [...]sync.Pool{ // 64B -> 64K
{New: func() any { return new([1 << 6]byte) }}, {New: func() any { return new([1 << 6]byte) }},
{New: func() any { return new([1 << 7]byte) }}, {New: func() any { return new([1 << 7]byte) }},
@@ -37,7 +42,7 @@ func NewAllocator() *Allocator {
} }
// Get a []byte from pool with most appropriate cap // Get a []byte from pool with most appropriate cap
func (alloc *Allocator) Get(size int) []byte { func (alloc *defaultAllocator) Get(size int) []byte {
switch { switch {
case size < 0: case size < 0:
panic("alloc.Get: len out of range") panic("alloc.Get: len out of range")
@@ -87,7 +92,7 @@ func (alloc *Allocator) Get(size int) []byte {
// Put returns a []byte to pool for future use, // Put returns a []byte to pool for future use,
// which the cap must be exactly 2^n // which the cap must be exactly 2^n
func (alloc *Allocator) Put(buf []byte) error { func (alloc *defaultAllocator) Put(buf []byte) error {
if cap(buf) == 0 || cap(buf) > 65536 { if cap(buf) == 0 || cap(buf) > 65536 {
return nil return nil
} }

View File

@@ -3,13 +3,12 @@
package pool package pool
const ( const (
// RelayBufferSize using for tcp
// io.Copy default buffer size is 32 KiB // io.Copy default buffer size is 32 KiB
// but the maximum packet size of vmess/shadowsocks is about 16 KiB
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 16 * 1024 RelayBufferSize = 16 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually // UDPBufferSize using for udp
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU // Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib // set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 8 * 1024 UDPBufferSize = 8 * 1024
) )

View File

@@ -3,13 +3,12 @@
package pool package pool
const ( const (
// RelayBufferSize using for tcp
// io.Copy default buffer size is 32 KiB // io.Copy default buffer size is 32 KiB
// but the maximum packet size of vmess/shadowsocks is about 16 KiB RelayBufferSize = 32 * 1024
// so define a buffer of 20 KiB to reduce the memory of each TCP relay
RelayBufferSize = 20 * 1024
// RelayBufferSize uses 20KiB, but due to the allocator it will actually // UDPBufferSize using for udp
// request 32Kib. Most UDPs are smaller than the MTU, and the TUN's MTU // Most UDPs are smaller than the MTU, and the TUN's MTU
// set to 9000, so the UDP Buffer size set to 16Kib // set to 9000, so the UDP Buffer size set to 16Kib
UDPBufferSize = 16 * 1024 UDPBufferSize = 16 * 1024
) )

View File

@@ -1,9 +1,9 @@
package pool package pool
func Get(size int) []byte { func Get(size int) []byte {
return defaultAllocator.Get(size) return DefaultAllocator.Get(size)
} }
func Put(buf []byte) error { func Put(buf []byte) error {
return defaultAllocator.Put(buf) return DefaultAllocator.Put(buf)
} }

View File

@@ -3,5 +3,5 @@ package pool
import "github.com/metacubex/sing/common/buf" import "github.com/metacubex/sing/common/buf"
func init() { func init() {
buf.DefaultAllocator = defaultAllocator buf.DefaultAllocator = DefaultAllocator
} }

View File

@@ -239,13 +239,15 @@ func (n *num) UnmarshalText(text []byte) (err error) {
func TestStructure_TextUnmarshaller(t *testing.T) { func TestStructure_TextUnmarshaller(t *testing.T) {
rawMap := map[string]any{ rawMap := map[string]any{
"num": "255", "num": "255",
"num_p": "127", "num_p": "127",
"num_arr": []string{"1", "2", "3"},
} }
s := &struct { s := &struct {
Num num `test:"num"` Num num `test:"num"`
NumP *num `test:"num_p"` NumP *num `test:"num_p"`
NumArr []num `test:"num_arr"`
}{} }{}
err := decoder.Decode(rawMap, s) err := decoder.Decode(rawMap, s)
@@ -253,6 +255,7 @@ func TestStructure_TextUnmarshaller(t *testing.T) {
assert.Equal(t, 255, s.Num.a) assert.Equal(t, 255, s.Num.a)
assert.NotNil(t, s.NumP) assert.NotNil(t, s.NumP)
assert.Equal(t, s.NumP.a, 127) assert.Equal(t, s.NumP.a, 127)
assert.Equal(t, s.NumArr, []num{{1}, {2}, {3}})
// test WeaklyTypedInput // test WeaklyTypedInput
rawMap["num"] = 256 rawMap["num"] = 256

View File

@@ -41,3 +41,11 @@ func NewAuthenticator(users []AuthUser) Authenticator {
} }
return au return au
} }
var AlwaysValid Authenticator = alwaysValid{}
type alwaysValid struct{}
func (alwaysValid) Verify(string, string) bool { return true }
func (alwaysValid) Users() []string { return nil }

View File

@@ -83,7 +83,7 @@ func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error)
if len(customCA) > 0 { if len(customCA) > 0 {
path := C.Path.Resolve(customCA) path := C.Path.Resolve(customCA)
if !C.Path.IsSafePath(path) { if !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path) return nil, C.Path.ErrNotSafePath(path)
} }
certificate, err = os.ReadFile(path) certificate, err = os.ReadFile(path)
if err != nil { if err != nil {

View File

@@ -17,6 +17,7 @@ import (
type Path interface { type Path interface {
Resolve(path string) string Resolve(path string) string
IsSafePath(path string) bool IsSafePath(path string) bool
ErrNotSafePath(path string) error
} }
// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution. // LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution.
@@ -42,10 +43,12 @@ func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate,
certificate = path.Resolve(certificate) certificate = path.Resolve(certificate)
privateKey = path.Resolve(privateKey) privateKey = path.Resolve(privateKey)
var loadErr error var loadErr error
if path.IsSafePath(certificate) && path.IsSafePath(privateKey) { if !path.IsSafePath(certificate) {
cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey) loadErr = path.ErrNotSafePath(certificate)
} else if !path.IsSafePath(privateKey) {
loadErr = path.ErrNotSafePath(privateKey)
} else { } else {
loadErr = fmt.Errorf("path is not subpath of home directory") cert, loadErr = tls.LoadX509KeyPair(certificate, privateKey)
} }
if loadErr != nil { if loadErr != nil {
return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())

View File

@@ -73,7 +73,7 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
} }
if opt.interfaceName == "" { if opt.interfaceName == "" {
if finder := DefaultInterfaceFinder.Load(); finder != nil { if finder := DefaultInterfaceFinder.Load(); finder != nil {
opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr()) opt.interfaceName = finder.FindInterfaceName(rAddrPort.Addr().Unmap())
} }
} }
if rAddrPort.Addr().Unmap().IsLoopback() { if rAddrPort.Addr().Unmap().IsLoopback() {

31
component/ech/ech.go Normal file
View File

@@ -0,0 +1,31 @@
package ech
import (
"context"
"fmt"
tlsC "github.com/metacubex/mihomo/component/tls"
)
type Config struct {
GetEncryptedClientHelloConfigList func(ctx context.Context, serverName string) ([]byte, error)
}
func (cfg *Config) ClientHandle(ctx context.Context, tlsConfig *tlsC.Config) (err error) {
if cfg == nil {
return nil
}
echConfigList, err := cfg.GetEncryptedClientHelloConfigList(ctx, tlsConfig.ServerName)
if err != nil {
return fmt.Errorf("resolve ECH config error: %w", err)
}
tlsConfig.EncryptedClientHelloConfigList = echConfigList
if tlsConfig.MinVersion != 0 && tlsConfig.MinVersion < tlsC.VersionTLS13 {
tlsConfig.MinVersion = tlsC.VersionTLS13
}
if tlsConfig.MaxVersion != 0 && tlsConfig.MaxVersion < tlsC.VersionTLS13 {
tlsConfig.MaxVersion = tlsC.VersionTLS13
}
return nil
}

143
component/ech/key.go Normal file
View File

@@ -0,0 +1,143 @@
package ech
import (
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"os"
"github.com/metacubex/mihomo/component/ca"
tlsC "github.com/metacubex/mihomo/component/tls"
"golang.org/x/crypto/cryptobyte"
)
const (
AEAD_AES_128_GCM = 0x0001
AEAD_AES_256_GCM = 0x0002
AEAD_ChaCha20Poly1305 = 0x0003
)
const extensionEncryptedClientHello = 0xfe0d
const DHKEM_X25519_HKDF_SHA256 = 0x0020
const KDF_HKDF_SHA256 = 0x0001
// sortedSupportedAEADs is just a sorted version of hpke.SupportedAEADS.
// We need this so that when we insert them into ECHConfigs the ordering
// is stable.
var sortedSupportedAEADs = []uint16{AEAD_AES_128_GCM, AEAD_AES_256_GCM, AEAD_ChaCha20Poly1305}
func marshalECHConfig(id uint8, pubKey []byte, publicName string, maxNameLen uint8) []byte {
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16(extensionEncryptedClientHello)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddUint8(id)
builder.AddUint16(DHKEM_X25519_HKDF_SHA256) // The only DHKEM we support
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(pubKey)
})
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
for _, aeadID := range sortedSupportedAEADs {
builder.AddUint16(KDF_HKDF_SHA256) // The only KDF we support
builder.AddUint16(aeadID)
}
})
builder.AddUint8(maxNameLen)
builder.AddUint8LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes([]byte(publicName))
})
builder.AddUint16(0) // extensions
})
return builder.BytesOrPanic()
}
func GenECHConfig(publicName string) (configBase64 string, keyPem string, err error) {
echKey, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return
}
echConfig := marshalECHConfig(0, echKey.PublicKey().Bytes(), publicName, 0)
builder := cryptobyte.NewBuilder(nil)
builder.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
echConfigList := builder.BytesOrPanic()
builder2 := cryptobyte.NewBuilder(nil)
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echKey.Bytes())
})
builder2.AddUint16LengthPrefixed(func(builder *cryptobyte.Builder) {
builder.AddBytes(echConfig)
})
echConfigKeys := builder2.BytesOrPanic()
configBase64 = base64.StdEncoding.EncodeToString(echConfigList)
keyPem = string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: echConfigKeys}))
return
}
func UnmarshalECHKeys(raw []byte) ([]tlsC.EncryptedClientHelloKey, error) {
var keys []tlsC.EncryptedClientHelloKey
rawString := cryptobyte.String(raw)
for !rawString.Empty() {
var key tlsC.EncryptedClientHelloKey
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.PrivateKey)) {
return nil, errors.New("error parsing private key")
}
if !rawString.ReadUint16LengthPrefixed((*cryptobyte.String)(&key.Config)) {
return nil, errors.New("error parsing config")
}
keys = append(keys, key)
}
if len(keys) == 0 {
return nil, errors.New("empty ECH keys")
}
return keys, nil
}
func LoadECHKey(key string, tlsConfig *tlsC.Config, path ca.Path) error {
if key == "" {
return nil
}
painTextErr := loadECHKey([]byte(key), tlsConfig)
if painTextErr == nil {
return nil
}
key = path.Resolve(key)
var loadErr error
if !path.IsSafePath(key) {
loadErr = path.ErrNotSafePath(key)
} else {
var echKey []byte
echKey, loadErr = os.ReadFile(key)
if loadErr == nil {
loadErr = loadECHKey(echKey, tlsConfig)
}
}
if loadErr != nil {
return fmt.Errorf("parse ECH keys failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error())
}
return nil
}
func loadECHKey(echKey []byte, tlsConfig *tlsC.Config) error {
block, rest := pem.Decode(echKey)
if block == nil || block.Type != "ECH KEYS" || len(rest) > 0 {
return errors.New("invalid ECH keys pem")
}
echKeys, err := UnmarshalECHKeys(block.Bytes)
if err != nil {
return fmt.Errorf("parse ECH keys: %w", err)
}
tlsConfig.EncryptedClientHelloKeys = echKeys
return nil
}

View File

@@ -4,12 +4,14 @@ import (
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"github.com/metacubex/mihomo/component/ech"
"github.com/gofrs/uuid/v5" "github.com/gofrs/uuid/v5"
) )
func Main(args []string) { func Main(args []string) {
if len(args) < 1 { if len(args) < 1 {
panic("Using: generate uuid/reality-keypair/wg-keypair") panic("Using: generate uuid/reality-keypair/wg-keypair/ech-keypair")
} }
switch args[0] { switch args[0] {
case "uuid": case "uuid":
@@ -33,5 +35,15 @@ func Main(args []string) {
} }
fmt.Println("PrivateKey: " + privateKey.String()) fmt.Println("PrivateKey: " + privateKey.String())
fmt.Println("PublicKey: " + privateKey.PublicKey().String()) fmt.Println("PublicKey: " + privateKey.PublicKey().String())
case "ech-keypair":
if len(args) < 2 {
panic("Using: generate ech-keypair <plain_server_name>")
}
configBase64, keyPem, err := ech.GenECHConfig(args[1])
if err != nil {
panic(err)
}
fmt.Println("Config:", configBase64)
fmt.Println("Key:", keyPem)
} }
} }

View File

@@ -1,7 +1,6 @@
package geodata package geodata
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
@@ -76,13 +75,13 @@ func LoadGeoSiteMatcher(countryCode string) (router.DomainMatcher, error) {
if countryCode[0] == '!' { if countryCode[0] == '!' {
not = true not = true
countryCode = countryCode[1:] countryCode = countryCode[1:]
if countryCode == "" {
return nil, fmt.Errorf("country code could not be empty")
}
} }
countryCode = strings.ToLower(countryCode) countryCode = strings.ToLower(countryCode)
parts := strings.Split(countryCode, "@") parts := strings.Split(countryCode, "@")
if len(parts) == 0 {
return nil, errors.New("empty rule")
}
listName := strings.TrimSpace(parts[0]) listName := strings.TrimSpace(parts[0])
attrVal := parts[1:] attrVal := parts[1:]
attrs := parseAttrs(attrVal) attrs := parseAttrs(attrVal)

View File

@@ -26,8 +26,9 @@ var (
) )
type ifaceCache struct { type ifaceCache struct {
ifMap map[string]*Interface ifMapByName map[string]*Interface
ifTable bart.Table[*Interface] ifMapByAddr map[netip.Addr]*Interface
ifTable bart.Table[*Interface]
} }
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20) var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
@@ -40,7 +41,8 @@ func getCache() (*ifaceCache, error) {
} }
cache := &ifaceCache{ cache := &ifaceCache{
ifMap: make(map[string]*Interface), ifMapByName: make(map[string]*Interface),
ifMapByAddr: make(map[netip.Addr]*Interface),
} }
for _, iface := range ifaces { for _, iface := range ifaces {
@@ -78,12 +80,13 @@ func getCache() (*ifaceCache, error) {
Flags: iface.Flags, Flags: iface.Flags,
Addresses: ipNets, Addresses: ipNets,
} }
cache.ifMap[iface.Name] = ifaceObj cache.ifMapByName[iface.Name] = ifaceObj
if iface.Flags&net.FlagUp == 0 { if iface.Flags&net.FlagUp == 0 {
continue // interface down continue // interface down
} }
for _, prefix := range ipNets { for _, prefix := range ipNets {
cache.ifMapByAddr[prefix.Addr()] = ifaceObj
cache.ifTable.Insert(prefix, ifaceObj) cache.ifTable.Insert(prefix, ifaceObj)
} }
} }
@@ -98,7 +101,7 @@ func Interfaces() (map[string]*Interface, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return cache.ifMap, nil return cache.ifMapByName, nil
} }
func ResolveInterface(name string) (*Interface, error) { func ResolveInterface(name string) (*Interface, error) {
@@ -120,6 +123,11 @@ func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
// maybe two interfaces have the same prefix but different address
// so direct check address equal before do a route lookup (longest prefix match)
if iface, ok := cache.ifMapByAddr[addr]; ok {
return iface, nil
}
iface, ok := cache.ifTable.Lookup(addr) iface, ok := cache.ifTable.Lookup(addr)
if !ok { if !ok {
return nil, ErrIfaceNotFound return nil, ErrIfaceNotFound
@@ -133,7 +141,8 @@ func IsLocalIp(addr netip.Addr) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
return cache.ifTable.Contains(addr), nil _, ok := cache.ifMapByAddr[addr]
return ok, nil
} }
func FlushCache() { func FlushCache() {

View File

@@ -1,57 +1,52 @@
package process package process
import ( import (
"encoding/json"
"errors" "errors"
"strings" "strings"
) )
const ( const (
FindProcessAlways = "always" FindProcessStrict FindProcessMode = iota
FindProcessStrict = "strict" FindProcessAlways
FindProcessOff = "off" FindProcessOff
) )
var ( var (
validModes = map[string]struct{}{ validModes = map[string]FindProcessMode{
FindProcessAlways: {}, FindProcessStrict.String(): FindProcessStrict,
FindProcessOff: {}, FindProcessAlways.String(): FindProcessAlways,
FindProcessStrict: {}, FindProcessOff.String(): FindProcessOff,
} }
) )
type FindProcessMode string type FindProcessMode int32
func (m FindProcessMode) Always() bool { // UnmarshalText unserialize FindProcessMode
return m == FindProcessAlways func (m *FindProcessMode) UnmarshalText(data []byte) error {
} return m.Set(string(data))
func (m FindProcessMode) Off() bool {
return m == FindProcessOff
}
func (m *FindProcessMode) UnmarshalYAML(unmarshal func(any) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
return m.Set(tp)
}
func (m *FindProcessMode) UnmarshalJSON(data []byte) error {
var tp string
if err := json.Unmarshal(data, &tp); err != nil {
return err
}
return m.Set(tp)
} }
func (m *FindProcessMode) Set(value string) error { func (m *FindProcessMode) Set(value string) error {
mode := strings.ToLower(value) mode, exist := validModes[strings.ToLower(value)]
_, exist := validModes[mode]
if !exist { if !exist {
return errors.New("invalid find process mode") return errors.New("invalid find process mode")
} }
*m = FindProcessMode(mode) *m = mode
return nil return nil
} }
// MarshalText serialize FindProcessMode
func (m FindProcessMode) MarshalText() ([]byte, error) {
return []byte(m.String()), nil
}
func (m FindProcessMode) String() string {
switch m {
case FindProcessAlways:
return "always"
case FindProcessOff:
return "off"
default:
return "strict"
}
}

View File

@@ -2,7 +2,6 @@ package proxydialer
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
@@ -10,7 +9,6 @@ import (
N "github.com/metacubex/mihomo/common/net" N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/dialer"
"github.com/metacubex/mihomo/component/resolver"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/mihomo/tunnel/statistic"
@@ -40,17 +38,16 @@ func (p proxyDialer) DialContext(ctx context.Context, network, address string) (
return nil, err return nil, err
} }
if strings.Contains(network, "udp") { // using in wireguard outbound if strings.Contains(network, "udp") { // using in wireguard outbound
if !currentMeta.Resolved() {
ip, err := resolver.ResolveIP(ctx, currentMeta.Host)
if err != nil {
return nil, errors.New("can't resolve ip")
}
currentMeta.DstIP = ip
}
pc, err := p.listenPacket(ctx, currentMeta) pc, err := p.listenPacket(ctx, currentMeta)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !currentMeta.Resolved() { // should not happen, maybe by a wrongly implemented proxy, but we can handle this (:
err = pc.ResolveUDP(ctx, currentMeta)
if err != nil {
return nil, err
}
}
return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil return N.NewBindPacketConn(pc, currentMeta.UDPAddr()), nil
} }
var conn C.Conn var conn C.Conn

View File

@@ -77,7 +77,7 @@ func NewHostValue(value any) (HostValue, error) {
isDomain = false isDomain = false
for _, str := range valueArr { for _, str := range valueArr {
if ip, err := netip.ParseAddr(str); err == nil { if ip, err := netip.ParseAddr(str); err == nil {
ips = append(ips, ip) ips = append(ips, ip.Unmap())
} else { } else {
return HostValue{}, err return HostValue{}, err
} }
@@ -85,7 +85,7 @@ func NewHostValue(value any) (HostValue, error) {
} else if len(valueArr) == 1 { } else if len(valueArr) == 1 {
host := valueArr[0] host := valueArr[0]
if ip, err := netip.ParseAddr(host); err == nil { if ip, err := netip.ParseAddr(host); err == nil {
ips = append(ips, ip) ips = append(ips, ip.Unmap())
isDomain = false isDomain = false
} else { } else {
domain = host domain = host

View File

@@ -46,17 +46,24 @@ func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration)
ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout)
defer cancel() defer cancel()
inData := buff[:n] inData := buff[:n]
msg, err := relayDnsPacket(ctx, inData, buff, 0) outBuff := buff[2:]
msg, err := relayDnsPacket(ctx, inData, outBuff, 0)
if err != nil { if err != nil {
return err return err
} }
err = binary.Write(conn, binary.BigEndian, uint16(len(msg))) if &msg[0] == &outBuff[0] { // msg is still in the buff
if err != nil { binary.BigEndian.PutUint16(buff[:2], uint16(len(msg)))
return err outBuff = buff[:2+len(msg)]
} else { // buff not big enough (WTF???)
newBuff := pool.Get(len(msg) + 2)
defer pool.Put(newBuff)
binary.BigEndian.PutUint16(newBuff[:2], uint16(len(msg)))
copy(newBuff[2:], msg)
outBuff = newBuff
} }
_, err = conn.Write(msg) _, err = conn.Write(outBuff)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/netip" "net/netip"
"strings"
"time" "time"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
@@ -49,6 +48,7 @@ type Resolver interface {
LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIP(ctx context.Context, host string) (ips []netip.Addr, err error)
LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIPv4(ctx context.Context, host string) (ips []netip.Addr, err error)
LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error) LookupIPv6(ctx context.Context, host string) (ips []netip.Addr, err error)
ResolveECH(ctx context.Context, host string) ([]byte, error)
ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error) ExchangeContext(ctx context.Context, m *dns.Msg) (msg *dns.Msg, err error)
Invalid() bool Invalid() bool
ClearCache() ClearCache()
@@ -67,7 +67,8 @@ func LookupIPv4WithResolver(ctx context.Context, host string, r Resolver) ([]net
ip, err := netip.ParseAddr(host) ip, err := netip.ParseAddr(host)
if err == nil { if err == nil {
if ip.Is4() || ip.Is4In6() { ip = ip.Unmap()
if ip.Is4() {
return []netip.Addr{ip}, nil return []netip.Addr{ip}, nil
} }
return []netip.Addr{}, ErrIPVersion return []netip.Addr{}, ErrIPVersion
@@ -116,7 +117,8 @@ func LookupIPv6WithResolver(ctx context.Context, host string, r Resolver) ([]net
} }
if ip, err := netip.ParseAddr(host); err == nil { if ip, err := netip.ParseAddr(host); err == nil {
if strings.Contains(host, ":") { ip = ip.Unmap()
if ip.Is6() {
return []netip.Addr{ip}, nil return []netip.Addr{ip}, nil
} }
return nil, ErrIPVersion return nil, ErrIPVersion
@@ -165,6 +167,7 @@ func LookupIPWithResolver(ctx context.Context, host string, r Resolver) ([]netip
} }
if ip, err := netip.ParseAddr(host); err == nil { if ip, err := netip.ParseAddr(host); err == nil {
ip = ip.Unmap()
return []netip.Addr{ip}, nil return []netip.Addr{ip}, nil
} }
@@ -216,6 +219,17 @@ func ResolveIPPrefer6(ctx context.Context, host string) (netip.Addr, error) {
return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver) return ResolveIPPrefer6WithResolver(ctx, host, DefaultResolver)
} }
func ResolveECHWithResolver(ctx context.Context, host string, r Resolver) ([]byte, error) {
if r != nil && r.Invalid() {
return r.ResolveECH(ctx, host)
}
return SystemResolver.ResolveECH(ctx, host)
}
func ResolveECH(ctx context.Context, host string) ([]byte, error) {
return ResolveECHWithResolver(ctx, host, DefaultResolver)
}
func ResetConnection() { func ResetConnection() {
if DefaultResolver != nil { if DefaultResolver != nil {
go DefaultResolver.ResetConnection() go DefaultResolver.ResetConnection()

View File

@@ -105,6 +105,7 @@ func (f *Fetcher[V]) loadBuf(buf []byte, hash utils.HashType, updateFile bool) (
_ = os.Chtimes(f.vehicle.Path(), now, now) _ = os.Chtimes(f.vehicle.Path(), now, now)
} }
f.updatedAt = now f.updatedAt = now
f.backoff.Reset() // no error, reset backoff
return lo.Empty[V](), true, nil return lo.Empty[V](), true, nil
} }
@@ -220,6 +221,10 @@ func (f *Fetcher[V]) updateWithLog() {
func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] { func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicle, parser Parser[V], onUpdate func(V)) *Fetcher[V] {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
minBackoff := 10 * time.Second
if interval < minBackoff {
minBackoff = interval
}
return &Fetcher[V]{ return &Fetcher[V]{
ctx: ctx, ctx: ctx,
ctxCancel: cancel, ctxCancel: cancel,
@@ -231,7 +236,7 @@ func NewFetcher[V any](name string, interval time.Duration, vehicle types.Vehicl
backoff: slowdown.Backoff{ backoff: slowdown.Backoff{
Factor: 2, Factor: 2,
Jitter: false, Jitter: false,
Min: time.Second, Min: minBackoff,
Max: interval, Max: interval,
}, },
} }

View File

@@ -6,6 +6,8 @@ import (
"net/netip" "net/netip"
"time" "time"
"github.com/metacubex/sing/common/metadata"
"github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/common/lru"
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"
@@ -72,8 +74,16 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
overrideDest := config.OverrideDest overrideDest := config.OverrideDest
if inWhitelist { if inWhitelist {
replaceDomain := func(metadata *C.Metadata, host string) {
if sd.domainCanReplace(host) {
replaceDomain(metadata, host, overrideDest)
} else {
log.Debugln("[Sniffer] Skip sni[%s]", host)
}
}
if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok { if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok {
return wrapable.WrapperSender(packetSender, overrideDest) return wrapable.WrapperSender(packetSender, replaceDomain)
} }
host, err := current.SniffData(packet.Data()) host, err := current.SniffData(packet.Data())
@@ -81,7 +91,7 @@ func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSend
continue continue
} }
replaceDomain(metadata, host, overrideDest) replaceDomain(metadata, host)
return packetSender return packetSender
} }
} }
@@ -128,11 +138,9 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool
return false return false
} }
for _, matcher := range sd.skipDomain { if !sd.domainCanReplace(host) {
if matcher.MatchDomain(host) { log.Debugln("[Sniffer] Skip sni[%s]", host)
log.Debugln("[Sniffer] Skip sni[%s]", host) return false
return false
}
} }
sd.skipList.Delete(dst) sd.skipList.Delete(dst)
@@ -152,10 +160,23 @@ func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
metadata.RemoteAddress(), metadata.RemoteAddress(),
metadata.Host, host) metadata.Host, host)
metadata.Host = host metadata.Host = host
metadata.DstIP = netip.Addr{}
} }
metadata.DNSMode = C.DNSNormal metadata.DNSMode = C.DNSNormal
} }
func (sd *Dispatcher) domainCanReplace(host string) bool {
if host == "." || !metadata.IsDomainName(host) {
return false
}
for _, matcher := range sd.skipDomain {
if matcher.MatchDomain(host) {
return false
}
}
return true
}
func (sd *Dispatcher) Enable() bool { func (sd *Dispatcher) Enable() bool {
return sd != nil && sd.enable return sd != nil && sd.enable
} }

View File

@@ -74,24 +74,25 @@ func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) {
return "", ErrorUnsupportedSniffer return "", ErrorUnsupportedSniffer
} }
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender { func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, replaceDomain sniffer.ReplaceDomain) constant.PacketSender {
return &quicPacketSender{ return &quicPacketSender{
sender: packetSender, PacketSender: packetSender,
chClose: make(chan struct{}), replaceDomain: replaceDomain,
override: override, chClose: make(chan struct{}),
} }
} }
var _ constant.PacketSender = (*quicPacketSender)(nil) var _ constant.PacketSender = (*quicPacketSender)(nil)
type quicPacketSender struct { type quicPacketSender struct {
lock sync.RWMutex lock sync.RWMutex
ranges utils.IntRanges[uint64] ranges utils.IntRanges[uint64]
buffer []byte buffer []byte
result string result *string
override bool
sender constant.PacketSender replaceDomain sniffer.ReplaceDomain
constant.PacketSender
chClose chan struct{} chClose chan struct{}
closed bool closed bool
@@ -100,7 +101,7 @@ type quicPacketSender struct {
// Send will send PacketAdapter nonblocking // Send will send PacketAdapter nonblocking
// the implement must call UDPPacket.Drop() inside Send // the implement must call UDPPacket.Drop() inside Send
func (q *quicPacketSender) Send(current constant.PacketAdapter) { func (q *quicPacketSender) Send(current constant.PacketAdapter) {
defer q.sender.Send(current) defer q.PacketSender.Send(current)
q.lock.RLock() q.lock.RLock()
if q.closed { if q.closed {
@@ -116,29 +117,27 @@ func (q *quicPacketSender) Send(current constant.PacketAdapter) {
} }
} }
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy // DoSniff wait sniffer recv all fragments and update the domain
func (q *quicPacketSender) Process(conn constant.PacketConn, proxy constant.WriteBackProxy) { func (q *quicPacketSender) DoSniff(metadata *constant.Metadata) error {
q.sender.Process(conn, proxy)
}
// ResolveUDP wait sniffer recv all fragments and update the domain
func (q *quicPacketSender) ResolveUDP(data *constant.Metadata) error {
select { select {
case <-q.chClose: case <-q.chClose:
q.lock.RLock() q.lock.RLock()
replaceDomain(data, q.result, q.override) if q.result != nil {
host := *q.result
q.replaceDomain(metadata, host)
}
q.lock.RUnlock() q.lock.RUnlock()
break break
case <-time.After(quicWaitConn): case <-time.After(quicWaitConn):
q.close() q.close()
} }
return q.sender.ResolveUDP(data) return q.PacketSender.DoSniff(metadata)
} }
// Close stop the Process loop // Close stop the Process loop
func (q *quicPacketSender) Close() { func (q *quicPacketSender) Close() {
q.sender.Close() q.PacketSender.Close()
q.close() q.close()
} }
@@ -433,7 +432,7 @@ func (q *quicPacketSender) tryAssemble() error {
} }
q.lock.Lock() q.lock.Lock()
q.result = *domain q.result = domain
q.closeLocked() q.closeLocked()
q.lock.Unlock() q.lock.Unlock()

View File

@@ -12,7 +12,7 @@ import (
) )
type fakeSender struct { type fakeSender struct {
resultCh chan *constant.Metadata constant.PacketSender
} }
var _ constant.PacketSender = (*fakeSender)(nil) var _ constant.PacketSender = (*fakeSender)(nil)
@@ -22,18 +22,7 @@ func (e *fakeSender) Send(packet constant.PacketAdapter) {
packet.Drop() packet.Drop()
} }
func (e *fakeSender) Process(constant.PacketConn, constant.WriteBackProxy) { func (e *fakeSender) DoSniff(metadata *constant.Metadata) error { return nil }
panic("not implemented")
}
func (e *fakeSender) ResolveUDP(metadata *constant.Metadata) error {
e.resultCh <- metadata
return nil
}
func (e *fakeSender) Close() {
panic("not implemented")
}
type fakeUDPPacket struct { type fakeUDPPacket struct {
data []byte data []byte
@@ -78,23 +67,28 @@ func asPacket(data string) constant.PacketAdapter {
return pktAdp return pktAdp
} }
func testQuicSniffer(data []string, async bool) (string, error) { const fakeHost = "fake.host.com"
func testQuicSniffer(data []string, async bool) (string, string, error) {
q, err := NewQuicSniffer(SnifferConfig{}) q, err := NewQuicSniffer(SnifferConfig{})
if err != nil { if err != nil {
return "", err return "", "", err
} }
resultCh := make(chan *constant.Metadata, 1) resultCh := make(chan *constant.Metadata, 1)
emptySender := &fakeSender{resultCh: resultCh} emptySender := &fakeSender{}
sender := q.WrapperSender(emptySender, true) sender := q.WrapperSender(emptySender, func(metadata *constant.Metadata, host string) {
replaceDomain(metadata, host, true)
})
go func() { go func() {
meta := constant.Metadata{} meta := constant.Metadata{Host: fakeHost}
err = sender.ResolveUDP(&meta) err := sender.DoSniff(&meta)
if err != nil { if err != nil {
panic(err) panic(err)
} }
resultCh <- &meta
}() }()
for _, d := range data { for _, d := range data {
@@ -106,14 +100,15 @@ func testQuicSniffer(data []string, async bool) (string, error) {
} }
meta := <-resultCh meta := <-resultCh
return meta.SniffHost, nil return meta.SniffHost, meta.Host, nil
} }
func TestQuicHeaders(t *testing.T) { func TestQuicHeaders(t *testing.T) {
cases := []struct { cases := []struct {
input []string input []string
domain string domain string
invalid bool
}{ }{
//Normal domain quic sniff //Normal domain quic sniff
{ {
@@ -171,16 +166,31 @@ func TestQuicHeaders(t *testing.T) {
}, },
domain: "www.google.com", domain: "www.google.com",
}, },
// invalid packet
{
input: []string{"00000000000000000000"},
invalid: true,
},
} }
for _, test := range cases { for _, test := range cases {
data, err := testQuicSniffer(test.input, true) data, host, err := testQuicSniffer(test.input, true)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, test.domain, data) assert.Equal(t, test.domain, data)
if test.invalid {
assert.Equal(t, fakeHost, host)
} else {
assert.Equal(t, test.domain, host)
}
data, err = testQuicSniffer(test.input, false) data, host, err = testQuicSniffer(test.input, false)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, test.domain, data) assert.Equal(t, test.domain, data)
if test.invalid {
assert.Equal(t, fakeHost, host)
} else {
assert.Equal(t, test.domain, host)
}
} }
} }

View File

@@ -0,0 +1,70 @@
package tls
import (
"context"
"net"
"net/http"
"runtime/debug"
"time"
N "github.com/metacubex/mihomo/common/net"
"github.com/metacubex/mihomo/log"
"golang.org/x/net/http2"
)
func extractTlsHandshakeTimeoutFromServer(s *http.Server) time.Duration {
var ret time.Duration
for _, v := range [...]time.Duration{
s.ReadHeaderTimeout,
s.ReadTimeout,
s.WriteTimeout,
} {
if v <= 0 {
continue
}
if ret == 0 || v < ret {
ret = v
}
}
return ret
}
// NewListenerForHttps returns a net.Listener for (*http.Server).Serve()
// the "func (c *conn) serve(ctx context.Context)" in http\server.go
// only do tls handshake and check NegotiatedProtocol with std's *tls.Conn
// so we do the same logic to let http2 (not h2c) work fine
func NewListenerForHttps(l net.Listener, httpServer *http.Server, tlsConfig *Config) net.Listener {
http2Server := &http2.Server{}
_ = http2.ConfigureServer(httpServer, http2Server)
return N.NewHandleContextListener(context.Background(), l, func(ctx context.Context, conn net.Conn) (net.Conn, error) {
c := Server(conn, tlsConfig)
tlsTO := extractTlsHandshakeTimeoutFromServer(httpServer)
if tlsTO > 0 {
dl := time.Now().Add(tlsTO)
_ = conn.SetReadDeadline(dl)
_ = conn.SetWriteDeadline(dl)
}
err := c.HandshakeContext(ctx)
if err != nil {
return nil, err
}
// Restore Conn-level deadlines.
if tlsTO > 0 {
_ = conn.SetReadDeadline(time.Time{})
_ = conn.SetWriteDeadline(time.Time{})
}
if c.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
http2Server.ServeConn(c, &http2.ServeConnOpts{BaseConfig: httpServer})
return nil, net.ErrClosed
}
return c, nil
}, func(a any) {
stack := debug.Stack()
log.Errorln("https server panic: %s\n%s", a, stack)
})
}

View File

@@ -26,7 +26,6 @@ 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"
) )
@@ -35,9 +34,11 @@ const RealityMaxShortIDLen = 8
type RealityConfig struct { type RealityConfig struct {
PublicKey *ecdh.PublicKey PublicKey *ecdh.PublicKey
ShortID [RealityMaxShortIDLen]byte ShortID [RealityMaxShortIDLen]byte
SupportX25519MLKEM768 bool
} }
func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *tls.Config, realityConfig *RealityConfig) (net.Conn, error) { func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHelloID, tlsConfig *Config, realityConfig *RealityConfig) (net.Conn, error) {
for retry := 0; ; retry++ { for retry := 0; ; retry++ {
verifier := &realityVerifier{ verifier := &realityVerifier{
serverName: tlsConfig.ServerName, serverName: tlsConfig.ServerName,
@@ -48,39 +49,18 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello
SessionTicketsDisabled: true, SessionTicketsDisabled: true,
VerifyPeerCertificate: verifier.VerifyPeerCertificate, VerifyPeerCertificate: verifier.VerifyPeerCertificate,
} }
clientID := utls.ClientHelloID{
Client: fingerprint.Client, if !realityConfig.SupportX25519MLKEM768 && fingerprint == HelloChrome_Auto {
Version: fingerprint.Version, fingerprint = HelloChrome_120 // old reality server doesn't work with X25519MLKEM768
Seed: fingerprint.Seed,
} }
uConn := utls.UClient(conn, uConfig, clientID)
uConn := utls.UClient(conn, uConfig, fingerprint)
verifier.UConn = uConn verifier.UConn = uConn
err := uConn.BuildHandshakeState() err := uConn.BuildHandshakeState()
if err != nil { if err != nil {
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
@@ -144,7 +124,7 @@ func GetRealityConn(ctx context.Context, conn net.Conn, fingerprint UClientHello
log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher) log.Debugln("REALITY Authentication: %v, AEAD: %T", verifier.verified, aeadCipher)
if !verifier.verified { if !verifier.verified {
go realityClientFallback(uConn, uConfig.ServerName, clientID) go realityClientFallback(uConn, uConfig.ServerName, fingerprint)
return nil, errors.New("REALITY authentication failed") return nil, errors.New("REALITY authentication failed")
} }

View File

@@ -16,6 +16,7 @@ type Conn = utls.Conn
type UConn = utls.UConn type UConn = utls.UConn
type UClientHelloID = utls.ClientHelloID type UClientHelloID = utls.ClientHelloID
const VersionTLS12 = utls.VersionTLS12
const VersionTLS13 = utls.VersionTLS13 const VersionTLS13 = utls.VersionTLS13
func Client(c net.Conn, config *utls.Config) *Conn { func Client(c net.Conn, config *utls.Config) *Conn {
@@ -26,6 +27,14 @@ func UClient(c net.Conn, config *utls.Config, fingerprint UClientHelloID) *UConn
return utls.UClient(c, config, fingerprint) return utls.UClient(c, config, fingerprint)
} }
func Server(c net.Conn, config *utls.Config) *Conn {
return utls.Server(c, config)
}
func NewListener(inner net.Listener, config *Config) net.Listener {
return utls.NewListener(inner, config)
}
func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) { func GetFingerprint(clientFingerprint string) (UClientHelloID, bool) {
if len(clientFingerprint) == 0 { if len(clientFingerprint) == 0 {
clientFingerprint = globalFingerprint clientFingerprint = globalFingerprint
@@ -65,21 +74,26 @@ var randomFingerprint = once.OnceValue(func() UClientHelloID {
return fingerprint return fingerprint
}) })
var HelloChrome_Auto = utls.HelloChrome_Auto
var HelloChrome_120 = utls.HelloChrome_120 // special fingerprint for some old protocols doesn't work with HelloChrome_Auto
var fingerprints = map[string]UClientHelloID{ var fingerprints = map[string]UClientHelloID{
"chrome": utls.HelloChrome_Auto, "chrome": utls.HelloChrome_Auto,
"firefox": utls.HelloFirefox_Auto,
"safari": utls.HelloSafari_Auto,
"ios": utls.HelloIOS_Auto,
"android": utls.HelloAndroid_11_OkHttp,
"edge": utls.HelloEdge_Auto,
"360": utls.Hello360_Auto,
"qq": utls.HelloQQ_Auto,
"random": {},
// deprecated fingerprints should not be used
"chrome_psk": utls.HelloChrome_100_PSK, "chrome_psk": utls.HelloChrome_100_PSK,
"chrome_psk_shuffle": utls.HelloChrome_106_Shuffle, "chrome_psk_shuffle": utls.HelloChrome_106_Shuffle,
"chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf, "chrome_padding_psk_shuffle": utls.HelloChrome_114_Padding_PSK_Shuf,
"chrome_pq": utls.HelloChrome_115_PQ, "chrome_pq": utls.HelloChrome_115_PQ,
"chrome_pq_psk": utls.HelloChrome_115_PQ_PSK, "chrome_pq_psk": utls.HelloChrome_115_PQ_PSK,
"firefox": utls.HelloFirefox_Auto,
"safari": utls.HelloSafari_Auto,
"ios": utls.HelloIOS_Auto,
"android": utls.HelloAndroid_11_OkHttp,
"edge": utls.HelloEdge_Auto,
"360": utls.Hello360_Auto,
"qq": utls.HelloQQ_Auto,
"random": {},
"randomized": utls.HelloRandomized, "randomized": utls.HelloRandomized,
} }
@@ -93,7 +107,9 @@ func init() {
fingerprints["randomized"] = randomized fingerprints["randomized"] = randomized
} }
func UCertificates(it tls.Certificate) utls.Certificate { type Certificate = utls.Certificate
func UCertificate(it tls.Certificate) utls.Certificate {
return utls.Certificate{ return utls.Certificate{
Certificate: it.Certificate, Certificate: it.Certificate,
PrivateKey: it.PrivateKey, PrivateKey: it.PrivateKey,
@@ -106,13 +122,15 @@ func UCertificates(it tls.Certificate) utls.Certificate {
} }
} }
type EncryptedClientHelloKey = utls.EncryptedClientHelloKey
type Config = utls.Config type Config = utls.Config
func UConfig(config *tls.Config) *utls.Config { func UConfig(config *tls.Config) *utls.Config {
return &utls.Config{ return &utls.Config{
Rand: config.Rand, Rand: config.Rand,
Time: config.Time, Time: config.Time,
Certificates: utils.Map(config.Certificates, UCertificates), Certificates: utils.Map(config.Certificates, UCertificate),
VerifyPeerCertificate: config.VerifyPeerCertificate, VerifyPeerCertificate: config.VerifyPeerCertificate,
RootCAs: config.RootCAs, RootCAs: config.RootCAs,
NextProtos: config.NextProtos, NextProtos: config.NextProtos,

View File

@@ -3,6 +3,7 @@ package updater
import ( import (
"archive/tar" "archive/tar"
"archive/zip" "archive/zip"
"bytes"
"compress/gzip" "compress/gzip"
"fmt" "fmt"
"io" "io"
@@ -32,6 +33,17 @@ const (
typeTarGzip typeTarGzip
) )
func (t compressionType) String() string {
switch t {
case typeZip:
return "zip"
case typeTarGzip:
return "tar.gz"
default:
return "unknown"
}
}
var DefaultUiUpdater = &UIUpdater{} var DefaultUiUpdater = &UIUpdater{}
func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater { func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater {
@@ -99,48 +111,38 @@ func detectFileType(data []byte) compressionType {
} }
func (u *UIUpdater) downloadUI() error { func (u *UIUpdater) downloadUI() error {
err := u.prepareUIPath()
if err != nil {
return fmt.Errorf("prepare UI path failed: %w", err)
}
data, err := downloadForBytes(u.externalUIURL) data, err := downloadForBytes(u.externalUIURL)
if err != nil { if err != nil {
return fmt.Errorf("can't download file: %w", err) return fmt.Errorf("can't download file: %w", err)
} }
fileType := detectFileType(data) tmpDir := C.Path.Resolve("downloadUI.tmp")
if fileType == typeUnknown { defer os.RemoveAll(tmpDir)
return fmt.Errorf("unknown or unsupported file type")
os.RemoveAll(tmpDir) // cleanup tmp dir before extract
log.Debugln("extractedFolder: %s", tmpDir)
err = extract(data, tmpDir)
if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err)
} }
ext := ".zip" log.Debugln("cleanupFolder: %s", u.externalUIPath)
if fileType == typeTarGzip { err = cleanup(u.externalUIPath) // cleanup files in dir don't remove dir itself
ext = ".tgz"
}
saved := path.Join(C.Path.HomeDir(), "download"+ext)
log.Debugln("compression Type: %s", ext)
if err = saveFile(data, saved); err != nil {
return fmt.Errorf("can't save compressed file: %w", err)
}
defer os.Remove(saved)
err = cleanup(u.externalUIPath)
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
return fmt.Errorf("cleanup exist file error: %w", err) return fmt.Errorf("cleanup exist file error: %w", err)
} }
} }
extractedFolder, err := extract(saved, C.Path.HomeDir()) err = u.prepareUIPath()
if err != nil { if err != nil {
return fmt.Errorf("can't extract compressed file: %w", err) return fmt.Errorf("prepare UI path failed: %w", err)
} }
err = os.Rename(extractedFolder, u.externalUIPath) log.Debugln("moveFolder from %s to %s", tmpDir, u.externalUIPath)
err = moveDir(tmpDir, u.externalUIPath) // move files from tmp to target
if err != nil { if err != nil {
return fmt.Errorf("rename UI folder failed: %w", err) return fmt.Errorf("move UI folder failed: %w", err)
} }
return nil return nil
} }
@@ -155,228 +157,109 @@ func (u *UIUpdater) prepareUIPath() error {
return nil return nil
} }
func unzip(src, dest string) (string, error) { func unzip(data []byte, dest string) error {
r, err := zip.OpenReader(src) r, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil { if err != nil {
return "", err return err
} }
defer r.Close()
// check whether or not only exists singleRoot dir // check whether or not only exists singleRoot dir
rootDir := ""
isSingleRoot := true
rootItemCount := 0
for _, f := range r.File {
parts := strings.Split(strings.Trim(f.Name, "/"), "/")
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := strings.HasSuffix(f.Name, "/")
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
// build the dir of extraction
var extractedFolder string
if isSingleRoot && rootDir != "" {
// if the singleRoot, use it directly
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
// or put the files/dirs into new dir
baseName := filepath.Base(src)
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
extractedFolder = filepath.Join(dest, baseName)
for i := 1; ; i++ {
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
break
}
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
}
log.Debugln("extractedFolder: %s", extractedFolder)
}
for _, f := range r.File { for _, f := range r.File {
var fpath string fpath := filepath.Join(dest, f.Name)
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, f.Name)
} else {
fpath = filepath.Join(extractedFolder, f.Name)
}
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { if !inDest(fpath, dest) {
return "", fmt.Errorf("invalid file path: %s", fpath) return fmt.Errorf("invalid file path: %s", fpath)
} }
if f.FileInfo().IsDir() { info := f.FileInfo()
if info.IsDir() {
os.MkdirAll(fpath, os.ModePerm) os.MkdirAll(fpath, os.ModePerm)
continue continue
} }
if info.Mode()&os.ModeSymlink != 0 {
continue // disallow symlink
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err return err
} }
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil { if err != nil {
return "", err return err
} }
rc, err := f.Open() rc, err := f.Open()
if err != nil { if err != nil {
return "", err return err
} }
_, err = io.Copy(outFile, rc) _, err = io.Copy(outFile, rc)
outFile.Close() outFile.Close()
rc.Close() rc.Close()
if err != nil { if err != nil {
return "", err return err
} }
} }
return extractedFolder, nil return nil
} }
func untgz(src, dest string) (string, error) { func untgz(data []byte, dest string) error {
file, err := os.Open(src) gzr, err := gzip.NewReader(bytes.NewReader(data))
if err != nil { if err != nil {
return "", err return err
}
defer file.Close()
gzr, err := gzip.NewReader(file)
if err != nil {
return "", err
} }
defer gzr.Close() defer gzr.Close()
tr := tar.NewReader(gzr) tr := tar.NewReader(gzr)
rootDir := "" _ = gzr.Reset(bytes.NewReader(data))
isSingleRoot := true
rootItemCount := 0
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return "", err
}
parts := strings.Split(cleanTarPath(header.Name), string(os.PathSeparator))
if len(parts) == 0 {
continue
}
if len(parts) == 1 {
isDir := header.Typeflag == tar.TypeDir
if !isDir {
isSingleRoot = false
break
}
if rootDir == "" {
rootDir = parts[0]
}
rootItemCount++
}
}
if rootItemCount != 1 {
isSingleRoot = false
}
file.Seek(0, 0)
gzr, _ = gzip.NewReader(file)
tr = tar.NewReader(gzr) tr = tar.NewReader(gzr)
var extractedFolder string
if isSingleRoot && rootDir != "" {
log.Debugln("Match the singleRoot")
extractedFolder = filepath.Join(dest, rootDir)
log.Debugln("extractedFolder: %s", extractedFolder)
} else {
log.Debugln("Match the multiRoot")
baseName := filepath.Base(src)
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
baseName = strings.TrimSuffix(baseName, ".tar")
extractedFolder = filepath.Join(dest, baseName)
for i := 1; ; i++ {
if _, err := os.Stat(extractedFolder); os.IsNotExist(err) {
break
}
extractedFolder = filepath.Join(dest, fmt.Sprintf("%s_%d", baseName, i))
}
log.Debugln("extractedFolder: %s", extractedFolder)
}
for { for {
header, err := tr.Next() header, err := tr.Next()
if err == io.EOF { if err == io.EOF {
break break
} }
if err != nil { if err != nil {
return "", err return err
} }
var fpath string fpath := filepath.Join(dest, header.Name)
if isSingleRoot && rootDir != "" {
fpath = filepath.Join(dest, cleanTarPath(header.Name))
} else {
fpath = filepath.Join(extractedFolder, cleanTarPath(header.Name))
}
if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { if !inDest(fpath, dest) {
return "", fmt.Errorf("invalid file path: %s", fpath) return fmt.Errorf("invalid file path: %s", fpath)
} }
switch header.Typeflag { switch header.Typeflag {
case tar.TypeDir: case tar.TypeDir:
if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil { if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil {
return "", err return err
} }
case tar.TypeReg: case tar.TypeReg:
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return "", err return err
} }
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode)) outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil { if err != nil {
return "", err return err
} }
if _, err := io.Copy(outFile, tr); err != nil { if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close() outFile.Close()
return "", err return err
} }
outFile.Close() outFile.Close()
} }
} }
return extractedFolder, nil return nil
} }
func extract(src, dest string) (string, error) { func extract(data []byte, dest string) error {
srcLower := strings.ToLower(src) fileType := detectFileType(data)
switch { log.Debugln("compression Type: %s", fileType)
case strings.HasSuffix(srcLower, ".tar.gz") || switch fileType {
strings.HasSuffix(srcLower, ".tgz"): case typeZip:
return untgz(src, dest) return unzip(data, dest)
case strings.HasSuffix(srcLower, ".zip"): case typeTarGzip:
return unzip(src, dest) return untgz(data, dest)
default: default:
return "", fmt.Errorf("unsupported file format: %s", src) return fmt.Errorf("unknown or unsupported file type")
} }
} }
@@ -398,22 +281,49 @@ func cleanTarPath(path string) string {
} }
func cleanup(root string) error { func cleanup(root string) error {
if _, err := os.Stat(root); os.IsNotExist(err) { dirEntryList, err := os.ReadDir(root)
return nil if err != nil {
return err
} }
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
for _, dirEntry := range dirEntryList {
err = os.RemoveAll(filepath.Join(root, dirEntry.Name()))
if err != nil { if err != nil {
return err return err
} }
if info.IsDir() { }
if err := os.RemoveAll(path); err != nil { return nil
return err }
}
} else { func moveDir(src string, dst string) error {
if err := os.Remove(path); err != nil { dirEntryList, err := os.ReadDir(src)
return err if err != nil {
} return err
} }
return nil
}) if len(dirEntryList) == 1 && dirEntryList[0].IsDir() {
src = filepath.Join(src, dirEntryList[0].Name())
log.Debugln("match the singleRoot: %s", src)
dirEntryList, err = os.ReadDir(src)
if err != nil {
return err
}
}
for _, dirEntry := range dirEntryList {
err = os.Rename(filepath.Join(src, dirEntry.Name()), filepath.Join(dst, dirEntry.Name()))
if err != nil {
return err
}
}
return nil
}
func inDest(fpath, dest string) bool {
if rel, err := filepath.Rel(dest, fpath); err == nil {
if filepath.IsLocal(rel) {
return true
}
}
return false
} }

View File

@@ -7,6 +7,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"net/url" "net/url"
"path/filepath"
"strings" "strings"
"time" "time"
_ "unsafe" _ "unsafe"
@@ -174,6 +175,7 @@ type Profile struct {
type TLS struct { type TLS struct {
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
CustomTrustCert []string CustomTrustCert []string
} }
@@ -268,6 +270,7 @@ type RawTun struct {
AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"`
RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"`
@@ -360,6 +363,7 @@ type RawSniffingConfig struct {
type RawTLS struct { type RawTLS struct {
Certificate string `yaml:"certificate" json:"certificate"` Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"` PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key"`
CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"` CustomTrustCert []string `yaml:"custom-certifactes" json:"custom-certifactes"`
} }
@@ -755,7 +759,10 @@ func parseGeneral(cfg *RawConfig) (*General, error) {
func parseController(cfg *RawConfig) (*Controller, error) { func parseController(cfg *RawConfig) (*Controller, error) {
if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) { if path := cfg.ExternalUI; path != "" && !C.Path.IsSafePath(path) {
return nil, fmt.Errorf("path is not subpath of home directory: %s", path) return nil, C.Path.ErrNotSafePath(path)
}
if uiName := cfg.ExternalUIName; uiName != "" && !filepath.IsLocal(uiName) {
return nil, fmt.Errorf("external UI name is not local: %s", uiName)
} }
return &Controller{ return &Controller{
ExternalController: cfg.ExternalController, ExternalController: cfg.ExternalController,
@@ -814,6 +821,7 @@ func parseTLS(cfg *RawConfig) (*TLS, error) {
return &TLS{ return &TLS{
Certificate: cfg.TLS.Certificate, Certificate: cfg.TLS.Certificate,
PrivateKey: cfg.TLS.PrivateKey, PrivateKey: cfg.TLS.PrivateKey,
EchKey: cfg.TLS.EchKey,
CustomTrustCert: cfg.TLS.CustomTrustCert, CustomTrustCert: cfg.TLS.CustomTrustCert,
}, nil }, nil
} }
@@ -1161,10 +1169,19 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error())
} }
proxyName := u.Fragment var proxyName string
params := map[string]string{}
for _, s := range strings.Split(u.Fragment, "&") {
arr := strings.SplitN(s, "=", 2)
switch len(arr) {
case 1:
proxyName = arr[0]
case 2:
params[arr[0]] = arr[1]
}
}
var addr, dnsNetType string var addr, dnsNetType string
params := map[string]string{}
switch u.Scheme { switch u.Scheme {
case "udp": case "udp":
addr, err = hostWithDefaultPort(u.Host, "53") addr, err = hostWithDefaultPort(u.Host, "53")
@@ -1182,23 +1199,8 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns.
addr, err = hostWithDefaultPort(u.Host, "80") addr, err = hostWithDefaultPort(u.Host, "80")
} }
if err == nil { if err == nil {
proxyName = ""
clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User}
addr = clearURL.String() addr = clearURL.String()
if len(u.Fragment) != 0 {
for _, s := range strings.Split(u.Fragment, "&") {
arr := strings.Split(s, "=")
if len(arr) == 0 {
continue
} else if len(arr) == 1 {
proxyName = arr[0]
} else if len(arr) == 2 {
params[arr[0]] = arr[1]
} else {
params[arr[0]] = strings.Join(arr[1:], "=")
}
}
}
} }
case "quic": case "quic":
addr, err = hostWithDefaultPort(u.Host, "853") addr, err = hostWithDefaultPort(u.Host, "853")
@@ -1556,6 +1558,7 @@ func parseTun(rawTun RawTun, general *General) error {
AutoRedirect: rawTun.AutoRedirect, AutoRedirect: rawTun.AutoRedirect,
AutoRedirectInputMark: rawTun.AutoRedirectInputMark, AutoRedirectInputMark: rawTun.AutoRedirectInputMark,
AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark, AutoRedirectOutputMark: rawTun.AutoRedirectOutputMark,
LoopbackAddress: rawTun.LoopbackAddress,
StrictRoute: rawTun.StrictRoute, StrictRoute: rawTun.StrictRoute,
RouteAddress: rawTun.RouteAddress, RouteAddress: rawTun.RouteAddress,
RouteAddressSet: rawTun.RouteAddressSet, RouteAddressSet: rawTun.RouteAddressSet,

View File

@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"net" "net"
"net/netip" "net/netip"
"os"
"strconv"
"strings" "strings"
"github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/adapter/outboundgroup"
@@ -150,6 +152,9 @@ func proxyGroupsDagSort(groupsConfig []map[string]any) error {
} }
func verifyIP6() bool { func verifyIP6() bool {
if skip, _ := strconv.ParseBool(os.Getenv("SKIP_SYSTEM_IPV6_CHECK")); skip {
return true
}
if iAddrs, err := net.InterfaceAddrs(); err == nil { if iAddrs, err := net.InterfaceAddrs(); err == nil {
for _, addr := range iAddrs { for _, addr := range iAddrs {
if prefix, err := netip.ParsePrefix(addr.String()); err == nil { if prefix, err := netip.ParsePrefix(addr.String()); err == nil {

View File

@@ -92,8 +92,7 @@ type Conn interface {
type PacketConn interface { type PacketConn interface {
N.EnhancePacketConn N.EnhancePacketConn
Connection Connection
// Deprecate WriteWithMetadata because of remote resolve DNS cause TURN failed ResolveUDP(ctx context.Context, metadata *Metadata) error
// WriteWithMetadata(p []byte, metadata *Metadata) (n int, err error)
} }
type Dialer interface { type Dialer interface {
@@ -319,10 +318,15 @@ type PacketSender interface {
Send(PacketAdapter) Send(PacketAdapter)
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy // Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
Process(PacketConn, WriteBackProxy) Process(PacketConn, WriteBackProxy)
// ResolveUDP do a local resolve UDP dns blocking if metadata is not resolved
ResolveUDP(*Metadata) error
// Close stop the Process loop // Close stop the Process loop
Close() Close()
// DoSniff will blocking after sniffer work done
DoSniff(*Metadata) error
// AddMapping add a destination NAT record
AddMapping(originMetadata *Metadata, metadata *Metadata)
// RestoreReadFrom restore destination NAT for ReadFrom
// the implement must ensure returned netip.Add is valid (or just return input addr)
RestoreReadFrom(addr netip.Addr) netip.Addr
} }
type NatTable interface { type NatTable interface {

View File

@@ -1,7 +1,6 @@
package constant package constant
import ( import (
"encoding/json"
"errors" "errors"
"strings" "strings"
) )
@@ -22,44 +21,6 @@ const (
type DNSMode int type DNSMode int
// UnmarshalYAML unserialize EnhancedMode with yaml
func (e *DNSMode) UnmarshalYAML(unmarshal func(any) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
mode, exist := DNSModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalYAML serialize EnhancedMode with yaml
func (e DNSMode) MarshalYAML() (any, error) {
return e.String(), nil
}
// UnmarshalJSON unserialize EnhancedMode with json
func (e *DNSMode) UnmarshalJSON(data []byte) error {
var tp string
if err := json.Unmarshal(data, &tp); err != nil {
return err
}
mode, exist := DNSModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
// MarshalJSON serialize EnhancedMode with json
func (e DNSMode) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}
// UnmarshalText unserialize EnhancedMode // UnmarshalText unserialize EnhancedMode
func (e *DNSMode) UnmarshalText(data []byte) error { func (e *DNSMode) UnmarshalText(data []byte) error {
mode, exist := DNSModeMapping[strings.ToLower(string(data))] mode, exist := DNSModeMapping[strings.ToLower(string(data))]
@@ -157,40 +118,6 @@ func (e FilterMode) String() string {
} }
} }
func (e FilterMode) MarshalYAML() (interface{}, error) {
return e.String(), nil
}
func (e *FilterMode) UnmarshalYAML(unmarshal func(interface{}) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
mode, exist := FilterModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
func (e FilterMode) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}
func (e *FilterMode) UnmarshalJSON(data []byte) error {
var tp string
if err := json.Unmarshal(data, &tp); err != nil {
return err
}
mode, exist := FilterModeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid mode")
}
*e = mode
return nil
}
func (e FilterMode) MarshalText() ([]byte, error) { func (e FilterMode) MarshalText() ([]byte, error) {
return []byte(e.String()), nil return []byte(e.String()), nil
} }

View File

@@ -261,6 +261,11 @@ func (m *Metadata) Pure() *Metadata {
return m return m
} }
func (m *Metadata) Clone() *Metadata {
copyM := *m
return &copyM
}
func (m *Metadata) AddrPort() netip.AddrPort { func (m *Metadata) AddrPort() netip.AddrPort {
return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort) return netip.AddrPortFrom(m.DstIP.Unmap(), m.DstPort)
} }

View File

@@ -1,6 +1,7 @@
package constant package constant
import ( import (
"fmt"
"os" "os"
P "path" P "path"
"path/filepath" "path/filepath"
@@ -87,10 +88,8 @@ func (p *path) IsSafePath(path string) bool {
if p.allowUnsafePath || features.CMFA { if p.allowUnsafePath || features.CMFA {
return true return true
} }
homedir := p.HomeDir()
path = p.Resolve(path) path = p.Resolve(path)
safePaths := append([]string{homedir}, p.safePaths...) // add homedir to safePaths for _, safePath := range p.SafePaths() {
for _, safePath := range safePaths {
if rel, err := filepath.Rel(safePath, path); err == nil { if rel, err := filepath.Rel(safePath, path); err == nil {
if filepath.IsLocal(rel) { if filepath.IsLocal(rel) {
return true return true
@@ -100,6 +99,23 @@ func (p *path) IsSafePath(path string) bool {
return false return false
} }
func (p *path) SafePaths() []string {
return append([]string{p.homeDir}, p.safePaths...) // add homedir to safePaths
}
func (p *path) ErrNotSafePath(path string) error {
return ErrNotSafePath{Path: path, SafePaths: p.SafePaths()}
}
type ErrNotSafePath struct {
Path string
SafePaths []string
}
func (e ErrNotSafePath) Error() string {
return fmt.Sprintf("path is not subpath of home directory or SAFE_PATHS: %s \n allowed paths: %s", e.Path, e.SafePaths)
}
func (p *path) GetPathByHash(prefix, name string) string { func (p *path) GetPathByHash(prefix, name string) string {
hash := utils.MakeHash([]byte(name)) hash := utils.MakeHash([]byte(name))
filename := hash.String() filename := hash.String()

View File

@@ -91,9 +91,7 @@ type RuleProvider interface {
Provider Provider
Behavior() RuleBehavior Behavior() RuleBehavior
Count() int Count() int
Match(*constant.Metadata) bool Match(metadata *constant.Metadata, helper constant.RuleMatchHelper) bool
ShouldResolveIP() bool
ShouldFindProcess() bool
Strategy() any Strategy() any
} }

View File

@@ -111,14 +111,17 @@ func (rt RuleType) String() string {
type Rule interface { type Rule interface {
RuleType() RuleType RuleType() RuleType
Match(metadata *Metadata) (bool, string) Match(metadata *Metadata, helper RuleMatchHelper) (bool, string)
Adapter() string Adapter() string
Payload() string Payload() string
ShouldResolveIP() bool
ShouldFindProcess() bool
ProviderNames() []string ProviderNames() []string
} }
type RuleMatchHelper struct {
ResolveIP func()
FindProcess func()
}
type RuleGroup interface { type RuleGroup interface {
Rule Rule
GetRecodeSize() int GetRecodeSize() int

View File

@@ -10,8 +10,10 @@ type Sniffer interface {
SupportPort(port uint16) bool SupportPort(port uint16) bool
} }
type ReplaceDomain func(metadata *constant.Metadata, host string)
type MultiPacketSniffer interface { type MultiPacketSniffer interface {
WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender WrapperSender(packetSender constant.PacketSender, replaceDomain ReplaceDomain) constant.PacketSender
} }
const ( const (

View File

@@ -1,7 +1,6 @@
package constant package constant
import ( import (
"encoding/json"
"errors" "errors"
"strings" "strings"
) )
@@ -20,42 +19,6 @@ const (
type TUNStack int type TUNStack int
// UnmarshalYAML unserialize TUNStack with yaml
func (e *TUNStack) UnmarshalYAML(unmarshal func(any) error) error {
var tp string
if err := unmarshal(&tp); err != nil {
return err
}
mode, exist := StackTypeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid tun stack")
}
*e = mode
return nil
}
// MarshalYAML serialize TUNStack with yaml
func (e TUNStack) MarshalYAML() (any, error) {
return e.String(), nil
}
// UnmarshalJSON unserialize TUNStack with json
func (e *TUNStack) UnmarshalJSON(data []byte) error {
var tp string
json.Unmarshal(data, &tp)
mode, exist := StackTypeMapping[strings.ToLower(tp)]
if !exist {
return errors.New("invalid tun stack")
}
*e = mode
return nil
}
// MarshalJSON serialize TUNStack with json
func (e TUNStack) MarshalJSON() ([]byte, error) {
return json.Marshal(e.String())
}
// UnmarshalText unserialize TUNStack // UnmarshalText unserialize TUNStack
func (e *TUNStack) UnmarshalText(data []byte) error { func (e *TUNStack) UnmarshalText(data []byte) error {
mode, exist := StackTypeMapping[strings.ToLower(string(data))] mode, exist := StackTypeMapping[strings.ToLower(string(data))]

View File

@@ -6,8 +6,10 @@ import (
"fmt" "fmt"
"net" "net"
"strings" "strings"
"time"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
D "github.com/miekg/dns" D "github.com/miekg/dns"
@@ -77,7 +79,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
msg, _, err := c.Client.ExchangeWithConn(m, dConn) msg, _, err := c.Client.ExchangeWithConn(m, dConn)
// Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)! // Resolvers MUST resend queries over TCP if they receive a truncated UDP response (with TC=1 set)!
if msg != nil && msg.Truncated && c.Client.Net == "" { if msg != nil && msg.Truncated && network == "udp" {
tcpClient := *c.Client // copy a client tcpClient := *c.Client // copy a client
tcpClient.Net = "tcp" tcpClient.Net = "tcp"
network = "tcp" network = "tcp"
@@ -105,3 +107,24 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error)
} }
func (c *client) ResetConnection() {} func (c *client) ResetConnection() {}
func newClient(addr string, resolver *Resolver, netType string, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *client {
host, port, _ := net.SplitHostPort(addr)
c := &client{
Client: &D.Client{
Net: netType,
TLSConfig: &tls.Config{
ServerName: host,
},
UDPSize: 4096,
Timeout: 5 * time.Second,
},
port: port,
host: host,
dialer: newDNSDialer(resolver, proxyAdapter, proxyName),
}
if params["skip-cert-verify"] == "true" {
c.TLSConfig.InsecureSkipVerify = true
}
return c
}

View File

@@ -9,7 +9,6 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"net/netip"
"net/url" "net/url"
"runtime" "runtime"
"strconv" "strconv"
@@ -71,8 +70,6 @@ type dnsOverHTTPS struct {
dialer *dnsDialer dialer *dnsDialer
addr string addr string
skipCertVerify bool skipCertVerify bool
ecsPrefix netip.Prefix
ecsOverride bool
} }
// type check // type check
@@ -105,28 +102,6 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin
doh.skipCertVerify = true doh.skipCertVerify = true
} }
if ecs := params["ecs"]; ecs != "" {
prefix, err := netip.ParsePrefix(ecs)
if err != nil {
addr, err := netip.ParseAddr(ecs)
if err != nil {
log.Warnln("DOH [%s] config with invalid ecs: %s", doh.addr, ecs)
} else {
doh.ecsPrefix = netip.PrefixFrom(addr, addr.BitLen())
}
} else {
doh.ecsPrefix = prefix
}
}
if doh.ecsPrefix.IsValid() {
log.Debugln("DOH [%s] config with ecs: %s", doh.addr, doh.ecsPrefix)
}
if params["ecs-override"] == "true" {
doh.ecsOverride = true
}
runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close) runtime.SetFinalizer(doh, (*dnsOverHTTPS).Close)
return doh return doh
@@ -154,10 +129,6 @@ func (doh *dnsOverHTTPS) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.
} }
}() }()
if doh.ecsPrefix.IsValid() {
setEdns0Subnet(m, doh.ecsPrefix, doh.ecsOverride)
}
// Check if there was already an active client before sending the request. // Check if there was already an active client before sending the request.
// We'll only attempt to re-connect if there was one. // We'll only attempt to re-connect if there was one.
client, isCached, err := doh.getClient(ctx) client, isCached, err := doh.getClient(ctx)
@@ -552,8 +523,8 @@ func (doh *dnsOverHTTPS) createTransportH3(
Dial: func( Dial: func(
ctx context.Context, ctx context.Context,
// Ignore the address and always connect to the one that we got // Ignore the address and always connect to the one that we got
// from the bootstrapper. // from the bootstrapper.
_ string, _ string,
tlsCfg *tlsC.Config, tlsCfg *tlsC.Config,
cfg *quic.Config, cfg *quic.Config,

View File

@@ -61,15 +61,16 @@ type dnsOverQUIC struct {
bytesPool *sync.Pool bytesPool *sync.Pool
bytesPoolGuard sync.Mutex bytesPoolGuard sync.Mutex
addr string addr string
dialer *dnsDialer dialer *dnsDialer
skipCertVerify bool
} }
// type check // type check
var _ dnsClient = (*dnsOverQUIC)(nil) var _ dnsClient = (*dnsOverQUIC)(nil)
// newDoQ returns the DNS-over-QUIC Upstream. // newDoQ returns the DNS-over-QUIC Upstream.
func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyName string) (dnsClient, error) { func newDoQ(addr string, resolver *Resolver, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) *dnsOverQUIC {
doq := &dnsOverQUIC{ doq := &dnsOverQUIC{
addr: addr, addr: addr,
dialer: newDNSDialer(resolver, proxyAdapter, proxyName), dialer: newDNSDialer(resolver, proxyAdapter, proxyName),
@@ -79,8 +80,12 @@ func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyN
}, },
} }
if params["skip-cert-verify"] == "true" {
doq.skipCertVerify = true
}
runtime.SetFinalizer(doq, (*dnsOverQUIC).Close) runtime.SetFinalizer(doq, (*dnsOverQUIC).Close)
return doq, nil return doq
} }
// Address implements the Upstream interface for *dnsOverQUIC. // Address implements the Upstream interface for *dnsOverQUIC.
@@ -329,7 +334,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio
tlsConfig := ca.GetGlobalTLSConfig( tlsConfig := ca.GetGlobalTLSConfig(
&tls.Config{ &tls.Config{
ServerName: host, ServerName: host,
InsecureSkipVerify: false, InsecureSkipVerify: doq.skipCertVerify,
NextProtos: []string{ NextProtos: []string{
NextProtoDQ, NextProtoDQ,
}, },

View File

@@ -127,6 +127,28 @@ func (r *Resolver) shouldIPFallback(ip netip.Addr) bool {
return false return false
} }
func (r *Resolver) ResolveECH(ctx context.Context, host string) ([]byte, error) {
query := &D.Msg{}
query.SetQuestion(D.Fqdn(host), D.TypeHTTPS)
msg, err := r.ExchangeContext(ctx, query)
if err != nil {
return nil, err
}
for _, rr := range msg.Answer {
switch resource := rr.(type) {
case *D.HTTPS:
for _, value := range resource.Value {
if echConfig, ok := value.(*D.SVCBECHConfig); ok {
return echConfig.ECH, nil
}
}
}
}
return nil, errors.New("no ECH config found in DNS records")
}
// ExchangeContext a batch of dns request with context.Context, and it use cache // ExchangeContext a batch of dns request with context.Context, and it use cache
func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) { func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) == 0 { if len(m.Question) == 0 {
@@ -326,7 +348,8 @@ func (r *Resolver) ipExchange(ctx context.Context, m *D.Msg) (msg *D.Msg, err er
func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) { func (r *Resolver) lookupIP(ctx context.Context, host string, dnsType uint16) (ips []netip.Addr, err error) {
ip, err := netip.ParseAddr(host) ip, err := netip.ParseAddr(host)
if err == nil { if err == nil {
isIPv4 := ip.Is4() || ip.Is4In6() ip = ip.Unmap()
isIPv4 := ip.Is4()
if dnsType == D.TypeAAAA && !isIPv4 { if dnsType == D.TypeAAAA && !isIPv4 {
return []netip.Addr{ip}, nil return []netip.Addr{ip}, nil
} else if dnsType == D.TypeA && isIPv4 { } else if dnsType == D.TypeA && isIPv4 {

View File

@@ -2,10 +2,8 @@ package dns
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"net"
"net/netip" "net/netip"
"strings" "strings"
"time" "time"
@@ -92,46 +90,95 @@ func isIPRequest(q D.Question) bool {
func transform(servers []NameServer, resolver *Resolver) []dnsClient { func transform(servers []NameServer, resolver *Resolver) []dnsClient {
ret := make([]dnsClient, 0, len(servers)) ret := make([]dnsClient, 0, len(servers))
for _, s := range servers { for _, s := range servers {
var c dnsClient
switch s.Net { switch s.Net {
case "https": case "https":
ret = append(ret, newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName)) c = newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName)
continue
case "dhcp": case "dhcp":
ret = append(ret, newDHCPClient(s.Addr)) c = newDHCPClient(s.Addr)
continue
case "system": case "system":
ret = append(ret, newSystemClient()) c = newSystemClient()
continue
case "rcode": case "rcode":
ret = append(ret, newRCodeClient(s.Addr)) c = newRCodeClient(s.Addr)
continue
case "quic": case "quic":
if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter, s.ProxyName); err == nil { c = newDoQ(s.Addr, resolver, s.Params, s.ProxyAdapter, s.ProxyName)
ret = append(ret, doq) default:
} else { c = newClient(s.Addr, resolver, s.Net, s.Params, s.ProxyAdapter, s.ProxyName)
log.Fatalln("DoQ format error: %v", err)
}
continue
} }
host, port, _ := net.SplitHostPort(s.Addr) c = warpClientWithEdns0Subnet(c, s.Params)
ret = append(ret, &client{
Client: &D.Client{ if s.Params["disable-ipv4"] == "true" {
Net: s.Net, c = warpClientWithDisableType(c, D.TypeA)
TLSConfig: &tls.Config{ }
ServerName: host,
}, if s.Params["disable-ipv6"] == "true" {
UDPSize: 4096, c = warpClientWithDisableType(c, D.TypeAAAA)
Timeout: 5 * time.Second, }
},
port: port, ret = append(ret, c)
host: host,
dialer: newDNSDialer(resolver, s.ProxyAdapter, s.ProxyName),
})
} }
return ret return ret
} }
type clientWithDisableType struct {
dnsClient
qType uint16
}
func (c clientWithDisableType) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, err error) {
if len(m.Question) > 0 {
q := m.Question[0]
if q.Qtype == c.qType {
return handleMsgWithEmptyAnswer(m), nil
}
}
return c.dnsClient.ExchangeContext(ctx, m)
}
func warpClientWithDisableType(c dnsClient, qType uint16) dnsClient {
return clientWithDisableType{c, qType}
}
type clientWithEdns0Subnet struct {
dnsClient
ecsPrefix netip.Prefix
ecsOverride bool
}
func (c clientWithEdns0Subnet) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) {
m = m.Copy()
setEdns0Subnet(m, c.ecsPrefix, c.ecsOverride)
return c.dnsClient.ExchangeContext(ctx, m)
}
func warpClientWithEdns0Subnet(c dnsClient, params map[string]string) dnsClient {
var ecsPrefix netip.Prefix
var ecsOverride bool
if ecs := params["ecs"]; ecs != "" {
prefix, err := netip.ParsePrefix(ecs)
if err != nil {
addr, err := netip.ParseAddr(ecs)
if err != nil {
log.Warnln("DNS [%s] config with invalid ecs: %s", c.Address(), ecs)
} else {
ecsPrefix = netip.PrefixFrom(addr, addr.BitLen())
}
} else {
ecsPrefix = prefix
}
}
if ecsPrefix.IsValid() {
log.Debugln("DNS [%s] config with ecs: %s", c.Address(), ecsPrefix)
if params["ecs-override"] == "true" {
ecsOverride = true
}
return clientWithEdns0Subnet{c, ecsPrefix, ecsOverride}
}
return c
}
func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg { func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg {
msg := &D.Msg{} msg := &D.Msg{}
msg.Answer = []D.RR{} msg.Answer = []D.RR{}

View File

@@ -48,6 +48,13 @@ ipv6: true # 开启 IPv6 总开关,关闭阻断所有 IPv6 链接和屏蔽 DNS
tls: tls:
certificate: string # 证书 PEM 格式,或者 证书的路径 certificate: string # 证书 PEM 格式,或者 证书的路径
private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径 private-key: string # 证书对应的私钥 PEM 格式,或者私钥路径
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
custom-certifactes: custom-certifactes:
- | - |
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
@@ -427,6 +434,10 @@ proxies: # socks5
# 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取 # 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取
# 配置指纹将实现 SSL Pining 效果 # 配置指纹将实现 SSL Pining 效果
# fingerprint: xxxx # fingerprint: xxxx
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: true # skip-cert-verify: true
# host: bing.com # host: bing.com
# path: "/" # path: "/"
@@ -527,6 +538,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# servername: example.com # priority over wss host # servername: example.com # priority over wss host
# network: ws # network: ws
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# ws-opts: # ws-opts:
# path: /path # path: /path
# headers: # headers:
@@ -599,6 +614,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# fingerprint: xxxx # fingerprint: xxxx
# client-fingerprint: random # Available: "chrome","firefox","safari","random","none" # client-fingerprint: random # Available: "chrome","firefox","safari","random","none"
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
- name: "vless-vision" - name: "vless-vision"
type: vless type: vless
@@ -626,6 +645,7 @@ proxies: # socks5
reality-opts: reality-opts:
public-key: xxx public-key: xxx
short-id: xxx # optional short-id: xxx # optional
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
client-fingerprint: chrome # cannot be empty client-fingerprint: chrome # cannot be empty
- name: "vless-reality-grpc" - name: "vless-reality-grpc"
@@ -645,6 +665,7 @@ proxies: # socks5
reality-opts: reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
short-id: 10f897e26c4b9478 short-id: 10f897e26c4b9478
support-x25519mlkem768: false # 如果服务端支持可手动设置为true
- name: "vless-ws" - name: "vless-ws"
type: vless type: vless
@@ -683,6 +704,10 @@ proxies: # socks5
# enabled: false # enabled: false
# method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305
# password: "example" # password: "example"
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
- name: trojan-grpc - name: trojan-grpc
server: server server: server
@@ -740,6 +765,10 @@ proxies: # socks5
up: "30 Mbps" # 若不写单位,默认为 Mbps up: "30 Mbps" # 若不写单位,默认为 Mbps
down: "200 Mbps" # 若不写单位,默认为 Mbps down: "200 Mbps" # 若不写单位,默认为 Mbps
# sni: server.com # sni: server.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: false # skip-cert-verify: false
# recv-window-conn: 12582912 # recv-window-conn: 12582912
# recv-window: 52428800 # recv-window: 52428800
@@ -763,6 +792,10 @@ proxies: # socks5
# obfs: salamander # 默认为空,如果填写则开启 obfs目前仅支持 salamander # obfs: salamander # 默认为空,如果填写则开启 obfs目前仅支持 salamander
# obfs-password: yourpassword # obfs-password: yourpassword
# sni: server.com # sni: server.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# skip-cert-verify: false # skip-cert-verify: false
# fingerprint: xxxx # fingerprint: xxxx
# alpn: # alpn:
@@ -838,6 +871,10 @@ proxies: # socks5
# skip-cert-verify: true # skip-cert-verify: true
# max-open-streams: 20 # default 100, too many open streams may hurt performance # max-open-streams: 20 # default 100, too many open streams may hurt performance
# sni: example.com # sni: example.com
# ech-opts:
# enable: true # 必须手动开启
# # 如果config为空则通过dns解析不为空则通过该值指定格式为经过base64编码的ech参数dig +short TYPE65 tls-ech.dev
# config: AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA
# #
# meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效 # meta 和 sing-box 私有扩展,将 ss-uot 用于 udp 中继,开启此选项后 udp-relay-mode 将失效
# 警告,与原版 tuic 不兼容!!! # 警告,与原版 tuic 不兼容!!!
@@ -983,7 +1020,7 @@ proxy-providers:
type: http # http 的 path 可空置,默认储存路径为 homedir 的 proxies 文件夹,文件名为 url 的 md5 type: http # http 的 path 可空置,默认储存路径为 homedir 的 proxies 文件夹,文件名为 url 的 md5
url: "url" url: "url"
interval: 3600 interval: 3600
path: ./provider1.yaml # 默认只允许存储在 mihomo 的 Home Dir如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 path: ./provider1.yaml # 默认只允许存储在 mihomo 的 Home Dir如果想存储到其他位置,请通过设置 SAFE_PATHS 环境变量指定额外的安全路径。该环境变量的语法同本操作系统的PATH环境变量解析规则即Windows下以分号分割其他系统下以冒号分割
proxy: DIRECT proxy: DIRECT
# size-limit: 10240 # 限制下载文件最大为10kb默认为0即不限制文件大小 # size-limit: 10240 # 限制下载文件最大为10kb默认为0即不限制文件大小
header: header:
@@ -1040,7 +1077,7 @@ rule-providers:
rule1: rule1:
behavior: classical # domain ipcidr behavior: classical # domain ipcidr
interval: 259200 interval: 259200
path: /path/to/save/file.yaml # 默认只允许存储在 mihomo 的 Home Dir如果想存储到任意位置,添加环境变量 SKIP_SAFE_PATH_CHECK=1 path: /path/to/save/file.yaml # 默认只允许存储在 mihomo 的 Home Dir如果想存储到其他位置,请通过设置 SAFE_PATHS 环境变量指定额外的安全路径。该环境变量的语法同本操作系统的PATH环境变量解析规则即Windows下以分号分割其他系统下以冒号分割
type: http # http 的 path 可空置,默认储存路径为 homedir 的 rules 文件夹,文件名为 url 的 md5 type: http # http 的 path 可空置,默认储存路径为 homedir 的 rules 文件夹,文件名为 url 的 md5
url: "url" url: "url"
proxy: DIRECT proxy: DIRECT
@@ -1127,6 +1164,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: http-in-1 - name: http-in-1
type: http type: http
@@ -1140,6 +1184,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: mixed-in-1 - name: mixed-in-1
type: mixed # HTTP(S) 和 SOCKS 代理混合 type: mixed # HTTP(S) 和 SOCKS 代理混合
@@ -1154,6 +1205,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
- name: reidr-in-1 - name: reidr-in-1
type: redir type: redir
@@ -1203,6 +1261,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写 # 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
# reality-config: # reality-config:
# dest: test.com:443 # dest: test.com:443
@@ -1211,6 +1276,16 @@ listeners:
# - 0123456789abcdef # - 0123456789abcdef
# server-names: # server-names:
# - test.com # - test.com
# #下列两个 limit 为选填可对未通过验证的回落连接限速bytesPerSec 默认为 0 即不启用
# #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化
# limit-fallback-upload:
# after-bytes: 0 # 传输指定字节后开始限速
# bytes-per-sec: 0 # 基准速率(字节/秒)
# burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
# limit-fallback-download:
# after-bytes: 0 # 传输指定字节后开始限速
# bytes-per-sec: 0 # 基准速率(字节/秒)
# burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
- name: tuic-in-1 - name: tuic-in-1
type: tuic type: tuic
@@ -1225,6 +1300,13 @@ listeners:
# 00000000-0000-0000-0000-000000000001: PASSWORD_1 # 00000000-0000-0000-0000-000000000001: PASSWORD_1
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# congestion-controller: bbr # congestion-controller: bbr
# max-idle-time: 15000 # max-idle-time: 15000
# authentication-timeout: 1000 # authentication-timeout: 1000
@@ -1256,6 +1338,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写 # 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
reality-config: reality-config:
dest: test.com:443 dest: test.com:443
@@ -1264,6 +1353,16 @@ listeners:
- 0123456789abcdef - 0123456789abcdef
server-names: server-names:
- test.com - test.com
#下列两个 limit 为选填可对未通过验证的回落连接限速bytesPerSec 默认为 0 即不启用
#回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化
limit-fallback-upload:
after-bytes: 0 # 传输指定字节后开始限速
bytes-per-sec: 0 # 基准速率(字节/秒)
burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
limit-fallback-download:
after-bytes: 0 # 传输指定字节后开始限速
bytes-per-sec: 0 # 基准速率(字节/秒)
burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
### 注意对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ### ### 注意对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ###
- name: anytls-in-1 - name: anytls-in-1
@@ -1276,6 +1375,13 @@ listeners:
# "certificate" and "private-key" are required # "certificate" and "private-key" are required
certificate: ./server.crt certificate: ./server.crt
private-key: ./server.key private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme # padding-scheme: "" # https://github.com/anytls/anytls-go/blob/main/docs/protocol.md#cmdupdatepaddingscheme
- name: trojan-in-1 - name: trojan-in-1
@@ -1292,6 +1398,13 @@ listeners:
# 下面两项如果填写则开启 tls需要同时填写 # 下面两项如果填写则开启 tls需要同时填写
certificate: ./server.crt certificate: ./server.crt
private-key: ./server.key private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写 # 如果填写reality-config则开启reality注意不可与certificate和private-key同时填写
# reality-config: # reality-config:
# dest: test.com:443 # dest: test.com:443
@@ -1300,6 +1413,16 @@ listeners:
# - 0123456789abcdef # - 0123456789abcdef
# server-names: # server-names:
# - test.com # - test.com
# #下列两个 limit 为选填可对未通过验证的回落连接限速bytesPerSec 默认为 0 即不启用
# #回落限速是一种特征,不建议启用,如果您是面板/一键脚本开发者,务必让这些参数随机化
# limit-fallback-upload:
# after-bytes: 0 # 传输指定字节后开始限速
# bytes-per-sec: 0 # 基准速率(字节/秒)
# burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
# limit-fallback-download:
# after-bytes: 0 # 传输指定字节后开始限速
# bytes-per-sec: 0 # 基准速率(字节/秒)
# burst-bytes-per-sec: 0 # 突发速率(字节/秒),大于 bytesPerSec 时生效
# ss-option: # like trojan-go's `shadowsocks` config # ss-option: # like trojan-go's `shadowsocks` config
# enabled: false # enabled: false
# method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305 # method: aes-128-gcm # aes-128-gcm/aes-256-gcm/chacha20-ietf-poly1305
@@ -1317,6 +1440,13 @@ listeners:
00000000-0000-0000-0000-000000000001: PASSWORD_1 00000000-0000-0000-0000-000000000001: PASSWORD_1
# certificate: ./server.crt # certificate: ./server.crt
# private-key: ./server.key # private-key: ./server.key
# 如果填写则开启ech可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
## up 和 down 均不写或为 0 则使用 BBR 流控 ## up 和 down 均不写或为 0 则使用 BBR 流控
# up: "30 Mbps" # 若不写单位,默认为 Mbps # up: "30 Mbps" # 若不写单位,默认为 Mbps
# down: "200 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps

24
go.mod
View File

@@ -18,25 +18,25 @@ require (
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/netlink v1.7.2
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.20.5
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
github.com/metacubex/chacha v0.1.2 github.com/metacubex/chacha v0.1.5
github.com/metacubex/fswatch v0.1.1 github.com/metacubex/fswatch v0.1.1
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639
github.com/metacubex/randv2 v0.2.0 github.com/metacubex/randv2 v0.2.0
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29
github.com/metacubex/sing-mux v0.3.2 github.com/metacubex/sing-mux v0.3.2
github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f
github.com/metacubex/sing-shadowsocks v0.2.9 github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92
github.com/metacubex/sing-shadowsocks2 v0.2.3 github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2
github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8
github.com/metacubex/sing-vmess v0.2.1 github.com/metacubex/sing-vmess v0.2.2
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee github.com/metacubex/smux v0.0.0-20250503055512-501391591dee
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4
github.com/metacubex/utls v1.7.0-alpha.2 github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73
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 // lastest version compatible with golang1.20 github.com/miekg/dns v1.1.63 // lastest version compatible with golang1.20
github.com/mroth/weightedrand/v2 v2.1.0 github.com/mroth/weightedrand/v2 v2.1.0
@@ -72,7 +72,7 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect github.com/buger/jsonparser v1.1.1 // indirect
github.com/cloudflare/circl v1.3.7 // indirect github.com/cloudflare/circl v1.3.7 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 // indirect github.com/ebitengine/purego v0.8.3 // indirect
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 // indirect
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect

52
go.sum
View File

@@ -26,8 +26,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615 h1:W7mpP4uiOAbBOdDnRXT9EUdauFv7bz+ERT5rPIord00= github.com/ebitengine/purego v0.8.3 h1:K+0AjQp63JEZTEMZiwsI9g0+hAMNohwUOtY0RPGexmc=
github.com/ebitengine/purego v0.8.3-0.20250507171810-1638563e3615/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.8.3/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98= github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM= github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo= github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
@@ -97,12 +97,12 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI= github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY= github.com/metacubex/bart v0.20.5 h1:XkgLZ17QxfxkqKdGsojoM2Zu01mmHyyQSFzt2/calTM=
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI= github.com/metacubex/bart v0.20.5/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.2 h1:QulCq3eVm3TO6+4nVIWJtmSe7BT2GMrgVHuAoqRQnlc= github.com/metacubex/chacha v0.1.5 h1:fKWMb/5c7ZrY8Uoqi79PPFxl+qwR7X/q0OrsAubyX2M=
github.com/metacubex/chacha v0.1.2/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8= github.com/metacubex/chacha v0.1.5/go.mod h1:Djn9bPZxLTXbJFSeyo0/qzEzQI+gUSSzttuzZM75GH8=
github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU= github.com/metacubex/fswatch v0.1.1 h1:jqU7C/v+g0qc2RUFgmAOPoVvfl2BXXUXEumn6oQuxhU=
github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI= github.com/metacubex/fswatch v0.1.1/go.mod h1:czrTT7Zlbz7vWft8RQu9Qqh+JoX+Nnb+UabuyN1YsgI=
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI= github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
@@ -111,35 +111,39 @@ github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b h1:RUh4OdVPz/jDrM
github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU= github.com/metacubex/gvisor v0.0.0-20250324165734-5857f47bd43b/go.mod h1:8LpS0IJW1VmWzUm3ylb0e2SK5QDm5lO/2qwWLZgRpBU=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793 h1:1Qpuy+sU3DmyX9HwI+CrBT/oLNJngvBorR2RbajJcqo=
github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA= github.com/metacubex/nftables v0.0.0-20250503052935-30a69ab87793/go.mod h1:RjRNb4G52yAgfR+Oe/kp9G4PJJ97Fnj89eY1BFO3YyA=
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b h1:8oDU32eJ+RRhl1FodGgPfxQxtoBAiD9D40XG2XtU/SE= github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639 h1:L+1brQNzBhCCxWlicwfK1TlceemCRmrDE4HmcVHc29w=
github.com/metacubex/quic-go v0.51.1-0.20250511032541-4e34341cf18b/go.mod h1:9R1NOzCgTcWsdWvOMlmtMuF0uKzuOpsfvEf7U3I8zM0= github.com/metacubex/quic-go v0.52.1-0.20250522021943-aef454b9e639/go.mod h1:Kc6h++Q/zf3AxcUCevJhJwgrskJumv+pZdR8g/E/10k=
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/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.2/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7 h1:m4nSxvw46JEgxMzzmnXams+ebwabcry4Ydep/zNiesQ= github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29 h1:SD9q025FNTaepuFXFOKDhnGLVu6PQYChBvw2ZYPXeLo=
github.com/metacubex/sing v0.5.3-0.20250504031621-1f99e54c15b7/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w= github.com/metacubex/sing v0.5.4-0.20250605054047-54dc6097da29/go.mod h1:ypf0mjwlZm0sKdQSY+yQvmsbWa0hNPtkeqyRMGgoN+w=
github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw= github.com/metacubex/sing-mux v0.3.2 h1:nJv52pyRivHcaZJKk2JgxpaVvj1GAXG81scSa9N7ncw=
github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw= github.com/metacubex/sing-mux v0.3.2/go.mod h1:3rt1soewn0O6j89GCLmwAQFsq257u0jf2zQSPhTL3Bw=
github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2 h1:wfmYgtECbEYo1slMtyo+2kMqscYYDSjU/TVgS3018F4= github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f h1:mP3vIm+9hRFI0C0Vl3pE0NESF/L85FDbuB0tGgUii6I=
github.com/metacubex/sing-quic v0.0.0-20250511034158-b46e0e3e81b2/go.mod h1:P1kd57U6XXmXv9PbwWdznUGT0k9bKgFJXF0fEORbIlk= github.com/metacubex/sing-quic v0.0.0-20250523120938-f1a248e5ec7f/go.mod h1:JPTpf7fpnojsSuwRJExhSZSy63pVbp3VM39+zj+sAJM=
github.com/metacubex/sing-shadowsocks v0.2.9 h1:2e++13WNN7EGjGtvrGLUzW1xrCdQbW2gIFpgw5GEw00= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621021503-4f85ef9bf4b3 h1:dtiRj7WaCAXp4UhCkmaIiFF6v886qXiuqeIDN4Z//9E=
github.com/metacubex/sing-shadowsocks v0.2.9/go.mod h1:CJSEGO4FWQAWe+ZiLZxCweGdjRR60A61SIoVjdjQeBA= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621021503-4f85ef9bf4b3/go.mod h1:/squZ38pXrYjqtg8qn+joVvwbpGNYQNp8yxKsMVbCto=
github.com/metacubex/sing-shadowsocks2 v0.2.3 h1:v3rNS/5Ywh0NIZ6VU/NmdERQIN5RePzyxCFeQsU4Cx0= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92 h1:Y9ebcKya6ow7VHoESCN5+l4zZvg5eaL2IhI5LLCQxQA=
github.com/metacubex/sing-shadowsocks2 v0.2.3/go.mod h1:/WNy/Q8ahLCoPRriWuFZFD0Jy+JNp1MEQl28Zw6SaF8= github.com/metacubex/sing-shadowsocks v0.2.11-0.20250621023810-0e9ef9dd0c92/go.mod h1:/squZ38pXrYjqtg8qn+joVvwbpGNYQNp8yxKsMVbCto=
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621021638-dcd503063651 h1:vwLj0DDjPYy4AHEZvfRVf8ih52o6wpBnJxXxqa+ztmE=
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621021638-dcd503063651/go.mod h1:+ukTd0OPFglT3bnKAYTJWYPbuox6HYNXE235r5tHdUk=
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d h1:Ey3A1tA8lVkRbK1FDmwuWj/57Nr8JMdpoVqe45mFzJg=
github.com/metacubex/sing-shadowsocks2 v0.2.5-0.20250621023950-93d605a2143d/go.mod h1:+ukTd0OPFglT3bnKAYTJWYPbuox6HYNXE235r5tHdUk=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2 h1:gXU+MYPm7Wme3/OAY2FFzVq9d9GxPHOqu5AQfg/ddhI=
github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E= github.com/metacubex/sing-shadowtls v0.0.0-20250503063515-5d9f966d17a2/go.mod h1:mbfboaXauKJNIHJYxQRa+NJs4JU9NZfkA+I33dS2+9E=
github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6 h1:TAwL91XPa6x1QK55CRm+VTzPvLPUfEr/uFDnOZArqEU= github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8 h1:4zWKqxTx75TbfW2EmlQ3hxM6RTRg2PYOAVMCnU4I61I=
github.com/metacubex/sing-tun v0.4.6-0.20250503065609-efb9f0beb6f6/go.mod h1:HDaHDL6onAX2ZGbAGUXKp++PohRdNb7Nzt6zxzhox+U= github.com/metacubex/sing-tun v0.4.7-0.20250611091011-60774779fdd8/go.mod h1:2YywXPWW8Z97kTH7RffOeykKzU+l0aiKlglWV1PAS64=
github.com/metacubex/sing-vmess v0.2.1 h1:I6gM3VUjtvJ15D805EUbNH+SRBuqzJeFnuIbKYUsWZ0= github.com/metacubex/sing-vmess v0.2.2 h1:nG6GIKF1UOGmlzs+BIetdGHkFZ20YqFVIYp5Htqzp+4=
github.com/metacubex/sing-vmess v0.2.1/go.mod h1:DsODWItJtOMZNna8Qhheg8r3tUivrcO3vWgaTYKnfTo= github.com/metacubex/sing-vmess v0.2.2/go.mod h1:CVDNcdSLVYFgTHQlubr88d8CdqupAUDqLjROos+H9xk=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f h1:Sr/DYKYofKHKc4GF3qkRGNuj6XA6c0eqPgEDN+VAsYU=
github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80= github.com/metacubex/sing-wireguard v0.0.0-20250503063753-2dc62acc626f/go.mod h1:jpAkVLPnCpGSfNyVmj6Cq4YbuZsFepm/Dc+9BAOcR80=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo= github.com/metacubex/smux v0.0.0-20250503055512-501391591dee h1:lp6hJ+4wCLZu113awp7P6odM2okB5s60HUyF0FMqKmo=
github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE= github.com/metacubex/smux v0.0.0-20250503055512-501391591dee/go.mod h1:4bPD8HWx9jPJ9aE4uadgyN7D1/Wz3KmPy+vale8sKLE=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4 h1:j1VRTiC9JLR4nUbSikx9OGdu/3AgFDqgcLj4GoqyQkc=
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw= github.com/metacubex/tfo-go v0.0.0-20250516165257-e29c16ae41d4/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
github.com/metacubex/utls v1.7.0-alpha.2 h1:kLRg6zDV12R1uclL5qW9Tx4RD6ztGIIrTZWY5zrJXCg= github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73 h1:HWKsf92BqLYqugATFIJ3hYiEBZ7JF6AoqyvqF39afuI=
github.com/metacubex/utls v1.7.0-alpha.2/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU= github.com/metacubex/utls v1.7.4-0.20250610022031-808d767c8c73/go.mod h1:oknYT0qTOwE4hjPmZOEpzVdefnW7bAdGLvZcqmk4TLU=
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=

View File

@@ -113,12 +113,11 @@ func ApplyConfig(cfg *config.Config, force bool) {
tunnel.OnInnerLoading() tunnel.OnInnerLoading()
initInnerTcp() initInnerTcp()
loadProxyProvider(cfg.Providers) loadProvider(cfg.Providers)
updateProfile(cfg) updateProfile(cfg)
loadRuleProvider(cfg.RuleProviders) loadProvider(cfg.RuleProviders)
runtime.GC() runtime.GC()
tunnel.OnRunning() tunnel.OnRunning()
hcCompatibleProvider(cfg.Providers)
updateUpdater(cfg) updateUpdater(cfg)
resolver.ResetConnection() resolver.ResetConnection()
@@ -303,79 +302,40 @@ func updateRules(rules []C.Rule, subRules map[string][]C.Rule, ruleProviders map
tunnel.UpdateRules(rules, subRules, ruleProviders) tunnel.UpdateRules(rules, subRules, ruleProviders)
} }
func loadProvider(pv provider.Provider) { func loadProvider[P provider.Provider](providers map[string]P) {
if pv.VehicleType() == provider.Compatible { load := func(pv P) {
return name := pv.Name()
} else { if pv.VehicleType() == provider.Compatible {
log.Infoln("Start initial provider %s", (pv).Name()) log.Infoln("Start initial compatible provider %s", name)
} } else {
log.Infoln("Start initial provider %s", name)
if err := pv.Initial(); err != nil {
switch pv.Type() {
case provider.Proxy:
{
log.Errorln("initial proxy provider %s error: %v", (pv).Name(), err)
}
case provider.Rule:
{
log.Errorln("initial rule provider %s error: %v", (pv).Name(), err)
}
} }
}
}
func loadRuleProvider(ruleProviders map[string]provider.RuleProvider) { if err := pv.Initial(); err != nil {
wg := sync.WaitGroup{} switch pv.Type() {
ch := make(chan struct{}, concurrentCount) case provider.Proxy:
for _, ruleProvider := range ruleProviders { {
ruleProvider := ruleProvider log.Errorln("initial proxy provider %s error: %v", name, err)
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
loadProvider(ruleProvider)
}()
}
wg.Wait()
}
func loadProxyProvider(proxyProviders map[string]provider.ProxyProvider) {
// limit concurrent size
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
loadProvider(proxyProvider)
}()
}
wg.Wait()
}
func hcCompatibleProvider(proxyProviders map[string]provider.ProxyProvider) {
// limit concurrent size
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, proxyProvider := range proxyProviders {
proxyProvider := proxyProvider
if proxyProvider.VehicleType() == provider.Compatible {
log.Infoln("Start initial Compatible provider %s", proxyProvider.Name())
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
if err := proxyProvider.Initial(); err != nil {
log.Errorln("initial Compatible provider %s error: %v", proxyProvider.Name(), err)
} }
}() case provider.Rule:
{
log.Errorln("initial rule provider %s error: %v", name, err)
}
}
} }
} }
wg := sync.WaitGroup{}
ch := make(chan struct{}, concurrentCount)
for _, pv := range providers {
pv := pv
wg.Add(1)
ch <- struct{}{}
go func() {
defer func() { <-ch; wg.Done() }()
load(pv)
}()
}
wg.Wait() wg.Wait()
} }

View File

@@ -57,6 +57,7 @@ func applyRoute(cfg *config.Config) {
Secret: cfg.Controller.Secret, Secret: cfg.Controller.Secret,
Certificate: cfg.TLS.Certificate, Certificate: cfg.TLS.Certificate,
PrivateKey: cfg.TLS.PrivateKey, PrivateKey: cfg.TLS.PrivateKey,
EchKey: cfg.TLS.EchKey,
DohServer: cfg.Controller.ExternalDohServer, DohServer: cfg.Controller.ExternalDohServer,
IsDebug: cfg.General.LogLevel == log.DEBUG, IsDebug: cfg.General.LogLevel == log.DEBUG,
Cors: route.Cors{ Cors: route.Cors{

View File

@@ -75,6 +75,7 @@ type tunSchema struct {
AutoRedirect *bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` AutoRedirect *bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
AutoRedirectInputMark *uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` AutoRedirectInputMark *uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
AutoRedirectOutputMark *uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` AutoRedirectOutputMark *uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
LoopbackAddress *[]netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"` StrictRoute *bool `yaml:"strict-route" json:"strict-route,omitempty"`
RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` RouteAddress *[]netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
RouteAddressSet *[]string `yaml:"route-address-set" json:"route-address-set,omitempty"` RouteAddressSet *[]string `yaml:"route-address-set" json:"route-address-set,omitempty"`
@@ -174,6 +175,9 @@ func pointerOrDefaultTun(p *tunSchema, def LC.Tun) LC.Tun {
if p.AutoRedirectOutputMark != nil { if p.AutoRedirectOutputMark != nil {
def.AutoRedirectOutputMark = *p.AutoRedirectOutputMark def.AutoRedirectOutputMark = *p.AutoRedirectOutputMark
} }
if p.LoopbackAddress != nil {
def.LoopbackAddress = *p.LoopbackAddress
}
if p.StrictRoute != nil { if p.StrictRoute != nil {
def.StrictRoute = *p.StrictRoute def.StrictRoute = *p.StrictRoute
} }
@@ -371,12 +375,20 @@ func updateConfigs(w http.ResponseWriter, r *http.Request) {
return return
} }
} else { } else {
if req.Path == "" { if req.Path == "" { // default path unneeded any safe check
req.Path = C.Path.Config() req.Path = C.Path.Config()
} else if !filepath.IsAbs(req.Path) { } else {
render.Status(r, http.StatusBadRequest) if !filepath.IsAbs(req.Path) {
render.JSON(w, r, newError("path is not a absolute path")) render.Status(r, http.StatusBadRequest)
return render.JSON(w, r, newError("path is not a absolute path"))
return
}
if !C.Path.IsSafePath(req.Path) {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(C.Path.ErrNotSafePath(req.Path).Error()))
return
}
} }
cfg, err = executor.ParseWithPath(req.Path) cfg, err = executor.ParseWithPath(req.Path)

View File

@@ -3,7 +3,6 @@ package route
import ( import (
"bytes" "bytes"
"crypto/subtle" "crypto/subtle"
"crypto/tls"
"encoding/json" "encoding/json"
"net" "net"
"net/http" "net/http"
@@ -17,6 +16,8 @@ import (
"github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/inbound"
"github.com/metacubex/mihomo/common/utils" "github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel/statistic" "github.com/metacubex/mihomo/tunnel/statistic"
@@ -62,6 +63,7 @@ type Config struct {
Secret string Secret string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
DohServer string DohServer string
IsDebug bool IsDebug bool
Cors Cors Cors Cors
@@ -186,7 +188,7 @@ func startTLS(cfg *Config) {
// handle tlsAddr // handle tlsAddr
if len(cfg.TLSAddr) > 0 { if len(cfg.TLSAddr) > 0 {
c, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path) cert, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path)
if err != nil { if err != nil {
log.Errorln("External controller tls listen error: %s", err) log.Errorln("External controller tls listen error: %s", err)
return return
@@ -199,14 +201,22 @@ func startTLS(cfg *Config) {
} }
log.Infoln("RESTful API tls listening at: %s", l.Addr().String()) log.Infoln("RESTful API tls listening at: %s", l.Addr().String())
tlsConfig := &tlsC.Config{}
tlsConfig.NextProtos = []string{"h2", "http/1.1"}
tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if cfg.EchKey != "" {
err = ech.LoadECHKey(cfg.EchKey, tlsConfig, C.Path)
if err != nil {
log.Errorln("External controller tls serve error: %s", err)
return
}
}
server := &http.Server{ server := &http.Server{
Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors), Handler: router(cfg.IsDebug, cfg.Secret, cfg.DohServer, cfg.Cors),
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{c},
},
} }
tlsServer = server tlsServer = server
if err = server.ServeTLS(l, "", ""); err != nil { if err = server.Serve(tlsC.NewListenerForHttps(l, server, tlsConfig)); err != nil {
log.Errorln("External controller tls serve error: %s", err) log.Errorln("External controller tls serve error: %s", err)
} }
} }

View File

@@ -3,7 +3,6 @@ package anytls
import ( import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"crypto/tls"
"encoding/binary" "encoding/binary"
"errors" "errors"
"net" "net"
@@ -13,6 +12,8 @@ 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"
"github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/ca"
"github.com/metacubex/mihomo/component/ech"
tlsC "github.com/metacubex/mihomo/component/tls"
C "github.com/metacubex/mihomo/constant" C "github.com/metacubex/mihomo/constant"
LC "github.com/metacubex/mihomo/listener/config" LC "github.com/metacubex/mihomo/listener/config"
"github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/listener/sing"
@@ -28,7 +29,7 @@ type Listener struct {
closed bool closed bool
config LC.AnyTLSServer config LC.AnyTLSServer
listeners []net.Listener listeners []net.Listener
tlsConfig *tls.Config tlsConfig *tlsC.Config
userMap map[[32]byte]string userMap map[[32]byte]string
padding atomic.TypedValue[*padding.PaddingFactory] padding atomic.TypedValue[*padding.PaddingFactory]
} }
@@ -41,13 +42,20 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
} }
} }
tlsConfig := &tls.Config{} tlsConfig := &tlsC.Config{}
if config.Certificate != "" && config.PrivateKey != "" { if config.Certificate != "" && config.PrivateKey != "" {
cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tlsConfig.Certificates = []tls.Certificate{cert} tlsConfig.Certificates = []tlsC.Certificate{tlsC.UCertificate(cert)}
if config.EchKey != "" {
err = ech.LoadECHKey(config.EchKey, tlsConfig, C.Path)
if err != nil {
return nil, err
}
}
} }
sl = &Listener{ sl = &Listener{
@@ -87,7 +95,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition)
return nil, err return nil, err
} }
if len(tlsConfig.Certificates) > 0 { if len(tlsConfig.Certificates) > 0 {
l = tls.NewListener(l, tlsConfig) l = tlsC.NewListener(l, tlsConfig)
} else { } else {
return nil, errors.New("disallow using AnyTLS without certificates config") return nil, errors.New("disallow using AnyTLS without certificates config")
} }

View File

@@ -10,6 +10,7 @@ type AnyTLSServer struct {
Users map[string]string `yaml:"users" json:"users,omitempty"` Users map[string]string `yaml:"users" json:"users,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"` Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"` PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key"`
PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"` PaddingScheme string `yaml:"padding-scheme" json:"padding-scheme,omitempty"`
} }

View File

@@ -12,5 +12,6 @@ type AuthServer struct {
AuthStore auth.AuthStore AuthStore auth.AuthStore
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
} }

View File

@@ -14,6 +14,7 @@ type Hysteria2Server struct {
ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"` ObfsPassword string `yaml:"obfs-password" json:"obfs-password,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"` Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"` PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
ALPN []string `yaml:"alpn" json:"alpn,omitempty"` ALPN []string `yaml:"alpn" json:"alpn,omitempty"`
Up string `yaml:"up" json:"up,omitempty"` Up string `yaml:"up" json:"up,omitempty"`

View File

@@ -20,6 +20,7 @@ type TrojanServer struct {
GrpcServiceName string GrpcServiceName string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
MuxOption sing.MuxOption MuxOption sing.MuxOption
TrojanSSOption TrojanSSOption TrojanSSOption TrojanSSOption

View File

@@ -13,6 +13,7 @@ type TuicServer struct {
Users map[string]string `yaml:"users" json:"users,omitempty"` Users map[string]string `yaml:"users" json:"users,omitempty"`
Certificate string `yaml:"certificate" json:"certificate"` Certificate string `yaml:"certificate" json:"certificate"`
PrivateKey string `yaml:"private-key" json:"private-key"` PrivateKey string `yaml:"private-key" json:"private-key"`
EchKey string `yaml:"ech-key" json:"ech-key"`
CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"` CongestionController string `yaml:"congestion-controller" json:"congestion-controller,omitempty"`
MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"` MaxIdleTime int `yaml:"max-idle-time" json:"max-idle-time,omitempty"`
AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"` AuthenticationTimeout int `yaml:"authentication-timeout" json:"authentication-timeout,omitempty"`

View File

@@ -9,18 +9,6 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
func StringSliceToNetipPrefixSlice(ss []string) ([]netip.Prefix, error) {
lps := make([]netip.Prefix, 0, len(ss))
for _, s := range ss {
prefix, err := netip.ParsePrefix(s)
if err != nil {
return nil, err
}
lps = append(lps, prefix)
}
return lps, nil
}
type Tun struct { type Tun struct {
Enable bool `yaml:"enable" json:"enable"` Enable bool `yaml:"enable" json:"enable"`
Device string `yaml:"device" json:"device"` Device string `yaml:"device" json:"device"`
@@ -39,6 +27,7 @@ type Tun struct {
AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"` AutoRedirect bool `yaml:"auto-redirect" json:"auto-redirect,omitempty"`
AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"` AutoRedirectInputMark uint32 `yaml:"auto-redirect-input-mark" json:"auto-redirect-input-mark,omitempty"`
AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"` AutoRedirectOutputMark uint32 `yaml:"auto-redirect-output-mark" json:"auto-redirect-output-mark,omitempty"`
LoopbackAddress []netip.Addr `yaml:"loopback-address" json:"loopback-address,omitempty"`
StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"` StrictRoute bool `yaml:"strict-route" json:"strict-route,omitempty"`
RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"` RouteAddress []netip.Prefix `yaml:"route-address" json:"route-address,omitempty"`
RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"` RouteAddressSet []string `yaml:"route-address-set" json:"route-address-set,omitempty"`
@@ -142,6 +131,9 @@ func (t *Tun) Equal(other Tun) bool {
if t.AutoRedirectOutputMark != other.AutoRedirectOutputMark { if t.AutoRedirectOutputMark != other.AutoRedirectOutputMark {
return false return false
} }
if !slices.Equal(t.RouteAddress, other.RouteAddress) {
return false
}
if t.StrictRoute != other.StrictRoute { if t.StrictRoute != other.StrictRoute {
return false return false
} }

View File

@@ -21,6 +21,7 @@ type VlessServer struct {
GrpcServiceName string GrpcServiceName string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
} }

View File

@@ -21,6 +21,7 @@ type VmessServer struct {
GrpcServiceName string GrpcServiceName string
Certificate string Certificate string
PrivateKey string PrivateKey string
EchKey string
RealityConfig reality.Config RealityConfig reality.Config
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
} }

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