diff --git a/common/arc/arc.go b/common/arc/arc.go new file mode 100644 index 0000000000..8d44a180a7 --- /dev/null +++ b/common/arc/arc.go @@ -0,0 +1,241 @@ +package arc + +import ( + "sync" + "time" + + list "github.com/bahlo/generic-list-go" + "github.com/samber/lo" +) + +//modify from https://github.com/alexanderGugel/arc + +// Option is part of Functional Options Pattern +type Option[K comparable, V any] func(*ARC[K, V]) + +func WithSize[K comparable, V any](maxSize int) Option[K, V] { + return func(a *ARC[K, V]) { + a.c = maxSize + } +} + +type ARC[K comparable, V any] struct { + p int + c int + t1 *list.List[*entry[K, V]] + b1 *list.List[*entry[K, V]] + t2 *list.List[*entry[K, V]] + b2 *list.List[*entry[K, V]] + mutex sync.Mutex + len int + cache map[K]*entry[K, V] +} + +// New returns a new Adaptive Replacement Cache (ARC). +func New[K comparable, V any](options ...Option[K, V]) *ARC[K, V] { + arc := &ARC[K, V]{} + arc.Clear() + + for _, option := range options { + option(arc) + } + return arc +} + +func (a *ARC[K, V]) Clear() { + a.mutex.Lock() + defer a.mutex.Unlock() + + a.p = 0 + a.t1 = list.New[*entry[K, V]]() + a.b1 = list.New[*entry[K, V]]() + a.t2 = list.New[*entry[K, V]]() + a.b2 = list.New[*entry[K, V]]() + a.len = 0 + a.cache = make(map[K]*entry[K, V]) +} + +// Set inserts a new key-value pair into the cache. +// This optimizes future access to this entry (side effect). +func (a *ARC[K, V]) Set(key K, value V) { + a.mutex.Lock() + defer a.mutex.Unlock() + + a.set(key, value) +} + +func (a *ARC[K, V]) set(key K, value V) { + a.setWithExpire(key, value, time.Unix(0, 0)) +} + +// SetWithExpire stores any representation of a response for a given key and given expires. +// The expires time will round to second. +func (a *ARC[K, V]) SetWithExpire(key K, value V, expires time.Time) { + a.mutex.Lock() + defer a.mutex.Unlock() + + a.setWithExpire(key, value, expires) +} + +func (a *ARC[K, V]) setWithExpire(key K, value V, expires time.Time) { + ent, ok := a.cache[key] + if !ok { + a.len++ + ent := &entry[K, V]{key: key, value: value, ghost: false, expires: expires.Unix()} + a.req(ent) + a.cache[key] = ent + return + } + + if ent.ghost { + a.len++ + } + + ent.value = value + ent.ghost = false + ent.expires = expires.Unix() + a.req(ent) +} + +// Get retrieves a previously via Set inserted entry. +// This optimizes future access to this entry (side effect). +func (a *ARC[K, V]) Get(key K) (value V, ok bool) { + a.mutex.Lock() + defer a.mutex.Unlock() + + ent, ok := a.get(key) + if !ok { + return lo.Empty[V](), false + } + return ent.value, true +} + +func (a *ARC[K, V]) get(key K) (e *entry[K, V], ok bool) { + ent, ok := a.cache[key] + if !ok { + return ent, false + } + a.req(ent) + return ent, !ent.ghost +} + +// GetWithExpire returns any representation of a cached response, +// a time.Time Give expected expires, +// and a bool set to true if the key was found. +// This method will NOT update the expires. +func (a *ARC[K, V]) GetWithExpire(key K) (V, time.Time, bool) { + a.mutex.Lock() + defer a.mutex.Unlock() + + ent, ok := a.get(key) + if !ok { + return lo.Empty[V](), time.Time{}, false + } + + return ent.value, time.Unix(ent.expires, 0), true +} + +// Len determines the number of currently cached entries. +// This method is side-effect free in the sense that it does not attempt to optimize random cache access. +func (a *ARC[K, V]) Len() int { + a.mutex.Lock() + defer a.mutex.Unlock() + + return a.len +} + +func (a *ARC[K, V]) req(ent *entry[K, V]) { + switch { + case ent.ll == a.t1 || ent.ll == a.t2: + // Case I + ent.setMRU(a.t2) + case ent.ll == a.b1: + // Case II + // Cache Miss in t1 and t2 + + // Adaptation + var d int + if a.b1.Len() >= a.b2.Len() { + d = 1 + } else { + d = a.b2.Len() / a.b1.Len() + } + a.p = min(a.p+d, a.c) + + a.replace(ent) + ent.setMRU(a.t2) + case ent.ll == a.b2: + // Case III + // Cache Miss in t1 and t2 + + // Adaptation + var d int + if a.b2.Len() >= a.b1.Len() { + d = 1 + } else { + d = a.b1.Len() / a.b2.Len() + } + a.p = max(a.p-d, 0) + + a.replace(ent) + ent.setMRU(a.t2) + case ent.ll == nil && a.t1.Len()+a.b1.Len() == a.c: + // Case IV A + if a.t1.Len() < a.c { + a.delLRU(a.b1) + a.replace(ent) + } else { + a.delLRU(a.t1) + } + ent.setMRU(a.t1) + case ent.ll == nil && a.t1.Len()+a.b1.Len() < a.c: + // Case IV B + if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() >= a.c { + if a.t1.Len()+a.t2.Len()+a.b1.Len()+a.b2.Len() == 2*a.c { + a.delLRU(a.b2) + } + a.replace(ent) + } + ent.setMRU(a.t1) + case ent.ll == nil: + // Case IV, not A nor B + ent.setMRU(a.t1) + } +} + +func (a *ARC[K, V]) delLRU(list *list.List[*entry[K, V]]) { + lru := list.Back() + list.Remove(lru) + a.len-- + delete(a.cache, lru.Value.key) +} + +func (a *ARC[K, V]) replace(ent *entry[K, V]) { + if a.t1.Len() > 0 && ((a.t1.Len() > a.p) || (ent.ll == a.b2 && a.t1.Len() == a.p)) { + lru := a.t1.Back().Value + lru.value = lo.Empty[V]() + lru.ghost = true + a.len-- + lru.setMRU(a.b1) + } else { + lru := a.t2.Back().Value + lru.value = lo.Empty[V]() + lru.ghost = true + a.len-- + lru.setMRU(a.b2) + } +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a int, b int) int { + if a < b { + return b + } + return a +} diff --git a/common/arc/arc_test.go b/common/arc/arc_test.go new file mode 100644 index 0000000000..a9d8a0c1bc --- /dev/null +++ b/common/arc/arc_test.go @@ -0,0 +1,105 @@ +package arc + +import ( + "testing" +) + +func TestInsertion(t *testing.T) { + cache := New[string, string](WithSize[string, string](3)) + if got, want := cache.Len(), 0; got != want { + t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) + } + + const ( + k1 = "Hello" + k2 = "Hallo" + k3 = "Ciao" + k4 = "Salut" + + v1 = "World" + v2 = "Worlds" + v3 = "Welt" + ) + + // Insert the first value + cache.Set(k1, v1) + if got, want := cache.Len(), 1; got != want { + t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) + } + if got, ok := cache.Get(k1); !ok || got != v1 { + t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v1) + } + + // Replace existing value for a given key + cache.Set(k1, v2) + if got, want := cache.Len(), 1; got != want { + t.Errorf("re-insertion: cache.Len(): got %d want %d", cache.Len(), want) + } + if got, ok := cache.Get(k1); !ok || got != v2 { + t.Errorf("re-insertion: cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) + } + + // Add a second different key + cache.Set(k2, v3) + if got, want := cache.Len(), 2; got != want { + t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) + } + if got, ok := cache.Get(k1); !ok || got != v2 { + t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k1, got, ok, v2) + } + if got, ok := cache.Get(k2); !ok || got != v3 { + t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", k2, got, ok, v3) + } + + // Fill cache + cache.Set(k3, v1) + if got, want := cache.Len(), 3; got != want { + t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) + } + + // Exceed size, this should not exceed size: + cache.Set(k4, v1) + if got, want := cache.Len(), 3; got != want { + t.Errorf("insertion of key out of size: cache.Len(): got %d want %d", cache.Len(), want) + } +} + +func TestEviction(t *testing.T) { + size := 3 + cache := New[string, string](WithSize[string, string](size)) + if got, want := cache.Len(), 0; got != want { + t.Errorf("empty cache.Len(): got %d want %d", cache.Len(), want) + } + + tests := []struct { + k, v string + }{ + {"k1", "v1"}, + {"k2", "v2"}, + {"k3", "v3"}, + {"k4", "v4"}, + } + for i, tt := range tests[:size] { + cache.Set(tt.k, tt.v) + if got, want := cache.Len(), i+1; got != want { + t.Errorf("insertion of key #%d: cache.Len(): got %d want %d", want, cache.Len(), want) + } + } + + // Exceed size and check we don't outgrow it: + cache.Set(tests[size].k, tests[size].v) + if got := cache.Len(); got != size { + t.Errorf("insertion of overflow key #%d: cache.Len(): got %d want %d", 4, cache.Len(), size) + } + + // Check that LRU got evicted: + if got, ok := cache.Get(tests[0].k); ok || got != "" { + t.Errorf("cache.Get(%v): got (%v,%t) want (,true)", tests[0].k, got, ok) + } + + for _, tt := range tests[1:] { + if got, ok := cache.Get(tt.k); !ok || got != tt.v { + t.Errorf("cache.Get(%v): got (%v,%t) want (%v,true)", tt.k, got, ok, tt.v) + } + } +} diff --git a/common/arc/entry.go b/common/arc/entry.go new file mode 100644 index 0000000000..679cbc1c3c --- /dev/null +++ b/common/arc/entry.go @@ -0,0 +1,32 @@ +package arc + +import ( + list "github.com/bahlo/generic-list-go" +) + +type entry[K comparable, V any] struct { + key K + value V + ll *list.List[*entry[K, V]] + el *list.Element[*entry[K, V]] + ghost bool + expires int64 +} + +func (e *entry[K, V]) setLRU(list *list.List[*entry[K, V]]) { + e.detach() + e.ll = list + e.el = e.ll.PushBack(e) +} + +func (e *entry[K, V]) setMRU(list *list.List[*entry[K, V]]) { + e.detach() + e.ll = list + e.el = e.ll.PushFront(e) +} + +func (e *entry[K, V]) detach() { + if e.ll != nil { + e.ll.Remove(e.el) + } +} diff --git a/common/cache/lrucache.go b/common/lru/lrucache.go similarity index 99% rename from common/cache/lrucache.go rename to common/lru/lrucache.go index f52b8c41c5..024315c051 100644 --- a/common/cache/lrucache.go +++ b/common/lru/lrucache.go @@ -1,4 +1,4 @@ -package cache +package lru // Modified by https://github.com/die-net/lrucache diff --git a/common/cache/lrucache_test.go b/common/lru/lrucache_test.go similarity index 99% rename from common/cache/lrucache_test.go rename to common/lru/lrucache_test.go index 4cbc1ff8a1..340b3da3df 100644 --- a/common/cache/lrucache_test.go +++ b/common/lru/lrucache_test.go @@ -1,4 +1,4 @@ -package cache +package lru import ( "testing" diff --git a/component/fakeip/memory.go b/component/fakeip/memory.go index 38967658a3..0a8492f8aa 100644 --- a/component/fakeip/memory.go +++ b/component/fakeip/memory.go @@ -3,12 +3,12 @@ package fakeip import ( "net/netip" - "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/lru" ) type memoryStore struct { - cacheIP *cache.LruCache[string, netip.Addr] - cacheHost *cache.LruCache[netip.Addr, string] + cacheIP *lru.LruCache[string, netip.Addr] + cacheHost *lru.LruCache[netip.Addr, string] } // GetByHost implements store.GetByHost @@ -74,7 +74,7 @@ func (m *memoryStore) FlushFakeIP() error { func newMemoryStore(size int) *memoryStore { return &memoryStore{ - cacheIP: cache.New[string, netip.Addr](cache.WithSize[string, netip.Addr](size)), - cacheHost: cache.New[netip.Addr, string](cache.WithSize[netip.Addr, string](size)), + cacheIP: lru.New[string, netip.Addr](lru.WithSize[string, netip.Addr](size)), + cacheHost: lru.New[netip.Addr, string](lru.WithSize[netip.Addr, string](size)), } } diff --git a/component/sniffer/dispatcher.go b/component/sniffer/dispatcher.go index 983af3ec9c..315532285b 100644 --- a/component/sniffer/dispatcher.go +++ b/component/sniffer/dispatcher.go @@ -6,7 +6,7 @@ import ( "net/netip" "time" - lru "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/constant/sniffer" diff --git a/config/config.go b/config/config.go index 36333cfb29..f2b0084498 100644 --- a/config/config.go +++ b/config/config.go @@ -110,6 +110,7 @@ type DNS struct { Listen string EnhancedMode C.DNSMode DefaultNameserver []dns.NameServer + CacheAlgorithm string FakeIPRange *fakeip.Pool Hosts *trie.DomainTrie[netip.Addr] NameServerPolicy []dns.Policy @@ -165,6 +166,7 @@ type RawDNS struct { FakeIPFilter []string `yaml:"fake-ip-filter" json:"fake-ip-filter"` FakeIPFilterMode C.FilterMode `yaml:"fake-ip-filter-mode" json:"fake-ip-filter-mode"` DefaultNameserver []string `yaml:"default-nameserver" json:"default-nameserver"` + CacheAlgorithm string `yaml:"cache-algorithm" json:"cache-algorithm"` NameServerPolicy *orderedmap.OrderedMap[string, any] `yaml:"nameserver-policy" json:"nameserver-policy"` ProxyServerNameserver []string `yaml:"proxy-server-nameserver" json:"proxy-server-nameserver"` SearchDomains []string `yaml:"search-domains" json:"search-domains"` @@ -1215,6 +1217,12 @@ func parseDNS(rawCfg *RawConfig, hosts *trie.DomainTrie[netip.Addr], ruleProvide dnsCfg.Hosts = hosts } + if cfg.CacheAlgorithm == "" || cfg.CacheAlgorithm == "lru" { + dnsCfg.CacheAlgorithm = "lru" + } else { + dnsCfg.CacheAlgorithm = "arc" + } + if len(cfg.SearchDomains) != 0 { for _, domain := range cfg.SearchDomains { if strings.HasPrefix(domain, ".") || strings.HasSuffix(domain, ".") { diff --git a/dns/enhancer.go b/dns/enhancer.go index fab8853b61..eca6eac2b3 100644 --- a/dns/enhancer.go +++ b/dns/enhancer.go @@ -3,7 +3,7 @@ package dns import ( "net/netip" - "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/component/fakeip" C "github.com/metacubex/mihomo/constant" ) @@ -11,7 +11,7 @@ import ( type ResolverEnhancer struct { mode C.DNSMode fakePool *fakeip.Pool - mapping *cache.LruCache[netip.Addr, string] + mapping *lru.LruCache[netip.Addr, string] } func (h *ResolverEnhancer) FakeIPEnabled() bool { @@ -92,11 +92,11 @@ func (h *ResolverEnhancer) StoreFakePoolState() { func NewEnhancer(cfg Config) *ResolverEnhancer { var fakePool *fakeip.Pool - var mapping *cache.LruCache[netip.Addr, string] + var mapping *lru.LruCache[netip.Addr, string] if cfg.EnhancedMode != C.DNSNormal { fakePool = cfg.Pool - mapping = cache.New[netip.Addr, string](cache.WithSize[netip.Addr, string](4096)) + mapping = lru.New[netip.Addr, string](lru.WithSize[netip.Addr, string](4096)) } return &ResolverEnhancer{ diff --git a/dns/middleware.go b/dns/middleware.go index 77bc393696..6524935747 100644 --- a/dns/middleware.go +++ b/dns/middleware.go @@ -5,7 +5,7 @@ import ( "strings" "time" - "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/trie" C "github.com/metacubex/mihomo/constant" @@ -63,7 +63,7 @@ func withHosts(hosts *trie.DomainTrie[netip.Addr]) middleware { } } -func withMapping(mapping *cache.LruCache[netip.Addr, string]) middleware { +func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware { return func(next handler) handler { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { q := r.Question[0] diff --git a/dns/resolver.go b/dns/resolver.go index 5ccb7168d4..d4ae991745 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -8,7 +8,8 @@ import ( "strings" "time" - lru "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/arc" + "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/mihomo/common/singleflight" "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/resolver" @@ -28,6 +29,12 @@ type dnsClient interface { ResetConnection() } +type dnsCache interface { + GetWithExpire(key string) (*D.Msg, time.Time, bool) + SetWithExpire(key string, value *D.Msg, expire time.Time) + Clear() +} + type result struct { Msg *D.Msg Error error @@ -41,7 +48,7 @@ type Resolver struct { fallbackDomainFilters []C.DomainMatcher fallbackIPFilters []C.IpMatcher group singleflight.Group[*D.Msg] - lruCache *lru.LruCache[string, *D.Msg] + cache dnsCache policy []dnsPolicy searchDomains []string defaultResolver *Resolver @@ -145,7 +152,7 @@ func (r *Resolver) ExchangeContext(ctx context.Context, m *D.Msg) (msg *D.Msg, e q := m.Question[0] domain := msgToDomain(m) _, qTypeStr := msgToQtype(m) - cacheM, expireTime, hit := r.lruCache.GetWithExpire(q.String()) + cacheM, expireTime, hit := r.cache.GetWithExpire(q.String()) if hit { ips := msgToIP(cacheM) log.Debugln("[DNS] cache hit %s --> %s %s, expire at %s", domain, ips, qTypeStr, expireTime.Format("2006-01-02 15:04:05")) @@ -189,7 +196,7 @@ func (r *Resolver) exchangeWithoutCache(ctx context.Context, m *D.Msg) (msg *D.M msg.Extra = lo.Filter(msg.Extra, func(rr D.RR, index int) bool { return rr.Header().Rrtype != D.TypeOPT }) - putMsgToCache(r.lruCache, q.String(), q, msg) + putMsgToCache(r.cache, q.String(), q, msg) } }() @@ -395,8 +402,8 @@ func (r *Resolver) Invalid() bool { } func (r *Resolver) ClearCache() { - if r != nil && r.lruCache != nil { - r.lruCache.Clear() + if r != nil && r.cache != nil { + r.cache.Clear() } } @@ -423,10 +430,6 @@ type NameServer struct { Params map[string]string } -func (config Config) newCache() *lru.LruCache[string, *D.Msg] { - return lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true)) -} - func (ns NameServer) Equal(ns2 NameServer) bool { defer func() { // C.ProxyAdapter compare maybe panic, just ignore @@ -463,12 +466,21 @@ type Config struct { Tunnel provider.Tunnel RuleProviders map[string]provider.RuleProvider SearchDomains []string + CacheAlgorithm string +} + +func (config Config) newCache() dnsCache { + if config.CacheAlgorithm == "" || config.CacheAlgorithm == "lru" { + return lru.New(lru.WithSize[string, *D.Msg](4096), lru.WithStale[string, *D.Msg](true)) + } else { + return arc.New(arc.WithSize[string, *D.Msg](4096)) + } } func NewResolver(config Config) (r *Resolver, pr *Resolver) { defaultResolver := &Resolver{ - main: transform(config.Default, nil), - lruCache: config.newCache(), + main: transform(config.Default, nil), + cache: config.newCache(), } var nameServerCache []struct { @@ -501,7 +513,7 @@ func NewResolver(config Config) (r *Resolver, pr *Resolver) { r = &Resolver{ ipv6: config.IPv6, main: cacheTransform(config.Main), - lruCache: config.newCache(), + cache: config.newCache(), hosts: config.Hosts, searchDomains: config.SearchDomains, } @@ -509,10 +521,10 @@ func NewResolver(config Config) (r *Resolver, pr *Resolver) { if len(config.ProxyServer) != 0 { pr = &Resolver{ - ipv6: config.IPv6, - main: cacheTransform(config.ProxyServer), - lruCache: config.newCache(), - hosts: config.Hosts, + ipv6: config.IPv6, + main: cacheTransform(config.ProxyServer), + cache: config.newCache(), + hosts: config.Hosts, searchDomains: config.SearchDomains, } } diff --git a/dns/util.go b/dns/util.go index cc700af933..4d53eb85e8 100644 --- a/dns/util.go +++ b/dns/util.go @@ -10,7 +10,6 @@ import ( "strings" "time" - "github.com/metacubex/mihomo/common/cache" "github.com/metacubex/mihomo/common/picker" "github.com/metacubex/mihomo/component/dialer" "github.com/metacubex/mihomo/component/resolver" @@ -42,7 +41,7 @@ func updateTTL(records []D.RR, ttl uint32) { } } -func putMsgToCache(c *cache.LruCache[string, *D.Msg], key string, q D.Question, msg *D.Msg) { +func putMsgToCache(c dnsCache, key string, q D.Question, msg *D.Msg) { // skip dns cache for acme challenge if q.Qtype == D.TypeTXT && strings.HasPrefix(q.Name, "_acme-challenge.") { log.Debugln("[DNS] dns cache ignored because of acme challenge for: %s", q.Name) diff --git a/hub/executor/executor.go b/hub/executor/executor.go index ba9cd2a72f..3fd6851ef5 100644 --- a/hub/executor/executor.go +++ b/hub/executor/executor.go @@ -226,6 +226,7 @@ func updateDNS(c *config.DNS, ruleProvider map[string]providerTypes.RuleProvider Tunnel: tunnel.Tunnel, RuleProviders: ruleProvider, SearchDomains: c.SearchDomains, + CacheAlgorithm: c.CacheAlgorithm, } r, pr := dns.NewResolver(cfg) diff --git a/transport/tuic/v5/frag.go b/transport/tuic/v5/frag.go index 523f0d72f0..d680a5d01f 100644 --- a/transport/tuic/v5/frag.go +++ b/transport/tuic/v5/frag.go @@ -4,7 +4,7 @@ import ( "bytes" "sync" - "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/lru" "github.com/metacubex/quic-go" ) @@ -49,7 +49,7 @@ func fragWriteNative(quicConn quic.Connection, packet Packet, buf *bytes.Buffer, } type deFragger struct { - lru *cache.LruCache[uint16, *packetBag] + lru *lru.LruCache[uint16, *packetBag] once sync.Once } @@ -65,9 +65,9 @@ func newPacketBag() *packetBag { func (d *deFragger) init() { if d.lru == nil { - d.lru = cache.New( - cache.WithAge[uint16, *packetBag](10), - cache.WithUpdateAgeOnGet[uint16, *packetBag](), + d.lru = lru.New( + lru.WithAge[uint16, *packetBag](10), + lru.WithUpdateAgeOnGet[uint16, *packetBag](), ) } } diff --git a/tunnel/connection.go b/tunnel/connection.go index ad3ce94d28..fe8dad79d3 100644 --- a/tunnel/connection.go +++ b/tunnel/connection.go @@ -7,7 +7,7 @@ import ( "net/netip" "time" - lru "github.com/metacubex/mihomo/common/cache" + "github.com/metacubex/mihomo/common/lru" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/resolver" C "github.com/metacubex/mihomo/constant"