feat: support fake-ip-range6 in dns module

This commit is contained in:
wwqgtxx 2025-10-28 11:40:00 +08:00
parent ff62386f6b
commit c8af92a01f
13 changed files with 234 additions and 153 deletions

View File

@ -50,6 +50,10 @@ func (c *cachefileStore) FlushFakeIP() error {
return c.cache.FlushFakeIP()
}
func newCachefileStore(cache *cachefile.CacheFile) *cachefileStore {
func newCachefileStore(cache *cachefile.CacheFile, prefix netip.Prefix) *cachefileStore {
if prefix.Addr().Is6() {
return &cachefileStore{cache.FakeIpStore6()}
} else {
return &cachefileStore{cache.FakeIpStore()}
}
}

View File

@ -7,7 +7,6 @@ import (
"sync"
"github.com/metacubex/mihomo/component/profile/cachefile"
C "github.com/metacubex/mihomo/constant"
"go4.org/netipx"
)
@ -36,8 +35,6 @@ type Pool struct {
offset netip.Addr
cycle bool
mux sync.Mutex
host []C.DomainMatcher
mode C.FilterMode
ipnet netip.Prefix
store store
}
@ -66,24 +63,6 @@ func (p *Pool) LookBack(ip netip.Addr) (string, bool) {
return p.store.GetByIP(ip)
}
// ShouldSkipped return if domain should be skipped
func (p *Pool) ShouldSkipped(domain string) bool {
should := p.shouldSkipped(domain)
if p.mode == C.FilterWhiteList {
return !should
}
return should
}
func (p *Pool) shouldSkipped(domain string) bool {
for _, matcher := range p.host {
if matcher.MatchDomain(domain) {
return true
}
}
return false
}
// Exist returns if given ip exists in fake-ip pool
func (p *Pool) Exist(ip netip.Addr) bool {
p.mux.Lock()
@ -166,8 +145,6 @@ func (p *Pool) restoreState() {
type Options struct {
IPNet netip.Prefix
Host []C.DomainMatcher
Mode C.FilterMode
// Size sets the maximum number of entries in memory
// and does not work if Persistence is true
@ -197,12 +174,10 @@ func New(options Options) (*Pool, error) {
last: last,
offset: first.Prev(),
cycle: false,
host: options.Host,
mode: options.Mode,
ipnet: options.IPNet,
}
if options.Persistence {
pool.store = newCachefileStore(cachefile.Cache())
pool.store = newCachefileStore(cachefile.Cache(), options.IPNet)
} else {
pool.store = newMemoryStore(options.Size)
}

View File

@ -8,8 +8,6 @@ import (
"time"
"github.com/metacubex/mihomo/component/profile/cachefile"
"github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/bbolt"
"github.com/stretchr/testify/assert"
@ -43,7 +41,7 @@ func createCachefileStore(options Options) (*Pool, string, error) {
return nil, "", err
}
pool.store = newCachefileStore(&cachefile.CacheFile{DB: db})
pool.store = newCachefileStore(&cachefile.CacheFile{DB: db}, options.IPNet)
return pool, f.Name(), nil
}
@ -146,47 +144,6 @@ func TestPool_CycleUsed(t *testing.T) {
}
}
func TestPool_Skip(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/29")
tree := trie.New[struct{}]()
assert.NoError(t, tree.Insert("example.com", struct{}{}))
assert.False(t, tree.IsEmpty())
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
Host: []C.DomainMatcher{tree.NewDomainSet()},
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
assert.True(t, pool.ShouldSkipped("example.com"))
assert.False(t, pool.ShouldSkipped("foo.com"))
assert.False(t, pool.shouldSkipped("baz.com"))
}
}
func TestPool_SkipWhiteList(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/29")
tree := trie.New[struct{}]()
assert.NoError(t, tree.Insert("example.com", struct{}{}))
assert.False(t, tree.IsEmpty())
pools, tempfile, err := createPools(Options{
IPNet: ipnet,
Size: 10,
Host: []C.DomainMatcher{tree.NewDomainSet()},
Mode: C.FilterWhiteList,
})
assert.Nil(t, err)
defer os.Remove(tempfile)
for _, pool := range pools {
assert.False(t, pool.ShouldSkipped("example.com"))
assert.True(t, pool.ShouldSkipped("foo.com"))
assert.True(t, pool.ShouldSkipped("baz.com"))
}
}
func TestPool_MaxCacheSize(t *testing.T) {
ipnet := netip.MustParsePrefix("192.168.0.1/24")
pool, _ := New(Options{

View File

@ -0,0 +1,28 @@
package fakeip
import (
C "github.com/metacubex/mihomo/constant"
)
type Skipper struct {
Host []C.DomainMatcher
Mode C.FilterMode
}
// ShouldSkipped return if domain should be skipped
func (p *Skipper) ShouldSkipped(domain string) bool {
should := p.shouldSkipped(domain)
if p.Mode == C.FilterWhiteList {
return !should
}
return should
}
func (p *Skipper) shouldSkipped(domain string) bool {
for _, matcher := range p.Host {
if matcher.MatchDomain(domain) {
return true
}
}
return false
}

View File

@ -0,0 +1,35 @@
package fakeip
import (
"testing"
"github.com/metacubex/mihomo/component/trie"
C "github.com/metacubex/mihomo/constant"
"github.com/stretchr/testify/assert"
)
func TestSkipper_BlackList(t *testing.T) {
tree := trie.New[struct{}]()
assert.NoError(t, tree.Insert("example.com", struct{}{}))
assert.False(t, tree.IsEmpty())
skipper := &Skipper{
Host: []C.DomainMatcher{tree.NewDomainSet()},
}
assert.True(t, skipper.ShouldSkipped("example.com"))
assert.False(t, skipper.ShouldSkipped("foo.com"))
assert.False(t, skipper.shouldSkipped("baz.com"))
}
func TestSkipper_WhiteList(t *testing.T) {
tree := trie.New[struct{}]()
assert.NoError(t, tree.Insert("example.com", struct{}{}))
assert.False(t, tree.IsEmpty())
skipper := &Skipper{
Host: []C.DomainMatcher{tree.NewDomainSet()},
Mode: C.FilterWhiteList,
}
assert.False(t, skipper.ShouldSkipped("example.com"))
assert.True(t, skipper.ShouldSkipped("foo.com"))
assert.True(t, skipper.ShouldSkipped("baz.com"))
}

View File

@ -19,6 +19,7 @@ var (
bucketSelected = []byte("selected")
bucketFakeip = []byte("fakeip")
bucketFakeip6 = []byte("fakeip6")
bucketETag = []byte("etag")
bucketSubscriptionInfo = []byte("subscriptioninfo")
)

View File

@ -10,10 +10,15 @@ import (
type FakeIpStore struct {
*CacheFile
bucketName []byte
}
func (c *CacheFile) FakeIpStore() *FakeIpStore {
return &FakeIpStore{c}
return &FakeIpStore{c, bucketFakeip}
}
func (c *CacheFile) FakeIpStore6() *FakeIpStore {
return &FakeIpStore{c, bucketFakeip6}
}
func (c *FakeIpStore) GetByHost(host string) (ip netip.Addr, exist bool) {
@ -21,7 +26,7 @@ func (c *FakeIpStore) GetByHost(host string) (ip netip.Addr, exist bool) {
return
}
c.DB.View(func(t *bbolt.Tx) error {
if bucket := t.Bucket(bucketFakeip); bucket != nil {
if bucket := t.Bucket(c.bucketName); bucket != nil {
if v := bucket.Get([]byte(host)); v != nil {
ip, exist = netip.AddrFromSlice(v)
}
@ -36,7 +41,7 @@ func (c *FakeIpStore) PutByHost(host string, ip netip.Addr) {
return
}
err := c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := t.CreateBucketIfNotExists(bucketFakeip)
bucket, err := t.CreateBucketIfNotExists(c.bucketName)
if err != nil {
return err
}
@ -52,7 +57,7 @@ func (c *FakeIpStore) GetByIP(ip netip.Addr) (host string, exist bool) {
return
}
c.DB.View(func(t *bbolt.Tx) error {
if bucket := t.Bucket(bucketFakeip); bucket != nil {
if bucket := t.Bucket(c.bucketName); bucket != nil {
if v := bucket.Get(ip.AsSlice()); v != nil {
host, exist = string(v), true
}
@ -67,7 +72,7 @@ func (c *FakeIpStore) PutByIP(ip netip.Addr, host string) {
return
}
err := c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := t.CreateBucketIfNotExists(bucketFakeip)
bucket, err := t.CreateBucketIfNotExists(c.bucketName)
if err != nil {
return err
}
@ -85,7 +90,7 @@ func (c *FakeIpStore) DelByIP(ip netip.Addr) {
addr := ip.AsSlice()
err := c.DB.Batch(func(t *bbolt.Tx) error {
bucket, err := t.CreateBucketIfNotExists(bucketFakeip)
bucket, err := t.CreateBucketIfNotExists(c.bucketName)
if err != nil {
return err
}
@ -105,11 +110,11 @@ func (c *FakeIpStore) DelByIP(ip netip.Addr) {
func (c *FakeIpStore) FlushFakeIP() error {
err := c.DB.Batch(func(t *bbolt.Tx) error {
bucket := t.Bucket(bucketFakeip)
bucket := t.Bucket(c.bucketName)
if bucket == nil {
return nil
}
return t.DeleteBucket(bucketFakeip)
return t.DeleteBucket(c.bucketName)
})
return err
}

View File

@ -157,7 +157,11 @@ type DNS struct {
DefaultNameserver []dns.NameServer
CacheAlgorithm string
CacheMaxSize int
FakeIPRange *fakeip.Pool
FakeIPRange netip.Prefix
FakeIPPool *fakeip.Pool
FakeIPRange6 netip.Prefix
FakeIPPool6 *fakeip.Pool
FakeIPSkipper *fakeip.Skipper
NameServerPolicy []dns.Policy
ProxyServerNameserver []dns.NameServer
DirectNameServer []dns.NameServer
@ -221,6 +225,7 @@ type RawDNS struct {
Listen string `yaml:"listen" json:"listen"`
EnhancedMode C.DNSMode `yaml:"enhanced-mode" json:"enhanced-mode"`
FakeIPRange string `yaml:"fake-ip-range" json:"fake-ip-range"`
FakeIPRange6 string `yaml:"fake-ip-range6" json:"fake-ip-range6"`
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"`
@ -686,7 +691,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) {
}
config.DNS = dnsCfg
err = parseTun(rawCfg.Tun, config.General)
err = parseTun(rawCfg.Tun, dnsCfg, config.General)
if err != nil {
return nil, err
}
@ -1407,13 +1412,21 @@ func parseDNS(rawCfg *RawConfig, ruleProviders map[string]providerTypes.RuleProv
}
}
fakeIPRange, err := netip.ParsePrefix(cfg.FakeIPRange)
T.SetFakeIPRange(fakeIPRange)
if cfg.EnhancedMode == C.DNSFakeIP {
if cfg.FakeIPRange != "" {
dnsCfg.FakeIPRange, err = netip.ParsePrefix(cfg.FakeIPRange)
if err != nil {
return nil, err
}
}
if cfg.FakeIPRange6 != "" {
dnsCfg.FakeIPRange6, err = netip.ParsePrefix(cfg.FakeIPRange6)
if err != nil {
return nil, err
}
}
if cfg.EnhancedMode == C.DNSFakeIP {
var fakeIPTrie *trie.DomainTrie[struct{}]
if len(dnsCfg.Fallback) != 0 {
fakeIPTrie = trie.New[struct{}]()
@ -1431,18 +1444,39 @@ func parseDNS(rawCfg *RawConfig, ruleProviders map[string]providerTypes.RuleProv
return nil, err
}
pool, err := fakeip.New(fakeip.Options{
IPNet: fakeIPRange,
Size: 1000,
skipper := &fakeip.Skipper{
Host: host,
Mode: cfg.FakeIPFilterMode,
}
dnsCfg.FakeIPSkipper = skipper
if dnsCfg.FakeIPRange.IsValid() {
pool, err := fakeip.New(fakeip.Options{
IPNet: dnsCfg.FakeIPRange,
Size: 1000,
Persistence: rawCfg.Profile.StoreFakeIP,
})
if err != nil {
return nil, err
}
dnsCfg.FakeIPPool = pool
}
dnsCfg.FakeIPRange = pool
if dnsCfg.FakeIPRange6.IsValid() {
pool6, err := fakeip.New(fakeip.Options{
IPNet: dnsCfg.FakeIPRange6,
Size: 1000,
Persistence: rawCfg.Profile.StoreFakeIP,
})
if err != nil {
return nil, err
}
dnsCfg.FakeIPPool6 = pool6
}
if dnsCfg.FakeIPPool == nil && dnsCfg.FakeIPPool6 == nil {
return nil, errors.New("disallow `fake-ip-range` and `fake-ip-range6` both empty with fake-ip mode")
}
}
if len(cfg.Fallback) != 0 {
@ -1504,9 +1538,9 @@ func parseAuthentication(rawRecords []string) []auth.AuthUser {
return users
}
func parseTun(rawTun RawTun, general *General) error {
tunAddressPrefix := T.FakeIPRange()
if !tunAddressPrefix.IsValid() {
func parseTun(rawTun RawTun, dns *DNS, general *General) error {
tunAddressPrefix := dns.FakeIPRange
if !tunAddressPrefix.IsValid() || !tunAddressPrefix.Addr().Is4() {
tunAddressPrefix = netip.MustParsePrefix("198.18.0.1/16")
}
tunAddressPrefix = netip.PrefixFrom(tunAddressPrefix.Addr(), 30)

View File

@ -10,7 +10,9 @@ import (
type ResolverEnhancer struct {
mode C.DNSMode
fakePool *fakeip.Pool
fakeIPPool *fakeip.Pool
fakeIPPool6 *fakeip.Pool
fakeIPSkipper *fakeip.Skipper
mapping *lru.LruCache[netip.Addr, string]
useHosts bool
}
@ -28,10 +30,14 @@ func (h *ResolverEnhancer) IsExistFakeIP(ip netip.Addr) bool {
return false
}
if pool := h.fakePool; pool != nil {
if pool := h.fakeIPPool; pool != nil {
return pool.Exist(ip)
}
if pool6 := h.fakeIPPool6; pool6 != nil {
return pool6.Exist(ip)
}
return false
}
@ -40,10 +46,14 @@ func (h *ResolverEnhancer) IsFakeIP(ip netip.Addr) bool {
return false
}
if pool := h.fakePool; pool != nil {
if pool := h.fakeIPPool; pool != nil {
return pool.IPNet().Contains(ip) && ip != pool.Gateway() && ip != pool.Broadcast()
}
if pool6 := h.fakeIPPool6; pool6 != nil {
return pool6.IPNet().Contains(ip) && ip != pool6.Gateway() && ip != pool6.Broadcast()
}
return false
}
@ -52,20 +62,30 @@ func (h *ResolverEnhancer) IsFakeBroadcastIP(ip netip.Addr) bool {
return false
}
if pool := h.fakePool; pool != nil {
if pool := h.fakeIPPool; pool != nil {
return pool.Broadcast() == ip
}
if pool6 := h.fakeIPPool6; pool6 != nil {
return pool6.Broadcast() == ip
}
return false
}
func (h *ResolverEnhancer) FindHostByIP(ip netip.Addr) (string, bool) {
if pool := h.fakePool; pool != nil {
if pool := h.fakeIPPool; pool != nil {
if host, existed := pool.LookBack(ip); existed {
return host, true
}
}
if pool6 := h.fakeIPPool6; pool6 != nil {
if host, existed := pool6.LookBack(ip); existed {
return host, true
}
}
if mapping := h.mapping; mapping != nil {
if host, existed := h.mapping.Get(ip); existed {
return host, true
@ -82,9 +102,12 @@ func (h *ResolverEnhancer) InsertHostByIP(ip netip.Addr, host string) {
}
func (h *ResolverEnhancer) FlushFakeIP() error {
if pool := h.fakePool; pool != nil {
if pool := h.fakeIPPool; pool != nil {
return pool.FlushFakeIP()
}
if pool6 := h.fakeIPPool6; pool6 != nil {
return pool6.FlushFakeIP()
}
return nil
}
@ -93,36 +116,48 @@ func (h *ResolverEnhancer) PatchFrom(o *ResolverEnhancer) {
o.mapping.CloneTo(h.mapping)
}
if h.fakePool != nil && o.fakePool != nil {
h.fakePool.CloneFrom(o.fakePool)
if h.fakeIPPool != nil && o.fakeIPPool != nil {
h.fakeIPPool.CloneFrom(o.fakeIPPool)
}
if h.fakeIPPool6 != nil && o.fakeIPPool6 != nil {
h.fakeIPPool6.CloneFrom(o.fakeIPPool6)
}
}
func (h *ResolverEnhancer) StoreFakePoolState() {
if h.fakePool != nil {
h.fakePool.StoreState()
if h.fakeIPPool != nil {
h.fakeIPPool.StoreState()
}
if h.fakeIPPool6 != nil {
h.fakeIPPool6.StoreState()
}
}
type EnhancerConfig struct {
IPv6 bool
EnhancedMode C.DNSMode
Pool *fakeip.Pool
FakeIPPool *fakeip.Pool
FakeIPPool6 *fakeip.Pool
FakeIPSkipper *fakeip.Skipper
UseHosts bool
}
func NewEnhancer(cfg EnhancerConfig) *ResolverEnhancer {
var fakePool *fakeip.Pool
var mapping *lru.LruCache[netip.Addr, string]
if cfg.EnhancedMode != C.DNSNormal {
fakePool = cfg.Pool
mapping = lru.New(lru.WithSize[netip.Addr, string](4096))
}
return &ResolverEnhancer{
e := &ResolverEnhancer{
mode: cfg.EnhancedMode,
fakePool: fakePool,
mapping: mapping,
useHosts: cfg.UseHosts,
}
if cfg.EnhancedMode != C.DNSNormal {
e.fakeIPPool = cfg.FakeIPPool
if cfg.IPv6 {
e.fakeIPPool6 = cfg.FakeIPPool6
}
e.fakeIPSkipper = cfg.FakeIPSkipper
e.mapping = lru.New(lru.WithSize[netip.Addr, string](4096))
}
return e
}

View File

@ -67,8 +67,7 @@ func withHosts(mapping *lru.LruCache[netip.Addr, string]) middleware {
} else if q.Qtype == D.TypeAAAA {
rr := &D.AAAA{}
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 10}
ip := ipAddr.As16()
rr.AAAA = ip[:]
rr.AAAA = ipAddr.AsSlice()
msg.Answer = append(msg.Answer, rr)
if mapping != nil {
mapping.SetWithExpire(ipAddr, host, time.Now().Add(time.Second*10))
@ -147,29 +146,42 @@ func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware {
}
}
func withFakeIP(fakePool *fakeip.Pool) middleware {
func withFakeIP(skipper *fakeip.Skipper, fakePool *fakeip.Pool, fakePool6 *fakeip.Pool) middleware {
return func(next handler) handler {
return func(ctx *icontext.DNSContext, r *D.Msg) (*D.Msg, error) {
q := r.Question[0]
host := strings.TrimRight(q.Name, ".")
if fakePool.ShouldSkipped(host) {
if skipper.ShouldSkipped(host) {
return next(ctx, r)
}
var rr D.RR
switch q.Qtype {
case D.TypeAAAA, D.TypeSVCB, D.TypeHTTPS:
case D.TypeA:
if fakePool == nil {
return handleMsgWithEmptyAnswer(r), nil
}
if q.Qtype != D.TypeA {
ip := fakePool.Lookup(host)
rr = &D.A{
Hdr: D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL},
A: ip.AsSlice(),
}
case D.TypeAAAA:
if fakePool6 == nil {
return handleMsgWithEmptyAnswer(r), nil
}
ip := fakePool6.Lookup(host)
rr = &D.AAAA{
Hdr: D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: dnsDefaultTTL},
AAAA: ip.AsSlice(),
}
case D.TypeSVCB, D.TypeHTTPS:
return handleMsgWithEmptyAnswer(r), nil
default:
return next(ctx, r)
}
rr := &D.A{}
rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL}
ip := fakePool.Lookup(host)
rr.A = ip.AsSlice()
msg := r.Copy()
msg.Answer = []D.RR{rr}
@ -226,7 +238,7 @@ func newHandler(resolver *Resolver, mapper *ResolverEnhancer) handler {
}
if mapper.mode == C.DNSFakeIP {
middlewares = append(middlewares, withFakeIP(mapper.fakePool))
middlewares = append(middlewares, withFakeIP(mapper.fakeIPSkipper, mapper.fakeIPPool, mapper.fakeIPPool6))
}
if mapper.mode != C.DNSNormal {

View File

@ -261,6 +261,7 @@ dns:
enhanced-mode: fake-ip # or redir-host
fake-ip-range: 198.18.0.1/16 # fake-ip 池设置
# fake-ip-range6: fdfe:dcba:9876::1/64 # fake-ip6 池设置
# 配置不使用 fake-ip 的域名
fake-ip-filter:

View File

@ -247,10 +247,11 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
return
}
ipv6 := c.IPv6 && generalIPv6
r := dns.NewResolver(dns.Config{
Main: c.NameServer,
Fallback: c.Fallback,
IPv6: c.IPv6 && generalIPv6,
IPv6: ipv6,
IPv6Timeout: c.IPv6Timeout,
FallbackIPFilter: c.FallbackIPFilter,
FallbackDomainFilter: c.FallbackDomainFilter,
@ -263,8 +264,11 @@ func updateDNS(c *config.DNS, generalIPv6 bool) {
CacheMaxSize: c.CacheMaxSize,
})
m := dns.NewEnhancer(dns.EnhancerConfig{
IPv6: ipv6,
EnhancedMode: c.EnhancedMode,
Pool: c.FakeIPRange,
FakeIPPool: c.FakeIPPool,
FakeIPPool6: c.FakeIPPool6,
FakeIPSkipper: c.FakeIPSkipper,
UseHosts: c.UseHosts,
})

View File

@ -61,8 +61,6 @@ var (
findProcessMode = atomic.NewInt32Enum(P.FindProcessStrict)
fakeIPRange netip.Prefix
snifferDispatcher *sniffer.Dispatcher
sniffingEnable = false
@ -142,14 +140,6 @@ func Status() TunnelStatus {
return status.Load()
}
func SetFakeIPRange(p netip.Prefix) {
fakeIPRange = p
}
func FakeIPRange() netip.Prefix {
return fakeIPRange
}
func SetSniffing(b bool) {
if snifferDispatcher.Enable() {
configMux.Lock()
@ -563,7 +553,7 @@ func handleTCPConn(connCtx C.ConnContext) {
dialMetadata := metadata
if len(metadata.Host) > 0 {
if node, ok := resolver.DefaultHosts.Search(metadata.Host, false); ok {
if dstIp, _ := node.RandIP(); !FakeIPRange().Contains(dstIp) {
if dstIp, _ := node.RandIP(); !resolver.IsFakeIP(dstIp) {
dialMetadata.DstIP = dstIp
dialMetadata.DNSMode = C.DNSHosts
dialMetadata = dialMetadata.Pure()