diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30abd89d..bad84cd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -204,18 +204,18 @@ jobs: alien --to-rpm --scripts mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb mv mihomo*.rpm mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.rpm - - name: Convert DEB to PKG - if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') && !contains(matrix.jobs.goarch, 'loong64') }} - run: | - docker pull archlinux - docker run --rm -v ./:/mnt archlinux bash -c " - pacman -Syu pkgfile base-devel --noconfirm - curl -L https://github.com/helixarch/debtap/raw/master/debtap > /usr/bin/debtap - chmod 755 /usr/bin/debtap - debtap -u - debtap -Q /mnt/mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb - " - mv mihomo*.pkg.tar.zst mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst + # - name: Convert DEB to PKG + # if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') && !contains(matrix.jobs.goarch, 'loong64') }} + # run: | + # docker pull archlinux + # docker run --rm -v ./:/mnt archlinux bash -c " + # pacman -Syu pkgfile base-devel --noconfirm + # curl -L https://github.com/helixarch/debtap/raw/master/debtap > /usr/bin/debtap + # chmod 755 /usr/bin/debtap + # debtap -u + # debtap -Q /mnt/mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.deb + # " + # mv mihomo*.pkg.tar.zst mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}.pkg.tar.zst - name: Save version run: | @@ -230,7 +230,6 @@ jobs: mihomo*.gz mihomo*.deb mihomo*.rpm - mihomo*.pkg.tar.zst mihomo*.zip version.txt diff --git a/adapter/outbound/dns.go b/adapter/outbound/dns.go index 405392a1..21a5b2b7 100644 --- a/adapter/outbound/dns.go +++ b/adapter/outbound/dns.go @@ -2,10 +2,10 @@ package outbound import ( "context" - "fmt" "net" "time" + N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" @@ -24,7 +24,9 @@ type DnsOption struct { // DialContext implements C.ProxyAdapter func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { - return nil, fmt.Errorf("dns outbound does not support tcp") + left, right := N.Pipe() + go resolver.RelayDnsConn(context.Background(), right, 0) + return NewConn(left, d), nil } // ListenPacketContext implements C.ProxyAdapter @@ -76,29 +78,44 @@ func (d *dnsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { } func (d *dnsPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { + select { + case <-d.ctx.Done(): + return 0, net.ErrClosed + default: + } + + if len(p) > resolver.SafeDnsPacketSize { + // wtf??? + return len(p), nil + } + ctx, cancel := context.WithTimeout(d.ctx, resolver.DefaultDnsRelayTimeout) defer cancel() buf := pool.Get(resolver.SafeDnsPacketSize) put := func() { _ = pool.Put(buf) } - buf, err = resolver.RelayDnsPacket(ctx, p, buf) - if err != nil { - put() - return 0, err - } + copy(buf, p) // avoid p be changed after WriteTo returned - packet := dnsPacket{ - data: buf, - put: put, - addr: addr, - } - select { - case d.response <- packet: - return len(p), nil - case <-d.ctx.Done(): - put() - return 0, net.ErrClosed - } + go func() { // don't block the WriteTo function + buf, err = resolver.RelayDnsPacket(ctx, buf[:len(p)], buf) + if err != nil { + put() + return + } + + packet := dnsPacket{ + data: buf, + put: put, + addr: addr, + } + select { + case d.response <- packet: + break + case <-d.ctx.Done(): + put() + } + }() + return len(p), nil } func (d *dnsPacketConn) Close() error { diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 47272ec8..e55237d6 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -5,15 +5,19 @@ import ( "crypto/tls" "errors" "fmt" + "math/rand" "net" "runtime" "strconv" + "strings" + "time" CN "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/ca" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/proxydialer" C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" tuicCommon "github.com/metacubex/mihomo/transport/tuic/common" "github.com/metacubex/sing-quic/hysteria2" @@ -25,6 +29,9 @@ func init() { hysteria2.SetCongestionController = tuicCommon.SetCongestionController } +const minHopInterval = 5 +const defaultHopInterval = 30 + type Hysteria2 struct { *Base @@ -38,6 +45,8 @@ type Hysteria2Option struct { Name string `proxy:"name"` Server string `proxy:"server"` Port int `proxy:"port"` + Ports string `proxy:"ports,omitempty"` + HopInterval int `proxy:"hop-interval,omitempty"` Up string `proxy:"up,omitempty"` Down string `proxy:"down,omitempty"` Password string `proxy:"password,omitempty"` @@ -82,6 +91,41 @@ func closeHysteria2(h *Hysteria2) { } } +func parsePorts(portStr string) (ports []uint16) { + portStrs := strings.Split(portStr, ",") + for _, portStr := range portStrs { + if strings.Contains(portStr, "-") { + // Port range + portRange := strings.Split(portStr, "-") + if len(portRange) != 2 { + return nil + } + start, err := strconv.ParseUint(portRange[0], 10, 16) + if err != nil { + return nil + } + end, err := strconv.ParseUint(portRange[1], 10, 16) + if err != nil { + return nil + } + if start > end { + start, end = end, start + } + for i := start; i <= end; i++ { + ports = append(ports, uint16(i)) + } + } else { + // Single port + port, err := strconv.ParseUint(portStr, 10, 16) + if err != nil { + return nil + } + ports = append(ports, uint16(port)) + } + } + return ports +} + func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) var salamanderPassword string @@ -129,6 +173,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { clientOptions := hysteria2.ClientOptions{ Context: context.TODO(), Dialer: singDialer, + Logger: log.SingLogger, ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), SendBPS: StringToBps(option.Up), ReceiveBPS: StringToBps(option.Down), @@ -140,6 +185,23 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { UdpMTU: option.UdpMTU, } + if option.Ports != "" { + ports := parsePorts(option.Ports) + if len(ports) > 0 { + for _, port := range ports { + clientOptions.ServerAddresses = append(clientOptions.ServerAddresses, M.ParseSocksaddrHostPort(option.Server, port)) + } + clientOptions.ServerAddress = clientOptions.ServerAddresses[rand.Intn(len(clientOptions.ServerAddresses))] + + if option.HopInterval == 0 { + option.HopInterval = defaultHopInterval + } else if option.HopInterval < minHopInterval { + option.HopInterval = minHopInterval + } + clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second + } + } + client, err := hysteria2.NewClient(clientOptions) if err != nil { return nil, err diff --git a/adapter/outbound/ssh.go b/adapter/outbound/ssh.go new file mode 100644 index 00000000..a41a8132 --- /dev/null +++ b/adapter/outbound/ssh.go @@ -0,0 +1,173 @@ +package outbound + +import ( + "context" + "net" + "os" + "runtime" + "strconv" + "sync" + + N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/dialer" + "github.com/metacubex/mihomo/component/proxydialer" + C "github.com/metacubex/mihomo/constant" + + "github.com/zhangyunhao116/fastrand" + "golang.org/x/crypto/ssh" +) + +type Ssh struct { + *Base + + option *SshOption + client *sshClient // using a standalone struct to avoid its inner loop invalidate the Finalizer +} + +type SshOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + UserName string `proxy:"username"` + Password string `proxy:"password,omitempty"` + PrivateKey string `proxy:"privateKey,omitempty"` +} + +func (s *Ssh) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + var cDialer C.Dialer = dialer.NewDialer(s.Base.DialOptions(opts...)...) + if len(s.option.DialerProxy) > 0 { + cDialer, err = proxydialer.NewByName(s.option.DialerProxy, cDialer) + if err != nil { + return nil, err + } + } + client, err := s.client.connect(ctx, cDialer, s.addr) + if err != nil { + return nil, err + } + c, err := client.DialContext(ctx, "tcp", metadata.RemoteAddress()) + if err != nil { + return nil, err + } + + return NewConn(N.NewRefConn(c, s), s), nil +} + +type sshClient struct { + config *ssh.ClientConfig + client *ssh.Client + cMutex sync.Mutex +} + +func (s *sshClient) connect(ctx context.Context, cDialer C.Dialer, addr string) (client *ssh.Client, err error) { + s.cMutex.Lock() + defer s.cMutex.Unlock() + if s.client != nil { + return s.client, nil + } + c, err := cDialer.DialContext(ctx, "tcp", addr) + if err != nil { + return nil, err + } + N.TCPKeepAlive(c) + + defer func(c net.Conn) { + safeConnClose(c, err) + }(c) + + if ctx.Done() != nil { + done := N.SetupContextForConn(ctx, c) + defer done(&err) + } + + clientConn, chans, reqs, err := ssh.NewClientConn(c, addr, s.config) + if err != nil { + return nil, err + } + client = ssh.NewClient(clientConn, chans, reqs) + + s.client = client + + go func() { + _ = client.Wait() // wait shutdown + _ = client.Close() + s.cMutex.Lock() + defer s.cMutex.Unlock() + if s.client == client { + s.client = nil + } + }() + + return client, nil +} + +func (s *sshClient) Close() error { + s.cMutex.Lock() + defer s.cMutex.Unlock() + if s.client != nil { + return s.client.Close() + } + return nil +} + +func closeSsh(s *Ssh) { + _ = s.client.Close() +} + +func NewSsh(option SshOption) (*Ssh, error) { + addr := net.JoinHostPort(option.Server, strconv.Itoa(option.Port)) + + config := ssh.ClientConfig{ + User: option.UserName, + HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { + return nil + }, + } + + if option.Password == "" { + b, err := os.ReadFile(option.PrivateKey) + if err != nil { + return nil, err + } + pKey, err := ssh.ParsePrivateKey(b) + if err != nil { + return nil, err + } + + config.Auth = []ssh.AuthMethod{ + ssh.PublicKeys(pKey), + } + } else { + config.Auth = []ssh.AuthMethod{ + ssh.Password(option.Password), + } + } + + version := "SSH-2.0-OpenSSH_" + if fastrand.Intn(2) == 0 { + version += "7." + strconv.Itoa(fastrand.Intn(10)) + } else { + version += "8." + strconv.Itoa(fastrand.Intn(9)) + } + config.ClientVersion = version + + outbound := &Ssh{ + Base: &Base{ + name: option.Name, + addr: addr, + tp: C.Ssh, + udp: false, + iface: option.Interface, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + option: &option, + client: &sshClient{ + config: &config, + }, + } + runtime.SetFinalizer(outbound, closeSsh) + + return outbound, nil +} diff --git a/adapter/parser.go b/adapter/parser.go index fa94708d..c64ee13a 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -134,6 +134,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy = outbound.NewRejectWithOption(*rejectOption) + case "ssh": + sshOption := &outbound.SshOption{} + err = decoder.Decode(mapping, sshOption) + if err != nil { + break + } + proxy, err = outbound.NewSsh(*sshOption) default: return nil, fmt.Errorf("unsupport proxy type: %s", proxyType) } diff --git a/common/net/deadline/conn.go b/common/net/deadline/conn.go index e8446ce2..fdf9334f 100644 --- a/common/net/deadline/conn.go +++ b/common/net/deadline/conn.go @@ -26,6 +26,11 @@ type Conn struct { resultCh chan *connReadResult } +func IsConn(conn any) bool { + _, ok := conn.(*Conn) + return ok +} + func NewConn(conn net.Conn) *Conn { c := &Conn{ ExtendedConn: bufio.NewExtendedConn(conn), diff --git a/common/net/deadline/pipe_sing.go b/common/net/deadline/pipe_sing.go index 20721fad..0f6d378d 100644 --- a/common/net/deadline/pipe_sing.go +++ b/common/net/deadline/pipe_sing.go @@ -215,3 +215,8 @@ func (p *pipe) waitReadBuffer() (buffer *buf.Buffer, err error) { return nil, os.ErrDeadlineExceeded } } + +func IsPipe(conn any) bool { + _, ok := conn.(*pipe) + return ok +} diff --git a/common/net/sing.go b/common/net/sing.go index 3296ad5b..d726f440 100644 --- a/common/net/sing.go +++ b/common/net/sing.go @@ -23,6 +23,12 @@ type ExtendedReader = network.ExtendedReader var WriteBuffer = bufio.WriteBuffer func NewDeadlineConn(conn net.Conn) ExtendedConn { + if deadline.IsPipe(conn) || deadline.IsPipe(network.UnwrapReader(conn)) { + return NewExtendedConn(conn) // pipe always have correctly deadline implement + } + if deadline.IsConn(conn) || deadline.IsConn(network.UnwrapReader(conn)) { + return NewExtendedConn(conn) // was a *deadline.Conn + } return deadline.NewConn(conn) } diff --git a/component/mmdb/reader.go b/component/mmdb/reader.go index 6247cd8a..787bdfe8 100644 --- a/component/mmdb/reader.go +++ b/component/mmdb/reader.go @@ -3,6 +3,7 @@ package mmdb import ( "fmt" "net" + "strings" "github.com/oschwald/maxminddb-golang" ) @@ -26,7 +27,7 @@ func (r Reader) LookupCode(ipAddress net.IP) []string { if country.Country.IsoCode == "" { return []string{} } - return []string{country.Country.IsoCode} + return []string{strings.ToLower(country.Country.IsoCode)} case typeSing: var code string diff --git a/component/resolver/relay.go b/component/resolver/relay.go index 3bc54445..27b25af1 100644 --- a/component/resolver/relay.go +++ b/component/resolver/relay.go @@ -17,15 +17,15 @@ const DefaultDnsRelayTimeout = time.Second * 5 const SafeDnsPacketSize = 2 * 1024 // safe size which is 1232 from https://dnsflagday.net/2020/, so 2048 is enough -func RelayDnsConn(ctx context.Context, conn net.Conn) error { +func RelayDnsConn(ctx context.Context, conn net.Conn, readTimeout time.Duration) error { buff := pool.Get(pool.UDPBufferSize) defer func() { _ = pool.Put(buff) _ = conn.Close() }() for { - if conn.SetReadDeadline(time.Now().Add(DefaultDnsReadTimeout)) != nil { - break + if readTimeout > 0 { + _ = conn.SetReadDeadline(time.Now().Add(readTimeout)) } length := uint16(0) diff --git a/constant/adapters.go b/constant/adapters.go index 105a7904..cb213b3c 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -41,6 +41,7 @@ const ( Hysteria2 WireGuard Tuic + Ssh ) const ( @@ -222,7 +223,8 @@ func (at AdapterType) String() string { return "URLTest" case LoadBalance: return "LoadBalance" - + case Ssh: + return "Ssh" default: return "Unknown" } diff --git a/constant/rule.go b/constant/rule.go index 66fc18bb..9b038221 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -5,6 +5,7 @@ const ( Domain RuleType = iota DomainSuffix DomainKeyword + DomainRegex GEOSITE GEOIP IPCIDR @@ -40,6 +41,8 @@ func (rt RuleType) String() string { return "DomainSuffix" case DomainKeyword: return "DomainKeyword" + case DomainRegex: + return "DomainRegex" case GEOSITE: return "GeoSite" case GEOIP: diff --git a/go.mod b/go.mod index 7c28d2bf..e37ebc8e 100644 --- a/go.mod +++ b/go.mod @@ -19,8 +19,8 @@ require ( github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/mdlayher/netlink v1.7.2 github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 - github.com/metacubex/quic-go v0.41.1-0.20240120014142-a02f4a533d4a - github.com/metacubex/sing-quic v0.0.0-20240130040922-cbe613c88f20 + github.com/metacubex/quic-go v0.41.1-0.20240307164142-46c6f7cdf2d1 + github.com/metacubex/sing-quic v0.0.0-20240308143007-4dd80423c25a github.com/metacubex/sing-shadowsocks v0.2.6 github.com/metacubex/sing-shadowsocks2 v0.2.0 github.com/metacubex/sing-tun v0.2.1-0.20240214100323-23e40bfb9067 diff --git a/go.sum b/go.sum index 957b30de..f51a7f17 100644 --- a/go.sum +++ b/go.sum @@ -104,12 +104,12 @@ github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvO github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88= github.com/metacubex/gvisor v0.0.0-20240214095142-666a73bcf165 h1:QIQI4gEm+gTwVNdiAyF4EIz5cHm7kSlfDGFpYlAa5dg= github.com/metacubex/gvisor v0.0.0-20240214095142-666a73bcf165/go.mod h1:SKY70wiF1UTSoyuDZyKPMsUC6MsMxh8Y3ZNkIa6J3fU= -github.com/metacubex/quic-go v0.41.1-0.20240120014142-a02f4a533d4a h1:IMr75VdMnDUhkANZemUWqmOPLfwnemiIaCHRnGCdAsY= -github.com/metacubex/quic-go v0.41.1-0.20240120014142-a02f4a533d4a/go.mod h1:F/t8VnA47xoia8ABlNA4InkZjssvFJ5p6E6jKdbkgAs= +github.com/metacubex/quic-go v0.41.1-0.20240307164142-46c6f7cdf2d1 h1:63zKmEWU4MB5MjUSCmeDhm3OzilF7ypXWPq0gAA2GE8= +github.com/metacubex/quic-go v0.41.1-0.20240307164142-46c6f7cdf2d1/go.mod h1:F/t8VnA47xoia8ABlNA4InkZjssvFJ5p6E6jKdbkgAs= github.com/metacubex/sing v0.0.0-20240111014253-f1818b6a82b2 h1:upEO8dt9WDBavhgcgkXB3hRcwVNbkTbnd+xyzy6ZQZo= github.com/metacubex/sing v0.0.0-20240111014253-f1818b6a82b2/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g= -github.com/metacubex/sing-quic v0.0.0-20240130040922-cbe613c88f20 h1:wt7ydRxm9Pvw+un6KD97tjLJHMrkzp83HyiGkoz6e7k= -github.com/metacubex/sing-quic v0.0.0-20240130040922-cbe613c88f20/go.mod h1:bdHqEysJclB9BzIa5jcKKSZ1qua+YEPjR8fOzzE3vZU= +github.com/metacubex/sing-quic v0.0.0-20240308143007-4dd80423c25a h1:ATj0jL+cp7n+NT3T010cXK5KoVvAbeGhZFtUFHvq2BU= +github.com/metacubex/sing-quic v0.0.0-20240308143007-4dd80423c25a/go.mod h1:WyY0zYxv+o+18R/Ece+QFontlgXoobKbNqbtYn2zjz8= github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ= github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg= github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A= @@ -255,6 +255,7 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 86237daa..42926732 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -37,7 +37,7 @@ func (h *ListenerHandler) ShouldHijackDns(targetAddr netip.AddrPort) bool { func (h *ListenerHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { if h.ShouldHijackDns(metadata.Destination.AddrPort()) { log.Debugln("[DNS] hijack tcp:%s", metadata.Destination.String()) - return resolver.RelayDnsConn(ctx, conn) + return resolver.RelayDnsConn(ctx, conn, resolver.DefaultDnsReadTimeout) } return h.ListenerHandler.NewConnection(ctx, conn, metadata) } diff --git a/main.go b/main.go index 748fa2e3..4b2dff9c 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "os" + "os/exec" "os/signal" "path/filepath" "runtime" @@ -48,6 +49,10 @@ func init() { } func main() { + if runtime.GOOS == "android" { + SetAndroidTZ() + } + _, _ = maxprocs.Set(maxprocs.Logger(func(string, ...any) {})) if version { fmt.Printf("Mihomo Meta %s %s %s with %s %s\n", @@ -176,3 +181,15 @@ func updateGeoDatabases() { executor.ApplyConfig(cfg, false) }() } + +func SetAndroidTZ() { + out, err := exec.Command("getprop", "persist.sys.timezone").Output() + if err != nil { + return + } + z, err := time.LoadLocation(strings.TrimSpace(string(out))) + if err != nil { + return + } + time.Local = z +} diff --git a/rules/common/domain_regex.go b/rules/common/domain_regex.go new file mode 100644 index 00000000..3d961542 --- /dev/null +++ b/rules/common/domain_regex.go @@ -0,0 +1,44 @@ +package common + +import ( + "regexp" + + C "github.com/metacubex/mihomo/constant" +) + +type DomainRegex struct { + *Base + regex *regexp.Regexp + adapter string +} + +func (dr *DomainRegex) RuleType() C.RuleType { + return C.DomainRegex +} + +func (dr *DomainRegex) Match(metadata *C.Metadata) (bool, string) { + domain := metadata.RuleHost() + return dr.regex.MatchString(domain), dr.adapter +} + +func (dr *DomainRegex) Adapter() string { + return dr.adapter +} + +func (dr *DomainRegex) Payload() string { + return dr.regex.String() +} + +func NewDomainRegex(regex string, adapter string) (*DomainRegex, error) { + r, err := regexp.Compile(regex) + if err != nil { + return nil, err + } + return &DomainRegex{ + Base: &Base{}, + regex: r, + adapter: adapter, + }, nil +} + +//var _ C.Rule = (*DomainRegex)(nil) diff --git a/rules/parser.go b/rules/parser.go index 7a79b18b..f7df5f49 100644 --- a/rules/parser.go +++ b/rules/parser.go @@ -2,7 +2,7 @@ package rules import ( "fmt" - + C "github.com/metacubex/mihomo/constant" RC "github.com/metacubex/mihomo/rules/common" "github.com/metacubex/mihomo/rules/logic" @@ -17,6 +17,8 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] parsed = RC.NewDomainSuffix(payload, target) case "DOMAIN-KEYWORD": parsed = RC.NewDomainKeyword(payload, target) + case "DOMAIN-REGEX": + parsed, parseErr = RC.NewDomainRegex(payload, target) case "GEOSITE": parsed, parseErr = RC.NewGEOSITE(payload, target) case "GEOIP":