diff --git a/config/config.go b/config/config.go index e02cfead9d..e942dbc6e6 100644 --- a/config/config.go +++ b/config/config.go @@ -1235,6 +1235,9 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns. default: err = fmt.Errorf("unsupported RCode type: %s", addr) } + case "records": + dnsNetType = "records" + addr, err = checkRecords(u.Host) default: return nil, fmt.Errorf("DNS NameServer[%d] unsupport scheme: %s", idx, u.Scheme) } @@ -1263,6 +1266,31 @@ func parseNameServer(servers []string, respectRules bool, preferH3 bool) ([]dns. return nameservers, nil } +// checkRecords check host is '[ip4],[ip6]', ip4 is required +func checkRecords(host string) (string, error) { + ips := strings.Split(host+",", ",") + ip4 := ips[0] + ip6 := ips[1] + + log.Debugln("ns-policy records check format => [%s]", host) + + // check ip4 format + ip4Addr, err := netip.ParseAddr(ip4) + if err != nil || !ip4Addr.Is4() { + return "", fmt.Errorf("ns-policy records[0] format error: records://%s", host) + } + + // if ip6 not empty, check ip6 + if len(ip6) > 0 { + ip6Addr, err := netip.ParseAddr(ip6) + if err != nil || !ip6Addr.Is6() { + return "", fmt.Errorf("ns-policy records[1] format error: records://%s", host) + } + } + + return host, nil +} + func init() { dns.ParseNameServer = func(servers []string) ([]dns.NameServer, error) { // using by wireguard return parseNameServer(servers, false, false) diff --git a/dns/records.go b/dns/records.go new file mode 100644 index 0000000000..c1a69fd87c --- /dev/null +++ b/dns/records.go @@ -0,0 +1,69 @@ +package dns + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/metacubex/mihomo/log" + D "github.com/miekg/dns" +) + +func newRecordsClient(addr string) recordsClient { + return recordsClient{ + rcode: D.RcodeSuccess, + addr: "records://" + addr, + } +} + +type recordsClient struct { + rcode int + addr string +} + +var _ dnsClient = (*recordsClient)(nil) + +func (r recordsClient) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) { + m.Response = true + m.Rcode = r.rcode + + // split ip + ips := strings.Split(r.addr[len("records://"):]+",", ",") + ip4 := ips[0] + ip6 := ips[1] + + var q *D.Question + if len(m.Question) > 0 { + q = &m.Question[0] + } else { + return nil, fmt.Errorf("[DNS] ns-policy records resolve failed: no Question") + } + + // queryType A/AAAA return ip4/6 records + if q.Qtype == D.TypeA { + rr := &D.A{ + Hdr: D.RR_Header{Name: q.Name, Rrtype: D.TypeA, Class: D.ClassINET, Ttl: 60}, + A: net.ParseIP(ip4), + } + m.Answer = append(m.Answer, rr) + } + + if q.Qtype == D.TypeAAAA { + rr := &D.AAAA{ + Hdr: D.RR_Header{Name: q.Name, Rrtype: D.TypeAAAA, Class: D.ClassINET, Ttl: 60}, + AAAA: net.ParseIP(ip6), + } + m.Answer = append(m.Answer, rr) + } + + log.Debugln("[DNS] ns-policy %s --> %s from %s", msgToDomain(m), msgToLogString(m), r.Address()) + + return m, nil +} + +func (r recordsClient) Address() string { + return r.addr +} + +func (r recordsClient) ResetConnection() {} diff --git a/dns/records_test.go b/dns/records_test.go new file mode 100644 index 0000000000..33e33f0e66 --- /dev/null +++ b/dns/records_test.go @@ -0,0 +1,61 @@ +package dns + +import ( + "net" + "testing" + + D "github.com/miekg/dns" + "github.com/stretchr/testify/assert" +) + +func TestExchangeContextTypeA(t *testing.T) { + // new recordsClient + ip4 := "127.0.0.1" + recordsCli := newRecordsClient(ip4 + ",2001:0db8:85a3:0000:0000:8a2e:0370:7334") + + // new D.Msg + question := D.Question{ + Name: "android-gateway.com.cn", + Qtype: D.TypeA, + Qclass: D.ClassANY, + } + + msg := &D.Msg{ + Question: []D.Question{question}, + } + + // test get answer [A] + replyMsg, _ := recordsCli.ExchangeContext(nil, msg) + + a, ok := replyMsg.Answer[0].(*D.A) + + assert.Equal(t, true, ok) + assert.Equal(t, ip4, a.A.String()) + assert.Implements(t, (*D.RR)(nil), replyMsg.Answer[0]) +} + +func TestExchangeContextTypeAAAA(t *testing.T) { + // new recordsClient + ip6 := "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + recordsCli := newRecordsClient("127.0.0.1," + ip6) + + // new D.Msg + question := D.Question{ + Name: "android-gateway.com.cn", + Qtype: D.TypeAAAA, + Qclass: D.ClassANY, + } + + msg := &D.Msg{ + Question: []D.Question{question}, + } + + // test get answer [AAAA] + replyMsg, _ := recordsCli.ExchangeContext(nil, msg) + + reply, ok := replyMsg.Answer[0].(*D.AAAA) + + assert.Equal(t, true, ok) + assert.Equal(t, net.ParseIP(ip6).String(), reply.AAAA.String()) + assert.Implements(t, (*D.RR)(nil), replyMsg.Answer[0]) +} diff --git a/dns/util.go b/dns/util.go index b7082818cf..9900fb1d6e 100644 --- a/dns/util.go +++ b/dns/util.go @@ -107,6 +107,8 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { c = newRCodeClient(s.Addr) case "quic": c = newDoQ(s.Addr, resolver, s.Params, s.ProxyAdapter, s.ProxyName) + case "records": + c = newRecordsClient(s.Addr) default: c = newClient(s.Addr, resolver, s.Net, s.Params, s.ProxyAdapter, s.ProxyName) } diff --git a/docs/config.yaml b/docs/config.yaml index 6368f5ede4..f8dca62162 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -355,6 +355,8 @@ dns: ## global,dns 为 rule-providers 中的名为 global 和 dns 规则订阅, ## 且 behavior 必须为 domain/classical,当为 classical 时仅会生效域名类规则 # "rule-set:global,dns": 8.8.8.8 + # 配置自定义返回记录, 通过配合fake-ip可透明访问代理网关, 格式(ip4必须): records://{ip4},{ip6} + "+.android-gateway.com.cn": records://127.0.0.1,::1 proxies: # socks5 - name: "socks"