From 49d54cc293ae80f5278d1b9fba466225c6c635e1 Mon Sep 17 00:00:00 2001 From: "J.K.SAGE" Date: Tue, 7 Jan 2025 13:23:05 +0800 Subject: [PATCH 01/14] fix: remote conn statistic error (#1776) TCP handshake traffic should be counted as upload traffic for the remote connection --- tunnel/tunnel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index b1b4add5ee..a4486df797 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -547,7 +547,7 @@ func handleTCPConn(connCtx C.ConnContext) { } logMetadata(metadata, rule, remoteConn) - remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, 0, int64(peekLen), true) + remoteConn = statistic.NewTCPTracker(remoteConn, statistic.DefaultManager, metadata, rule, int64(peekLen), 0, true) defer func(remoteConn C.Conn) { _ = remoteConn.Close() }(remoteConn) From f4806b49b452a5d41885c896482273ef62032a89 Mon Sep 17 00:00:00 2001 From: enfein <83481737+enfein@users.noreply.github.com> Date: Tue, 7 Jan 2025 05:25:32 +0000 Subject: [PATCH 02/14] chore: update mieru version (#1762) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 39b39152d2..1db2d3b580 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 github.com/dlclark/regexp2 v1.11.4 - github.com/enfein/mieru/v3 v3.9.0 + github.com/enfein/mieru/v3 v3.10.0 github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/render v1.0.3 github.com/gobwas/ws v1.4.0 diff --git a/go.sum b/go.sum index 2d7cbd53e1..3e1055935b 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yA github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/enfein/mieru/v3 v3.9.0 h1:h8KIXKxwPg8jT0uFdABIi0864O10UMAAOkkWo4zKl04= -github.com/enfein/mieru/v3 v3.9.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o= +github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ= +github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= From 56c128880c894fa4320757d4d1893702272c006d Mon Sep 17 00:00:00 2001 From: Mossia <63834850+MuXia-0326@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:26:56 +0800 Subject: [PATCH 03/14] fix: empty proxy provider subscription info not omitted (#1759) --- adapter/provider/provider.go | 6 ++---- adapter/provider/subscription_info.go | 12 +++++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/adapter/provider/provider.go b/adapter/provider/provider.go index 8558c4347d..64d83b0840 100644 --- a/adapter/provider/provider.go +++ b/adapter/provider/provider.go @@ -137,7 +137,7 @@ func (pp *proxySetProvider) Initial() error { return err } if subscriptionInfo := cachefile.Cache().GetSubscriptionInfo(pp.Name()); subscriptionInfo != "" { - pp.subscriptionInfo.Update(subscriptionInfo) + pp.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) } pp.closeAllConnections() return nil @@ -165,14 +165,12 @@ func NewProxySetProvider(name string, interval time.Duration, parser resource.Pa go hc.process() } - si := new(SubscriptionInfo) pd := &proxySetProvider{ baseProvider: baseProvider{ name: name, proxies: []C.Proxy{}, healthCheck: hc, }, - subscriptionInfo: si, } fetcher := resource.NewFetcher[[]C.Proxy](name, interval, vehicle, parser, proxiesOnUpdate(pd)) @@ -181,7 +179,7 @@ func NewProxySetProvider(name string, interval time.Duration, parser resource.Pa httpVehicle.SetInRead(func(resp *http.Response) { if subscriptionInfo := resp.Header.Get("subscription-userinfo"); subscriptionInfo != "" { cachefile.Cache().SetSubscriptionInfo(name, subscriptionInfo) - si.Update(subscriptionInfo) + pd.subscriptionInfo = NewSubscriptionInfo(subscriptionInfo) } }) } diff --git a/adapter/provider/subscription_info.go b/adapter/provider/subscription_info.go index 194e398464..2ec8537d7d 100644 --- a/adapter/provider/subscription_info.go +++ b/adapter/provider/subscription_info.go @@ -15,8 +15,9 @@ type SubscriptionInfo struct { Expire int64 } -func (info *SubscriptionInfo) Update(userinfo string) { +func NewSubscriptionInfo(userinfo string) (si *SubscriptionInfo) { userinfo = strings.ReplaceAll(strings.ToLower(userinfo), " ", "") + si = new(SubscriptionInfo) for _, field := range strings.Split(userinfo, ";") { name, value, ok := strings.Cut(field, "=") @@ -32,15 +33,16 @@ func (info *SubscriptionInfo) Update(userinfo string) { switch name { case "upload": - info.Upload = intValue + si.Upload = intValue case "download": - info.Download = intValue + si.Download = intValue case "total": - info.Total = intValue + si.Total = intValue case "expire": - info.Expire = intValue + si.Expire = intValue } } + return si } func parseValue(value string) (int64, error) { From c7661d7765fb62acbc6571e76f2922083b0b2b04 Mon Sep 17 00:00:00 2001 From: lucidhz <38235555+lucidhz@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:25:54 +0800 Subject: [PATCH 04/14] fix: initialize error message with cipher (#1760) --- adapter/outbound/shadowsocks.go | 2 +- adapter/outbound/shadowsocksr.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/adapter/outbound/shadowsocks.go b/adapter/outbound/shadowsocks.go index f73d5302a4..94a7763d8f 100644 --- a/adapter/outbound/shadowsocks.go +++ b/adapter/outbound/shadowsocks.go @@ -236,7 +236,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) { Password: option.Password, }) if err != nil { - return nil, fmt.Errorf("ss %s initialize error: %w", addr, err) + return nil, fmt.Errorf("ss %s cipher: %s initialize error: %w", addr, option.Cipher, err) } var v2rayOption *v2rayObfs.Option diff --git a/adapter/outbound/shadowsocksr.go b/adapter/outbound/shadowsocksr.go index d0752e79dd..7012b30b67 100644 --- a/adapter/outbound/shadowsocksr.go +++ b/adapter/outbound/shadowsocksr.go @@ -141,7 +141,7 @@ func NewShadowSocksR(option ShadowSocksROption) (*ShadowSocksR, error) { password := option.Password coreCiph, err := core.PickCipher(cipher, nil, password) if err != nil { - return nil, fmt.Errorf("ssr %s initialize error: %w", addr, err) + return nil, fmt.Errorf("ssr %s cipher: %s initialize error: %w", addr, cipher, err) } var ( ivSize int From c99c71a9690f016d1b9cb7e600302dfa7a33afdb Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Thu, 16 Jan 2025 10:16:37 +0800 Subject: [PATCH 05/14] chore: listening tcp together for dns server (#1792) --- adapter/inbound/listen.go | 27 +++++++- common/sockopt/reuse_common.go | 30 +++++++++ .../sockopt/reuse_other.go | 8 +-- common/sockopt/reuse_unix.go | 22 +++++++ common/sockopt/reuse_windows.go | 9 +++ common/sockopt/reuseaddr_linux.go | 19 ------ common/sockopt/reuseaddr_other.go | 11 ---- .../dialer/{reuse_windows.go => reuse.go} | 6 +- component/dialer/reuse_unix.go | 20 ------ dns/server.go | 66 +++++++++++-------- listener/shadowsocks/udp.go | 5 +- listener/sing_hysteria2/server.go | 5 +- listener/sing_shadowsocks/server.go | 5 +- listener/socks/udp.go | 4 +- listener/tuic/server.go | 5 +- 15 files changed, 140 insertions(+), 102 deletions(-) create mode 100644 common/sockopt/reuse_common.go rename component/dialer/reuse_others.go => common/sockopt/reuse_other.go (55%) create mode 100644 common/sockopt/reuse_unix.go create mode 100644 common/sockopt/reuse_windows.go delete mode 100644 common/sockopt/reuseaddr_linux.go delete mode 100644 common/sockopt/reuseaddr_other.go rename component/dialer/{reuse_windows.go => reuse.go} (58%) delete mode 100644 component/dialer/reuse_unix.go diff --git a/adapter/inbound/listen.go b/adapter/inbound/listen.go index ad944006a2..b4c6bf6b6a 100644 --- a/adapter/inbound/listen.go +++ b/adapter/inbound/listen.go @@ -43,7 +43,7 @@ func MPTCP() bool { return getMultiPathTCP(&lc.ListenConfig) } -func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { +func preResolve(network, address string) (string, error) { switch network { // like net.Resolver.internetAddrList but filter domain to avoid call net.Resolver.lookupIPAddr case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6", "ip", "ip4", "ip6": if host, port, err := net.SplitHostPort(address); err == nil { @@ -59,11 +59,19 @@ func ListenContext(ctx context.Context, network, address string) (net.Listener, break default: if _, err := netip.ParseAddr(host); err != nil { // not ip - return nil, fmt.Errorf("invalid network address: %s", address) + return "", fmt.Errorf("invalid network address: %s", address) } } } } + return address, nil +} + +func ListenContext(ctx context.Context, network, address string) (net.Listener, error) { + address, err := preResolve(network, address) + if err != nil { + return nil, err + } mutex.RLock() defer mutex.RUnlock() @@ -74,6 +82,21 @@ func Listen(network, address string) (net.Listener, error) { return ListenContext(context.Background(), network, address) } +func ListenPacketContext(ctx context.Context, network, address string) (net.PacketConn, error) { + address, err := preResolve(network, address) + if err != nil { + return nil, err + } + + mutex.RLock() + defer mutex.RUnlock() + return lc.ListenPacket(ctx, network, address) +} + +func ListenPacket(network, address string) (net.PacketConn, error) { + return ListenPacketContext(context.Background(), network, address) +} + func init() { keepalive.SetDisableKeepAliveCallback.Register(func(b bool) { mutex.Lock() diff --git a/common/sockopt/reuse_common.go b/common/sockopt/reuse_common.go new file mode 100644 index 0000000000..907697c32c --- /dev/null +++ b/common/sockopt/reuse_common.go @@ -0,0 +1,30 @@ +package sockopt + +import ( + "net" + "syscall" +) + +func RawConnReuseaddr(rc syscall.RawConn) (err error) { + var innerErr error + err = rc.Control(func(fd uintptr) { + innerErr = reuseControl(fd) + }) + + if innerErr != nil { + err = innerErr + } + return +} + +func UDPReuseaddr(c net.PacketConn) error { + if c, ok := c.(syscall.Conn); ok { + rc, err := c.SyscallConn() + if err != nil { + return err + } + + return RawConnReuseaddr(rc) + } + return nil +} diff --git a/component/dialer/reuse_others.go b/common/sockopt/reuse_other.go similarity index 55% rename from component/dialer/reuse_others.go rename to common/sockopt/reuse_other.go index db67a095d6..39d901a050 100644 --- a/component/dialer/reuse_others.go +++ b/common/sockopt/reuse_other.go @@ -1,9 +1,5 @@ //go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows -package dialer +package sockopt -import ( - "net" -) - -func addrReuseToListenConfig(*net.ListenConfig) {} +func reuseControl(fd uintptr) error { return nil } diff --git a/common/sockopt/reuse_unix.go b/common/sockopt/reuse_unix.go new file mode 100644 index 0000000000..e000b9e89c --- /dev/null +++ b/common/sockopt/reuse_unix.go @@ -0,0 +1,22 @@ +//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris + +package sockopt + +import ( + "golang.org/x/sys/unix" +) + +func reuseControl(fd uintptr) error { + e1 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) + e2 := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) + + if e1 != nil { + return e1 + } + + if e2 != nil { + return e2 + } + + return nil +} diff --git a/common/sockopt/reuse_windows.go b/common/sockopt/reuse_windows.go new file mode 100644 index 0000000000..79305d02b0 --- /dev/null +++ b/common/sockopt/reuse_windows.go @@ -0,0 +1,9 @@ +package sockopt + +import ( + "golang.org/x/sys/windows" +) + +func reuseControl(fd uintptr) error { + return windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) +} diff --git a/common/sockopt/reuseaddr_linux.go b/common/sockopt/reuseaddr_linux.go deleted file mode 100644 index a1d19bfdab..0000000000 --- a/common/sockopt/reuseaddr_linux.go +++ /dev/null @@ -1,19 +0,0 @@ -package sockopt - -import ( - "net" - "syscall" -) - -func UDPReuseaddr(c *net.UDPConn) (err error) { - rc, err := c.SyscallConn() - if err != nil { - return - } - - rc.Control(func(fd uintptr) { - err = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) - }) - - return -} diff --git a/common/sockopt/reuseaddr_other.go b/common/sockopt/reuseaddr_other.go deleted file mode 100644 index 04fc8ed731..0000000000 --- a/common/sockopt/reuseaddr_other.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !linux - -package sockopt - -import ( - "net" -) - -func UDPReuseaddr(c *net.UDPConn) (err error) { - return -} diff --git a/component/dialer/reuse_windows.go b/component/dialer/reuse.go similarity index 58% rename from component/dialer/reuse_windows.go rename to component/dialer/reuse.go index b8d0d809c2..4a46e572fd 100644 --- a/component/dialer/reuse_windows.go +++ b/component/dialer/reuse.go @@ -5,13 +5,11 @@ import ( "net" "syscall" - "golang.org/x/sys/windows" + "github.com/metacubex/mihomo/common/sockopt" ) func addrReuseToListenConfig(lc *net.ListenConfig) { addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { - return c.Control(func(fd uintptr) { - windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) - }) + return sockopt.RawConnReuseaddr(c) }) } diff --git a/component/dialer/reuse_unix.go b/component/dialer/reuse_unix.go deleted file mode 100644 index a0cf738817..0000000000 --- a/component/dialer/reuse_unix.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris - -package dialer - -import ( - "context" - "net" - "syscall" - - "golang.org/x/sys/unix" -) - -func addrReuseToListenConfig(lc *net.ListenConfig) { - addControlToListenConfig(lc, func(ctx context.Context, network, address string, c syscall.RawConn) error { - return c.Control(func(fd uintptr) { - unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1) - unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1) - }) - }) -} diff --git a/dns/server.go b/dns/server.go index 1cf58d4d8a..caf1c2891a 100644 --- a/dns/server.go +++ b/dns/server.go @@ -5,6 +5,7 @@ import ( "errors" "net" + "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/sockopt" "github.com/metacubex/mihomo/context" "github.com/metacubex/mihomo/log" @@ -20,8 +21,9 @@ var ( ) type Server struct { - *D.Server - handler handler + handler handler + tcpServer *D.Server + udpServer *D.Server } // ServeDNS implement D.Handler ServeDNS @@ -55,12 +57,19 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { return } - if server.Server != nil { - server.Shutdown() - server = &Server{} - address = "" + if server.tcpServer != nil { + _ = server.tcpServer.Shutdown() + server.tcpServer = nil } + if server.udpServer != nil { + _ = server.udpServer.Shutdown() + server.udpServer = nil + } + + server.handler = nil + address = "" + if addr == "" { return } @@ -77,31 +86,36 @@ func ReCreateServer(addr string, resolver *Resolver, mapper *ResolverEnhancer) { return } - udpAddr, err := net.ResolveUDPAddr("udp", addr) - if err != nil { - return - } - - p, err := net.ListenUDP("udp", udpAddr) - if err != nil { - return - } - - err = sockopt.UDPReuseaddr(p) - if err != nil { - log.Warnln("Failed to Reuse UDP Address: %s", err) - - err = nil - } - address = addr handler := NewHandler(resolver, mapper) server = &Server{handler: handler} - server.Server = &D.Server{Addr: addr, PacketConn: p, Handler: server} go func() { - server.ActivateAndServe() + p, err := inbound.ListenPacket("udp", addr) + if err != nil { + log.Errorln("Start DNS server(UDP) error: %s", err.Error()) + return + } + + if err := sockopt.UDPReuseaddr(p); err != nil { + log.Warnln("Failed to Reuse UDP Address: %s", err) + } + + log.Infoln("DNS server(UDP) listening at: %s", p.LocalAddr().String()) + server.udpServer = &D.Server{Addr: addr, PacketConn: p, Handler: server} + _ = server.udpServer.ActivateAndServe() + }() + + go func() { + l, err := inbound.Listen("tcp", addr) + if err != nil { + log.Errorln("Start DNS server(TCP) error: %s", err.Error()) + return + } + + log.Infoln("DNS server(TCP) listening at: %s", l.Addr().String()) + server.tcpServer = &D.Server{Addr: addr, Listener: l, Handler: server} + _ = server.tcpServer.ActivateAndServe() }() - log.Infoln("DNS server listening at: %s", p.LocalAddr().String()) } diff --git a/listener/shadowsocks/udp.go b/listener/shadowsocks/udp.go index 77932ed1b1..bdb6739ed7 100644 --- a/listener/shadowsocks/udp.go +++ b/listener/shadowsocks/udp.go @@ -18,13 +18,12 @@ type UDPListener struct { } func NewUDP(addr string, pickCipher core.Cipher, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPListener, error) { - l, err := net.ListenPacket("udp", addr) + l, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } - err = sockopt.UDPReuseaddr(l.(*net.UDPConn)) - if err != nil { + if err := sockopt.UDPReuseaddr(l); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go index 77ad8efdeb..524aa9a137 100644 --- a/listener/sing_hysteria2/server.go +++ b/listener/sing_hysteria2/server.go @@ -140,13 +140,12 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi _service := *service service := &_service // make a copy - ul, err := net.ListenPacket("udp", addr) + ul, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } - err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) - if err != nil { + if err := sockopt.UDPReuseaddr(ul); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } diff --git a/listener/sing_shadowsocks/server.go b/listener/sing_shadowsocks/server.go index 5f2a4292e3..7ce1d4e916 100644 --- a/listener/sing_shadowsocks/server.go +++ b/listener/sing_shadowsocks/server.go @@ -82,13 +82,12 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi if config.Udp { //UDP - ul, err := net.ListenPacket("udp", addr) + ul, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } - err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) - if err != nil { + if err := sockopt.UDPReuseaddr(ul); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } diff --git a/listener/socks/udp.go b/listener/socks/udp.go index ef31b20e97..cb4fa37294 100644 --- a/listener/socks/udp.go +++ b/listener/socks/udp.go @@ -40,12 +40,12 @@ func NewUDP(addr string, tunnel C.Tunnel, additions ...inbound.Addition) (*UDPLi inbound.WithSpecialRules(""), } } - l, err := net.ListenPacket("udp", addr) + l, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } - if err := sockopt.UDPReuseaddr(l.(*net.UDPConn)); err != nil { + if err := sockopt.UDPReuseaddr(l); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } diff --git a/listener/tuic/server.go b/listener/tuic/server.go index 5d807cbc21..837c1d10f9 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -152,13 +152,12 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( for _, addr := range strings.Split(config.Listen, ",") { addr := addr - ul, err := net.ListenPacket("udp", addr) + ul, err := inbound.ListenPacket("udp", addr) if err != nil { return nil, err } - err = sockopt.UDPReuseaddr(ul.(*net.UDPConn)) - if err != nil { + if err := sockopt.UDPReuseaddr(ul); err != nil { log.Warnln("Failed to Reuse UDP Address: %s", err) } From 192d769f7587f333a3a0798e2f121be441a95c16 Mon Sep 17 00:00:00 2001 From: tnextday Date: Thu, 16 Jan 2025 10:17:32 +0800 Subject: [PATCH 06/14] chore: ensure forced domains are always sniffed (#1793) When a domain matches forceDomain: - SkipList is not checked - Failed attempts are not cached - Sniffing is attempted every time This ensures forced domains are always sniffed regardless of previous failures. --- component/sniffer/dispatcher.go | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index ada4317686..5d457cf165 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -48,6 +48,10 @@ func (sd *Dispatcher) shouldOverride(metadata *C.Metadata) bool { if metadata.DNSMode == C.DNSMapping && sd.forceDnsMapping { return true } + return sd.forceSniff(metadata) +} + +func (sd *Dispatcher) forceSniff(metadata *C.Metadata) bool { for _, matcher := range sd.forceDomain { if matcher.MatchDomain(metadata.Host) { return true @@ -98,16 +102,21 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool if !inWhitelist { return false } + forceSniffer := sd.forceSniff(metadata) dst := metadata.AddrPort() - if count, ok := sd.skipList.Get(dst); ok && count > 5 { - log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) - return false + if !forceSniffer { + if count, ok := sd.skipList.Get(dst); ok && count > 5 { + log.Debugln("[Sniffer] Skip sniffing[%s] due to multiple failures", dst) + return false + } } host, err := sd.sniffDomain(conn, metadata) if err != nil { - sd.cacheSniffFailed(metadata) + if !forceSniffer { + sd.cacheSniffFailed(metadata) + } log.Debugln("[Sniffer] All sniffing sniff failed with from [%s:%d] to [%s:%d]", metadata.SrcIP, metadata.SrcPort, metadata.String(), metadata.DstPort) return false } From fc233184fd048d0ced500d146235d11ebf6c6a4f Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Sun, 19 Jan 2025 09:56:16 +0800 Subject: [PATCH 07/14] feat: add receive window config for hy2 https://github.com/MetaCubeX/mihomo/issues/1796 --- adapter/outbound/hysteria2.go | 15 +++++++++++++++ docs/config.yaml | 5 +++++ go.mod | 2 +- go.sum | 4 ++-- listener/config/hysteria2.go | 6 ++++++ listener/inbound/hysteria2.go | 11 +++++++++++ listener/sing_hysteria2/server.go | 9 +++++++++ 7 files changed, 49 insertions(+), 3 deletions(-) diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index e7a9adaee4..fa0cebdbf7 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -21,6 +21,7 @@ import ( "github.com/metacubex/sing-quic/hysteria2" + "github.com/metacubex/quic-go" "github.com/metacubex/randv2" M "github.com/sagernet/sing/common/metadata" ) @@ -62,6 +63,12 @@ type Hysteria2Option struct { CustomCAString string `proxy:"ca-str,omitempty"` CWND int `proxy:"cwnd,omitempty"` UdpMTU int `proxy:"udp-mtu,omitempty"` + + // quic-go special config + InitialStreamReceiveWindow uint64 `proxy:"initial-stream-receive-window,omitempty"` + MaxStreamReceiveWindow uint64 `proxy:"max-stream-receive-window,omitempty"` + InitialConnectionReceiveWindow uint64 `proxy:"initial-connection-receive-window,omitempty"` + MaxConnectionReceiveWindow uint64 `proxy:"max-connection-receive-window,omitempty"` } func (h *Hysteria2) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { @@ -145,6 +152,13 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { option.UdpMTU = 1200 - 3 } + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: option.InitialStreamReceiveWindow, + MaxStreamReceiveWindow: option.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: option.InitialConnectionReceiveWindow, + MaxConnectionReceiveWindow: option.MaxConnectionReceiveWindow, + } + singDialer := proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()) clientOptions := hysteria2.ClientOptions{ @@ -156,6 +170,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { SalamanderPassword: salamanderPassword, Password: option.Password, TLSConfig: tlsConfig, + QUICConfig: quicConfig, UDPDisabled: false, CWND: option.CWND, UdpMTU: option.UdpMTU, diff --git a/docs/config.yaml b/docs/config.yaml index 6dfeb5866e..b92246ae2e 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -747,6 +747,11 @@ proxies: # socks5 # - h3 # ca: "./my.ca" # ca-str: "xyz" + ###quic-go特殊配置项,不要随意修改除非你知道你在干什么### + # initial-stream-receive-window: 8388608 + # max-stream-receive-window: 8388608 + # initial-connection-receive-window: 20971520 + # max-connection-receive-window: 20971520 # wireguard - name: "wg" diff --git a/go.mod b/go.mod index 1db2d3b580..3585bc797e 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/quic-go v0.48.3-0.20241126053724-b69fea3888da github.com/metacubex/randv2 v0.2.0 - github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 + github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 github.com/metacubex/sing-shadowsocks v0.2.8 github.com/metacubex/sing-shadowsocks2 v0.2.2 github.com/metacubex/sing-tun v0.4.5 diff --git a/go.sum b/go.sum index 3e1055935b..33ad535553 100644 --- a/go.sum +++ b/go.sum @@ -114,8 +114,8 @@ github.com/metacubex/randv2 v0.2.0 h1:uP38uBvV2SxYfLj53kuvAjbND4RUDfFJjwr4UigMiL github.com/metacubex/randv2 v0.2.0/go.mod h1:kFi2SzrQ5WuneuoLLCMkABtiBu6VRrMrWFqSPyj2cxY= github.com/metacubex/sing v0.0.0-20241121030428-33b6ebc52000 h1:gUbMXcQXhXGj0vCpCVFTUyIH7TMpD1dpTcNv/MCS+ok= github.com/metacubex/sing v0.0.0-20241121030428-33b6ebc52000/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4 h1:HobpULaPK6OoxrHMmgcwLkwwIduXVmwdcznwUfH1GQM= -github.com/metacubex/sing-quic v0.0.0-20240827003841-cd97758ed8b4/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= +github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 h1:UkPoRAnoBQMn7IK5qpoIV3OejU15q+rqel3NrbSCFKA= +github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8= github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJRafgwBHO5B4= github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0= github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo= diff --git a/listener/config/hysteria2.go b/listener/config/hysteria2.go index 3c9df5089e..e26fbaa8aa 100644 --- a/listener/config/hysteria2.go +++ b/listener/config/hysteria2.go @@ -23,6 +23,12 @@ type Hysteria2Server struct { CWND int `yaml:"cwnd" json:"cwnd,omitempty"` UdpMTU int `yaml:"udp-mtu" json:"udp-mtu,omitempty"` MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` + + // quic-go special config + InitialStreamReceiveWindow uint64 `yaml:"initial-stream-receive-window" json:"initial-stream-receive-window,omitempty"` + MaxStreamReceiveWindow uint64 `yaml:"max-stream-receive-window" json:"max-stream-receive-window,omitempty"` + InitialConnectionReceiveWindow uint64 `yaml:"initial-connection-receive-window" json:"initial-connection-receive-window,omitempty"` + MaxConnectionReceiveWindow uint64 `yaml:"max-connection-receive-window" json:"max-connection-receive-window,omitempty"` } func (h Hysteria2Server) String() string { diff --git a/listener/inbound/hysteria2.go b/listener/inbound/hysteria2.go index 31c2c4b8ab..23b2ada372 100644 --- a/listener/inbound/hysteria2.go +++ b/listener/inbound/hysteria2.go @@ -23,6 +23,12 @@ type Hysteria2Option struct { CWND int `inbound:"cwnd,omitempty"` UdpMTU int `inbound:"udp-mtu,omitempty"` MuxOption MuxOption `inbound:"mux-option,omitempty"` + + // quic-go special config + InitialStreamReceiveWindow uint64 `inbound:"initial-stream-receive-window,omitempty"` + MaxStreamReceiveWindow uint64 `inbound:"max-stream-receive-window,omitempty"` + InitialConnectionReceiveWindow uint64 `inbound:"initial-connection-receive-window,omitempty"` + MaxConnectionReceiveWindow uint64 `inbound:"max-connection-receive-window,omitempty"` } func (o Hysteria2Option) Equal(config C.InboundConfig) bool { @@ -61,6 +67,11 @@ func NewHysteria2(options *Hysteria2Option) (*Hysteria2, error) { CWND: options.CWND, UdpMTU: options.UdpMTU, MuxOption: options.MuxOption.Build(), + // quic-go special config + InitialStreamReceiveWindow: options.InitialStreamReceiveWindow, + MaxStreamReceiveWindow: options.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: options.InitialConnectionReceiveWindow, + MaxConnectionReceiveWindow: options.MaxConnectionReceiveWindow, }, }, nil } diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go index 524aa9a137..a415256f95 100644 --- a/listener/sing_hysteria2/server.go +++ b/listener/sing_hysteria2/server.go @@ -22,6 +22,7 @@ import ( "github.com/metacubex/sing-quic/hysteria2" + "github.com/metacubex/quic-go" E "github.com/sagernet/sing/common/exceptions" ) @@ -110,6 +111,13 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi config.UdpMTU = 1200 - 3 } + quicConfig := &quic.Config{ + InitialStreamReceiveWindow: config.InitialStreamReceiveWindow, + MaxStreamReceiveWindow: config.MaxStreamReceiveWindow, + InitialConnectionReceiveWindow: config.InitialConnectionReceiveWindow, + MaxConnectionReceiveWindow: config.MaxConnectionReceiveWindow, + } + service, err := hysteria2.NewService[string](hysteria2.ServiceOptions{ Context: context.Background(), Logger: log.SingLogger, @@ -117,6 +125,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi ReceiveBPS: outbound.StringToBps(config.Down), SalamanderPassword: salamanderPassword, TLSConfig: tlsConfig, + QUICConfig: quicConfig, IgnoreClientBandwidth: config.IgnoreClientBandwidth, Handler: h, MasqueradeHandler: masqueradeHandler, From 9c73b5b750e31f1bb1e230f2a338f5498217603e Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Mon, 20 Jan 2025 23:01:26 +0800 Subject: [PATCH 08/14] fix: the trustcerts not add to globalCerts after ca.ResetCertificate (#1801) support PEM format for custom-certificates too --- component/ca/config.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/component/ca/config.go b/component/ca/config.go index 9d002db639..0a76189270 100644 --- a/component/ca/config.go +++ b/component/ca/config.go @@ -17,7 +17,6 @@ import ( C "github.com/metacubex/mihomo/constant" ) -var trustCerts []*x509.Certificate var globalCertPool *x509.CertPool var mutex sync.RWMutex var errNotMatch = errors.New("certificate fingerprints do not match") @@ -30,11 +29,19 @@ var DisableSystemCa, _ = strconv.ParseBool(os.Getenv("DISABLE_SYSTEM_CA")) func AddCertificate(certificate string) error { mutex.Lock() defer mutex.Unlock() + if certificate == "" { return fmt.Errorf("certificate is empty") } - if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil { - trustCerts = append(trustCerts, cert) + + if globalCertPool == nil { + initializeCertPool() + } + + if globalCertPool.AppendCertsFromPEM([]byte(certificate)) { + return nil + } else if cert, err := x509.ParseCertificate([]byte(certificate)); err == nil { + globalCertPool.AddCert(cert) return nil } else { return fmt.Errorf("add certificate failed") @@ -51,9 +58,6 @@ func initializeCertPool() { globalCertPool = x509.NewCertPool() } } - for _, cert := range trustCerts { - globalCertPool.AddCert(cert) - } if !DisableEmbedCa { globalCertPool.AppendCertsFromPEM(_CaCertificates) } @@ -62,7 +66,6 @@ func initializeCertPool() { func ResetCertificate() { mutex.Lock() defer mutex.Unlock() - trustCerts = nil initializeCertPool() } From b69e52d4d72846b8201a4073ed68c4c332c40db9 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 21 Jan 2025 00:45:49 +0800 Subject: [PATCH 09/14] chore: deprecated `routing-mark` and `interface-name` of the group, please set it directly on the proxy instead --- adapter/outbound/base.go | 6 +++--- adapter/outboundgroup/groupbase.go | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/adapter/outbound/base.go b/adapter/outbound/base.go index dd226f74a7..56bed9d53e 100644 --- a/adapter/outbound/base.go +++ b/adapter/outbound/base.go @@ -153,11 +153,11 @@ func (b *Base) DialOptions(opts ...dialer.Option) []dialer.Option { } type BasicOption struct { - TFO bool `proxy:"tfo,omitempty" group:"tfo,omitempty"` - MPTCP bool `proxy:"mptcp,omitempty" group:"mptcp,omitempty"` + TFO bool `proxy:"tfo,omitempty"` + MPTCP bool `proxy:"mptcp,omitempty"` Interface string `proxy:"interface-name,omitempty" group:"interface-name,omitempty"` RoutingMark int `proxy:"routing-mark,omitempty" group:"routing-mark,omitempty"` - IPVersion string `proxy:"ip-version,omitempty" group:"ip-version,omitempty"` + IPVersion string `proxy:"ip-version,omitempty"` DialerProxy string `proxy:"dialer-proxy,omitempty"` // don't apply this option into groups, but can set a group name in a proxy } diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go index 69fbb61799..f2a567ae8c 100644 --- a/adapter/outboundgroup/groupbase.go +++ b/adapter/outboundgroup/groupbase.go @@ -46,6 +46,13 @@ type GroupBaseOption struct { } func NewGroupBase(opt GroupBaseOption) *GroupBase { + if opt.RoutingMark != 0 { + log.Warnln("The group [%s] with routing-mark configuration is deprecated, please set it directly on the proxy instead", opt.Name) + } + if opt.Interface != "" { + log.Warnln("The group [%s] with interface-name configuration is deprecated, please set it directly on the proxy instead", opt.Name) + } + var excludeFilterReg *regexp2.Regexp if opt.excludeFilter != "" { excludeFilterReg = regexp2.MustCompile(opt.excludeFilter, regexp2.None) From 0ac6c3b185da3a7a312f939a1da521c02052e535 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 4 Feb 2025 00:44:18 +0800 Subject: [PATCH 10/14] feat: inbound support vless --- adapter/outbound/vless.go | 3 - component/generater/cmd.go | 37 +++++ component/generater/types.go | 97 +++++++++++++ constant/metadata.go | 5 + docs/config.yaml | 24 ++++ go.mod | 3 +- go.sum | 6 +- listener/config/vless.go | 38 +++++ listener/inbound/vless.go | 125 ++++++++++++++++ listener/parse.go | 7 + listener/sing_vless/server.go | 263 ++++++++++++++++++++++++++++++++++ main.go | 6 + 12 files changed, 608 insertions(+), 6 deletions(-) create mode 100644 component/generater/cmd.go create mode 100644 component/generater/types.go create mode 100644 listener/config/vless.go create mode 100644 listener/inbound/vless.go create mode 100644 listener/sing_vless/server.go diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go index 7ab61ff624..ab5167bf2b 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -21,7 +21,6 @@ import ( "github.com/metacubex/mihomo/component/resolver" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" - "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/vless" @@ -513,8 +512,6 @@ func NewVless(option VlessOption) (*Vless, error) { if option.Flow != vless.XRV { return nil, fmt.Errorf("unsupported xtls flow type: %s", option.Flow) } - - log.Warnln("To use %s, ensure your server is upgrade to Xray-core v1.8.0+", vless.XRV) addons = &vless.Addons{ Flow: option.Flow, } diff --git a/component/generater/cmd.go b/component/generater/cmd.go new file mode 100644 index 0000000000..9d2c3d976b --- /dev/null +++ b/component/generater/cmd.go @@ -0,0 +1,37 @@ +package generater + +import ( + "encoding/base64" + "fmt" + + "github.com/gofrs/uuid/v5" +) + +func Main(args []string) { + if len(args) < 1 { + panic("Using: generate uuid/reality-keypair/wg-keypair") + } + switch args[0] { + case "uuid": + newUUID, err := uuid.NewV4() + if err != nil { + panic(err) + } + fmt.Println(newUUID.String()) + case "reality-keypair": + privateKey, err := GeneratePrivateKey() + if err != nil { + panic(err) + } + publicKey := privateKey.PublicKey() + fmt.Println("PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:])) + fmt.Println("PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])) + case "wg-keypair": + privateKey, err := GeneratePrivateKey() + if err != nil { + panic(err) + } + fmt.Println("PrivateKey: " + privateKey.String()) + fmt.Println("PublicKey: " + privateKey.PublicKey().String()) + } +} diff --git a/component/generater/types.go b/component/generater/types.go new file mode 100644 index 0000000000..06f59e9468 --- /dev/null +++ b/component/generater/types.go @@ -0,0 +1,97 @@ +// Copy from https://github.com/WireGuard/wgctrl-go/blob/a9ab2273dd1075ea74b88c76f8757f8b4003fcbf/wgtypes/types.go#L71-L155 + +package generater + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + + "golang.org/x/crypto/curve25519" +) + +// KeyLen is the expected key length for a WireGuard key. +const KeyLen = 32 // wgh.KeyLen + +// A Key is a public, private, or pre-shared secret key. The Key constructor +// functions in this package can be used to create Keys suitable for each of +// these applications. +type Key [KeyLen]byte + +// GenerateKey generates a Key suitable for use as a pre-shared secret key from +// a cryptographically safe source. +// +// The output Key should not be used as a private key; use GeneratePrivateKey +// instead. +func GenerateKey() (Key, error) { + b := make([]byte, KeyLen) + if _, err := rand.Read(b); err != nil { + return Key{}, fmt.Errorf("wgtypes: failed to read random bytes: %v", err) + } + + return NewKey(b) +} + +// GeneratePrivateKey generates a Key suitable for use as a private key from a +// cryptographically safe source. +func GeneratePrivateKey() (Key, error) { + key, err := GenerateKey() + if err != nil { + return Key{}, err + } + + // Modify random bytes using algorithm described at: + // https://cr.yp.to/ecdh.html. + key[0] &= 248 + key[31] &= 127 + key[31] |= 64 + + return key, nil +} + +// NewKey creates a Key from an existing byte slice. The byte slice must be +// exactly 32 bytes in length. +func NewKey(b []byte) (Key, error) { + if len(b) != KeyLen { + return Key{}, fmt.Errorf("wgtypes: incorrect key size: %d", len(b)) + } + + var k Key + copy(k[:], b) + + return k, nil +} + +// ParseKey parses a Key from a base64-encoded string, as produced by the +// Key.String method. +func ParseKey(s string) (Key, error) { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return Key{}, fmt.Errorf("wgtypes: failed to parse base64-encoded key: %v", err) + } + + return NewKey(b) +} + +// PublicKey computes a public key from the private key k. +// +// PublicKey should only be called when k is a private key. +func (k Key) PublicKey() Key { + var ( + pub [KeyLen]byte + priv = [KeyLen]byte(k) + ) + + // ScalarBaseMult uses the correct base value per https://cr.yp.to/ecdh.html, + // so no need to specify it. + curve25519.ScalarBaseMult(&pub, &priv) + + return Key(pub) +} + +// String returns the base64-encoded string representation of a Key. +// +// ParseKey can be used to produce a new Key from this string. +func (k Key) String() string { + return base64.StdEncoding.EncodeToString(k[:]) +} diff --git a/constant/metadata.go b/constant/metadata.go index 5436298925..e4167845fa 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -25,6 +25,7 @@ const ( SOCKS5 SHADOWSOCKS VMESS + VLESS REDIR TPROXY TUNNEL @@ -69,6 +70,8 @@ func (t Type) String() string { return "ShadowSocks" case VMESS: return "Vmess" + case VLESS: + return "Vless" case REDIR: return "Redir" case TPROXY: @@ -103,6 +106,8 @@ func ParseType(t string) (*Type, error) { res = SHADOWSOCKS case "VMESS": res = VMESS + case "VLESS": + res = VLESS case "REDIR": res = REDIR case "TPROXY": diff --git a/docs/config.yaml b/docs/config.yaml index b92246ae2e..26e0f7672f 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1176,6 +1176,30 @@ listeners: network: [tcp, udp] target: target.com + - name: vless-in-1 + type: vless + port: 10817 + listen: 0.0.0.0 + # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules + # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) + users: + - username: 1 + uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 + flow: xtls-rprx-vision + # ws-path: "/" # 如果不为空则开启 websocket 传输层 + # 下面两项如果填写则开启 tls(需要同时填写) + # certificate: ./server.crt + # private-key: ./server.key + # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) + reality-config: + dest: test.com:443 + private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 + short-id: + - 0123456789abcdef + server-names: + - test.com + ### 注意,对于vless listener, 至少需要填写 “certificate和private-key” 或 “reality-config” 的其中一项 ### + - name: tun-in-1 type: tun # rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules diff --git a/go.mod b/go.mod index 3585bc797e..585e5c5948 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/metacubex/sing-shadowsocks v0.2.8 github.com/metacubex/sing-shadowsocks2 v0.2.2 github.com/metacubex/sing-tun v0.4.5 - github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 + github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3 github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 github.com/metacubex/utls v1.6.6 @@ -40,6 +40,7 @@ require ( github.com/sagernet/cors v1.2.1 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a + github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-mux v0.2.1 github.com/sagernet/sing-shadowtls v0.1.5 diff --git a/go.sum b/go.sum index 33ad535553..ae4f31d8bc 100644 --- a/go.sum +++ b/go.sum @@ -122,8 +122,8 @@ github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhD github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q= github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0= github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0= -github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9 h1:OAXiCosqY8xKDp3pqTW3qbrCprZ1l6WkrXSFSCwyY4I= -github.com/metacubex/sing-vmess v0.1.9-0.20240719134745-1df6fb20bbf9/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= +github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3 h1:2kq6azIvsTjTnyw66xXDl5zMzIJqF7GTbvLpkroHssg= +github.com/metacubex/sing-vmess v0.1.14-0.20250203033000-f61322b3dbe3/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg= github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589/go.mod h1:4NclTLIZuk+QkHVCGrP87rHi/y8YjgPytxTgApJNMhc= github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY= @@ -170,6 +170,8 @@ github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZN github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= +github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= +github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo= github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE= github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo= diff --git a/listener/config/vless.go b/listener/config/vless.go new file mode 100644 index 0000000000..97456acc0a --- /dev/null +++ b/listener/config/vless.go @@ -0,0 +1,38 @@ +package config + +import ( + "github.com/metacubex/mihomo/listener/sing" + + "encoding/json" +) + +type VlessUser struct { + Username string + UUID string + Flow string +} + +type VlessServer struct { + Enable bool + Listen string + Users []VlessUser + WsPath string + Certificate string + PrivateKey string + RealityConfig RealityConfig + MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` +} + +func (t VlessServer) String() string { + b, _ := json.Marshal(t) + return string(b) +} + +type RealityConfig struct { + Dest string + PrivateKey string + ShortID []string + ServerNames []string + MaxTimeDifference int + Proxy string +} diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go new file mode 100644 index 0000000000..5a6da1337e --- /dev/null +++ b/listener/inbound/vless.go @@ -0,0 +1,125 @@ +package inbound + +import ( + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/listener/sing_vless" + "github.com/metacubex/mihomo/log" +) + +type VlessOption struct { + BaseOption + Users []VlessUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` +} + +type VlessUser struct { + Username string `inbound:"username,omitempty"` + UUID string `inbound:"uuid"` + Flow string `inbound:"flow,omitempty"` +} + +type RealityConfig struct { + Dest string `inbound:"dest"` + PrivateKey string `inbound:"private-key"` + ShortID []string `inbound:"short-id"` + ServerNames []string `inbound:"server-names"` + MaxTimeDifference int `inbound:"max-time-difference,omitempty"` + Proxy string `inbound:"proxy,omitempty"` +} + +func (c RealityConfig) Build() LC.RealityConfig { + return LC.RealityConfig{ + Dest: c.Dest, + PrivateKey: c.PrivateKey, + ShortID: c.ShortID, + ServerNames: c.ServerNames, + MaxTimeDifference: c.MaxTimeDifference, + Proxy: c.Proxy, + } +} + +func (o VlessOption) Equal(config C.InboundConfig) bool { + return optionToString(o) == optionToString(config) +} + +type Vless struct { + *Base + config *VlessOption + l C.MultiAddrListener + vs LC.VlessServer +} + +func NewVless(options *VlessOption) (*Vless, error) { + base, err := NewBase(&options.BaseOption) + if err != nil { + return nil, err + } + users := make([]LC.VlessUser, len(options.Users)) + for i, v := range options.Users { + users[i] = LC.VlessUser{ + Username: v.Username, + UUID: v.UUID, + Flow: v.Flow, + } + } + return &Vless{ + Base: base, + config: options, + vs: LC.VlessServer{ + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), + }, + }, nil +} + +// Config implements constant.InboundListener +func (v *Vless) Config() C.InboundConfig { + return v.config +} + +// Address implements constant.InboundListener +func (v *Vless) Address() string { + if v.l != nil { + for _, addr := range v.l.AddrList() { + return addr.String() + } + } + return "" +} + +// Listen implements constant.InboundListener +func (v *Vless) Listen(tunnel C.Tunnel) error { + var err error + users := make([]LC.VlessUser, len(v.config.Users)) + for i, v := range v.config.Users { + users[i] = LC.VlessUser{ + Username: v.Username, + UUID: v.UUID, + Flow: v.Flow, + } + } + v.l, err = sing_vless.New(v.vs, tunnel, v.Additions()...) + if err != nil { + return err + } + log.Infoln("Vless[%s] proxy listening at: %s", v.Name(), v.Address()) + return nil +} + +// Close implements constant.InboundListener +func (v *Vless) Close() error { + return v.l.Close() +} + +var _ C.InboundListener = (*Vless)(nil) diff --git a/listener/parse.go b/listener/parse.go index 1c8b6463e1..38082e92bd 100644 --- a/listener/parse.go +++ b/listener/parse.go @@ -86,6 +86,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) { return nil, err } listener, err = IN.NewVmess(vmessOption) + case "vless": + vlessOption := &IN.VlessOption{} + err = decoder.Decode(mapping, vlessOption) + if err != nil { + return nil, err + } + listener, err = IN.NewVless(vlessOption) case "hysteria2": hysteria2Option := &IN.Hysteria2Option{} err = decoder.Decode(mapping, hysteria2Option) diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go new file mode 100644 index 0000000000..f537de2d9a --- /dev/null +++ b/listener/sing_vless/server.go @@ -0,0 +1,263 @@ +package sing_vless + +import ( + "context" + "crypto/tls" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "net" + "net/http" + "reflect" + "strings" + "time" + "unsafe" + + "github.com/metacubex/mihomo/adapter/inbound" + N "github.com/metacubex/mihomo/common/net" + tlsC "github.com/metacubex/mihomo/component/tls" + C "github.com/metacubex/mihomo/constant" + LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/listener/inner" + "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/ntp" + mihomoVMess "github.com/metacubex/mihomo/transport/vmess" + + "github.com/metacubex/sing-vmess/vless" + utls "github.com/metacubex/utls" + "github.com/sagernet/reality" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/metadata" +) + +func init() { + vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { + tlsConn, loaded := common.Cast[*reality.Conn](conn) + if !loaded { + return + } + return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn) + }) + + vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { + tlsConn, loaded := common.Cast[*utls.UConn](conn) + if !loaded { + return + } + return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn) + }) + + vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) { + tlsConn, loaded := common.Cast[*tlsC.UConn](conn) + if !loaded { + return + } + return true, tlsConn.NetConn(), reflect.TypeOf(tlsConn.Conn).Elem(), unsafe.Pointer(tlsConn.Conn) + }) +} + +type Listener struct { + closed bool + config LC.VlessServer + listeners []net.Listener + service *vless.Service[string] +} + +func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) (sl *Listener, err error) { + if len(additions) == 0 { + additions = []inbound.Addition{ + inbound.WithInName("DEFAULT-VLESS"), + inbound.WithSpecialRules(""), + } + } + h, err := sing.NewListenerHandler(sing.ListenerConfig{ + Tunnel: tunnel, + Type: C.VLESS, + Additions: additions, + MuxOption: config.MuxOption, + }) + if err != nil { + return nil, err + } + + service := vless.NewService[string](log.SingLogger, h) + service.UpdateUsers( + common.Map(config.Users, func(it LC.VlessUser) string { + return it.Username + }), + common.Map(config.Users, func(it LC.VlessUser) string { + return it.UUID + }), + common.Map(config.Users, func(it LC.VlessUser) string { + return it.Flow + })) + + sl = &Listener{false, config, nil, service} + + tlsConfig := &tls.Config{} + var realityConfig *reality.Config + var httpMux *http.ServeMux + + if config.Certificate != "" && config.PrivateKey != "" { + cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + if config.WsPath != "" { + httpMux = http.NewServeMux() + httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { + conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + sl.HandleConn(conn, tunnel) + }) + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") + } + if config.RealityConfig.PrivateKey != "" { + if tlsConfig.Certificates != nil { + return nil, errors.New("certificate is unavailable in reality") + } + realityConfig = &reality.Config{} + realityConfig.SessionTicketsDisabled = true + realityConfig.Type = "tcp" + realityConfig.Dest = config.RealityConfig.Dest + realityConfig.Time = ntp.Now + realityConfig.ServerNames = make(map[string]bool) + for _, it := range config.RealityConfig.ServerNames { + realityConfig.ServerNames[it] = true + } + privateKey, err := base64.RawURLEncoding.DecodeString(config.RealityConfig.PrivateKey) + if err != nil { + return nil, fmt.Errorf("decode private key: %w", err) + } + if len(privateKey) != 32 { + return nil, errors.New("invalid private key") + } + realityConfig.PrivateKey = privateKey + + realityConfig.MaxTimeDiff = time.Duration(config.RealityConfig.MaxTimeDifference) * time.Microsecond + + realityConfig.ShortIds = make(map[[8]byte]bool) + for i, shortIDString := range config.RealityConfig.ShortID { + var shortID [8]byte + decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString)) + if err != nil { + return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err) + } + if decodedLen > 8 { + return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString) + } + realityConfig.ShortIds[shortID] = true + } + + realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { + return inner.HandleTcp(address, config.RealityConfig.Proxy) + } + } + + for _, addr := range strings.Split(config.Listen, ",") { + addr := addr + + //TCP + l, err := inbound.Listen("tcp", addr) + if err != nil { + return nil, err + } + if realityConfig != nil { + l = reality.NewListener(l, realityConfig) + // Due to low implementation quality, the reality server intercepted half close and caused memory leaks. + // We fixed it by calling Close() directly. + l = realityListenerWrapper{l} + } else if len(tlsConfig.Certificates) > 0 { + l = tls.NewListener(l, tlsConfig) + } else { + return nil, errors.New("disallow using Vless without both certificates/reality config") + } + sl.listeners = append(sl.listeners, l) + + go func() { + if httpMux != nil { + _ = http.Serve(l, httpMux) + return + } + for { + c, err := l.Accept() + if err != nil { + if sl.closed { + break + } + continue + } + + go sl.HandleConn(c, tunnel) + } + }() + } + + return sl, nil +} + +func (l *Listener) Close() error { + l.closed = true + var retErr error + for _, lis := range l.listeners { + err := lis.Close() + if err != nil { + retErr = err + } + } + return retErr +} + +func (l *Listener) Config() string { + return l.config.String() +} + +func (l *Listener) AddrList() (addrList []net.Addr) { + for _, lis := range l.listeners { + addrList = append(addrList, lis.Addr()) + } + return +} + +func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) { + ctx := sing.WithAdditions(context.TODO(), additions...) + err := l.service.NewConnection(ctx, conn, metadata.Metadata{ + Protocol: "vless", + Source: metadata.ParseSocksaddr(conn.RemoteAddr().String()), + }) + if err != nil { + _ = conn.Close() + return + } +} + +type realityConnWrapper struct { + *reality.Conn +} + +func (c realityConnWrapper) Upstream() any { + return c.Conn +} + +func (c realityConnWrapper) CloseWrite() error { + return c.Close() +} + +type realityListenerWrapper struct { + net.Listener +} + +func (l realityListenerWrapper) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + return realityConnWrapper{c.(*reality.Conn)}, nil +} diff --git a/main.go b/main.go index 9a2222df15..3bc3d74f73 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "strings" "syscall" + "github.com/metacubex/mihomo/component/generater" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" @@ -71,6 +72,11 @@ func main() { return } + if len(os.Args) > 1 && os.Args[1] == "generate" { + generater.Main(os.Args[2:]) + return + } + if version { fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", C.Version, runtime.GOOS, runtime.GOARCH, runtime.Version(), C.BuildTime) From a440f640808d1e6f9fc4cbab2e2238619a6a374c Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 4 Feb 2025 15:09:27 +0800 Subject: [PATCH 11/14] chore: alignment capability for vmess inbound --- docs/config.yaml | 8 +++ listener/config/vless.go | 16 ++---- listener/config/vmess.go | 20 ++++--- listener/inbound/reality.go | 23 ++++++++ listener/inbound/vless.go | 20 ------- listener/inbound/vmess.go | 26 +++++---- listener/reality/reality.go | 104 ++++++++++++++++++++++++++++++++++ listener/sing_vless/server.go | 91 +++++------------------------ listener/sing_vmess/server.go | 16 +++++- 9 files changed, 192 insertions(+), 132 deletions(-) create mode 100644 listener/inbound/reality.go create mode 100644 listener/reality/reality.go diff --git a/docs/config.yaml b/docs/config.yaml index 26e0f7672f..2c2f3f43b3 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1146,6 +1146,14 @@ listeners: # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key + # 如果填写reality-config则开启reality(注意不可与certificate和private-key同时填写) + # reality-config: + # dest: test.com:443 + # private-key: jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0 # 可由 mihomo generate reality-keypair 命令生成 + # short-id: + # - 0123456789abcdef + # server-names: + # - test.com - name: tuic-in-1 type: tuic diff --git a/listener/config/vless.go b/listener/config/vless.go index 97456acc0a..2f88c1c406 100644 --- a/listener/config/vless.go +++ b/listener/config/vless.go @@ -1,9 +1,10 @@ package config import ( - "github.com/metacubex/mihomo/listener/sing" - "encoding/json" + + "github.com/metacubex/mihomo/listener/reality" + "github.com/metacubex/mihomo/listener/sing" ) type VlessUser struct { @@ -19,7 +20,7 @@ type VlessServer struct { WsPath string Certificate string PrivateKey string - RealityConfig RealityConfig + RealityConfig reality.Config MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } @@ -27,12 +28,3 @@ func (t VlessServer) String() string { b, _ := json.Marshal(t) return string(b) } - -type RealityConfig struct { - Dest string - PrivateKey string - ShortID []string - ServerNames []string - MaxTimeDifference int - Proxy string -} diff --git a/listener/config/vmess.go b/listener/config/vmess.go index 810d6bc125..0d9e9c4a3b 100644 --- a/listener/config/vmess.go +++ b/listener/config/vmess.go @@ -1,9 +1,10 @@ package config import ( - "github.com/metacubex/mihomo/listener/sing" - "encoding/json" + + "github.com/metacubex/mihomo/listener/reality" + "github.com/metacubex/mihomo/listener/sing" ) type VmessUser struct { @@ -13,13 +14,14 @@ type VmessUser struct { } type VmessServer struct { - Enable bool - Listen string - Users []VmessUser - WsPath string - Certificate string - PrivateKey string - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` + Enable bool + Listen string + Users []VmessUser + WsPath string + Certificate string + PrivateKey string + RealityConfig reality.Config + MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } func (t VmessServer) String() string { diff --git a/listener/inbound/reality.go b/listener/inbound/reality.go new file mode 100644 index 0000000000..7653cd389d --- /dev/null +++ b/listener/inbound/reality.go @@ -0,0 +1,23 @@ +package inbound + +import "github.com/metacubex/mihomo/listener/reality" + +type RealityConfig struct { + Dest string `inbound:"dest"` + PrivateKey string `inbound:"private-key"` + ShortID []string `inbound:"short-id"` + ServerNames []string `inbound:"server-names"` + MaxTimeDifference int `inbound:"max-time-difference,omitempty"` + Proxy string `inbound:"proxy,omitempty"` +} + +func (c RealityConfig) Build() reality.Config { + return reality.Config{ + Dest: c.Dest, + PrivateKey: c.PrivateKey, + ShortID: c.ShortID, + ServerNames: c.ServerNames, + MaxTimeDifference: c.MaxTimeDifference, + Proxy: c.Proxy, + } +} diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go index 5a6da1337e..eb3b3c5a42 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -23,26 +23,6 @@ type VlessUser struct { Flow string `inbound:"flow,omitempty"` } -type RealityConfig struct { - Dest string `inbound:"dest"` - PrivateKey string `inbound:"private-key"` - ShortID []string `inbound:"short-id"` - ServerNames []string `inbound:"server-names"` - MaxTimeDifference int `inbound:"max-time-difference,omitempty"` - Proxy string `inbound:"proxy,omitempty"` -} - -func (c RealityConfig) Build() LC.RealityConfig { - return LC.RealityConfig{ - Dest: c.Dest, - PrivateKey: c.PrivateKey, - ShortID: c.ShortID, - ServerNames: c.ServerNames, - MaxTimeDifference: c.MaxTimeDifference, - Proxy: c.Proxy, - } -} - func (o VlessOption) Equal(config C.InboundConfig) bool { return optionToString(o) == optionToString(config) } diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go index 226a54d520..cf2379e107 100644 --- a/listener/inbound/vmess.go +++ b/listener/inbound/vmess.go @@ -9,11 +9,12 @@ import ( type VmessOption struct { BaseOption - Users []VmessUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` + Users []VmessUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` } type VmessUser struct { @@ -50,13 +51,14 @@ func NewVmess(options *VmessOption) (*Vmess, error) { Base: base, config: options, vs: LC.VmessServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - MuxOption: options.MuxOption.Build(), + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), }, }, nil } diff --git a/listener/reality/reality.go b/listener/reality/reality.go new file mode 100644 index 0000000000..16245e2bb7 --- /dev/null +++ b/listener/reality/reality.go @@ -0,0 +1,104 @@ +package reality + +import ( + "context" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "net" + "time" + + "github.com/metacubex/mihomo/listener/inner" + "github.com/metacubex/mihomo/ntp" + + "github.com/sagernet/reality" +) + +type Conn = reality.Conn + +type Config struct { + Dest string + PrivateKey string + ShortID []string + ServerNames []string + MaxTimeDifference int + Proxy string +} + +func (c Config) Build() (*Builder, error) { + realityConfig := &reality.Config{} + realityConfig.SessionTicketsDisabled = true + realityConfig.Type = "tcp" + realityConfig.Dest = c.Dest + realityConfig.Time = ntp.Now + realityConfig.ServerNames = make(map[string]bool) + for _, it := range c.ServerNames { + realityConfig.ServerNames[it] = true + } + privateKey, err := base64.RawURLEncoding.DecodeString(c.PrivateKey) + if err != nil { + return nil, fmt.Errorf("decode private key: %w", err) + } + if len(privateKey) != 32 { + return nil, errors.New("invalid private key") + } + realityConfig.PrivateKey = privateKey + + realityConfig.MaxTimeDiff = time.Duration(c.MaxTimeDifference) * time.Microsecond + + realityConfig.ShortIds = make(map[[8]byte]bool) + for i, shortIDString := range c.ShortID { + var shortID [8]byte + decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString)) + if err != nil { + return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err) + } + if decodedLen > 8 { + return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString) + } + realityConfig.ShortIds[shortID] = true + } + + realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { + return inner.HandleTcp(address, c.Proxy) + } + + return &Builder{realityConfig}, nil +} + +type Builder struct { + realityConfig *reality.Config +} + +func (b Builder) NewListener(l net.Listener) net.Listener { + l = reality.NewListener(l, b.realityConfig) + // Due to low implementation quality, the reality server intercepted half close and caused memory leaks. + // We fixed it by calling Close() directly. + l = realityListenerWrapper{l} + return l +} + +type realityConnWrapper struct { + *reality.Conn +} + +func (c realityConnWrapper) Upstream() any { + return c.Conn +} + +func (c realityConnWrapper) CloseWrite() error { + return c.Close() +} + +type realityListenerWrapper struct { + net.Listener +} + +func (l realityListenerWrapper) Accept() (net.Conn, error) { + c, err := l.Listener.Accept() + if err != nil { + return nil, err + } + return realityConnWrapper{c.(*reality.Conn)}, nil +} diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index f537de2d9a..2377ff62b6 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -3,15 +3,11 @@ package sing_vless import ( "context" "crypto/tls" - "encoding/base64" - "encoding/hex" "errors" - "fmt" "net" "net/http" "reflect" "strings" - "time" "unsafe" "github.com/metacubex/mihomo/adapter/inbound" @@ -19,15 +15,13 @@ import ( tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" - "github.com/metacubex/mihomo/listener/inner" + "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" - "github.com/metacubex/mihomo/ntp" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/sing-vmess/vless" utls "github.com/metacubex/utls" - "github.com/sagernet/reality" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/metadata" ) @@ -97,7 +91,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl = &Listener{false, config, nil, service} tlsConfig := &tls.Config{} - var realityConfig *reality.Config + var realityBuilder *reality.Builder var httpMux *http.ServeMux if config.Certificate != "" && config.PrivateKey != "" { @@ -107,6 +101,15 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } tlsConfig.Certificates = []tls.Certificate{cert} } + if config.RealityConfig.PrivateKey != "" { + if tlsConfig.Certificates != nil { + return nil, errors.New("certificate is unavailable in reality") + } + realityBuilder, err = config.RealityConfig.Build() + if err != nil { + return nil, err + } + } if config.WsPath != "" { httpMux = http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { @@ -119,47 +122,6 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) }) tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } - if config.RealityConfig.PrivateKey != "" { - if tlsConfig.Certificates != nil { - return nil, errors.New("certificate is unavailable in reality") - } - realityConfig = &reality.Config{} - realityConfig.SessionTicketsDisabled = true - realityConfig.Type = "tcp" - realityConfig.Dest = config.RealityConfig.Dest - realityConfig.Time = ntp.Now - realityConfig.ServerNames = make(map[string]bool) - for _, it := range config.RealityConfig.ServerNames { - realityConfig.ServerNames[it] = true - } - privateKey, err := base64.RawURLEncoding.DecodeString(config.RealityConfig.PrivateKey) - if err != nil { - return nil, fmt.Errorf("decode private key: %w", err) - } - if len(privateKey) != 32 { - return nil, errors.New("invalid private key") - } - realityConfig.PrivateKey = privateKey - - realityConfig.MaxTimeDiff = time.Duration(config.RealityConfig.MaxTimeDifference) * time.Microsecond - - realityConfig.ShortIds = make(map[[8]byte]bool) - for i, shortIDString := range config.RealityConfig.ShortID { - var shortID [8]byte - decodedLen, err := hex.Decode(shortID[:], []byte(shortIDString)) - if err != nil { - return nil, fmt.Errorf("decode short_id[%d] '%s': %w", i, shortIDString, err) - } - if decodedLen > 8 { - return nil, fmt.Errorf("invalid short_id[%d]: %s", i, shortIDString) - } - realityConfig.ShortIds[shortID] = true - } - - realityConfig.DialContext = func(ctx context.Context, network, address string) (net.Conn, error) { - return inner.HandleTcp(address, config.RealityConfig.Proxy) - } - } for _, addr := range strings.Split(config.Listen, ",") { addr := addr @@ -169,11 +131,8 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) if err != nil { return nil, err } - if realityConfig != nil { - l = reality.NewListener(l, realityConfig) - // Due to low implementation quality, the reality server intercepted half close and caused memory leaks. - // We fixed it by calling Close() directly. - l = realityListenerWrapper{l} + if realityBuilder != nil { + l = realityBuilder.NewListener(l) } else if len(tlsConfig.Certificates) > 0 { l = tls.NewListener(l, tlsConfig) } else { @@ -237,27 +196,3 @@ func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbou return } } - -type realityConnWrapper struct { - *reality.Conn -} - -func (c realityConnWrapper) Upstream() any { - return c.Conn -} - -func (c realityConnWrapper) CloseWrite() error { - return c.Close() -} - -type realityListenerWrapper struct { - net.Listener -} - -func (l realityListenerWrapper) Accept() (net.Conn, error) { - c, err := l.Listener.Accept() - if err != nil { - return nil, err - } - return realityConnWrapper{c.(*reality.Conn)}, nil -} diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index 7a0afa0b73..b344fcb476 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -3,6 +3,7 @@ package sing_vmess import ( "context" "crypto/tls" + "errors" "net" "net/http" "net/url" @@ -12,6 +13,7 @@ import ( N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" + "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/ntp" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" @@ -73,6 +75,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl = &Listener{false, config, nil, service} tlsConfig := &tls.Config{} + var realityBuilder *reality.Builder var httpMux *http.ServeMux if config.Certificate != "" && config.PrivateKey != "" { @@ -82,6 +85,15 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } tlsConfig.Certificates = []tls.Certificate{cert} } + if config.RealityConfig.PrivateKey != "" { + if tlsConfig.Certificates != nil { + return nil, errors.New("certificate is unavailable in reality") + } + realityBuilder, err = config.RealityConfig.Build() + if err != nil { + return nil, err + } + } if config.WsPath != "" { httpMux = http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { @@ -103,7 +115,9 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) if err != nil { return nil, err } - if len(tlsConfig.Certificates) > 0 { + if realityBuilder != nil { + l = realityBuilder.NewListener(l) + } else if len(tlsConfig.Certificates) > 0 { l = tls.NewListener(l, tlsConfig) } sl.listeners = append(sl.listeners, l) From 0a5ea37c07b796ffe69baccb34edb480543902b7 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 4 Feb 2025 15:28:06 +0800 Subject: [PATCH 12/14] chore: update dependencies --- go.mod | 21 ++++++++++----------- go.sum | 48 +++++++++++++++++++++++------------------------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index 585e5c5948..9829af442a 100644 --- a/go.mod +++ b/go.mod @@ -7,12 +7,12 @@ require ( github.com/bahlo/generic-list-go v0.2.0 github.com/coreos/go-iptables v0.8.0 github.com/dlclark/regexp2 v1.11.4 - github.com/enfein/mieru/v3 v3.10.0 + github.com/enfein/mieru/v3 v3.11.1 github.com/go-chi/chi/v5 v5.2.0 github.com/go-chi/render v1.0.3 github.com/gobwas/ws v1.4.0 github.com/gofrs/uuid/v5 v5.3.0 - github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d + github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20 github.com/klauspost/cpuid/v2 v2.2.9 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 @@ -32,11 +32,11 @@ require ( github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 github.com/metacubex/utls v1.6.6 github.com/metacubex/wireguard-go v0.0.0-20240922131502-c182e7471181 - github.com/miekg/dns v1.1.62 + github.com/miekg/dns v1.1.63 github.com/mroth/weightedrand/v2 v2.1.0 github.com/openacid/low v0.1.21 github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20 - github.com/puzpuzpuz/xsync/v3 v3.4.0 + github.com/puzpuzpuz/xsync/v3 v3.5.0 github.com/sagernet/cors v1.2.1 github.com/sagernet/fswatch v0.1.1 github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a @@ -44,8 +44,8 @@ require ( github.com/sagernet/sing v0.5.1 github.com/sagernet/sing-mux v0.2.1 github.com/sagernet/sing-shadowtls v0.1.5 - github.com/samber/lo v1.47.0 - github.com/shirou/gopsutil/v4 v4.24.11 + github.com/samber/lo v1.49.1 + github.com/shirou/gopsutil/v4 v4.25.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.10.0 github.com/vmihailenco/msgpack/v5 v5.4.1 @@ -53,10 +53,10 @@ require ( gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7 go.uber.org/automaxprocs v1.6.0 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.31.0 + golang.org/x/crypto v0.32.0 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // lastest version compatible with golang1.20 - golang.org/x/net v0.33.0 - golang.org/x/sys v0.28.0 + golang.org/x/net v0.34.0 + golang.org/x/sys v0.29.0 google.golang.org/protobuf v1.34.2 // lastest version compatible with golang1.20 gopkg.in/yaml.v3 v3.0.1 lukechampine.com/blake3 v1.3.0 @@ -70,7 +70,7 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/ebitengine/purego v0.8.1 // indirect + github.com/ebitengine/purego v0.8.2 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect @@ -86,7 +86,6 @@ require ( github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/yamux v0.1.2 // indirect github.com/josharian/native v1.1.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mdlayher/socket v0.4.1 // indirect diff --git a/go.sum b/go.sum index ae4f31d8bc..b916e5b4ac 100644 --- a/go.sum +++ b/go.sum @@ -21,16 +21,15 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc= github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE= -github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/enfein/mieru/v3 v3.10.0 h1:KMnAtY4s8MB74sUg4GbvF9R9v3jkXPQTSkxPxl1emxQ= -github.com/enfein/mieru/v3 v3.10.0/go.mod h1:jH2nXzJSNUn6UWuzD8E8AsRVa9Ca0CqcTcr9Z+CJO1o= +github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= +github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/enfein/mieru/v3 v3.11.1 h1:G6PjSWPvzvgWUoMqnD4sBe1OGkDgrpNVT2UqCQhE6SE= +github.com/enfein/mieru/v3 v3.11.1/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g= @@ -75,8 +74,8 @@ github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d h1:VkCNWh6tuQLgDBc6KrUOz/L1mCUQGnR1Ujj8uTgpwwk= -github.com/insomniacslk/dhcp v0.0.0-20241224095048-b56fa0d5f25d/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= +github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905 h1:q3OEI9RaN/wwcx+qgGo6ZaoJkCiDYe/gjDLfq7lQQF4= +github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905/go.mod h1:VvGYjkZoJyKqlmT1yzakUs4mfKMNB0XdODP0+rdml6k= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= @@ -85,9 +84,7 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= @@ -132,10 +129,11 @@ github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8= github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo= 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/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= -github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY= +github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q= @@ -156,8 +154,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= -github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= -github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4= +github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= @@ -178,10 +176,10 @@ github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiT github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= -github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= -github.com/shirou/gopsutil/v4 v4.24.11 h1:WaU9xqGFKvFfsUv94SXcUPD7rCkU0vr/asVdQOBZNj8= -github.com/shirou/gopsutil/v4 v4.24.11/go.mod h1:s4D/wg+ag4rG0WO7AiTj2BeYCRhym0vM7DHbZRxnIT8= +github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= +github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= +github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= +github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8= github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM= github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk= @@ -234,8 +232,8 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -244,8 +242,8 @@ golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= @@ -264,9 +262,9 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= @@ -280,7 +278,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 9bfb10d7aefee0799f0116c22479627f312ccf4f Mon Sep 17 00:00:00 2001 From: ForestL <45709305+ForestL18@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:10:58 +0800 Subject: [PATCH 13/14] chore: extracting compressed files to correct location (#1823) --- component/updater/update_ui.go | 259 +++++++++++++++++++++++++++++++-- 1 file changed, 250 insertions(+), 9 deletions(-) diff --git a/component/updater/update_ui.go b/component/updater/update_ui.go index bd2a588156..94bc27de58 100644 --- a/component/updater/update_ui.go +++ b/component/updater/update_ui.go @@ -1,7 +1,9 @@ package updater import ( + "archive/tar" "archive/zip" + "compress/gzip" "fmt" "io" "os" @@ -22,6 +24,14 @@ type UIUpdater struct { mutex sync.Mutex } +type compressionType int + +const ( + typeUnknown compressionType = iota + typeZip + typeTarGzip +) + var DefaultUiUpdater = &UIUpdater{} func NewUiUpdater(externalUI, externalUIURL, externalUIName string) *UIUpdater { @@ -70,6 +80,24 @@ func (u *UIUpdater) DownloadUI() error { return u.downloadUI() } +func detectFileType(data []byte) compressionType { + if len(data) < 4 { + return typeUnknown + } + + // Zip: 0x50 0x4B 0x03 0x04 + if data[0] == 0x50 && data[1] == 0x4B && data[2] == 0x03 && data[3] == 0x04 { + return typeZip + } + + // GZip: 0x1F 0x8B + if data[0] == 0x1F && data[1] == 0x8B { + return typeTarGzip + } + + return typeUnknown +} + func (u *UIUpdater) downloadUI() error { err := u.prepareUIPath() if err != nil { @@ -78,12 +106,23 @@ func (u *UIUpdater) downloadUI() error { data, err := downloadForBytes(u.externalUIURL) if err != nil { - return fmt.Errorf("can't download file: %w", err) + return fmt.Errorf("can't download file: %w", err) + } + + fileType := detectFileType(data) + if fileType == typeUnknown { + return fmt.Errorf("unknown or unsupported file type") } - saved := path.Join(C.Path.HomeDir(), "download.zip") + ext := ".zip" + if fileType == typeTarGzip { + 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 zip file: %w", err) + return fmt.Errorf("can't save compressed file: %w", err) } defer os.Remove(saved) @@ -94,12 +133,12 @@ func (u *UIUpdater) downloadUI() error { } } - unzipFolder, err := unzip(saved, C.Path.HomeDir()) + extractedFolder, err := extract(saved, C.Path.HomeDir()) if err != nil { - return fmt.Errorf("can't extract zip file: %w", err) + return fmt.Errorf("can't extract compressed file: %w", err) } - err = os.Rename(unzipFolder, u.externalUIPath) + err = os.Rename(extractedFolder, u.externalUIPath) if err != nil { return fmt.Errorf("rename UI folder failed: %w", err) } @@ -122,9 +161,66 @@ func unzip(src, dest string) (string, error) { return "", err } defer r.Close() + + // 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 { - fpath := filepath.Join(dest, f.Name) + var fpath string + 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)) { return "", fmt.Errorf("invalid file path: %s", fpath) } @@ -149,13 +245,158 @@ func unzip(src, dest string) (string, error) { if err != nil { return "", err } - if extractedFolder == "" { - extractedFolder = filepath.Dir(fpath) + } + return extractedFolder, nil +} + +func untgz(src, dest string) (string, error) { + file, err := os.Open(src) + if err != nil { + return "", err + } + defer file.Close() + + gzr, err := gzip.NewReader(file) + if err != nil { + return "", err + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + + rootDir := "" + 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) + + 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 { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return "", err + } + + var fpath string + 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)) { + return "", fmt.Errorf("invalid file path: %s", fpath) + } + + switch header.Typeflag { + case tar.TypeDir: + if err = os.MkdirAll(fpath, os.FileMode(header.Mode)); err != nil { + return "", err + } + case tar.TypeReg: + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + return "", err + } + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(header.Mode)) + if err != nil { + return "", err + } + if _, err := io.Copy(outFile, tr); err != nil { + outFile.Close() + return "", err + } + outFile.Close() } } return extractedFolder, nil } +func extract(src, dest string) (string, error) { + srcLower := strings.ToLower(src) + switch { + case strings.HasSuffix(srcLower, ".tar.gz") || + strings.HasSuffix(srcLower, ".tgz"): + return untgz(src, dest) + case strings.HasSuffix(srcLower, ".zip"): + return unzip(src, dest) + default: + return "", fmt.Errorf("unsupported file format: %s", src) + } +} + +func cleanTarPath(path string) string { + // remove prefix ./ or ../ + path = strings.TrimPrefix(path, "./") + path = strings.TrimPrefix(path, "../") + + // normalize path + path = filepath.Clean(path) + + // transfer delimiters to system std + path = filepath.FromSlash(path) + + // remove prefix path delimiters + path = strings.TrimPrefix(path, string(os.PathSeparator)) + + return path +} + func cleanup(root string) error { if _, err := os.Stat(root); os.IsNotExist(err) { return nil From ccc3f84da22dad5210ce2f7986c34198cc75a3dc Mon Sep 17 00:00:00 2001 From: "clash-meta-maintainer[bot]" <148681994+clash-meta-maintainer[bot]@users.noreply.github.com> Date: Sat, 8 Feb 2025 23:28:08 +0800 Subject: [PATCH 14/14] license: any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d38fbedd8a..05f03799eb 100644 --- a/README.md +++ b/README.md @@ -98,3 +98,4 @@ API. This software is released under the GPL-3.0 license. +**In addition, any downstream projects not affiliated with `MetaCubeX` shall not contain the word `mihomo` in their names.** \ No newline at end of file