mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Mon Mar 17 19:35:52 CET 2025
This commit is contained in:
@@ -944,3 +944,4 @@ Update On Thu Mar 13 19:35:33 CET 2025
|
||||
Update On Fri Mar 14 19:34:32 CET 2025
|
||||
Update On Sat Mar 15 19:32:45 CET 2025
|
||||
Update On Sun Mar 16 19:33:44 CET 2025
|
||||
Update On Mon Mar 17 19:35:42 CET 2025
|
||||
|
||||
@@ -5,8 +5,8 @@ After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
|
||||
@@ -5,8 +5,8 @@ After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/metacubex/mihomo/component/proxydialer"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
gost "github.com/metacubex/mihomo/transport/gost-plugin"
|
||||
"github.com/metacubex/mihomo/transport/restls"
|
||||
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
|
||||
shadowtls "github.com/metacubex/mihomo/transport/sing-shadowtls"
|
||||
@@ -34,6 +35,7 @@ type ShadowSocks struct {
|
||||
obfsMode string
|
||||
obfsOption *simpleObfsOption
|
||||
v2rayOption *v2rayObfs.Option
|
||||
gostOption *gost.Option
|
||||
shadowTLSOption *shadowtls.ShadowTLSOption
|
||||
restlsConfig *restlsC.Config
|
||||
}
|
||||
@@ -71,6 +73,17 @@ type v2rayObfsOption struct {
|
||||
V2rayHttpUpgradeFastOpen bool `obfs:"v2ray-http-upgrade-fast-open,omitempty"`
|
||||
}
|
||||
|
||||
type gostObfsOption struct {
|
||||
Mode string `obfs:"mode"`
|
||||
Host string `obfs:"host,omitempty"`
|
||||
Path string `obfs:"path,omitempty"`
|
||||
TLS bool `obfs:"tls,omitempty"`
|
||||
Fingerprint string `obfs:"fingerprint,omitempty"`
|
||||
Headers map[string]string `obfs:"headers,omitempty"`
|
||||
SkipCertVerify bool `obfs:"skip-cert-verify,omitempty"`
|
||||
Mux bool `obfs:"mux,omitempty"`
|
||||
}
|
||||
|
||||
type shadowTLSOption struct {
|
||||
Password string `obfs:"password"`
|
||||
Host string `obfs:"host"`
|
||||
@@ -97,7 +110,13 @@ func (ss *ShadowSocks) StreamConnContext(ctx context.Context, c net.Conn, metada
|
||||
c = obfs.NewHTTPObfs(c, ss.obfsOption.Host, port)
|
||||
case "websocket":
|
||||
var err error
|
||||
c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
|
||||
if ss.v2rayOption != nil {
|
||||
c, err = v2rayObfs.NewV2rayObfs(ctx, c, ss.v2rayOption)
|
||||
} else if ss.gostOption != nil {
|
||||
c, err = gost.NewGostWebsocket(ctx, c, ss.gostOption)
|
||||
} else {
|
||||
return nil, fmt.Errorf("plugin options is required")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s connect error: %w", ss.addr, err)
|
||||
}
|
||||
@@ -240,6 +259,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
}
|
||||
|
||||
var v2rayOption *v2rayObfs.Option
|
||||
var gostOption *gost.Option
|
||||
var obfsOption *simpleObfsOption
|
||||
var shadowTLSOpt *shadowtls.ShadowTLSOption
|
||||
var restlsConfig *restlsC.Config
|
||||
@@ -281,6 +301,28 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
v2rayOption.SkipCertVerify = opts.SkipCertVerify
|
||||
v2rayOption.Fingerprint = opts.Fingerprint
|
||||
}
|
||||
} else if option.Plugin == "gost-plugin" {
|
||||
opts := gostObfsOption{Host: "bing.com", Mux: true}
|
||||
if err := decoder.Decode(option.PluginOpts, &opts); err != nil {
|
||||
return nil, fmt.Errorf("ss %s initialize gost-plugin error: %w", addr, err)
|
||||
}
|
||||
|
||||
if opts.Mode != "websocket" {
|
||||
return nil, fmt.Errorf("ss %s obfs mode error: %s", addr, opts.Mode)
|
||||
}
|
||||
obfsMode = opts.Mode
|
||||
gostOption = &gost.Option{
|
||||
Host: opts.Host,
|
||||
Path: opts.Path,
|
||||
Headers: opts.Headers,
|
||||
Mux: opts.Mux,
|
||||
}
|
||||
|
||||
if opts.TLS {
|
||||
gostOption.TLS = true
|
||||
gostOption.SkipCertVerify = opts.SkipCertVerify
|
||||
gostOption.Fingerprint = opts.Fingerprint
|
||||
}
|
||||
} else if option.Plugin == shadowtls.Mode {
|
||||
obfsMode = shadowtls.Mode
|
||||
opt := &shadowTLSOption{
|
||||
@@ -336,6 +378,7 @@ func NewShadowSocks(option ShadowSocksOption) (*ShadowSocks, error) {
|
||||
option: &option,
|
||||
obfsMode: obfsMode,
|
||||
v2rayOption: v2rayOption,
|
||||
gostOption: gostOption,
|
||||
obfsOption: obfsOption,
|
||||
shadowTLSOption: shadowTLSOpt,
|
||||
restlsConfig: restlsConfig,
|
||||
|
||||
@@ -326,13 +326,6 @@ func (cp *CompatibleProvider) Close() error {
|
||||
return cp.compatibleProvider.Close()
|
||||
}
|
||||
|
||||
func proxiesOnUpdate(pd *proxySetProvider) func([]C.Proxy) {
|
||||
return func(elm []C.Proxy) {
|
||||
pd.setProxies(elm)
|
||||
pd.version += 1
|
||||
}
|
||||
}
|
||||
|
||||
func NewProxiesParser(filter string, excludeFilter string, excludeType string, dialerProxy string, override OverrideSchema) (resource.Parser[[]C.Proxy], error) {
|
||||
excludeFilterReg, err := regexp2.Compile(excludeFilter, regexp2.None)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package utils
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -149,3 +150,24 @@ func (ranges IntRanges[T]) Range(f func(t T) bool) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ranges IntRanges[T]) Merge() (mergedRanges IntRanges[T]) {
|
||||
if len(ranges) == 0 {
|
||||
return
|
||||
}
|
||||
sort.Slice(ranges, func(i, j int) bool {
|
||||
return ranges[i].Start() < ranges[j].Start()
|
||||
})
|
||||
mergedRanges = ranges[:1]
|
||||
var rangeIndex int
|
||||
for _, r := range ranges[1:] {
|
||||
if mergedRanges[rangeIndex].End()+1 > mergedRanges[rangeIndex].End() && // integer overflow
|
||||
r.Start() > mergedRanges[rangeIndex].End()+1 {
|
||||
mergedRanges = append(mergedRanges, r)
|
||||
rangeIndex++
|
||||
} else if r.End() > mergedRanges[rangeIndex].End() {
|
||||
mergedRanges[rangeIndex].end = r.End()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMergeRanges(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, testRange := range []struct {
|
||||
ranges IntRanges[uint16]
|
||||
expected IntRanges[uint16]
|
||||
}{
|
||||
{
|
||||
ranges: IntRanges[uint16]{
|
||||
NewRange[uint16](0, 1),
|
||||
NewRange[uint16](1, 2),
|
||||
},
|
||||
expected: IntRanges[uint16]{
|
||||
NewRange[uint16](0, 2),
|
||||
},
|
||||
},
|
||||
{
|
||||
ranges: IntRanges[uint16]{
|
||||
NewRange[uint16](0, 3),
|
||||
NewRange[uint16](5, 7),
|
||||
NewRange[uint16](8, 9),
|
||||
NewRange[uint16](10, 10),
|
||||
},
|
||||
expected: IntRanges[uint16]{
|
||||
NewRange[uint16](0, 3),
|
||||
NewRange[uint16](5, 10),
|
||||
},
|
||||
},
|
||||
{
|
||||
ranges: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 3),
|
||||
NewRange[uint16](2, 6),
|
||||
NewRange[uint16](8, 10),
|
||||
NewRange[uint16](15, 18),
|
||||
},
|
||||
expected: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 6),
|
||||
NewRange[uint16](8, 10),
|
||||
NewRange[uint16](15, 18),
|
||||
},
|
||||
},
|
||||
{
|
||||
ranges: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 3),
|
||||
NewRange[uint16](2, 7),
|
||||
NewRange[uint16](2, 6),
|
||||
},
|
||||
expected: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 7),
|
||||
},
|
||||
},
|
||||
{
|
||||
ranges: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 3),
|
||||
NewRange[uint16](2, 6),
|
||||
NewRange[uint16](2, 7),
|
||||
},
|
||||
expected: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 7),
|
||||
},
|
||||
},
|
||||
{
|
||||
ranges: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 3),
|
||||
NewRange[uint16](2, 65535),
|
||||
NewRange[uint16](2, 7),
|
||||
NewRange[uint16](3, 16),
|
||||
},
|
||||
expected: IntRanges[uint16]{
|
||||
NewRange[uint16](1, 65535),
|
||||
},
|
||||
},
|
||||
} {
|
||||
assert.Equal(t, testRange.expected, testRange.ranges.Merge())
|
||||
}
|
||||
}
|
||||
@@ -88,21 +88,22 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToListenConfig(lc)
|
||||
} else {
|
||||
if cfg.interfaceName == "" {
|
||||
interfaceName := cfg.interfaceName
|
||||
if interfaceName == "" {
|
||||
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
||||
cfg.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
|
||||
interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
|
||||
}
|
||||
}
|
||||
if rAddrPort.Addr().Unmap().IsLoopback() {
|
||||
// avoid "The requested address is not valid in its context."
|
||||
cfg.interfaceName = ""
|
||||
interfaceName = ""
|
||||
}
|
||||
if cfg.interfaceName != "" {
|
||||
if interfaceName != "" {
|
||||
bind := bindIfaceToListenConfig
|
||||
if cfg.fallbackBind {
|
||||
bind = fallbackBindIfaceToListenConfig
|
||||
}
|
||||
addr, err := bind(cfg.interfaceName, lc, network, address, rAddrPort)
|
||||
addr, err := bind(interfaceName, lc, network, address, rAddrPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -162,17 +163,18 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToToDialer(dialer)
|
||||
} else {
|
||||
if opt.interfaceName == "" {
|
||||
interfaceName := opt.interfaceName // don't change the "opt", it's a pointer
|
||||
if interfaceName == "" {
|
||||
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
||||
opt.interfaceName = finder.FindInterfaceName(destination)
|
||||
interfaceName = finder.FindInterfaceName(destination)
|
||||
}
|
||||
}
|
||||
if opt.interfaceName != "" {
|
||||
if interfaceName != "" {
|
||||
bind := bindIfaceToDialer
|
||||
if opt.fallbackBind {
|
||||
bind = fallbackBindIfaceToDialer
|
||||
}
|
||||
if err := bind(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
if err := bind(interfaceName, dialer, network, destination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,17 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/singledo"
|
||||
|
||||
"github.com/metacubex/bart"
|
||||
)
|
||||
|
||||
type Interface struct {
|
||||
Index int
|
||||
MTU int
|
||||
Name string
|
||||
Addresses []netip.Prefix
|
||||
HardwareAddr net.HardwareAddr
|
||||
Flags net.Flags
|
||||
Addresses []netip.Prefix
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -23,16 +25,23 @@ var (
|
||||
ErrAddrNotFound = errors.New("addr not found")
|
||||
)
|
||||
|
||||
var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20)
|
||||
type ifaceCache struct {
|
||||
ifMap map[string]*Interface
|
||||
ifTable bart.Table[*Interface]
|
||||
}
|
||||
|
||||
func Interfaces() (map[string]*Interface, error) {
|
||||
value, err, _ := interfaces.Do(func() (map[string]*Interface, error) {
|
||||
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
|
||||
|
||||
func getCache() (*ifaceCache, error) {
|
||||
value, err, _ := caches.Do(func() (*ifaceCache, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := map[string]*Interface{}
|
||||
cache := &ifaceCache{
|
||||
ifMap: make(map[string]*Interface),
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
@@ -61,21 +70,34 @@ func Interfaces() (map[string]*Interface, error) {
|
||||
}
|
||||
}
|
||||
|
||||
r[iface.Name] = &Interface{
|
||||
ifaceObj := &Interface{
|
||||
Index: iface.Index,
|
||||
MTU: iface.MTU,
|
||||
Name: iface.Name,
|
||||
Addresses: ipNets,
|
||||
HardwareAddr: iface.HardwareAddr,
|
||||
Flags: iface.Flags,
|
||||
Addresses: ipNets,
|
||||
}
|
||||
cache.ifMap[iface.Name] = ifaceObj
|
||||
|
||||
for _, prefix := range ipNets {
|
||||
cache.ifTable.Insert(prefix, ifaceObj)
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
return cache, nil
|
||||
})
|
||||
return value, err
|
||||
}
|
||||
|
||||
func Interfaces() (map[string]*Interface, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cache.ifMap, nil
|
||||
}
|
||||
|
||||
func ResolveInterface(name string) (*Interface, error) {
|
||||
ifaces, err := Interfaces()
|
||||
if err != nil {
|
||||
@@ -90,23 +112,29 @@ func ResolveInterface(name string) (*Interface, error) {
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func IsLocalIp(ip netip.Addr) (bool, error) {
|
||||
ifaces, err := Interfaces()
|
||||
func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iface, ok := cache.ifTable.Lookup(addr)
|
||||
if !ok {
|
||||
return nil, ErrIfaceNotFound
|
||||
}
|
||||
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func IsLocalIp(addr netip.Addr) (bool, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
for _, addr := range iface.Addresses {
|
||||
if addr.Contains(ip) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
return cache.ifTable.Contains(addr), nil
|
||||
}
|
||||
|
||||
func FlushCache() {
|
||||
interfaces.Reset()
|
||||
caches.Reset()
|
||||
}
|
||||
|
||||
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) {
|
||||
|
||||
@@ -60,28 +60,35 @@ func (sd *Dispatcher) forceSniff(metadata *C.Metadata) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter) bool {
|
||||
// UDPSniff is called when a UDP NAT is created and passed the first initialization packet.
|
||||
// It may return a wrapped packetSender if the sniffer process needs to wait for multiple packets.
|
||||
// This function must be non-blocking, and any blocking operations should be done in the wrapped packetSender.
|
||||
func (sd *Dispatcher) UDPSniff(packet C.PacketAdapter, packetSender C.PacketSender) C.PacketSender {
|
||||
metadata := packet.Metadata()
|
||||
if sd.shouldOverride(metadata) {
|
||||
for sniffer, config := range sd.sniffers {
|
||||
if sniffer.SupportNetwork() == C.UDP || sniffer.SupportNetwork() == C.ALLNet {
|
||||
inWhitelist := sniffer.SupportPort(metadata.DstPort)
|
||||
for current, config := range sd.sniffers {
|
||||
if current.SupportNetwork() == C.UDP || current.SupportNetwork() == C.ALLNet {
|
||||
inWhitelist := current.SupportPort(metadata.DstPort)
|
||||
overrideDest := config.OverrideDest
|
||||
|
||||
if inWhitelist {
|
||||
host, err := sniffer.SniffData(packet.Data())
|
||||
if wrapable, ok := current.(sniffer.MultiPacketSniffer); ok {
|
||||
return wrapable.WrapperSender(packetSender, overrideDest)
|
||||
}
|
||||
|
||||
host, err := current.SniffData(packet.Data())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sd.replaceDomain(metadata, host, overrideDest)
|
||||
return true
|
||||
replaceDomain(metadata, host, overrideDest)
|
||||
return packetSender
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return packetSender
|
||||
}
|
||||
|
||||
// TCPSniff returns true if the connection is sniffed to have a domain
|
||||
@@ -130,13 +137,13 @@ func (sd *Dispatcher) TCPSniff(conn *N.BufferedConn, metadata *C.Metadata) bool
|
||||
|
||||
sd.skipList.Delete(dst)
|
||||
|
||||
sd.replaceDomain(metadata, host, overrideDest)
|
||||
replaceDomain(metadata, host, overrideDest)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (sd *Dispatcher) replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
|
||||
func replaceDomain(metadata *C.Metadata, host string, overrideDest bool) {
|
||||
metadata.SniffHost = host
|
||||
if overrideDest {
|
||||
log.Debugln("[Sniffer] Sniff %s [%s]-->[%s] success, replace domain [%s]-->[%s]",
|
||||
|
||||
@@ -7,10 +7,15 @@ import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/buf"
|
||||
"github.com/metacubex/mihomo/common/pool"
|
||||
"github.com/metacubex/mihomo/common/utils"
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
"github.com/metacubex/mihomo/constant/sniffer"
|
||||
|
||||
"github.com/metacubex/quic-go/quicvarint"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
@@ -21,6 +26,16 @@ import (
|
||||
const (
|
||||
versionDraft29 uint32 = 0xff00001d
|
||||
version1 uint32 = 0x1
|
||||
|
||||
quicPacketTypeInitial = 0x00
|
||||
quicPacketType0RTT = 0x01
|
||||
|
||||
// Timeout before quic sniffer all packets
|
||||
quicWaitConn = time.Second * 3
|
||||
|
||||
// maxCryptoStreamOffset is the maximum offset allowed on any of the crypto streams.
|
||||
// This limits the size of the ClientHello and Certificates that can be received.
|
||||
maxCryptoStreamOffset = 16 * (1 << 10)
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -30,6 +45,9 @@ var (
|
||||
errNotQuicInitial = errors.New("not QUIC initial packet")
|
||||
)
|
||||
|
||||
var _ sniffer.Sniffer = (*QuicSniffer)(nil)
|
||||
var _ sniffer.MultiPacketSniffer = (*QuicSniffer)(nil)
|
||||
|
||||
type QuicSniffer struct {
|
||||
*BaseSniffer
|
||||
}
|
||||
@@ -44,67 +62,160 @@ func NewQuicSniffer(snifferConfig SnifferConfig) (*QuicSniffer, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (quic QuicSniffer) Protocol() string {
|
||||
func (sniffer *QuicSniffer) Protocol() string {
|
||||
return "quic"
|
||||
}
|
||||
|
||||
func (quic QuicSniffer) SupportNetwork() C.NetWork {
|
||||
func (sniffer *QuicSniffer) SupportNetwork() C.NetWork {
|
||||
return C.UDP
|
||||
}
|
||||
|
||||
func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
func (sniffer *QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
return "", ErrorUnsupportedSniffer
|
||||
}
|
||||
|
||||
func (sniffer *QuicSniffer) WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender {
|
||||
return &quicPacketSender{
|
||||
sender: packetSender,
|
||||
chClose: make(chan struct{}),
|
||||
override: override,
|
||||
}
|
||||
}
|
||||
|
||||
var _ constant.PacketSender = (*quicPacketSender)(nil)
|
||||
|
||||
type quicPacketSender struct {
|
||||
lock sync.RWMutex
|
||||
ranges utils.IntRanges[uint64]
|
||||
buffer []byte
|
||||
result string
|
||||
override bool
|
||||
|
||||
sender constant.PacketSender
|
||||
|
||||
chClose chan struct{}
|
||||
closed bool
|
||||
}
|
||||
|
||||
// Send will send PacketAdapter nonblocking
|
||||
// the implement must call UDPPacket.Drop() inside Send
|
||||
func (q *quicPacketSender) Send(current constant.PacketAdapter) {
|
||||
defer q.sender.Send(current)
|
||||
|
||||
q.lock.RLock()
|
||||
if q.closed {
|
||||
q.lock.RUnlock()
|
||||
return
|
||||
}
|
||||
q.lock.RUnlock()
|
||||
|
||||
err := q.readQuicData(current.Data())
|
||||
if err != nil {
|
||||
q.close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Process is a blocking loop to send PacketAdapter to PacketConn and update the WriteBackProxy
|
||||
func (q *quicPacketSender) Process(conn constant.PacketConn, proxy constant.WriteBackProxy) {
|
||||
q.sender.Process(conn, proxy)
|
||||
}
|
||||
|
||||
// ResolveUDP wait sniffer recv all fragments and update the domain
|
||||
func (q *quicPacketSender) ResolveUDP(data *constant.Metadata) error {
|
||||
select {
|
||||
case <-q.chClose:
|
||||
q.lock.RLock()
|
||||
replaceDomain(data, q.result, q.override)
|
||||
q.lock.RUnlock()
|
||||
break
|
||||
case <-time.After(quicWaitConn):
|
||||
q.close()
|
||||
}
|
||||
|
||||
return q.sender.ResolveUDP(data)
|
||||
}
|
||||
|
||||
// Close stop the Process loop
|
||||
func (q *quicPacketSender) Close() {
|
||||
q.sender.Close()
|
||||
q.close()
|
||||
}
|
||||
|
||||
func (q *quicPacketSender) close() {
|
||||
q.lock.Lock()
|
||||
q.closeLocked()
|
||||
q.lock.Unlock()
|
||||
}
|
||||
|
||||
func (q *quicPacketSender) closeLocked() {
|
||||
if !q.closed {
|
||||
close(q.chClose)
|
||||
q.closed = true
|
||||
if q.buffer != nil {
|
||||
_ = pool.Put(q.buffer)
|
||||
q.buffer = nil
|
||||
}
|
||||
q.ranges = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (q *quicPacketSender) readQuicData(b []byte) error {
|
||||
buffer := buf.As(b)
|
||||
typeByte, err := buffer.ReadByte()
|
||||
if err != nil {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
}
|
||||
isLongHeader := typeByte&0x80 > 0
|
||||
if !isLongHeader || typeByte&0x40 == 0 {
|
||||
return "", errNotQuicInitial
|
||||
return errNotQuicInitial
|
||||
}
|
||||
|
||||
vb, err := buffer.ReadBytes(4)
|
||||
if err != nil {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
versionNumber := binary.BigEndian.Uint32(vb)
|
||||
|
||||
if versionNumber != 0 && typeByte&0x40 == 0 {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
} else if versionNumber != versionDraft29 && versionNumber != version1 {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
if (typeByte&0x30)>>4 != 0x0 {
|
||||
return "", errNotQuicInitial
|
||||
connIdLen, err := buffer.ReadByte()
|
||||
if err != nil || connIdLen == 0 {
|
||||
return errNotQuic
|
||||
}
|
||||
destConnID := make([]byte, int(connIdLen))
|
||||
if _, err := io.ReadFull(buffer, destConnID); err != nil {
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
var destConnID []byte
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return "", errNotQuic
|
||||
} else if destConnID, err = buffer.ReadBytes(int(l)); err != nil {
|
||||
return "", errNotQuic
|
||||
packetType := (typeByte & 0x30) >> 4
|
||||
if packetType != quicPacketTypeInitial {
|
||||
return nil
|
||||
}
|
||||
|
||||
if l, err := buffer.ReadByte(); err != nil {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
} else if _, err := buffer.ReadBytes(int(l)); err != nil {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
tokenLen, err := quicvarint.Read(buffer)
|
||||
if err != nil || tokenLen > uint64(len(b)) {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
if _, err = buffer.ReadBytes(int(tokenLen)); err != nil {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
packetLen, err := quicvarint.Read(buffer)
|
||||
if err != nil {
|
||||
return "", errNotQuic
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
hdrLen := len(b) - buffer.Len()
|
||||
@@ -120,7 +231,7 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
hpKey := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic hp", 16)
|
||||
block, err := aes.NewCipher(hpKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
cache := buf.NewPacket()
|
||||
@@ -130,6 +241,7 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
block.Encrypt(mask, b[hdrLen+4:hdrLen+4+16])
|
||||
firstByte := b[0]
|
||||
// Encrypt/decrypt first byte.
|
||||
|
||||
if isLongHeader {
|
||||
// Long header: 4 bits masked
|
||||
// High 4 bits are not protected.
|
||||
@@ -153,8 +265,8 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
packetNumber[i] ^= mask[1+i]
|
||||
}
|
||||
|
||||
if packetNumber[0] != 0 && packetNumber[0] != 1 {
|
||||
return "", errNotQuicInitial
|
||||
if int(packetLen)+hdrLen > len(b) || extHdrLen > len(b) {
|
||||
return errNotQuic
|
||||
}
|
||||
|
||||
data := b[extHdrLen : int(packetLen)+hdrLen]
|
||||
@@ -163,12 +275,13 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
iv := hkdfExpandLabel(crypto.SHA256, secret, []byte{}, "quic iv", 12)
|
||||
aesCipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
aead, err := cipher.NewGCM(aesCipher)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
// We only decrypt once, so we do not need to XOR it back.
|
||||
// https://github.com/quic-go/qtls-go1-20/blob/e132a0e6cb45e20ac0b705454849a11d09ba5a54/cipher_suites.go#L496
|
||||
for i, b := range packetNumber {
|
||||
@@ -177,13 +290,20 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
dst := cache.Extend(len(data))
|
||||
decrypted, err := aead.Open(dst[:0], iv, data, extHdr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
|
||||
buffer = buf.As(decrypted)
|
||||
|
||||
cryptoLen := uint(0)
|
||||
cryptoData := cache.Extend(buffer.Len())
|
||||
for i := 0; !buffer.IsEmpty(); i++ {
|
||||
q.lock.RLock()
|
||||
if q.closed {
|
||||
q.lock.RUnlock()
|
||||
// close() was called, just return
|
||||
return nil
|
||||
}
|
||||
q.lock.RUnlock()
|
||||
|
||||
frameType := byte(0x0) // Default to PADDING frame
|
||||
for frameType == 0x0 && !buffer.IsEmpty() {
|
||||
frameType, _ = buffer.ReadByte()
|
||||
@@ -193,79 +313,123 @@ func (quic QuicSniffer) SniffData(b []byte) (string, error) {
|
||||
case 0x01: // PING frame
|
||||
case 0x02, 0x03: // ACK frame
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: Largest Acknowledged
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Delay
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
ackRangeCount, err := quicvarint.Read(buffer) // Field: ACK Range Count
|
||||
if err != nil {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: First ACK Range
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
for i := 0; i < int(ackRangeCount); i++ { // Field: ACK Range
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> Gap
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ACK Range -> ACK Range Length
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
if frameType == 0x03 {
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT0 Count
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: ECN Counts -> ECT1 Count
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { //nolint:misspell // Field: ECN Counts -> ECT-CE Count
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
}
|
||||
case 0x06: // CRYPTO frame, we will use this frame
|
||||
offset, err := quicvarint.Read(buffer) // Field: Offset
|
||||
if err != nil {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
length, err := quicvarint.Read(buffer) // Field: Length
|
||||
if err != nil || length > uint64(buffer.Len()) {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if cryptoLen < uint(offset+length) {
|
||||
cryptoLen = uint(offset + length)
|
||||
|
||||
end := offset + length
|
||||
if end > maxCryptoStreamOffset {
|
||||
return io.ErrShortBuffer
|
||||
}
|
||||
if _, err := buffer.Read(cryptoData[offset : offset+length]); err != nil { // Field: Crypto Data
|
||||
return "", io.ErrUnexpectedEOF
|
||||
|
||||
q.lock.Lock()
|
||||
if q.closed {
|
||||
q.lock.Unlock()
|
||||
// close() was called, just return
|
||||
return nil
|
||||
}
|
||||
if q.buffer == nil {
|
||||
q.buffer = pool.Get(maxCryptoStreamOffset)[:end]
|
||||
} else if end > uint64(len(q.buffer)) {
|
||||
q.buffer = q.buffer[:end]
|
||||
}
|
||||
target := q.buffer[offset:end]
|
||||
if _, err := buffer.Read(target); err != nil { // Field: Crypto Data
|
||||
q.lock.Unlock()
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
q.ranges = append(q.ranges, utils.NewRange(offset, end))
|
||||
q.ranges = q.ranges.Merge()
|
||||
q.lock.Unlock()
|
||||
case 0x1c: // CONNECTION_CLOSE frame, only 0x1c is permitted in initial packet
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: Error Code
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err = quicvarint.Read(buffer); err != nil { // Field: Frame Type
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
length, err := quicvarint.Read(buffer) // Field: Reason Phrase Length
|
||||
if err != nil {
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
if _, err := buffer.ReadBytes(int(length)); err != nil { // Field: Reason Phrase
|
||||
return "", io.ErrUnexpectedEOF
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
default:
|
||||
// Only above frame types are permitted in initial packet.
|
||||
// See https://www.rfc-editor.org/rfc/rfc9000.html#section-17.2.2-8
|
||||
return "", errNotQuicInitial
|
||||
return errNotQuicInitial
|
||||
}
|
||||
}
|
||||
|
||||
domain, err := ReadClientHello(cryptoData[:cryptoLen])
|
||||
if err != nil {
|
||||
return "", err
|
||||
_ = q.tryAssemble()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *quicPacketSender) tryAssemble() error {
|
||||
q.lock.RLock()
|
||||
|
||||
if q.closed {
|
||||
q.lock.RUnlock()
|
||||
// close() was called, just return
|
||||
return nil
|
||||
}
|
||||
|
||||
return *domain, nil
|
||||
if len(q.ranges) != 1 || q.ranges[0].Start() != 0 || q.ranges[0].End() != uint64(len(q.buffer)) {
|
||||
q.lock.RUnlock()
|
||||
return ErrNoClue
|
||||
}
|
||||
|
||||
domain, err := ReadClientHello(q.buffer)
|
||||
q.lock.RUnlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q.lock.Lock()
|
||||
q.result = *domain
|
||||
q.closeLocked()
|
||||
q.lock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hkdfExpandLabel(hash crypto.Hash, secret, context []byte, label string, length int) []byte {
|
||||
|
||||
@@ -3,35 +3,184 @@ package sniffer
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"net"
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/metacubex/mihomo/constant"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type fakeSender struct {
|
||||
resultCh chan *constant.Metadata
|
||||
}
|
||||
|
||||
var _ constant.PacketSender = (*fakeSender)(nil)
|
||||
|
||||
func (e *fakeSender) Send(packet constant.PacketAdapter) {
|
||||
// Ensure that the wrapper's Send can correctly handle the situation where the packet is directly discarded.
|
||||
packet.Drop()
|
||||
}
|
||||
|
||||
func (e *fakeSender) Process(constant.PacketConn, constant.WriteBackProxy) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (e *fakeSender) ResolveUDP(metadata *constant.Metadata) error {
|
||||
e.resultCh <- metadata
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *fakeSender) Close() {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
type fakeUDPPacket struct {
|
||||
data []byte
|
||||
data2 []byte // backup
|
||||
}
|
||||
|
||||
func (s *fakeUDPPacket) InAddr() net.Addr {
|
||||
return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||
}
|
||||
|
||||
func (s *fakeUDPPacket) LocalAddr() net.Addr {
|
||||
return net.UDPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||
}
|
||||
|
||||
func (s *fakeUDPPacket) Data() []byte {
|
||||
return s.data
|
||||
}
|
||||
|
||||
func (s *fakeUDPPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
|
||||
func (s *fakeUDPPacket) Drop() {
|
||||
for i := range s.data {
|
||||
if s.data[i] != s.data2[i] { // ensure input data not changed
|
||||
panic("data has been changed!")
|
||||
}
|
||||
s.data[i] = 0 // forcing data to become illegal
|
||||
}
|
||||
s.data = nil
|
||||
}
|
||||
|
||||
var _ constant.UDPPacket = (*fakeUDPPacket)(nil)
|
||||
|
||||
func asPacket(data string) constant.PacketAdapter {
|
||||
pktData, _ := hex.DecodeString(data)
|
||||
|
||||
meta := &constant.Metadata{}
|
||||
pkt := &fakeUDPPacket{data: pktData, data2: bytes.Clone(pktData)}
|
||||
pktAdp := constant.NewPacketAdapter(pkt, meta)
|
||||
|
||||
return pktAdp
|
||||
}
|
||||
|
||||
func testQuicSniffer(data []string, async bool) (string, error) {
|
||||
q, err := NewQuicSniffer(SnifferConfig{})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resultCh := make(chan *constant.Metadata, 1)
|
||||
emptySender := &fakeSender{resultCh: resultCh}
|
||||
|
||||
sender := q.WrapperSender(emptySender, true)
|
||||
|
||||
go func() {
|
||||
meta := constant.Metadata{}
|
||||
err = sender.ResolveUDP(&meta)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, d := range data {
|
||||
if async {
|
||||
go sender.Send(asPacket(d))
|
||||
} else {
|
||||
sender.Send(asPacket(d))
|
||||
}
|
||||
}
|
||||
|
||||
meta := <-resultCh
|
||||
return meta.SniffHost, nil
|
||||
}
|
||||
|
||||
func TestQuicHeaders(t *testing.T) {
|
||||
|
||||
cases := []struct {
|
||||
input string
|
||||
input []string
|
||||
domain string
|
||||
}{
|
||||
//Normal domain quic sniff
|
||||
{
|
||||
input: "cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b",
|
||||
input: []string{"cd0000000108f1fb7bcc78aa5e7203a8f86400421531fe825b19541876db6c55c38890cd73149d267a084afee6087304095417a3033df6a81bbb71d8512e7a3e16df1e277cae5df3182cb214b8fe982ba3fdffbaa9ffec474547d55945f0fddbeadfb0b5243890b2fa3da45169e2bd34ec04b2e29382f48d612b28432a559757504d158e9e505407a77dd34f4b60b8d3b555ee85aacd6648686802f4de25e7216b19e54c5f78e8a5963380c742d861306db4c16e4f7fc94957aa50b9578a0b61f1e406b2ad5f0cd3cd271c4d99476409797b0c3cb3efec256118912d4b7e4fd79d9cb9016b6e5eaa4f5e57b637b217755daf8968a4092bed0ed5413f5d04904b3a61e4064f9211b2629e5b52a89c7b19f37a713e41e27743ea6dfa736dfa1bb0a4b2bc8c8dc632c6ce963493a20c550e6fdb2475213665e9a85cfc394da9cec0cf41f0c8abed3fc83be5245b2b5aa5e825d29349f721d30774ef5bf965b540f3d8d98febe20956b1fc8fa047e10e7d2f921c9c6622389e02322e80621a1cf5264e245b7276966eb02932584e3f7038bd36aa908766ad3fb98344025dec18670d6db43a1c5daac00937fce7b7c7d61ff4e6efd01a2bdee0ee183108b926393df4f3d74bbcbb015f240e7e346b7d01c41111a401225ce3b095ab4623a5836169bf9599eeca79d1d2e9b2202b5960a09211e978058d6fc0484eff3e91ce4649a5e3ba15b906d334cf66e28d9ff575406e1ae1ac2febafd72870b6f5d58fc5fb949cb1f40feb7c1d9ce5e71b"},
|
||||
domain: "www.google.com",
|
||||
},
|
||||
{
|
||||
input: "c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738",
|
||||
input: []string{"c3000000011266f50524e8d0fe88cbf51e3ad71a13198235000044c82dc5d943fb34cc6d5c5e433610dc7a44f5951935c2c1d14ac641b02472340a892c4492dbfe3f8262109108fc36d96bdc1e9e46b5f1f6ef6104add2aafbfd8e79246eb3b4637541aaed7d195571724e642ab4d31c909f1db86e7d8516117ce8716bd1e3acb664c499086b0f3bc7258595420e7bb969f934457d195e832ffff4ffddf11123eeadacc48190e356c8f0f6abc381deb7e285e3b0613a795b19bddb9f002ffdf6fd70f0ff2072302b33d2421aac6540bb9f0e85c7237af0dd56225b2264d769160febab952e64bd5155f23e58c6113891143f946591032b41816aed3ac54f521f60605f86791de24c5765b664c1348cc53d5d631b4bbefe1915f2b21fefafb47badeb72d8ba1fd5c3cfeb0ba9d0112396f170e94cd33952c4fa87997b870931bf1a300e8e127f530815ff087815b4f9d004cbcd17013ac143847572a1655a5b36e054e8b9951d747c2c6ff25d7b2edb13a2a6b8074062332f2191f6830cf435a4ed9db5d9c4eb43a143bf3edf0c48f6f9435dafad4afb743a5a33990379df953ecd388e848aff0ebba9ccc052b8303c0bd1fee7e7553af1894e81b7772818bb69249540ccb8cfb47b1517abaf71c81c3bd271f1a5f1b66465f850f377c9db682b8e543c3d0c10fcd2dee263630889b7d1d521d1d27e866ea4ab5f43790d6a7f76ceefd5783678ca92cc131fa42fc4a01e2a81cad734ddf17a53e1bda8e0a21afc9e8c1118c9459b13519f5b3c3d9692c92234f01129d47ae8ec70625170847472801190b46d36f73b868f55f5a18a3cb05af6d38610e0829e4fbf13ddcc202341702e43dcf33be76ff4afe327e5783287c137aad075752940b41e7d9f5146e36d908897c6d7a9fdc343fde2d9c9d6e6a6b237669bd3e6abe0a732861a679eadfa29a876c6a646953c9361830811b012b26b31c9e7158f8de9c9a108346ddee3dd3886da6258364c1281bff8e055f6384e3a23e198b5e6b726fa7f811b3338072019d4b5fd05891770d11e3ed6ab5f7ed33db1c6220c5aa8fa1909949ac55d5435b75982e17aa80940fa574f0aba4dc340129cad491fdf1f5e05c4e83e36ad29ff38f15e1c9436c792024442f57f07583d671dd05446c84ea20b471303f6ae4e5e13f244d671e0ebe94d3d5c17d3f3f378cdd51fa8a6d2c977c78a2397dd1e251cd979803d617d45f575e5d9db0a28b3c4c25fe2af24af5bddac09786b6d6d8aa19cfbd5409bdbfed7d518ef5c863f3ee757bd9d37cddc546cc57d2e52b6ae58789f297a300f1d76c3842603eae4b1224de31a939a68875c86e697aeebf7ebc65568f43fc681bacab830ac4a2164d324e90067125bad702192d01cb3cb3d2689ae681967e86fd7ac93a25cf2e905c88ca5ad7d11962f021754cf3f61224517bd3411d5b5a83955bcea79d702466d073a6eaadc1202b3693e555b051a5b19457023a01e7f943742bb7f5f8aeba8d4e363973aebdccfb12479619cfb93e833be702a307e796dc7431a48abd9b755b392c510b98cd20ef778e2ac88d6a04f23ba8a253d7eb7c13e0c88c3a21f7e23857c58704d139703a47e0965bf2dc8810dc36894ac1f3da73c155e271c106a718b2d184e4e5637c820fe909984642960edfc9e62ac50af5dd3feee6bc560ced7bda676d4e290c9c5916fad52180bbc83d3483e95c79bac15c209936f21042dc2b6253eefdac06e7f4745044eaa0acedabf1d1c8cd9402738"},
|
||||
domain: "cloudflare-dns.com",
|
||||
},
|
||||
|
||||
// Fragmented quic sniff
|
||||
{
|
||||
input: []string{
|
||||
"c70000000108afb466a232f7f9f2000044d0168a15a021477ecb9731ed77784d42301462e2d59b0395adc1fa6b569d428583f100860d6b6ae29b6c1b8c0f9c0d9081475ff801f34a9e0677adf685f02b1169fe86c683fb51934915ff43921a73b98fb0b734406f8dd90ce6060d75e923b0d3c738291b421bf16de27ed4785d727ce589f5d0957c413c81d6ee75052e3ab50fe53f1abbb24a138a52e1412683992ad769e65ed301a736914843543e2a3e11eb395726d4fcc9283f8607b38685069f63d05ab8bf38aa24d4073a1e68fa1b6087cec44d7fa628342e9d88a0d20b381014cdd1a07b9d913a3bbcad0cfbddd0560617cf26054138075eb86e06db1e68781541587302e6dda86cae779f9848fcefcc33626f8953bfe4dc293d23e74c87020e79e9ffd58ee345382bd4d1d6e5a3389b0a977124708d05e3c305545857041734dc7092901ab54604b3750b3139dd3b8f2bd94cda89d85be3756fda6f0cfb6f66af3d2e36a7808ff7bce271a0272f8dbc88193ede31613433985cd35c7bd9b627d434e7b2e94b38402b8f1b5619a903572dcf4c2b864c6ee66657c9ec81e03fbe765037f83b2229171888ba08651fc78a1b50c7cc52f6dfe8273723e08932b1a16a6b717a80b5520cf3f40e46f9d9c350eaa914bf99dd4ab700cfdae21437daf695916d4f3121235e4913e0657d8cdcf4afd8f2c7ef977a2dfe49f46fef46c8fa6932e745311d4a6eb3124d5e0a204b9e3227e86a55e662f7002d4f4a72cba8c77c3adc3eff076dfb9195cf68455cecbbfc9b5444d9c4a4775bba68d57ff52edac6ce6ff4efbf6466579bf68308f2ba9a59b2c09506064091a86af621e9dae52366a90599db0d64a23944bc48966b6d3ab8e20f4afb5b0e94370d26a89a9c4207b454554e58ac74f62ffb3eb2686eaa596b9610322a5ce8eeb42f2ead1c71b11b51bc4f1800eb549a2bb529ca4a0d165ae461e45b556b2365e9459d531489d59d0dfa544a76c5c00b0a01270741d4061a331c32fd6cd0e68bbce49137b852e215c9db52f3e430416d8979520e5270be324f3d93132358c0eac35a4618ea7aad997dbbd8e99d4ea577271b935e3fe928f90abd94593806d272a565a414686b8e56c28e34b77671de6a696b09414380bc658c69a309d3225ba8493e9076dac776c845ce11a7ccd6cae58fba5434014250f3e211058b2efe3424b991d679a02ba949b086ba12144c7df3e049b5d026f386e4ae712c9b0b4b02730dd6862ed4e72730224cb6ec9101c5cbb7ee4fc30d497bb1dbf74ffdd49d8cae6c7c9a364ede453d9ae25edf27a2153ab285f3e3be66b2968d67a56480f1f74c4fe61dc69db3451f5b113d7ca02e5afa8627f579c07a9b1814853fb8fdaf0c0f220f89725c757f5617ba4e43cb4f3ad9ce18f48f23d10f9e8950b0fd737070655730532896d93df8768860ebf941365d0634db399feab1f8a88bad28d25e689c5a57321debb8d1435130e90a699e17fa5255f2063f09659a432e9ab5f89eeabe12756bfc5e02fcae2b78a9d0f570934b8d4af8f4afbd57549176f465a0cea485dd89c95a8ae915b4b99548a4c939710c16908f968368baf5f547cfee07f3cbb6142041d6e6084aac253a0d3aeac628cfe76f87b94c3806cb14a912ce8e4981e316511d5ede36f526805d6c3fab5b72d9d91f4eacd26e28cb181ec66611818f5c206ddd52488707a940dc12144ae825d25929bc32b718f46e471fdb30762d299b45c84f6310a72b60",
|
||||
"c20000000108afb466a232f7f9f2000044d00582e8683e329a63e5bf4dc93e93e325ff661e74b9cabefdfbf6065c7ab203c8a629534e87e5f2d4c0f463352904642358b8f137e99802c3a26cf22235782a777769ecd134c6b4d0dce6aa10b485c45ccdcf6deb805342e99ef97e2777aee0b2a44073843fccc2f8eb837031f76a8e968cb01c13c1268af095f54f860958e4062a84e2527bcc9b25a7791650a844de1b0c4b2476282a0e00c9de9d39a41914d1e797a88a8997b96b25a4c194762912b2ddee0e01a365f1afa1e82ea266c14ae94e47c90b5679e2cd00e63ee5a834505ca33463751bac22f3b87afc80099335dc7bfd12b7df224a23ced3d2e25b58a04c4b5cb089ca187abc54d782973c7bb157cc515c7508431ff5bdc227871da58b9ca8a9a576960f38edb384112b08e4c70672a6f23d17d9d901342e56c12370deaaafcb22810eb352f1a6d9377e96bdc1ad4dd397dbc6a227b70f204c1a4e9a4db2705763b82ec4df1fab11420aae547155c6b49abceeed997ff01b7d24e369c65f7edf18665d067c7d2bda5ec8623281fce8c77d893cb8a42053756713e910894a58ef5bf3d9f3a41071026660dd7cd05e1640767ec68f78e22c1716700ca9c0f076f90a65cffc394c10a32071c6532d07b59414181070d08c9c84e3d13842718d51bf90dd36ab1b3f708df7eeb3939dc8553787308983c3e9ba971e7d447788477a7140196c2f717b9ba4f5da92d73316dd11c1d1830b4200f26f733a6c65ec1cc21549b485e3a43dc7a2b68e95466a53544082a20d9a43387a7ccbfd353f7e590b7047f13bfc0d91923c2d75dad4f8091ea96502f98e83e5c30e52e4cd5c670f6c2248ce37cd6ee8b3970531fbf0c53c5fa9a0d73200442b755c91fa4f70524ffe8a36063b6709d3aa9f6b53eb0aaecc57a8c8c9a7ac5e57e03e9cfb290b67dd8222a245ff5439914147e2799fd1cd2ca2cb22fda299443b81e8024adc59d098058432fa4bde376b8e59075f6b86427b4ef6cd7c83b5c08add0c3d3543aee8d672c41cb287c1f0a17f1bc30f62a57490afb2d9f401bf302fd473ddbaf63f6883221579743d6aa1f386b8b2f5db06d7d6c36be81f29fafd14b82e863d744f116ce2be4921631f1fb2797289fffa9ee16a3e537ddfa52350546bc544459c0c9d66fdcbd41612cfc0e2744f50927983a3224291c1ae51608fbc00f40c60ec72573a7e128c3415b0d9a7db52de8ff763dd66e2eeb03ef2e67838c9e68cfddae4b86a3f34a69e0a473b5a73ab627282648df7912c11a4bf033ade185a8f438036b99b960aa6213c800abbbd751248a7ae600357ab888433125d49c5643705ecb8c86f2980050edd7e3c579ad6fcae9bbe2c8d8b38004426f35eadb543a3bef42355acb1b94c21d7eae7b6ed422ca0d58fa03b227b035628871465ed6509254c8a3bf43dfadbb247ecbc52d80d65e9c03c4bc7bc35a829502bde3868af9c33737cd88d70f7427790313eed4ed1938955c5dd360212ef700f274efcc8c26ea94c4e2e0937d475c5c4909edfb66714d15d12e153e5586725ce0c47e8a1506bb197366754ca8960508f22fe7b83a5eaa40f05f3cb87464dc6b848080c0e0cecf2dae82bfa42cc6f52694478dc3d00ab0e1ed696b98e26c7fd34d2efd969f83e284c28ce3f27b178f4691c772011f61722266153142dd0d526393e6c6848d201115b256e65f12b911a983bc2f96a5b4b99f63f0b58485a521553a3e1d4498ac5d4ee70c3f9",
|
||||
},
|
||||
domain: "quic.nginx.org",
|
||||
},
|
||||
{
|
||||
input: []string{
|
||||
"c00000000108e63b9140d034563d000044d066e1913892ec1d84c179dfa9596e0ce930171a134a09446a888d9e579a6f7bd77df6deda715b028d64f7866603c6deb468d60ecc6488b5e5ee2e2daa1840b76ead998023593c9ebc4178ec89cb198d3c79a867e27177a74ee5f3db74ea194e36e328047ffc3890192665a6feba09ba1e224967fa9575dc7b094e1c29c7f3be9961ba62e3e063f674a09786b7611138e1edaee32cd1d47839e840a74f25ed786463fc48bf3d38a4c793178ab7cbf5a3eb974415b9f9ef7861dfc73460594332f5545c7b7037043afdfc1aa62ac3dfb76ec2c6ae8ebd351f7483992c762d6483b3e2c1454c8ed939ce43f858ccca22d9149cc9da16af86a010be7f3248cf19fa442e94d625ec7f7144b01ac9afb8fb8c595d4cd12fcd2b2d9986371ae65f6f216bed152b79d2782d60f1f01e06b359f88900c4bb3f987f3ce336854a5beaaa616813af4e5f9bd82dca0af6886b544fff0261807bbd8cf90213299f5802b98edc27a6606be8e2bbc18fa7519eac260dcda139f164796a082908459c31aa964a5d3f6fed8944ad61bda126991468f3b7627f2470179619864f234a395ea3bd4f7ba4c0cdf9f5f0dd95d7d59476f2d2a36521c13886265a2fbbd4345e8d1d1e7b5d01a58fb11de23730b087e2b702200155a1ebd50db5751d279438822ac158173533140998a3056893bf470ac84720cb37a4a3205fa88267abc56520bcddacee06011d929c3a114314822d8ccf7cfef89f2fcf0a4fef800afbfca4a62ee848f22066f68c7d3c5c9a24402d422fc2fd5da6d3b470b0ea253f12a883705f7f78bd67006ade4f1c8a3e8fa052656b5b40dacd8062228871cc3bfb1a9c38472b0a720c3c750430edbcbdcecd46b144dfcaa009fee06770238d0270e80671e8ee5f5df18b86dfe8df2f121245c0710ccaefecbeda0ba3db945c768624dc38f21a4ac53741f4e58a5052f3d667fc466b69905f05d0843cfcb830163fae18dd1eb0ce62a59420db9c44958a0eca9ba4258c8060a9956343f155da6c55b2060427d07d9e311729d2971439c7541ae2babfce25a3f5f361fde86c39ef6c04e4e3cf7dd70c9cc0758ec5db3f0cb368e2447080af51c8a5fa6b84ec3175d2d3e6d877b6953e433b4e94b52e1a5f2a1ca37124c27e47f9de5d4c74644181cd37f3f3863ca529c0847bba91c246dadba94b4566b08eaa06a0db4d58b8cb0c8d3070533306a3089891b24a7c4e11b3aa50d5628fc1d136388e8bfbc420a6f12701333ccdc95dec25d09ce25fa4b654260965b91f05b1542c2ee02008d01de4419f14d6749c4bcfcc45a332ba0772def720ea3c8d207802418137b733e779eb406dace0b4b5f5e5e14c787f3e044e6d8160f90fc3c65bcc7f3449205b63294fbc11e9bb92c007d1cb59183eafbf76be9680224cb442806500d71870777d087bf864890848f4a79424c02304f2a6ee2b07f9257f4a2f185ee21239625e246cf680e74b85d292cca44261c6cee6da39bfac3882d28fe547a500f79519ffcd3f54ff5a905c99f22a5e8142c903c41adbe1eb9770b6cf554688529091b126ed2168a23bb191c2b89728e31773623bc58bcb9baebc2c664c79d6ffee7e4404e039723eb05e7f7835c87212431a0131603fcc3fe090cc2fda8239b8f42188b35f98d7fff949b3044544b3bb962ae236a664d76d0c751d9c9ed1271715d240f111febdf7045502f2afd7de8aaaac650511e7bc7716a5b6622ae925abb7",
|
||||
"c90000000108e63b9140d034563d000044d00b2498988864d8b7f59a00d26165f5ae638fc9b1c12d546ffd86212ccd85f654259cb8b8c9d753c696ddad7ee4847bf3b3c10063606cf3972f75e17ae23e73b6a3029f23541f674256d19677665cdd0b8ac15c3f60984bc14ff5dc7a9ae37395516204f2020965713fccaf35cb0a5823085cd6211d681dc6b39be9db46cbfef154a2b9049ed202e9088961b0b710e94bd73259b0967e4d6b8cdfd5b72774fee2f2ceb16bcafa010f247c43b0a9ca25578e7d45bfda7edb82e91f8e1c0a2cfa990223bf97ece42862d3f329521fe2d12493b717f174f966d173102e5cca10943d5b612101d65d0dd48b44416f9ac1eac4575558ecaaa39c47ade2dee6e25fd219d799b499143b47a5bf449701b939c1dde111349cd0d63efd2ff74fbd3573ed40abfdb2310e2740da40fc50c7a137a3f32c3a26b3d407f80e669fe7f9a3542fdd412a9cb53f845d9c1af0814377bf92e30f05ee387fb8675807a6de083c85d3d7860601c8170923c53e5773ee388b68e510a28cd7009c485bd4cb861eddfdd265de042e5a018d20cb810614e2bb17b0f52d6bf620a6f173e0b41951e1b83ffb29e3b3b3c5d9fff13acd3b409021195201d003e281d8cda7b0f02c273e17b1f9b9e8cec4296d65a1c4923b78a2e4273cb42e4e159980472e440078e542eeddcc5a9bfefa5a72871fbcd9ebb74fef20a50215bf75cfd8572d5ab9ac5945e8d6ca35884caf0af0446ee9aab0a1cc3a452ec79c9de786119e63bb3a75fce0ae29c15a0c320fff87e87cc23a05e75b4f4b30b75c6aa036c4b6657f8200ea014185b31ee7fcd00d1eaf40973f347fae227f89d41794fa57ac1ed1efda3ba840ef27852cf33a9dc9e2d77b56af9ced9e75707837aa8c5395cdc15134ba132de87152ce53d506c53284dab912bbc276542504cc94afaca71a5173ff13ea6cb45b47dde9965428ba5d8eb968cc2a5729c2f9b8f1c1de208943a2cd565196e040dcc415d769ceb6300c7909d7e32bbbe83c4cbf4d49f6e34fe56b651838628f3a0001e99f39cafe45c98e455aff8d98f89942a862f7505b9f7fe3f64dacf8c574affacf91c2c05f094127acaa5187f9dfa188f67db421243a02e583942138c2edf45fec4c6b6a8a791da9055be247e9b252e9f7c1330e76f9cb3aa5feebb21f871315b5fb90a1df0b8056513b74daeb6ac995f85c64150ad115a14830d145e5f4e6638c26987b676a1dd19a9775df29ab442ce6143b0fbf8f8d4618084896e34812ed59d63041e2b4ccf6c959a6c849813dd926082bb7b1adedf69246547f335552bcdbae7e466ac31e07e442530ad114abebc6f58015b786e7f35644307fa7ad3d9248c56c8ff472735c6911da1843fe53821b8f5180f8844db4a9f7a826a919fd93c4db4d25861054929260dcdc46d085827c46d60f1097424a6ef250f5aaf3235c80230eda4eb580ce93e1ac8aac422a7aa1241562af601981b84b74949f1c476705c8030eb5d447b2414f9716ff3fd606cd750030b94345c016078bdcb97b7ebc24f661fbd08802f32df18d6a2aa85bfe2e9b8dc76b121c44ae9f29e4413051b527e99fde29720724337476c0eff325cb6220a290a9eb852151c84836729d6a223032e2c638857d9e7f469b84d7d650c45e56e763aee73f902e82b055425c4568725e2d4efd7fde8b02906bda48af86bf47ea27ff00f4528494b74be9bbff001cc841449a184a4e00d64e51a72660a2c21f704f",
|
||||
},
|
||||
domain: "chat.openai.com",
|
||||
},
|
||||
// Fragmented quic and 0-rtt packet sniff
|
||||
{
|
||||
input: []string{
|
||||
"de0000000108c2751a596bd51c6e004041948ab7d9d493e9e1e9902a7734534fb9eaddc70ca7f821d1b58a406b23ba9db1d03266ae74765b03fac21c284fd50cb0a3d1ca71d8c3cabef5553dd1cb748ac662",
|
||||
"c50000000108c2751a596bd51c6e000044d0538af4ba75e226a6fc7f43e7f1f59610973b8a6670bb8338ca7ef7d90f81aa59f179dae5f8f6dbd24ec6fe576b28f6ce6cd46f26de143b8c99cdadaecf2041948a61bd5a8591486e10022fd100aa20e6423b4f4ca5773edb1aba79b73d6150ee185e66da60e658b2a698098462122b6b80c7fbc5542b0b8e9532898c1f31aa2ef55cbdf036d74c3069abbb261660f048d950b00b7db279ec2bc39912102679ddbffb53f1b1921f137fce43e164af86c72908532f4cdc48eb462a9d9e9cdd6d3c3faaf8aa8aea312dcac5d6aa75b1ade4af6901576649da7e3efd4199b92107d7acee8bbf06734b2484957c3d8cbb1f3fc0ccd56c55223628ed8ea514ffd101bac370c97b28c7da81175ab0508c0002d458cf41f7159dfce22b447c1ec502c186b782c1854718b7fc0fc39e5c09aee31113fc4c5003803fc27ca48850c08a54dbbfdef6ea9a6a138cac0ecd045cfd5607cb6c99c39c0cb21778857f97416b78fa7c6ac8ae3fa2ef2adb3b85fe3fdba70ef9265bb3d54e56ec68b8887d54d02d4a571a6b793ae4df8ff171c881a554b5c5a7848351d446ab94c90ee9c600f03b785fee6300450a4ffc2a55d417952e15449a491296d463ac6942bce4ca93c99440396bc8984073ec028b11ad412e97e26f9248031dc4b1a6ae385803bb578fa1a3b3a58a8ef19c6c511f17b28a275e8c40e51fca8f410a4a1879b5d8749a44a6a9f97c0c9df25318cc28fd0cc61eea78ddc603a17e74eb542c8c08cdbaafa3b44566db4d67e8d1429332375cd30cdaead9594c46d8ce91bce9813c3ca23f55ec2f4dd3ff141471bc3df590367bc65e4830018ff7d845ec4987d11e471d114c48acd1ae9b7670341a34077ae59ea6c3bfc4675cf419d37db48a98a5573b69867039731f537098b46415a193f50b2c85bf9e5da45d6757c5c366e21f04ea62d64b81c28be5148d89e53535414067cf609e59686b7fd135f5cb473e57f6c82dbb291308a1065e0f755935d77517adecee55e72cf37ecaab1b5c0c6e0c7463a014e7e439757913f6e43abb6af775d21ab6e43cbdbcd1935a000cf8025ebc11378d86d6f72d51bf2dfe4be1db5d3b0fcacd13e1b9fbaac6e9153c3d1f4e876f2fa9c3cfc84fd0910b778105b66be70827b1830b7b3c9633af5d83ad527efd81498cbbdd112873cc5ced573e6579acfe817b62280c2122b582b591d52b96cac047bff91192a5cfc001d15c811e055dcb1c9710dc892258ed1ab5152af2cfc57a0b93205dd41fd82b86090b4281b1493a8828ebc96bbd603b888cbca4a15799a5f3eaef93655d5609948080ca57c696d0ffc9a07665bdb063b547bb5a862c3b058c9efb2e7b79cf405fd83efacaa4b8e3a1fd126270587119756562c03d69a9cb67550369030a0204e531cb8df91ab2dfa2e4106c590c59b1b13c447843937929a574d3ea1785db0d52b4b2eeefd1a07c69729bec7c2813c9eb1249f706b3cc14a3d489d6b42a641dfd9e91aa70c7d3222e154af2d7fc1a8f48e5ba11739ae128d1f32ff929aaf4b249df5ea23f7847301e36ffda02342cdf1bd9dfd1979cbd8de32eb8b1eb8c415ddd267efe53f54678d9fc32435b34b00ee2256d8b6190e30a280df5bc48cf9fd669a52469954deccb0f1da37371d513ea57f31ead22a34f9379c7931fd18286d9fde6ecfaac8ca2a9be79d688c5401c65407543c066532f6621f256551c4a98a86b543c576ed0f3254daa4915",
|
||||
"cc0000000108c2751a596bd51c6e000044d07b624bf3d95fb3b7299b67dd836fbbbeb05a51650f9b2da3b2695070a0d19ab0d5334cc04de7ea7494fbe6c438f4e84fa56a3f246132468b5b4f1ba0fcc0251cf278338e15fdd715d5bfed18c1f98ca3cd3cc7b6f904aeeea2914a8b998dd3ae7df694c49c1742dccbf4c3472ddfe2e447959655459c11f18bddd9481eb597b887fb3f90a7d0f05224a144f87a5fdad502ea1e46c1c9f4b4154bafa4542c026296040228703bcd020202acceb772b596bf788341cceca864c8907037c39739e511b04e8ba956efa0fb5cb151ac90eb5817444f6488d593325ad4466058ba45214b965c5738f33d5591624584559ba18e89913b868619d498072e3aa1f333f5d6e3d1db88b28adf7d9350c3c383c1eda894f36bf1bb2a58c7a5e5c8b20597b71a099e46bbd3d8894877e43b0183919185b4e9f059472203979d3334c535fc4eaedebebc79bd1e423184765047a50e6dcc76ba2b23ad23511cae2edd2ad8e7f7f302226dbf6c0e4dbc8c08cda26340b9abfef1ef3333cd511295f14c87197d7890576b4076dd9686047854e67733599d96a99194aecf7b927cae2e5fa4568afc71e748dabd3bd71e6c3984f45b06a068a7c9c3a1ca7b5c245a9bb2cc7e2726e833e283430a25b6ccad55bc5b7644b44f99fedeff3c3bbf995a0387cf1e45a5684e5d1c01350d0cd2d615ffb6d1011d80ad16b75925efcbee483e4e2c0e2386e9e1b35b5a107ed97058adb60e323342989559856faeaafe5149bdcd60c113230f9923b2f654c95f986944a014198686f9c2275053c05080e3bf9fab7d46302948b152e2f2fb1ecbe71b412016b3f25ae512ad45cd096d5f284a0c2808b5eab03b4b9b2dff4d81bf234e75e30d480f39a5f9737563e31a19b14d1038296915af33e0ac0dc18e9c871e539e8772d525e5fa19afc582b1c00ae573af39fe293e16d182bbe57af5bee1c0939862ffb62e3d52a60aeb71e4db2a4a1708e75afa5f37d72cd6c0e036abcb4eb8db6515fbbdf98be95d0a6d261a9445797a8f38c3579a2f04c9f5b74dfd1ffaba2c6aa05959704b9b8cb0db30bcc360711c5afef0d1e7c2b076466dcaab104c70f3cc0cda33d7a47462c3fa3d7e34a99b2d8ff3fe5cafd27ede28b9e09b547cf955b97b0d0d4ec126957601c6982d176252be422df3366118895ca25fe27a96c9c234d484fe98634fb9e970e0d2b096f2ced5d56603505990a65363726c828aed2df0f112e0c44f058424ff5c25ae60aa2cb5fdfa289e8ebb63908365aa4e4609eae87e567f1e86d92c43992e6d505f55226fe3533f9fc9c9facff9dae02a3e3c97ca54191bebab93881c0e89b9de5bb4acd5c6fed5b1e7978803f693bfdbc125b4d08fd34fdc6aaf02444c4b06010b0eb2f15d86850a7aa5af05af438f6b7345fad4315f631bc5b017c7482e7af725a09844472f48e4de79b15284932a7e99a46ae72b187ee3faaa0f31a36726056e86eb706bc8eac04b68a3302307a157c91639f30bafc2d180670625673310a9a45a171063011e59c57c8eb67353a8ea344a87853e7b600c2b49a7a1b60a2904c0ea55951af6430667ecdfa6e90a8d2d0ed9857ba5b876cf78af190d5013d16208d2b30d02cf2c23e6ad1466f76c30d11034d5d2eca113e2764b2fb6298fc4940c16d971e28e3e6e5d0e8eea1ccb9b4b89741ed675861fc3680457ee08547f4efcf68bb6247313f8218ae3ec372e51ba8786ecae115dcff241e0",
|
||||
},
|
||||
domain: "fonts.gstatic.com",
|
||||
},
|
||||
// Test sniffer packet out of order
|
||||
{
|
||||
input: []string{
|
||||
"c50000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a448875483ec1f071622121a49c456dc3ce16bae5f61f84ceaef9e8b71db56479845b764507dd9416e8c44b8c93406a230945eb8e484471c1b6207c9afd944fa0fee555a5c966f27ccffb4bfed37fe3936f2c84e9852c0d46c7e2e94b897fcef18c4b0b83d966aef75c0af4240325a24668bc017e0d3f69680ea5b2f59bd0b964062bc40190be86aef3ed0716a18a67057f309faaf3a040222812142a399deb72ebb330d03d59961e2ca10cf78d40886dd094368a881db261068920968f6adf7a7b1266faf8842e71840a29859e877c66e3ebc47d7fe3ee586b6512d9b0e1bea82b302647706473e68dc8209f4e9ca19f1dd25fe386e62c21d9c741e75cb8b11606739ba3de6d6325ee3a9cd1bb2b9613746140ccdaaf936eefdaa1ca7ad73d684e5d82b1ba1dd3356ca0c881f6eee72c02c8b78d02a8217a8fd972e463c77374d0fcbb761459e3ab0bb5492e516d7d4304c19c16a4bed11ea7f4e75616a26a7c81b04ffa580cce04d59825b8ed929578f9219e64bdbc6352ae6e4150a993fc3cc27ce4d66c62893866b9053bb737ac40364094b53d91e8b325b9dab5f537af04f10bf8db644897b0b03b42b1bd6c3aedfe018a6e4f6533183649f4ef6a6300383430f86e802fb4e51976d056a3c40c3b53c847b8308cfbe54dc2d20b8cdc870c73f5fc22c376c35d9a85348ca6a2288ae03dda6b97f0f502f35219e19cff3a810143289cb1f0715f8785028f887bf02c656c9cc372bdc419290f05957ad3dee82b56db352db65aca58e6fa0bd2f753160dd9e7214968c0496be1ab49f978a9252e49266939fedf542760abd653dd38b1659bcf452c753cb89e8235bcf732afcff8f524331be9b6f4a5081c81255e68c358b3444fb1d57bf5659d86b6674544fe2826ca81ee52f93a17b3291826678e488c3074c259223845e4083a413af7fc93d9992823620a8d29d321438a760293e36c4232216207060dd3ee5c4036250ede71ca9cbe335a1e068eb3ff6c10a7f1c8204750d6d0f3145014949a7b4e88a723566ee5446f960a95d9f81cc45155443da561d85a3a311df8172a1c4eb118bf27ec4b3cc4573b1ab421d96d41cc1e5557797ca68f701fe75c474527144d30b9bb00a117637f88896b0b2dcb9bb29ba144ec384b5a085e82e7387e0560a4621423c306b041ad42e84928ce23bc2a7f995ef5c21616de43be8a1657847489b32c8e364846389e7c8cae99530c499f3662a2ae7090e54958ba940b5d3eaf1333ebcecc7f06f29f68ffd97defe65017519c29d355ecb0a4b47ab08dbad8cb0cc5c86de65dfa703110c60a0c55281925018fb4ef49fe5d0132dcc86602c2ab9921a8f3451480d3e931f01c2f9a81873435bb83860128aa78dcc950fb13e416d90ea969aa92763f9caefa0fe3ef4ea82e3af4a3e717fabcc589fe8cb9bfba6810ddf7def8c1445fc0048fb07be043a628e9c920bb72c04d3b9472caafc6c14bffb854a1ba2170dda919322a6d79eab92e3a88888a224093946b87840033fe41941f780f569eaf1fdac55e36b74514d72d09823d71f48f5d5f0ceb7b6d69c5da0e0408c1b13c265d4775db6a0f952ae72bd5c277b22c4be2f2728451ce31e921c856000d20da0489103bad6a6ab4",
|
||||
"c60000000108b4b6d5c8b9a19769004047007e07df0d887979774085206f2e7f0146b02a8699715a54fc71ef27ab5a9e8cfbf155497bed9e25934aa74db1b3b270112472b7bf7587423b3ab2aaf99de34cdc591bfe04cc0a44884e9e716461869ca408431e1ba92740c598aa74e9cd45706f28942f9cc64dbfd7c292cd33e82b50ae0e2e08dc478c19886718cde33e56c38517f8834d64904bf4fb1d30650caedecb9567ea8ef50157c287a2741e98a00f8e7e19e76bbb0143ac7862a49393f17ec66aa0e2c02123ffb5abcc96ccf92cd542c8f571bd7a4382ff81432d11f83796959696c38f2029db6c6a536a9ea24b74c848b95882562d74739ac95f5a069d48e8756d1a9750c7ebc23d4ee22d617b29b415b7458b3bb8106c22de3a9ace9ec689e6e00471aa33e570f7481d15911d7cf46a429cee1a416558c5e78360795d905ff1e0c81d18fcf4954131fa5b9289ed2291e122cdffd666c66209aa2cab01730739249ce293b3ba3abb31683c108bdfd51f54593f47411077e948f01105bd9bfff1578d235674e96a8b9cfdde119edaa960b84e70fd681312514151de1d5939c79abdfe4953e22be5ad3e6e242d0ce9b3f2e589ce3c768f610d4d3a32e33225d8a5ce2ad74a9b40859cdd9ea99f14fa2a7018e4b6aa6e46a0d73d46d161ec5d3b30bd55078e23987865551a605a33472931428ce222040d20c07d1ebe970e576d9d54ae688a3fe9388adda3da4d011a7cbb604f1f19d2ef1be7ef4713bfa84d4d69ffa606a08b61a1ebb99aacc4e19d0c5034642da1ce2d7d5abecc8adbbc6d7f72ff2da4ce5228ff8626509b38e17b31717c0b7821558b021ba81502d54da7e778d4526367109333383e7c67d5d5bde86bd4001fa13a703ff9259e1c2268ca8f4ed2e6c022a7466e2178bc725f59792803ba28c629e3df7696c416dc294b510920077b2d2b258fdc3506c36c42d37796c8fdb20ba797ee68fdc410325a355f6c1189aa9fc9ee220d42186677e3955cd3c844ce505cd601f04201cc390e923db2ea6407fa2fb4ca7f3f82d0a82d52697ff5ba5d4633bb0d655d7ee3348b89c9cb42870cdfe7c0c162babab4208a9a54700c5785d4134e9e33361480e3512ac8b556e11775536e90ee1270a4cb4d6bf2faa72d7e1f23ceb4fc3aded0e423b6be6a55bc25e5a99163b4f5f72ec4a24fe96f68c739d1848c92c4236a5a637d19871456b8dae671ea6ae5c16ed4fc257612a0821e6dc1cbe2ef4963a1436925dcc4e6ce528fa75e41f7721b379fae8ca09e6fb51d0c3e3ae6c19b98860ab9f74013146c6d375656dd1f530abfa64670a510390e9a54bb9a4ad19977491377c8cd743597bc156ee3f58cfcafa5a547b20852749e66fa8838c100ebde039ea25c8ec32b0c6325b793797546a095e79b9388d8e67dc6b4b3892f93ecd13e64ba4b2ad26fc810fedc374b831921531344c581927da9ba822bd625584d98c7582759ae40f01e14277a0a13d30c2c12536df698330d8aa6a3613a42c493c42692b468b4a2cc6bb6dd45684ee6115848110bf517074efd93bf212c071013f4359f140cfed17bbe10328f2026cb8ada16427122d3fc8a933119a1e3e4cfe2b95cbc73af5044cb099cf34247228972495488ebaa4696280d17665c421be5f1727c5d5b013d8aac0e9943bbbb7fbc2162a4000a306dffe3bc4425cf272f1ebb63c8e4998f867fa6b05d71a8642e29392244d4e2e2351bc149d665efe1b9519cb1b15005393f938d",
|
||||
},
|
||||
domain: "ogads-pa.clients6.google.com",
|
||||
},
|
||||
{
|
||||
input: []string{
|
||||
"cf0000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f9944884c88b10f198e849dddbc1e9f9bac61f98f67f27d5452da6e2bda1f5210a145b1f1416ad2fc15e60aa00444362630650bcd0ee47999b689a40100dcacf40a4c3d74fa6293d4a5cb0487d8c76787c04dd2b47ec7718df5a2dc6942069062617b3d40a95360802957419433436c9065bda5a6156291d909a079b6d3819941368d7e17a2e97e36be829bb421b44545af47e37d7815ee1f200ca28ffd361d955ebe0484fb234a7e8a7c68ad824fd14d517fa7b35f878beebaf3dd22bd9f7a39cb7e0fd8369cdd28c05a06323be7af0b2d69ed2a2f4ea9f25d000de71bf5bd6765a20ddf81d976cff2321f1a4584ad6c4b7e9a42a6d4aa3a02b59f7d994a8e4a3070a4646e51fdf354448420ebfd0aa9118d010d019cc168f2fe5a9ff0c42e6091676be11f28a372ea97d008a1a02efd58149106cfdec7ef86f5416c4b1a408d8efba6c8d4742d781374ff0a1a8ac183bffa1345dc8e3a7cce04f66cc865f434decb912dc9e8e811eb59b80d3e39d5788639ae7c5ede73a935edb47d907725656be0522195bd2c099b0241f36664fad1543e4ae43862252662707fb424a8f5f9486b8e3779ac24bac457671ad664475d1fc9eb1de3c46f624b559742b3477953552e44f20cc1725a11ab915423fdce7cfbc8dafebc0c43d1ac3d3373ca2f0210924433c46e5fcface47a65579efaa1999d52b2632f69c33c3c63537c01be68fb679f9229f8f68c5caaa23dc4c61d3c45dee90affed984dbbfb06b2659447400b4dcbf6e574719e8d49fe0dacea9509182a42f6463138d8693a3b8d797d3bb6b0b02648829d666341373939ac41a57e90fdc2469623b6e2d772199d7c806d5998f439603c0de8413f9d29f79323ec5410b409ab8c95547ab50bb921fe0c407b7aaaf663389bdea5ba56c023dc4622d6dd9cacd8f318a6a0297d041cc6ed455d906be50dc85a25ecb32f4a565432fec9f359833be1c6a6b7b4bd119d3c4b29932eeec8d140dd467ab4d969bd23e9d2a95b92835587f32428f957b6785b8206a4834e00a3013e0b6a5855f16207268bbdf311572c54d2e6ff9c659cd02c258f494c3b168ea170c69138b63e0dde487b72576e87657befa44548b0b4e1e5a837dbbe66a559cd1df8f2151ba513930243fd2b7705bd29b183dff966224d87ffabb74017d634ab2e4b368052504a7f6bc1c62d39a29dc2dcfba683bee2039e376ff391abbd13a0b89512fd8f6a4e66051dfa04e0e1a3cb4bd56a9b17e27651873bf2ed50f65cf1cc608afaf06fe7e6238347adb66f01d1f0b9b51f0078615553cb8ff8d6786b87e19dbc44000025693c4b34cfd695601a680efdc1e7465a981b0f028cbd3dbb938789f240e39223290e34ec303ff5c78a4a637ac04dad60d744f82e96c3c9e8ed6cb0248ac73b5b3a92007edfc1277c3cc6fa1d0045c1c371820f06bedaf046dd999665cc4745ddf8934084ae02e9238acae6dea330b5798e046138f5b15011875eae72d6eb6689e56e0ac5c5d9e25dc4fc1874cf37265e68ce5b8630b84ad8dab7704474f0bfd08ac295b3a508284fb6ff201f0aee6388d0e1d5cdaaf4c20429874792109f5b8e2f3eae6c397e46a510ed829a6746e523481465f64be4e145c83d6fa6951229d3",
|
||||
"c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f994488828e6082edd53e228164d8862067483762ea9523c90d565b9e4b185b7805eaf8220664264e82a95164ab6cab4fb3f5e795e246e7205aca236b3c94dde0ff4fa66ce0924d654829d59c3eb690470b20c5011c739102257e9c2247dee67c0b98190d0015154d31041aadb026b8d3a828c861a15dccdab0cec8cc99b8d6c2acddbb93ab66253e87ac39016507dba42e8fa9f5d22c7f27645a02361842a59ebb2eafefd0f3b92bd9692a96b93875defcfe2796243be8861c59ce5ab03f1d65d308ae456cb9656da1f01026ef0807cc9930021b29d69b36881c3e7d70fc68799ca81922008db93c9ca4a365ee191e214d9829481fd430194ad4583a0ab2e920c25244d7d64662872b3b69ab413ccf0dfb6bf2ea9a9b93e04ed19f8a0e146613ca9d511179f80aeab40d573590d38a7c10840e3f8b9ac1bd23b0826aecabf6d1cdb2aef02deb982c2029dd6d8bc21da6c262c8116b7b383ce8c9eec69da3e16c044dd96ae08a98595d128e89dd55e6dc8eb08b8d51327278027137f60a0e1b42878f98ca898587474f6d509c3a58ff4dd7f8b10905c200cf3170bdec725ee14a1ac8ebd1022509d3e499f5e72168eac43264d7246daa0bfc81a216ca97730b7e043cad8d8a9af5c443a5d15e9a88d82b6750c740eecfd63561712a185c69532b1a18b23513d7cf871f14d164ec544f22b6a8cc77d6fae5fc6e47eb64f08617098d229da78a378d6a0864684a7978f650c7922c907f97b0ebf2be29cc834ffe995c9636b310f4a8c2c5623c3b7b533518193d226923f111da1a0e8055b9053ad7f7504d194fbc3ae2b41cef30aa099624d5e229ffb56d5883a5a09163d22455cac52e37ee0ed5367b7c3bbcd4818a46b9b363b592c53c780eeae2c8b80a1d60d296614c998a9774f76453a58bc55d1c26bb10dc321c159858d7ba2f7855ba01aadf3585632c097e5471591dcc24d87e9b76509c10e2710310e4869de710ce0f484d326be751f8e9f765a685312423f1801aefb28dfe0c8f286432356d06857101a67a432497c5849111db2792fa0ee4ffff49a9124c152bcff82da1951258f989681e4f1338357f2c9f82333f6051b188f640bf200a0a75be1d35d2301e8d3813f7ba1926a28a0df05c21413cc0c4090c1e4ba4877dca8e129876c72ab3a801b4093320f5f685120680541d97889eea5dfcaf07a7ecb00c0ba0ae193969a4cddcbd753609a5304ea88783358ab0ae005c6af27bb58b2c4282186461ea50540845e2e2a2f4efced88c8ab9cd9fb4a226a265714c77ce7b79d1a40bd00b24cbba498dafde6bbad91686cf2e13e75669234bc342218887ba910ac81680122ddd36466e7e8a983d5a0fc18a6e9a386762c32132be08abe5554e334ec7d88734cfad9a378553b71222c55f6aa114392e015dfa2bb6cb4ad241c6bca82fdd0a00eb8d6b4afac61268130dea2807a97e4c0adc0e2be39abccbe64dca5c480e09c4bebb8b598e4f60afb0e92dce710859013b1ffa9c78fbf380160f31b1e72340dea86d353ff0e95884b72e2c2c10f6eff5f36c588ee845b7bc97c3b6bec4aa879dd0eb56b838b7bc2ec6e66a5b5517908197a67566dce7df421a8daedc98848c70d1d2c39b2f3538e6f17800bee3d3",
|
||||
"df0000000108277148f2b916666000403a52317841946860def0d7829c06fec03ffe8b97f84e10116fafd93d1d2d39bbfda0d148778c21bb1e1667eb789b1ff70c2e3d557ed9c31570d20d",
|
||||
"d00000000108277148f2b91666600044cd5e860e3fa7ac4769ec75d9b7d20f19e69265939a42afd3c4248a7f5210358a044f42869567e72a05642e96ddffa67bf24ec2d966d860c6accdee01d6917c8c43d4d089d8bb63ff848b617c13fbeefafcbb049ab0822a9ca7716c95af84d019b755b145dfe43c218555d1a7e047deda7d8db352a386b2b6d03f2e7f4510f47ff4ab199348dfa81c86bea5d09d7c7af4ef3f04e99fe4e6c21d53c4335407e27913129152033f17580f97d0345c8487a7ad329dc5c97b298ec7b80fee7813f1d6f94945a44ff662a69453c2dc7ac5e8a1cb90400e63818632d7f9654f140a61280df183b3d9f9b824e53d82f2c14ea3de89befdc79b84a4a3eb659a41db25622add94f2ad4b0d5977f1091aae0a4b83c7b41bca61c6c8d807ef02a8ce6240b76d442559a8b338b39418d27e99aff38840fc79a20995af65b3bbe1e3177074079a47578c51655a4016363364fd2c108d384e602deebd022da3c814549cd57d73c5bfc20e279045e2ad436fbd7e7c9e1985f0ec2f422e310e7aa8cfc48e637f9ac61d06d6482cb40b4376ff3c7abff3c3c26634689ae16d704bab1343d6413fc7b6c076eb0454eda2e0d1e077db40c922ebba6b0b1fa814e3ba76d8d6c4289abbd655f0cf5968eb2aba7131680b44da8910056a76647a6dfea95f27364a7ce694b8fbe19ebcb2a47e7350d33a36f7f5ca67af5e934f449125f4aae870a5b23b4370680ee02b194784d5d188ecdf58ae5454221406bde0ddd3e50d3363a564d6ca9fe0fb57d4df8716cb430cf553be573aa690e5645075ec74edd38cf23215bd50bcda0639dfbbe08dd6c476249e35da819ea6ccef808911b0eef6efaa4947244472795bc071d7154ed87e4a43575b3d61a551fcfccfb7ca3edaee9324f33f54dc9809747e59e24e79f256e8e72f01b8647f71c4b9dc260715fd9d83d3dbd9e124c432c04b3398e74efa3869fe129e368c15b6ca234a243fcac675adcc1db247e3f8485ac4a78f4a1ce2db3b437a1960b02f0c227901d165dcc05abdb3929a80dff2eaf72816185d4af4e28eea05430b736ddd2962e03ec64fa48649dd610e0e221c48f781b45cd9963c176126110e662369874e6a55f28039a23484c5c53714fc2d2030b48f1c895102ca9ad8acae1ec4eb0ae8d8bde31cd74fc515930078d22ad07dc3c7221ddbc4027c746207fa038b31080714091459c9a66ba4f5912d8d3905d3a9a47e4d8829a8110c96c0c9c81291c7985073808814109364df15b04520dc07e8d67cafcda71f0ca59423df5fadae92417a8661b3cdbbf6b1059780fb8b43eb4dcdacf731bb8db26294f978f6be7506b87d17a95367cdb83000565a4986e66dd60d0851f9b593d68790f8097434f62ea7a7396017c3c84754845d3a97f028cf8697d929a2826451653ccf84aba4d2f40fa530b258c13f08c6523c3c02d9669fd46b6a51f20ad323857d767150e3530a66bf88976dbadf99aeea549254c07e11e14085979b60f3b7e1728a4a2d7a35b0377c6501ae7d1d4bba338fb51a17ca8f7e698bd70cd01e8f30edf3e83591a2eb0038811e347bfcfab159b0d1ff6153e0f9ee4c129cfb7687e30b82eed74130c466eee06506dde50805b58c2acccd4cf4b2cc86c52fa2af602a8a7064eb9d90e1c568373b19e43ef4e7c1e4e1c9a58ccedf80a02a46ed64e68e72d4e75c7436e2bc0ba59f95a00456e5680af9e6cf4bf3a6d302ddaf8847cfd5ea606797",
|
||||
"d30000000108277148f2b91666600043d24b66b2531ed9f9c13b07b2654186b0410a608592fdf728479734933197ec06a1cde860f36b3170fb2a9c85c62a7867ba6520dcb2d0ab2f6a484d9ebf8237d7a6f3c1fb16c1e0458ccf20e6d1b298a7530cea42636166027d92812915e76fbcc436a5e414147672dd7b0d19ff24513800e63cd86984f1c93ef1430bb848d37830eed61675d7c9999b92c6e5796d384554c74dd5a163de341ab309d6b0cb028aa08e56d79c60980d4a49a1c095456ca119fc3f04e496c93a084d017f60c6e031d6e9ad2e4fa699bb4b0c92fdcb44131129db0d30ce9efb740d3db0339127d9bdd1d4f677b1cb532a33647851ba9bb20bd8d6aa593271a85c3a9dc9835065663e61faa8dc6af209a0caf183d0fda3d4839d40edd5659dd053778642db8fba21f1f793e45c5c517e68bbef8543e3a727743c7bf87d047d441d13226b9021fac56904872774cf6768dc91db8ea489a244500e9e527acdc0088437357acf9397b014e66fef2db1248f9c6a578af07d7a02b1356fee02e27b8207e57633fa7bfd87ccd382e368c14b946aea780fcbe696d6e4fa3aa589184e104177db2fc3d91d4af120d9da3bdad021d003796b8261b590d8113f995dc1db4fac1c62cb68370d41cc87c982815017ae2143d5a469b742d019e5556d813877fec9d021cb37f80e5987d9f743c2b39093a34f6654164a8185a5caefbbef8ea17f62f6801a3fd89fae333c878cec9b25d10dfee2abca65d7c909ad2e4f11736d13b1642df4c5a0761f8f29f35f37def9ed327f4a9d8e53269fa6c7cedd0f4fd67d6cde81934e291d9fca695cc9745890cb54503e29e09f4a30f80e2f574bbbeedb7d20481c583d8362d22b2dbec09494095a043cdae283e86f905d8807f7b7c0f06ce968487bbca1e20b87245b68f24537a7c7e768c838f1bf26650afdabec2c0bb9736b345473f279c9b73ecf0d2c4aea49330ecfef0949ef7cb81861b05950ec0772db856365b136ba75d5509c01d7a970c84ebc77d8d5c3ceae1ef5f3079afc7d78965ffa3bc4c64ef1b4718ffb488a571528c83b615c43022616bb4c494c838b556df5ede711a688b0315c1ce6e2892247df582b7c3f2b06cac0bd8d670e2b581f074750596ba162189060b8af3dfc650ba3b45932edc4f94f08741d3072bfd1ef8159b27a7f3673a4fc504304c12116e3c2d7636c663c9fa1b2f5571be88769f33ccb94a09abd9c5a7dc8a8c2031bb2bc256b84aeb68a9abf7673151cec41b48bdd74f395a46acf30dae43e060e596bb2e739274210701ee9bb6cc3ff81ace751e375a01f17b3c5cc5f1234c488d69611bb27f6e3ee17e3c3843ebe4a280d6aa8ef017058a872810a437f85331adb3cb8d382650897b1b1589ee6",
|
||||
"dd0000000108277148f2b9166660004053972df1beb451f73eb070e33ed63f681eb9b7e1e03f20baff3f54157598c7dd90a0de49850a3ccd6eb1b1cfc9dc6d3ba9ed1c0a19c69bf433da300d3cecc4ef151c44a721d680e3e3aaaf3eefec23091c5fde22",
|
||||
"c90000000108277148f2b916666000404700403986db57eaa4b165be8ab9c95452bddb922eb35b7610a8e664f6b4620d870507c241290ce885c36d7672c51d94063bf893e01bc79e1d81bf023338da3d22f63bc7aa433f99448825ee873013b006b2a6c87c7581c7117bfeb4ec3d68405a68d9488f6d58474dd16539677e869812ead055e70d655a660062e17083995c0dbecd565c79800b11c8ca0c351ecffa61e707d62d443b3810bc60d4ef87aa99b979ff55ee1ea46b65436c15534e5315113138aed6daa9f04d3050d77a7e379c83b948d3797177c1793e59b2555423bd52595d93e293ea8ffa3c428c6dbba4e202d76933caf6a5609b0a4aa6cf4fd2aadb6505382381ef2d5b33efc43eba24c84b7805baf2ddab44a50180e5e6f2a31f9ea8089aef562d3b578a799d61befec99c016fadec3363f68a1be4ca1e13e8bdd2809a1dacc41134663e22f21978167c5ee8ef49652ae152fc6c1bcf52109cd3076cdd599cb43261941de7aed148d7d3e956cd615549a9647496f43f998daad4c841cc40ce1501fbfc152b957c94be558f6743061e312d746137db2ae6a44e181587dbf6b0d9508cef4aefd99ea5d3369898bd4c3df5e95ac89eaaba54019ffe0402b8f567c91b9371e80c621c67d3c831331acc063892bbd8a81cfc0498e78474b11e8c05dd8f540c449505342ae95f6281940aae973db35b8e31ff801f6bc8975f592538881ae9cc4cedcbdb39a784a9fe962a1f12be51c11b91d4dedf649bb5672dec8e03db97b0d69fce36edfbecb6836644bad1ab8e6d4e13644d9c3476db0e8a8eb4b5a5c32f7a5604c8e19700c53602839478531579cb4c4bb5cc969cf482f325dd837629318baf128920d9978e23296d7016e6c05c954f95881b4f9f7e43bcea393951e91af0e4a671400dc435bd2a1616c60618df2476d0ece060dbbda11e751e256956a0dbcd7e4a8d6d85a3319f22a2c5f26dad50e82f70f3dd91feff19c775aa60499a3b7daa57e344c07c3787e99d53303488801d2b17cdbfdee61ea3fc473f6c146f06eb60d70594a59e0ed79cee6ca4a5f78b037637ddab69fb8522c0f7bf37aa7f59cc7fa659e759db69966455944975cd22a1a1355f35a589a4978c8f3272e1c4f6793288a00ab879299aa6ad02d966e3dc67cee0c808b1a046458cff9bdac25a4071eb10038a6389a0ef7233003641bd4ee1efad0e9b2f693396a89ca0db3c05b6abfed3b246eb1b23a6b77e8b486f26d9c3dde9dd6f3637a8115940ed2ca762ca6320609f61c37ffc9c3f2f7a0f27edc9891c2eeac49ba258a0d09c35c4fe1dc52d4d9319aa9b1a271a5d8d2d3a75fef4d59fb04679ba526aecbd19d73f72fee537630444326e2543ce564c669bf378499738385dda9ac63521a1b91f580d0737a7326009f0ff0dcb05aa8b86222c934d9ddb4628e30b6e12ae370154ab39c605431b4c40683592afcfd6fccf35df9fe5850442595d24be3d9f4298bf3d541f09e7e71f552c88eed9642df46953622d5aea05b5060325304ec81c0447ac95b90f9da4359e3286938f06aea3d45030cb836be15b1c65e3edf44cbcfe2f01ef8d7209c69d7c81334c866ebee50e418a28336cea1982069b4df090eab81303761d1af337e083f1e0ad1440a02ef1eefb03506c39d2377807e335ee64bdb76527f786223cee5233299eda9fcb1d38f19c34480f790a328b0735f80908e3aa70086df828d56b6c79516f71a24c9d94f60335f86e9d29c0c5d3872b",
|
||||
"dd0000000108277148f2b916666000406672db10ab41db38c01f7021709bac4d1659d872623eb5852b12b494535d13779a88d37e9685da572f6b2de35793a519a457493456ac4ee242933cf92d783f783656899c31832274bf1c26d24720d9d8ecfec598e19c58a478d2991dfc1cda3000f7bd7bd17e80",
|
||||
"d60000000108277148f2b916666000404ed98b1b4ac35c0c0ef18c88adf08a6701ccb0876ea75aac8c128349936fa3cb6728e4e58de8673dd7dc8457b092957f26bc8194233bb81c7e78127844f9b833f196dc46c5cb4064c773f3c6e0bc73",
|
||||
},
|
||||
domain: "www.google.com",
|
||||
},
|
||||
}
|
||||
q, err := NewQuicSniffer(SnifferConfig{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, test := range cases {
|
||||
pkt, err := hex.DecodeString(test.input)
|
||||
data, err := testQuicSniffer(test.input, true)
|
||||
assert.NoError(t, err)
|
||||
oriPkt := bytes.Clone(pkt)
|
||||
domain, err := q.SniffData(pkt)
|
||||
assert.Equal(t, test.domain, data)
|
||||
|
||||
data, err = testQuicSniffer(test.input, false)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, test.domain, domain)
|
||||
assert.Equal(t, oriPkt, pkt) // ensure input data not changed
|
||||
assert.Equal(t, test.domain, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -428,12 +428,6 @@ type RawConfig struct {
|
||||
ClashForAndroid RawClashForAndroid `yaml:"clash-for-android" json:"clash-for-android"`
|
||||
}
|
||||
|
||||
var (
|
||||
GroupsList = list.New()
|
||||
ProxiesList = list.New()
|
||||
ParsingProxiesCallback func(groupsList *list.List, proxiesList *list.List)
|
||||
)
|
||||
|
||||
// Parse config
|
||||
func Parse(buf []byte) (*Config, error) {
|
||||
rawCfg, err := UnmarshalRawConfig(buf)
|
||||
@@ -927,12 +921,6 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
|
||||
)
|
||||
proxies["GLOBAL"] = adapter.NewProxy(global)
|
||||
}
|
||||
ProxiesList = proxiesList
|
||||
GroupsList = groupsList
|
||||
if ParsingProxiesCallback != nil {
|
||||
// refresh tray menu
|
||||
go ParsingProxiesCallback(GroupsList, ProxiesList)
|
||||
}
|
||||
return proxies, providersMap, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ type Sniffer interface {
|
||||
SupportPort(port uint16) bool
|
||||
}
|
||||
|
||||
type MultiPacketSniffer interface {
|
||||
WrapperSender(packetSender constant.PacketSender, override bool) constant.PacketSender
|
||||
}
|
||||
|
||||
const (
|
||||
TLS Type = iota
|
||||
HTTP
|
||||
|
||||
@@ -449,6 +449,26 @@ proxies: # socks5
|
||||
password: "shadow_tls_password"
|
||||
version: 2 # support 1/2/3
|
||||
|
||||
- name: "ss5"
|
||||
type: ss
|
||||
server: server
|
||||
port: 443
|
||||
cipher: chacha20-ietf-poly1305
|
||||
password: "password"
|
||||
plugin: gost-plugin
|
||||
plugin-opts:
|
||||
mode: websocket
|
||||
# tls: true # wss
|
||||
# 可使用 openssl x509 -noout -fingerprint -sha256 -inform pem -in yourcert.pem 获取
|
||||
# 配置指纹将实现 SSL Pining 效果
|
||||
# fingerprint: xxxx
|
||||
# skip-cert-verify: true
|
||||
# host: bing.com
|
||||
# path: "/"
|
||||
# mux: true
|
||||
# headers:
|
||||
# custom: value
|
||||
|
||||
- name: "ss-restls-tls13"
|
||||
type: ss
|
||||
server: [YOUR_SERVER_IP]
|
||||
|
||||
@@ -7,17 +7,18 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0
|
||||
github.com/coreos/go-iptables v0.8.0
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/enfein/mieru/v3 v3.11.2
|
||||
github.com/enfein/mieru/v3 v3.12.0
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.3.1
|
||||
github.com/insomniacslk/dhcp v0.0.0-20250109001534-8abf58130905
|
||||
github.com/klauspost/compress v1.17.9 // lastest version compatible with golang1.20
|
||||
github.com/klauspost/cpuid/v2 v2.2.9
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // lastest version compatible with golang1.20
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
|
||||
github.com/mdlayher/netlink v1.7.2
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab
|
||||
github.com/metacubex/bart v0.19.0
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
|
||||
github.com/metacubex/chacha v0.1.1
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
@@ -27,7 +28,7 @@ require (
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2
|
||||
github.com/metacubex/sing-tun v0.4.5
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422
|
||||
@@ -37,7 +38,7 @@ require (
|
||||
github.com/mroth/weightedrand/v2 v2.1.0
|
||||
github.com/openacid/low v0.1.21
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // lastest version compatible with golang1.20
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1
|
||||
github.com/sagernet/cors v1.2.1
|
||||
github.com/sagernet/fswatch v0.1.1
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a
|
||||
@@ -46,7 +47,7 @@ require (
|
||||
github.com/sagernet/sing-shadowtls v0.1.5
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
||||
github.com/samber/lo v1.49.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.1 // lastest version compatible with golang1.20
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||
@@ -54,13 +55,13 @@ require (
|
||||
gitlab.com/go-extension/aes-ccm v0.0.0-20230221065045-e58665ef23c7
|
||||
go.uber.org/automaxprocs v1.6.0
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.33.0
|
||||
golang.org/x/crypto v0.33.0 // lastest version compatible with golang1.20
|
||||
golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // lastest version compatible with golang1.20
|
||||
golang.org/x/net v0.35.0
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/net v0.35.0 // lastest version compatible with golang1.20
|
||||
golang.org/x/sys v0.30.0 // lastest version compatible with golang1.20
|
||||
google.golang.org/protobuf v1.34.2 // lastest version compatible with golang1.20
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
lukechampine.com/blake3 v1.3.0
|
||||
lukechampine.com/blake3 v1.3.0 // lastest version compatible with golang1.20
|
||||
)
|
||||
|
||||
require (
|
||||
|
||||
@@ -28,8 +28,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
|
||||
github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw=
|
||||
github.com/enfein/mieru/v3 v3.12.0 h1:sV3moozWpRjjqwqFZJjGtMB0EacN8+D7BpjzsmacsXM=
|
||||
github.com/enfein/mieru/v3 v3.12.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -97,6 +97,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
@@ -119,8 +121,8 @@ github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJ
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
@@ -156,8 +158,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package sing_tun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/metacubex/mihomo/component/iface"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
"github.com/metacubex/sing-tun/control"
|
||||
)
|
||||
|
||||
type defaultInterfaceFinder struct{}
|
||||
@@ -15,7 +15,8 @@ var DefaultInterfaceFinder control.InterfaceFinder = (*defaultInterfaceFinder)(n
|
||||
|
||||
func (f *defaultInterfaceFinder) Update() error {
|
||||
iface.FlushCache()
|
||||
return nil
|
||||
_, err := iface.Interfaces()
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) Interfaces() []control.Interface {
|
||||
@@ -31,45 +32,46 @@ func (f *defaultInterfaceFinder) Interfaces() []control.Interface {
|
||||
return interfaces
|
||||
}
|
||||
|
||||
var errNoSuchInterface = errors.New("no such network interface")
|
||||
|
||||
func (f *defaultInterfaceFinder) InterfaceIndexByName(name string) (int, error) {
|
||||
ifaces, err := iface.Interfaces()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
func (f *defaultInterfaceFinder) ByName(name string) (*control.Interface, error) {
|
||||
netInterface, err := iface.ResolveInterface(name)
|
||||
if err == nil {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
for _, netInterface := range ifaces {
|
||||
if netInterface.Name == name {
|
||||
return netInterface.Index, nil
|
||||
if _, err := net.InterfaceByName(name); err == nil {
|
||||
err = f.Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.ByName(name)
|
||||
}
|
||||
return 0, errNoSuchInterface
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) InterfaceNameByIndex(index int) (string, error) {
|
||||
ifaces, err := iface.Interfaces()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, netInterface := range ifaces {
|
||||
if netInterface.Index == index {
|
||||
return netInterface.Name, nil
|
||||
}
|
||||
}
|
||||
return "", errNoSuchInterface
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) InterfaceByAddr(addr netip.Addr) (*control.Interface, error) {
|
||||
func (f *defaultInterfaceFinder) ByIndex(index int) (*control.Interface, error) {
|
||||
ifaces, err := iface.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, netInterface := range ifaces {
|
||||
for _, prefix := range netInterface.Addresses {
|
||||
if prefix.Contains(addr) {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
if netInterface.Index == index {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
}
|
||||
return nil, errNoSuchInterface
|
||||
_, err = net.InterfaceByIndex(index)
|
||||
if err == nil {
|
||||
err = f.Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.ByIndex(index)
|
||||
}
|
||||
return nil, iface.ErrIfaceNotFound
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) ByAddr(addr netip.Addr) (*control.Interface, error) {
|
||||
netInterface, err := iface.ResolveInterfaceByAddr(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
tun "github.com/metacubex/sing-tun"
|
||||
"github.com/metacubex/sing-tun/control"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
@@ -291,7 +292,12 @@ func New(options LC.Tun, tunnel C.Tunnel, additions ...inbound.Addition) (l *Lis
|
||||
return
|
||||
}
|
||||
l.defaultInterfaceMonitor = defaultInterfaceMonitor
|
||||
defaultInterfaceMonitor.RegisterCallback(func(event int) {
|
||||
defaultInterfaceMonitor.RegisterCallback(func(defaultInterface *control.Interface, event int) {
|
||||
if defaultInterface != nil {
|
||||
log.Warnln("[TUN] default interface changed by monitor, => %s", defaultInterface.Name)
|
||||
} else {
|
||||
log.Errorln("[TUN] default interface lost by monitor")
|
||||
}
|
||||
iface.FlushCache()
|
||||
resolver.ResetConnection() // reset resolver's connection after default interface changed
|
||||
})
|
||||
@@ -534,9 +540,19 @@ type cDialerInterfaceFinder struct {
|
||||
defaultInterfaceMonitor tun.DefaultInterfaceMonitor
|
||||
}
|
||||
|
||||
func (d *cDialerInterfaceFinder) DefaultInterfaceName(destination netip.Addr) string {
|
||||
if netInterface, _ := DefaultInterfaceFinder.ByAddr(destination); netInterface != nil {
|
||||
return netInterface.Name
|
||||
}
|
||||
if netInterface := d.defaultInterfaceMonitor.DefaultInterface(); netInterface != nil {
|
||||
return netInterface.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d *cDialerInterfaceFinder) FindInterfaceName(destination netip.Addr) string {
|
||||
for _, dest := range []netip.Addr{destination, netip.IPv4Unspecified(), netip.IPv6Unspecified()} {
|
||||
autoDetectInterfaceName := d.defaultInterfaceMonitor.DefaultInterfaceName(dest)
|
||||
autoDetectInterfaceName := d.DefaultInterfaceName(dest)
|
||||
if autoDetectInterfaceName == d.tunName {
|
||||
log.Warnln("[TUN] Auto detect interface for %s get same name with tun", destination.String())
|
||||
} else if autoDetectInterfaceName == "" || autoDetectInterfaceName == "<nil>" {
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package gost
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/metacubex/mihomo/component/ca"
|
||||
"github.com/metacubex/mihomo/transport/vmess"
|
||||
smux "github.com/sagernet/smux"
|
||||
)
|
||||
|
||||
// Option is options of gost websocket
|
||||
type Option struct {
|
||||
Host string
|
||||
Port string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
TLS bool
|
||||
SkipCertVerify bool
|
||||
Fingerprint string
|
||||
Mux bool
|
||||
}
|
||||
|
||||
// muxConn is a wrapper around smux.Stream that also closes the session when closed
|
||||
type muxConn struct {
|
||||
net.Conn
|
||||
session *smux.Session
|
||||
}
|
||||
|
||||
func (m *muxConn) Close() error {
|
||||
streamErr := m.Conn.Close()
|
||||
sessionErr := m.session.Close()
|
||||
|
||||
// Return stream error if there is one, otherwise return session error
|
||||
if streamErr != nil {
|
||||
return streamErr
|
||||
}
|
||||
return sessionErr
|
||||
}
|
||||
|
||||
// NewGostWebsocket return a gost websocket
|
||||
func NewGostWebsocket(ctx context.Context, conn net.Conn, option *Option) (net.Conn, error) {
|
||||
header := http.Header{}
|
||||
for k, v := range option.Headers {
|
||||
header.Add(k, v)
|
||||
}
|
||||
|
||||
config := &vmess.WebsocketConfig{
|
||||
Host: option.Host,
|
||||
Port: option.Port,
|
||||
Path: option.Path,
|
||||
Headers: header,
|
||||
}
|
||||
|
||||
if option.TLS {
|
||||
config.TLS = true
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: option.Host,
|
||||
InsecureSkipVerify: option.SkipCertVerify,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
}
|
||||
var err error
|
||||
config.TLSConfig, err = ca.GetSpecifiedFingerprintTLSConfig(tlsConfig, option.Fingerprint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if host := config.Headers.Get("Host"); host != "" {
|
||||
config.TLSConfig.ServerName = host
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
conn, err = vmess.StreamWebsocketConn(ctx, conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if option.Mux {
|
||||
config := smux.DefaultConfig()
|
||||
config.KeepAliveDisabled = true
|
||||
|
||||
session, err := smux.Client(conn, config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream, err := session.OpenStream()
|
||||
if err != nil {
|
||||
session.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &muxConn{
|
||||
Conn: stream,
|
||||
session: session,
|
||||
}, nil
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
@@ -378,12 +378,14 @@ func handleUDPConn(packet C.PacketAdapter) {
|
||||
return
|
||||
}
|
||||
|
||||
if sniffingEnable && snifferDispatcher.Enable() {
|
||||
snifferDispatcher.UDPSniff(packet)
|
||||
}
|
||||
|
||||
key := packet.Key()
|
||||
sender, loaded := natTable.GetOrCreate(key, newPacketSender)
|
||||
sender, loaded := natTable.GetOrCreate(key, func() C.PacketSender {
|
||||
sender := newPacketSender()
|
||||
if sniffingEnable && snifferDispatcher.Enable() {
|
||||
return snifferDispatcher.UDPSniff(packet, sender)
|
||||
}
|
||||
return sender
|
||||
})
|
||||
if !loaded {
|
||||
dial := func() (C.PacketConn, C.WriteBackProxy, error) {
|
||||
if err := sender.ResolveUDP(metadata); err != nil {
|
||||
|
||||
@@ -16,7 +16,7 @@ require (
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.11.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.12.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
@@ -45,6 +45,7 @@ require (
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bart v0.19.0 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.1 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
@@ -56,7 +57,7 @@ require (
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.5 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
@@ -70,7 +71,7 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
|
||||
@@ -28,8 +28,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
|
||||
github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw=
|
||||
github.com/enfein/mieru/v3 v3.12.0 h1:sV3moozWpRjjqwqFZJjGtMB0EacN8+D7BpjzsmacsXM=
|
||||
github.com/enfein/mieru/v3 v3.12.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -97,6 +97,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
@@ -119,8 +121,8 @@ github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJ
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
@@ -155,8 +157,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/coreos/go-iptables v0.8.0 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.11.2 // indirect
|
||||
github.com/enfein/mieru/v3 v3.12.0 // indirect
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 // indirect
|
||||
github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect
|
||||
@@ -54,6 +54,7 @@ require (
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.4.1 // indirect
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab // indirect
|
||||
github.com/metacubex/bart v0.19.0 // indirect
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 // indirect
|
||||
github.com/metacubex/chacha v0.1.1 // indirect
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
|
||||
@@ -64,7 +65,7 @@ require (
|
||||
github.com/metacubex/sing-quic v0.0.0-20250119013740-2a19cce83925 // indirect
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.5 // indirect
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 // indirect
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 // indirect
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 // indirect
|
||||
github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
|
||||
@@ -78,7 +79,7 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.12.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.14 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
|
||||
github.com/quic-go/qpack v0.4.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/sagernet/cors v1.2.1 // indirect
|
||||
|
||||
@@ -28,8 +28,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/enfein/mieru/v3 v3.11.2 h1:06KyGbXiiGz2nSHLJDOOkztAVY3cRr3wBMOpYxPotTo=
|
||||
github.com/enfein/mieru/v3 v3.11.2/go.mod h1:XvVfNsM78lUMSlJJKXJZ0Hn3lAB2o/ETXTbb84x5egw=
|
||||
github.com/enfein/mieru/v3 v3.12.0 h1:sV3moozWpRjjqwqFZJjGtMB0EacN8+D7BpjzsmacsXM=
|
||||
github.com/enfein/mieru/v3 v3.12.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
|
||||
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
@@ -98,6 +98,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
@@ -120,8 +122,8 @@ github.com/metacubex/sing-shadowsocks v0.2.8 h1:wIhlaigswzjPw4hej75sEvWte3QR0+AJ
|
||||
github.com/metacubex/sing-shadowsocks v0.2.8/go.mod h1:X3x88XtJpBxG0W0/ECOJL6Ib0SJ3xdniAkU/6/RMWU0=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2 h1:eaf42uVx4Lr21S6MDYs0ZdTvGA0GEhDpb9no4+gdXPo=
|
||||
github.com/metacubex/sing-shadowsocks2 v0.2.2/go.mod h1:BhOug03a/RbI7y6hp6q+6ITM1dXjnLTmeWBHSTwvv2Q=
|
||||
github.com/metacubex/sing-tun v0.4.5 h1:kWSyQzuzHI40r50OFBczfWIDvMBMy1RIk+JsXeBPRB0=
|
||||
github.com/metacubex/sing-tun v0.4.5/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04 h1:B211C+i/I8CWf4I/BaAV0mmkEHrDBJ0XR9EWxjPbFEg=
|
||||
github.com/metacubex/sing-tun v0.4.6-0.20250312042506-6d3b4dc05c04/go.mod h1:V0N4rr0dWPBEE20ESkTXdbtx2riQYcb6YtwC5w/9wl0=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82 h1:zZp5uct9+/0Hb1jKGyqDjCU4/72t43rs7qOq3Rc9oU8=
|
||||
github.com/metacubex/sing-vmess v0.1.14-0.20250228002636-abc39e113b82/go.mod h1:nE7Mdzj/QUDwgRi/8BASPtsxtIFZTHA4Yst5GgwbGCQ=
|
||||
github.com/metacubex/sing-wireguard v0.0.0-20241126021510-0827d417b589 h1:Z6bNy0HLTjx6BKIkV48sV/yia/GP8Bnyb5JQuGgSGzg=
|
||||
@@ -156,8 +158,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0 h1:i+cMcpEDY1BkNm7lPDkCtE4oElsYLn+EKF8kAu2vXT4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
|
||||
@@ -49,7 +49,7 @@ object Clash {
|
||||
}
|
||||
|
||||
fun notifyDnsChanged(dns: List<String>) {
|
||||
Bridge.nativeNotifyDnsChanged(dns.joinToString(separator = ","))
|
||||
Bridge.nativeNotifyDnsChanged(dns.toSet().joinToString(separator = ","))
|
||||
}
|
||||
|
||||
fun notifyTimeZoneChanged(name: String, offset: Int) {
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ import android.os.Build
|
||||
import androidx.core.content.getSystemService
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.core.Clash
|
||||
import com.github.kr328.clash.service.util.resolvePrimaryDns
|
||||
import com.github.kr328.clash.service.util.resolveDns
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
@@ -69,8 +69,8 @@ class NetworkObserveModule(service: Service) : Module<Network?>(service) {
|
||||
}
|
||||
}
|
||||
|
||||
val dns = networks.mapNotNull {
|
||||
connectivity.resolvePrimaryDns(it)
|
||||
val dns = networks.flatMap { network ->
|
||||
connectivity?.resolveDns(network) ?: emptyList()
|
||||
}
|
||||
|
||||
Clash.notifyDnsChanged(dns)
|
||||
|
||||
+11
-2
@@ -7,7 +7,7 @@ import java.net.InetAddress
|
||||
fun InetAddress.asSocketAddressText(port: Int): String {
|
||||
return when (this) {
|
||||
is Inet6Address ->
|
||||
"[${numericToTextFormat(this.address)}]:$port"
|
||||
"[${numericToTextFormat(this)}]:$port"
|
||||
is Inet4Address ->
|
||||
"${this.hostAddress}:$port"
|
||||
else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}")
|
||||
@@ -16,7 +16,8 @@ fun InetAddress.asSocketAddressText(port: Int): String {
|
||||
|
||||
private const val INT16SZ = 2
|
||||
private const val INADDRSZ = 16
|
||||
private fun numericToTextFormat(src: ByteArray): String {
|
||||
private fun numericToTextFormat(address: Inet6Address): String {
|
||||
var src = address.getAddress()
|
||||
val sb = StringBuilder(39)
|
||||
for (i in 0 until INADDRSZ / INT16SZ) {
|
||||
sb.append(
|
||||
@@ -29,6 +30,14 @@ private fun numericToTextFormat(src: ByteArray): String {
|
||||
sb.append(":")
|
||||
}
|
||||
}
|
||||
// handle [fe80::1%wlan0] like address from Inet6Address.getHostAddress()
|
||||
// For the Android system, a ScopeId must be carried when initiating a connection to an ipv6 link-local address
|
||||
// Note that the Scope must be returned as an int type, not a string format
|
||||
// Reference: https://github.com/golang/go/issues/68082
|
||||
if (address.getScopeId() > 0) {
|
||||
sb.append("%")
|
||||
sb.append(address.getScopeId())
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
|
||||
+3
-4
@@ -3,8 +3,7 @@ package com.github.kr328.clash.service.util
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Network
|
||||
|
||||
fun ConnectivityManager.resolvePrimaryDns(network: Network?): String? {
|
||||
val properties = getLinkProperties(network) ?: return null
|
||||
|
||||
return properties.dnsServers.firstOrNull()?.asSocketAddressText(53)
|
||||
fun ConnectivityManager.resolveDns(network: Network?): List<String> {
|
||||
val properties = getLinkProperties(network) ?: return listOf()
|
||||
return properties.dnsServers.map { it.asSocketAddressText(53) }
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -5,8 +5,8 @@ After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
|
||||
Vendored
+2
-2
@@ -5,8 +5,8 @@ After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
|
||||
@@ -45,7 +45,11 @@ func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr,
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
|
||||
case *syscall.SockaddrInet6:
|
||||
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)}
|
||||
zone := ""
|
||||
if from.ZoneId != 0 {
|
||||
zone = strconv.FormatInt(int64(from.ZoneId), 10)
|
||||
}
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone}
|
||||
}
|
||||
}
|
||||
// udp should not convert readN == 0 to io.EOF
|
||||
|
||||
@@ -54,7 +54,11 @@ func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr,
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
|
||||
case *windows.SockaddrInet6:
|
||||
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)}
|
||||
zone := ""
|
||||
if from.ZoneId != 0 {
|
||||
zone = strconv.FormatInt(int64(from.ZoneId), 10)
|
||||
}
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone}
|
||||
}
|
||||
}
|
||||
// udp should not convert readN == 0 to io.EOF
|
||||
|
||||
@@ -88,21 +88,22 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToListenConfig(lc)
|
||||
} else {
|
||||
if cfg.interfaceName == "" {
|
||||
interfaceName := cfg.interfaceName
|
||||
if interfaceName == "" {
|
||||
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
||||
cfg.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
|
||||
interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
|
||||
}
|
||||
}
|
||||
if rAddrPort.Addr().Unmap().IsLoopback() {
|
||||
// avoid "The requested address is not valid in its context."
|
||||
cfg.interfaceName = ""
|
||||
interfaceName = ""
|
||||
}
|
||||
if cfg.interfaceName != "" {
|
||||
if interfaceName != "" {
|
||||
bind := bindIfaceToListenConfig
|
||||
if cfg.fallbackBind {
|
||||
bind = fallbackBindIfaceToListenConfig
|
||||
}
|
||||
addr, err := bind(cfg.interfaceName, lc, network, address, rAddrPort)
|
||||
addr, err := bind(interfaceName, lc, network, address, rAddrPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -162,17 +163,18 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToToDialer(dialer)
|
||||
} else {
|
||||
if opt.interfaceName == "" {
|
||||
interfaceName := opt.interfaceName // don't change the "opt", it's a pointer
|
||||
if interfaceName == "" {
|
||||
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
||||
opt.interfaceName = finder.FindInterfaceName(destination)
|
||||
interfaceName = finder.FindInterfaceName(destination)
|
||||
}
|
||||
}
|
||||
if opt.interfaceName != "" {
|
||||
if interfaceName != "" {
|
||||
bind := bindIfaceToDialer
|
||||
if opt.fallbackBind {
|
||||
bind = fallbackBindIfaceToDialer
|
||||
}
|
||||
if err := bind(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
if err := bind(interfaceName, dialer, network, destination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/singledo"
|
||||
|
||||
"github.com/metacubex/bart"
|
||||
)
|
||||
|
||||
type Interface struct {
|
||||
@@ -23,16 +25,23 @@ var (
|
||||
ErrAddrNotFound = errors.New("addr not found")
|
||||
)
|
||||
|
||||
var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20)
|
||||
type ifaceCache struct {
|
||||
ifMap map[string]*Interface
|
||||
ifTable bart.Table[*Interface]
|
||||
}
|
||||
|
||||
func Interfaces() (map[string]*Interface, error) {
|
||||
value, err, _ := interfaces.Do(func() (map[string]*Interface, error) {
|
||||
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
|
||||
|
||||
func getCache() (*ifaceCache, error) {
|
||||
value, err, _ := caches.Do(func() (*ifaceCache, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := map[string]*Interface{}
|
||||
cache := &ifaceCache{
|
||||
ifMap: make(map[string]*Interface),
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
@@ -61,7 +70,7 @@ func Interfaces() (map[string]*Interface, error) {
|
||||
}
|
||||
}
|
||||
|
||||
r[iface.Name] = &Interface{
|
||||
ifaceObj := &Interface{
|
||||
Index: iface.Index,
|
||||
MTU: iface.MTU,
|
||||
Name: iface.Name,
|
||||
@@ -69,13 +78,26 @@ func Interfaces() (map[string]*Interface, error) {
|
||||
Flags: iface.Flags,
|
||||
Addresses: ipNets,
|
||||
}
|
||||
cache.ifMap[iface.Name] = ifaceObj
|
||||
|
||||
for _, prefix := range ipNets {
|
||||
cache.ifTable.Insert(prefix, ifaceObj)
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
return cache, nil
|
||||
})
|
||||
return value, err
|
||||
}
|
||||
|
||||
func Interfaces() (map[string]*Interface, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cache.ifMap, nil
|
||||
}
|
||||
|
||||
func ResolveInterface(name string) (*Interface, error) {
|
||||
ifaces, err := Interfaces()
|
||||
if err != nil {
|
||||
@@ -90,23 +112,29 @@ func ResolveInterface(name string) (*Interface, error) {
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func IsLocalIp(ip netip.Addr) (bool, error) {
|
||||
ifaces, err := Interfaces()
|
||||
func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iface, ok := cache.ifTable.Lookup(addr)
|
||||
if !ok {
|
||||
return nil, ErrIfaceNotFound
|
||||
}
|
||||
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func IsLocalIp(addr netip.Addr) (bool, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
for _, addr := range iface.Addresses {
|
||||
if addr.Contains(ip) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
return cache.ifTable.Contains(addr), nil
|
||||
}
|
||||
|
||||
func FlushCache() {
|
||||
interfaces.Reset()
|
||||
caches.Reset()
|
||||
}
|
||||
|
||||
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) {
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
@@ -23,28 +24,31 @@ func dnsReadConfig() (servers []string, err error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var ip net.IP
|
||||
var ip netip.Addr
|
||||
switch sa := sa.(type) {
|
||||
case *syscall.SockaddrInet4:
|
||||
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
|
||||
ip = netip.AddrFrom4(sa.Addr)
|
||||
case *syscall.SockaddrInet6:
|
||||
ip = make(net.IP, net.IPv6len)
|
||||
copy(ip, sa.Addr[:])
|
||||
if ip[0] == 0xfe && ip[1] == 0xc0 {
|
||||
if sa.Addr[0] == 0xfe && sa.Addr[1] == 0xc0 {
|
||||
// Ignore these fec0/10 ones. Windows seems to
|
||||
// populate them as defaults on its misc rando
|
||||
// interfaces.
|
||||
continue
|
||||
}
|
||||
ip = netip.AddrFrom16(sa.Addr)
|
||||
if sa.ZoneId != 0 {
|
||||
ip = ip.WithZone(strconv.FormatInt(int64(sa.ZoneId), 10))
|
||||
}
|
||||
//continue
|
||||
default:
|
||||
// Unexpected type.
|
||||
continue
|
||||
}
|
||||
if slices.Contains(servers, ip.String()) {
|
||||
ipStr := ip.String()
|
||||
if slices.Contains(servers, ipStr) {
|
||||
continue
|
||||
}
|
||||
servers = append(servers, ip.String())
|
||||
servers = append(servers, ipStr)
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
@@ -18,6 +18,7 @@ require (
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
|
||||
github.com/mdlayher/netlink v1.7.2
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab
|
||||
github.com/metacubex/bart v0.19.0
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
|
||||
github.com/metacubex/chacha v0.1.1
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
|
||||
@@ -97,6 +97,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sing_tun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
@@ -16,7 +15,8 @@ var DefaultInterfaceFinder control.InterfaceFinder = (*defaultInterfaceFinder)(n
|
||||
|
||||
func (f *defaultInterfaceFinder) Update() error {
|
||||
iface.FlushCache()
|
||||
return nil
|
||||
_, err := iface.Interfaces()
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) Interfaces() []control.Interface {
|
||||
@@ -32,27 +32,19 @@ func (f *defaultInterfaceFinder) Interfaces() []control.Interface {
|
||||
return interfaces
|
||||
}
|
||||
|
||||
var errNoSuchInterface = errors.New("no such network interface")
|
||||
|
||||
func (f *defaultInterfaceFinder) ByName(name string) (*control.Interface, error) {
|
||||
ifaces, err := iface.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, netInterface := range ifaces {
|
||||
if netInterface.Name == name {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
}
|
||||
_, err = net.InterfaceByName(name)
|
||||
netInterface, err := iface.ResolveInterface(name)
|
||||
if err == nil {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
if _, err := net.InterfaceByName(name); err == nil {
|
||||
err = f.Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.ByName(name)
|
||||
}
|
||||
return nil, errNoSuchInterface
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) ByIndex(index int) (*control.Interface, error) {
|
||||
@@ -73,20 +65,13 @@ func (f *defaultInterfaceFinder) ByIndex(index int) (*control.Interface, error)
|
||||
}
|
||||
return f.ByIndex(index)
|
||||
}
|
||||
return nil, errNoSuchInterface
|
||||
return nil, iface.ErrIfaceNotFound
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) ByAddr(addr netip.Addr) (*control.Interface, error) {
|
||||
ifaces, err := iface.Interfaces()
|
||||
netInterface, err := iface.ResolveInterfaceByAddr(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, netInterface := range ifaces {
|
||||
for _, prefix := range netInterface.Addresses {
|
||||
if prefix.Contains(addr) {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errNoSuchInterface
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "5.67.3",
|
||||
"@tanstack/react-query": "5.68.0",
|
||||
"@tauri-apps/api": "2.3.0",
|
||||
"ahooks": "3.8.4",
|
||||
"dayjs": "1.11.13",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"allotment": "1.20.3",
|
||||
"country-code-emoji": "2.3.0",
|
||||
"dayjs": "1.11.13",
|
||||
"framer-motion": "12.4.10",
|
||||
"framer-motion": "12.5.0",
|
||||
"i18next": "24.2.3",
|
||||
"jotai": "2.12.2",
|
||||
"json-schema": "0.4.0",
|
||||
@@ -55,10 +55,10 @@
|
||||
"@emotion/react": "11.14.0",
|
||||
"@iconify/json": "2.2.317",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@tanstack/react-query": "5.67.3",
|
||||
"@tanstack/react-router": "1.114.22",
|
||||
"@tanstack/router-devtools": "1.114.22",
|
||||
"@tanstack/router-plugin": "1.114.22",
|
||||
"@tanstack/react-query": "5.68.0",
|
||||
"@tanstack/react-router": "1.114.23",
|
||||
"@tanstack/router-devtools": "1.114.23",
|
||||
"@tanstack/router-plugin": "1.114.23",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.2.2",
|
||||
"@tauri-apps/plugin-dialog": "2.2.0",
|
||||
"@tauri-apps/plugin-fs": "2.2.0",
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"ahooks": "3.8.4",
|
||||
"d3": "7.9.0",
|
||||
"framer-motion": "12.4.10",
|
||||
"framer-motion": "12.5.0",
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
"react-error-boundary": "5.0.0",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"mihomo_alpha": "alpha-dee5898",
|
||||
"clash_rs": "v0.7.6",
|
||||
"clash_premium": "2023-09-05-gdcc8d87",
|
||||
"clash_rs_alpha": "0.7.6-alpha+sha.4124feb"
|
||||
"clash_rs_alpha": "0.7.6-alpha+sha.47236aa"
|
||||
},
|
||||
"arch_template": {
|
||||
"mihomo": {
|
||||
@@ -69,5 +69,5 @@
|
||||
"linux-armv7hf": "clash-armv7-unknown-linux-gnueabihf"
|
||||
}
|
||||
},
|
||||
"updated_at": "2025-03-15T22:20:39.234Z"
|
||||
"updated_at": "2025-03-16T22:20:31.758Z"
|
||||
}
|
||||
|
||||
Generated
+72
-72
@@ -175,8 +175,8 @@ importers:
|
||||
frontend/interface:
|
||||
dependencies:
|
||||
'@tanstack/react-query':
|
||||
specifier: 5.67.3
|
||||
version: 5.67.3(react@19.0.0)
|
||||
specifier: 5.68.0
|
||||
version: 5.68.0(react@19.0.0)
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
@@ -246,7 +246,7 @@ importers:
|
||||
version: 4.0.14
|
||||
'@tanstack/router-zod-adapter':
|
||||
specifier: 1.81.5
|
||||
version: 1.81.5(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(zod@3.24.2)
|
||||
version: 1.81.5(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(zod@3.24.2)
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.3.0
|
||||
version: 2.3.0
|
||||
@@ -266,8 +266,8 @@ importers:
|
||||
specifier: 1.11.13
|
||||
version: 1.11.13
|
||||
framer-motion:
|
||||
specifier: 12.4.10
|
||||
version: 12.4.10(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: 12.5.0
|
||||
version: 12.5.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
i18next:
|
||||
specifier: 24.2.3
|
||||
version: 24.2.3(typescript@5.8.2)
|
||||
@@ -339,17 +339,17 @@ importers:
|
||||
specifier: 4.7.0
|
||||
version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/react-query':
|
||||
specifier: 5.67.3
|
||||
version: 5.67.3(react@19.0.0)
|
||||
specifier: 5.68.0
|
||||
version: 5.68.0(react@19.0.0)
|
||||
'@tanstack/react-router':
|
||||
specifier: 1.114.22
|
||||
version: 1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: 1.114.23
|
||||
version: 1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/router-devtools':
|
||||
specifier: 1.114.22
|
||||
version: 1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.22)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)
|
||||
specifier: 1.114.23
|
||||
version: 1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.23)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)
|
||||
'@tanstack/router-plugin':
|
||||
specifier: 1.114.22
|
||||
version: 1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.85.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0))
|
||||
specifier: 1.114.23
|
||||
version: 1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.85.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0))
|
||||
'@tauri-apps/plugin-clipboard-manager':
|
||||
specifier: 2.2.2
|
||||
version: 2.2.2
|
||||
@@ -486,8 +486,8 @@ importers:
|
||||
specifier: 7.9.0
|
||||
version: 7.9.0
|
||||
framer-motion:
|
||||
specifier: 12.4.10
|
||||
version: 12.4.10(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
specifier: 12.5.0
|
||||
version: 12.5.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
react:
|
||||
specifier: 19.0.0
|
||||
version: 19.0.0
|
||||
@@ -2738,24 +2738,24 @@ packages:
|
||||
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tanstack/query-core@5.67.3':
|
||||
resolution: {integrity: sha512-pq76ObpjcaspAW4OmCbpXLF6BCZP2Zr/J5ztnyizXhSlNe7fIUp0QKZsd0JMkw9aDa+vxDX/OY7N+hjNY/dCGg==}
|
||||
'@tanstack/query-core@5.68.0':
|
||||
resolution: {integrity: sha512-r8rFYYo8/sY/LNaOqX84h12w7EQev4abFXDWy4UoDVUJzJ5d9Fbmb8ayTi7ScG+V0ap44SF3vNs/45mkzDGyGw==}
|
||||
|
||||
'@tanstack/react-query@5.67.3':
|
||||
resolution: {integrity: sha512-u/n2HsQeH1vpZIOzB/w2lqKlXUDUKo6BxTdGXSMvNzIq5MHYFckRMVuFABp+QB7RN8LFXWV6X1/oSkuDq+MPIA==}
|
||||
'@tanstack/react-query@5.68.0':
|
||||
resolution: {integrity: sha512-mMOdGDKlwTP/WV72QqSNf4PAMeoBp/DqBHQ222wBfb51Looi8QUqnCnb9O98ZgvNISmy6fzxRGBJdZ+9IBvX2Q==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
'@tanstack/react-router-devtools@1.114.22':
|
||||
resolution: {integrity: sha512-jPbj1ocehP4iV7nClb9e7mJjcSygWxI7dRWIy0ehMYZelsE7QjQ27RwFxmHVNBZRbHr7MfqlBaArP81qWMZ4wA==}
|
||||
'@tanstack/react-router-devtools@1.114.23':
|
||||
resolution: {integrity: sha512-eOTlXeLlT5PfSrOrDEljTz8F7Sl3YjjI+yTGn5ern0FrFlc3GlBxJqFaKC19mT9H4ZkwETnSqDHrLZKV06ykRA==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
'@tanstack/react-router': ^1.114.22
|
||||
'@tanstack/react-router': ^1.114.23
|
||||
react: '>=18.0.0 || >=19.0.0'
|
||||
react-dom: '>=18.0.0 || >=19.0.0'
|
||||
|
||||
'@tanstack/react-router@1.114.22':
|
||||
resolution: {integrity: sha512-Z58+pgN1uPAlw42uq3iH+S08+rS93594MNmdMY6wvTsHnanZ28eOd5pgfn9tshrmD7zdKJrfJ8zD2xpgRvpA5Q==}
|
||||
'@tanstack/react-router@1.114.23':
|
||||
resolution: {integrity: sha512-L9TS8HE5nvXzCyjwlwfXwnC7pfiofbDLCSAGN7Me2jUbxHPyQpAtxRYqtByANaFVclOJgYBmZkx1m6/GVk9oiA==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
react: '>=18.0.0 || >=19.0.0'
|
||||
@@ -2780,15 +2780,15 @@ packages:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
'@tanstack/router-core@1.114.22':
|
||||
resolution: {integrity: sha512-gWiHB+QxOiwIFm43xsOiQu5BKUucOxW3yiV3YBnrZQu8IpdREEKoYkBxuEEKW23gMi2mmoJMZC8N4mN7NiIxrg==}
|
||||
'@tanstack/router-core@1.114.23':
|
||||
resolution: {integrity: sha512-aJkqAtVyRbdNBuoAsiy5Jmc9MYa2Gt9r1s/xlYBDfSEWoJzhMmWVoKfgBI6Ggeu3dwe6uCr/gkBRmpRkgWjg1w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tanstack/router-devtools-core@1.114.22':
|
||||
resolution: {integrity: sha512-3vm/5EDFc6asJ+d1ATNrWH/dbdZi/hRG4WDdyBRc27glyqpiFXlkTZzrxN6aqZXGe84y3J0vIjilDpv6kBdO/A==}
|
||||
'@tanstack/router-devtools-core@1.114.23':
|
||||
resolution: {integrity: sha512-PP2JFokatFdL3RANKibZPWK1L1vy4LM2chEvwyJYVUhAKg+gWPIB4aE5FnhkI/sn7N/8dzt83Xbruj+NPEPfmg==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
'@tanstack/router-core': ^1.114.22
|
||||
'@tanstack/router-core': ^1.114.23
|
||||
csstype: ^3.0.10
|
||||
solid-js: '>=1.9.5'
|
||||
tiny-invariant: ^1.3.3
|
||||
@@ -2796,11 +2796,11 @@ packages:
|
||||
csstype:
|
||||
optional: true
|
||||
|
||||
'@tanstack/router-devtools@1.114.22':
|
||||
resolution: {integrity: sha512-wkT4JjjMRxpHi4+iLlrXMizVPWlBdV5zZCA/Aa8M9is28eIaw4ML+tEC2ckX292zPs0NpySdtPSMD3Ml50TLrQ==}
|
||||
'@tanstack/router-devtools@1.114.23':
|
||||
resolution: {integrity: sha512-i7F7VkA73fVvaZuPc/hsAiTlkDu/PF11Y3ERDNGL8G9NUaDEKeW8rvK0V+KTNNg8Tnm0FOHH4xohquzBXPkcuA==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
'@tanstack/react-router': ^1.114.22
|
||||
'@tanstack/react-router': ^1.114.23
|
||||
csstype: ^3.0.10
|
||||
react: '>=18.0.0 || >=19.0.0'
|
||||
react-dom: '>=18.0.0 || >=19.0.0'
|
||||
@@ -2808,21 +2808,21 @@ packages:
|
||||
csstype:
|
||||
optional: true
|
||||
|
||||
'@tanstack/router-generator@1.114.22':
|
||||
resolution: {integrity: sha512-MpUYgW/EfFbse63QJqaE1QLEyzxpVpaf2UadByhCpD+DB1MFpLagg/kbegc0+PO3Plz/GcW0H8rBxgTfrsx99Q==}
|
||||
'@tanstack/router-generator@1.114.23':
|
||||
resolution: {integrity: sha512-+6YwCXXL4H6CwdWLIrO8Bmy9HZIB8p9yJGcIBGXefN3D0vOUOi7EJa2+MQgQCQgjdygP4zuutqXD/5qxEZQQNw==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
'@tanstack/react-router': ^1.114.22
|
||||
'@tanstack/react-router': ^1.114.23
|
||||
peerDependenciesMeta:
|
||||
'@tanstack/react-router':
|
||||
optional: true
|
||||
|
||||
'@tanstack/router-plugin@1.114.22':
|
||||
resolution: {integrity: sha512-QQ4GjZvPJ3/xZ5JrujwPmP7Jgtg3rx6eIVgigZL9Q/yHW8bzN5CMxFbMvA6ICJY2y2s/9W3v+TIjj4SwlvxCpw==}
|
||||
'@tanstack/router-plugin@1.114.23':
|
||||
resolution: {integrity: sha512-4D5558eQ4So2HXnVrN5inZs4tU5nlv75VH6u95NZvVSGc+zUBryzBg7ElkhzMh+UPQoJ0RTf4NxNcKG3vw2+6w==}
|
||||
engines: {node: '>=12'}
|
||||
peerDependencies:
|
||||
'@rsbuild/core': '>=1.0.2'
|
||||
'@tanstack/react-router': ^1.114.22
|
||||
'@tanstack/react-router': ^1.114.23
|
||||
vite: '>=5.0.0 || >=6.0.0'
|
||||
vite-plugin-solid: ^2.11.2
|
||||
webpack: '>=5.92.0'
|
||||
@@ -4776,8 +4776,8 @@ packages:
|
||||
fraction.js@4.3.7:
|
||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||
|
||||
framer-motion@12.4.10:
|
||||
resolution: {integrity: sha512-3Msuyjcr1Pb5hjkn4EJcRe1HumaveP0Gbv4DBMKTPKcV/1GSMkQXj+Uqgneys+9DPcZM18Hac9qY9iUEF5LZtg==}
|
||||
framer-motion@12.5.0:
|
||||
resolution: {integrity: sha512-buPlioFbH9/W7rDzYh1C09AuZHAk2D1xTA1BlounJ2Rb9aRg84OXexP0GLd+R83v0khURdMX7b5MKnGTaSg5iA==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
@@ -6053,11 +6053,11 @@ packages:
|
||||
peerDependencies:
|
||||
monaco-editor: '>=0.36'
|
||||
|
||||
motion-dom@12.4.10:
|
||||
resolution: {integrity: sha512-ISP5u6FTceoD6qKdLupIPU/LyXBrxGox+P2e3mBbm1+pLdlBbwv01YENJr7+1WZnW5ucVKzFScYsV1eXTCG4Xg==}
|
||||
motion-dom@12.5.0:
|
||||
resolution: {integrity: sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==}
|
||||
|
||||
motion-utils@12.4.10:
|
||||
resolution: {integrity: sha512-NPwZd94V013SwRf++jMrk2+HEBgPkeIE2RiOzhAuuQlqxMJPkKt/LXVh6Upl+iN8oarSGD2dlY5/bqgsYXDABA==}
|
||||
motion-utils@12.5.0:
|
||||
resolution: {integrity: sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==}
|
||||
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
@@ -10485,17 +10485,17 @@ snapshots:
|
||||
dependencies:
|
||||
remove-accents: 0.5.0
|
||||
|
||||
'@tanstack/query-core@5.67.3': {}
|
||||
'@tanstack/query-core@5.68.0': {}
|
||||
|
||||
'@tanstack/react-query@5.67.3(react@19.0.0)':
|
||||
'@tanstack/react-query@5.68.0(react@19.0.0)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.67.3
|
||||
'@tanstack/query-core': 5.68.0
|
||||
react: 19.0.0
|
||||
|
||||
'@tanstack/react-router-devtools@1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.22)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)':
|
||||
'@tanstack/react-router-devtools@1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.23)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)':
|
||||
dependencies:
|
||||
'@tanstack/react-router': 1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/router-devtools-core': 1.114.22(@tanstack/router-core@1.114.22)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)
|
||||
'@tanstack/react-router': 1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/router-devtools-core': 1.114.23(@tanstack/router-core@1.114.23)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
solid-js: 1.9.5
|
||||
@@ -10504,11 +10504,11 @@ snapshots:
|
||||
- csstype
|
||||
- tiny-invariant
|
||||
|
||||
'@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
'@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.114.22
|
||||
'@tanstack/react-store': 0.7.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/router-core': 1.114.22
|
||||
'@tanstack/router-core': 1.114.23
|
||||
jsesc: 3.1.0
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
@@ -10534,14 +10534,14 @@ snapshots:
|
||||
react: 19.0.0
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
'@tanstack/router-core@1.114.22':
|
||||
'@tanstack/router-core@1.114.23':
|
||||
dependencies:
|
||||
'@tanstack/history': 1.114.22
|
||||
'@tanstack/store': 0.7.0
|
||||
|
||||
'@tanstack/router-devtools-core@1.114.22(@tanstack/router-core@1.114.22)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)':
|
||||
'@tanstack/router-devtools-core@1.114.23(@tanstack/router-core@1.114.23)(csstype@3.1.3)(solid-js@1.9.5)(tiny-invariant@1.3.3)':
|
||||
dependencies:
|
||||
'@tanstack/router-core': 1.114.22
|
||||
'@tanstack/router-core': 1.114.23
|
||||
clsx: 2.1.1
|
||||
goober: 2.1.16(csstype@3.1.3)
|
||||
solid-js: 1.9.5
|
||||
@@ -10549,10 +10549,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
csstype: 3.1.3
|
||||
|
||||
'@tanstack/router-devtools@1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.22)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)':
|
||||
'@tanstack/router-devtools@1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.23)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)':
|
||||
dependencies:
|
||||
'@tanstack/react-router': 1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/react-router-devtools': 1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.22)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)
|
||||
'@tanstack/react-router': 1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/react-router-devtools': 1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@tanstack/router-core@1.114.23)(csstype@3.1.3)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(tiny-invariant@1.3.3)
|
||||
clsx: 2.1.1
|
||||
goober: 2.1.16(csstype@3.1.3)
|
||||
react: 19.0.0
|
||||
@@ -10563,16 +10563,16 @@ snapshots:
|
||||
- '@tanstack/router-core'
|
||||
- tiny-invariant
|
||||
|
||||
'@tanstack/router-generator@1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))':
|
||||
'@tanstack/router-generator@1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))':
|
||||
dependencies:
|
||||
'@tanstack/virtual-file-routes': 1.114.12
|
||||
prettier: 3.5.3
|
||||
tsx: 4.19.3
|
||||
zod: 3.24.2
|
||||
optionalDependencies:
|
||||
'@tanstack/react-router': 1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/react-router': 1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
|
||||
'@tanstack/router-plugin@1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.85.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0))':
|
||||
'@tanstack/router-plugin@1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(vite@6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.85.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0))':
|
||||
dependencies:
|
||||
'@babel/core': 7.26.9
|
||||
'@babel/plugin-syntax-jsx': 7.25.9(@babel/core@7.26.9)
|
||||
@@ -10580,8 +10580,8 @@ snapshots:
|
||||
'@babel/template': 7.26.9
|
||||
'@babel/traverse': 7.26.9
|
||||
'@babel/types': 7.26.9
|
||||
'@tanstack/router-core': 1.114.22
|
||||
'@tanstack/router-generator': 1.114.22(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
|
||||
'@tanstack/router-core': 1.114.23
|
||||
'@tanstack/router-generator': 1.114.23(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
|
||||
'@tanstack/router-utils': 1.114.12
|
||||
'@tanstack/virtual-file-routes': 1.114.12
|
||||
'@types/babel__core': 7.20.5
|
||||
@@ -10592,7 +10592,7 @@ snapshots:
|
||||
unplugin: 2.2.0
|
||||
zod: 3.24.2
|
||||
optionalDependencies:
|
||||
'@tanstack/react-router': 1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/react-router': 1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
vite: 6.2.2(@types/node@22.13.10)(jiti@2.4.2)(less@4.2.0)(lightningcss@1.29.2)(sass-embedded@1.85.1)(sass@1.83.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.19.3)(yaml@2.7.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -10604,9 +10604,9 @@ snapshots:
|
||||
ansis: 3.12.0
|
||||
diff: 7.0.0
|
||||
|
||||
'@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(zod@3.24.2)':
|
||||
'@tanstack/router-zod-adapter@1.81.5(@tanstack/react-router@1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@tanstack/react-router': 1.114.22(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
'@tanstack/react-router': 1.114.23(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
zod: 3.24.2
|
||||
|
||||
'@tanstack/store@0.7.0': {}
|
||||
@@ -12917,10 +12917,10 @@ snapshots:
|
||||
|
||||
fraction.js@4.3.7: {}
|
||||
|
||||
framer-motion@12.4.10(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
framer-motion@12.5.0(@emotion/is-prop-valid@1.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
motion-dom: 12.4.10
|
||||
motion-utils: 12.4.10
|
||||
motion-dom: 12.5.0
|
||||
motion-utils: 12.5.0
|
||||
tslib: 2.7.0
|
||||
optionalDependencies:
|
||||
'@emotion/is-prop-valid': 1.3.0
|
||||
@@ -14311,11 +14311,11 @@ snapshots:
|
||||
vscode-uri: 3.0.8
|
||||
yaml: 2.7.0
|
||||
|
||||
motion-dom@12.4.10:
|
||||
motion-dom@12.5.0:
|
||||
dependencies:
|
||||
motion-utils: 12.4.10
|
||||
motion-utils: 12.5.0
|
||||
|
||||
motion-utils@12.4.10: {}
|
||||
motion-utils@12.5.0: {}
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
|
||||
+8
@@ -269,6 +269,9 @@ jobs:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust Stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Add Rust Target
|
||||
run: rustup target add ${{ matrix.target }}
|
||||
|
||||
@@ -310,6 +313,11 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: alpha
|
||||
releaseName: "Clash Verge Rev Alpha"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
tauriScript: pnpm
|
||||
args: --target ${{ matrix.target }}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -70,6 +70,13 @@ pub fn get_app_dir() -> CmdResult<String> {
|
||||
Ok(app_home_dir)
|
||||
}
|
||||
|
||||
/// 获取当前自启动状态
|
||||
#[tauri::command]
|
||||
pub fn get_auto_launch_status() -> CmdResult<bool> {
|
||||
use crate::core::sysopt::Sysopt;
|
||||
wrap_err!(Sysopt::global().get_launch_status())
|
||||
}
|
||||
|
||||
/// 下载图标缓存
|
||||
#[tauri::command]
|
||||
pub async fn download_icon_cache(url: String, name: String) -> CmdResult<String> {
|
||||
|
||||
@@ -4,6 +4,7 @@ use reqwest::header::{HeaderMap, HeaderValue};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use tauri::command;
|
||||
use tokio::sync::Mutex;
|
||||
@@ -286,7 +287,7 @@ async fn check_gemini(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
|
||||
// 测试YouTube Premium
|
||||
// 测试 YouTube Premium
|
||||
async fn check_youtube_premium(client: &Client) -> UnlockItem {
|
||||
let url = "https://www.youtube.com/premium";
|
||||
|
||||
@@ -453,39 +454,43 @@ async fn check_bahamut_anime(client: &Client) -> UnlockItem {
|
||||
|
||||
// 测试 Netflix
|
||||
async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
// 首先尝试使用Fast.com API检测Netflix CDN区域
|
||||
let cdn_result = check_netflix_cdn(client).await;
|
||||
if cdn_result.status == "Yes" {
|
||||
return cdn_result;
|
||||
}
|
||||
|
||||
// 如果CDN方法失败,尝试传统的内容检测方法
|
||||
// 测试两个 Netflix 内容 (LEGO Ninjago 和 Breaking Bad)
|
||||
let url1 = "https://www.netflix.com/title/81280792"; // LEGO Ninjago
|
||||
let url2 = "https://www.netflix.com/title/70143836"; // Breaking Bad
|
||||
|
||||
let headers = {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"accept-language",
|
||||
HeaderValue::from_static("en-US,en;q=0.9"),
|
||||
);
|
||||
headers.insert("sec-ch-ua-mobile", HeaderValue::from_static("?0"));
|
||||
headers.insert(
|
||||
"sec-ch-ua-platform",
|
||||
HeaderValue::from_static("\"Windows\""),
|
||||
);
|
||||
headers.insert("sec-fetch-site", HeaderValue::from_static("none"));
|
||||
headers.insert("sec-fetch-mode", HeaderValue::from_static("navigate"));
|
||||
headers.insert("sec-fetch-user", HeaderValue::from_static("?1"));
|
||||
headers.insert("sec-fetch-dest", HeaderValue::from_static("document"));
|
||||
headers
|
||||
};
|
||||
|
||||
// 执行第一个请求
|
||||
let result1 = client.get(url1).headers(headers.clone()).send().await;
|
||||
|
||||
// 执行第二个请求
|
||||
let result2 = client.get(url2).headers(headers.clone()).send().await;
|
||||
// 创建简单的请求(不添加太多头部信息)
|
||||
let result1 = client.get(url1)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send().await;
|
||||
|
||||
// 检查连接失败情况
|
||||
if result1.is_err() || result2.is_err() {
|
||||
if let Err(e) = &result1 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (Network Connection)".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
// 如果第一个请求成功,尝试第二个请求
|
||||
let result2 = client.get(url2)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send().await;
|
||||
|
||||
if let Err(e) = &result2 {
|
||||
eprintln!("Netflix请求错误: {}", e);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
@@ -514,21 +519,23 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
};
|
||||
}
|
||||
|
||||
if status1 == 200 || status2 == 200 {
|
||||
if status1 == 200 || status1 == 301 || status2 == 200 || status2 == 301 {
|
||||
// 成功解锁,尝试获取地区信息
|
||||
match client
|
||||
.get("https://www.netflix.com/")
|
||||
.headers(headers)
|
||||
// 使用Netflix测试内容获取区域
|
||||
let test_url = "https://www.netflix.com/title/80018499";
|
||||
match client.get(test_url)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
if let Ok(body) = response.text().await {
|
||||
// 使用正则表达式提取区域代码
|
||||
let re = Regex::new(r#""id":"([A-Z]{2})"#).unwrap();
|
||||
if let Some(caps) = re.captures(&body) {
|
||||
if let Some(region_match) = caps.get(1) {
|
||||
let region_code = region_match.as_str();
|
||||
// 检查重定向位置
|
||||
if let Some(location) = response.headers().get("location") {
|
||||
if let Ok(location_str) = location.to_str() {
|
||||
// 解析位置获取区域
|
||||
let parts: Vec<&str> = location_str.split('/').collect();
|
||||
if parts.len() >= 4 {
|
||||
let region_code = parts[3].split('-').next().unwrap_or("unknown");
|
||||
let emoji = country_code_to_emoji(region_code);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
@@ -539,32 +546,108 @@ async fn check_netflix(client: &Client) -> UnlockItem {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果无法获取区域信息,仍然返回成功
|
||||
// 如果没有重定向,假设是美国
|
||||
let emoji = country_code_to_emoji("us");
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: None,
|
||||
region: Some(format!("{}{}", emoji, "us")),
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
Err(_) => UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
Err(e) => {
|
||||
eprintln!("获取Netflix区域信息失败: {}", e);
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes (但无法获取区域)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// 其他未知错误状态
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: format!("Failed (Error: {}_{}", status1, status2),
|
||||
status: format!("Failed (状态码: {}_{}", status1, status2),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用Fast.com API检测Netflix CDN区域
|
||||
async fn check_netflix_cdn(client: &Client) -> UnlockItem {
|
||||
// Fast.com API URL
|
||||
let url = "https://api.fast.com/netflix/speedtest/v2?https=true&token=YXNkZmFzZGxmbnNkYWZoYXNkZmhrYWxm&urlCount=5";
|
||||
|
||||
let result = client.get(url)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send().await;
|
||||
|
||||
match result {
|
||||
Ok(response) => {
|
||||
// 检查状态码
|
||||
if response.status().as_u16() == 403 {
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "No (IP Banned By Netflix)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
|
||||
// 尝试解析响应
|
||||
match response.json::<serde_json::Value>().await {
|
||||
Ok(data) => {
|
||||
// 尝试从数据中提取区域信息
|
||||
if let Some(targets) = data.get("targets").and_then(|t| t.as_array()) {
|
||||
if !targets.is_empty() {
|
||||
if let Some(location) = targets[0].get("location") {
|
||||
if let Some(country) = location.get("country").and_then(|c| c.as_str()) {
|
||||
let emoji = country_code_to_emoji(country);
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Yes".to_string(),
|
||||
region: Some(format!("{}{}", emoji, country)),
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法解析区域信息
|
||||
return UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Unknown".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("解析Fast.com API响应失败: {}", e);
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (解析错误)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Fast.com API请求失败: {}", e);
|
||||
UnlockItem {
|
||||
name: "Netflix".to_string(),
|
||||
status: "Failed (CDN API)".to_string(),
|
||||
region: None,
|
||||
check_time: Some(get_local_date_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试 Disney+
|
||||
async fn check_disney_plus(client: &Client) -> UnlockItem {
|
||||
// Disney+ 不支持 IPv6,但这里不做额外检查,因为我们使用的是系统默认网络
|
||||
@@ -1072,9 +1155,14 @@ pub async fn get_unlock_items() -> Result<Vec<UnlockItem>, String> {
|
||||
// 开始检测流媒体解锁状态
|
||||
#[command]
|
||||
pub async fn check_media_unlock() -> Result<Vec<UnlockItem>, String> {
|
||||
// 创建一个http客户端
|
||||
// 创建一个http客户端,增加更多配置
|
||||
let client = match Client::builder()
|
||||
.user_agent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
|
||||
.timeout(std::time::Duration::from_secs(30)) // 全局超时设置
|
||||
.danger_accept_invalid_certs(true) // 接受无效证书,防止SSL错误
|
||||
.danger_accept_invalid_hostnames(true) // 接受无效主机名
|
||||
.tcp_keepalive(std::time::Duration::from_secs(60)) // TCP keepalive
|
||||
.connection_verbose(true) // 详细连接信息
|
||||
.build() {
|
||||
Ok(client) => client,
|
||||
Err(e) => return Err(format!("创建HTTP客户端失败: {}", e)),
|
||||
|
||||
@@ -220,15 +220,50 @@ impl Sysopt {
|
||||
let enable = enable.unwrap_or(false);
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
println!("enable: {}", enable);
|
||||
|
||||
log::info!(target: "app", "Setting auto launch to: {}", enable);
|
||||
|
||||
match enable {
|
||||
true => log_err!(autostart_manager.enable()),
|
||||
false => log_err!(autostart_manager.disable()),
|
||||
true => {
|
||||
let result = autostart_manager.enable();
|
||||
if let Err(ref e) = result {
|
||||
log::error!(target: "app", "Failed to enable auto launch: {}", e);
|
||||
} else {
|
||||
log::info!(target: "app", "Auto launch enabled successfully");
|
||||
}
|
||||
log_err!(result)
|
||||
},
|
||||
false => {
|
||||
let result = autostart_manager.disable();
|
||||
if let Err(ref e) = result {
|
||||
log::error!(target: "app", "Failed to disable auto launch: {}", e);
|
||||
} else {
|
||||
log::info!(target: "app", "Auto launch disabled successfully");
|
||||
}
|
||||
log_err!(result)
|
||||
},
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取当前自启动的实际状态
|
||||
pub fn get_launch_status(&self) -> Result<bool> {
|
||||
let app_handle = Handle::global().app_handle().unwrap();
|
||||
let autostart_manager = app_handle.autolaunch();
|
||||
|
||||
match autostart_manager.is_enabled() {
|
||||
Ok(status) => {
|
||||
log::info!(target: "app", "Auto launch status: {}", status);
|
||||
Ok(status)
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "Failed to get auto launch status: {}", e);
|
||||
Err(anyhow::anyhow!("Failed to get auto launch status: {}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn guard_proxy(&self) {
|
||||
let _lock = self.guard_state.lock();
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ pub fn run() {
|
||||
cmd::get_running_mode,
|
||||
cmd::install_service,
|
||||
cmd::get_app_uptime,
|
||||
cmd::get_auto_launch_status,
|
||||
// clash
|
||||
cmd::get_clash_info,
|
||||
cmd::patch_clash_config,
|
||||
|
||||
@@ -7,47 +7,94 @@ import { EnhancedCard } from "./enhanced-card";
|
||||
import useSWR from "swr";
|
||||
import { getRules } from "@/services/api";
|
||||
import { getAppUptime } from "@/services/cmds";
|
||||
import { useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
// 将毫秒转换为时:分:秒格式的函数
|
||||
const formatUptime = (uptimeMs: number) => {
|
||||
const hours = Math.floor(uptimeMs / 3600000);
|
||||
const minutes = Math.floor((uptimeMs % 3600000) / 60000);
|
||||
const seconds = Math.floor((uptimeMs % 60000) / 1000);
|
||||
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||
};
|
||||
|
||||
export const ClashInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { clashInfo } = useClashInfo();
|
||||
const { version: clashVersion } = useClash();
|
||||
|
||||
// 计算运行时间
|
||||
const [uptime, setUptime] = useState("0:00:00");
|
||||
|
||||
// 使用SWR定期获取应用运行时间
|
||||
useSWR(
|
||||
// 使用SWR获取应用运行时间,降低更新频率
|
||||
const { data: uptimeMs = 0 } = useSWR(
|
||||
"appUptime",
|
||||
async () => {
|
||||
const uptimeMs = await getAppUptime();
|
||||
// 将毫秒转换为时:分:秒格式
|
||||
const hours = Math.floor(uptimeMs / 3600000);
|
||||
const minutes = Math.floor((uptimeMs % 3600000) / 60000);
|
||||
const seconds = Math.floor((uptimeMs % 60000) / 1000);
|
||||
setUptime(
|
||||
`${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`,
|
||||
);
|
||||
return uptimeMs;
|
||||
},
|
||||
getAppUptime,
|
||||
{
|
||||
refreshInterval: 1000, // 每秒更新一次
|
||||
refreshInterval: 1000,
|
||||
revalidateOnFocus: false,
|
||||
dedupingInterval: 500,
|
||||
dedupingInterval: 1000,
|
||||
},
|
||||
);
|
||||
|
||||
// 获取规则数
|
||||
const { data: rulesData } = useSWR("getRules", getRules, {
|
||||
fallbackData: [],
|
||||
suspense: false,
|
||||
// 使用useMemo缓存格式化后的uptime,避免频繁计算
|
||||
const uptime = useMemo(() => formatUptime(uptimeMs), [uptimeMs]);
|
||||
|
||||
// 获取规则数据,只在组件加载时获取一次
|
||||
const { data: rules = [] } = useSWR("getRules", getRules, {
|
||||
revalidateOnFocus: false,
|
||||
errorRetryCount: 2,
|
||||
});
|
||||
|
||||
// 获取规则数据
|
||||
const rules = rulesData || [];
|
||||
// 使用备忘录组件内容,减少重新渲染
|
||||
const cardContent = useMemo(() => {
|
||||
if (!clashInfo) return null;
|
||||
|
||||
return (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Core Version")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashVersion || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("System Proxy Address")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.server || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Mixed Port")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.mixed_port || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Uptime")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{uptime}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Rules Count")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{rules.length}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}, [clashInfo, clashVersion, t, uptime, rules.length]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
@@ -56,54 +103,7 @@ export const ClashInfoCard = () => {
|
||||
iconColor="warning"
|
||||
action={null}
|
||||
>
|
||||
{clashInfo && (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Core Version")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashVersion || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("System Proxy Address")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.server || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Mixed Port")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{clashInfo.mixed_port || "-"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Uptime")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{uptime}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Rules Count")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{rules.length}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
)}
|
||||
{cardContent}
|
||||
</EnhancedCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
MultipleStopRounded,
|
||||
DirectionsRounded,
|
||||
} from "@mui/icons-material";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
|
||||
export const ClashModeCard = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -20,15 +20,13 @@ export const ClashModeCard = () => {
|
||||
const { data: clashConfig, mutate: mutateClash } = useSWR(
|
||||
"getClashConfig",
|
||||
getClashConfig,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
},
|
||||
{ revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// 支持的模式列表 - 添加直连模式
|
||||
const modeList = ["rule", "global", "direct"];
|
||||
// 支持的模式列表
|
||||
const modeList = useMemo(() => ["rule", "global", "direct"] as const, []);
|
||||
|
||||
// 本地状态记录当前模式,提供更快的UI响应
|
||||
// 本地状态记录当前模式
|
||||
const [localMode, setLocalMode] = useState<string>("rule");
|
||||
|
||||
// 当从API获取到当前模式时更新本地状态
|
||||
@@ -38,25 +36,27 @@ export const ClashModeCard = () => {
|
||||
}
|
||||
}, [clashConfig]);
|
||||
|
||||
// 模式图标映射
|
||||
const modeIcons = useMemo(() => ({
|
||||
rule: <MultipleStopRounded fontSize="small" />,
|
||||
global: <LanguageRounded fontSize="small" />,
|
||||
direct: <DirectionsRounded fontSize="small" />
|
||||
}), []);
|
||||
|
||||
// 切换模式的处理函数
|
||||
const onChangeMode = useLockFn(async (mode: string) => {
|
||||
// 如果已经是当前模式,不做任何操作
|
||||
if (mode === localMode) return;
|
||||
|
||||
// 立即更新本地UI状态
|
||||
|
||||
setLocalMode(mode);
|
||||
|
||||
// 断开连接(如果启用了设置)
|
||||
|
||||
if (verge?.auto_close_connection) {
|
||||
closeAllConnections();
|
||||
}
|
||||
|
||||
try {
|
||||
await patchClashMode(mode);
|
||||
// 成功后刷新数据
|
||||
mutateClash();
|
||||
} catch (error) {
|
||||
// 如果操作失败,恢复之前的状态
|
||||
console.error("Failed to change mode:", error);
|
||||
if (clashConfig?.mode) {
|
||||
setLocalMode(clashConfig.mode.toLowerCase());
|
||||
@@ -64,32 +64,55 @@ export const ClashModeCard = () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 获取模式对应的图标
|
||||
const getModeIcon = (mode: string) => {
|
||||
switch (mode) {
|
||||
case "rule":
|
||||
return <MultipleStopRounded fontSize="small" />;
|
||||
case "global":
|
||||
return <LanguageRounded fontSize="small" />;
|
||||
case "direct":
|
||||
return <DirectionsRounded fontSize="small" />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
// 按钮样式
|
||||
const buttonStyles = (mode: string) => ({
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: 1.2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor: mode === localMode ? "primary.main" : "background.paper",
|
||||
color: mode === localMode ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
overflow: "visible",
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: 1,
|
||||
},
|
||||
"&:active": {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
"&::after": mode === localMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -16,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 16,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
});
|
||||
|
||||
// 获取模式说明文字
|
||||
const getModeDescription = (mode: string) => {
|
||||
switch (mode) {
|
||||
case "rule":
|
||||
return t("Rule Mode Description");
|
||||
case "global":
|
||||
return t("Global Mode Description");
|
||||
case "direct":
|
||||
return t("Direct Mode Description");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
// 描述样式
|
||||
const descriptionStyles = {
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -111,44 +134,9 @@ export const ClashModeCard = () => {
|
||||
key={mode}
|
||||
elevation={mode === localMode ? 2 : 0}
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: 1.2,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor: mode === localMode ? "primary.main" : "background.paper",
|
||||
color:
|
||||
mode === localMode ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
overflow: "visible",
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: 1,
|
||||
},
|
||||
"&:active": {
|
||||
transform: "translateY(1px)",
|
||||
},
|
||||
"&::after":
|
||||
mode === localMode
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -16,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 16,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
sx={buttonStyles(mode)}
|
||||
>
|
||||
{getModeIcon(mode)}
|
||||
{modeIcons[mode]}
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
@@ -173,77 +161,15 @@ export const ClashModeCard = () => {
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
{localMode === "rule" && (
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
{getModeDescription("rule")}
|
||||
</Typography>
|
||||
</Fade>
|
||||
)}
|
||||
|
||||
{localMode === "global" && (
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
{getModeDescription("global")}
|
||||
</Typography>
|
||||
</Fade>
|
||||
)}
|
||||
|
||||
{localMode === "direct" && (
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
{getModeDescription("direct")}
|
||||
</Typography>
|
||||
</Fade>
|
||||
)}
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={descriptionStyles}
|
||||
>
|
||||
{t(`${localMode} Mode Description`)}
|
||||
</Typography>
|
||||
</Fade>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
SelectChangeEvent,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useMemo, useCallback, useRef } from "react";
|
||||
import {
|
||||
SignalWifi4Bar as SignalStrong,
|
||||
SignalWifi3Bar as SignalGood,
|
||||
@@ -45,17 +45,7 @@ interface ProxyOption {
|
||||
}
|
||||
|
||||
// 将delayManager返回的颜色格式转换为MUI Chip组件需要的格式
|
||||
function convertDelayColor(
|
||||
delayValue: number,
|
||||
):
|
||||
| "default"
|
||||
| "success"
|
||||
| "warning"
|
||||
| "error"
|
||||
| "primary"
|
||||
| "secondary"
|
||||
| "info"
|
||||
| undefined {
|
||||
function convertDelayColor(delayValue: number) {
|
||||
const colorStr = delayManager.formatDelayColor(delayValue);
|
||||
if (!colorStr) return "default";
|
||||
|
||||
@@ -63,445 +53,365 @@ function convertDelayColor(
|
||||
const mainColor = colorStr.split(".")[0];
|
||||
|
||||
switch (mainColor) {
|
||||
case "success":
|
||||
return "success";
|
||||
case "warning":
|
||||
return "warning";
|
||||
case "error":
|
||||
return "error";
|
||||
case "primary":
|
||||
return "primary";
|
||||
default:
|
||||
return "default";
|
||||
case "success": return "success";
|
||||
case "warning": return "warning";
|
||||
case "error": return "error";
|
||||
case "primary": return "primary";
|
||||
default: return "default";
|
||||
}
|
||||
}
|
||||
|
||||
// 根据延迟值获取合适的WiFi信号图标
|
||||
function getSignalIcon(delay: number): {
|
||||
icon: JSX.Element;
|
||||
text: string;
|
||||
color: string;
|
||||
} {
|
||||
function getSignalIcon(delay: number) {
|
||||
if (delay < 0)
|
||||
return {
|
||||
icon: <SignalNone />,
|
||||
text: "未测试",
|
||||
color: "text.secondary",
|
||||
};
|
||||
return { icon: <SignalNone />, text: "未测试", color: "text.secondary" };
|
||||
if (delay >= 10000)
|
||||
return {
|
||||
icon: <SignalError />,
|
||||
text: "超时",
|
||||
color: "error.main",
|
||||
};
|
||||
return { icon: <SignalError />, text: "超时", color: "error.main" };
|
||||
if (delay >= 500)
|
||||
return {
|
||||
icon: <SignalWeak />,
|
||||
text: "延迟较高",
|
||||
color: "error.main",
|
||||
};
|
||||
return { icon: <SignalWeak />, text: "延迟较高", color: "error.main" };
|
||||
if (delay >= 300)
|
||||
return {
|
||||
icon: <SignalMedium />,
|
||||
text: "延迟中等",
|
||||
color: "warning.main",
|
||||
};
|
||||
return { icon: <SignalMedium />, text: "延迟中等", color: "warning.main" };
|
||||
if (delay >= 200)
|
||||
return {
|
||||
icon: <SignalGood />,
|
||||
text: "延迟良好",
|
||||
color: "info.main",
|
||||
};
|
||||
return {
|
||||
icon: <SignalStrong />,
|
||||
text: "延迟极佳",
|
||||
color: "success.main",
|
||||
return { icon: <SignalGood />, text: "延迟良好", color: "info.main" };
|
||||
return { icon: <SignalStrong />, text: "延迟极佳", color: "success.main" };
|
||||
}
|
||||
|
||||
// 简单的防抖函数
|
||||
function debounce(fn: Function, ms = 100) {
|
||||
let timeoutId: ReturnType<typeof setTimeout>;
|
||||
return function(this: any, ...args: any[]) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn.apply(this, args), ms);
|
||||
};
|
||||
}
|
||||
|
||||
export const CurrentProxyCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const { currentProxy, primaryGroupName, mode, refreshProxy } =
|
||||
useCurrentProxy();
|
||||
const { currentProxy, primaryGroupName, mode, refreshProxy } = useCurrentProxy();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const { verge } = useVerge();
|
||||
|
||||
// 判断模式
|
||||
const isGlobalMode = mode === "global";
|
||||
const isDirectMode = mode === "direct"; // 添加直连模式判断
|
||||
const isDirectMode = mode === "direct";
|
||||
|
||||
// 从本地存储获取初始值,如果是特殊模式或没有存储值则使用默认值
|
||||
const getSavedGroup = () => {
|
||||
// 全局模式使用 GLOBAL 组
|
||||
if (isGlobalMode) {
|
||||
return "GLOBAL";
|
||||
}
|
||||
// 直连模式使用 DIRECT
|
||||
if (isDirectMode) {
|
||||
return "DIRECT";
|
||||
}
|
||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
||||
return savedGroup || primaryGroupName || "GLOBAL";
|
||||
// 使用 useRef 存储最后一次刷新时间和是否正在刷新
|
||||
const lastRefreshRef = useRef<number>(0);
|
||||
const isRefreshingRef = useRef<boolean>(false);
|
||||
const pendingRefreshRef = useRef<boolean>(false);
|
||||
|
||||
// 定义状态类型
|
||||
type ProxyState = {
|
||||
proxyData: {
|
||||
groups: { name: string; now: string; all: string[] }[];
|
||||
records: Record<string, any>;
|
||||
globalProxy: string;
|
||||
directProxy: any;
|
||||
};
|
||||
selection: {
|
||||
group: string;
|
||||
proxy: string;
|
||||
};
|
||||
displayProxy: any;
|
||||
};
|
||||
|
||||
// 状态管理
|
||||
const [groups, setGroups] = useState<
|
||||
{ name: string; now: string; all: string[] }[]
|
||||
>([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState<string>(getSavedGroup());
|
||||
const [proxyOptions, setProxyOptions] = useState<ProxyOption[]>([]);
|
||||
const [selectedProxy, setSelectedProxy] = useState<string>("");
|
||||
const [displayProxy, setDisplayProxy] = useState<any>(null);
|
||||
const [records, setRecords] = useState<Record<string, any>>({});
|
||||
const [globalProxy, setGlobalProxy] = useState<string>(""); // 存储全局代理
|
||||
const [directProxy, setDirectProxy] = useState<any>(null); // 存储直连代理信息
|
||||
// 合并状态,减少状态更新次数
|
||||
const [state, setState] = useState<ProxyState>({
|
||||
proxyData: {
|
||||
groups: [],
|
||||
records: {},
|
||||
globalProxy: "",
|
||||
directProxy: null,
|
||||
},
|
||||
selection: {
|
||||
group: "",
|
||||
proxy: "",
|
||||
},
|
||||
displayProxy: null,
|
||||
});
|
||||
|
||||
// 保存选择的代理组到本地存储
|
||||
useEffect(() => {
|
||||
// 只有在普通模式下才保存到本地存储
|
||||
if (selectedGroup && !isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, selectedGroup);
|
||||
}
|
||||
}, [selectedGroup, isGlobalMode, isDirectMode]);
|
||||
|
||||
// 保存选择的代理节点到本地存储
|
||||
useEffect(() => {
|
||||
// 只有在普通模式下才保存到本地存储
|
||||
if (selectedProxy && !isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, selectedProxy);
|
||||
}
|
||||
}, [selectedProxy, isGlobalMode, isDirectMode]);
|
||||
|
||||
// 当模式变化时更新选择的组
|
||||
// 初始化选择的组
|
||||
useEffect(() => {
|
||||
// 根据模式确定初始组
|
||||
if (isGlobalMode) {
|
||||
setSelectedGroup("GLOBAL");
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: "GLOBAL"
|
||||
}
|
||||
}));
|
||||
} else if (isDirectMode) {
|
||||
setSelectedGroup("DIRECT");
|
||||
} else if (primaryGroupName) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: "DIRECT"
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
||||
setSelectedGroup(savedGroup || primaryGroupName);
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: savedGroup || primaryGroupName || ""
|
||||
}
|
||||
}));
|
||||
}
|
||||
}, [isGlobalMode, isDirectMode, primaryGroupName]);
|
||||
|
||||
// 获取所有代理组和代理信息
|
||||
useEffect(() => {
|
||||
const fetchProxies = async () => {
|
||||
try {
|
||||
const data = await getProxies();
|
||||
// 保存所有节点记录信息,用于显示详细节点信息
|
||||
setRecords(data.records);
|
||||
|
||||
// 检查并存储全局代理信息
|
||||
if (data.global) {
|
||||
setGlobalProxy(data.global.now || "");
|
||||
}
|
||||
|
||||
// 查找并存储直连代理信息
|
||||
if (data.records && data.records["DIRECT"]) {
|
||||
setDirectProxy(data.records["DIRECT"]);
|
||||
}
|
||||
|
||||
const filteredGroups = data.groups
|
||||
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
|
||||
.map((g) => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map((p) => p.name),
|
||||
}));
|
||||
|
||||
setGroups(filteredGroups);
|
||||
|
||||
// 直连模式处理
|
||||
if (isDirectMode) {
|
||||
// 直连模式下使用 DIRECT 节点
|
||||
setSelectedGroup("DIRECT");
|
||||
setSelectedProxy("DIRECT");
|
||||
|
||||
if (data.records && data.records["DIRECT"]) {
|
||||
setDisplayProxy(data.records["DIRECT"]);
|
||||
}
|
||||
|
||||
// 设置仅包含 DIRECT 节点的选项
|
||||
setProxyOptions([{ name: "DIRECT" }]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 全局模式处理
|
||||
if (isGlobalMode) {
|
||||
// 在全局模式下,使用 GLOBAL 组和 data.global.now 作为选中节点
|
||||
if (data.global) {
|
||||
const globalNow = data.global.now || "";
|
||||
setSelectedGroup("GLOBAL");
|
||||
setSelectedProxy(globalNow);
|
||||
|
||||
if (globalNow && data.records[globalNow]) {
|
||||
setDisplayProxy(data.records[globalNow]);
|
||||
}
|
||||
|
||||
// 设置全局组的代理选项
|
||||
const options = data.global.all.map((proxy) => ({
|
||||
name: proxy.name,
|
||||
}));
|
||||
|
||||
setProxyOptions(options);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 以下是普通模式的处理逻辑
|
||||
let targetGroup = primaryGroupName;
|
||||
|
||||
// 非特殊模式下,尝试从本地存储获取上次选择的代理组
|
||||
const savedGroup = localStorage.getItem(STORAGE_KEY_GROUP);
|
||||
targetGroup = savedGroup || primaryGroupName;
|
||||
|
||||
// 如果目标组在列表中,则选择它
|
||||
if (targetGroup && filteredGroups.some((g) => g.name === targetGroup)) {
|
||||
setSelectedGroup(targetGroup);
|
||||
|
||||
// 设置该组下的代理选项
|
||||
const currentGroup = filteredGroups.find(
|
||||
(g) => g.name === targetGroup,
|
||||
);
|
||||
if (currentGroup) {
|
||||
// 创建代理选项
|
||||
const options = currentGroup.all.map((proxyName) => {
|
||||
return { name: proxyName };
|
||||
});
|
||||
|
||||
setProxyOptions(options);
|
||||
|
||||
let targetProxy = currentGroup.now;
|
||||
|
||||
const savedProxy = localStorage.getItem(STORAGE_KEY_PROXY);
|
||||
// 如果有保存的代理节点且该节点在当前组中,则选择它
|
||||
if (savedProxy && currentGroup.all.includes(savedProxy)) {
|
||||
targetProxy = savedProxy;
|
||||
}
|
||||
|
||||
setSelectedProxy(targetProxy);
|
||||
|
||||
if (targetProxy && data.records[targetProxy]) {
|
||||
setDisplayProxy(data.records[targetProxy]);
|
||||
}
|
||||
}
|
||||
} else if (filteredGroups.length > 0) {
|
||||
// 否则选择第一个组
|
||||
setSelectedGroup(filteredGroups[0].name);
|
||||
|
||||
// 创建代理选项
|
||||
const options = filteredGroups[0].all.map((proxyName) => {
|
||||
return { name: proxyName };
|
||||
});
|
||||
|
||||
setProxyOptions(options);
|
||||
setSelectedProxy(filteredGroups[0].now);
|
||||
|
||||
// 更新显示的代理节点信息
|
||||
if (filteredGroups[0].now && data.records[filteredGroups[0].now]) {
|
||||
setDisplayProxy(data.records[filteredGroups[0].now]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取代理信息失败", error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchProxies();
|
||||
}, [primaryGroupName, isGlobalMode, isDirectMode]);
|
||||
|
||||
// 当选择的组发生变化时更新代理选项
|
||||
useEffect(() => {
|
||||
// 如果是特殊模式,已在 fetchProxies 中处理
|
||||
if (isGlobalMode || isDirectMode) return;
|
||||
|
||||
const group = groups.find((g) => g.name === selectedGroup);
|
||||
if (group && records) {
|
||||
// 创建代理选项
|
||||
const options = group.all.map((proxyName) => {
|
||||
return { name: proxyName };
|
||||
});
|
||||
|
||||
setProxyOptions(options);
|
||||
|
||||
let targetProxy = group.now;
|
||||
|
||||
const savedProxy = localStorage.getItem(STORAGE_KEY_PROXY);
|
||||
// 如果保存的代理节点在当前组中,则选择它
|
||||
if (savedProxy && group.all.includes(savedProxy)) {
|
||||
targetProxy = savedProxy;
|
||||
}
|
||||
|
||||
setSelectedProxy(targetProxy);
|
||||
|
||||
if (targetProxy && records[targetProxy]) {
|
||||
setDisplayProxy(records[targetProxy]);
|
||||
}
|
||||
// 带锁的代理数据获取函数,防止并发请求
|
||||
const fetchProxyData = useCallback(async (force = false) => {
|
||||
// 防止重复请求
|
||||
if (isRefreshingRef.current) {
|
||||
pendingRefreshRef.current = true;
|
||||
return;
|
||||
}
|
||||
}, [selectedGroup, groups, records, isGlobalMode, isDirectMode]);
|
||||
|
||||
// 刷新代理信息
|
||||
const refreshProxyData = async () => {
|
||||
// 检查刷新间隔
|
||||
const now = Date.now();
|
||||
if (!force && now - lastRefreshRef.current < 1000) {
|
||||
return;
|
||||
}
|
||||
|
||||
isRefreshingRef.current = true;
|
||||
lastRefreshRef.current = now;
|
||||
|
||||
try {
|
||||
const data = await getProxies();
|
||||
// 更新所有代理记录
|
||||
setRecords(data.records);
|
||||
|
||||
// 更新代理组信息
|
||||
|
||||
// 过滤和格式化组
|
||||
const filteredGroups = data.groups
|
||||
.filter((g) => g.name !== "DIRECT" && g.name !== "REJECT")
|
||||
.map((g) => ({
|
||||
.filter(g => g.name !== "DIRECT" && g.name !== "REJECT")
|
||||
.map(g => ({
|
||||
name: g.name,
|
||||
now: g.now || "",
|
||||
all: g.all.map((p) => p.name),
|
||||
all: g.all.map(p => p.name),
|
||||
}));
|
||||
|
||||
setGroups(filteredGroups);
|
||||
// 使用函数式更新确保状态更新的原子性
|
||||
setState(prev => {
|
||||
let newProxy = "";
|
||||
let newDisplayProxy = null;
|
||||
let newGroup = prev.selection.group;
|
||||
|
||||
// 检查并更新全局代理信息
|
||||
if (isGlobalMode && data.global) {
|
||||
const globalNow = data.global.now || "";
|
||||
setSelectedProxy(globalNow);
|
||||
|
||||
if (globalNow && data.records[globalNow]) {
|
||||
setDisplayProxy(data.records[globalNow]);
|
||||
}
|
||||
|
||||
// 更新全局组的代理选项
|
||||
const options = data.global.all.map((proxy) => ({
|
||||
name: proxy.name,
|
||||
}));
|
||||
|
||||
setProxyOptions(options);
|
||||
}
|
||||
// 更新直连代理信息
|
||||
else if (isDirectMode && data.records["DIRECT"]) {
|
||||
setDirectProxy(data.records["DIRECT"]);
|
||||
setDisplayProxy(data.records["DIRECT"]);
|
||||
}
|
||||
// 更新普通模式下当前选中组的信息
|
||||
else {
|
||||
const currentGroup = filteredGroups.find(
|
||||
(g) => g.name === selectedGroup,
|
||||
);
|
||||
if (currentGroup) {
|
||||
// 如果当前选中的代理节点与组中的now不一致,则需要更新
|
||||
if (currentGroup.now !== selectedProxy) {
|
||||
setSelectedProxy(currentGroup.now);
|
||||
|
||||
if (data.records[currentGroup.now]) {
|
||||
setDisplayProxy(data.records[currentGroup.now]);
|
||||
// 根据模式确定新代理
|
||||
if (isDirectMode) {
|
||||
newGroup = "DIRECT";
|
||||
newProxy = "DIRECT";
|
||||
newDisplayProxy = data.records?.DIRECT || null;
|
||||
} else if (isGlobalMode && data.global) {
|
||||
newGroup = "GLOBAL";
|
||||
newProxy = data.global.now || "";
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
} else {
|
||||
// 普通模式 - 检查当前选择的组是否存在
|
||||
const currentGroup = filteredGroups.find(g => g.name === prev.selection.group);
|
||||
|
||||
// 如果当前组不存在或为空,自动选择第一个组
|
||||
if (!currentGroup && filteredGroups.length > 0) {
|
||||
newGroup = filteredGroups[0].name;
|
||||
const firstGroup = filteredGroups[0];
|
||||
newProxy = firstGroup.now;
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
|
||||
// 保存到本地存储
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
if (newProxy) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
}
|
||||
} else if (currentGroup) {
|
||||
// 使用当前组的代理
|
||||
newProxy = currentGroup.now;
|
||||
newDisplayProxy = data.records?.[newProxy] || null;
|
||||
}
|
||||
|
||||
// 更新代理选项
|
||||
const options = currentGroup.all.map((proxyName) => ({
|
||||
name: proxyName,
|
||||
}));
|
||||
|
||||
setProxyOptions(options);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回新状态
|
||||
return {
|
||||
proxyData: {
|
||||
groups: filteredGroups,
|
||||
records: data.records || {},
|
||||
globalProxy: data.global?.now || "",
|
||||
directProxy: data.records?.DIRECT || null,
|
||||
},
|
||||
selection: {
|
||||
group: newGroup,
|
||||
proxy: newProxy
|
||||
},
|
||||
displayProxy: newDisplayProxy
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("刷新代理信息失败", error);
|
||||
console.error("获取代理信息失败", error);
|
||||
} finally {
|
||||
isRefreshingRef.current = false;
|
||||
|
||||
// 处理待处理的刷新请求
|
||||
if (pendingRefreshRef.current) {
|
||||
pendingRefreshRef.current = false;
|
||||
setTimeout(() => fetchProxyData(), 100);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [isGlobalMode, isDirectMode]);
|
||||
|
||||
// 每隔一段时间刷新代理信息 - 修改为在所有模式下都刷新
|
||||
// 响应 currentProxy 变化
|
||||
useEffect(() => {
|
||||
// 初始刷新一次
|
||||
refreshProxyData();
|
||||
if (currentProxy && (!state.displayProxy || currentProxy.name !== state.displayProxy.name)) {
|
||||
fetchProxyData(true);
|
||||
}
|
||||
}, [currentProxy, fetchProxyData, state.displayProxy]);
|
||||
|
||||
// 定期刷新所有模式下的代理信息
|
||||
const refreshInterval = setInterval(refreshProxyData, 2000);
|
||||
return () => clearInterval(refreshInterval);
|
||||
}, [isGlobalMode, isDirectMode, selectedGroup]); // 依赖项添加selectedGroup以便在切换组时重新设置定时器
|
||||
// 平滑的定期刷新,使用固定间隔
|
||||
useEffect(() => {
|
||||
fetchProxyData();
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
fetchProxyData();
|
||||
}, 3000); // 使用固定的3秒间隔,平衡响应速度和性能
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [fetchProxyData]);
|
||||
|
||||
// 计算要显示的代理选项 - 使用 useMemo 优化
|
||||
const proxyOptions = useMemo(() => {
|
||||
if (isDirectMode) {
|
||||
return [{ name: "DIRECT" }];
|
||||
}
|
||||
if (isGlobalMode && state.proxyData.records) {
|
||||
// 全局模式下的选项
|
||||
return Object.keys(state.proxyData.records)
|
||||
.filter(name => name !== "DIRECT" && name !== "REJECT")
|
||||
.map(name => ({ name }));
|
||||
}
|
||||
|
||||
// 普通模式
|
||||
const group = state.proxyData.groups.find(g => g.name === state.selection.group);
|
||||
if (group) {
|
||||
return group.all.map(name => ({ name }));
|
||||
}
|
||||
return [];
|
||||
}, [isDirectMode, isGlobalMode, state.proxyData, state.selection.group]);
|
||||
|
||||
// 使用防抖包装状态更新,避免快速连续更新
|
||||
const debouncedSetState = useCallback(
|
||||
debounce((updateFn: (prev: ProxyState) => ProxyState) => {
|
||||
setState(updateFn);
|
||||
}, 50),
|
||||
[]
|
||||
);
|
||||
|
||||
// 处理代理组变更
|
||||
const handleGroupChange = (event: SelectChangeEvent) => {
|
||||
// 特殊模式下不允许切换组
|
||||
const handleGroupChange = useCallback((event: SelectChangeEvent) => {
|
||||
if (isGlobalMode || isDirectMode) return;
|
||||
|
||||
|
||||
const newGroup = event.target.value;
|
||||
setSelectedGroup(newGroup);
|
||||
};
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem(STORAGE_KEY_GROUP, newGroup);
|
||||
|
||||
// 获取该组当前选中的代理
|
||||
setState(prev => {
|
||||
const group = prev.proxyData.groups.find(g => g.name === newGroup);
|
||||
if (group) {
|
||||
return {
|
||||
...prev,
|
||||
selection: {
|
||||
group: newGroup,
|
||||
proxy: group.now
|
||||
},
|
||||
displayProxy: prev.proxyData.records[group.now] || null
|
||||
};
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
group: newGroup
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [isGlobalMode, isDirectMode]);
|
||||
|
||||
// 处理代理节点变更
|
||||
const handleProxyChange = async (event: SelectChangeEvent) => {
|
||||
// 直连模式下不允许切换节点
|
||||
const handleProxyChange = useCallback(async (event: SelectChangeEvent) => {
|
||||
if (isDirectMode) return;
|
||||
|
||||
|
||||
const newProxy = event.target.value;
|
||||
const previousProxy = selectedProxy; // 保存变更前的代理节点名称
|
||||
|
||||
setSelectedProxy(newProxy);
|
||||
|
||||
// 更新显示的代理节点信息
|
||||
if (records[newProxy]) {
|
||||
setDisplayProxy(records[newProxy]);
|
||||
const currentGroup = state.selection.group;
|
||||
const previousProxy = state.selection.proxy;
|
||||
|
||||
// 立即更新UI,优化体验
|
||||
debouncedSetState((prev: ProxyState) => ({
|
||||
...prev,
|
||||
selection: {
|
||||
...prev.selection,
|
||||
proxy: newProxy
|
||||
},
|
||||
displayProxy: prev.proxyData.records[newProxy] || null
|
||||
}));
|
||||
|
||||
// 非特殊模式下保存到本地存储
|
||||
if (!isGlobalMode && !isDirectMode) {
|
||||
localStorage.setItem(STORAGE_KEY_PROXY, newProxy);
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 更新代理设置
|
||||
await updateProxy(selectedGroup, newProxy);
|
||||
|
||||
// 添加断开连接逻辑 - 与proxy-groups.tsx中的逻辑相同
|
||||
await updateProxy(currentGroup, newProxy);
|
||||
|
||||
// 自动关闭连接设置
|
||||
if (verge?.auto_close_connection && previousProxy) {
|
||||
getConnections().then(({ connections }) => {
|
||||
connections.forEach((conn) => {
|
||||
connections.forEach(conn => {
|
||||
if (conn.chains.includes(previousProxy)) {
|
||||
deleteConnection(conn.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 刷新代理信息,使用较短的延迟
|
||||
setTimeout(() => {
|
||||
refreshProxy();
|
||||
if (isGlobalMode || isDirectMode) {
|
||||
refreshProxyData(); // 特殊模式下额外刷新数据
|
||||
}
|
||||
}, 300);
|
||||
fetchProxyData(true);
|
||||
}, 200);
|
||||
} catch (error) {
|
||||
console.error("更新代理失败", error);
|
||||
}
|
||||
};
|
||||
}, [isDirectMode, isGlobalMode, state.proxyData.records, state.selection, verge?.auto_close_connection, refreshProxy, fetchProxyData, debouncedSetState]);
|
||||
|
||||
// 导航到代理页面
|
||||
const goToProxies = () => {
|
||||
// 修正路由路径,根据_routers.tsx配置,代理页面的路径是"/"
|
||||
const goToProxies = useCallback(() => {
|
||||
navigate("/");
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
// 获取要显示的代理节点
|
||||
const proxyToDisplay = displayProxy || currentProxy;
|
||||
|
||||
const proxyToDisplay = state.displayProxy || currentProxy;
|
||||
|
||||
// 获取当前节点的延迟
|
||||
const currentDelay = proxyToDisplay
|
||||
? delayManager.getDelayFix(proxyToDisplay, selectedGroup)
|
||||
? delayManager.getDelayFix(proxyToDisplay, state.selection.group)
|
||||
: -1;
|
||||
|
||||
|
||||
// 获取信号图标
|
||||
const signalInfo = getSignalIcon(currentDelay);
|
||||
|
||||
// 自定义渲染选择框中的值
|
||||
const renderProxyValue = (selected: string) => {
|
||||
if (!selected || !records[selected]) return selected;
|
||||
const renderProxyValue = useCallback((selected: string) => {
|
||||
if (!selected || !state.proxyData.records[selected]) return selected;
|
||||
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
records[selected],
|
||||
selectedGroup,
|
||||
state.proxyData.records[selected],
|
||||
state.selection.group
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||
<Typography noWrap>{selected}</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
@@ -510,7 +420,7 @@ export const CurrentProxyCard = () => {
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
}, [state.proxyData.records, state.selection.group]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
@@ -561,48 +471,22 @@ export const CurrentProxyCard = () => {
|
||||
{proxyToDisplay.name}
|
||||
</Typography>
|
||||
|
||||
<Box
|
||||
sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
<Box sx={{ display: "flex", alignItems: "center", flexWrap: "wrap" }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ mr: 1 }}>
|
||||
{proxyToDisplay.type}
|
||||
</Typography>
|
||||
{isGlobalMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Global Mode")}
|
||||
color="primary"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<Chip size="small" label={t("Global Mode")} color="primary" sx={{ mr: 0.5 }} />
|
||||
)}
|
||||
{isDirectMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={t("Direct Mode")}
|
||||
color="success"
|
||||
sx={{ mr: 0.5 }}
|
||||
/>
|
||||
<Chip size="small" label={t("Direct Mode")} color="success" sx={{ mr: 0.5 }} />
|
||||
)}
|
||||
{/* 节点特性 */}
|
||||
{proxyToDisplay.udp && (
|
||||
<Chip size="small" label="UDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.tfo && (
|
||||
<Chip size="small" label="TFO" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.xudp && (
|
||||
<Chip size="small" label="XUDP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.mptcp && (
|
||||
<Chip size="small" label="MPTCP" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.smux && (
|
||||
<Chip size="small" label="SMUX" variant="outlined" />
|
||||
)}
|
||||
{proxyToDisplay.udp && <Chip size="small" label="UDP" variant="outlined" />}
|
||||
{proxyToDisplay.tfo && <Chip size="small" label="TFO" variant="outlined" />}
|
||||
{proxyToDisplay.xudp && <Chip size="small" label="XUDP" variant="outlined" />}
|
||||
{proxyToDisplay.mptcp && <Chip size="small" label="MPTCP" variant="outlined" />}
|
||||
{proxyToDisplay.smux && <Chip size="small" label="SMUX" variant="outlined" />}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -610,31 +494,22 @@ export const CurrentProxyCard = () => {
|
||||
{proxyToDisplay && !isDirectMode && (
|
||||
<Chip
|
||||
size="small"
|
||||
label={delayManager.formatDelay(
|
||||
delayManager.getDelayFix(proxyToDisplay, selectedGroup),
|
||||
)}
|
||||
color={convertDelayColor(
|
||||
delayManager.getDelayFix(proxyToDisplay, selectedGroup),
|
||||
)}
|
||||
label={delayManager.formatDelay(currentDelay)}
|
||||
color={convertDelayColor(currentDelay)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{/* 代理组选择器 */}
|
||||
<FormControl
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{ mb: 1.5 }}
|
||||
>
|
||||
<FormControl fullWidth variant="outlined" size="small" sx={{ mb: 1.5 }}>
|
||||
<InputLabel id="proxy-group-select-label">{t("Group")}</InputLabel>
|
||||
<Select
|
||||
labelId="proxy-group-select-label"
|
||||
value={selectedGroup}
|
||||
value={state.selection.group}
|
||||
onChange={handleGroupChange}
|
||||
label={t("Group")}
|
||||
disabled={isGlobalMode || isDirectMode} // 特殊模式下禁用选择器
|
||||
disabled={isGlobalMode || isDirectMode}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
{state.proxyData.groups.map((group) => (
|
||||
<MenuItem key={group.name} value={group.name}>
|
||||
{group.name}
|
||||
</MenuItem>
|
||||
@@ -647,10 +522,10 @@ export const CurrentProxyCard = () => {
|
||||
<InputLabel id="proxy-select-label">{t("Proxy")}</InputLabel>
|
||||
<Select
|
||||
labelId="proxy-select-label"
|
||||
value={selectedProxy}
|
||||
value={state.selection.proxy}
|
||||
onChange={handleProxyChange}
|
||||
label={t("Proxy")}
|
||||
disabled={isDirectMode} // 直连模式下禁用选择器
|
||||
disabled={isDirectMode}
|
||||
renderValue={renderProxyValue}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
@@ -662,8 +537,8 @@ export const CurrentProxyCard = () => {
|
||||
>
|
||||
{proxyOptions.map((proxy) => {
|
||||
const delayValue = delayManager.getDelayFix(
|
||||
records[proxy.name],
|
||||
selectedGroup,
|
||||
state.proxyData.records[proxy.name],
|
||||
state.selection.group
|
||||
);
|
||||
return (
|
||||
<MenuItem
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
ReactElement,
|
||||
useRef,
|
||||
memo,
|
||||
} from "react";
|
||||
@@ -44,8 +43,24 @@ type TimeRange = 1 | 5 | 10; // 分钟
|
||||
type DataPoint = ITrafficItem & { name: string; timestamp: number };
|
||||
|
||||
// 控制帧率的工具函数
|
||||
const FPS_LIMIT = 30; // 限制最高30fps
|
||||
const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔
|
||||
const FPS_LIMIT = 1; // 限制为1fps,因为数据每秒才更新一次
|
||||
const FRAME_MIN_TIME = 1000 / FPS_LIMIT; // 每帧最小时间间隔,即1000ms
|
||||
|
||||
// 全局存储流量数据历史记录
|
||||
declare global {
|
||||
interface Window {
|
||||
trafficHistoryData?: DataPoint[];
|
||||
trafficHistoryStyle?: "line" | "area";
|
||||
trafficHistoryTimeRange?: TimeRange;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化全局存储
|
||||
if (typeof window !== "undefined" && !window.trafficHistoryData) {
|
||||
window.trafficHistoryData = [];
|
||||
window.trafficHistoryStyle = "area";
|
||||
window.trafficHistoryTimeRange = 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强型流量图表组件
|
||||
@@ -56,8 +71,13 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 时间范围状态(默认10分钟)
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>(10);
|
||||
// 从全局变量恢复状态
|
||||
const [timeRange, setTimeRange] = useState<TimeRange>(
|
||||
window.trafficHistoryTimeRange || 10
|
||||
);
|
||||
const [chartStyle, setChartStyle] = useState<"line" | "area">(
|
||||
window.trafficHistoryStyle || "area"
|
||||
);
|
||||
|
||||
// 使用useRef存储数据,避免不必要的重渲染
|
||||
const dataBufferRef = useRef<DataPoint[]>([]);
|
||||
@@ -84,9 +104,6 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
[getMaxPointsByTimeRange],
|
||||
);
|
||||
|
||||
// 图表样式:line 或 area
|
||||
const [chartStyle, setChartStyle] = useState<"line" | "area">("area");
|
||||
|
||||
// 颜色配置
|
||||
const colors = useMemo(
|
||||
() => ({
|
||||
@@ -103,43 +120,90 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
const handleTimeRangeClick = useCallback(() => {
|
||||
setTimeRange((prevRange) => {
|
||||
// 在1、5、10分钟之间循环切换
|
||||
if (prevRange === 1) return 5;
|
||||
if (prevRange === 5) return 10;
|
||||
return 1;
|
||||
const newRange = prevRange === 1 ? 5 : prevRange === 5 ? 10 : 1;
|
||||
window.trafficHistoryTimeRange = newRange; // 保存到全局
|
||||
return newRange;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 初始化空数据缓冲区
|
||||
// 初始化数据缓冲区
|
||||
useEffect(() => {
|
||||
// 生成10分钟的初始数据点
|
||||
const now = Date.now();
|
||||
const tenMinutesAgo = now - 10 * 60 * 1000;
|
||||
let initialBuffer: DataPoint[] = [];
|
||||
|
||||
// 如果全局有保存的数据,优先使用
|
||||
if (window.trafficHistoryData && window.trafficHistoryData.length > 0) {
|
||||
initialBuffer = [...window.trafficHistoryData];
|
||||
|
||||
// 确保数据长度符合要求
|
||||
if (initialBuffer.length > MAX_BUFFER_SIZE) {
|
||||
initialBuffer = initialBuffer.slice(-MAX_BUFFER_SIZE);
|
||||
} else if (initialBuffer.length < MAX_BUFFER_SIZE) {
|
||||
// 如果历史数据不足,则在前面补充空数据
|
||||
const now = Date.now();
|
||||
const oldestTimestamp = initialBuffer.length > 0
|
||||
? initialBuffer[0].timestamp
|
||||
: now - 10 * 60 * 1000;
|
||||
|
||||
const additionalPoints = MAX_BUFFER_SIZE - initialBuffer.length;
|
||||
const timeInterval = initialBuffer.length > 0
|
||||
? (initialBuffer[0].timestamp - (now - 10 * 60 * 1000)) / additionalPoints
|
||||
: (10 * 60 * 1000) / MAX_BUFFER_SIZE;
|
||||
|
||||
const emptyPrefix: DataPoint[] = Array.from(
|
||||
{ length: additionalPoints },
|
||||
(_, index) => {
|
||||
const pointTime = oldestTimestamp - (additionalPoints - index) * timeInterval;
|
||||
const date = new Date(pointTime);
|
||||
|
||||
return {
|
||||
up: 0,
|
||||
down: 0,
|
||||
timestamp: pointTime,
|
||||
name: date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
initialBuffer = [...emptyPrefix, ...initialBuffer];
|
||||
}
|
||||
} else {
|
||||
// 没有历史数据时,创建空的初始缓冲区
|
||||
const now = Date.now();
|
||||
const tenMinutesAgo = now - 10 * 60 * 1000;
|
||||
|
||||
// 创建初始缓冲区,降低点的密度
|
||||
const initialBuffer: DataPoint[] = Array.from(
|
||||
{ length: MAX_BUFFER_SIZE },
|
||||
(_, index) => {
|
||||
// 计算每个点的时间
|
||||
const pointTime =
|
||||
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
||||
const date = new Date(pointTime);
|
||||
initialBuffer = Array.from(
|
||||
{ length: MAX_BUFFER_SIZE },
|
||||
(_, index) => {
|
||||
const pointTime =
|
||||
tenMinutesAgo + index * ((10 * 60 * 1000) / MAX_BUFFER_SIZE);
|
||||
const date = new Date(pointTime);
|
||||
|
||||
return {
|
||||
up: 0,
|
||||
down: 0,
|
||||
timestamp: pointTime,
|
||||
name: date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
};
|
||||
},
|
||||
);
|
||||
return {
|
||||
up: 0,
|
||||
down: 0,
|
||||
timestamp: pointTime,
|
||||
name: date.toLocaleTimeString("en-US", {
|
||||
hour12: false,
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
}),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dataBufferRef.current = initialBuffer;
|
||||
setDisplayData(initialBuffer);
|
||||
window.trafficHistoryData = initialBuffer; // 保存到全局
|
||||
|
||||
// 更新显示数据
|
||||
const pointsToShow = getMaxPointsByTimeRange(timeRange);
|
||||
setDisplayData(initialBuffer.slice(-pointsToShow));
|
||||
|
||||
// 清理函数,取消任何未完成的动画帧
|
||||
return () => {
|
||||
@@ -148,7 +212,7 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
rafIdRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [MAX_BUFFER_SIZE]);
|
||||
}, [MAX_BUFFER_SIZE, getMaxPointsByTimeRange, timeRange]);
|
||||
|
||||
// 处理数据更新并控制帧率的函数
|
||||
const updateDisplayData = useCallback(() => {
|
||||
@@ -217,8 +281,12 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
timestamp: timestamp,
|
||||
};
|
||||
|
||||
// 直接更新ref,不触发重渲染
|
||||
dataBufferRef.current = [...dataBufferRef.current.slice(1), newPoint];
|
||||
// 更新ref,但保持原数组大小
|
||||
const newBuffer = [...dataBufferRef.current.slice(1), newPoint];
|
||||
dataBufferRef.current = newBuffer;
|
||||
|
||||
// 保存到全局变量
|
||||
window.trafficHistoryData = newBuffer;
|
||||
|
||||
// 使用节流更新显示数据
|
||||
throttledUpdateData();
|
||||
@@ -226,7 +294,11 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
|
||||
// 切换图表样式
|
||||
const toggleStyle = useCallback(() => {
|
||||
setChartStyle((prev) => (prev === "line" ? "area" : "line"));
|
||||
setChartStyle((prev) => {
|
||||
const newStyle = prev === "line" ? "area" : "line";
|
||||
window.trafficHistoryStyle = newStyle; // 保存到全局
|
||||
return newStyle;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 暴露方法给父组件
|
||||
@@ -264,161 +336,23 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
return t("{{time}} Minutes", { time: timeRange });
|
||||
}, [timeRange, t]);
|
||||
|
||||
// 渲染图表内的标签
|
||||
const renderInnerLabels = useCallback(() => (
|
||||
<>
|
||||
{/* 上传标签 - 右上角 */}
|
||||
<text
|
||||
x="98%"
|
||||
y="7%"
|
||||
textAnchor="end"
|
||||
fill={colors.up}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{t("Upload")}
|
||||
</text>
|
||||
|
||||
{/* 下载标签 - 右上角下方 */}
|
||||
<text
|
||||
x="98%"
|
||||
y="16%"
|
||||
textAnchor="end"
|
||||
fill={colors.down}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{t("Download")}
|
||||
</text>
|
||||
</>
|
||||
), [colors.up, colors.down, t]);
|
||||
|
||||
// 共享图表配置
|
||||
const commonProps = useMemo(() => ({
|
||||
const chartConfig = useMemo(() => ({
|
||||
data: displayData,
|
||||
margin: { top: 10, right: 20, left: 0, bottom: 0 },
|
||||
}), [displayData]);
|
||||
|
||||
// 曲线类型 - 使用平滑曲线
|
||||
const curveType = "basis";
|
||||
// 共享的线条/区域配置
|
||||
const commonLineProps = useMemo(() => ({
|
||||
dot: false,
|
||||
strokeWidth: 2,
|
||||
connectNulls: false,
|
||||
activeDot: { r: 4, strokeWidth: 1 },
|
||||
isAnimationActive: false, // 禁用动画以减少CPU使用
|
||||
}), []);
|
||||
|
||||
// 共享图表子组件
|
||||
const commonChildren = useMemo(() => (
|
||||
<>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
vertical={false}
|
||||
stroke={colors.grid}
|
||||
opacity={0.3}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 10, fill: colors.text }}
|
||||
tickLine={{ stroke: colors.grid }}
|
||||
axisLine={{ stroke: colors.grid }}
|
||||
interval="preserveStart"
|
||||
tickFormatter={formatXLabel}
|
||||
minTickGap={timeRange === 1 ? 40 : 80}
|
||||
tickCount={Math.min(6, timeRange * 2)}
|
||||
domain={["dataMin", "dataMax"]}
|
||||
scale="auto"
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={formatYAxis}
|
||||
tick={{ fontSize: 10, fill: colors.text }}
|
||||
tickLine={{ stroke: colors.grid }}
|
||||
axisLine={{ stroke: colors.grid }}
|
||||
width={40}
|
||||
domain={[0, "auto"]}
|
||||
padding={{ top: 10, bottom: 0 }}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={formatTooltip}
|
||||
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
||||
contentStyle={{
|
||||
backgroundColor: colors.tooltip,
|
||||
borderColor: colors.grid,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
itemStyle={{ color: colors.text }}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
|
||||
{/* 可点击的时间范围标签 */}
|
||||
<g
|
||||
className="time-range-selector"
|
||||
onClick={handleTimeRangeClick}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<text
|
||||
x="1%"
|
||||
y="6%"
|
||||
textAnchor="start"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{getTimeRangeText()}
|
||||
</text>
|
||||
</g>
|
||||
</>
|
||||
), [colors, formatXLabel, formatYAxis, formatTooltip, timeRange, theme.palette.text.secondary, handleTimeRangeClick, getTimeRangeText, t]);
|
||||
|
||||
// 渲染图表 - 线图或面积图
|
||||
const renderChart = useCallback(() => {
|
||||
// 共享的线条/区域配置
|
||||
const commonLineProps = {
|
||||
dot: false,
|
||||
strokeWidth: 2,
|
||||
connectNulls: false,
|
||||
activeDot: { r: 4, strokeWidth: 1 },
|
||||
isAnimationActive: false, // 禁用动画以减少CPU使用
|
||||
};
|
||||
|
||||
return chartStyle === "line" ? (
|
||||
<LineChart {...commonProps}>
|
||||
{commonChildren}
|
||||
<Line
|
||||
type="basis"
|
||||
{...commonLineProps}
|
||||
dataKey="up"
|
||||
name={t("Upload")}
|
||||
stroke={colors.up}
|
||||
/>
|
||||
<Line
|
||||
type="basis"
|
||||
{...commonLineProps}
|
||||
dataKey="down"
|
||||
name={t("Download")}
|
||||
stroke={colors.down}
|
||||
/>
|
||||
{renderInnerLabels()}
|
||||
</LineChart>
|
||||
) : (
|
||||
<AreaChart {...commonProps}>
|
||||
{commonChildren}
|
||||
<Area
|
||||
type="basis"
|
||||
{...commonLineProps}
|
||||
dataKey="up"
|
||||
name={t("Upload")}
|
||||
stroke={colors.up}
|
||||
fill={colors.up}
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
<Area
|
||||
type="basis"
|
||||
{...commonLineProps}
|
||||
dataKey="down"
|
||||
name={t("Download")}
|
||||
stroke={colors.down}
|
||||
fill={colors.down}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
{renderInnerLabels()}
|
||||
</AreaChart>
|
||||
);
|
||||
}, [chartStyle, commonProps, commonChildren, renderInnerLabels, colors, t]);
|
||||
// 曲线类型 - 使用线性曲线避免错位
|
||||
const curveType = "monotone";
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -434,7 +368,180 @@ export const EnhancedTrafficGraph = memo(forwardRef<EnhancedTrafficGraphRef>(
|
||||
onClick={toggleStyle}
|
||||
>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
{renderChart()}
|
||||
{chartStyle === "line" ? (
|
||||
<LineChart {...chartConfig}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 10, fill: colors.text }}
|
||||
tickLine={{ stroke: colors.grid }}
|
||||
axisLine={{ stroke: colors.grid }}
|
||||
interval="preserveStart"
|
||||
tickFormatter={formatXLabel}
|
||||
minTickGap={30}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={formatYAxis}
|
||||
tick={{ fontSize: 10, fill: colors.text }}
|
||||
tickLine={{ stroke: colors.grid }}
|
||||
axisLine={{ stroke: colors.grid }}
|
||||
width={40}
|
||||
domain={[0, "auto"]}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={formatTooltip}
|
||||
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
||||
contentStyle={{
|
||||
backgroundColor: colors.tooltip,
|
||||
borderColor: colors.grid,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
itemStyle={{ color: colors.text }}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Line
|
||||
type={curveType}
|
||||
{...commonLineProps}
|
||||
dataKey="up"
|
||||
name={t("Upload")}
|
||||
stroke={colors.up}
|
||||
/>
|
||||
<Line
|
||||
type={curveType}
|
||||
{...commonLineProps}
|
||||
dataKey="down"
|
||||
name={t("Download")}
|
||||
stroke={colors.down}
|
||||
/>
|
||||
|
||||
{/* 可点击的时间范围标签 */}
|
||||
<text
|
||||
x="1%"
|
||||
y="6%"
|
||||
textAnchor="start"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
onClick={handleTimeRangeClick}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{getTimeRangeText()}
|
||||
</text>
|
||||
|
||||
{/* 上传标签 - 右上角 */}
|
||||
<text
|
||||
x="98%"
|
||||
y="7%"
|
||||
textAnchor="end"
|
||||
fill={colors.up}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{t("Upload")}
|
||||
</text>
|
||||
|
||||
{/* 下载标签 - 右上角下方 */}
|
||||
<text
|
||||
x="98%"
|
||||
y="16%"
|
||||
textAnchor="end"
|
||||
fill={colors.down}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{t("Download")}
|
||||
</text>
|
||||
</LineChart>
|
||||
) : (
|
||||
<AreaChart {...chartConfig}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={colors.grid} opacity={0.3} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tick={{ fontSize: 10, fill: colors.text }}
|
||||
tickLine={{ stroke: colors.grid }}
|
||||
axisLine={{ stroke: colors.grid }}
|
||||
interval="preserveStart"
|
||||
tickFormatter={formatXLabel}
|
||||
minTickGap={30}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={formatYAxis}
|
||||
tick={{ fontSize: 10, fill: colors.text }}
|
||||
tickLine={{ stroke: colors.grid }}
|
||||
axisLine={{ stroke: colors.grid }}
|
||||
width={40}
|
||||
domain={[0, "auto"]}
|
||||
padding={{ top: 10, bottom: 0 }}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={formatTooltip}
|
||||
labelFormatter={(label) => `${t("Time")}: ${label}`}
|
||||
contentStyle={{
|
||||
backgroundColor: colors.tooltip,
|
||||
borderColor: colors.grid,
|
||||
borderRadius: 4,
|
||||
}}
|
||||
itemStyle={{ color: colors.text }}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
<Area
|
||||
type={curveType}
|
||||
{...commonLineProps}
|
||||
dataKey="up"
|
||||
name={t("Upload")}
|
||||
stroke={colors.up}
|
||||
fill={colors.up}
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
<Area
|
||||
type={curveType}
|
||||
{...commonLineProps}
|
||||
dataKey="down"
|
||||
name={t("Download")}
|
||||
stroke={colors.down}
|
||||
fill={colors.down}
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
|
||||
{/* 可点击的时间范围标签 */}
|
||||
<text
|
||||
x="1%"
|
||||
y="6%"
|
||||
textAnchor="start"
|
||||
fill={theme.palette.text.secondary}
|
||||
fontSize={11}
|
||||
fontWeight="bold"
|
||||
onClick={handleTimeRangeClick}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{getTimeRangeText()}
|
||||
</text>
|
||||
|
||||
{/* 上传标签 - 右上角 */}
|
||||
<text
|
||||
x="98%"
|
||||
y="7%"
|
||||
textAnchor="end"
|
||||
fill={colors.up}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{t("Upload")}
|
||||
</text>
|
||||
|
||||
{/* 下载标签 - 右上角下方 */}
|
||||
<text
|
||||
x="98%"
|
||||
y="16%"
|
||||
textAnchor="end"
|
||||
fill={colors.down}
|
||||
fontSize={12}
|
||||
fontWeight="bold"
|
||||
>
|
||||
{t("Download")}
|
||||
</text>
|
||||
</AreaChart>
|
||||
)}
|
||||
</ResponsiveContainer>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useCallback, memo } from "react";
|
||||
import { useState, useEffect, useRef, useCallback, memo, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Typography,
|
||||
@@ -62,6 +62,7 @@ declare global {
|
||||
|
||||
// 控制更新频率
|
||||
const CONNECTIONS_UPDATE_INTERVAL = 5000; // 5秒更新一次连接数据
|
||||
const THROTTLE_TRAFFIC_UPDATE = 500; // 500ms节流流量数据更新
|
||||
|
||||
// 统计卡片组件 - 使用memo优化
|
||||
const CompactStatCard = memo(({
|
||||
@@ -74,18 +75,18 @@ const CompactStatCard = memo(({
|
||||
}: StatCardProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
// 获取调色板颜色
|
||||
const getColorFromPalette = (colorName: string) => {
|
||||
// 获取调色板颜色 - 使用useMemo避免重复计算
|
||||
const colorValue = useMemo(() => {
|
||||
const palette = theme.palette;
|
||||
if (
|
||||
colorName in palette &&
|
||||
palette[colorName as keyof typeof palette] &&
|
||||
"main" in (palette[colorName as keyof typeof palette] as PaletteColor)
|
||||
color in palette &&
|
||||
palette[color as keyof typeof palette] &&
|
||||
"main" in (palette[color as keyof typeof palette] as PaletteColor)
|
||||
) {
|
||||
return (palette[colorName as keyof typeof palette] as PaletteColor).main;
|
||||
return (palette[color as keyof typeof palette] as PaletteColor).main;
|
||||
}
|
||||
return palette.primary.main;
|
||||
};
|
||||
}, [theme.palette, color]);
|
||||
|
||||
return (
|
||||
<Paper
|
||||
@@ -94,14 +95,14 @@ const CompactStatCard = memo(({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
borderRadius: 2,
|
||||
bgcolor: alpha(getColorFromPalette(color), 0.05),
|
||||
border: `1px solid ${alpha(getColorFromPalette(color), 0.15)}`,
|
||||
bgcolor: alpha(colorValue, 0.05),
|
||||
border: `1px solid ${alpha(colorValue, 0.15)}`,
|
||||
padding: "8px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
cursor: onClick ? "pointer" : "default",
|
||||
"&:hover": onClick ? {
|
||||
bgcolor: alpha(getColorFromPalette(color), 0.1),
|
||||
border: `1px solid ${alpha(getColorFromPalette(color), 0.3)}`,
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
border: `1px solid ${alpha(colorValue, 0.3)}`,
|
||||
boxShadow: `0 4px 8px rgba(0,0,0,0.05)`,
|
||||
} : {},
|
||||
}}
|
||||
@@ -119,8 +120,8 @@ const CompactStatCard = memo(({
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: "50%",
|
||||
bgcolor: alpha(getColorFromPalette(color), 0.1),
|
||||
color: getColorFromPalette(color),
|
||||
bgcolor: alpha(colorValue, 0.1),
|
||||
color: colorValue,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
@@ -156,24 +157,24 @@ export const EnhancedTrafficStats = () => {
|
||||
const pageVisible = useVisibility();
|
||||
const [isDebug, setIsDebug] = useState(false);
|
||||
|
||||
// 为流量数据和内存数据准备状态
|
||||
const [trafficData, setTrafficData] = useState<ITrafficItem>({
|
||||
up: 0,
|
||||
down: 0,
|
||||
});
|
||||
const [memoryData, setMemoryData] = useState<MemoryUsage>({ inuse: 0 });
|
||||
const [trafficStats, setTrafficStats] = useState<TrafficStatData>({
|
||||
uploadTotal: 0,
|
||||
downloadTotal: 0,
|
||||
activeConnections: 0,
|
||||
// 使用单一状态对象减少状态更新次数
|
||||
const [stats, setStats] = useState({
|
||||
traffic: { up: 0, down: 0 },
|
||||
memory: { inuse: 0, oslimit: undefined as number | undefined },
|
||||
connections: { uploadTotal: 0, downloadTotal: 0, activeConnections: 0 },
|
||||
});
|
||||
|
||||
// 创建一个标记来追踪最后更新时间,用于节流
|
||||
const lastUpdateRef = useRef({ traffic: 0 });
|
||||
|
||||
// 是否显示流量图表
|
||||
const trafficGraph = verge?.traffic_graph ?? true;
|
||||
|
||||
// WebSocket引用
|
||||
const trafficSocketRef = useRef<ReturnType<typeof createAuthSockette> | null>(null);
|
||||
const memorySocketRef = useRef<ReturnType<typeof createAuthSockette> | null>(null);
|
||||
const socketRefs = useRef({
|
||||
traffic: null as ReturnType<typeof createAuthSockette> | null,
|
||||
memory: null as ReturnType<typeof createAuthSockette> | null,
|
||||
});
|
||||
|
||||
// 获取连接数据
|
||||
const fetchConnections = useCallback(async () => {
|
||||
@@ -191,11 +192,14 @@ export const EnhancedTrafficStats = () => {
|
||||
0,
|
||||
);
|
||||
|
||||
setTrafficStats({
|
||||
uploadTotal,
|
||||
downloadTotal,
|
||||
activeConnections: connections.connections.length,
|
||||
});
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
connections: {
|
||||
uploadTotal,
|
||||
downloadTotal,
|
||||
activeConnections: connections.connections.length,
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch connections:", err);
|
||||
@@ -204,11 +208,11 @@ export const EnhancedTrafficStats = () => {
|
||||
|
||||
// 定期更新连接数据
|
||||
useEffect(() => {
|
||||
if (pageVisible) {
|
||||
fetchConnections();
|
||||
const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL);
|
||||
return () => clearInterval(intervalId);
|
||||
}
|
||||
if (!pageVisible) return;
|
||||
|
||||
fetchConnections();
|
||||
const intervalId = setInterval(fetchConnections, CONNECTIONS_UPDATE_INTERVAL);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [pageVisible, fetchConnections]);
|
||||
|
||||
// 检查是否支持调试
|
||||
@@ -216,7 +220,7 @@ export const EnhancedTrafficStats = () => {
|
||||
isDebugEnabled().then((flag) => setIsDebug(flag));
|
||||
}, []);
|
||||
|
||||
// 处理流量数据更新
|
||||
// 处理流量数据更新 - 使用节流控制更新频率
|
||||
const handleTrafficUpdate = useCallback((event: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as ITrafficItem;
|
||||
@@ -225,28 +229,40 @@ export const EnhancedTrafficStats = () => {
|
||||
typeof data.up === "number" &&
|
||||
typeof data.down === "number"
|
||||
) {
|
||||
// 使用节流控制更新频率
|
||||
const now = Date.now();
|
||||
if (now - lastUpdateRef.current.traffic < THROTTLE_TRAFFIC_UPDATE) {
|
||||
// 如果距离上次更新时间小于阈值,只更新图表不更新状态
|
||||
if (trafficRef.current) {
|
||||
trafficRef.current.appendData({
|
||||
up: data.up,
|
||||
down: data.down,
|
||||
timestamp: now,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新最后更新时间
|
||||
lastUpdateRef.current.traffic = now;
|
||||
|
||||
// 验证数据有效性,防止NaN
|
||||
const safeUp = isNaN(data.up) ? 0 : data.up;
|
||||
const safeDown = isNaN(data.down) ? 0 : data.down;
|
||||
|
||||
setTrafficData({
|
||||
up: safeUp,
|
||||
down: safeDown,
|
||||
});
|
||||
// 批量更新状态
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
traffic: { up: safeUp, down: safeDown }
|
||||
}));
|
||||
|
||||
// 更新图表数据
|
||||
if (trafficRef.current) {
|
||||
trafficRef.current.appendData({
|
||||
up: safeUp,
|
||||
down: safeDown,
|
||||
timestamp: Date.now(),
|
||||
timestamp: now,
|
||||
});
|
||||
|
||||
// 清除之前可能存在的动画帧
|
||||
if (window.animationFrameId) {
|
||||
cancelAnimationFrame(window.animationFrameId);
|
||||
window.animationFrameId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -259,74 +275,50 @@ export const EnhancedTrafficStats = () => {
|
||||
try {
|
||||
const data = JSON.parse(event.data) as MemoryUsage;
|
||||
if (data && typeof data.inuse === "number") {
|
||||
setMemoryData({
|
||||
inuse: isNaN(data.inuse) ? 0 : data.inuse,
|
||||
oslimit: data.oslimit,
|
||||
});
|
||||
setStats(prev => ({
|
||||
...prev,
|
||||
memory: {
|
||||
inuse: isNaN(data.inuse) ? 0 : data.inuse,
|
||||
oslimit: data.oslimit,
|
||||
}
|
||||
}));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Memory] 解析数据错误:", err);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 使用 WebSocket 连接获取流量数据
|
||||
// 使用 WebSocket 连接获取数据 - 合并流量和内存连接逻辑
|
||||
useEffect(() => {
|
||||
if (!clashInfo || !pageVisible) return;
|
||||
|
||||
const { server, secret = "" } = clashInfo;
|
||||
if (!server) return;
|
||||
|
||||
// 清理现有连接的函数
|
||||
const cleanupSockets = () => {
|
||||
Object.values(socketRefs.current).forEach(socket => {
|
||||
if (socket) {
|
||||
socket.close();
|
||||
}
|
||||
});
|
||||
socketRefs.current = { traffic: null, memory: null };
|
||||
};
|
||||
|
||||
// 关闭现有连接
|
||||
if (trafficSocketRef.current) {
|
||||
trafficSocketRef.current.close();
|
||||
}
|
||||
cleanupSockets();
|
||||
|
||||
// 创建新连接
|
||||
trafficSocketRef.current = createAuthSockette(`${server}/traffic`, secret, {
|
||||
socketRefs.current.traffic = createAuthSockette(`${server}/traffic`, secret, {
|
||||
onmessage: handleTrafficUpdate,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (trafficSocketRef.current) {
|
||||
trafficSocketRef.current.close();
|
||||
trafficSocketRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [clashInfo, pageVisible, handleTrafficUpdate]);
|
||||
|
||||
// 使用 WebSocket 连接获取内存数据
|
||||
useEffect(() => {
|
||||
if (!clashInfo || !pageVisible) return;
|
||||
|
||||
const { server, secret = "" } = clashInfo;
|
||||
if (!server) return;
|
||||
|
||||
// 关闭现有连接
|
||||
if (memorySocketRef.current) {
|
||||
memorySocketRef.current.close();
|
||||
}
|
||||
|
||||
// 创建新连接
|
||||
memorySocketRef.current = createAuthSockette(`${server}/memory`, secret, {
|
||||
|
||||
socketRefs.current.memory = createAuthSockette(`${server}/memory`, secret, {
|
||||
onmessage: handleMemoryUpdate,
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (memorySocketRef.current) {
|
||||
memorySocketRef.current.close();
|
||||
memorySocketRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [clashInfo, pageVisible, handleMemoryUpdate]);
|
||||
|
||||
// 解析流量数据
|
||||
const [up, upUnit] = parseTraffic(trafficData.up);
|
||||
const [down, downUnit] = parseTraffic(trafficData.down);
|
||||
const [inuse, inuseUnit] = parseTraffic(memoryData.inuse);
|
||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(trafficStats.uploadTotal);
|
||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(
|
||||
trafficStats.downloadTotal,
|
||||
);
|
||||
return cleanupSockets;
|
||||
}, [clashInfo, pageVisible, handleTrafficUpdate, handleMemoryUpdate]);
|
||||
|
||||
// 执行垃圾回收
|
||||
const handleGarbageCollection = useCallback(async () => {
|
||||
@@ -340,8 +332,22 @@ export const EnhancedTrafficStats = () => {
|
||||
}
|
||||
}, [isDebug]);
|
||||
|
||||
// 渲染流量图表
|
||||
const renderTrafficGraph = useCallback(() => {
|
||||
// 使用useMemo计算解析后的流量数据
|
||||
const parsedData = useMemo(() => {
|
||||
const [up, upUnit] = parseTraffic(stats.traffic.up);
|
||||
const [down, downUnit] = parseTraffic(stats.traffic.down);
|
||||
const [inuse, inuseUnit] = parseTraffic(stats.memory.inuse);
|
||||
const [uploadTotal, uploadTotalUnit] = parseTraffic(stats.connections.uploadTotal);
|
||||
const [downloadTotal, downloadTotalUnit] = parseTraffic(stats.connections.downloadTotal);
|
||||
|
||||
return {
|
||||
up, upUnit, down, downUnit, inuse, inuseUnit,
|
||||
uploadTotal, uploadTotalUnit, downloadTotal, downloadTotalUnit
|
||||
};
|
||||
}, [stats]);
|
||||
|
||||
// 渲染流量图表 - 使用useMemo缓存渲染结果
|
||||
const trafficGraphComponent = useMemo(() => {
|
||||
if (!trafficGraph || !pageVisible) return null;
|
||||
|
||||
return (
|
||||
@@ -382,58 +388,58 @@ export const EnhancedTrafficStats = () => {
|
||||
);
|
||||
}, [trafficGraph, pageVisible, theme.palette.divider, isDebug]);
|
||||
|
||||
// 统计卡片配置
|
||||
const statCards = [
|
||||
// 使用useMemo计算统计卡片配置
|
||||
const statCards = useMemo(() => [
|
||||
{
|
||||
icon: <ArrowUpwardRounded fontSize="small" />,
|
||||
title: t("Upload Speed"),
|
||||
value: up,
|
||||
unit: `${upUnit}/s`,
|
||||
value: parsedData.up,
|
||||
unit: `${parsedData.upUnit}/s`,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <ArrowDownwardRounded fontSize="small" />,
|
||||
title: t("Download Speed"),
|
||||
value: down,
|
||||
unit: `${downUnit}/s`,
|
||||
value: parsedData.down,
|
||||
unit: `${parsedData.downUnit}/s`,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <LinkRounded fontSize="small" />,
|
||||
title: t("Active Connections"),
|
||||
value: trafficStats.activeConnections,
|
||||
value: stats.connections.activeConnections,
|
||||
unit: "",
|
||||
color: "success" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudUploadRounded fontSize="small" />,
|
||||
title: t("Uploaded"),
|
||||
value: uploadTotal,
|
||||
unit: uploadTotalUnit,
|
||||
value: parsedData.uploadTotal,
|
||||
unit: parsedData.uploadTotalUnit,
|
||||
color: "secondary" as const,
|
||||
},
|
||||
{
|
||||
icon: <CloudDownloadRounded fontSize="small" />,
|
||||
title: t("Downloaded"),
|
||||
value: downloadTotal,
|
||||
unit: downloadTotalUnit,
|
||||
value: parsedData.downloadTotal,
|
||||
unit: parsedData.downloadTotalUnit,
|
||||
color: "primary" as const,
|
||||
},
|
||||
{
|
||||
icon: <MemoryRounded fontSize="small" />,
|
||||
title: t("Memory Usage"),
|
||||
value: inuse,
|
||||
unit: inuseUnit,
|
||||
value: parsedData.inuse,
|
||||
unit: parsedData.inuseUnit,
|
||||
color: "error" as const,
|
||||
onClick: isDebug ? handleGarbageCollection : undefined,
|
||||
},
|
||||
];
|
||||
], [t, parsedData, stats.connections.activeConnections, isDebug, handleGarbageCollection]);
|
||||
|
||||
return (
|
||||
<Grid container spacing={1} columns={{ xs: 8, sm: 8, md: 12 }}>
|
||||
<Grid size={12}>
|
||||
{/* 流量图表区域 */}
|
||||
{renderTrafficGraph()}
|
||||
{trafficGraphComponent}
|
||||
</Grid>
|
||||
{/* 统计卡片区域 */}
|
||||
{statCards.map((card, index) => (
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import dayjs from "dayjs";
|
||||
import parseTraffic from "@/utils/parse-traffic";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useCallback, useState } from "react";
|
||||
import { openWebUrl, updateProfile } from "@/services/cmds";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Notice } from "@/components/base";
|
||||
@@ -35,16 +35,25 @@ const round = keyframes`
|
||||
`;
|
||||
|
||||
// 辅助函数解析URL和过期时间
|
||||
function parseUrl(url?: string) {
|
||||
const parseUrl = (url?: string, maxLength: number = 25) => {
|
||||
if (!url) return "-";
|
||||
if (url.startsWith("http")) return new URL(url).host;
|
||||
return "local";
|
||||
}
|
||||
let parsedUrl = "";
|
||||
if (url.startsWith("http")) {
|
||||
parsedUrl = new URL(url).host;
|
||||
} else {
|
||||
parsedUrl = "local";
|
||||
}
|
||||
|
||||
if (parsedUrl.length > maxLength) {
|
||||
return parsedUrl.substring(0, maxLength - 3) + "...";
|
||||
}
|
||||
return parsedUrl;
|
||||
};
|
||||
|
||||
function parseExpire(expire?: number) {
|
||||
const parseExpire = (expire?: number) => {
|
||||
if (!expire) return "-";
|
||||
return dayjs(expire * 1000).format("YYYY-MM-DD");
|
||||
}
|
||||
};
|
||||
|
||||
// 使用类型定义,而不是导入
|
||||
interface ProfileExtra {
|
||||
@@ -64,20 +73,184 @@ export interface ProfileItem {
|
||||
updated?: number;
|
||||
extra?: ProfileExtra;
|
||||
home?: string;
|
||||
option?: any; // 添加option以兼容原始类型
|
||||
option?: any;
|
||||
}
|
||||
|
||||
export interface HomeProfileCardProps {
|
||||
current: ProfileItem | null | undefined;
|
||||
onProfileUpdated?: () => void;
|
||||
}
|
||||
|
||||
export const HomeProfileCard = ({ current }: HomeProfileCardProps) => {
|
||||
// 提取独立组件减少主组件复杂度
|
||||
const ProfileDetails = ({ current, onUpdateProfile, updating }: {
|
||||
current: ProfileItem;
|
||||
onUpdateProfile: () => void;
|
||||
updating: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const usedTraffic = useMemo(() => {
|
||||
if (!current.extra) return 0;
|
||||
return current.extra.upload + current.extra.download;
|
||||
}, [current.extra]);
|
||||
|
||||
const trafficPercentage = useMemo(() => {
|
||||
if (!current.extra || !current.extra.total) return 1;
|
||||
return Math.min(
|
||||
Math.round((usedTraffic * 100) / (current.extra.total + 0.01)) + 1,
|
||||
100
|
||||
);
|
||||
}, [current.extra, usedTraffic]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={2}>
|
||||
{current.url && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<DnsOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("From")}:{" "}
|
||||
{current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
fontWeight="medium"
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center"
|
||||
}}
|
||||
>
|
||||
{parseUrl(current.url)}
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7, flexShrink: 0 }}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Box
|
||||
component="span"
|
||||
fontWeight="medium"
|
||||
>
|
||||
{parseUrl(current.url)}
|
||||
</Box>
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{current.updated && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<UpdateOutlined
|
||||
fontSize="small"
|
||||
color="action"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
animation: updating ? `${round} 1.5s linear infinite` : "none",
|
||||
}}
|
||||
onClick={onUpdateProfile}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={onUpdateProfile}
|
||||
>
|
||||
{t("Update Time")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{current.extra && (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<SpeedOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Used / Total")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseTraffic(usedTraffic)} / {parseTraffic(current.extra.total)}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{current.extra.expire > 0 && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<EventOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Expire Time")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseExpire(current.extra.expire)}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 0.5, display: "block" }}
|
||||
>
|
||||
{trafficPercentage}%
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={trafficPercentage}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.12),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
// 提取空配置组件
|
||||
const EmptyProfile = ({ onClick }: { onClick: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 2.4,
|
||||
cursor: "pointer",
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
borderRadius: 2,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<CloudUploadOutlined
|
||||
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
||||
/>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("Import")} {t("Profiles")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Click to import subscription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const HomeProfileCard = ({ current, onProfileUpdated }: HomeProfileCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 更新当前订阅
|
||||
const [updating, setUpdating] = useState(false);
|
||||
|
||||
const onUpdateProfile = useLockFn(async () => {
|
||||
if (!current?.uid) return;
|
||||
|
||||
@@ -85,6 +258,7 @@ export const HomeProfileCard = ({ current }: HomeProfileCardProps) => {
|
||||
try {
|
||||
await updateProfile(current.uid);
|
||||
Notice.success(t("Update subscription successfully"));
|
||||
onProfileUpdated?.();
|
||||
} catch (err: any) {
|
||||
Notice.error(err?.message || err.toString());
|
||||
} finally {
|
||||
@@ -93,204 +267,71 @@ export const HomeProfileCard = ({ current }: HomeProfileCardProps) => {
|
||||
});
|
||||
|
||||
// 导航到订阅页面
|
||||
const goToProfiles = () => {
|
||||
const goToProfiles = useCallback(() => {
|
||||
navigate("/profile");
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
// 卡片标题
|
||||
const cardTitle = useMemo(() => {
|
||||
if (!current) return t("Profiles");
|
||||
|
||||
if (!current.home) return current.name;
|
||||
|
||||
return (
|
||||
<Link
|
||||
component="button"
|
||||
variant="h6"
|
||||
fontWeight="medium"
|
||||
fontSize={18}
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
{current.name}
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}, [current, t]);
|
||||
|
||||
// 卡片操作按钮
|
||||
const cardAction = useMemo(() => {
|
||||
if (!current) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={goToProfiles}
|
||||
endIcon={<StorageOutlined fontSize="small" />}
|
||||
sx={{ borderRadius: 1.5 }}
|
||||
>
|
||||
{t("Label-Profiles")}
|
||||
</Button>
|
||||
);
|
||||
}, [current, goToProfiles, t]);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={
|
||||
current ? (
|
||||
current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
variant="h6"
|
||||
fontWeight="medium"
|
||||
fontSize={18}
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
color: "inherit",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
>
|
||||
{current.name}
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
current.name
|
||||
)
|
||||
) : (
|
||||
t("Profiles")
|
||||
)
|
||||
}
|
||||
title={cardTitle}
|
||||
icon={<CloudUploadOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
current && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={goToProfiles}
|
||||
endIcon={<StorageOutlined fontSize="small" />}
|
||||
sx={{ borderRadius: 1.5 }}
|
||||
>
|
||||
{t("Label-Profiles")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
action={cardAction}
|
||||
>
|
||||
{current ? (
|
||||
// 已导入订阅,显示详情
|
||||
<Box>
|
||||
<Stack spacing={2}>
|
||||
{current.url && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<DnsOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("From")}:{" "}
|
||||
{current.home ? (
|
||||
<Link
|
||||
component="button"
|
||||
fontWeight="medium"
|
||||
onClick={() => current.home && openWebUrl(current.home)}
|
||||
sx={{ display: "inline-flex", alignItems: "center" }}
|
||||
>
|
||||
{parseUrl(current.url)}
|
||||
<LaunchOutlined
|
||||
fontSize="inherit"
|
||||
sx={{ ml: 0.5, fontSize: "0.8rem", opacity: 0.7 }}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseUrl(current.url)}
|
||||
</Box>
|
||||
)}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{current.updated && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<UpdateOutlined
|
||||
fontSize="small"
|
||||
color="action"
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
animation: updating
|
||||
? `${round} 1.5s linear infinite`
|
||||
: "none",
|
||||
}}
|
||||
onClick={onUpdateProfile}
|
||||
/>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ cursor: "pointer" }}
|
||||
onClick={onUpdateProfile}
|
||||
>
|
||||
{t("Update Time")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{dayjs(current.updated * 1000).format("YYYY-MM-DD HH:mm")}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{current.extra && (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<SpeedOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Used / Total")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseTraffic(
|
||||
current.extra.upload + current.extra.download,
|
||||
)}{" "}
|
||||
/ {parseTraffic(current.extra.total)}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{current.extra.expire > 0 && (
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<EventOutlined fontSize="small" color="action" />
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Expire Time")}:{" "}
|
||||
<Box component="span" fontWeight="medium">
|
||||
{parseExpire(current.extra.expire)}
|
||||
</Box>
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
sx={{ mb: 0.5, display: "block" }}
|
||||
>
|
||||
{Math.min(
|
||||
Math.round(
|
||||
((current.extra.download + current.extra.upload) *
|
||||
100) /
|
||||
(current.extra.total + 0.01),
|
||||
) + 1,
|
||||
100,
|
||||
)}
|
||||
%
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={Math.min(
|
||||
Math.round(
|
||||
((current.extra.download + current.extra.upload) *
|
||||
100) /
|
||||
(current.extra.total + 0.01),
|
||||
) + 1,
|
||||
100,
|
||||
)}
|
||||
sx={{
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.12),
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
<ProfileDetails
|
||||
current={current}
|
||||
onUpdateProfile={onUpdateProfile}
|
||||
updating={updating}
|
||||
/>
|
||||
) : (
|
||||
// 未导入订阅,显示导入按钮
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
py: 2.4,
|
||||
cursor: "pointer",
|
||||
"&:hover": { bgcolor: "action.hover" },
|
||||
borderRadius: 2,
|
||||
}}
|
||||
onClick={goToProfiles}
|
||||
>
|
||||
<CloudUploadOutlined
|
||||
sx={{ fontSize: 60, color: "primary.main", mb: 2 }}
|
||||
/>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
{t("Import")} {t("Profiles")}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Click to import subscription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<EmptyProfile onClick={goToProfiles} />
|
||||
)}
|
||||
</EnhancedCard>
|
||||
);
|
||||
|
||||
@@ -15,15 +15,50 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import { EnhancedCard } from "./enhanced-card";
|
||||
import { getIpInfo } from "@/services/api";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, memo } from "react";
|
||||
|
||||
// 定义刷新时间(秒)
|
||||
const IP_REFRESH_SECONDS = 300;
|
||||
|
||||
// 提取InfoItem子组件并使用memo优化
|
||||
const InfoItem = memo(({ label, value }: { label: string; value: string }) => (
|
||||
<Box sx={{ mb: 0.7, display: "flex", alignItems: "flex-start" }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ minwidth: 60, mr: 0.5, flexShrink: 0, textAlign: "right" }}
|
||||
>
|
||||
{label}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1,
|
||||
}}
|
||||
>
|
||||
{value || "Unknown"}
|
||||
</Typography>
|
||||
</Box>
|
||||
));
|
||||
|
||||
// 获取国旗表情
|
||||
const getCountryFlag = (countryCode: string) => {
|
||||
if (!countryCode) return "";
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split("")
|
||||
.map((char) => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
|
||||
// IP信息卡片组件
|
||||
export const IpInfoCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [ipInfo, setIpInfo] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
@@ -48,250 +83,241 @@ export const IpInfoCard = () => {
|
||||
// 组件加载时获取IP信息
|
||||
useEffect(() => {
|
||||
fetchIpInfo();
|
||||
}, [fetchIpInfo]);
|
||||
|
||||
// 倒计时自动刷新
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setCountdown((prev) => {
|
||||
if (prev <= 1) {
|
||||
|
||||
// 倒计时实现优化,减少不必要的重渲染
|
||||
let timer: number | null = null;
|
||||
let currentCount = IP_REFRESH_SECONDS;
|
||||
|
||||
// 只在必要时更新状态,减少重渲染次数
|
||||
const startCountdown = () => {
|
||||
timer = window.setInterval(() => {
|
||||
currentCount -= 1;
|
||||
|
||||
if (currentCount <= 0) {
|
||||
fetchIpInfo();
|
||||
return IP_REFRESH_SECONDS;
|
||||
currentCount = IP_REFRESH_SECONDS;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
|
||||
// 每5秒或倒计时结束时才更新UI
|
||||
if (currentCount % 5 === 0 || currentCount <= 0) {
|
||||
setCountdown(currentCount);
|
||||
}
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
startCountdown();
|
||||
return () => {
|
||||
if (timer) clearInterval(timer);
|
||||
};
|
||||
}, [fetchIpInfo]);
|
||||
|
||||
// 刷新按钮点击处理
|
||||
const handleRefresh = () => {
|
||||
fetchIpInfo();
|
||||
};
|
||||
const toggleShowIp = useCallback(() => {
|
||||
setShowIp(prev => !prev);
|
||||
}, []);
|
||||
|
||||
// 切换显示/隐藏IP
|
||||
const toggleShowIp = () => {
|
||||
setShowIp(!showIp);
|
||||
};
|
||||
|
||||
// 获取国旗表情
|
||||
const getCountryFlag = (countryCode: string) => {
|
||||
if (!countryCode) return "";
|
||||
const codePoints = countryCode
|
||||
.toUpperCase()
|
||||
.split("")
|
||||
.map((char) => 127397 + char.charCodeAt(0));
|
||||
return String.fromCodePoint(...codePoints);
|
||||
};
|
||||
|
||||
// 信息项组件 - 默认不换行,但在需要时可以换行
|
||||
const InfoItem = ({ label, value }: { label: string; value: string }) => (
|
||||
<Box sx={{ mb: 0.7, display: "flex", alignItems: "flex-start" }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
minwidth: 60,
|
||||
mr: 0.5,
|
||||
flexShrink: 0,
|
||||
textAlign: "right",
|
||||
}}
|
||||
// 渲染加载状态
|
||||
if (loading) {
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("IP Information")}
|
||||
icon={<LocationOnOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
<IconButton size="small" onClick={fetchIpInfo} disabled={true}>
|
||||
<RefreshOutlined />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{label}:
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
ml: 0.5,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "normal",
|
||||
flexGrow: 1, // 让内容占用剩余空间
|
||||
}}
|
||||
>
|
||||
{value || t("Unknown")}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<Skeleton variant="text" width="60%" height={32} />
|
||||
<Skeleton variant="text" width="80%" height={24} />
|
||||
<Skeleton variant="text" width="70%" height={24} />
|
||||
<Skeleton variant="text" width="50%" height={24} />
|
||||
</Box>
|
||||
</EnhancedCard>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染错误状态
|
||||
if (error) {
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("IP Information")}
|
||||
icon={<LocationOnOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
<IconButton size="small" onClick={fetchIpInfo}>
|
||||
<RefreshOutlined />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "error.main",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="error">
|
||||
{error}
|
||||
</Typography>
|
||||
<Button onClick={fetchIpInfo} sx={{ mt: 2 }}>
|
||||
{t("Retry")}
|
||||
</Button>
|
||||
</Box>
|
||||
</EnhancedCard>
|
||||
);
|
||||
}
|
||||
|
||||
// 渲染正常数据
|
||||
return (
|
||||
<EnhancedCard
|
||||
title={t("IP Information")}
|
||||
icon={<LocationOnOutlined />}
|
||||
iconColor="info"
|
||||
action={
|
||||
<IconButton size="small" onClick={handleRefresh} disabled={loading}>
|
||||
<IconButton size="small" onClick={fetchIpInfo}>
|
||||
<RefreshOutlined />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<Box sx={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<Skeleton variant="text" width="60%" height={34} />
|
||||
<Skeleton variant="text" width="80%" height={24} />
|
||||
<Skeleton variant="text" width="70%" height={24} />
|
||||
<Skeleton variant="text" width="50%" height={24} />
|
||||
</Box>
|
||||
) : error ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
color: "error.main",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="error">
|
||||
{error}
|
||||
</Typography>
|
||||
<Button onClick={handleRefresh} sx={{ mt: 2 }}>
|
||||
{t("Retry")}
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 左侧:国家和IP地址 */}
|
||||
<Box sx={{ width: "40%", overflow: "hidden" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
mb: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* 左侧:国家和IP地址 */}
|
||||
<Box sx={{ width: "40%", overflow: "hidden" }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
mb: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
fontSize: "1.5rem",
|
||||
mr: 1,
|
||||
display: "inline-block",
|
||||
width: 28,
|
||||
textAlign: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{getCountryFlag(ipInfo?.country_code)}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: "medium",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{ipInfo?.country || t("Unknown")}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{t("IP")}:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
ml: 1,
|
||||
overflow: "hidden",
|
||||
maxWidth: "calc(100% - 30px)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.75rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{showIp ? ipInfo?.ip : "••••••••••"}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={toggleShowIp}>
|
||||
{showIp ? (
|
||||
<VisibilityOffOutlined fontSize="small" />
|
||||
) : (
|
||||
<VisibilityOutlined fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<InfoItem
|
||||
label={t("ASN")}
|
||||
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:组织、ISP和位置信息 */}
|
||||
<Box sx={{ width: "60%", overflow: "auto" }}>
|
||||
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
|
||||
|
||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
||||
|
||||
<InfoItem
|
||||
label={t("Location")}
|
||||
value={[ipInfo?.city, ipInfo?.region]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
/>
|
||||
|
||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: "auto",
|
||||
pt: 0.5,
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
opacity: 0.7,
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{t("Auto refresh")}: {countdown}s
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
fontSize: "1.5rem",
|
||||
mr: 1,
|
||||
display: "inline-block",
|
||||
width: 28,
|
||||
textAlign: "center",
|
||||
flexShrink: 0,
|
||||
fontFamily: '"twemoji mozilla", sans-serif',
|
||||
}}
|
||||
>
|
||||
{ipInfo?.country_code}, {ipInfo?.longitude?.toFixed(2)},{" "}
|
||||
{ipInfo?.latitude?.toFixed(2)}
|
||||
{getCountryFlag(ipInfo?.country_code)}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: "medium",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
maxWidth: "100%",
|
||||
}}
|
||||
>
|
||||
{ipInfo?.country || t("Unknown")}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ flexShrink: 0 }}
|
||||
>
|
||||
{t("IP")}:
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
ml: 1,
|
||||
overflow: "hidden",
|
||||
maxWidth: "calc(100% - 30px)",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.75rem",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
wordBreak: "break-all",
|
||||
}}
|
||||
>
|
||||
{showIp ? ipInfo?.ip : "••••••••••"}
|
||||
</Typography>
|
||||
<IconButton size="small" onClick={toggleShowIp}>
|
||||
{showIp ? (
|
||||
<VisibilityOffOutlined fontSize="small" />
|
||||
) : (
|
||||
<VisibilityOutlined fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<InfoItem
|
||||
label={t("ASN")}
|
||||
value={ipInfo?.asn ? `AS${ipInfo.asn}` : "N/A"}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 右侧:组织、ISP和位置信息 */}
|
||||
<Box sx={{ width: "60%", overflow: "auto" }}>
|
||||
<InfoItem label={t("ISP")} value={ipInfo?.isp} />
|
||||
<InfoItem label={t("ORG")} value={ipInfo?.asn_organization} />
|
||||
<InfoItem
|
||||
label={t("Location")}
|
||||
value={[ipInfo?.city, ipInfo?.region]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
/>
|
||||
<InfoItem label={t("Timezone")} value={ipInfo?.timezone} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
mt: "auto",
|
||||
pt: 0.5,
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
opacity: 0.7,
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption">
|
||||
{t("Auto refresh")}: {countdown}s
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{ipInfo?.country_code}, {ipInfo?.longitude?.toFixed(2)},{" "}
|
||||
{ipInfo?.latitude?.toFixed(2)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</EnhancedCard>
|
||||
);
|
||||
|
||||
@@ -7,17 +7,16 @@ import {
|
||||
Tooltip,
|
||||
alpha,
|
||||
useTheme,
|
||||
Button,
|
||||
Fade,
|
||||
} from "@mui/material";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useMemo, memo, FC } from "react";
|
||||
import ProxyControlSwitches from "@/components/shared/ProxyControlSwitches";
|
||||
import { Notice } from "@/components/base";
|
||||
import {
|
||||
LanguageRounded,
|
||||
ComputerRounded,
|
||||
TroubleshootRounded,
|
||||
HelpOutlineRounded,
|
||||
SvgIconComponent,
|
||||
} from "@mui/icons-material";
|
||||
import useSWR from "swr";
|
||||
import {
|
||||
@@ -26,15 +25,131 @@ import {
|
||||
getRunningMode,
|
||||
} from "@/services/cmds";
|
||||
|
||||
export const ProxyTunCard = () => {
|
||||
const LOCAL_STORAGE_TAB_KEY = "clash-verge-proxy-active-tab";
|
||||
|
||||
interface TabButtonProps {
|
||||
isActive: boolean;
|
||||
onClick: () => void;
|
||||
icon: SvgIconComponent;
|
||||
label: string;
|
||||
hasIndicator?: boolean;
|
||||
}
|
||||
|
||||
// 抽取Tab组件以减少重复代码
|
||||
const TabButton: FC<TabButtonProps> = memo(({
|
||||
isActive,
|
||||
onClick,
|
||||
icon: Icon,
|
||||
label,
|
||||
hasIndicator = false
|
||||
}) => (
|
||||
<Paper
|
||||
elevation={isActive ? 2 : 0}
|
||||
onClick={onClick}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor: isActive ? "primary.main" : "background.paper",
|
||||
color: isActive ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
flex: 1,
|
||||
maxWidth: 160,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: 1,
|
||||
},
|
||||
"&:after": isActive
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -9,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 9,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: isActive ? 600 : 400 }}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
{hasIndicator && (
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
bgcolor: isActive ? "#fff" : "success.main",
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
));
|
||||
|
||||
interface TabDescriptionProps {
|
||||
description: string;
|
||||
tooltipTitle: string;
|
||||
}
|
||||
|
||||
// 抽取描述文本组件
|
||||
const TabDescription: FC<TabDescriptionProps> = memo(({ description, tooltipTitle }) => (
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 0.5,
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
<Tooltip title={tooltipTitle}>
|
||||
<HelpOutlineRounded
|
||||
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
</Fade>
|
||||
));
|
||||
|
||||
export const ProxyTunCard: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<string>("system");
|
||||
const [activeTab, setActiveTab] = useState<string>(() =>
|
||||
localStorage.getItem(LOCAL_STORAGE_TAB_KEY) || "system"
|
||||
);
|
||||
|
||||
// 获取代理状态信息
|
||||
const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy);
|
||||
const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy);
|
||||
const { data: runningMode } = useSWR("getRunningMode", getRunningMode);
|
||||
|
||||
// 是否以sidecar模式运行
|
||||
@@ -42,26 +157,34 @@ export const ProxyTunCard = () => {
|
||||
|
||||
// 处理错误
|
||||
const handleError = (err: Error) => {
|
||||
setError(err.message);
|
||||
Notice.error(err.message || err.toString(), 3000);
|
||||
};
|
||||
|
||||
// 用户提示文本
|
||||
const getTabDescription = (tab: string) => {
|
||||
switch (tab) {
|
||||
case "system":
|
||||
return sysproxy?.enable
|
||||
? t("System Proxy Enabled")
|
||||
: t("System Proxy Disabled");
|
||||
case "tun":
|
||||
return isSidecarMode
|
||||
? t("TUN Mode Service Required")
|
||||
: t("TUN Mode Intercept Info");
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
// 处理标签切换并保存到localStorage
|
||||
const handleTabChange = (tab: string) => {
|
||||
setActiveTab(tab);
|
||||
localStorage.setItem(LOCAL_STORAGE_TAB_KEY, tab);
|
||||
};
|
||||
|
||||
// 用户提示文本 - 使用useMemo避免重复计算
|
||||
const tabDescription = useMemo(() => {
|
||||
if (activeTab === "system") {
|
||||
return {
|
||||
text: sysproxy?.enable
|
||||
? t("System Proxy Enabled")
|
||||
: t("System Proxy Disabled"),
|
||||
tooltip: t("System Proxy Info")
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
text: isSidecarMode
|
||||
? t("TUN Mode Service Required")
|
||||
: t("TUN Mode Intercept Info"),
|
||||
tooltip: t("Tun Mode Info")
|
||||
};
|
||||
}
|
||||
}, [activeTab, sysproxy?.enable, isSidecarMode, t]);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", width: "100%" }}>
|
||||
{/* 选项卡 */}
|
||||
@@ -75,112 +198,19 @@ export const ProxyTunCard = () => {
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
elevation={activeTab === "system" ? 2 : 0}
|
||||
onClick={() => setActiveTab("system")}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor:
|
||||
activeTab === "system" ? "primary.main" : "background.paper",
|
||||
color:
|
||||
activeTab === "system" ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
flex: 1,
|
||||
maxWidth: 160,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: 1,
|
||||
},
|
||||
"&:after":
|
||||
activeTab === "system"
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -9,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 9,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
<ComputerRounded fontSize="small" />
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: activeTab === "system" ? 600 : 400 }}
|
||||
>
|
||||
{t("System Proxy")}
|
||||
</Typography>
|
||||
{sysproxy?.enable && (
|
||||
<Box
|
||||
sx={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "50%",
|
||||
bgcolor: activeTab === "system" ? "#fff" : "success.main",
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
<Paper
|
||||
elevation={activeTab === "tun" ? 2 : 0}
|
||||
onClick={() => setActiveTab("tun")}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
px: 2,
|
||||
py: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 1,
|
||||
bgcolor: activeTab === "tun" ? "primary.main" : "background.paper",
|
||||
color:
|
||||
activeTab === "tun" ? "primary.contrastText" : "text.primary",
|
||||
borderRadius: 1.5,
|
||||
flex: 1,
|
||||
maxWidth: 160,
|
||||
transition: "all 0.2s ease-in-out",
|
||||
position: "relative",
|
||||
"&:hover": {
|
||||
transform: "translateY(-1px)",
|
||||
boxShadow: 1,
|
||||
},
|
||||
"&:after":
|
||||
activeTab === "tun"
|
||||
? {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
bottom: -9,
|
||||
left: "50%",
|
||||
width: 2,
|
||||
height: 9,
|
||||
bgcolor: "primary.main",
|
||||
transform: "translateX(-50%)",
|
||||
}
|
||||
: {},
|
||||
}}
|
||||
>
|
||||
<TroubleshootRounded fontSize="small" />
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ fontWeight: activeTab === "tun" ? 600 : 400 }}
|
||||
>
|
||||
{t("Tun Mode")}
|
||||
</Typography>
|
||||
</Paper>
|
||||
<TabButton
|
||||
isActive={activeTab === "system"}
|
||||
onClick={() => handleTabChange("system")}
|
||||
icon={ComputerRounded}
|
||||
label={t("System Proxy")}
|
||||
hasIndicator={sysproxy?.enable}
|
||||
/>
|
||||
<TabButton
|
||||
isActive={activeTab === "tun"}
|
||||
onClick={() => handleTabChange("tun")}
|
||||
icon={TroubleshootRounded}
|
||||
label={t("Tun Mode")}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{/* 说明文本区域 */}
|
||||
@@ -194,71 +224,10 @@ export const ProxyTunCard = () => {
|
||||
overflow: "visible",
|
||||
}}
|
||||
>
|
||||
{activeTab === "system" && (
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 0.5,
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
{getTabDescription("system")}
|
||||
<Tooltip title={t("System Proxy Info")}>
|
||||
<HelpOutlineRounded
|
||||
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
</Fade>
|
||||
)}
|
||||
|
||||
{activeTab === "tun" && (
|
||||
<Fade in={true} timeout={200}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="div"
|
||||
sx={{
|
||||
width: "95%",
|
||||
textAlign: "center",
|
||||
color: "text.secondary",
|
||||
p: 0.8,
|
||||
borderRadius: 1,
|
||||
borderColor: "primary.main",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
backgroundColor: "background.paper",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 0.5,
|
||||
wordBreak: "break-word",
|
||||
hyphens: "auto",
|
||||
}}
|
||||
>
|
||||
{getTabDescription("tun")}
|
||||
<Tooltip title={t("Tun Mode Info")}>
|
||||
<HelpOutlineRounded
|
||||
sx={{ fontSize: 14, opacity: 0.7, flexShrink: 0 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Typography>
|
||||
</Fade>
|
||||
)}
|
||||
<TabDescription
|
||||
description={tabDescription.text}
|
||||
tooltipTitle={tabDescription.tooltip}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* 控制开关部分 */}
|
||||
|
||||
@@ -7,7 +7,7 @@ import useSWR from "swr";
|
||||
import { getRunningMode, getSystemInfo, installService } from "@/services/cmds";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { version as appVersion } from "@root/package.json";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { check as checkUpdate } from "@tauri-apps/plugin-updater";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Notice } from "@/components/base";
|
||||
@@ -17,99 +17,97 @@ export const SystemInfoCard = () => {
|
||||
const { verge, patchVerge } = useVerge();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 系统信息状态
|
||||
const [systemState, setSystemState] = useState({
|
||||
osInfo: "",
|
||||
lastCheckUpdate: "-",
|
||||
});
|
||||
|
||||
// 获取运行模式
|
||||
const { data: runningMode = "sidecar", mutate: mutateRunningMode } = useSWR(
|
||||
"getRunningMode",
|
||||
getRunningMode,
|
||||
{ suspense: false, revalidateOnFocus: false }
|
||||
);
|
||||
|
||||
// 获取系统信息
|
||||
const [osInfo, setOsInfo] = useState<string>("");
|
||||
// 初始化系统信息
|
||||
useEffect(() => {
|
||||
// 获取系统信息
|
||||
getSystemInfo()
|
||||
.then((info) => {
|
||||
const lines = info.split("\n");
|
||||
if (lines.length > 0) {
|
||||
// 提取系统名称和版本信息
|
||||
const sysNameLine = lines[0]; // System Name: xxx
|
||||
const sysVersionLine = lines[1]; // System Version: xxx
|
||||
|
||||
const sysName = sysNameLine.split(": ")[1] || "";
|
||||
const sysVersion = sysVersionLine.split(": ")[1] || "";
|
||||
|
||||
setOsInfo(`${sysName} ${sysVersion}`);
|
||||
const sysName = lines[0].split(": ")[1] || "";
|
||||
const sysVersion = lines[1].split(": ")[1] || "";
|
||||
setSystemState(prev => ({ ...prev, osInfo: `${sysName} ${sysVersion}` }));
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error getting system info:", err);
|
||||
});
|
||||
}, []);
|
||||
.catch(console.error);
|
||||
|
||||
// 获取最后检查更新时间
|
||||
const [lastCheckUpdate, setLastCheckUpdate] = useState<string>("-");
|
||||
|
||||
// 在组件挂载时检查本地存储中的最后更新时间
|
||||
useEffect(() => {
|
||||
// 获取最后检查更新时间
|
||||
const lastCheck = localStorage.getItem("last_check_update");
|
||||
if (lastCheck) {
|
||||
try {
|
||||
const timestamp = parseInt(lastCheck, 10);
|
||||
if (!isNaN(timestamp)) {
|
||||
const date = new Date(timestamp);
|
||||
setLastCheckUpdate(date.toLocaleString());
|
||||
setSystemState(prev => ({
|
||||
...prev,
|
||||
lastCheckUpdate: new Date(timestamp).toLocaleString()
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing last check update time", e);
|
||||
}
|
||||
} else if (verge?.auto_check_update) {
|
||||
// 如果启用了自动检查更新但没有最后检查时间记录,则触发一次检查
|
||||
// 如果启用了自动检查更新但没有记录,设置当前时间并延迟检查
|
||||
const now = Date.now();
|
||||
localStorage.setItem("last_check_update", now.toString());
|
||||
setLastCheckUpdate(new Date(now).toLocaleString());
|
||||
|
||||
// 延迟执行检查更新,避免在应用启动时立即执行
|
||||
setSystemState(prev => ({
|
||||
...prev,
|
||||
lastCheckUpdate: new Date(now).toLocaleString()
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
checkUpdate().catch((e) => console.error("Error checking update:", e));
|
||||
if (verge?.auto_check_update) {
|
||||
checkUpdate().catch(console.error);
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
}, [verge?.auto_check_update]);
|
||||
|
||||
// 监听 checkUpdate 调用并更新时间
|
||||
// 自动检查更新逻辑
|
||||
useSWR(
|
||||
"checkUpdate",
|
||||
verge?.auto_check_update ? "checkUpdate" : null,
|
||||
async () => {
|
||||
// 更新最后检查时间
|
||||
const now = Date.now();
|
||||
localStorage.setItem("last_check_update", now.toString());
|
||||
setLastCheckUpdate(new Date(now).toLocaleString());
|
||||
|
||||
// 实际执行检查更新
|
||||
setSystemState(prev => ({
|
||||
...prev,
|
||||
lastCheckUpdate: new Date(now).toLocaleString()
|
||||
}));
|
||||
return await checkUpdate();
|
||||
},
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次更新
|
||||
dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查,
|
||||
isPaused: () => !(verge?.auto_check_update ?? true), // 根据 auto_check_update 设置决定是否启用
|
||||
},
|
||||
refreshInterval: 24 * 60 * 60 * 1000, // 每天检查一次
|
||||
dedupingInterval: 60 * 60 * 1000, // 1小时内不重复检查
|
||||
}
|
||||
);
|
||||
|
||||
// 导航到设置页面
|
||||
const goToSettings = () => {
|
||||
const goToSettings = useCallback(() => {
|
||||
navigate("/settings");
|
||||
};
|
||||
}, [navigate]);
|
||||
|
||||
// 切换自启动状态
|
||||
const toggleAutoLaunch = async () => {
|
||||
const toggleAutoLaunch = useCallback(async () => {
|
||||
if (!verge) return;
|
||||
try {
|
||||
if (!verge) return;
|
||||
// 将当前的启动状态取反
|
||||
await patchVerge({ enable_auto_launch: !verge.enable_auto_launch });
|
||||
} catch (err) {
|
||||
console.error("切换开机自启动状态失败:", err);
|
||||
}
|
||||
};
|
||||
}, [verge, patchVerge]);
|
||||
|
||||
// 安装系统服务
|
||||
const onInstallService = useLockFn(async () => {
|
||||
@@ -117,34 +115,48 @@ export const SystemInfoCard = () => {
|
||||
Notice.info(t("Installing Service..."), 1000);
|
||||
await installService();
|
||||
Notice.success(t("Service Installed Successfully"), 2000);
|
||||
// 重新获取运行模式
|
||||
await mutateRunningMode();
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString(), 3000);
|
||||
}
|
||||
});
|
||||
|
||||
// 点击运行模式
|
||||
const handleRunningModeClick = () => {
|
||||
// 点击运行模式处理
|
||||
const handleRunningModeClick = useCallback(() => {
|
||||
if (runningMode === "sidecar") {
|
||||
onInstallService();
|
||||
}
|
||||
};
|
||||
}, [runningMode, onInstallService]);
|
||||
|
||||
// 检查更新
|
||||
const onCheckUpdate = async () => {
|
||||
const onCheckUpdate = useLockFn(async () => {
|
||||
try {
|
||||
const info = await checkUpdate();
|
||||
if (!info?.available) {
|
||||
Notice.success(t("Currently on the Latest Version"));
|
||||
} else {
|
||||
Notice.info(t("Update Available"), 2000);
|
||||
goToSettings(); // 跳转到设置页面查看更新
|
||||
goToSettings();
|
||||
}
|
||||
} catch (err: any) {
|
||||
Notice.error(err.message || err.toString());
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 是否启用自启动
|
||||
const autoLaunchEnabled = useMemo(() => verge?.enable_auto_launch || false, [verge]);
|
||||
|
||||
// 运行模式样式
|
||||
const runningModeStyle = useMemo(() => ({
|
||||
cursor: runningMode === "sidecar" ? "pointer" : "default",
|
||||
textDecoration: runningMode === "sidecar" ? "underline" : "none",
|
||||
"&:hover": {
|
||||
opacity: runningMode === "sidecar" ? 0.7 : 1,
|
||||
},
|
||||
}), [runningMode]);
|
||||
|
||||
// 只有当verge存在时才渲染内容
|
||||
if (!verge) return null;
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
@@ -157,84 +169,71 @@ export const SystemInfoCard = () => {
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
{verge && (
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("OS Info")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{osInfo}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Auto Launch")}
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={verge.enable_auto_launch ? t("Enabled") : t("Disabled")}
|
||||
color={verge.enable_auto_launch ? "success" : "default"}
|
||||
variant={verge.enable_auto_launch ? "filled" : "outlined"}
|
||||
onClick={toggleAutoLaunch}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Running Mode")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight="medium"
|
||||
onClick={handleRunningModeClick}
|
||||
sx={{
|
||||
cursor: runningMode === "sidecar" ? "pointer" : "default",
|
||||
textDecoration:
|
||||
runningMode === "sidecar" ? "underline" : "none",
|
||||
"&:hover": {
|
||||
opacity: runningMode === "sidecar" ? 0.7 : 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{runningMode === "service"
|
||||
? t("Service Mode")
|
||||
: t("Sidecar Mode")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Last Check Update")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight="medium"
|
||||
onClick={onCheckUpdate}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
"&:hover": {
|
||||
opacity: 0.7,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{lastCheckUpdate}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Verge Version")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
v{appVersion}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack spacing={1.5}>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("OS Info")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{systemState.osInfo}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Auto Launch")}
|
||||
</Typography>
|
||||
<Chip
|
||||
size="small"
|
||||
label={autoLaunchEnabled ? t("Enabled") : t("Disabled")}
|
||||
color={autoLaunchEnabled ? "success" : "default"}
|
||||
variant={autoLaunchEnabled ? "filled" : "outlined"}
|
||||
onClick={toggleAutoLaunch}
|
||||
sx={{ cursor: "pointer" }}
|
||||
/>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Running Mode")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight="medium"
|
||||
onClick={handleRunningModeClick}
|
||||
sx={runningModeStyle}
|
||||
>
|
||||
{runningMode === "service" ? t("Service Mode") : t("Sidecar Mode")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Last Check Update")}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
fontWeight="medium"
|
||||
onClick={onCheckUpdate}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
textDecoration: "underline",
|
||||
"&:hover": { opacity: 0.7 },
|
||||
}}
|
||||
>
|
||||
{systemState.lastCheckUpdate}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("Verge Version")}
|
||||
</Typography>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
v{appVersion}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</EnhancedCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useMemo, useCallback } from "react";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { Box, IconButton, Tooltip, alpha, styled } from "@mui/material";
|
||||
import Grid from "@mui/material/Grid2";
|
||||
@@ -40,67 +40,79 @@ const ScrollBox = styled(Box)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// 默认测试列表,移到组件外部避免重复创建
|
||||
const DEFAULT_TEST_LIST = [
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Apple",
|
||||
url: "https://www.apple.com",
|
||||
icon: apple,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "GitHub",
|
||||
url: "https://www.github.com",
|
||||
icon: github,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Google",
|
||||
url: "https://www.google.com",
|
||||
icon: google,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Youtube",
|
||||
url: "https://www.youtube.com",
|
||||
icon: youtube,
|
||||
},
|
||||
];
|
||||
|
||||
export const TestCard = () => {
|
||||
const { t } = useTranslation();
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
const { verge, mutateVerge, patchVerge } = useVerge();
|
||||
const viewerRef = useRef<TestViewerRef>(null);
|
||||
|
||||
// test list
|
||||
const testList = verge?.test_list ?? [
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Apple",
|
||||
url: "https://www.apple.com",
|
||||
icon: apple,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "GitHub",
|
||||
url: "https://www.github.com",
|
||||
icon: github,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Google",
|
||||
url: "https://www.google.com",
|
||||
icon: google,
|
||||
},
|
||||
{
|
||||
uid: nanoid(),
|
||||
name: "Youtube",
|
||||
url: "https://www.youtube.com",
|
||||
icon: youtube,
|
||||
},
|
||||
];
|
||||
// 使用useMemo优化测试列表,避免每次渲染重新计算
|
||||
const testList = useMemo(() => {
|
||||
return verge?.test_list ?? DEFAULT_TEST_LIST;
|
||||
}, [verge?.test_list]);
|
||||
|
||||
const onTestListItemChange = (
|
||||
uid: string,
|
||||
patch?: Partial<IVergeTestItem>,
|
||||
) => {
|
||||
if (patch) {
|
||||
const newList = testList.map((x) => {
|
||||
if (x.uid === uid) {
|
||||
return { ...x, ...patch };
|
||||
}
|
||||
return x;
|
||||
});
|
||||
// 使用useCallback优化函数引用,避免不必要的重新渲染
|
||||
const onTestListItemChange = useCallback(
|
||||
(uid: string, patch?: Partial<IVergeTestItem>) => {
|
||||
if (!patch) {
|
||||
mutateVerge();
|
||||
return;
|
||||
}
|
||||
|
||||
const newList = testList.map((x) =>
|
||||
x.uid === uid ? { ...x, ...patch } : x
|
||||
);
|
||||
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
} else {
|
||||
mutateVerge();
|
||||
}
|
||||
};
|
||||
},
|
||||
[testList, verge, mutateVerge]
|
||||
);
|
||||
|
||||
const onDeleteTestListItem = (uid: string) => {
|
||||
const newList = testList.filter((x) => x.uid !== uid);
|
||||
patchVerge({ test_list: newList });
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
};
|
||||
const onDeleteTestListItem = useCallback(
|
||||
(uid: string) => {
|
||||
const newList = testList.filter((x) => x.uid !== uid);
|
||||
patchVerge({ test_list: newList });
|
||||
mutateVerge({ ...verge, test_list: newList }, false);
|
||||
},
|
||||
[testList, verge, patchVerge, mutateVerge]
|
||||
);
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (over && active.id !== over.id) {
|
||||
let old_index = testList.findIndex((x) => x.uid === active.id);
|
||||
let new_index = testList.findIndex((x) => x.uid === over.id);
|
||||
const onDragEnd = useCallback(
|
||||
async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const old_index = testList.findIndex((x) => x.uid === active.id);
|
||||
const new_index = testList.findIndex((x) => x.uid === over.id);
|
||||
|
||||
if (old_index >= 0 && new_index >= 0) {
|
||||
const newList = [...testList];
|
||||
const [removed] = newList.splice(old_index, 1);
|
||||
@@ -109,17 +121,42 @@ export const TestCard = () => {
|
||||
await mutateVerge({ ...verge, test_list: newList }, false);
|
||||
await patchVerge({ test_list: newList });
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
[testList, verge, mutateVerge, patchVerge]
|
||||
);
|
||||
|
||||
// 仅在verge首次加载时初始化测试列表
|
||||
useEffect(() => {
|
||||
if (!verge) return;
|
||||
if (!verge?.test_list) {
|
||||
patchVerge({ test_list: testList });
|
||||
if (verge && !verge.test_list) {
|
||||
patchVerge({ test_list: DEFAULT_TEST_LIST });
|
||||
}
|
||||
}, [verge]);
|
||||
}, [verge, patchVerge]);
|
||||
|
||||
const viewerRef = useRef<TestViewerRef>(null);
|
||||
// 使用useMemo优化UI内容,减少渲染计算
|
||||
const renderTestItems = useMemo(() => (
|
||||
<Grid container spacing={1} columns={12}>
|
||||
<SortableContext items={testList.map((x) => x.uid)}>
|
||||
{testList.map((item) => (
|
||||
<Grid key={item.uid} size={3}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
), [testList, onDeleteTestListItem]);
|
||||
|
||||
const handleTestAll = useCallback(() => {
|
||||
emit("verge://test-all");
|
||||
}, []);
|
||||
|
||||
const handleCreateTest = useCallback(() => {
|
||||
viewerRef.current?.create();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<EnhancedCard
|
||||
@@ -128,15 +165,12 @@ export const TestCard = () => {
|
||||
action={
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Tooltip title={t("Test All")} arrow>
|
||||
<IconButton size="small" onClick={() => emit("verge://test-all")}>
|
||||
<IconButton size="small" onClick={handleTestAll}>
|
||||
<NetworkCheck fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t("Create Test")} arrow>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => viewerRef.current?.create()}
|
||||
>
|
||||
<IconButton size="small" onClick={handleCreateTest}>
|
||||
<Add fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -149,20 +183,7 @@ export const TestCard = () => {
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<Grid container spacing={1} columns={12}>
|
||||
<SortableContext items={testList.map((x) => x.uid)}>
|
||||
{testList.map((item) => (
|
||||
<Grid key={item.uid} size={3}>
|
||||
<TestItem
|
||||
id={item.uid}
|
||||
itemData={item}
|
||||
onEdit={() => viewerRef.current?.edit(item)}
|
||||
onDelete={onDeleteTestListItem}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</SortableContext>
|
||||
</Grid>
|
||||
{renderTestItems}
|
||||
</DndContext>
|
||||
</ScrollBox>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { useRef } from "react";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
SettingsRounded,
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getAutotemProxy,
|
||||
getRunningMode,
|
||||
installService,
|
||||
getAutoLaunchStatus,
|
||||
} from "@/services/cmds";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { Box, Button, Tooltip } from "@mui/material";
|
||||
@@ -39,6 +40,15 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
"getRunningMode",
|
||||
getRunningMode,
|
||||
);
|
||||
const { data: autoLaunchEnabled } = useSWR("getAutoLaunchStatus", getAutoLaunchStatus);
|
||||
|
||||
// 当实际自启动状态与配置不同步时更新配置
|
||||
useEffect(() => {
|
||||
if (autoLaunchEnabled !== undefined && verge && verge.enable_auto_launch !== autoLaunchEnabled) {
|
||||
// 静默更新配置,不触发UI刷新
|
||||
mutateVerge({ ...verge, enable_auto_launch: autoLaunchEnabled }, false);
|
||||
}
|
||||
}, [autoLaunchEnabled]);
|
||||
|
||||
// 是否以sidecar模式运行
|
||||
const isSidecarMode = runningMode === "sidecar";
|
||||
@@ -181,7 +191,20 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
onChange={(e) => onChangeData({ enable_auto_launch: e })}
|
||||
onGuard={(e) => patchVerge({ enable_auto_launch: e })}
|
||||
onGuard={async (e) => {
|
||||
try {
|
||||
// 在应用更改之前先触发UI更新,让用户立即看到反馈
|
||||
onChangeData({ enable_auto_launch: e });
|
||||
await patchVerge({ enable_auto_launch: e });
|
||||
// 更新实际状态
|
||||
await mutate("getAutoLaunchStatus");
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
// 如果出错,恢复原始状态
|
||||
onChangeData({ enable_auto_launch: !e });
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Switch edge="end" />
|
||||
</GuardState>
|
||||
|
||||
@@ -207,9 +207,9 @@
|
||||
"System Proxy Disabled": "System proxy is disabled, it is recommended for most users to turn on this option",
|
||||
"TUN Mode Service Required": "TUN mode requires service mode, please install the service first",
|
||||
"TUN Mode Intercept Info": "TUN mode can take over all application traffic, suitable for special applications",
|
||||
"Rule Mode Description": "Routes traffic according to preset rules, provides flexible proxy strategies",
|
||||
"Global Mode Description": "All traffic goes through proxy servers, suitable for scenarios requiring global internet access",
|
||||
"Direct Mode Description": "All traffic doesn't go through proxy nodes, but is forwarded by Clash kernel to target servers, suitable for specific scenarios requiring kernel traffic distribution",
|
||||
"rule Mode Description": "Routes traffic according to preset rules, provides flexible proxy strategies",
|
||||
"global Mode Description": "All traffic goes through proxy servers, suitable for scenarios requiring global internet access",
|
||||
"direct Mode Description": "All traffic doesn't go through proxy nodes, but is forwarded by Clash kernel to target servers, suitable for specific scenarios requiring kernel traffic distribution",
|
||||
"Stack": "Tun Stack",
|
||||
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode",
|
||||
"Device": "Device Name",
|
||||
@@ -565,5 +565,9 @@
|
||||
"Failed": "Failed",
|
||||
"Completed": "Completed",
|
||||
"Bahamut Anime": "Bahamut Anime",
|
||||
"Disallowed ISP": "Disallowed ISP"
|
||||
"Disallowed ISP": "Disallowed ISP",
|
||||
"Originals Only": "Originals Only",
|
||||
"No (IP Banned By Disney+)": "No (IP Banned By Disney+)",
|
||||
"Unsupported Country": "Unsupported Country",
|
||||
"Failed (Network Connection)": "Failed (Network Connection)"
|
||||
}
|
||||
|
||||
@@ -207,9 +207,9 @@
|
||||
"System Proxy Disabled": "系统代理已关闭,建议大多数用户打开此选项",
|
||||
"TUN Mode Service Required": "TUN模式需要服务模式,请先安装服务",
|
||||
"TUN Mode Intercept Info": "TUN模式可以接管所有应用流量,适用于特殊应用",
|
||||
"Rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略",
|
||||
"Global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
|
||||
"Direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景",
|
||||
"rule Mode Description": "基于预设规则智能判断流量走向,提供灵活的代理策略",
|
||||
"global Mode Description": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
|
||||
"direct Mode Description": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景",
|
||||
"Stack": "TUN 模式堆栈",
|
||||
"System and Mixed Can Only be Used in Service Mode": "System 和 Mixed 只能在服务模式下使用",
|
||||
"Device": "TUN 网卡名称",
|
||||
@@ -479,7 +479,7 @@
|
||||
"Service Administrator Prompt": "Clash Verge 需要使用管理员权限来重新安装系统服务",
|
||||
"DNS Settings": "DNS 设置",
|
||||
"DNS Overwrite": "DNS 覆写",
|
||||
"DNS Settings Warning": "如果你不清楚这里的设置请不要修改,并保持 DNS 设置开启",
|
||||
"DNS Settings Warning": "如果你不清楚这里的设置请不要修改,并保持 DNS 覆写开启",
|
||||
"Enable DNS": "启用 DNS",
|
||||
"DNS Listen": "DNS 监听地址",
|
||||
"Enhanced Mode": "增强模式",
|
||||
@@ -565,5 +565,9 @@
|
||||
"Failed": "测试失败",
|
||||
"Completed": "检测完成",
|
||||
"Bahamut Anime": "动画疯",
|
||||
"Disallowed ISP": "不允许的 ISP"
|
||||
"Disallowed ISP": "不允许的 ISP",
|
||||
"Originals Only": "仅限原创",
|
||||
"No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)",
|
||||
"Unsupported Country": "不支持的国家",
|
||||
"Failed (Network Connection)": "测试失败(网络连接问题)"
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ const HomeSettingsDialog = ({
|
||||
const HomePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { verge } = useVerge();
|
||||
const { current } = useProfiles();
|
||||
const { current, mutateProfiles } = useProfiles();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -275,7 +275,10 @@ const HomePage = () => {
|
||||
{/* 订阅和当前节点部分 */}
|
||||
{homeCards.profile && (
|
||||
<Grid size={6}>
|
||||
<HomeProfileCard current={current} />
|
||||
<HomeProfileCard
|
||||
current={current}
|
||||
onProfileUpdated={mutateProfiles}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
|
||||
@@ -118,6 +118,16 @@ export async function getAutotemProxy() {
|
||||
}>("get_auto_proxy");
|
||||
}
|
||||
|
||||
export async function getAutoLaunchStatus() {
|
||||
try {
|
||||
return await invoke<boolean>("get_auto_launch_status");
|
||||
} catch (error) {
|
||||
console.error("获取自启动状态失败:", error);
|
||||
// 出错时返回false作为默认值
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function changeClashCore(clashCore: string) {
|
||||
return invoke<string | null>("change_clash_core", { clashCore });
|
||||
}
|
||||
|
||||
Vendored
+2
-2
@@ -5,8 +5,8 @@ After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
|
||||
Vendored
+2
-2
@@ -5,8 +5,8 @@ After=network.target nss-lookup.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_PTRACE CAP_DAC_READ_SEARCH
|
||||
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW CAP_NET_BIND_SERVICE CAP_SYS_TIME CAP_SYS_PTRACE CAP_DAC_READ_SEARCH CAP_DAC_OVERRIDE
|
||||
ExecStart=/usr/bin/mihomo -d /etc/mihomo
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
Restart=on-failure
|
||||
|
||||
@@ -45,7 +45,11 @@ func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr,
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
|
||||
case *syscall.SockaddrInet6:
|
||||
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)}
|
||||
zone := ""
|
||||
if from.ZoneId != 0 {
|
||||
zone = strconv.FormatInt(int64(from.ZoneId), 10)
|
||||
}
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone}
|
||||
}
|
||||
}
|
||||
// udp should not convert readN == 0 to io.EOF
|
||||
|
||||
@@ -54,7 +54,11 @@ func (c *enhanceUDPConn) WaitReadFrom() (data []byte, put func(), addr net.Addr,
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port}
|
||||
case *windows.SockaddrInet6:
|
||||
ip := from.Addr // copy from.Addr; ip escapes, so this line allocates 16 bytes
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: strconv.FormatInt(int64(from.ZoneId), 10)}
|
||||
zone := ""
|
||||
if from.ZoneId != 0 {
|
||||
zone = strconv.FormatInt(int64(from.ZoneId), 10)
|
||||
}
|
||||
addr = &net.UDPAddr{IP: ip[:], Port: from.Port, Zone: zone}
|
||||
}
|
||||
}
|
||||
// udp should not convert readN == 0 to io.EOF
|
||||
|
||||
@@ -88,21 +88,22 @@ func ListenPacket(ctx context.Context, network, address string, rAddrPort netip.
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToListenConfig(lc)
|
||||
} else {
|
||||
if cfg.interfaceName == "" {
|
||||
interfaceName := cfg.interfaceName
|
||||
if interfaceName == "" {
|
||||
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
||||
cfg.interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
|
||||
interfaceName = finder.FindInterfaceName(rAddrPort.Addr())
|
||||
}
|
||||
}
|
||||
if rAddrPort.Addr().Unmap().IsLoopback() {
|
||||
// avoid "The requested address is not valid in its context."
|
||||
cfg.interfaceName = ""
|
||||
interfaceName = ""
|
||||
}
|
||||
if cfg.interfaceName != "" {
|
||||
if interfaceName != "" {
|
||||
bind := bindIfaceToListenConfig
|
||||
if cfg.fallbackBind {
|
||||
bind = fallbackBindIfaceToListenConfig
|
||||
}
|
||||
addr, err := bind(cfg.interfaceName, lc, network, address, rAddrPort)
|
||||
addr, err := bind(interfaceName, lc, network, address, rAddrPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -162,17 +163,18 @@ func dialContext(ctx context.Context, network string, destination netip.Addr, po
|
||||
if DefaultSocketHook != nil { // ignore interfaceName, routingMark and tfo when DefaultSocketHook not null (in CMFA)
|
||||
socketHookToToDialer(dialer)
|
||||
} else {
|
||||
if opt.interfaceName == "" {
|
||||
interfaceName := opt.interfaceName // don't change the "opt", it's a pointer
|
||||
if interfaceName == "" {
|
||||
if finder := DefaultInterfaceFinder.Load(); finder != nil {
|
||||
opt.interfaceName = finder.FindInterfaceName(destination)
|
||||
interfaceName = finder.FindInterfaceName(destination)
|
||||
}
|
||||
}
|
||||
if opt.interfaceName != "" {
|
||||
if interfaceName != "" {
|
||||
bind := bindIfaceToDialer
|
||||
if opt.fallbackBind {
|
||||
bind = fallbackBindIfaceToDialer
|
||||
}
|
||||
if err := bind(opt.interfaceName, dialer, network, destination); err != nil {
|
||||
if err := bind(interfaceName, dialer, network, destination); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/metacubex/mihomo/common/singledo"
|
||||
|
||||
"github.com/metacubex/bart"
|
||||
)
|
||||
|
||||
type Interface struct {
|
||||
@@ -23,16 +25,23 @@ var (
|
||||
ErrAddrNotFound = errors.New("addr not found")
|
||||
)
|
||||
|
||||
var interfaces = singledo.NewSingle[map[string]*Interface](time.Second * 20)
|
||||
type ifaceCache struct {
|
||||
ifMap map[string]*Interface
|
||||
ifTable bart.Table[*Interface]
|
||||
}
|
||||
|
||||
func Interfaces() (map[string]*Interface, error) {
|
||||
value, err, _ := interfaces.Do(func() (map[string]*Interface, error) {
|
||||
var caches = singledo.NewSingle[*ifaceCache](time.Second * 20)
|
||||
|
||||
func getCache() (*ifaceCache, error) {
|
||||
value, err, _ := caches.Do(func() (*ifaceCache, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := map[string]*Interface{}
|
||||
cache := &ifaceCache{
|
||||
ifMap: make(map[string]*Interface),
|
||||
}
|
||||
|
||||
for _, iface := range ifaces {
|
||||
addrs, err := iface.Addrs()
|
||||
@@ -61,7 +70,7 @@ func Interfaces() (map[string]*Interface, error) {
|
||||
}
|
||||
}
|
||||
|
||||
r[iface.Name] = &Interface{
|
||||
ifaceObj := &Interface{
|
||||
Index: iface.Index,
|
||||
MTU: iface.MTU,
|
||||
Name: iface.Name,
|
||||
@@ -69,13 +78,26 @@ func Interfaces() (map[string]*Interface, error) {
|
||||
Flags: iface.Flags,
|
||||
Addresses: ipNets,
|
||||
}
|
||||
cache.ifMap[iface.Name] = ifaceObj
|
||||
|
||||
for _, prefix := range ipNets {
|
||||
cache.ifTable.Insert(prefix, ifaceObj)
|
||||
}
|
||||
}
|
||||
|
||||
return r, nil
|
||||
return cache, nil
|
||||
})
|
||||
return value, err
|
||||
}
|
||||
|
||||
func Interfaces() (map[string]*Interface, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cache.ifMap, nil
|
||||
}
|
||||
|
||||
func ResolveInterface(name string) (*Interface, error) {
|
||||
ifaces, err := Interfaces()
|
||||
if err != nil {
|
||||
@@ -90,23 +112,29 @@ func ResolveInterface(name string) (*Interface, error) {
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func IsLocalIp(ip netip.Addr) (bool, error) {
|
||||
ifaces, err := Interfaces()
|
||||
func ResolveInterfaceByAddr(addr netip.Addr) (*Interface, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iface, ok := cache.ifTable.Lookup(addr)
|
||||
if !ok {
|
||||
return nil, ErrIfaceNotFound
|
||||
}
|
||||
|
||||
return iface, nil
|
||||
}
|
||||
|
||||
func IsLocalIp(addr netip.Addr) (bool, error) {
|
||||
cache, err := getCache()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
for _, addr := range iface.Addresses {
|
||||
if addr.Contains(ip) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
return cache.ifTable.Contains(addr), nil
|
||||
}
|
||||
|
||||
func FlushCache() {
|
||||
interfaces.Reset()
|
||||
caches.Reset()
|
||||
}
|
||||
|
||||
func (iface *Interface) PickIPv4Addr(destination netip.Addr) (netip.Prefix, error) {
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
@@ -23,28 +24,31 @@ func dnsReadConfig() (servers []string, err error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var ip net.IP
|
||||
var ip netip.Addr
|
||||
switch sa := sa.(type) {
|
||||
case *syscall.SockaddrInet4:
|
||||
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
|
||||
ip = netip.AddrFrom4(sa.Addr)
|
||||
case *syscall.SockaddrInet6:
|
||||
ip = make(net.IP, net.IPv6len)
|
||||
copy(ip, sa.Addr[:])
|
||||
if ip[0] == 0xfe && ip[1] == 0xc0 {
|
||||
if sa.Addr[0] == 0xfe && sa.Addr[1] == 0xc0 {
|
||||
// Ignore these fec0/10 ones. Windows seems to
|
||||
// populate them as defaults on its misc rando
|
||||
// interfaces.
|
||||
continue
|
||||
}
|
||||
ip = netip.AddrFrom16(sa.Addr)
|
||||
if sa.ZoneId != 0 {
|
||||
ip = ip.WithZone(strconv.FormatInt(int64(sa.ZoneId), 10))
|
||||
}
|
||||
//continue
|
||||
default:
|
||||
// Unexpected type.
|
||||
continue
|
||||
}
|
||||
if slices.Contains(servers, ip.String()) {
|
||||
ipStr := ip.String()
|
||||
if slices.Contains(servers, ipStr) {
|
||||
continue
|
||||
}
|
||||
servers = append(servers, ip.String())
|
||||
servers = append(servers, ipStr)
|
||||
}
|
||||
}
|
||||
return
|
||||
|
||||
@@ -18,6 +18,7 @@ require (
|
||||
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
|
||||
github.com/mdlayher/netlink v1.7.2
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab
|
||||
github.com/metacubex/bart v0.19.0
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399
|
||||
github.com/metacubex/chacha v0.1.1
|
||||
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759
|
||||
|
||||
@@ -97,6 +97,8 @@ github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U
|
||||
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab h1:Chbw+/31UC14YFNr78pESt5Vowlc62zziw05JCUqoL4=
|
||||
github.com/metacubex/amneziawg-go v0.0.0-20240922133038-fdf3a4d5a4ab/go.mod h1:xVKK8jC5Sd3hfh7WjmCq+HorehIbrBijaUWmcuKjPcI=
|
||||
github.com/metacubex/bart v0.19.0 h1:XQ9AJeI+WO+phRPkUOoflAFwlqDJnm5BPQpixciJQBY=
|
||||
github.com/metacubex/bart v0.19.0/go.mod h1:DCcyfP4MC+Zy7sLK7XeGuMw+P5K9mIRsYOBgiE8icsI=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 h1:oBowHVKZycNtAFbZ6avaCSZJYeme2Nrj+4RpV2cNJig=
|
||||
github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399/go.mod h1:4xcieuIK+M4bGQmQYZVqEaIYqjS1ahO4kXG7EmDgEro=
|
||||
github.com/metacubex/chacha v0.1.1 h1:OHIv11Nd9CISAIzegpjfupIoZp9DYm6uQw41RxvmU/c=
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package sing_tun
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
@@ -16,7 +15,8 @@ var DefaultInterfaceFinder control.InterfaceFinder = (*defaultInterfaceFinder)(n
|
||||
|
||||
func (f *defaultInterfaceFinder) Update() error {
|
||||
iface.FlushCache()
|
||||
return nil
|
||||
_, err := iface.Interfaces()
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) Interfaces() []control.Interface {
|
||||
@@ -32,27 +32,19 @@ func (f *defaultInterfaceFinder) Interfaces() []control.Interface {
|
||||
return interfaces
|
||||
}
|
||||
|
||||
var errNoSuchInterface = errors.New("no such network interface")
|
||||
|
||||
func (f *defaultInterfaceFinder) ByName(name string) (*control.Interface, error) {
|
||||
ifaces, err := iface.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, netInterface := range ifaces {
|
||||
if netInterface.Name == name {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
}
|
||||
_, err = net.InterfaceByName(name)
|
||||
netInterface, err := iface.ResolveInterface(name)
|
||||
if err == nil {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
if _, err := net.InterfaceByName(name); err == nil {
|
||||
err = f.Update()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f.ByName(name)
|
||||
}
|
||||
return nil, errNoSuchInterface
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) ByIndex(index int) (*control.Interface, error) {
|
||||
@@ -73,20 +65,13 @@ func (f *defaultInterfaceFinder) ByIndex(index int) (*control.Interface, error)
|
||||
}
|
||||
return f.ByIndex(index)
|
||||
}
|
||||
return nil, errNoSuchInterface
|
||||
return nil, iface.ErrIfaceNotFound
|
||||
}
|
||||
|
||||
func (f *defaultInterfaceFinder) ByAddr(addr netip.Addr) (*control.Interface, error) {
|
||||
ifaces, err := iface.Interfaces()
|
||||
netInterface, err := iface.ResolveInterfaceByAddr(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, netInterface := range ifaces {
|
||||
for _, prefix := range netInterface.Addresses {
|
||||
if prefix.Contains(addr) {
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, errNoSuchInterface
|
||||
return (*control.Interface)(netInterface), nil
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-partexp
|
||||
PKG_VERSION:=1.1.2
|
||||
PKG_RELEASE:=20250305
|
||||
PKG_VERSION:=1.2.1
|
||||
PKG_RELEASE:=20250315
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Sirpdboy <herboy2008@gmail.com>
|
||||
|
||||
LUCI_TITLE:=LuCI Support for Automatic Partition Mount
|
||||
LUCI_DEPENDS:=+fdisk +block-mount
|
||||
LUCI_DEPENDS:=+fdisk +block-mount +bc +parted
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
|
||||
|
||||
@@ -7,18 +7,17 @@ local util = require "nixio.util"
|
||||
local tp = require "luci.template.parser"
|
||||
local uci=luci.model.uci.cursor()
|
||||
luci.sys.exec("echo '-' >/tmp/partexp.log&&echo 1 > /tmp/lucilogpos" )
|
||||
-- get all device names (sdX and mmcblkX)
|
||||
local target_devnames = {}
|
||||
for dev in fs.dir("/dev") do
|
||||
local target_devnames = {}
|
||||
for dev in fs.dir("/dev") do
|
||||
if dev:match("^sd[a-z]$")
|
||||
or dev:match("^mmcblk%d+$")
|
||||
or dev:match("^sata[a-z]$")
|
||||
or dev:match("^nvme%d+n%d+$")
|
||||
or dev:match("^vd[a-z]$")
|
||||
or dev:match("^vd[a-z]")
|
||||
then
|
||||
table.insert(target_devnames, dev)
|
||||
table.insert(target_devnames, dev)
|
||||
end
|
||||
end
|
||||
end
|
||||
local devices = {}
|
||||
for i, bname in pairs(target_devnames) do
|
||||
local device_info = {}
|
||||
@@ -41,8 +40,6 @@ t.anonymous=true
|
||||
|
||||
e=t:option(ListValue,"target_function", translate("Select function"),translate("Select the function to be performed"))
|
||||
e:value("/overlay", translate("Expand application space overlay (/overlay)"))
|
||||
-- e:value("/", translate("Use as root filesystem (/)"))
|
||||
-- e:value("/lnoverlay", translate("Soft chain partition expansion(/overlay)"))
|
||||
e:value("/opt", translate("Used as Docker data disk (/opt)"))
|
||||
e:value("/dev", translate("Normal mount and use by device name(/dev/x1)"))
|
||||
e.default="/opt"
|
||||
@@ -63,7 +60,6 @@ e.default=0
|
||||
e=t:option(Flag,'auto_format', translate('Format before use'),translate("Ticking indicates formatting"))
|
||||
e:depends("target_function", "/opt")
|
||||
e:depends("target_function", "/dev")
|
||||
-- e:depends("target_function", "/lnoverlay")
|
||||
e.default=0
|
||||
|
||||
e=t:option(Button, "restart", translate("Perform operation"))
|
||||
|
||||
@@ -17,6 +17,7 @@ local d.list_disks = function()
|
||||
or dev:match("^mmcblk%d+$")
|
||||
or dev:match("^sata[a-z]$")
|
||||
or dev:match("^nvme%d+n%d+$")
|
||||
or dev:match("^vd[a-z]$")
|
||||
then
|
||||
table.insert(target_devnames, dev)
|
||||
end
|
||||
|
||||
@@ -45,6 +45,9 @@ msgstr "选择需要操作的硬盘设备"
|
||||
msgid "Select function"
|
||||
msgstr "选择功能"
|
||||
|
||||
msgid "Select the function to be performed"
|
||||
msgstr "选择要执行的功能"
|
||||
|
||||
msgid "Click to execute"
|
||||
msgstr "点击执行"
|
||||
|
||||
@@ -73,4 +76,4 @@ msgid "Ticking indicates formatting"
|
||||
msgstr "打勾选择表示格式化"
|
||||
|
||||
msgid "Tick means to retain the settings"
|
||||
msgstr "打勾选择表示保留设置"
|
||||
msgstr "打勾选择表示保留设置"
|
||||
|
||||
@@ -45,6 +45,9 @@ msgstr "选择需要操作的硬盘设备"
|
||||
msgid "Select function"
|
||||
msgstr "选择功能"
|
||||
|
||||
msgid "Select the function to be performed"
|
||||
msgstr "选择要执行的功能"
|
||||
|
||||
msgid "Click to execute"
|
||||
msgstr "点击执行"
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ limit_log() {
|
||||
local sc=100
|
||||
[ -n "$2" ] && sc=$2
|
||||
local count=$(grep -c "" $logf)
|
||||
if [ $count -gt $sc ];then
|
||||
if [[ $count -gt $sc ]];then
|
||||
let count=count-$sc
|
||||
sed -i "1,$count d" $logf
|
||||
fi
|
||||
@@ -50,19 +50,54 @@ get_config() {
|
||||
config_get_bool auto_format $1 auto_format 1
|
||||
}
|
||||
|
||||
nfdisk(){
|
||||
# 定义函数:检查硬盘是否已挂载
|
||||
is_disk_mounted() {
|
||||
DISK=$1
|
||||
# 检查是否已挂载
|
||||
if mount | grep -q "^/dev/$DISK"; then
|
||||
return 0 # 已挂载
|
||||
else
|
||||
return 1 # 未挂载
|
||||
fi
|
||||
}
|
||||
|
||||
#Start partition
|
||||
fdisk $1 << EOF >/dev/null 2> /dev/null
|
||||
n
|
||||
p
|
||||
|
||||
|
||||
|
||||
wq
|
||||
EOF
|
||||
# 定义函数:取消挂载硬盘
|
||||
unmount_disk() {
|
||||
DISK=$1
|
||||
# log "正在取消挂载 /dev/$DISK..."
|
||||
MOUNT='';eval $(block info "/dev/$DISK" | grep -o -e "MOUNT=\S*")
|
||||
if [ "$MOUNT" ] ; then
|
||||
umount /dev/$DISK 2>/dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
log "取消挂载成功: /dev/$DISK"
|
||||
else
|
||||
log "取消挂载失败: /dev/$DISK"
|
||||
fi
|
||||
fi
|
||||
|
||||
}
|
||||
|
||||
# 定义函数:检查是否有共享挂载(如 Samba 或 NFS)
|
||||
check_shared_mount() {
|
||||
DISK=$1
|
||||
if [ -f /etc/config/samba ]; then
|
||||
SHARED=$(grep -q "/dev/$DISK" /etc/config/samba)
|
||||
if [ $? -eq 0 ]; then
|
||||
log "检测到 Samba 共享挂载: /dev/$DISK"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
# 检查是否有 NFS 共享
|
||||
if [ -f /etc/exports ]; then
|
||||
SHARED=$(grep -q "/dev/$DISK" /etc/exports)
|
||||
if [ $? -eq 0 ]; then
|
||||
log "检测到 NFS 共享挂载: /dev/$DISK"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
usamba(){
|
||||
s=$1
|
||||
s2=$2
|
||||
@@ -75,33 +110,63 @@ s2=$2
|
||||
uci delete $s.@sambashare[$i]
|
||||
uci commit $s
|
||||
log "分区/dev/$b被挂载$MOUNT共享使用,删除$s共享成功!"
|
||||
sleep 5
|
||||
/etc/init.d/$s restart
|
||||
sleep 1
|
||||
}
|
||||
done
|
||||
}
|
||||
# 取消 Samba 共享
|
||||
if [ -f /etc/config/$s ]; then
|
||||
sed -i "/\/dev\/$b/d" /etc/config/$s
|
||||
/etc/init.d/$s restart
|
||||
fi
|
||||
# 取消 NFS 共享
|
||||
if [ -f /etc/exports ]; then
|
||||
sed -i "/\/dev\/$b/d" /etc/exports
|
||||
/etc/init.d/nfs restart
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
is_disk_partitioned() {
|
||||
PARTITION_COUNT=$(fdisk -l /dev/$1 2>/dev/null | grep -E "^/dev/$2" | wc -l)
|
||||
if [[ "$PARTITION_COUNT" -gt 0 ]]; then
|
||||
echo 1
|
||||
else
|
||||
echo 0
|
||||
fi
|
||||
}
|
||||
|
||||
partednew(){
|
||||
DISK=$1
|
||||
parted -s /dev/$DISK mklabel gpt
|
||||
parted -s /dev/$DISK mkpart primary ext4 1MiB -1
|
||||
}
|
||||
|
||||
fdisknew(){
|
||||
echo -e "n\np\n\n\n\nw" | fdisk /dev/$1 >/dev/null 2> /dev/null
|
||||
}
|
||||
|
||||
fdisksave(){
|
||||
echo -e "n\w" | fdisk /dev/$1 >/dev/null 2> /dev/null
|
||||
}
|
||||
|
||||
fdiskB(){
|
||||
a=$1
|
||||
b=$1$2
|
||||
log "检测$a是否分区..."
|
||||
isP=`fdisk -l /dev/$a |grep -v "bytes"|grep "/dev/$b" `
|
||||
sleep 2
|
||||
log "检测$a是否分区$b..."
|
||||
isfdisk=0
|
||||
if [ ! "$isP" ];then
|
||||
nfdisk /dev/$a
|
||||
isP=$(is_disk_partitioned $a $b)
|
||||
if [ "$isP" == '0' ] ;then
|
||||
fdisksave $a
|
||||
fdisknew $a
|
||||
sleep 2
|
||||
isfdisk=1
|
||||
|
||||
else
|
||||
isfdisk=2
|
||||
fi
|
||||
|
||||
isP=`fdisk -l /dev/$a |grep -v "bytes" | grep "/dev/$b"| wc -l`
|
||||
if [ "$isP" == 1 -a "$isfdisk" = 1 ] ;then
|
||||
isP=$(is_disk_partitioned $a $b)
|
||||
if [ $isP == '1' -a "$isfdisk" == 1 ] ;then
|
||||
log "分区$b建立成功!"
|
||||
elif [ "$isP" == 1 -a "$isfdisk" = 2 ] ;then
|
||||
elif [ $isP == '1' -a "$isfdisk" = 0 ] ;then
|
||||
log "检测目标分区$b已存在."
|
||||
else
|
||||
log "分区$b建立失败,没有足够的空间操作失败!"
|
||||
@@ -109,6 +174,7 @@ fdiskB(){
|
||||
fi
|
||||
sleep 1
|
||||
[ -e "/etc/config/dockerd" ] && /etc/init.d/dockerd stop >/dev/null 2> /dev/null
|
||||
|
||||
block detect > /etc/config/fstab
|
||||
isD=` block info "/dev/$b" `
|
||||
if [ "$isD" ];then
|
||||
@@ -119,7 +185,7 @@ fdiskB(){
|
||||
usamba samba4 $MOUNT
|
||||
usamba samba $MOUNT
|
||||
sleep 5
|
||||
umount $MOUNT || block umount /dev/$b
|
||||
umount $MOUNT || block umount /dev/$b >/dev/null 2> /dev/null
|
||||
sleep 5
|
||||
MOUNT='';eval $(block info "/dev/$b" | grep -o -e "MOUNT=\S*")
|
||||
if [ "x$MOUNT" != 'x' ] ;then
|
||||
@@ -137,7 +203,6 @@ fdiskB(){
|
||||
mkfs.ext4 -L extroot /dev/$b >/dev/null 2> /dev/null
|
||||
log "设备/dev/$b格式化EXT4成功!"
|
||||
elif [ "$auto_format" = "1" -o "$isfdisk" = "1" ] ; then
|
||||
log "正在将/dev/$b格式化为EXT4文件系统..."
|
||||
mkfs.ext4 -F /dev/$b >/dev/null 2> /dev/null
|
||||
log "设备/dev/$b格式化EXT4成功!"
|
||||
elif [ "$TYPE" ];then
|
||||
@@ -150,7 +215,7 @@ fdiskB(){
|
||||
[ -d "/mnt/$b" ] || mkdir -p /mnt/$b
|
||||
MOUNT='';eval $(block info "/dev/$b" | grep -o -e "MOUNT=\S*")
|
||||
if [ "x$MOUNT" != 'x' ] ;then
|
||||
umount /mnt/$b || block umount /dev/$b
|
||||
umount /mnt/$b || block umount /dev/$b >/dev/null 2> /dev/null
|
||||
MOUNT='';eval $(block info "/dev/$b" | grep -o -e "MOUNT=\S*")
|
||||
if [ "$MOUNT" ] ; then
|
||||
log "检测设备/dev/$b被占用,请检查设备硬件或中止其他插件等占用权限再重试!"
|
||||
@@ -158,8 +223,8 @@ fdiskB(){
|
||||
fi
|
||||
fi
|
||||
TYPE='';eval $(block info "/dev/$b" | grep -o -e "TYPE=\S*")
|
||||
log "检测设备/dev/$b分区是$TYPE格式!"
|
||||
if [ "$TYPE" = "ntfs" ];then
|
||||
log "检测设备/dev/$b分区是$TYPE格式!"
|
||||
if [ `which ntfs-3g ` ] ;then
|
||||
mount -t ntfs-3g /dev/$b /mnt/$b >/dev/null 2> /dev/null
|
||||
elif [ `which ntfs3 ` ] ;then
|
||||
@@ -169,7 +234,7 @@ fdiskB(){
|
||||
expquit 1
|
||||
fi
|
||||
else
|
||||
mount /dev/$b /mnt/$b >/dev/null 2> /dev/null
|
||||
/usr/bin/mount /dev/$b /mnt/$b >/dev/null 2> /dev/null
|
||||
fi
|
||||
UUID='';eval $(block info /dev/$b | grep -o -e "UUID=\S*")
|
||||
if [ ! "$UUID" ] ; then
|
||||
@@ -184,7 +249,7 @@ fdiskB(){
|
||||
mount --bind / /tmp/introot
|
||||
tar -C /tmp/introot -cvf - . | tar -C /mnt/$b -xf -
|
||||
umount /tmp/introot || block umount /tmp/introot
|
||||
umount /mnt/$b || block umount /dev/$b
|
||||
umount /mnt/$b || block umount /dev/$b >/dev/null 2> /dev/null
|
||||
block detect > /etc/config/fstab
|
||||
OVERLAY=`uci -q get fstab.@mount[0].target `
|
||||
if [ "$OVERLAY" == "/overlay" -o "$OVERLAY" == "/dev/loop0" ] ;then
|
||||
@@ -203,7 +268,7 @@ fdiskB(){
|
||||
|
||||
# cp -a -f /overlay/* /mnt/$b/ || cp -a -f /rom/overlay/* /mnt/$b/
|
||||
tar -C /overlay -cvf - . | tar -C /mnt/$b/ -xf - || tar -C /rom/overlay -cvf - . | tar -C /mnt/$b/ -xf -
|
||||
umount /mnt/$b || block umount /dev/$b
|
||||
umount /mnt/$b || block umount /dev/$b >/dev/null 2> /dev/null
|
||||
block detect > /etc/config/fstab
|
||||
OVERLAY=`uci -q get fstab.@mount[0].target `
|
||||
if [ "$OVERLAY" == "/overlay" -o "$OVERLAY" == "/dev/loop0" ] ;then
|
||||
@@ -230,7 +295,7 @@ fdiskB(){
|
||||
|
||||
else
|
||||
|
||||
umount /mnt/$b || block umount /dev/$b
|
||||
umount /mnt/$b || block umount /dev/$b >/dev/null 2> /dev/null
|
||||
block detect > /etc/config/fstab
|
||||
OVERLAY=`uci -q get fstab.@mount[0].target `
|
||||
if [ "$OVERLAY" == "/overlay" -o "$OVERLAY" == "/dev/loop0" ] ;then
|
||||
@@ -258,7 +323,7 @@ fdiskB(){
|
||||
|
||||
fi
|
||||
elif [ "$target_function" = "/lnoverlay" ] ; then
|
||||
umount /mnt/$b || block umount /dev/$b
|
||||
umount /mnt/$b || block umount /dev/$b >/dev/null 2> /dev/null
|
||||
|
||||
block detect > /etc/config/fstab
|
||||
mkdir -p $target_function
|
||||
@@ -278,7 +343,7 @@ fdiskB(){
|
||||
log "设备重启才能生效"
|
||||
expquit 2
|
||||
else
|
||||
umount /mnt/$b || block umount /dev/$b
|
||||
umount /mnt/$b || block umount /dev/$b >/dev/null 2> /dev/null
|
||||
|
||||
block detect > /etc/config/fstab
|
||||
mkdir -p $target_function
|
||||
@@ -302,55 +367,115 @@ fdiskB(){
|
||||
fi
|
||||
}
|
||||
|
||||
get_system_disk() {
|
||||
SYSTEM_DISK=$(mount | grep 'on /overlay' | awk '{print $1}' | sed 's/[0-9]*$//' |head -1)
|
||||
[ -z ${SYSTEM_DISK} ] && SYSTEM_DISK=$( df -h | grep boot |awk -F ' ' '{print $1}' |head -1)
|
||||
echo "$SYSTEM_DISK"
|
||||
}
|
||||
|
||||
get_all_disks() {
|
||||
DISKS=`find /dev -regex '.*/\(sd[a-z]\|mmcblk[0-9]\+\|sata[a-z]\|nvme[0-9]\+n[0-9]\+\|vd[a-z]\)$'`
|
||||
echo "$DISKS"
|
||||
}
|
||||
|
||||
check_free_space() {
|
||||
DISK=$1
|
||||
PARTED_OUTPUT=$(parted /dev/$DISK unit GB print free 2>/dev/null)
|
||||
FREE_SPACE=$(echo "$PARTED_OUTPUT" | grep "Free Space" | awk '{print $3}' )
|
||||
echo $FREE_SPACE |awk -F '.' '{print $1}'
|
||||
}
|
||||
|
||||
# 定义函数:获取下一个新的可用的分区号
|
||||
get_next_partition_number() {
|
||||
DISK=$1
|
||||
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | awk -F '[^0-9]+' '{print $NF}')
|
||||
MAX_PARTITION=$(echo "$PARTITIONS" | sort -n | tail -n 1)
|
||||
NEXT_PARTITION=$(awk -v n="$MAX_PARTITION" 'BEGIN { print n + 1 }')
|
||||
#NEXT_PARTITION=$((MAX_PARTITION + 1))
|
||||
echo "$NEXT_PARTITION"
|
||||
}
|
||||
|
||||
# 定义函数:获取最后一个可用的分区号
|
||||
get_last_partition_number() {
|
||||
DISK=$1
|
||||
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | awk -F '[^0-9]+' '{print $NF}')
|
||||
MAX_PARTITION=$(echo "$PARTITIONS" | sort -n | tail -n 1)
|
||||
echo "$MAX_PARTITION"
|
||||
}
|
||||
|
||||
|
||||
# 定义函数:获取硬盘分区数
|
||||
get_partition_number() {
|
||||
DISK=$1
|
||||
PARTITIONS=$(fdisk -l /dev/$DISK 2>/dev/null | grep -v boot | grep -E "^/dev/$DISK" | awk '{print $1}' | sed 's/\/dev\/[a-z]*//g' | wc -l)
|
||||
echo "$PARTITIONS"
|
||||
}
|
||||
|
||||
autopart() {
|
||||
# [ -f $LOCK ] && exit
|
||||
config_load partexp
|
||||
config_foreach get_config global
|
||||
init_env
|
||||
# touch $LOCK
|
||||
gen_log
|
||||
log "此次执行操作功能:$target_function ,目标盘:/dev/$target_disk"
|
||||
isP=`fdisk -l /dev/$target_disk |grep "Disk /dev/$target_disk"`
|
||||
if [ "$isP" = "" ];then
|
||||
log "没有检测到/dev/$target_disk目标设备!操作失败!"
|
||||
expquit 1
|
||||
else
|
||||
isB="$(sed -n -e "\|\s/boot\s.*$|{s///p;q}" /etc/mtab)"
|
||||
isb=`fdisk -l | grep /dev/$target_disk | grep -v "bytes" | wc -l`
|
||||
if [ "$isb" = 0 -o "$isb" = 1 ] ;then
|
||||
isb=1
|
||||
|
||||
log "检测到设备/dev/$target_disk分区数为$isb个!"
|
||||
elif [ "$isb" = 3 -o "$isb" = 4 ] ;then
|
||||
isb=3
|
||||
|
||||
log "检测到设备/dev/$target_disk分区数为$isb个!"
|
||||
else
|
||||
log "检测到设备/dev/$target_disk分区数$isb个,请备份数据后删除分区重新操作!"
|
||||
expquit 1
|
||||
fi
|
||||
log "检测到操作目标设备:/dev/$target_disk$isb!"
|
||||
case "$target_disk" in
|
||||
vd*)
|
||||
[ "$isB" = "/dev/${target_disk}1" -a "$target_function" = "/overlay" ] && fdiskB $target_disk 3 || fdiskB $target_disk $isb
|
||||
;;
|
||||
NEXTPART=1 #目标分区
|
||||
DISK=$target_disk #目标硬盘
|
||||
DISKSALL=$(get_all_disks) #硬盘名列表
|
||||
DISK_COUNT=$(echo "$DISKSALL" | wc -l) #所有硬盘数量
|
||||
log "系统中检测到的硬盘数量: $DISK_COUNT"
|
||||
log "硬盘信息列表:" $DISKSALL
|
||||
SYSTEM_DISK=$(get_system_disk)
|
||||
log "系统盘: "$SYSTEM_DISK
|
||||
if [ "/dev/$DISK" == "$SYSTEM_DISK" ] ;then
|
||||
fdisksave /dev/$DISK
|
||||
log "此次执行操作功能:$target_function ,目标盘是系统盘:/dev/$DISK"
|
||||
PARTITIONSUM=$(get_partition_number $DISK)
|
||||
log "目标盘 $DISK 一共有分区数: $PARTITIONSUM个"
|
||||
if [[ "$PARTITIONSUM" -gt 3 ]];then
|
||||
FREE_SPACE=$(check_free_space $(basename $DISK))
|
||||
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
|
||||
if [[ "$FREE_SPACE" -gt 2 ]]; then
|
||||
NEXTPART=$(get_next_partition_number $DISK)
|
||||
else
|
||||
NEXTPART=$(get_last_partition_number $DISK)
|
||||
fi
|
||||
else
|
||||
FREE_SPACE=$(check_free_space $(basename $DISK))
|
||||
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
|
||||
if [[ "$FREE_SPACE" -gt 2 ]]; then
|
||||
NEXTPART=$(get_next_partition_number $DISK)
|
||||
else
|
||||
|
||||
sd*)
|
||||
[ "$isB" = "/dev/${target_disk}1" -a "$target_function" = "/overlay" ] && fdiskB $target_disk 3 || fdiskB $target_disk $isb
|
||||
;;
|
||||
nvme*)
|
||||
[ "$isB" = "/dev/${target_disk}p1" -a "$target_function" = "/overlay" ] && fdiskB $target_disk p3 || fdiskB $target_disk p$isb
|
||||
;;
|
||||
mmc*)
|
||||
[ "$isB" = "/dev/${target_disk}p1" -a "$target_function" = "/overlay" ] && fdiskB $target_disk p3 || fdiskB $target_disk p$isb
|
||||
;;
|
||||
log "目标盘 $SYSTEM_DISK 没有足够的剩余空间!"
|
||||
expquit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log "此次执行操作功能:$target_function ,目标盘是:/dev/$DISK"
|
||||
PARTITIONSUM=$(get_partition_number $DISK)
|
||||
log "目标盘 $DISK 一共有分区数: $PARTITIONSUM个"
|
||||
if [[ "$PARTITIONSUM" -gt 1 ]];then
|
||||
FREE_SPACE=$(check_free_space $(basename $DISK))
|
||||
log "目标盘 $DISK 有剩余空间: $FREE_SPACE Gb"
|
||||
|
||||
[[ $FREE_SPACE -gt 2 ]] && NEXTPART=$(get_next_partition_number $DISK) || NEXTPART=$(get_last_partition_number $DISK)
|
||||
else
|
||||
NEXTPART=1
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
log "定位到操作目标设备分区:/dev/$DISK$NEXTPART"
|
||||
case "$DISK" in
|
||||
vd*) fdiskB $DISK $NEXTPART;;
|
||||
|
||||
sd*) fdiskB $DISK $NEXTPART;;
|
||||
nvme*) fdiskB $DISK p$NEXTPART;;
|
||||
mmc*) fdiskB $DISK p$NEXTPART;;
|
||||
*)
|
||||
log "目标设备/dev/$target_disk暂不支持!请联系作者sirpdboy!"
|
||||
expquit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
fi
|
||||
rm -f $LOCK
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ function get_redir_log()
|
||||
proto = "TCP"
|
||||
end
|
||||
if fs.access(path .. "/" .. proto .. ".log") then
|
||||
local content = luci.sys.exec("cat ".. path .. "/" .. proto .. ".log")
|
||||
local content = luci.sys.exec("tail -n 19999 ".. path .. "/" .. proto .. ".log")
|
||||
content = content:gsub("\n", "<br />")
|
||||
luci.http.write(content)
|
||||
else
|
||||
@@ -216,7 +216,7 @@ function get_chinadns_log()
|
||||
local flag = luci.http.formvalue("flag")
|
||||
local path = "/tmp/etc/passwall/acl/" .. flag .. "/chinadns_ng.log"
|
||||
if fs.access(path) then
|
||||
local content = luci.sys.exec("cat ".. path)
|
||||
local content = luci.sys.exec("tail -n 5000 ".. path)
|
||||
content = content:gsub("\n", "<br />")
|
||||
luci.http.write(content)
|
||||
else
|
||||
|
||||
@@ -401,6 +401,10 @@ if singbox_tags:find("with_quic") then
|
||||
end
|
||||
|
||||
if singbox_tags:find("with_quic") then
|
||||
o = s:option(Value, _n("hysteria2_ports"), translate("Port hopping range"))
|
||||
o.description = translate("Format as 1000:2000 Multiple groups are separated by commas (,).")
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
|
||||
o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps"))
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
|
||||
|
||||
@@ -383,7 +383,17 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
end
|
||||
|
||||
if node.protocol == "hysteria2" then
|
||||
local server_ports = {}
|
||||
if node.hysteria2_ports then
|
||||
for range in node.hysteria2_ports:gmatch("([^,]+)") do
|
||||
if range:match("^%d+:%d+$") then
|
||||
table.insert(server_ports, range)
|
||||
end
|
||||
end
|
||||
end
|
||||
protocol_table = {
|
||||
server_ports = next(server_ports) and server_ports or nil,
|
||||
hop_interval = next(server_ports) and "30s" or nil,
|
||||
up_mbps = (node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil,
|
||||
down_mbps = (node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil,
|
||||
obfs = {
|
||||
|
||||
@@ -1821,3 +1821,9 @@ msgstr "中断现有连接"
|
||||
|
||||
msgid "Interrupt existing connections when the selected outbound has changed."
|
||||
msgstr "当选择的出站发生变化时中断现有连接。"
|
||||
|
||||
msgid "Port hopping range"
|
||||
msgstr "端口跳跃范围"
|
||||
|
||||
msgid "Format as 1000:2000 Multiple groups are separated by commas (,)."
|
||||
msgstr "格式为:1000:2000 多组时用逗号(,)隔开。"
|
||||
|
||||
@@ -6,9 +6,9 @@ all: build
|
||||
|
||||
build:
|
||||
ifeq (${TARGET}, release)
|
||||
cargo build --release --features "local-tun local-redir"
|
||||
cargo build --release --features "full"
|
||||
else
|
||||
cargo build --features "local-tun local-redir"
|
||||
cargo build --features "full"
|
||||
endif
|
||||
|
||||
install:
|
||||
|
||||
@@ -52,7 +52,7 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour) {
|
||||
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||
var defaultOptions adapter.NetworkOptions
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.12.0-alpha.18
|
||||
|
||||
* Add wildcard SNI support for ShadowTLS inbound **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [ShadowTLS](/configuration/inbound/shadowtls/#wildcard_sni).
|
||||
|
||||
#### 1.12.0-alpha.17
|
||||
|
||||
* Add NTP sniffer **1**
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [wildcard_sni](#wildcard_sni)
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@@ -29,7 +37,8 @@
|
||||
... // Dial Fields
|
||||
}
|
||||
},
|
||||
"strict_mode": false
|
||||
"strict_mode": false,
|
||||
"wildcard_sni": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -55,7 +64,6 @@ ShadowTLS password.
|
||||
|
||||
Only available in the ShadowTLS protocol 2.
|
||||
|
||||
|
||||
#### users
|
||||
|
||||
ShadowTLS users.
|
||||
@@ -66,6 +74,8 @@ Only available in the ShadowTLS protocol 3.
|
||||
|
||||
==Required==
|
||||
|
||||
When `wildcard_sni` is configured to `all`, the server address is optional.
|
||||
|
||||
Handshake server address and [Dial Fields](/configuration/shared/dial/).
|
||||
|
||||
#### handshake_for_server_name
|
||||
@@ -79,3 +89,19 @@ Only available in the ShadowTLS protocol 2/3.
|
||||
ShadowTLS strict mode.
|
||||
|
||||
Only available in the ShadowTLS protocol 3.
|
||||
|
||||
#### wildcard_sni
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
ShadowTLS wildcard SNI mode.
|
||||
|
||||
Available values are:
|
||||
|
||||
* `off`: (default) Disabled.
|
||||
* `authed`: Authenticated connections will have their destination overwritten to `(servername):443`
|
||||
* `all`: All connections will have their destination overwritten to `(servername):443`
|
||||
|
||||
Additionally, connections matching `handshake_for_server_name` are not affected.
|
||||
|
||||
Only available in the ShadowTLS protocol 3.
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [wildcard_sni](#wildcard_sni)
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@@ -29,7 +37,8 @@
|
||||
... // 拨号字段
|
||||
}
|
||||
},
|
||||
"strict_mode": false
|
||||
"strict_mode": false,
|
||||
"wildcard_sni": ""
|
||||
}
|
||||
```
|
||||
|
||||
@@ -80,3 +89,19 @@ ShadowTLS 用户。
|
||||
ShadowTLS 严格模式。
|
||||
|
||||
仅在 ShadowTLS 协议版本 3 中可用。
|
||||
|
||||
#### wildcard_sni
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
ShadowTLS 通配符 SNI 模式。
|
||||
|
||||
可用值:
|
||||
|
||||
* `off`:(默认)禁用。
|
||||
* `authed`:已认证的连接的目标将被重写为 `(servername):443`。
|
||||
* `all`:所有连接的目标将被重写为 `(servername):443`。
|
||||
|
||||
此外,匹配 `handshake_for_server_name` 的连接不受影响。
|
||||
|
||||
仅在 ShadowTLS 协议 3 中可用。
|
||||
|
||||
@@ -6,6 +6,7 @@ config homeproxy 'infra'
|
||||
option redirect_port '5331'
|
||||
option tproxy_port '5332'
|
||||
option dns_port '5333'
|
||||
option sniff_override '1'
|
||||
option udp_timeout ''
|
||||
option tun_name 'singtun0'
|
||||
option tun_addr4 '172.19.0.1/30'
|
||||
|
||||
@@ -51,7 +51,7 @@ if (!wan_dns)
|
||||
|
||||
const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333';
|
||||
|
||||
let main_node, main_udp_node, dedicated_udp_node, default_outbound, domain_strategy, sniff_override = '1',
|
||||
let main_node, main_udp_node, dedicated_udp_node, default_outbound, domain_strategy, sniff_override,
|
||||
dns_server, china_dns_server, dns_default_strategy, dns_default_server, dns_disable_cache,
|
||||
dns_disable_cache_expire, dns_independent_cache, dns_client_subnet, cache_file_store_rdrc,
|
||||
cache_file_rdrc_timeout, direct_domain_list, proxy_domain_list;
|
||||
@@ -78,6 +78,8 @@ if (routing_mode !== 'custom') {
|
||||
proxy_domain_list = trim(readfile(HP_DIR + '/resources/proxy_list.txt'));
|
||||
if (proxy_domain_list)
|
||||
proxy_domain_list = split(proxy_domain_list, /[\r\n]/);
|
||||
|
||||
sniff_override = uci.get(uciconfig, uciinfra, 'sniff_override') || '1';
|
||||
} else {
|
||||
/* DNS settings */
|
||||
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.21.0
|
||||
PKG_VERSION:=1.21.1
|
||||
|
||||
LUCI_TITLE:=LuCI Support for nikki
|
||||
LUCI_DEPENDS:=+luci-base +nikki
|
||||
|
||||
@@ -105,8 +105,9 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
api: async function (method, path, query, body) {
|
||||
const apiListen = uci.get('nikki', 'mixin', 'api_listen');
|
||||
const apiSecret = uci.get('nikki', 'mixin', 'api_secret') ?? '';
|
||||
const profile = await callNikkiProfile();
|
||||
const apiListen = profile['external-controller'];
|
||||
const apiSecret = profile['secret'] ?? '';
|
||||
const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1);
|
||||
const url = `http://${window.location.hostname}:${apiPort}${path}`;
|
||||
return request.request(url, {
|
||||
@@ -118,9 +119,10 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
openDashboard: async function () {
|
||||
const uiName = uci.get('nikki', 'mixin', 'ui_name');
|
||||
const apiListen = uci.get('nikki', 'mixin', 'api_listen');
|
||||
const apiSecret = uci.get('nikki', 'mixin', 'api_secret') ?? '';
|
||||
const profile = await callNikkiProfile();
|
||||
const uiName = profile['external-ui-name'];
|
||||
const apiListen = profile['external-controller'];
|
||||
const apiSecret = profile['secret'] ?? '';
|
||||
const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1);
|
||||
const params = {
|
||||
host: window.location.hostname,
|
||||
|
||||
@@ -125,7 +125,7 @@ return view.extend({
|
||||
|
||||
o = s.taboption('external_control', form.Value, 'api_secret', _('API Secret'));
|
||||
o.password = true;
|
||||
o.rmempty = false;
|
||||
o.placeholder = _('Unmodified');
|
||||
|
||||
o = s.taboption('external_control', form.ListValue, 'selection_cache', _('Save Proxy Selection'));
|
||||
o.optional = true;
|
||||
|
||||
@@ -192,7 +192,7 @@ function get_redir_log()
|
||||
proto = "TCP"
|
||||
end
|
||||
if fs.access(path .. "/" .. proto .. ".log") then
|
||||
local content = luci.sys.exec("cat ".. path .. "/" .. proto .. ".log")
|
||||
local content = luci.sys.exec("tail -n 19999 ".. path .. "/" .. proto .. ".log")
|
||||
content = content:gsub("\n", "<br />")
|
||||
luci.http.write(content)
|
||||
else
|
||||
@@ -216,7 +216,7 @@ function get_chinadns_log()
|
||||
local flag = luci.http.formvalue("flag")
|
||||
local path = "/tmp/etc/passwall/acl/" .. flag .. "/chinadns_ng.log"
|
||||
if fs.access(path) then
|
||||
local content = luci.sys.exec("cat ".. path)
|
||||
local content = luci.sys.exec("tail -n 5000 ".. path)
|
||||
content = content:gsub("\n", "<br />")
|
||||
luci.http.write(content)
|
||||
else
|
||||
|
||||
@@ -401,6 +401,10 @@ if singbox_tags:find("with_quic") then
|
||||
end
|
||||
|
||||
if singbox_tags:find("with_quic") then
|
||||
o = s:option(Value, _n("hysteria2_ports"), translate("Port hopping range"))
|
||||
o.description = translate("Format as 1000:2000 Multiple groups are separated by commas (,).")
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
|
||||
o = s:option(Value, _n("hysteria2_up_mbps"), translate("Max upload Mbps"))
|
||||
o:depends({ [_n("protocol")] = "hysteria2" })
|
||||
|
||||
|
||||
@@ -383,7 +383,17 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
end
|
||||
|
||||
if node.protocol == "hysteria2" then
|
||||
local server_ports = {}
|
||||
if node.hysteria2_ports then
|
||||
for range in node.hysteria2_ports:gmatch("([^,]+)") do
|
||||
if range:match("^%d+:%d+$") then
|
||||
table.insert(server_ports, range)
|
||||
end
|
||||
end
|
||||
end
|
||||
protocol_table = {
|
||||
server_ports = next(server_ports) and server_ports or nil,
|
||||
hop_interval = next(server_ports) and "30s" or nil,
|
||||
up_mbps = (node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil,
|
||||
down_mbps = (node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil,
|
||||
obfs = {
|
||||
|
||||
@@ -1821,3 +1821,9 @@ msgstr "中断现有连接"
|
||||
|
||||
msgid "Interrupt existing connections when the selected outbound has changed."
|
||||
msgstr "当选择的出站发生变化时中断现有连接。"
|
||||
|
||||
msgid "Port hopping range"
|
||||
msgstr "端口跳跃范围"
|
||||
|
||||
msgid "Format as 1000:2000 Multiple groups are separated by commas (,)."
|
||||
msgstr "格式为:1000:2000 多组时用逗号(,)隔开。"
|
||||
|
||||
@@ -5,9 +5,9 @@ PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git
|
||||
PKG_SOURCE_DATE:=2025-03-14
|
||||
PKG_SOURCE_VERSION:=1e22f4daa964c54abea4c8b0f09f8171398a2821
|
||||
PKG_MIRROR_HASH:=0b65cd12cb6927b118d3802303b97dbc3299db1b3e6d8a849dedb32a276c30ff
|
||||
PKG_SOURCE_DATE:=2025-03-17
|
||||
PKG_SOURCE_VERSION:=e3d4ec247670dc88f8a3410e9bca2c69ca053f32
|
||||
PKG_MIRROR_HASH:=ab8e39fec0675a77d575acf5ed705bb327a5f9f2b8d72792896e884e491b25e6
|
||||
|
||||
PKG_LICENSE:=GPL3.0+
|
||||
PKG_MAINTAINER:=Joseph Mory <morytyann@gmail.com>
|
||||
@@ -16,7 +16,7 @@ PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
PKG_BUILD_VERSION:=alpha-1e22f4d
|
||||
PKG_BUILD_VERSION:=alpha-e3d4ec2
|
||||
PKG_BUILD_TIME:=$(shell date -u -Iseconds)
|
||||
|
||||
GO_PKG:=github.com/metacubex/mihomo
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user