From ab3fce29ab88ec113ce7a75f0937bf983c49f696 Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Tue, 11 Apr 2023 10:29:55 +0800 Subject: [PATCH] feat: wireguard add `remote-dns-resolve` and `dns` settings --- adapter/outbound/wireguard.go | 44 ++++++++++++++++++++++++++++++--- config/config.go | 24 +++++++++++------- dns/client.go | 11 +++++---- dns/doh.go | 28 ++++++++++++--------- dns/doq.go | 15 +++++++----- dns/resolver.go | 7 ++++-- dns/util.go | 46 ++++++++++++++++++++--------------- docs/config.yaml | 2 ++ 8 files changed, 121 insertions(+), 56 deletions(-) diff --git a/adapter/outbound/wireguard.go b/adapter/outbound/wireguard.go index 9a302de5..0070ce46 100644 --- a/adapter/outbound/wireguard.go +++ b/adapter/outbound/wireguard.go @@ -18,6 +18,7 @@ import ( "github.com/Dreamacro/clash/component/proxydialer" "github.com/Dreamacro/clash/component/resolver" C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/log" wireguard "github.com/metacubex/sing-wireguard" @@ -38,6 +39,7 @@ type WireGuard struct { dialer *wgSingDialer startOnce sync.Once startErr error + resolver *dns.Resolver } type WireGuardOption struct { @@ -51,6 +53,9 @@ type WireGuardOption struct { PersistentKeepalive int `proxy:"persistent-keepalive,omitempty"` Peers []WireGuardPeerOption `proxy:"peers,omitempty"` + + RemoteDnsResolve bool `proxy:"remote-dns-resolve,omitempty"` + Dns []string `proxy:"dns,omitempty"` } type WireGuardPeerOption struct { @@ -298,6 +303,29 @@ func NewWireGuard(option WireGuardOption) (*WireGuard, error) { return nil, E.Cause(err, "setup wireguard") } //err = outbound.tunDevice.Start() + + var has6 bool + for _, address := range localPrefixes { + if !address.Addr().Unmap().Is4() { + has6 = true + break + } + } + + if option.RemoteDnsResolve && len(option.Dns) > 0 { + nss, err := dns.ParseNameServer(option.Dns) + if err != nil { + return nil, err + } + for i := range nss { + nss[i].ProxyAdapter = outbound + } + outbound.resolver = dns.NewResolver(dns.Config{ + Main: nss, + IPv6: has6, + }) + } + return outbound, nil } @@ -318,8 +346,12 @@ func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts if w.startErr != nil { return nil, w.startErr } - if !metadata.Resolved() { - options = append(options, dialer.WithResolver(resolver.DefaultResolver)) + if !metadata.Resolved() || w.resolver != nil { + r := resolver.DefaultResolver + if w.resolver != nil { + r = w.resolver + } + options = append(options, dialer.WithResolver(r)) options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice})) conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress()) } else { @@ -348,8 +380,12 @@ func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadat if err != nil { return nil, err } - if !metadata.Resolved() { - ip, err := resolver.ResolveIP(ctx, metadata.Host) + if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" { + r := resolver.DefaultResolver + if w.resolver != nil { + r = w.resolver + } + ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r) if err != nil { return nil, errors.New("can't resolve ip") } diff --git a/config/config.go b/config/config.go index e7720c40..24594ee2 100644 --- a/config/config.go +++ b/config/config.go @@ -896,7 +896,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) return nil, fmt.Errorf("DNS NameServer[%d] format error: %s", idx, err.Error()) } - proxyAdapter := u.Fragment + proxyName := u.Fragment var addr, dnsNetType string params := map[string]string{} @@ -913,7 +913,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) case "https": addr, err = hostWithDefaultPort(u.Host, "443") if err == nil { - proxyAdapter = "" + proxyName = "" clearURL := url.URL{Scheme: "https", Host: addr, Path: u.Path} addr = clearURL.String() dnsNetType = "https" // DNS over HTTPS @@ -923,7 +923,7 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) if len(arr) == 0 { continue } else if len(arr) == 1 { - proxyAdapter = arr[0] + proxyName = arr[0] } else if len(arr) == 2 { params[arr[0]] = arr[1] } else { @@ -949,18 +949,24 @@ func parseNameServer(servers []string, preferH3 bool) ([]dns.NameServer, error) nameservers = append( nameservers, dns.NameServer{ - Net: dnsNetType, - Addr: addr, - ProxyAdapter: proxyAdapter, - Interface: dialer.DefaultInterface, - Params: params, - PreferH3: preferH3, + Net: dnsNetType, + Addr: addr, + ProxyName: proxyName, + Interface: dialer.DefaultInterface, + Params: params, + PreferH3: preferH3, }, ) } return nameservers, nil } +func init() { + dns.ParseNameServer = func(servers []string) ([]dns.NameServer, error) { // using by wireguard + return parseNameServer(servers, false) + } +} + func parsePureDNSServer(server string) string { addPre := func(server string) string { return "udp://" + server diff --git a/dns/client.go b/dns/client.go index 936a5882..637207f3 100644 --- a/dns/client.go +++ b/dns/client.go @@ -8,14 +8,14 @@ import ( "net/netip" "strings" - tlsC "github.com/Dreamacro/clash/component/tls" - "go.uber.org/atomic" - "github.com/Dreamacro/clash/component/dialer" "github.com/Dreamacro/clash/component/resolver" + tlsC "github.com/Dreamacro/clash/component/tls" + C "github.com/Dreamacro/clash/constant" D "github.com/miekg/dns" "github.com/zhangyunhao116/fastrand" + "go.uber.org/atomic" ) type client struct { @@ -24,7 +24,8 @@ type client struct { port string host string iface *atomic.String - proxyAdapter string + proxyAdapter C.ProxyAdapter + proxyName string addr string } @@ -81,7 +82,7 @@ func (c *client) ExchangeContext(ctx context.Context, m *D.Msg) (*D.Msg, error) options = append(options, dialer.WithInterface(c.iface.Load())) } - conn, err := getDialHandler(c.r, c.proxyAdapter, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port)) + conn, err := getDialHandler(c.r, c.proxyAdapter, c.proxyName, options...)(ctx, network, net.JoinHostPort(ip.String(), c.port)) if err != nil { return nil, err } diff --git a/dns/doh.go b/dns/doh.go index 1e6528d9..dd8ba435 100644 --- a/dns/doh.go +++ b/dns/doh.go @@ -21,6 +21,7 @@ import ( "github.com/metacubex/quic-go" "github.com/metacubex/quic-go/http3" D "github.com/miekg/dns" + "golang.org/x/exp/slices" "golang.org/x/net/http2" ) @@ -63,7 +64,8 @@ type dnsOverHTTPS struct { url *url.URL r *Resolver httpVersions []C.HTTPVersion - proxyAdapter string + proxyAdapter C.ProxyAdapter + proxyName string addr string } @@ -71,7 +73,7 @@ type dnsOverHTTPS struct { var _ dnsClient = (*dnsOverHTTPS)(nil) // newDoH returns the DNS-over-HTTPS Upstream. -func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[string]string, proxyAdapter string) dnsClient { +func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[string]string, proxyAdapter C.ProxyAdapter, proxyName string) dnsClient { u, _ := url.Parse(urlString) httpVersions := DefaultHTTPVersions if preferH3 { @@ -87,6 +89,7 @@ func newDoHClient(urlString string, r *Resolver, preferH3 bool, params map[strin addr: u.String(), r: r, proxyAdapter: proxyAdapter, + proxyName: proxyName, quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, TokenStore: newQUICTokenStore(), @@ -390,14 +393,17 @@ func (doh *dnsOverHTTPS) createTransport(ctx context.Context) (t http.RoundTripp nextProtos = append(nextProtos, string(v)) } tlsConfig.NextProtos = nextProtos - dialContext := getDialHandler(doh.r, doh.proxyAdapter) - // First, we attempt to create an HTTP3 transport. If the probe QUIC - // connection is established successfully, we'll be using HTTP3 for this - // upstream. - transportH3, err := doh.createTransportH3(ctx, tlsConfig, dialContext) - if err == nil { - log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String()) - return transportH3, nil + dialContext := getDialHandler(doh.r, doh.proxyAdapter, doh.proxyName) + + if slices.Contains(doh.httpVersions, C.HTTPVersion3) { + // First, we attempt to create an HTTP3 transport. If the probe QUIC + // connection is established successfully, we'll be using HTTP3 for this + // upstream. + transportH3, err := doh.createTransportH3(ctx, tlsConfig, dialContext) + if err == nil { + log.Debugln("[%s] using HTTP/3 for this upstream: QUIC was faster", doh.url.String()) + return transportH3, nil + } } log.Debugln("[%s] using HTTP/2 for this upstream: %v", doh.url.String(), err) @@ -533,7 +539,7 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls. IP: net.ParseIP(ip), Port: portInt, } - conn, err := listenPacket(ctx, doh.proxyAdapter, "udp", addr, doh.r) + conn, err := listenPacket(ctx, doh.proxyAdapter, doh.proxyName, "udp", addr, doh.r) if err != nil { return nil, err } diff --git a/dns/doq.go b/dns/doq.go index 1354f177..73310340 100644 --- a/dns/doq.go +++ b/dns/doq.go @@ -13,9 +13,10 @@ import ( "time" tlsC "github.com/Dreamacro/clash/component/tls" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/log" "github.com/metacubex/quic-go" - "github.com/Dreamacro/clash/log" D "github.com/miekg/dns" ) @@ -60,7 +61,8 @@ type dnsOverQUIC struct { bytesPoolGuard sync.Mutex addr string - proxyAdapter string + proxyAdapter C.ProxyAdapter + proxyName string r *Resolver } @@ -68,10 +70,11 @@ type dnsOverQUIC struct { var _ dnsClient = (*dnsOverQUIC)(nil) // newDoQ returns the DNS-over-QUIC Upstream. -func newDoQ(resolver *Resolver, addr string, adapter string) (dnsClient, error) { +func newDoQ(resolver *Resolver, addr string, proxyAdapter C.ProxyAdapter, proxyName string) (dnsClient, error) { doq := &dnsOverQUIC{ addr: addr, - proxyAdapter: adapter, + proxyAdapter: proxyAdapter, + proxyName: proxyName, r: resolver, quicConfig: &quic.Config{ KeepAlivePeriod: QUICKeepAlivePeriod, @@ -310,7 +313,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio // we're using bootstrapped address instead of what's passed to the function // it does not create an actual connection, but it helps us determine // what IP is actually reachable (when there're v4/v6 addresses). - rawConn, err := getDialHandler(doq.r, doq.proxyAdapter)(ctx, "udp", doq.addr) + rawConn, err := getDialHandler(doq.r, doq.proxyAdapter, doq.proxyName)(ctx, "udp", doq.addr) if err != nil { return nil, fmt.Errorf("failed to open a QUIC connection: %w", err) } @@ -325,7 +328,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn quic.Connectio p, err := strconv.Atoi(port) udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: p} - udp, err := listenPacket(ctx, doq.proxyAdapter, "udp", addr, doq.r) + udp, err := listenPacket(ctx, doq.proxyAdapter, doq.proxyName, "udp", addr, doq.r) if err != nil { return nil, err } diff --git a/dns/resolver.go b/dns/resolver.go index b5a09fd0..25bfedf0 100644 --- a/dns/resolver.go +++ b/dns/resolver.go @@ -100,7 +100,7 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ips []netip.Addr, ips, err = r.lookupIP(ctx, host, D.TypeA) var waitIPv6 *time.Timer - if r != nil { + if r != nil && r.ipv6Timeout > 0 { waitIPv6 = time.NewTimer(r.ipv6Timeout) } else { waitIPv6 = time.NewTimer(100 * time.Millisecond) @@ -421,7 +421,8 @@ type NameServer struct { Net string Addr string Interface *atomic.String - ProxyAdapter string + ProxyAdapter C.ProxyAdapter + ProxyName string Params map[string]string PreferH3 bool } @@ -544,3 +545,5 @@ func NewProxyServerHostResolver(old *Resolver) *Resolver { } return r } + +var ParseNameServer func(servers []string) ([]NameServer, error) // define in config/config.go diff --git a/dns/util.go b/dns/util.go index 4821195d..2fe85931 100644 --- a/dns/util.go +++ b/dns/util.go @@ -74,13 +74,13 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { for _, s := range servers { switch s.Net { case "https": - ret = append(ret, newDoHClient(s.Addr, resolver, s.PreferH3, s.Params, s.ProxyAdapter)) + 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 "quic": - if doq, err := newDoQ(resolver, s.Addr, s.ProxyAdapter); err == nil { + 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) @@ -103,6 +103,7 @@ func transform(servers []NameServer, resolver *Resolver) []dnsClient { iface: s.Interface, r: resolver, proxyAdapter: s.ProxyAdapter, + proxyName: s.ProxyName, }) } return ret @@ -144,9 +145,9 @@ func msgToDomain(msg *D.Msg) string { type dialHandler func(ctx context.Context, network, addr string) (net.Conn, error) -func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dialHandler { +func getDialHandler(r *Resolver, proxyAdapter C.ProxyAdapter, proxyName string, opts ...dialer.Option) dialHandler { return func(ctx context.Context, network, addr string) (net.Conn, error) { - if len(proxyAdapter) == 0 { + if len(proxyName) == 0 && proxyAdapter == nil { opts = append(opts, dialer.WithResolver(r)) return dialer.DialContext(ctx, network, addr, opts...) } else { @@ -154,10 +155,14 @@ func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dia if err != nil { return nil, err } - adapter, ok := tunnel.Proxies()[proxyAdapter] - if !ok { - opts = append(opts, dialer.WithInterface(proxyAdapter)) + if proxyAdapter == nil { + var ok bool + proxyAdapter, ok = tunnel.Proxies()[proxyName] + if !ok { + opts = append(opts, dialer.WithInterface(proxyName)) + } } + if strings.Contains(network, "tcp") { // tcp can resolve host by remote metadata := &C.Metadata{ @@ -165,8 +170,8 @@ func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dia Host: host, DstPort: port, } - if ok { - return adapter.DialContext(ctx, metadata, opts...) + if proxyAdapter != nil { + return proxyAdapter.DialContext(ctx, metadata, opts...) } opts = append(opts, dialer.WithResolver(r)) return dialer.DialContext(ctx, network, addr, opts...) @@ -182,15 +187,15 @@ func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dia DstIP: dstIP, DstPort: port, } - if !ok { + if proxyAdapter == nil { return dialer.DialContext(ctx, network, addr, opts...) } - if !adapter.SupportUDP() { + if !proxyAdapter.SupportUDP() { return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) } - packetConn, err := adapter.ListenPacketContext(ctx, metadata, opts...) + packetConn, err := proxyAdapter.ListenPacketContext(ctx, metadata, opts...) if err != nil { return nil, err } @@ -201,14 +206,17 @@ func getDialHandler(r *Resolver, proxyAdapter string, opts ...dialer.Option) dia } } -func listenPacket(ctx context.Context, proxyAdapter string, network string, addr string, r *Resolver, opts ...dialer.Option) (net.PacketConn, error) { +func listenPacket(ctx context.Context, proxyAdapter C.ProxyAdapter, proxyName string, network string, addr string, r *Resolver, opts ...dialer.Option) (net.PacketConn, error) { host, port, err := net.SplitHostPort(addr) if err != nil { return nil, err } - adapter, ok := tunnel.Proxies()[proxyAdapter] - if !ok && len(proxyAdapter) != 0 { - opts = append(opts, dialer.WithInterface(proxyAdapter)) + if proxyAdapter == nil { + var ok bool + proxyAdapter, ok = tunnel.Proxies()[proxyName] + if !ok { + opts = append(opts, dialer.WithInterface(proxyName)) + } } // udp must resolve host first @@ -222,15 +230,15 @@ func listenPacket(ctx context.Context, proxyAdapter string, network string, addr DstIP: dstIP, DstPort: port, } - if !ok { + if proxyAdapter == nil { return dialer.ListenPacket(ctx, dialer.ParseNetwork(network, dstIP), "", opts...) } - if !adapter.SupportUDP() { + if !proxyAdapter.SupportUDP() { return nil, fmt.Errorf("proxy adapter [%s] UDP is not supported", proxyAdapter) } - return adapter.ListenPacketContext(ctx, metadata, opts...) + return proxyAdapter.ListenPacketContext(ctx, metadata, opts...) } func batchExchange(ctx context.Context, clients []dnsClient, m *D.Msg) (msg *D.Msg, err error) { diff --git a/docs/config.yaml b/docs/config.yaml index 11891478..99f40ce7 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -631,6 +631,8 @@ proxies: # socks5 # reserved: [209,98,59] # 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接 # dialer-proxy: "ss1" + # remote-dns-resolve: true # 强制dns远程解析,默认值为false + # dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在remote-dns-resolve为true时生效 # 如果peers不为空,该段落中的allowed_ips不可为空;前面段落的server,port,ip,ipv6,public-key,pre-shared-key均会被忽略,但private-key会被保留且只能在顶层指定 # peers: # - server: 162.159.192.1