diff --git a/.github/mihomo.service b/.github/mihomo.service index 057c0236..a884059f 100644 --- a/.github/mihomo.service +++ b/.github/mihomo.service @@ -10,7 +10,7 @@ CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIM AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH Restart=always ExecStartPre=/usr/bin/sleep 2s -ExecStart=/usr/local/bin/mihomo -d /etc/mihomo +ExecStart=/usr/bin/mihomo -d /etc/mihomo ExecReload=/bin/kill -HUP $MAINPID [Install] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ebbe9d0d..bad84cd1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64 } - { goos: linux, goarch: '386', output: '386' } - - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible } + - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible, test: test } - { goos: linux, goarch: amd64, goamd64: v3, output: amd64 } - { goos: linux, goarch: arm64, output: arm64 } - { goos: linux, goarch: arm, goarm: '7', output: armv7 } @@ -41,7 +41,8 @@ jobs: - { goos: linux, goarch: mipsle, mips: softfloat, output: mipsle-softfloat } - { goos: linux, goarch: mips64, output: mips64 } - { goos: linux, goarch: mips64le, output: mips64le } - - { goos: linux, goarch: loong64, output: loong64 } + - { goos: linux, goarch: loong64, output: loong64-abi1, abi: '1' } + - { goos: linux, goarch: loong64, output: loong64-abi2, abi: '2' } - { goos: linux, goarch: riscv64, output: riscv64 } - { goos: linux, goarch: s390x, output: s390x } @@ -61,32 +62,49 @@ jobs: - { goos: android, goarch: arm, ndk: armv7a-linux-androideabi34, output: armv7 } - { goos: android, goarch: arm64, ndk: aarch64-linux-android34, output: arm64-v8 } - - { goos: windows, goarch: '386', output: '386-go120', version: 20 } - - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, version: 20 } - - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go120, version: 20 } + # Go 1.20 is the last release that will run on any release of Windows 7, 8, Server 2008 and Server 2012. Go 1.21 will require at least Windows 10 or Server 2016. + - { goos: windows, goarch: '386', output: '386-go120', goversion: '1.20' } + - { goos: windows, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } + - { goos: windows, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - - { goos: darwin, goarch: arm64, output: arm64-go120, version: 20 } - - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, version: 20 } - - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, version: 20 } + # Go 1.20 is the last release that will run on macOS 10.13 High Sierra or 10.14 Mojave. Go 1.21 will require macOS 10.15 Catalina or later. + - { goos: darwin, goarch: arm64, output: arm64-go120, goversion: '1.20' } + - { goos: darwin, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20' } + - { goos: darwin, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } - - { goos: linux, goarch: '386', output: '386-go120', version: 20 } - - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, version: 20 } - - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go120, version: 20 } + # only for test + - { goos: linux, goarch: '386', output: '386-go120', goversion: '1.20' } + - { goos: linux, goarch: amd64, goamd64: v1, output: amd64-compatible-go120, goversion: '1.20', test: test } + - { goos: linux, goarch: amd64, goamd64: v3, output: amd64-go120, goversion: '1.20' } steps: - uses: actions/checkout@v4 - - name: Set up Go1.22 - if: ${{ matrix.jobs.version != '20' }} + - name: Set up Go + if: ${{ matrix.jobs.goversion == '' && matrix.jobs.goarch != 'loong64' }} uses: actions/setup-go@v5 with: - go-version: ^1.22 + go-version: '1.22' - - name: Set up Go1.20 - if: ${{ matrix.jobs.version == '20' }} + - name: Set up Go + if: ${{ matrix.jobs.goversion != '' && matrix.jobs.goarch != 'loong64' }} uses: actions/setup-go@v5 with: - go-version: ^1.20 + go-version: ${{ matrix.jobs.goversion }} + + - name: Set up Go1.21 loongarch abi1 + if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '1' }} + run: | + wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.21.5/go1.21.5.linux-amd64-abi1.tar.gz + sudo tar zxf go1.21.5.linux-amd64-abi1.tar.gz -C /usr/local + echo "/usr/local/go/bin" >> $GITHUB_PATH + + - name: Set up Go1.21 loongarch abi2 + if: ${{ matrix.jobs.goarch == 'loong64' && matrix.jobs.abi == '2' }} + run: | + wget -q https://github.com/xishang0128/loongarch64-golang/releases/download/1.21.5/go1.21.5.linux-amd64-abi2.tar.gz + sudo tar zxf go1.21.5.linux-amd64-abi2.tar.gz -C /usr/local + echo "/usr/local/go/bin" >> $GITHUB_PATH - name: Set variables if: ${{github.ref_name=='Alpha'}} @@ -118,7 +136,12 @@ jobs: echo "CGO_ENABLED=1" >> $GITHUB_ENV echo "BUILDTAG=" >> $GITHUB_ENV - - name: build core + - name: Test + if: ${{ matrix.jobs.test == 'test' }} + run: | + go test ./... + + - name: Build core env: GOOS: ${{matrix.jobs.goos}} GOARCH: ${{matrix.jobs.goarch}} @@ -141,7 +164,11 @@ jobs: if: ${{ matrix.jobs.goos == 'linux' && !contains(matrix.jobs.goarch, 'mips') }} run: | sudo apt-get install dpkg - + if [ "${{matrix.jobs.abi}}" = "1" ]; then + ARCH=loongarch64 + else + ARCH=${{matrix.jobs.goarch}} + fi mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/DEBIAN mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/usr/bin mkdir -p mihomo-${{matrix.jobs.goos}}-${{matrix.jobs.output}}-${VERSION}/etc/mihomo @@ -162,7 +189,7 @@ jobs: Version: 1.18.2-${VERSION} Section: Priority: extra - Architecture: ${{matrix.jobs.goarch}} + Architecture: ${ARCH} Maintainer: MetaCubeX Homepage: https://wiki.metacubex.one/ Description: The universal proxy platform. @@ -177,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') }} - 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: | @@ -203,7 +230,6 @@ jobs: mihomo*.gz mihomo*.deb mihomo*.rpm - mihomo*.pkg.tar.zst mihomo*.zip version.txt diff --git a/adapter/inbound/listen.go b/adapter/inbound/listen.go index 8b7b5fb2..18dc1bc2 100644 --- a/adapter/inbound/listen.go +++ b/adapter/inbound/listen.go @@ -4,7 +4,7 @@ import ( "context" "net" - "github.com/sagernet/tfo-go" + "github.com/metacubex/tfo-go" ) var ( diff --git a/adapter/outbound/dns.go b/adapter/outbound/dns.go new file mode 100644 index 00000000..21a5b2b7 --- /dev/null +++ b/adapter/outbound/dns.go @@ -0,0 +1,159 @@ +package outbound + +import ( + "context" + "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" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" +) + +type Dns struct { + *Base +} + +type DnsOption struct { + BasicOption + Name string `proxy:"name"` +} + +// DialContext implements C.ProxyAdapter +func (d *Dns) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) { + left, right := N.Pipe() + go resolver.RelayDnsConn(context.Background(), right, 0) + return NewConn(left, d), nil +} + +// ListenPacketContext implements C.ProxyAdapter +func (d *Dns) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) { + log.Debugln("[DNS] hijack udp:%s from %s", metadata.RemoteAddress(), metadata.SourceAddrPort()) + + ctx, cancel := context.WithCancel(context.Background()) + + return newPacketConn(&dnsPacketConn{ + response: make(chan dnsPacket, 1), + ctx: ctx, + cancel: cancel, + }, d), nil +} + +type dnsPacket struct { + data []byte + put func() + addr net.Addr +} + +// dnsPacketConn implements net.PacketConn +type dnsPacketConn struct { + response chan dnsPacket + ctx context.Context + cancel context.CancelFunc +} + +func (d *dnsPacketConn) WaitReadFrom() (data []byte, put func(), addr net.Addr, err error) { + select { + case packet := <-d.response: + return packet.data, packet.put, packet.addr, nil + case <-d.ctx.Done(): + return nil, nil, nil, net.ErrClosed + } +} + +func (d *dnsPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + select { + case packet := <-d.response: + n = copy(p, packet.data) + if packet.put != nil { + packet.put() + } + return n, packet.addr, nil + case <-d.ctx.Done(): + return 0, nil, net.ErrClosed + } +} + +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) } + copy(buf, p) // avoid p be changed after WriteTo returned + + 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 { + d.cancel() + return nil +} + +func (*dnsPacketConn) LocalAddr() net.Addr { + return &net.UDPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 53, + Zone: "", + } +} + +func (*dnsPacketConn) SetDeadline(t time.Time) error { + return nil +} + +func (*dnsPacketConn) SetReadDeadline(t time.Time) error { + return nil +} + +func (*dnsPacketConn) SetWriteDeadline(t time.Time) error { + return nil +} + +func NewDnsWithOption(option DnsOption) *Dns { + return &Dns{ + Base: &Base{ + name: option.Name, + tp: C.Dns, + udp: true, + tfo: option.TFO, + mpTcp: option.MPTCP, + iface: option.Interface, + rmark: option.RoutingMark, + prefer: C.NewDNSPrefer(option.IPVersion), + }, + } +} diff --git a/adapter/outbound/hysteria2.go b/adapter/outbound/hysteria2.go index 47272ec8..0ad7c214 100644 --- a/adapter/outbound/hysteria2.go +++ b/adapter/outbound/hysteria2.go @@ -8,23 +8,30 @@ import ( "net" "runtime" "strconv" + "time" CN "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/common/utils" "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" M "github.com/sagernet/sing/common/metadata" + "github.com/zhangyunhao116/fastrand" ) func init() { hysteria2.SetCongestionController = tuicCommon.SetCongestionController } +const minHopInterval = 5 +const defaultHopInterval = 30 + type Hysteria2 struct { *Base @@ -37,7 +44,9 @@ type Hysteria2Option struct { BasicOption Name string `proxy:"name"` Server string `proxy:"server"` - Port int `proxy:"port"` + Port int `proxy:"port,omitempty"` + 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"` @@ -129,7 +138,7 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { clientOptions := hysteria2.ClientOptions{ Context: context.TODO(), Dialer: singDialer, - ServerAddress: M.ParseSocksaddrHostPort(option.Server, uint16(option.Port)), + Logger: log.SingLogger, SendBPS: StringToBps(option.Up), ReceiveBPS: StringToBps(option.Down), SalamanderPassword: salamanderPassword, @@ -138,6 +147,37 @@ func NewHysteria2(option Hysteria2Option) (*Hysteria2, error) { UDPDisabled: false, CWND: option.CWND, UdpMTU: option.UdpMTU, + ServerAddress: func(ctx context.Context) (*net.UDPAddr, error) { + return resolveUDPAddrWithPrefer(ctx, "udp", addr, C.NewDNSPrefer(option.IPVersion)) + }, + } + + var ranges utils.IntRanges[uint16] + var serverAddress []string + if option.Ports != "" { + ranges, err = utils.NewUnsignedRanges[uint16](option.Ports) + if err != nil { + return nil, err + } + ranges.Range(func(port uint16) bool { + serverAddress = append(serverAddress, net.JoinHostPort(option.Server, strconv.Itoa(int(port)))) + return true + }) + if len(serverAddress) > 0 { + clientOptions.ServerAddress = func(ctx context.Context) (*net.UDPAddr, error) { + return resolveUDPAddrWithPrefer(ctx, "udp", serverAddress[fastrand.Intn(len(serverAddress))], C.NewDNSPrefer(option.IPVersion)) + } + + if option.HopInterval == 0 { + option.HopInterval = defaultHopInterval + } else if option.HopInterval < minHopInterval { + option.HopInterval = minHopInterval + } + clientOptions.HopInterval = time.Duration(option.HopInterval) * time.Second + } + } + if option.Port == 0 && len(serverAddress) == 0 { + return nil, errors.New("invalid port") } client, err := hysteria2.NewClient(clientOptions) 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/outbound/vless.go b/adapter/outbound/vless.go index ceeb52a5..43b4aa21 100644 --- a/adapter/outbound/vless.go +++ b/adapter/outbound/vless.go @@ -373,7 +373,7 @@ func (v *Vless) ListenPacketOnStreamConn(ctx context.Context, c net.Conn, metada }, M.SocksaddrFromNet(metadata.UDPAddr())), ), v), nil } - return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil + return newPacketConn(N.NewThreadSafePacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}), v), nil } // SupportUOT implements C.ProxyAdapter diff --git a/adapter/outboundgroup/fallback.go b/adapter/outboundgroup/fallback.go index 4c8a2247..9387f7de 100644 --- a/adapter/outboundgroup/fallback.go +++ b/adapter/outboundgroup/fallback.go @@ -164,6 +164,8 @@ func NewFallback(option *GroupCommonOption, providers []provider.ProxyProvider) option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), disableUDP: option.DisableUDP, diff --git a/adapter/outboundgroup/groupbase.go b/adapter/outboundgroup/groupbase.go index 0ea3685b..b39ee3a6 100644 --- a/adapter/outboundgroup/groupbase.go +++ b/adapter/outboundgroup/groupbase.go @@ -31,14 +31,18 @@ type GroupBase struct { failedTesting atomic.Bool proxies [][]C.Proxy versions []atomic.Uint32 + TestTimeout int + maxFailedTimes int } type GroupBaseOption struct { outbound.BaseOption - filter string - excludeFilter string - excludeType string - providers []provider.ProxyProvider + filter string + excludeFilter string + excludeType string + TestTimeout int + maxFailedTimes int + providers []provider.ProxyProvider } func NewGroupBase(opt GroupBaseOption) *GroupBase { @@ -66,6 +70,15 @@ func NewGroupBase(opt GroupBaseOption) *GroupBase { excludeTypeArray: excludeTypeArray, providers: opt.providers, failedTesting: atomic.NewBool(false), + TestTimeout: opt.TestTimeout, + maxFailedTimes: opt.maxFailedTimes, + } + + if gb.TestTimeout == 0 { + gb.TestTimeout = 5000 + } + if gb.maxFailedTimes == 0 { + gb.maxFailedTimes = 5 } gb.proxies = make([][]C.Proxy, len(opt.providers)) @@ -240,13 +253,13 @@ func (gb *GroupBase) onDialFailed(adapterType C.AdapterType, err error) { log.Debugln("ProxyGroup: %s first failed", gb.Name()) gb.failedTime = time.Now() } else { - if time.Since(gb.failedTime) > gb.failedTimeoutInterval() { + if time.Since(gb.failedTime) > time.Duration(gb.TestTimeout)*time.Millisecond { gb.failedTimes = 0 return } log.Debugln("ProxyGroup: %s failed count: %d", gb.Name(), gb.failedTimes) - if gb.failedTimes >= gb.maxFailedTimes() { + if gb.failedTimes >= gb.maxFailedTimes { log.Warnln("because %s failed multiple times, active health check", gb.Name()) gb.healthCheck() } @@ -275,20 +288,8 @@ func (gb *GroupBase) healthCheck() { gb.failedTimes = 0 } -func (gb *GroupBase) failedIntervalTime() int64 { - return 5 * time.Second.Milliseconds() -} - func (gb *GroupBase) onDialSuccess() { if !gb.failedTesting.Load() { gb.failedTimes = 0 } } - -func (gb *GroupBase) maxFailedTimes() int { - return 5 -} - -func (gb *GroupBase) failedTimeoutInterval() time.Duration { - return 5 * time.Second -} diff --git a/adapter/outboundgroup/loadbalance.go b/adapter/outboundgroup/loadbalance.go index 976a2e89..4cb0db00 100644 --- a/adapter/outboundgroup/loadbalance.go +++ b/adapter/outboundgroup/loadbalance.go @@ -266,6 +266,8 @@ func NewLoadBalance(option *GroupCommonOption, providers []provider.ProxyProvide option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), strategyFn: strategyFn, diff --git a/adapter/outboundgroup/parser.go b/adapter/outboundgroup/parser.go index 96b23eb2..74947587 100644 --- a/adapter/outboundgroup/parser.go +++ b/adapter/outboundgroup/parser.go @@ -29,6 +29,7 @@ type GroupCommonOption struct { URL string `group:"url,omitempty"` Interval int `group:"interval,omitempty"` TestTimeout int `group:"timeout,omitempty"` + MaxFailedTimes int `group:"max-failed-times,omitempty"` Lazy bool `group:"lazy,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"` Filter string `group:"filter,omitempty"` diff --git a/adapter/outboundgroup/relay.go b/adapter/outboundgroup/relay.go index 6a8e8bb1..07fbcd95 100644 --- a/adapter/outboundgroup/relay.go +++ b/adapter/outboundgroup/relay.go @@ -160,6 +160,8 @@ func NewRelay(option *GroupCommonOption, providers []provider.ProxyProvider) *Re "", "", "", + 5000, + 5, providers, }), Hidden: option.Hidden, diff --git a/adapter/outboundgroup/selector.go b/adapter/outboundgroup/selector.go index 3ac740f4..20eca70f 100644 --- a/adapter/outboundgroup/selector.go +++ b/adapter/outboundgroup/selector.go @@ -114,6 +114,8 @@ func NewSelector(option *GroupCommonOption, providers []provider.ProxyProvider) option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), selected: "COMPATIBLE", diff --git a/adapter/outboundgroup/urltest.go b/adapter/outboundgroup/urltest.go index 8439772c..5da44f38 100644 --- a/adapter/outboundgroup/urltest.go +++ b/adapter/outboundgroup/urltest.go @@ -235,6 +235,8 @@ func NewURLTest(option *GroupCommonOption, providers []provider.ProxyProvider, o option.Filter, option.ExcludeFilter, option.ExcludeType, + option.TestTimeout, + option.MaxFailedTimes, providers, }), fastSingle: singledo.NewSingle[C.Proxy](time.Second * 10), diff --git a/adapter/parser.go b/adapter/parser.go index 1d363c1f..c64ee13a 100644 --- a/adapter/parser.go +++ b/adapter/parser.go @@ -120,6 +120,13 @@ func ParseProxy(mapping map[string]any) (C.Proxy, error) { break } proxy = outbound.NewDirectWithOption(*directOption) + case "dns": + dnsOptions := &outbound.DnsOption{} + err = decoder.Decode(mapping, dnsOptions) + if err != nil { + break + } + proxy = outbound.NewDnsWithOption(*dnsOptions) case "reject": rejectOption := &outbound.RejectOption{} err = decoder.Decode(mapping, rejectOption) @@ -127,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/android_tz.go b/android_tz.go new file mode 100644 index 00000000..82fc38e3 --- /dev/null +++ b/android_tz.go @@ -0,0 +1,21 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// kanged from https://github.com/golang/mobile/blob/c713f31d574bb632a93f169b2cc99c9e753fef0e/app/android.go#L89 + +package main + +// #include +import "C" +import "time" + +func init() { + var currentT C.time_t + var currentTM C.struct_tm + C.time(¤tT) + C.localtime_r(¤tT, ¤tTM) + tzOffset := int(currentTM.tm_gmtoff) + tz := C.GoString(currentTM.tm_zone) + time.Local = time.FixedZone(tz, tzOffset) +} 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/earlyconn.go b/common/net/earlyconn.go index c9a42819..82d392ee 100644 --- a/common/net/earlyconn.go +++ b/common/net/earlyconn.go @@ -3,10 +3,9 @@ package net import ( "net" "sync" - "sync/atomic" - "unsafe" "github.com/metacubex/mihomo/common/buf" + "github.com/metacubex/mihomo/common/once" ) type earlyConn struct { @@ -44,8 +43,7 @@ func (conn *earlyConn) Upstream() any { } func (conn *earlyConn) Success() bool { - // atomic visit sync.Once.done - return atomic.LoadUint32((*uint32)(unsafe.Pointer(&conn.resOnce))) == 1 && conn.resErr == nil + return once.Done(&conn.resOnce) && conn.resErr == nil } func (conn *earlyConn) ReaderReplaceable() bool { 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/common/once/once_go120.go b/common/once/once_go120.go new file mode 100644 index 00000000..51578a2d --- /dev/null +++ b/common/once/once_go120.go @@ -0,0 +1,26 @@ +//go:build !go1.22 + +package once + +import ( + "sync" + "sync/atomic" + "unsafe" +) + +type Once struct { + done uint32 + m sync.Mutex +} + +func Done(once *sync.Once) bool { + // atomic visit sync.Once.done + return atomic.LoadUint32((*uint32)(unsafe.Pointer(once))) == 1 +} + +func Reset(once *sync.Once) { + o := (*Once)(unsafe.Pointer(once)) + o.m.Lock() + defer o.m.Unlock() + atomic.StoreUint32(&o.done, 0) +} diff --git a/common/once/once_go122.go b/common/once/once_go122.go new file mode 100644 index 00000000..5ff8461d --- /dev/null +++ b/common/once/once_go122.go @@ -0,0 +1,26 @@ +//go:build go1.22 + +package once + +import ( + "sync" + "sync/atomic" + "unsafe" +) + +type Once struct { + done atomic.Uint32 + m sync.Mutex +} + +func Done(once *sync.Once) bool { + // atomic visit sync.Once.done + return (*atomic.Uint32)(unsafe.Pointer(once)).Load() == 1 +} + +func Reset(once *sync.Once) { + o := (*Once)(unsafe.Pointer(once)) + o.m.Lock() + defer o.m.Unlock() + o.done.Store(0) +} diff --git a/common/utils/ranges.go b/common/utils/ranges.go index 810105ff..e656e34b 100644 --- a/common/utils/ranges.go +++ b/common/utils/ranges.go @@ -20,6 +20,8 @@ func newIntRanges[T constraints.Integer](expected string, parseFn func(string) ( return nil, nil } + // support: 200,302 or 200,204,401-429,501-503 + expected = strings.ReplaceAll(expected, ",", "/") list := strings.Split(expected, "/") if len(list) > 28 { return nil, fmt.Errorf("%w, too many ranges to use, maximum support 28 ranges", errIntRanges) @@ -47,9 +49,9 @@ func newIntRangesFromList[T constraints.Integer](list []string, parseFn func(str } switch statusLen { - case 1: + case 1: // Port range ranges = append(ranges, NewRange(T(start), T(start))) - case 2: + case 2: // Single port end, err := parseFn(strings.Trim(status[1], "[ ]")) if err != nil { return nil, errIntRanges @@ -130,3 +132,13 @@ func (ranges IntRanges[T]) ToString() string { return strings.Join(terms, "/") } + +func (ranges IntRanges[T]) Range(f func(t T) bool) { + for _, r := range ranges { + for i := r.Start(); i <= r.End(); i++ { + if !f(i) { + return + } + } + } +} diff --git a/component/dialer/dialer.go b/component/dialer/dialer.go index 070d98b8..12e7c960 100644 --- a/component/dialer/dialer.go +++ b/component/dialer/dialer.go @@ -7,12 +7,14 @@ import ( "net" "net/netip" "os" + "strconv" "strings" "sync" "time" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/constant/features" + "github.com/metacubex/mihomo/log" ) const ( @@ -24,6 +26,7 @@ type dialFunc func(ctx context.Context, network string, ips []netip.Addr, port s var ( dialMux sync.Mutex + IP4PEnable bool actualSingleStackDialContext = serialSingleStackDialContext actualDualStackDialContext = serialDualStackDialContext tcpConcurrent = false @@ -128,7 +131,13 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po return dialContextHooked(ctx, network, destination, port) } - address := net.JoinHostPort(destination.String(), port) + var address string + if IP4PEnable { + NewDestination, NewPort := lookupIP4P(destination.String(), port) + address = net.JoinHostPort(NewDestination, NewPort) + } else { + address = net.JoinHostPort(destination.String(), port) + } netDialer := opt.netDialer switch netDialer.(type) { @@ -383,3 +392,21 @@ func NewDialer(options ...Option) Dialer { opt := applyOptions(options...) return Dialer{Opt: *opt} } + +func GetIP4PEnable(enableIP4PConvert bool) { + IP4PEnable = enableIP4PConvert +} + +// kanged from https://github.com/heiher/frp/blob/ip4p/client/ip4p.go + +func lookupIP4P(addr string, port string) (string, string) { + ip := net.ParseIP(addr) + if ip[0] == 0x20 && ip[1] == 0x01 && + ip[2] == 0x00 && ip[3] == 0x00 { + addr = net.IPv4(ip[12], ip[13], ip[14], ip[15]).String() + port = strconv.Itoa(int(ip[10])<<8 + int(ip[11])) + log.Debugln("Convert IP4P address %s to %s", ip, net.JoinHostPort(addr, port)) + return addr, port + } + return addr, port +} diff --git a/component/dialer/tfo.go b/component/dialer/tfo.go index 950bdfe4..228e9689 100644 --- a/component/dialer/tfo.go +++ b/component/dialer/tfo.go @@ -6,7 +6,7 @@ import ( "net" "time" - "github.com/sagernet/tfo-go" + "github.com/metacubex/tfo-go" ) type tfoConn struct { diff --git a/component/geodata/init.go b/component/geodata/init.go index 842efcc5..834567a4 100644 --- a/component/geodata/init.go +++ b/component/geodata/init.go @@ -14,8 +14,11 @@ import ( "github.com/metacubex/mihomo/log" ) -var initGeoSite bool -var initGeoIP int +var ( + initGeoSite bool + initGeoIP int + initASN bool +) func InitGeoSite() error { if _, err := os.Stat(C.Path.GeoSite()); os.IsNotExist(err) { @@ -113,7 +116,7 @@ func InitGeoIP() error { } if initGeoIP != 2 { - if !mmdb.Verify() { + if !mmdb.Verify(C.Path.MMDB()) { log.Warnln("MMDB invalid, remove and download") if err := os.Remove(C.Path.MMDB()); err != nil { return fmt.Errorf("can't remove invalid MMDB: %s", err.Error()) @@ -126,3 +129,27 @@ func InitGeoIP() error { } return nil } + +func InitASN() error { + if _, err := os.Stat(C.Path.ASN()); os.IsNotExist(err) { + log.Infoln("Can't find ASN.mmdb, start download") + if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + return fmt.Errorf("can't download ASN.mmdb: %s", err.Error()) + } + log.Infoln("Download ASN.mmdb finish") + initASN = false + } + if !initASN { + if !mmdb.Verify(C.Path.ASN()) { + log.Warnln("ASN invalid, remove and download") + if err := os.Remove(C.Path.ASN()); err != nil { + return fmt.Errorf("can't remove invalid ASN: %s", err.Error()) + } + if err := mmdb.DownloadASN(C.Path.ASN()); err != nil { + return fmt.Errorf("can't download ASN: %s", err.Error()) + } + } + initASN = true + } + return nil +} diff --git a/component/mmdb/mmdb.go b/component/mmdb/mmdb.go index 24edb5e5..81156bc6 100644 --- a/component/mmdb/mmdb.go +++ b/component/mmdb/mmdb.go @@ -8,6 +8,7 @@ import ( "sync" "time" + mihomoOnce "github.com/metacubex/mihomo/common/once" mihomoHttp "github.com/metacubex/mihomo/component/http" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" @@ -24,56 +25,58 @@ const ( ) var ( - reader Reader - once sync.Once + IPreader IPReader + ASNreader ASNReader + IPonce sync.Once + ASNonce sync.Once ) func LoadFromBytes(buffer []byte) { - once.Do(func() { + IPonce.Do(func() { mmdb, err := maxminddb.FromBytes(buffer) if err != nil { log.Fatalln("Can't load mmdb: %s", err.Error()) } - reader = Reader{Reader: mmdb} + IPreader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } }) } -func Verify() bool { - instance, err := maxminddb.Open(C.Path.MMDB()) +func Verify(path string) bool { + instance, err := maxminddb.Open(path) if err == nil { instance.Close() } return err == nil } -func Instance() Reader { - once.Do(func() { +func IPInstance() IPReader { + IPonce.Do(func() { mmdbPath := C.Path.MMDB() log.Infoln("Load MMDB file: %s", mmdbPath) mmdb, err := maxminddb.Open(mmdbPath) if err != nil { log.Fatalln("Can't load MMDB: %s", err.Error()) } - reader = Reader{Reader: mmdb} + IPreader = IPReader{Reader: mmdb} switch mmdb.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } }) - return reader + return IPreader } func DownloadMMDB(path string) (err error) { @@ -95,6 +98,43 @@ func DownloadMMDB(path string) (err error) { return err } -func Reload() { - once = sync.Once{} +func ASNInstance() ASNReader { + ASNonce.Do(func() { + ASNPath := C.Path.ASN() + log.Infoln("Load ASN file: %s", ASNPath) + asn, err := maxminddb.Open(ASNPath) + if err != nil { + log.Fatalln("Can't load ASN: %s", err.Error()) + } + ASNreader = ASNReader{Reader: asn} + }) + + return ASNreader +} + +func DownloadASN(path string) (err error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*90) + defer cancel() + resp, err := mihomoHttp.HttpRequest(ctx, C.ASNUrl, http.MethodGet, http.Header{"User-Agent": {C.UA}}, nil) + if err != nil { + return + } + defer resp.Body.Close() + + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(f, resp.Body) + + return err +} + +func ReloadIP() { + mihomoOnce.Reset(&IPonce) +} + +func ReloadASN() { + mihomoOnce.Reset(&ASNonce) } diff --git a/component/mmdb/patch_android.go b/component/mmdb/patch_android.go index a994b75e..147a3324 100644 --- a/component/mmdb/patch_android.go +++ b/component/mmdb/patch_android.go @@ -5,14 +5,14 @@ package mmdb import "github.com/oschwald/maxminddb-golang" func InstallOverride(override *maxminddb.Reader) { - newReader := Reader{Reader: override} + newReader := IPReader{Reader: override} switch override.Metadata.DatabaseType { case "sing-geoip": - reader.databaseType = typeSing + IPreader.databaseType = typeSing case "Meta-geoip0": - reader.databaseType = typeMetaV0 + IPreader.databaseType = typeMetaV0 default: - reader.databaseType = typeMaxmind + IPreader.databaseType = typeMaxmind } - reader = newReader + IPreader = newReader } diff --git a/component/mmdb/reader.go b/component/mmdb/reader.go index 4db53d4f..e76e9939 100644 --- a/component/mmdb/reader.go +++ b/component/mmdb/reader.go @@ -3,9 +3,9 @@ package mmdb import ( "fmt" "net" + "strings" "github.com/oschwald/maxminddb-golang" - "github.com/sagernet/sing/common" ) type geoip2Country struct { @@ -14,12 +14,21 @@ type geoip2Country struct { } `maxminddb:"country"` } -type Reader struct { +type IPReader struct { *maxminddb.Reader databaseType } -func (r Reader) LookupCode(ipAddress net.IP) []string { +type ASNReader struct { + *maxminddb.Reader +} + +type ASNResult struct { + AutonomousSystemNumber uint32 `maxminddb:"autonomous_system_number"` + AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` +} + +func (r IPReader) LookupCode(ipAddress net.IP) []string { switch r.databaseType { case typeMaxmind: var country geoip2Country @@ -27,7 +36,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 @@ -44,9 +53,11 @@ func (r Reader) LookupCode(ipAddress net.IP) []string { case string: return []string{record} case []any: // lookup returned type of slice is []any - return common.Map(record, func(it any) string { - return it.(string) - }) + result := make([]string, 0, len(record)) + for _, item := range record { + result = append(result, item.(string)) + } + return result } return []string{} @@ -54,3 +65,9 @@ func (r Reader) LookupCode(ipAddress net.IP) []string { panic(fmt.Sprint("unknown geoip database type:", r.databaseType)) } } + +func (r ASNReader) LookupASN(ip net.IP) ASNResult { + var result ASNResult + r.Lookup(ip, &result) + return result +} diff --git a/component/resolver/relay.go b/component/resolver/relay.go new file mode 100644 index 00000000..27b25af1 --- /dev/null +++ b/component/resolver/relay.go @@ -0,0 +1,88 @@ +package resolver + +import ( + "context" + "encoding/binary" + "io" + "net" + "time" + + "github.com/metacubex/mihomo/common/pool" + + D "github.com/miekg/dns" +) + +const DefaultDnsReadTimeout = time.Second * 10 +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, readTimeout time.Duration) error { + buff := pool.Get(pool.UDPBufferSize) + defer func() { + _ = pool.Put(buff) + _ = conn.Close() + }() + for { + if readTimeout > 0 { + _ = conn.SetReadDeadline(time.Now().Add(readTimeout)) + } + + length := uint16(0) + if err := binary.Read(conn, binary.BigEndian, &length); err != nil { + break + } + + if int(length) > len(buff) { + break + } + + n, err := io.ReadFull(conn, buff[:length]) + if err != nil { + break + } + + err = func() error { + ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) + defer cancel() + inData := buff[:n] + msg, err := RelayDnsPacket(ctx, inData, buff) + if err != nil { + return err + } + + err = binary.Write(conn, binary.BigEndian, uint16(len(msg))) + if err != nil { + return err + } + + _, err = conn.Write(msg) + if err != nil { + return err + } + return nil + }() + if err != nil { + return err + } + } + return nil +} + +func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { + msg := &D.Msg{} + if err := msg.Unpack(payload); err != nil { + return nil, err + } + + r, err := ServeMsg(ctx, msg) + if err != nil { + m := new(D.Msg) + m.SetRcode(msg, D.RcodeServerFailure) + return m.PackBuffer(target) + } + + r.SetRcode(msg, r.Rcode) + r.Compress = true + return r.PackBuffer(target) +} diff --git a/config/config.go b/config/config.go index c5f4bb77..ca866491 100644 --- a/config/config.go +++ b/config/config.go @@ -152,6 +152,7 @@ type IPTables struct { Enable bool `yaml:"enable" json:"enable"` InboundInterface string `yaml:"inbound-interface" json:"inbound-interface"` Bypass []string `yaml:"bypass" json:"bypass"` + DnsRedirect bool `yaml:"dns-redirect" json:"dns-redirect"` } type Sniffer struct { @@ -168,6 +169,7 @@ type Experimental struct { Fingerprints []string `yaml:"fingerprints"` QUICGoDisableGSO bool `yaml:"quic-go-disable-gso"` QUICGoDisableECN bool `yaml:"quic-go-disable-ecn"` + IP4PEnable bool `yaml:"dialer-ip4p-convert"` } // Config is mihomo config manager @@ -346,6 +348,7 @@ type RawConfig struct { type GeoXUrl struct { GeoIp string `yaml:"geoip" json:"geoip"` Mmdb string `yaml:"mmdb" json:"mmdb"` + ASN string `yaml:"asn" json:"asn"` GeoSite string `yaml:"geosite" json:"geosite"` } @@ -440,6 +443,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { Enable: false, InboundInterface: "lo", Bypass: []string{}, + DnsRedirect: true, }, NTP: RawNTP{ Enable: false, @@ -492,6 +496,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { }, GeoXUrl: GeoXUrl{ Mmdb: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb", + ASN: "https://github.com/xishang0128/geoip/releases/download/latest/GeoLite2-ASN.mmdb", GeoIp: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat", GeoSite: "https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat", }, @@ -617,6 +622,7 @@ func parseGeneral(cfg *RawConfig) (*General, error) { C.GeoIpUrl = cfg.GeoXUrl.GeoIp C.GeoSiteUrl = cfg.GeoXUrl.GeoSite C.MmdbUrl = cfg.GeoXUrl.Mmdb + C.ASNUrl = cfg.GeoXUrl.ASN C.GeodataMode = cfg.GeodataMode C.UA = cfg.GlobalUA if cfg.KeepAliveInterval != 0 { diff --git a/config/update_geo.go b/config/update_geo.go index bf3d0810..43cac25c 100644 --- a/config/update_geo.go +++ b/config/update_geo.go @@ -34,7 +34,7 @@ func UpdateGeoDatabases() error { } } else { - defer mmdb.Reload() + defer mmdb.ReloadIP() data, err := downloadForBytes(C.MmdbUrl) if err != nil { return fmt.Errorf("can't download MMDB database file: %w", err) @@ -46,12 +46,31 @@ func UpdateGeoDatabases() error { } _ = instance.Close() - mmdb.Instance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file + mmdb.IPInstance().Reader.Close() // mmdb is loaded with mmap, so it needs to be closed before overwriting the file if err = saveFile(data, C.Path.MMDB()); err != nil { return fmt.Errorf("can't save MMDB database file: %w", err) } } + if C.ASNEnable { + defer mmdb.ReloadASN() + data, err := downloadForBytes(C.ASNUrl) + if err != nil { + return fmt.Errorf("can't download ASN database file: %w", err) + } + + instance, err := maxminddb.FromBytes(data) + if err != nil { + return fmt.Errorf("invalid ASN database file: %s", err) + } + _ = instance.Close() + + mmdb.ASNInstance().Reader.Close() + if err = saveFile(data, C.Path.ASN()); err != nil { + return fmt.Errorf("can't save ASN database file: %w", err) + } + } + data, err := downloadForBytes(C.GeoSiteUrl) if err != nil { return fmt.Errorf("can't download GeoSite database file: %w", err) diff --git a/constant/adapters.go b/constant/adapters.go index 83c8223a..cb213b3c 100644 --- a/constant/adapters.go +++ b/constant/adapters.go @@ -21,6 +21,7 @@ const ( RejectDrop Compatible Pass + Dns Relay Selector @@ -40,6 +41,7 @@ const ( Hysteria2 WireGuard Tuic + Ssh ) const ( @@ -184,6 +186,8 @@ func (at AdapterType) String() string { return "Compatible" case Pass: return "Pass" + case Dns: + return "Dns" case Shadowsocks: return "Shadowsocks" case ShadowsocksR: @@ -219,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/geodata.go b/constant/geodata.go index e93d56b3..cd3f74e3 100644 --- a/constant/geodata.go +++ b/constant/geodata.go @@ -1,10 +1,12 @@ package constant var ( + ASNEnable bool GeodataMode bool GeoAutoUpdate bool GeoUpdateInterval int GeoIpUrl string MmdbUrl string GeoSiteUrl string + ASNUrl string ) diff --git a/constant/metadata.go b/constant/metadata.go index 6df6ff43..381e2dd4 100644 --- a/constant/metadata.go +++ b/constant/metadata.go @@ -133,6 +133,8 @@ type Metadata struct { Type Type `json:"type"` SrcIP netip.Addr `json:"sourceIP"` DstIP netip.Addr `json:"destinationIP"` + DstGeoIP []string `json:"destinationGeoIP"` // can be nil if never queried, empty slice if got no result + DstIPASN string `json:"destinationIPASN"` SrcPort uint16 `json:"sourcePort,string"` // `,string` is used to compatible with old version json output DstPort uint16 `json:"destinationPort,string"` // `,string` is used to compatible with old version json output InIP netip.Addr `json:"inboundIP"` diff --git a/constant/path.go b/constant/path.go index a920fbbc..77f7d0ef 100644 --- a/constant/path.go +++ b/constant/path.go @@ -15,6 +15,7 @@ const Name = "mihomo" var ( GeositeName = "GeoSite.dat" GeoipName = "GeoIP.dat" + ASNName = "ASN.mmdb" ) // Path is used to get the configuration path @@ -112,6 +113,25 @@ func (p *path) MMDB() string { return P.Join(p.homeDir, "geoip.metadb") } +func (p *path) ASN() string { + files, err := os.ReadDir(p.homeDir) + if err != nil { + return "" + } + for _, fi := range files { + if fi.IsDir() { + // 目录则直接跳过 + continue + } else { + if strings.EqualFold(fi.Name(), "ASN.mmdb") { + ASNName = fi.Name() + return P.Join(p.homeDir, fi.Name()) + } + } + } + return P.Join(p.homeDir, ASNName) +} + func (p *path) OldCache() string { return P.Join(p.homeDir, ".cache") } diff --git a/constant/rule.go b/constant/rule.go index 66fc18bb..fcefaba6 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -5,9 +5,11 @@ const ( Domain RuleType = iota DomainSuffix DomainKeyword + DomainRegex GEOSITE GEOIP IPCIDR + IPASN SrcIPCIDR IPSuffix SrcIPSuffix @@ -40,12 +42,16 @@ func (rt RuleType) String() string { return "DomainSuffix" case DomainKeyword: return "DomainKeyword" + case DomainRegex: + return "DomainRegex" case GEOSITE: return "GeoSite" case GEOIP: return "GeoIP" case IPCIDR: return "IPCIDR" + case IPASN: + return "IPASN" case SrcIPCIDR: return "SrcIPCIDR" case IPSuffix: diff --git a/dns/filters.go b/dns/filters.go index f183f11f..501acd7c 100644 --- a/dns/filters.go +++ b/dns/filters.go @@ -25,7 +25,7 @@ var geoIPMatcher *router.GeoIPMatcher func (gf *geoipFilter) Match(ip netip.Addr) bool { if !C.GeodataMode { - codes := mmdb.Instance().LookupCode(ip.AsSlice()) + codes := mmdb.IPInstance().LookupCode(ip.AsSlice()) for _, code := range codes { if !strings.EqualFold(code, gf.code) && !nnip.IsPrivateIP(ip) { return true diff --git a/docs/config.yaml b/docs/config.yaml index dc257ee2..d912eb65 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -674,6 +674,8 @@ proxies: # socks5 type: hysteria2 server: server.com port: 443 + # ports: 1000,2000-3000,5000 # port 不可省略 + # hop-interval: 15 # up和down均不写或为0则使用BBR流控 # up: "30 Mbps" # 若不写单位,默认为 Mbps # down: "200 Mbps" # 若不写单位,默认为 Mbps @@ -767,6 +769,18 @@ proxies: # socks5 # protocol-param: "#" # udp: true + - name: "ssh-out" + type: ssh + + server: 127.0.0.1 + port: 22 + username: root + password: password + privateKey: path + +# dns出站会将请求劫持到内部dns模块,所有请求均在内部处理 + - name: "dns-out" + type: dns proxy-groups: # 代理链,目前relay可以支持udp的只有vmess/vless/trojan/ss/ssr/tuic # wireguard目前不支持在relay中使用,请使用proxy中的dialer-proxy配置项 @@ -885,6 +899,8 @@ rule-providers: type: file rules: - RULE-SET,rule1,REJECT + - IP-ASN,1,PROXY + - DOMAIN-REGEX,^abc,DIRECT - DOMAIN-SUFFIX,baidu.com,DIRECT - DOMAIN-KEYWORD,google,ss1 - IP-CIDR,1.1.1.1/32,ss1 diff --git a/go.mod b/go.mod index bba88573..cdc2c8cb 100644 --- a/go.mod +++ b/go.mod @@ -19,13 +19,14 @@ 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-20240310154810-47bca850fc01 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 github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232 + github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 github.com/miekg/dns v1.1.57 github.com/mroth/weightedrand/v2 v2.1.0 github.com/openacid/low v0.1.21 @@ -36,7 +37,6 @@ require ( github.com/sagernet/sing v0.3.0 github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 github.com/sagernet/sing-shadowtls v0.1.4 - github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 github.com/sagernet/utls v1.5.4 github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e github.com/samber/lo v1.39.0 diff --git a/go.sum b/go.sum index 4e41bf44..fe671108 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-20240310154810-47bca850fc01 h1:5INHs85Gp1JZsdF7fQp1pXUjfJOX2dhwZjuUQWJVSt8= +github.com/metacubex/sing-quic v0.0.0-20240310154810-47bca850fc01/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= @@ -120,6 +120,8 @@ github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbT github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY= github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232 h1:loWjR+k9dxqBSgruGyT5hE8UCRMmCEjxqZbryfY9no4= github.com/metacubex/sing-wireguard v0.0.0-20231209125515-0594297f7232/go.mod h1:NGCrBZ+fUmp81yaA1kVskcNWBnwl5z4UHxz47A01zm8= +github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c= +github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU= @@ -161,8 +163,6 @@ github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnV github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 h1:z3SJQhVyU63FT26Wn/UByW6b7q8QKB0ZkPqsyqcz2PI= -github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6/go.mod h1:73xRZuxwkFk4aiLw28hG8W6o9cr2UPrGL9pdY2UTbvY= github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co= github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s= github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo= @@ -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/hub/executor/executor.go b/hub/executor/executor.go index e4a31a79..c23eb9f3 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -197,6 +197,7 @@ func updateExperimental(c *config.Config) { if c.Experimental.QUICGoDisableECN { _ = os.Setenv("QUIC_GO_DISABLE_ECN", strconv.FormatBool(true)) } + dialer.GetIP4PEnable(c.Experimental.IP4PEnable) } func updateNTP(c *config.NTP) { @@ -478,6 +479,9 @@ func updateIPTables(cfg *config.Config) { bypass = iptables.Bypass tProxyPort = cfg.General.TProxyPort dnsCfg = cfg.DNS + DnsRedirect = iptables.DnsRedirect + + dnsPort netip.AddrPort ) if tProxyPort == 0 { @@ -485,15 +489,17 @@ func updateIPTables(cfg *config.Config) { return } - if !dnsCfg.Enable { - err = fmt.Errorf("DNS server must be enable") - return - } + if DnsRedirect { + if !dnsCfg.Enable { + err = fmt.Errorf("DNS server must be enable") + return + } - dnsPort, err := netip.ParseAddrPort(dnsCfg.Listen) - if err != nil { - err = fmt.Errorf("DNS server must be correct") - return + dnsPort, err = netip.ParseAddrPort(dnsCfg.Listen) + if err != nil { + err = fmt.Errorf("DNS server must be correct") + return + } } if iptables.InboundInterface != "" { @@ -504,7 +510,7 @@ func updateIPTables(cfg *config.Config) { dialer.DefaultRoutingMark.Store(2158) } - err = tproxy.SetTProxyIPTables(inboundInterface, bypass, uint16(tProxyPort), dnsPort.Port()) + err = tproxy.SetTProxyIPTables(inboundInterface, bypass, uint16(tProxyPort), DnsRedirect, dnsPort.Port()) if err != nil { return } diff --git a/hub/updater/updater.go b/hub/updater/updater.go index 1967af42..02ff07ba 100644 --- a/hub/updater/updater.go +++ b/hub/updater/updater.go @@ -137,6 +137,8 @@ func prepare(exePath string) (err error) { if runtime.GOOS == "windows" { updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible + ".exe" + } else if runtime.GOOS == "android" && runtime.GOARCH == "arm64" { + updateExeName = "mihomo-android-arm64-v8" } else { updateExeName = "mihomo" + "-" + runtime.GOOS + "-" + runtime.GOARCH + amd64Compatible } @@ -440,7 +442,11 @@ func updateDownloadURL() { middle = fmt.Sprintf("-%s-%s%s-%s", runtime.GOOS, runtime.GOARCH, goarm, latestVersion) } else if runtime.GOARCH == "arm64" { //-linux-arm64-alpha-e552b54.gz - middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion) + if runtime.GOOS == "android" { + middle = fmt.Sprintf("-%s-%s-v8-%s", runtime.GOOS, runtime.GOARCH, latestVersion) + } else { + middle = fmt.Sprintf("-%s-%s-%s", runtime.GOOS, runtime.GOARCH, latestVersion) + } } else if isMIPS(runtime.GOARCH) && gomips != "" { middle = fmt.Sprintf("-%s-%s-%s-%s", runtime.GOOS, runtime.GOARCH, gomips, latestVersion) } else { diff --git a/listener/sing_tun/dns.go b/listener/sing_tun/dns.go index 056c9169..42926732 100644 --- a/listener/sing_tun/dns.go +++ b/listener/sing_tun/dns.go @@ -2,29 +2,21 @@ package sing_tun import ( "context" - "encoding/binary" - "io" "net" "net/netip" "sync" "time" - "github.com/metacubex/mihomo/common/pool" "github.com/metacubex/mihomo/component/resolver" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" - D "github.com/miekg/dns" - "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/network" ) -const DefaultDnsReadTimeout = time.Second * 10 -const DefaultDnsRelayTimeout = time.Second * 5 - type ListenerHandler struct { *sing.ListenerHandler DnsAdds []netip.AddrPort @@ -45,61 +37,11 @@ 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()) - buff := pool.Get(pool.UDPBufferSize) - defer func() { - _ = pool.Put(buff) - _ = conn.Close() - }() - for { - if conn.SetReadDeadline(time.Now().Add(DefaultDnsReadTimeout)) != nil { - break - } - - length := uint16(0) - if err := binary.Read(conn, binary.BigEndian, &length); err != nil { - break - } - - if int(length) > len(buff) { - break - } - - n, err := io.ReadFull(conn, buff[:length]) - if err != nil { - break - } - - err = func() error { - ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) - defer cancel() - inData := buff[:n] - msg, err := RelayDnsPacket(ctx, inData, buff) - if err != nil { - return err - } - - err = binary.Write(conn, binary.BigEndian, uint16(len(msg))) - if err != nil { - return err - } - - _, err = conn.Write(msg) - if err != nil { - return err - } - return nil - }() - if err != nil { - return err - } - } - return nil + return resolver.RelayDnsConn(ctx, conn, resolver.DefaultDnsReadTimeout) } return h.ListenerHandler.NewConnection(ctx, conn, metadata) } -const SafeDnsPacketSize = 2 * 1024 // safe size which is 1232 from https://dnsflagday.net/2020/, so 2048 is enough - func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network.PacketConn, metadata M.Metadata) error { if h.ShouldHijackDns(metadata.Destination.AddrPort()) { log.Debugln("[DNS] hijack udp:%s from %s", metadata.Destination.String(), metadata.Source.String()) @@ -114,7 +56,7 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. rwOptions := network.ReadWaitOptions{ FrontHeadroom: network.CalculateFrontHeadroom(conn), RearHeadroom: network.CalculateRearHeadroom(conn), - MTU: SafeDnsPacketSize, + MTU: resolver.SafeDnsPacketSize, } readWaiter, isReadWaiter := bufio.CreatePacketReadWaiter(conn) if isReadWaiter { @@ -126,7 +68,7 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. dest M.Socksaddr err error ) - _ = conn.SetReadDeadline(time.Now().Add(DefaultDnsReadTimeout)) + _ = conn.SetReadDeadline(time.Now().Add(resolver.DefaultDnsReadTimeout)) readBuff = nil // clear last loop status, avoid repeat release if isReadWaiter { readBuff, dest, err = readWaiter.WaitReadPacket() @@ -147,15 +89,15 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. return err } go func() { - ctx, cancel := context.WithTimeout(ctx, DefaultDnsRelayTimeout) + ctx, cancel := context.WithTimeout(ctx, resolver.DefaultDnsRelayTimeout) defer cancel() inData := readBuff.Bytes() writeBuff := readBuff writeBuff.Resize(writeBuff.Start(), 0) - if len(writeBuff.FreeBytes()) < SafeDnsPacketSize { // only create a new buffer when space don't enough + if len(writeBuff.FreeBytes()) < resolver.SafeDnsPacketSize { // only create a new buffer when space don't enough writeBuff = rwOptions.NewPacketBuffer() } - msg, err := RelayDnsPacket(ctx, inData, writeBuff.FreeBytes()) + msg, err := resolver.RelayDnsPacket(ctx, inData, writeBuff.FreeBytes()) if writeBuff != readBuff { readBuff.Release() } @@ -182,21 +124,3 @@ func (h *ListenerHandler) NewPacketConnection(ctx context.Context, conn network. } return h.ListenerHandler.NewPacketConnection(ctx, conn, metadata) } - -func RelayDnsPacket(ctx context.Context, payload []byte, target []byte) ([]byte, error) { - msg := &D.Msg{} - if err := msg.Unpack(payload); err != nil { - return nil, err - } - - r, err := resolver.ServeMsg(ctx, msg) - if err != nil { - m := new(D.Msg) - m.SetRcode(msg, D.RcodeServerFailure) - return m.PackBuffer(target) - } - - r.SetRcode(msg, r.Rcode) - r.Compress = true - return r.PackBuffer(target) -} diff --git a/listener/tproxy/tproxy_iptables.go b/listener/tproxy/tproxy_iptables.go index 5ddd7b4c..6c6e2cc8 100644 --- a/listener/tproxy/tproxy_iptables.go +++ b/listener/tproxy/tproxy_iptables.go @@ -15,6 +15,7 @@ var ( dnsPort uint16 tProxyPort uint16 interfaceName string + DnsRedirect bool ) const ( @@ -22,7 +23,7 @@ const ( PROXY_ROUTE_TABLE = "0x2d0" ) -func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint16) error { +func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dnsredir bool, dport uint16) error { if _, err := cmd.ExecCmd("iptables -V"); err != nil { return fmt.Errorf("current operations system [%s] are not support iptables or command iptables does not exist", runtime.GOOS) } @@ -33,6 +34,7 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 interfaceName = ifname tProxyPort = tport + DnsRedirect = dnsredir dnsPort = dport // add route @@ -58,8 +60,10 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd("iptables -t mangle -N mihomo_prerouting") execCmd("iptables -t mangle -F mihomo_prerouting") execCmd("iptables -t mangle -A mihomo_prerouting -s 172.17.0.0/16 -j RETURN") - execCmd("iptables -t mangle -A mihomo_prerouting -p udp --dport 53 -j ACCEPT") - execCmd("iptables -t mangle -A mihomo_prerouting -p tcp --dport 53 -j ACCEPT") + if DnsRedirect { + execCmd("iptables -t mangle -A mihomo_prerouting -p udp --dport 53 -j ACCEPT") + execCmd("iptables -t mangle -A mihomo_prerouting -p tcp --dport 53 -j ACCEPT") + } execCmd("iptables -t mangle -A mihomo_prerouting -m addrtype --dst-type LOCAL -j RETURN") addLocalnetworkToChain("mihomo_prerouting", bypass) execCmd("iptables -t mangle -A mihomo_prerouting -p tcp -m socket -j mihomo_divert") @@ -68,8 +72,10 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_prerouting -p udp -j TPROXY --on-port %d --tproxy-mark %s/%s", tProxyPort, PROXY_FWMARK, PROXY_FWMARK)) execCmd("iptables -t mangle -A PREROUTING -j mihomo_prerouting") - execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + if DnsRedirect { + execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) + execCmd(fmt.Sprintf("iptables -t nat -I PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + } // set post routing if interfaceName != "lo" { @@ -80,8 +86,10 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd("iptables -t mangle -N mihomo_output") execCmd("iptables -t mangle -F mihomo_output") execCmd(fmt.Sprintf("iptables -t mangle -A mihomo_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - execCmd("iptables -t mangle -A mihomo_output -p udp -m multiport --dports 53,123,137 -j ACCEPT") - execCmd("iptables -t mangle -A mihomo_output -p tcp --dport 53 -j ACCEPT") + if DnsRedirect { + execCmd("iptables -t mangle -A mihomo_output -p udp -m multiport --dports 53,123,137 -j ACCEPT") + execCmd("iptables -t mangle -A mihomo_output -p tcp --dport 53 -j ACCEPT") + } execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type LOCAL -j RETURN") execCmd("iptables -t mangle -A mihomo_output -m addrtype --dst-type BROADCAST -j RETURN") addLocalnetworkToChain("mihomo_output", bypass) @@ -90,20 +98,22 @@ func SetTProxyIPTables(ifname string, bypass []string, tport uint16, dport uint1 execCmd(fmt.Sprintf("iptables -t mangle -I OUTPUT -o %s -j mihomo_output", interfaceName)) // set dns output - execCmd("iptables -t nat -N mihomo_dns_output") - execCmd("iptables -t nat -F mihomo_dns_output") - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) - execCmd("iptables -t nat -A mihomo_dns_output -s 172.17.0.0/16 -j RETURN") - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p udp -j REDIRECT --to-ports %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p tcp -j REDIRECT --to-ports %d", dnsPort)) - execCmd("iptables -t nat -I OUTPUT -p tcp --dport 53 -j mihomo_dns_output") - execCmd("iptables -t nat -I OUTPUT -p udp --dport 53 -j mihomo_dns_output") + if DnsRedirect { + execCmd("iptables -t nat -N mihomo_dns_output") + execCmd("iptables -t nat -F mihomo_dns_output") + execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -m mark --mark %#x -j RETURN", dialer.DefaultRoutingMark.Load())) + execCmd("iptables -t nat -A mihomo_dns_output -s 172.17.0.0/16 -j RETURN") + execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p udp -j REDIRECT --to-ports %d", dnsPort)) + execCmd(fmt.Sprintf("iptables -t nat -A mihomo_dns_output -p tcp -j REDIRECT --to-ports %d", dnsPort)) + execCmd("iptables -t nat -I OUTPUT -p tcp --dport 53 -j mihomo_dns_output") + execCmd("iptables -t nat -I OUTPUT -p udp --dport 53 -j mihomo_dns_output") + } return nil } func CleanupTProxyIPTables() { - if runtime.GOOS != "linux" || interfaceName == "" || tProxyPort == 0 || dnsPort == 0 { + if runtime.GOOS != "linux" || interfaceName == "" || tProxyPort == 0 { return } @@ -130,8 +140,10 @@ func CleanupTProxyIPTables() { } // clean PREROUTING - execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) - execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + if DnsRedirect { + execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p tcp --dport 53 -j REDIRECT --to %d", dnsPort)) + execCmd(fmt.Sprintf("iptables -t nat -D PREROUTING ! -s 172.17.0.0/16 ! -d 127.0.0.0/8 -p udp --dport 53 -j REDIRECT --to %d", dnsPort)) + } execCmd("iptables -t mangle -D PREROUTING -j mihomo_prerouting") // clean POSTROUTING @@ -141,8 +153,10 @@ func CleanupTProxyIPTables() { // clean OUTPUT execCmd(fmt.Sprintf("iptables -t mangle -D OUTPUT -o %s -j mihomo_output", interfaceName)) - execCmd("iptables -t nat -D OUTPUT -p tcp --dport 53 -j mihomo_dns_output") - execCmd("iptables -t nat -D OUTPUT -p udp --dport 53 -j mihomo_dns_output") + if DnsRedirect { + execCmd("iptables -t nat -D OUTPUT -p tcp --dport 53 -j mihomo_dns_output") + execCmd("iptables -t nat -D OUTPUT -p udp --dport 53 -j mihomo_dns_output") + } // clean chain execCmd("iptables -t mangle -F mihomo_prerouting") @@ -151,9 +165,10 @@ func CleanupTProxyIPTables() { execCmd("iptables -t mangle -X mihomo_divert") execCmd("iptables -t mangle -F mihomo_output") execCmd("iptables -t mangle -X mihomo_output") - execCmd("iptables -t nat -F mihomo_dns_output") - execCmd("iptables -t nat -X mihomo_dns_output") - + if DnsRedirect { + execCmd("iptables -t nat -F mihomo_dns_output") + execCmd("iptables -t nat -X mihomo_dns_output") + } interfaceName = "" tProxyPort = 0 dnsPort = 0 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/common/geoip.go b/rules/common/geoip.go index fe9a1b91..223a7904 100644 --- a/rules/common/geoip.go +++ b/rules/common/geoip.go @@ -4,7 +4,6 @@ import ( "fmt" "strings" - "github.com/metacubex/mihomo/common/nnip" "github.com/metacubex/mihomo/component/geodata" "github.com/metacubex/mihomo/component/geodata/router" "github.com/metacubex/mihomo/component/mmdb" @@ -22,6 +21,8 @@ type GEOIP struct { recodeSize int } +var _ C.Rule = (*GEOIP)(nil) + func (g *GEOIP) RuleType() C.RuleType { return C.GEOIP } @@ -32,24 +33,39 @@ func (g *GEOIP) Match(metadata *C.Metadata) (bool, string) { return false, "" } - if strings.EqualFold(g.country, "LAN") { - return nnip.IsPrivateIP(ip) || + if g.country == "lan" { + return ip.IsPrivate() || ip.IsUnspecified() || ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || resolver.IsFakeBroadcastIP(ip), g.adapter } + + for _, code := range metadata.DstGeoIP { + if g.country == code { + return true, g.adapter + } + } + if !C.GeodataMode { - codes := mmdb.Instance().LookupCode(ip.AsSlice()) - for _, code := range codes { - if strings.EqualFold(code, g.country) { + if metadata.DstGeoIP != nil { + return false, g.adapter + } + metadata.DstGeoIP = mmdb.IPInstance().LookupCode(ip.AsSlice()) + for _, code := range metadata.DstGeoIP { + if g.country == code { return true, g.adapter } } return false, g.adapter } - return g.geoIPMatcher.Match(ip), g.adapter + + match := g.geoIPMatcher.Match(ip) + if match { + metadata.DstGeoIP = append(metadata.DstGeoIP, g.country) + } + return match, g.adapter } func (g *GEOIP) Adapter() string { @@ -81,8 +97,9 @@ func NewGEOIP(country string, adapter string, noResolveIP bool) (*GEOIP, error) log.Errorln("can't initial GeoIP: %s", err) return nil, err } + country = strings.ToLower(country) - if !C.GeodataMode || strings.EqualFold(country, "LAN") { + if !C.GeodataMode || country == "lan" { geoip := &GEOIP{ Base: &Base{}, country: country, @@ -94,7 +111,7 @@ func NewGEOIP(country string, adapter string, noResolveIP bool) (*GEOIP, error) geoIPMatcher, size, err := geodata.LoadGeoIPMatcher(country) if err != nil { - return nil, fmt.Errorf("[GeoIP] %s", err.Error()) + return nil, fmt.Errorf("[GeoIP] %w", err) } log.Infoln("Start initial GeoIP rule %s => %s, records: %d", country, adapter, size) @@ -108,5 +125,3 @@ func NewGEOIP(country string, adapter string, noResolveIP bool) (*GEOIP, error) } return geoip, nil } - -//var _ C.Rule = (*GEOIP)(nil) diff --git a/rules/common/ipasn.go b/rules/common/ipasn.go new file mode 100644 index 00000000..1fce8af4 --- /dev/null +++ b/rules/common/ipasn.go @@ -0,0 +1,67 @@ +package common + +import ( + "strconv" + + "github.com/metacubex/mihomo/component/geodata" + "github.com/metacubex/mihomo/component/mmdb" + C "github.com/metacubex/mihomo/constant" + "github.com/metacubex/mihomo/log" +) + +type ASN struct { + *Base + asn string + adapter string + noResolveIP bool +} + +func (a *ASN) Match(metadata *C.Metadata) (bool, string) { + ip := metadata.DstIP + if !ip.IsValid() { + return false, "" + } + + result := mmdb.ASNInstance().LookupASN(ip.AsSlice()) + + asnNumber := strconv.FormatUint(uint64(result.AutonomousSystemNumber), 10) + metadata.DstIPASN = asnNumber + " " + result.AutonomousSystemOrganization + + match := a.asn == asnNumber + return match, a.adapter +} + +func (a *ASN) RuleType() C.RuleType { + return C.IPASN +} + +func (a *ASN) Adapter() string { + return a.adapter +} + +func (a *ASN) Payload() string { + return a.asn +} + +func (a *ASN) ShouldResolveIP() bool { + return !a.noResolveIP +} + +func (a *ASN) GetASN() string { + return a.asn +} + +func NewIPASN(asn string, adapter string, noResolveIP bool) (*ASN, error) { + C.ASNEnable = true + if err := geodata.InitASN(); err != nil { + log.Errorln("can't initial ASN: %s", err) + return nil, err + } + + return &ASN{ + Base: &Base{}, + asn: asn, + adapter: adapter, + noResolveIP: noResolveIP, + }, nil +} diff --git a/rules/parser.go b/rules/parser.go index 7a79b18b..b69cc4fd 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": @@ -25,6 +27,9 @@ func ParseRule(tp, payload, target string, params []string, subRules map[string] case "IP-CIDR", "IP-CIDR6": noResolve := RC.HasNoResolve(params) parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRNoResolve(noResolve)) + case "IP-ASN": + noResolve := RC.HasNoResolve(params) + parsed, parseErr = RC.NewIPASN(payload, target, noResolve) case "SRC-IP-CIDR": parsed, parseErr = RC.NewIPCIDR(payload, target, RC.WithIPCIDRSourceIP(true), RC.WithIPCIDRNoResolve(true)) case "IP-SUFFIX": diff --git a/transport/vmess/http.go b/transport/vmess/http.go index c77a7e9d..6da9759e 100644 --- a/transport/vmess/http.go +++ b/transport/vmess/http.go @@ -60,7 +60,7 @@ func (hc *httpConn) Write(b []byte) (int, error) { host = header[fastrand.Intn(len(header))] } - u := fmt.Sprintf("http://%s%s", host, path) + u := fmt.Sprintf("http://%s%s", net.JoinHostPort(host, "80"), path) req, _ := http.NewRequest(utils.EmptyOr(hc.cfg.Method, http.MethodGet), u, bytes.NewBuffer(b)) for key, list := range hc.cfg.Headers { req.Header.Set(key, list[fastrand.Intn(len(list))])