mirror of
https://github.com/MetaCubeX/Clash.Meta.git
synced 2025-04-11 04:50:56 +00:00
Merge 94a5b1a679
into 7b37fcfc8d
This commit is contained in:
commit
31f1bb1874
11 changed files with 694 additions and 228 deletions
266
config/config.go
266
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
|
||||
}
|
||||
|
||||
|
|
60
config/dns_policy.go
Normal file
60
config/dns_policy.go
Normal file
|
@ -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
|
||||
}
|
89
config/dns_policy_rule.go
Normal file
89
config/dns_policy_rule.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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 {
|
||||
meta := &C.Metadata{
|
||||
NetWork: C.TCP,
|
||||
Type: C.INNER, // avoid process lookup
|
||||
Host: domain,
|
||||
}
|
||||
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
|
||||
}
|
||||
// parse multi-layer nesting
|
||||
adapter := p
|
||||
for {
|
||||
next := adapter.Unwrap(meta, false)
|
||||
if next != nil {
|
||||
adapter = next
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
log.Debugln("[DNS] ruleMatcher: domain(%s, %s) rule(%s)",
|
||||
domain, typ.String(), r)
|
||||
|
||||
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" {
|
||||
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)
|
||||
}
|
||||
}
|
262
config/dns_policy_test.go
Normal file
262
config/dns_policy_test.go
Normal file
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
83
dns/fakeip.go
Normal file
83
dns/fakeip.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
package dns
|
||||
|
||||
import (
|
||||
stdCtx "context"
|
||||
"strings"
|
||||
|
||||
"github.com/metacubex/mihomo/component/fakeip"
|
||||
"github.com/metacubex/mihomo/component/resolver"
|
||||
"github.com/metacubex/mihomo/context"
|
||||
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) {
|
||||
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, ".")
|
||||
|
||||
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 back(ctx, r)
|
||||
}
|
||||
|
||||
ctx.SetType(context.DNSTypeFakeIP)
|
||||
|
||||
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, nil
|
||||
}
|
||||
|
||||
func (r fakeIPclient) Address() string {
|
||||
return "fakeip://" + r.back.Address()
|
||||
}
|
||||
|
||||
func (r fakeIPclient) ResetConnection() {}
|
|
@ -144,36 +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)
|
||||
}
|
||||
|
||||
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(ctx, r, &fakeipExchangeOption{
|
||||
fakePool: fakePool,
|
||||
forceFakeip: false,
|
||||
back: next,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
56
dns/util.go
56
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 {
|
||||
|
|
|
@ -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
|
||||
|
@ -902,7 +913,7 @@ proxies: # socks5
|
|||
# - http/1.1
|
||||
# skip-cert-verify: true
|
||||
|
||||
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
|
||||
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
|
||||
- name: "dns-out"
|
||||
type: dns
|
||||
proxy-groups:
|
||||
|
@ -989,8 +1000,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:
|
||||
|
@ -1073,9 +1084,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
|
||||
|
@ -1304,14 +1315,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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
@ -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, &ResolveOption{SkipResolveIP: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Reference in a new issue