From 1739283a27e9e19db211bc6a30a9ca095d4d5483 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sun, 16 Mar 2025 16:34:48 +0800 Subject: [PATCH 1/4] feat(dns): nameserver-policy add rule & add fakeip dns server 1. rule match, use network type TCP and domain to match rule, result is one of proxy, direct, reject 2. add fakeip dns server, use config in fake-ip-range --- config/config.go | 266 +++++++++++++++++--------------------- config/dns_policy.go | 60 +++++++++ config/dns_policy_rule.go | 72 +++++++++++ config/dns_policy_test.go | 262 +++++++++++++++++++++++++++++++++++++ dns/fakeip.go | 61 +++++++++ dns/middleware.go | 14 +- dns/resolver.go | 5 +- dns/util.go | 56 ++++---- docs/config.yaml | 45 ++++--- tunnel/dns_dialer.go | 5 +- tunnel/tunnel.go | 6 +- 11 files changed, 644 insertions(+), 208 deletions(-) create mode 100644 config/dns_policy.go create mode 100644 config/dns_policy_rule.go create mode 100644 config/dns_policy_test.go create mode 100644 dns/fakeip.go diff --git a/config/config.go b/config/config.go index ce1e0fb7..1317537c 100644 --- a/config/config.go +++ b/config/config.go @@ -261,7 +261,7 @@ type RawTun struct { MTU uint32 `yaml:"mtu" json:"mtu,omitempty"` GSO bool `yaml:"gso" json:"gso,omitempty"` GSOMaxSize uint32 `yaml:"gso-max-size" json:"gso-max-size,omitempty"` - //Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` + // Inet4Address []netip.Prefix `yaml:"inet4-address" json:"inet4-address,omitempty"` Inet6Address []netip.Prefix `yaml:"inet6-address" json:"inet6-address,omitempty"` IPRoute2TableIndex int `yaml:"iproute2-table-index" json:"iproute2-table-index,omitempty"` IPRoute2RuleIndex int `yaml:"iproute2-rule-index" json:"iproute2-rule-index,omitempty"` @@ -571,7 +571,7 @@ func UnmarshalRawConfig(buf []byte) (*RawConfig, error) { func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { config := &Config{} - log.Infoln("Start initial configuration in progress") //Segment finished in xxm + log.Infoln("Start initial configuration in progress") // Segment finished in xxm startTime := time.Now() general, err := parseGeneral(rawCfg) @@ -695,7 +695,7 @@ func ParseRawConfig(rawCfg *RawConfig) (*Config, error) { } elapsedTime := time.Since(startTime) / time.Millisecond // duration in ms - log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) //Segment finished in xxm + log.Infoln("Initial configuration complete, total time: %dms", elapsedTime) // Segment finished in xxm return config, nil } @@ -1148,94 +1148,9 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns. var nameservers []dns.NameServer for idx, server := range servers { - server = parsePureDNSServer(server) - u, err := url.Parse(server) + nameserver, err := parseOneNameServer(server, idx, respectRules, preferH3) if err != nil { - return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) - } - - proxyName := u.Fragment - - var addr, dnsNetType string - params := map[string]string{} - switch u.Scheme { - case "udp": - addr, err = hostWithDefaultPort(u.Host, "53") - dnsNetType = "" // UDP - case "tcp": - addr, err = hostWithDefaultPort(u.Host, "53") - dnsNetType = "tcp" // TCP - case "tls": - addr, err = hostWithDefaultPort(u.Host, "853") - dnsNetType = "tcp-tls" // DNS over TLS - case "http", "https": - addr, err = hostWithDefaultPort(u.Host, "443") - dnsNetType = "https" // DNS over HTTPS - if u.Scheme == "http" { - addr, err = hostWithDefaultPort(u.Host, "80") - } - if err == nil { - proxyName = "" - clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} - addr = clearURL.String() - if len(u.Fragment) != 0 { - for _, s := range strings.Split(u.Fragment, "&") { - arr := strings.Split(s, "=") - if len(arr) == 0 { - continue - } else if len(arr) == 1 { - proxyName = arr[0] - } else if len(arr) == 2 { - params[arr[0]] = arr[1] - } else { - params[arr[0]] = strings.Join(arr[1:], "=") - } - } - } - } - case "quic": - addr, err = hostWithDefaultPort(u.Host, "853") - dnsNetType = "quic" // DNS over QUIC - case "system": - dnsNetType = "system" // System DNS - case "dhcp": - addr = server[len("dhcp://"):] // some special notation cannot be parsed by url - dnsNetType = "dhcp" // UDP from DHCP - if addr == "system" { // Compatible with old writing "dhcp://system" - dnsNetType = "system" - addr = "" - } - case "rcode": - dnsNetType = "rcode" - addr = u.Host - switch addr { - case "success", - "format_error", - "server_failure", - "name_error", - "not_implemented", - "refused": - default: - err = fmt.Errorf("unsupported RCode type: %s", addr) - } - default: - return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) - } - - if err != nil { - return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) - } - - if respectRules && len(proxyName) == 0 { - proxyName = dns.RespectRules - } - - nameserver := dns.NameServer{ - Net: dnsNetType, - Addr: addr, - ProxyName: proxyName, - Params: params, - PreferH3: preferH3, + return nil, err } if slices.ContainsFunc(nameservers, nameserver.Equal) { continue // skip duplicates nameserver @@ -1246,6 +1161,116 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns. return nameservers, nil } +func parseOneNameServer(server string, idx int, respectRules bool, preferH3 bool) (dns.NameServer, error) { + empty := dns.NameServer{} + server = parsePureDNSServer(server) + u, err := url.Parse(server) + if err != nil { + return empty, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) + } + + proxyName := u.Fragment + + var addr, dnsNetType string + params := map[string]string{} + switch u.Scheme { + case "udp": + addr, err = hostWithDefaultPort(u.Host, "53") + dnsNetType = "" // UDP + case "tcp": + addr, err = hostWithDefaultPort(u.Host, "53") + dnsNetType = "tcp" // TCP + case "tls": + addr, err = hostWithDefaultPort(u.Host, "853") + dnsNetType = "tcp-tls" // DNS over TLS + case "http", "https": + addr, err = hostWithDefaultPort(u.Host, "443") + dnsNetType = "https" // DNS over HTTPS + if u.Scheme == "http" { + addr, err = hostWithDefaultPort(u.Host, "80") + } + if err == nil { + proxyName = "" + clearURL := url.URL{Scheme: u.Scheme, Host: addr, Path: u.Path, User: u.User} + addr = clearURL.String() + if len(u.Fragment) != 0 { + for _, s := range strings.Split(u.Fragment, "&") { + arr := strings.Split(s, "=") + if len(arr) == 0 { + continue + } else if len(arr) == 1 { + proxyName = arr[0] + } else if len(arr) == 2 { + params[arr[0]] = arr[1] + } else { + params[arr[0]] = strings.Join(arr[1:], "=") + } + } + } + } + case "quic": + addr, err = hostWithDefaultPort(u.Host, "853") + dnsNetType = "quic" // DNS over QUIC + case "system": + dnsNetType = "system" // System DNS + case "dhcp": + addr = server[len("dhcp://"):] // some special notation cannot be parsed by url + dnsNetType = "dhcp" // UDP from DHCP + if addr == "system" { // Compatible with old writing "dhcp://system" + dnsNetType = "system" + addr = "" + } + case "rcode": + dnsNetType = "rcode" + addr = u.Host + switch addr { + case "success", + "format_error", + "server_failure", + "name_error", + "not_implemented", + "refused": + default: + err = fmt.Errorf("unsupported RCode type: %s", addr) + } + case "fakeip": + dnsNetType = "fakeip" + // prase back dns + // fakeip://quic://223.5.5.5 + back := strings.TrimPrefix(server, "fakeip://") + if back != "" { + ns, err := parseOneNameServer(back, idx, respectRules, preferH3) + if err != nil { + return empty, err + } + return dns.NameServer{ + Net: dnsNetType, + Back: &ns, + }, nil + } + + default: + return empty, fmt.Errorf("DNS NameServer[%d](%s) unsupport scheme: %s", + idx, u.String(), u.Scheme) + } + + if err != nil { + return empty, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) + } + + if respectRules && len(proxyName) == 0 { + proxyName = dns.RespectRules + } + + return dns.NameServer{ + Net: dnsNetType, + Addr: addr, + ProxyName: proxyName, + Params: params, + PreferH3: preferH3, + }, nil +} + func init() { dns.ParseNameServer = func(servers []string) ([]dns.NameServer, error) { // using by wireguard return parseNameServer(servers, false, false) @@ -1288,65 +1313,12 @@ func parseNameServerPolicy(nsPolicy *orderedmap.OrderedMap[string, any], rulePro if err != nil { return nil, err } - kLower := strings.ToLower(k) - if strings.Contains(kLower, ",") { - if strings.Contains(kLower, "geosite:") { - subkeys := strings.Split(k, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, subkey := range subkeys { - newKey := "geosite:" + subkey - policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) - } - } else if strings.Contains(kLower, "rule-set:") { - subkeys := strings.Split(k, ":") - subkeys = subkeys[1:] - subkeys = strings.Split(subkeys[0], ",") - for _, subkey := range subkeys { - newKey := "rule-set:" + subkey - policy = append(policy, dns.Policy{Domain: newKey, NameServers: nameservers}) - } - } else { - subkeys := strings.Split(k, ",") - for _, subkey := range subkeys { - policy = append(policy, dns.Policy{Domain: subkey, NameServers: nameservers}) - } - } - } else { - if strings.Contains(kLower, "geosite:") { - policy = append(policy, dns.Policy{Domain: "geosite:" + k[8:], NameServers: nameservers}) - } else if strings.Contains(kLower, "rule-set:") { - policy = append(policy, dns.Policy{Domain: "rule-set:" + k[9:], NameServers: nameservers}) - } else { - policy = append(policy, dns.Policy{Domain: k, NameServers: nameservers}) - } + ps, err := parseDNSPolicy(k, nameservers, ruleProviders) + if err != nil { + return nil, err } + policy = append(policy, ps...) } - - for idx, p := range policy { - domain, nameservers := p.Domain, p.NameServers - - if strings.HasPrefix(domain, "rule-set:") { - domainSetName := domain[9:] - matcher, err := parseDomainRuleSet(domainSetName, "dns.nameserver-policy", ruleProviders) - if err != nil { - return nil, err - } - policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} - } else if strings.HasPrefix(domain, "geosite:") { - country := domain[8:] - matcher, err := RC.NewGEOSITE(country, "dns.nameserver-policy") - if err != nil { - return nil, err - } - policy[idx] = dns.Policy{Matcher: matcher, NameServers: nameservers} - } else { - if _, valid := trie.ValidAndSplitDomain(domain); !valid { - return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", domain) - } - } - } - return policy, nil } diff --git a/config/dns_policy.go b/config/dns_policy.go new file mode 100644 index 00000000..a745f30d --- /dev/null +++ b/config/dns_policy.go @@ -0,0 +1,60 @@ +package config + +import ( + "fmt" + "strings" + + "github.com/metacubex/mihomo/component/trie" + C "github.com/metacubex/mihomo/constant" + providerTypes "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/dns" + RC "github.com/metacubex/mihomo/rules/common" +) + +type matcher func(value string, ruleProviders map[string]providerTypes.RuleProvider) (C.DomainMatcher, error) + +var policyMatcherMap = map[string]matcher{ + "rule-set": func(value string, ruleProviders map[string]providerTypes.RuleProvider) (C.DomainMatcher, error) { + return parseDomainRuleSet(value, "dns.nameserver-policy", ruleProviders) + }, + "geosite": func(value string, _ map[string]providerTypes.RuleProvider) (C.DomainMatcher, error) { + return RC.NewGEOSITE(value, "dns.nameserver-policy") + }, + "rule": newRuleMatcher, +} + +func getMatcher(k string) (string, matcher) { + typ, v, found := strings.Cut(k, ":") + if !found { + return k, nil + } + matcher := policyMatcherMap[strings.ToLower(typ)] + // unknown, keep as original + if matcher == nil { + return k, nil + } + return v, matcher +} + +func parseDNSPolicy(k string, nameservers []dns.NameServer, + ruleProviders map[string]providerTypes.RuleProvider, +) ([]dns.Policy, error) { + var policy []dns.Policy + + v, matcher := getMatcher(k) + for _, subkey := range strings.Split(v, ",") { + if matcher != nil { + m, err := matcher(subkey, ruleProviders) + if err != nil { + return nil, err + } + policy = append(policy, dns.Policy{Matcher: m, NameServers: nameservers}) + } else { + if _, valid := trie.ValidAndSplitDomain(subkey); !valid { + return nil, fmt.Errorf("DNS ResoverRule invalid domain: %s", subkey) + } + policy = append(policy, dns.Policy{Domain: subkey, NameServers: nameservers}) + } + } + return policy, nil +} diff --git a/config/dns_policy_rule.go b/config/dns_policy_rule.go new file mode 100644 index 00000000..ba3be5e3 --- /dev/null +++ b/config/dns_policy_rule.go @@ -0,0 +1,72 @@ +package config + +import ( + "errors" + "net/netip" + "strings" + + "github.com/metacubex/mihomo/common/lru" + C "github.com/metacubex/mihomo/constant" + providerTypes "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/log" + + "github.com/metacubex/mihomo/tunnel" +) + +var cache = lru.New[string, C.AdapterType]( + lru.WithAge[string, C.AdapterType](1), +) + +type ruleMatcher struct { + typ string +} + +func (r *ruleMatcher) MatchDomain(domain string) bool { + typ, found := cache.Get(domain) + if !found { + p, _, err := tunnel.ResolveMetadata( + &C.Metadata{ + NetWork: C.TCP, + Type: C.INNER, // avoid process lookup + Host: domain, + DstIP: netip.AddrFrom4([4]byte{}), // avoid dns lookup + }, + ) + if err != nil { + log.Warnln("[DNS] ruleMatcher: match(%s) got err %v", domain, err.Error()) + return false + } + log.Debugln("[DNS] ruleMatcher: match(%s) -> %s", domain, p.Type().String()) + cache.Set(domain, p.Type()) + typ = p.Type() + } + switch typ { + case C.Direct, C.Compatible: + if r.typ == "direct" { + return true + } + case C.Reject, C.RejectDrop: + if r.typ == "reject" { + return true + } + case C.Pass: // should not happen + default: + if r.typ == "proxy" { + return true + } + } + return false +} + +func newRuleMatcher(value string, _ map[string]providerTypes.RuleProvider) (C.DomainMatcher, error) { + switch strings.ToLower(value) { + case "direct": + return &ruleMatcher{typ: "direct"}, nil + case "reject": + return &ruleMatcher{typ: "reject"}, nil + case "proxy": + return &ruleMatcher{typ: "proxy"}, nil + default: + return nil, errors.New("[DNS] rulePolicy: unknown rule type: " + value) + } +} diff --git a/config/dns_policy_test.go b/config/dns_policy_test.go new file mode 100644 index 00000000..90cf65d4 --- /dev/null +++ b/config/dns_policy_test.go @@ -0,0 +1,262 @@ +package config + +import ( + "reflect" + "testing" + + C "github.com/metacubex/mihomo/constant" + providerTypes "github.com/metacubex/mihomo/constant/provider" + "github.com/metacubex/mihomo/dns" + RP "github.com/metacubex/mihomo/rules/provider" +) + +func Test_getMatcher(t *testing.T) { + type args struct { + k string + } + tests := []struct { + name string + args args + want string + want1 matcher + }{ + { + name: "rule-set", + args: args{k: "rule-set:cn"}, + want: "cn", + want1: policyMatcherMap["rule-set"], + }, + { + name: "rule-set", + args: args{k: "rULE-set:cn,c2"}, + want: "cn,c2", + want1: policyMatcherMap["rule-set"], + }, + { + name: "unknown", + args: args{k: "xxx:cn,c2"}, + want: "xxx:cn,c2", + want1: nil, + }, + { + name: "domain", + args: args{k: "baidu.com"}, + want: "baidu.com", + want1: nil, + }, + + { + name: "domain list", + args: args{k: "baidu.com,+.baidu.com"}, + want: "baidu.com,+.baidu.com", + want1: nil, + }, + { + name: "rule", + args: args{k: "RuLE:cn,c2"}, + want: "cn,c2", + want1: policyMatcherMap["rule"], + }, + { + name: "geosite", + args: args{k: "geoSiTe:cn,c2"}, + want: "cn,c2", + want1: policyMatcherMap["geosite"], + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, got1 := getMatcher(tt.args.k) + if got != tt.want { + t.Errorf("getMatcher() got = %v, want %v", got, tt.want) + } + + if getPointer(got1) != getPointer(tt.want1) { + t.Errorf("getMatcher() got1 = %v, want %v", got1, tt.want1) + } + }) + } +} + +func getPointer(i any) uintptr { + return reflect.ValueOf(i).Pointer() +} + +func Test_parseDNSPolicy(t *testing.T) { + type args struct { + k string + nameservers []dns.NameServer + ruleProviders map[string]providerTypes.RuleProvider + } + ns := []dns.NameServer{ + { + Net: "rcode", + Addr: "success", + }, + } + rp := map[string]providerTypes.RuleProvider{ + "cn": RP.NewInlineProvider("cn", providerTypes.Domain, + []string{ + "www.baidu.com", + }, + nil), + } + getDomainMatcher := func(str string) C.DomainMatcher { + m, err := policyMatcherMap["geosite"](str, rp) + if err != nil { + t.Error(err) + } + return m + } + getRuleSetMatcher := func(str string) C.DomainMatcher { + m, err := policyMatcherMap["rule-set"](str, rp) + if err != nil { + t.Error(err) + } + return m + } + getRuleMatcher := func(str string) C.DomainMatcher { + m, err := policyMatcherMap["rule"](str, rp) + if err != nil { + t.Error(err) + } + return m + } + tests := []struct { + name string + args args + want []dns.Policy + wantErr bool + }{ + { + name: "未知", + args: args{ + k: "xxx:cn", + nameservers: ns, + }, + want: []dns.Policy{ + { + Domain: "xxx:cn", + NameServers: ns, + }, + }, + wantErr: false, + }, + { + name: "普通域名", + args: args{ + k: "www.baidu.com", + nameservers: ns, + }, + want: []dns.Policy{ + { + Domain: "www.baidu.com", + NameServers: ns, + }, + }, + wantErr: false, + }, + { + name: "多个普通域名", + args: args{ + k: "www.baidu.com,+.internal.crop.com", + nameservers: ns, + }, + want: []dns.Policy{ + { + Domain: "www.baidu.com", + NameServers: ns, + }, + { + Domain: "+.internal.crop.com", + NameServers: ns, + }, + }, + wantErr: false, + }, + { + name: "geosite 单个", + args: args{ + k: "GEoSite:cn", + nameservers: ns, + }, + want: []dns.Policy{ + { + Matcher: getDomainMatcher("cn"), + NameServers: ns, + }, + }, + wantErr: false, + }, + { + name: "geosite 多个", + args: args{ + k: "GEoSite:cn,category-ads-all", + nameservers: ns, + }, + want: []dns.Policy{ + { + Matcher: getDomainMatcher("cn"), + NameServers: ns, + }, + { + Matcher: getDomainMatcher("category-ads-all"), + NameServers: ns, + }, + }, + wantErr: false, + }, + + { + name: "rule-set", + args: args{ + k: "RuLe-sEt:cn", + nameservers: ns, + ruleProviders: rp, + }, + want: []dns.Policy{ + { + Matcher: getRuleSetMatcher("cn"), + NameServers: ns, + }, + }, + wantErr: false, + }, + { + name: "rule", + args: args{ + k: "RuLe:direct", + nameservers: ns, + ruleProviders: rp, + }, + want: []dns.Policy{ + { + Matcher: getRuleMatcher("direct"), + NameServers: ns, + }, + }, + wantErr: false, + }, + { + name: "rule unknown", + args: args{ + k: "RuLe:xxxx", + nameservers: ns, + ruleProviders: rp, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseDNSPolicy(tt.args.k, tt.args.nameservers, tt.args.ruleProviders) + if (err != nil) != tt.wantErr { + t.Errorf("parseDNSPolicy() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseDNSPolicy() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/dns/fakeip.go b/dns/fakeip.go new file mode 100644 index 00000000..560cd128 --- /dev/null +++ b/dns/fakeip.go @@ -0,0 +1,61 @@ +package dns + +import ( + stdCtx "context" + "strings" + + "github.com/metacubex/mihomo/component/fakeip" + "github.com/metacubex/mihomo/component/resolver" + D "github.com/miekg/dns" +) + +type fakeIPclient struct { + back dnsClient +} + +var _ dnsClient = &fakeIPclient{} + +func newFakeIPClient(back dnsClient) *fakeIPclient { + return &fakeIPclient{back: back} +} + +func (f *fakeIPclient) ExchangeContext(ctx stdCtx.Context, r *D.Msg) (*D.Msg, error) { + q := r.Question[0] + host := strings.TrimRight(q.Name, ".") + + // todo unify + switch q.Qtype { + case D.TypeAAAA, D.TypeSVCB, D.TypeHTTPS: + return handleMsgWithEmptyAnswer(r), nil + } + + if q.Qtype != D.TypeA { + return f.back.ExchangeContext(ctx, r) + } + + fakeip := resolver.DefaultHostMapper.(*ResolverEnhancer).fakePool + + msg := fakeipExchange(q, fakeip, host, r) + return msg, nil +} + +func fakeipExchange(q D.Question, fakePool *fakeip.Pool, host string, r *D.Msg) *D.Msg { + 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} + + setMsgTTL(msg, 1) + msg.SetRcode(r, D.RcodeSuccess) + msg.Authoritative = true + msg.RecursionAvailable = true + return msg +} + +func (r fakeIPclient) Address() string { + return "fakeip://" + r.back.Address() +} + +func (r fakeIPclient) ResetConnection() {} diff --git a/dns/middleware.go b/dns/middleware.go index b55adc6b..16b8a395 100644 --- a/dns/middleware.go +++ b/dns/middleware.go @@ -160,20 +160,8 @@ func withFakeIP(fakePool *fakeip.Pool) middleware { 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} - ctx.SetType(context.DNSTypeFakeIP) - setMsgTTL(msg, 1) - msg.SetRcode(r, D.RcodeSuccess) - msg.Authoritative = true - msg.RecursionAvailable = true - - return msg, nil + return fakeipExchange(q, fakePool, host, r), nil } } } diff --git a/dns/resolver.go b/dns/resolver.go index 9f7e28f3..5b47142d 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -398,6 +398,8 @@ type NameServer struct { ProxyName string Params map[string]string PreferH3 bool + // fakeip will use this + Back *NameServer } func (ns NameServer) Equal(ns2 NameServer) bool { @@ -411,7 +413,8 @@ func (ns NameServer) Equal(ns2 NameServer) bool { ns.ProxyAdapter == ns2.ProxyAdapter && ns.ProxyName == ns2.ProxyName && maps.Equal(ns.Params, ns2.Params) && - ns.PreferH3 == ns2.PreferH3 { + ns.PreferH3 == ns2.PreferH3 && + (ns.Back == ns2.Back || (ns.Back != nil && ns2.Back != nil && ns.Back.Equal(*ns2.Back))) { return true } return false diff --git a/dns/util.go b/dns/util.go index 50459cc1..30e83562 100644 --- a/dns/util.go +++ b/dns/util.go @@ -94,35 +94,44 @@ func isIPRequest(q D.Question) bool { func transform(servers []NameServer, resolver *Resolver) []dnsClient { ret := make([]dnsClient, 0, len(servers)) for _, s := range servers { - switch s.Net { - case "https": - ret = append(ret, newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName)) - continue - case "dhcp": - ret = append(ret, newDHCPClient(s.Addr)) - continue - case "system": - ret = append(ret, newSystemClient()) - continue - case "rcode": - ret = append(ret, newRCodeClient(s.Addr)) - continue - case "quic": - if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter, s.ProxyName); err == nil { - ret = append(ret, doq) - } else { - log.Fatalln("DoQ format error: %v", err) - } - continue - } + ret = append(ret, getDnsClient(s, resolver)) + } + return ret +} +func getDnsClient(s NameServer, resolver *Resolver) dnsClient { + switch s.Net { + case "https": + return newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter, s.ProxyName) + case "dhcp": + return newDHCPClient(s.Addr) + case "system": + return newSystemClient() + case "rcode": + return newRCodeClient(s.Addr) + case "quic": + if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter, s.ProxyName); err == nil { + return doq + } else { + log.Fatalln("DoQ format error: %v", err) + return nil + } + case "fakeip": + var back dnsClient + if s.Back == nil { + back = newSystemClient() + } else { + back = getDnsClient(*s.Back, resolver) + } + return newFakeIPClient(back) + default: var options []dialer.Option if s.Interface != "" { options = append(options, dialer.WithInterface(s.Interface)) } host, port, _ := net.SplitHostPort(s.Addr) - ret = append(ret, &client{ + return &client{ Client: &D.Client{ Net: s.Net, TLSConfig: &tls.Config{ @@ -134,9 +143,8 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { port: port, host: host, dialer: newDNSDialer(resolver, s.ProxyAdapter, s.ProxyName, options...), - }) + } } - return ret } func handleMsgWithEmptyAnswer(r *D.Msg) *D.Msg { diff --git a/docs/config.yaml b/docs/config.yaml index 263d67b6..0fce570d 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -253,7 +253,7 @@ dns: # 配置不使用 fake-ip 的域名 fake-ip-filter: - - '*.lan' + - "*.lan" - localhost.ptlogin2.qq.com # fakeip-filter 为 rule-providers 中的名为 fakeip-filter 规则订阅, # 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 @@ -333,6 +333,17 @@ dns: ## 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 # "rule-set:global,dns": 8.8.8.8 + ## 使用network TCP 进行代理规则匹配 + ## direct 直连,包含 Direct、Compatible + ## proxy 使用了代理 + ## reject 拒绝, 包含Reject、RejectDrop + ## + ## fakeip 后面可选后备dns,默认为system,当dns查询为txt等非A AAAA SVCB HTTPS时使用 + ## fakeip 使用的 fake-ip-range, + ## 由于全局fakeip优先级高,需要配置 fake-ip-filter-mode、fake-ip-filter 失效从而让nameserver-policy策略生效 + "rule:proxy": "fakeip://quic://223.5.5.5" + "rule:direct": quic://223.5.5.5 + "rule:reject": rcode://success proxies: # socks5 - name: "socks" type: socks5 @@ -527,13 +538,13 @@ proxies: # socks5 # servername: example.com # priority over wss host # network: ws # ws-opts: - # path: /path - # headers: - # Host: v2ray.com - # max-early-data: 2048 - # early-data-header-name: Sec-WebSocket-Protocol - # v2ray-http-upgrade: false - # v2ray-http-upgrade-fast-open: false + # path: /path + # headers: + # Host: v2ray.com + # max-early-data: 2048 + # early-data-header-name: Sec-WebSocket-Protocol + # v2ray-http-upgrade: false + # v2ray-http-upgrade-fast-open: false - name: "vmess-h2" type: vmess @@ -901,7 +912,7 @@ proxies: # socks5 # - http/1.1 # skip-cert-verify: true -# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理 + # dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理 - name: "dns-out" type: dns proxy-groups: @@ -988,8 +999,8 @@ proxy-providers: # size-limit: 10240 # 限制下载文件最大为10kb,默认为0即不限制文件大小 header: User-Agent: - - "Clash/v1.18.0" - - "mihomo/1.18.3" + - "Clash/v1.18.0" + - "mihomo/1.18.3" # Accept: # - 'application/vnd.github.v3.raw' # Authorization: @@ -1072,9 +1083,9 @@ rule-providers: type: inline behavior: domain # classical / ipcidr payload: - - '.blogger.com' - - '*.*.microsoft.com' - - 'books.itunes.apple.com' + - ".blogger.com" + - "*.*.microsoft.com" + - "books.itunes.apple.com" rules: - RULE-SET,rule1,REJECT @@ -1303,14 +1314,14 @@ listeners: # proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错) stack: system # gvisor / mixed dns-hijack: - - 0.0.0.0:53 # 需要劫持的 DNS + - 0.0.0.0:53 # 需要劫持的 DNS # auto-detect-interface: false # 自动识别出口网卡 # auto-route: false # 配置路由表 # mtu: 9000 # 最大传输单元 inet4-address: # 必须手动设置 ipv4 地址段 - - 198.19.0.1/30 + - 198.19.0.1/30 inet6-address: # 必须手动设置 ipv6 地址段 - - "fdfe:dcba:9877::1/126" + - "fdfe:dcba:9877::1/126" # strict-route: true # 将所有连接路由到 tun 来防止泄漏,但你的设备将无法其他设备被访问 # inet4-route-address: # 启用 auto-route 时使用自定义路由而不是默认路由 # - 0.0.0.0/1 diff --git a/tunnel/dns_dialer.go b/tunnel/dns_dialer.go index 1839869b..8f1b904e 100644 --- a/tunnel/dns_dialer.go +++ b/tunnel/dns_dialer.go @@ -64,7 +64,7 @@ func (d *DNSDialer) DialContext(ctx context.Context, network, addr string) (net. } metadata.DstIP = dstIP } - proxyAdapter, rule, err = resolveMetadata(metadata) + proxyAdapter, rule, err = ResolveMetadata(metadata) if err != nil { return nil, err } @@ -124,7 +124,6 @@ func (d *DNSDialer) DialContext(ctx context.Context, network, addr string) (net. return N.NewBindPacketConn(packetConn, metadata.UDPAddr()), nil } - } func (d *DNSDialer) ListenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) { @@ -152,7 +151,7 @@ func (d *DNSDialer) ListenPacket(ctx context.Context, network, addr string) (net var rule C.Rule if proxyAdapter == nil { if proxyName == DnsRespectRules { - proxyAdapter, rule, err = resolveMetadata(metadata) + proxyAdapter, rule, err = ResolveMetadata(metadata) if err != nil { return nil, err } diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index b9507930..4b6f7d96 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -324,7 +324,7 @@ func preHandleMetadata(metadata *C.Metadata) error { return nil } -func resolveMetadata(metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { +func ResolveMetadata(metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { if metadata.SpecialProxy != "" { var exist bool proxy, exist = proxies[metadata.SpecialProxy] @@ -393,7 +393,7 @@ func handleUDPConn(packet C.PacketAdapter) { return nil, nil, err } - proxy, rule, err := resolveMetadata(metadata) + proxy, rule, err := ResolveMetadata(metadata) if err != nil { log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) return nil, nil, err @@ -489,7 +489,7 @@ func handleTCPConn(connCtx C.ConnContext) { }() } - proxy, rule, err := resolveMetadata(metadata) + proxy, rule, err := ResolveMetadata(metadata) if err != nil { log.Warnln("[Metadata] parse failed: %s", err.Error()) return From 09d5715826798272953cad662ee5f37b1e24edb4 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sun, 16 Mar 2025 17:43:59 +0800 Subject: [PATCH 2/4] chore(dns): unify fakeip logic --- dns/fakeip.go | 40 +++++++++++++++++++++++++++++++--------- dns/middleware.go | 23 +++++------------------ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/dns/fakeip.go b/dns/fakeip.go index 560cd128..ecf79d3c 100644 --- a/dns/fakeip.go +++ b/dns/fakeip.go @@ -6,6 +6,7 @@ import ( "github.com/metacubex/mihomo/component/fakeip" "github.com/metacubex/mihomo/component/resolver" + "github.com/metacubex/mihomo/context" D "github.com/miekg/dns" ) @@ -20,26 +21,47 @@ func newFakeIPClient(back dnsClient) *fakeIPclient { } func (f *fakeIPclient) ExchangeContext(ctx stdCtx.Context, r *D.Msg) (*D.Msg, error) { + fakeip := resolver.DefaultHostMapper.(*ResolverEnhancer).fakePool + + dnsCtx := context.NewDNSContext(ctx, r) + return fakeipExchange(dnsCtx, r, &fakeipExchangeOption{ + fakePool: fakeip, + forceFakeip: true, + back: func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { + return f.back.ExchangeContext(ctx.Context, r) + }, + }) +} + +type fakeipExchangeOption struct { + fakePool *fakeip.Pool + forceFakeip bool + back handler +} + +func fakeipExchange(ctx *context.DNSContext, r *D.Msg, option *fakeipExchangeOption) (*D.Msg, error) { q := r.Question[0] host := strings.TrimRight(q.Name, ".") - // todo unify + fakePool := option.fakePool + back := option.back + if !option.forceFakeip { + if fakePool.ShouldSkipped(host) { + return back(ctx, r) + } + } + switch q.Qtype { case D.TypeAAAA, D.TypeSVCB, D.TypeHTTPS: return handleMsgWithEmptyAnswer(r), nil } if q.Qtype != D.TypeA { - return f.back.ExchangeContext(ctx, r) + return back(ctx, r) } - fakeip := resolver.DefaultHostMapper.(*ResolverEnhancer).fakePool + ctx.SetType(context.DNSTypeFakeIP) - msg := fakeipExchange(q, fakeip, host, r) - return msg, nil -} - -func fakeipExchange(q D.Question, fakePool *fakeip.Pool, host string, r *D.Msg) *D.Msg { rr := &D.A{} rr.Hdr = D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: dnsDefaultTTL} ip := fakePool.Lookup(host) @@ -51,7 +73,7 @@ func fakeipExchange(q D.Question, fakePool *fakeip.Pool, host string, r *D.Msg) msg.SetRcode(r, D.RcodeSuccess) msg.Authoritative = true msg.RecursionAvailable = true - return msg + return msg, nil } func (r fakeIPclient) Address() string { diff --git a/dns/middleware.go b/dns/middleware.go index 16b8a395..1e9774b3 100644 --- a/dns/middleware.go +++ b/dns/middleware.go @@ -144,24 +144,11 @@ func withMapping(mapping *lru.LruCache[netip.Addr, string]) middleware { func withFakeIP(fakePool *fakeip.Pool) middleware { return func(next handler) handler { return func(ctx *context.DNSContext, r *D.Msg) (*D.Msg, error) { - q := r.Question[0] - - host := strings.TrimRight(q.Name, ".") - if fakePool.ShouldSkipped(host) { - return next(ctx, r) - } - - switch q.Qtype { - case D.TypeAAAA, D.TypeSVCB, D.TypeHTTPS: - return handleMsgWithEmptyAnswer(r), nil - } - - if q.Qtype != D.TypeA { - return next(ctx, r) - } - - ctx.SetType(context.DNSTypeFakeIP) - return fakeipExchange(q, fakePool, host, r), nil + return fakeipExchange(ctx, r, &fakeipExchangeOption{ + fakePool: fakePool, + forceFakeip: false, + back: next, + }) } } } From f213cd678f45574d05e73c45f450ed021e1ed2e8 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:28:04 +0800 Subject: [PATCH 3/4] fix(dns): fix proxy nesting --- config/dns_policy_rule.go | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/config/dns_policy_rule.go b/config/dns_policy_rule.go index ba3be5e3..ff73d0d2 100644 --- a/config/dns_policy_rule.go +++ b/config/dns_policy_rule.go @@ -24,22 +24,38 @@ type ruleMatcher struct { func (r *ruleMatcher) MatchDomain(domain string) bool { typ, found := cache.Get(domain) if !found { - p, _, err := tunnel.ResolveMetadata( - &C.Metadata{ - NetWork: C.TCP, - Type: C.INNER, // avoid process lookup - Host: domain, - DstIP: netip.AddrFrom4([4]byte{}), // avoid dns lookup - }, - ) + meta := &C.Metadata{ + NetWork: C.TCP, + Type: C.INNER, // avoid process lookup + Host: domain, + DstIP: netip.AddrFrom4([4]byte{}), // avoid dns lookup + } + p, _, err := tunnel.ResolveMetadata(meta) if err != nil { log.Warnln("[DNS] ruleMatcher: match(%s) got err %v", domain, err.Error()) return false } - log.Debugln("[DNS] ruleMatcher: match(%s) -> %s", domain, p.Type().String()) - cache.Set(domain, p.Type()) - typ = p.Type() + // parse multi-layer nesting + adapter := p + for { + next := adapter.Unwrap(meta, false) + if next != nil { + adapter = next + } else { + break + } + } + typ = adapter.Type() + cache.Set(domain, typ) } + ret := ruleMatch(typ, r) + log.Debugln("[DNS] ruleMatcher: domain(%s, %s) rule(%s), match: %v", + domain, typ.String(), r.typ, ret) + + return ret +} + +func ruleMatch(typ C.AdapterType, r *ruleMatcher) bool { switch typ { case C.Direct, C.Compatible: if r.typ == "direct" { From 94a5b1a6790f0c56cb182f823cda285181b09ec3 Mon Sep 17 00:00:00 2001 From: qianlongzt <18493471+qianlongzt@users.noreply.github.com> Date: Sun, 16 Mar 2025 22:58:41 +0800 Subject: [PATCH 4/4] refactor(dns): use option to avoid dns resolve --- config/dns_policy_rule.go | 7 ++++--- tunnel/dns_dialer.go | 4 ++-- tunnel/tunnel.go | 16 ++++++++++------ 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/config/dns_policy_rule.go b/config/dns_policy_rule.go index ff73d0d2..2d5b2b07 100644 --- a/config/dns_policy_rule.go +++ b/config/dns_policy_rule.go @@ -2,7 +2,6 @@ package config import ( "errors" - "net/netip" "strings" "github.com/metacubex/mihomo/common/lru" @@ -28,9 +27,8 @@ func (r *ruleMatcher) MatchDomain(domain string) bool { NetWork: C.TCP, Type: C.INNER, // avoid process lookup Host: domain, - DstIP: netip.AddrFrom4([4]byte{}), // avoid dns lookup } - p, _, err := tunnel.ResolveMetadata(meta) + p, r, err := tunnel.ResolveMetadata(meta, &tunnel.ResolveOption{SkipResolveIP: true}) if err != nil { log.Warnln("[DNS] ruleMatcher: match(%s) got err %v", domain, err.Error()) return false @@ -45,6 +43,9 @@ func (r *ruleMatcher) MatchDomain(domain string) bool { break } } + log.Debugln("[DNS] ruleMatcher: domain(%s, %s) rule(%s)", + domain, typ.String(), r) + typ = adapter.Type() cache.Set(domain, typ) } diff --git a/tunnel/dns_dialer.go b/tunnel/dns_dialer.go index 8f1b904e..6f4aec02 100644 --- a/tunnel/dns_dialer.go +++ b/tunnel/dns_dialer.go @@ -64,7 +64,7 @@ func (d *DNSDialer) DialContext(ctx context.Context, network, addr string) (net. } metadata.DstIP = dstIP } - proxyAdapter, rule, err = ResolveMetadata(metadata) + proxyAdapter, rule, err = ResolveMetadata(metadata, &ResolveOption{SkipResolveIP: true}) if err != nil { return nil, err } @@ -151,7 +151,7 @@ func (d *DNSDialer) ListenPacket(ctx context.Context, network, addr string) (net var rule C.Rule if proxyAdapter == nil { if proxyName == DnsRespectRules { - proxyAdapter, rule, err = ResolveMetadata(metadata) + proxyAdapter, rule, err = ResolveMetadata(metadata, &ResolveOption{SkipResolveIP: true}) if err != nil { return nil, err } diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go index 4b6f7d96..c206800a 100644 --- a/tunnel/tunnel.go +++ b/tunnel/tunnel.go @@ -324,7 +324,11 @@ func preHandleMetadata(metadata *C.Metadata) error { return nil } -func ResolveMetadata(metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err error) { +type ResolveOption struct { + SkipResolveIP bool +} + +func ResolveMetadata(metadata *C.Metadata, option *ResolveOption) (proxy C.Proxy, rule C.Rule, err error) { if metadata.SpecialProxy != "" { var exist bool proxy, exist = proxies[metadata.SpecialProxy] @@ -341,7 +345,7 @@ func ResolveMetadata(metadata *C.Metadata) (proxy C.Proxy, rule C.Rule, err erro proxy = proxies["GLOBAL"] // Rule default: - proxy, rule, err = match(metadata) + proxy, rule, err = match(metadata, option) } return } @@ -393,7 +397,7 @@ func handleUDPConn(packet C.PacketAdapter) { return nil, nil, err } - proxy, rule, err := ResolveMetadata(metadata) + proxy, rule, err := ResolveMetadata(metadata, &ResolveOption{}) if err != nil { log.Warnln("[UDP] Parse metadata failed: %s", err.Error()) return nil, nil, err @@ -489,7 +493,7 @@ func handleTCPConn(connCtx C.ConnContext) { }() } - proxy, rule, err := ResolveMetadata(metadata) + proxy, rule, err := ResolveMetadata(metadata, &ResolveOption{}) if err != nil { log.Warnln("[Metadata] parse failed: %s", err.Error()) return @@ -592,7 +596,7 @@ func shouldResolveIP(rule C.Rule, metadata *C.Metadata) bool { return rule.ShouldResolveIP() && metadata.Host != "" && !metadata.DstIP.IsValid() } -func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { +func match(metadata *C.Metadata, option *ResolveOption) (C.Proxy, C.Rule, error) { configMux.RLock() defer configMux.RUnlock() var ( @@ -606,7 +610,7 @@ func match(metadata *C.Metadata) (C.Proxy, C.Rule, error) { } for _, rule := range getRules(metadata) { - if !resolved && shouldResolveIP(rule, metadata) { + if !resolved && !option.SkipResolveIP && shouldResolveIP(rule, metadata) { func() { ctx, cancel := context.WithTimeout(context.Background(), resolver.DefaultDNSTimeout) defer cancel()