From fa6ca7820da521a72e2ed5f020d29816407c24e9 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 14 Aug 2026 14:26:02 +0800 Subject: [PATCH 1/3] wg-dns --- infra/conf/wireguard.go | 2 + proxy/wireguard/client.go | 73 +++++++++++++++++++++++++++--------- proxy/wireguard/config.pb.go | 14 ++++++- proxy/wireguard/config.proto | 1 + proxy/wireguard/netstack.go | 21 ++++++----- 5 files changed, 82 insertions(+), 29 deletions(-) diff --git a/infra/conf/wireguard.go b/infra/conf/wireguard.go index 8131be28a969..0e41c9c61f8d 100644 --- a/infra/conf/wireguard.go +++ b/infra/conf/wireguard.go @@ -66,6 +66,7 @@ type WireGuardConfig struct { MTU int32 `json:"mtu"` Reserved []byte `json:"reserved"` DomainStrategy string `json:"domainStrategy"` + DNS []string `json:"remoteDNS"` } func (c *WireGuardConfig) Build() (proto.Message, error) { @@ -141,6 +142,7 @@ func (c *WireGuardConfig) Build() (proto.Message, error) { config.IsClient = c.IsClient config.NoKernelTun = c.NoKernelTun + config.DNS = c.DNS return config, nil } diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index 59709b4bf915..bd5f7d113199 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -5,9 +5,10 @@ import ( "fmt" gonet "net" "net/netip" - reflect "reflect" + "reflect" "strings" "sync" + "time" "golang.zx2c4.com/wireguard/tun" @@ -30,6 +31,11 @@ import ( "golang.zx2c4.com/wireguard/device" ) +type entry struct { + ip net.IP + time time.Time +} + type Handler struct { conf *DeviceConfig policyManager policy.Manager @@ -43,6 +49,10 @@ type Handler struct { tnet *Net dev *device.Device mu sync.Mutex + + local bool + cache map[string]entry + cacheMu sync.Mutex } func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { @@ -98,6 +108,20 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { return nil, err } + local := false + dns := conf.DNS + if len(dns) == 0 { + dns = []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001"} + } + if len(dns) == 1 && dns[0] == "local" { + local = true + dns = nil + } + dnses := make([]netip.Addr, 0, len(dns)) + for _, dns := range dns { + dnses = append(dnses, netip.MustParseAddr(dns)) + } + kernelTunSupported, err := KernelTunSupported() if err != nil { errors.LogWarningInner(context.Background(), err, "Failed to check kernel TUN support") @@ -106,10 +130,10 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { var tnet *Net if !conf.NoKernelTun && kernelTunSupported { errors.LogWarning(context.Background(), "Using kernel TUN") - tun, tnet, err = createKernelTun(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu)) + tun, tnet, err = createKernelTun(localAddresses, dnses, int(conf.Mtu)) } else { errors.LogWarning(context.Background(), "Using gVisor TUN") - tun, tnet, _, err = CreateNetTUN(localAddresses, []netip.Addr{netip.MustParseAddr("1.1.1.1"), netip.MustParseAddr("1.0.0.1"), netip.MustParseAddr("2606:4700:4700::1111"), netip.MustParseAddr("2606:4700:4700::1001")}, int(conf.Mtu), true) + tun, tnet, _, err = CreateNetTUN(localAddresses, dnses, int(conf.Mtu), true) } if err != nil { return nil, err @@ -126,6 +150,9 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { tun: tun, tnet: tnet, + + local: local, + cache: make(map[string]entry), }, nil } @@ -343,31 +370,34 @@ func (h *Handler) init(ctx context.Context) error { } func (h *Handler) resolveLocal(host string) (net.IP, error) { - return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) { - ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) - return ips, err + return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) { + return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) }) } func (h *Handler) resolveRemote(host string) (net.IP, error) { - return resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, error) { - addrs, err := h.tnet.LookupHost(host) - if err != nil { - return nil, err - } - ips := make([]net.IP, 0, len(addrs)) - for _, addr := range addrs { - ips = append(ips, net.ParseIP(addr)) + return h.resolveDomain(host, h.conf.DomainStrategy, func(host string) ([]net.IP, uint32, error) { + if h.local { + return h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) } - return ips, nil + return h.tnet.LookupHost(host) }) } -func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, error)) (net.IP, error) { +func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP func(host string) ([]net.IP, uint32, error)) (net.IP, error) { if ip := net.ParseIP(host); ip != nil { return ip, nil } - ips, err := lookupIP(host) + h.cacheMu.Lock() + if entry, ok := h.cache[host]; ok { + if time.Now().Before(entry.time) { + h.cacheMu.Unlock() + return entry.ip, nil + } + delete(h.cache, host) + } + h.cacheMu.Unlock() + ips, ttl, err := lookupIP(host) if err != nil { return nil, err } @@ -407,7 +437,14 @@ func resolveDomain(host string, strategy DeviceConfig_DomainStrategy, lookupIP f if len(got) == 0 { return nil, dns.ErrEmptyResponse } - return got[dice.Roll(len(got))], nil + entry := entry{ + ip: got[dice.Roll(len(got))], + time: time.Now().Add(time.Duration(ttl) * time.Second), + } + h.cacheMu.Lock() + h.cache[host] = entry + h.cacheMu.Unlock() + return entry.ip, nil } type udpConnClient struct { diff --git a/proxy/wireguard/config.pb.go b/proxy/wireguard/config.pb.go index 59db6564f7bd..4f434d1fc7aa 100644 --- a/proxy/wireguard/config.pb.go +++ b/proxy/wireguard/config.pb.go @@ -164,6 +164,7 @@ type DeviceConfig struct { DomainStrategy DeviceConfig_DomainStrategy `protobuf:"varint,7,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.proxy.wireguard.DeviceConfig_DomainStrategy" json:"domain_strategy,omitempty"` IsClient bool `protobuf:"varint,8,opt,name=is_client,json=isClient,proto3" json:"is_client,omitempty"` NoKernelTun bool `protobuf:"varint,9,opt,name=no_kernel_tun,json=noKernelTun,proto3" json:"no_kernel_tun,omitempty"` + DNS []string `protobuf:"bytes,10,rep,name=DNS,proto3" json:"DNS,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -261,6 +262,13 @@ func (x *DeviceConfig) GetNoKernelTun() bool { return false } +func (x *DeviceConfig) GetDNS() []string { + if x != nil { + return x.DNS + } + return nil +} + var File_proxy_wireguard_config_proto protoreflect.FileDescriptor const file_proxy_wireguard_config_proto_rawDesc = "" + @@ -275,7 +283,7 @@ const file_proxy_wireguard_config_proto_rawDesc = "" + "\n" + "keep_alive\x18\x04 \x01(\tR\tkeepAlive\x12\x1f\n" + "\vallowed_ips\x18\x05 \x03(\tR\n" + - "allowedIps\"\xdc\x03\n" + + "allowedIps\"\xee\x03\n" + "\fDeviceConfig\x12\x1d\n" + "\n" + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" + @@ -286,7 +294,9 @@ const file_proxy_wireguard_config_proto_rawDesc = "" + "\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" + "\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" + "\tis_client\x18\b \x01(\bR\bisClient\x12\"\n" + - "\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\"\\\n" + + "\rno_kernel_tun\x18\t \x01(\bR\vnoKernelTun\x12\x10\n" + + "\x03DNS\x18\n" + + " \x03(\tR\x03DNS\"\\\n" + "\x0eDomainStrategy\x12\f\n" + "\bFORCE_IP\x10\x00\x12\r\n" + "\tFORCE_IP4\x10\x01\x12\r\n" + diff --git a/proxy/wireguard/config.proto b/proxy/wireguard/config.proto index 171d5b21aef5..628059827441 100644 --- a/proxy/wireguard/config.proto +++ b/proxy/wireguard/config.proto @@ -34,4 +34,5 @@ message DeviceConfig { DomainStrategy domain_strategy = 7; bool is_client = 8; bool no_kernel_tun = 9; + repeated string DNS = 10; } \ No newline at end of file diff --git a/proxy/wireguard/netstack.go b/proxy/wireguard/netstack.go index 22c42e3f9f68..813f46790571 100644 --- a/proxy/wireguard/netstack.go +++ b/proxy/wireguard/netstack.go @@ -248,7 +248,7 @@ var ( errTimeout = errors.New("i/o timeout") ) -func (net *Net) LookupHost(host string) (addrs []string, err error) { +func (net *Net) LookupHost(host string) (addrs []net.IP, ttl uint32, err error) { return net.LookupContextHost(context.Background(), host) } @@ -567,9 +567,9 @@ func (tnet *Net) tryOneName(ctx context.Context, name string, qtype dnsmessage.T return dnsmessage.Parser{}, "", lastErr } -func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) { +func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, uint32, error) { if host == "" || (!tnet.hasV6 && !tnet.hasV4) { - return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} } zlen := len(host) if strings.IndexByte(host, ':') != -1 { @@ -578,11 +578,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, } } if ip, err := netip.ParseAddr(host[:zlen]); err == nil { - return []string{ip.String()}, nil + return []net.IP{ip.AsSlice()}, 0, nil } if !isDomainName(host) { - return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} } type result struct { p dnsmessage.Parser @@ -611,6 +611,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, lane <- result{p, server, err} }() } + ttl := uint32(300) for l := 0; l < lanes; l++ { result := <-lane if result.error != nil { @@ -644,6 +645,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, } break loop } + ttl = min(ttl, h.TTL) addrsV4 = append(addrsV4, netip.AddrFrom4(a.A)) case dnsmessage.TypeAAAA: @@ -656,6 +658,7 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, } break loop } + ttl = min(ttl, h.TTL) addrsV6 = append(addrsV6, netip.AddrFrom16(aaaa.AAAA)) default: @@ -680,11 +683,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, } if len(addrs) == 0 && lastErr != nil { - return nil, lastErr + return nil, 0, lastErr } - saddrs := make([]string, 0, len(addrs)) + ips := make([]net.IP, 0, len(addrs)) for _, ip := range addrs { - saddrs = append(saddrs, ip.String()) + ips = append(ips, ip.AsSlice()) } - return saddrs, nil + return ips, ttl, nil } From ebdd8f98a10dbc7f843d570469ce54378c4c14d5 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 14 Aug 2026 18:54:56 +0800 Subject: [PATCH 2/3] cache got --- proxy/wireguard/client.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index bd5f7d113199..b3e7fa75b8eb 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -32,7 +32,7 @@ import ( ) type entry struct { - ip net.IP + got []net.IP time time.Time } @@ -392,7 +392,7 @@ func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrateg if entry, ok := h.cache[host]; ok { if time.Now().Before(entry.time) { h.cacheMu.Unlock() - return entry.ip, nil + return entry.got[dice.Roll(len(entry.got))], nil } delete(h.cache, host) } @@ -438,13 +438,13 @@ func (h *Handler) resolveDomain(host string, strategy DeviceConfig_DomainStrateg return nil, dns.ErrEmptyResponse } entry := entry{ - ip: got[dice.Roll(len(got))], + got: got, time: time.Now().Add(time.Duration(ttl) * time.Second), } h.cacheMu.Lock() h.cache[host] = entry h.cacheMu.Unlock() - return entry.ip, nil + return got[dice.Roll(len(got))], nil } type udpConnClient struct { From a28d95e1d3272f280763e2e4906b257d5933c806 Mon Sep 17 00:00:00 2001 From: null Date: Fri, 14 Aug 2026 19:04:56 +0800 Subject: [PATCH 3/3] TODO --- proxy/wireguard/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index b3e7fa75b8eb..ef0cc1e2a8c4 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -50,6 +50,7 @@ type Handler struct { dev *device.Device mu sync.Mutex + // TODO: cache cleanup loop local bool cache map[string]entry cacheMu sync.Mutex