diff --git a/infra/conf/wireguard.go b/infra/conf/wireguard.go index 0e41c9c61..f489d3871 100644 --- a/infra/conf/wireguard.go +++ b/infra/conf/wireguard.go @@ -59,14 +59,13 @@ func (c *WireGuardPeerConfig) Build() (*wireguard.PeerConfig, error) { type WireGuardConfig struct { IsClient bool `json:""` - NoKernelTun bool `json:"noKernelTun"` - SecretKey string `json:"secretKey"` - Address []string `json:"address"` - Peers []*WireGuardPeerConfig `json:"peers"` - MTU int32 `json:"mtu"` - Reserved []byte `json:"reserved"` - DomainStrategy string `json:"domainStrategy"` - DNS []string `json:"remoteDNS"` + NoKernelTun bool `json:"noKernelTun"` + SecretKey string `json:"secretKey"` + Address []string `json:"address"` + Peers []*WireGuardPeerConfig `json:"peers"` + MTU int32 `json:"mtu"` + Reserved []byte `json:"reserved"` + DNS []string `json:"remoteDNS"` } func (c *WireGuardConfig) Build() (proto.Message, error) { @@ -125,21 +124,6 @@ func (c *WireGuardConfig) Build() (proto.Message, error) { } config.Reserved = c.Reserved - switch strings.ToLower(c.DomainStrategy) { - case "forceip", "": - config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP - case "forceipv4": - config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP4 - case "forceipv6": - config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP6 - case "forceipv4v6": - config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP46 - case "forceipv6v4": - config.DomainStrategy = wireguard.DeviceConfig_FORCE_IP64 - default: - return nil, errors.New("unsupported domain strategy: ", c.DomainStrategy) - } - config.IsClient = c.IsClient config.NoKernelTun = c.NoKernelTun config.DNS = c.DNS diff --git a/proxy/tun/handler.go b/proxy/tun/handler.go index 709fc1ec6..53c74a5e3 100644 --- a/proxy/tun/handler.go +++ b/proxy/tun/handler.go @@ -123,7 +123,7 @@ func (t *Handler) Start() error { iface := updater.Get() if iface == nil { errors.LogInfo(context.Background(), "[tun] falied to set interface > iface == nil") - return nil + return errors.New("iface not found") } return c.Control(func(fd uintptr) { addrPort, _ := netip.ParseAddrPort(address) diff --git a/proxy/wireguard/client.go b/proxy/wireguard/client.go index c43758eb8..4b4811f28 100644 --- a/proxy/wireguard/client.go +++ b/proxy/wireguard/client.go @@ -3,7 +3,6 @@ package wireguard import ( "context" "fmt" - gonet "net" "net/netip" "reflect" "strings" @@ -32,11 +31,6 @@ import ( "golang.zx2c4.com/wireguard/device" ) -type entry struct { - got []net.IP - time time.Time -} - type Handler struct { conf *DeviceConfig policyManager policy.Manager @@ -50,11 +44,6 @@ type Handler struct { tnet *Net dev *device.Device mu sync.Mutex - - // TODO: cache cleanup loop - local bool - cache map[string]entry - cacheMu sync.Mutex } func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { @@ -110,15 +99,10 @@ 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)) @@ -152,9 +136,6 @@ func NewClient(ctx context.Context, conf *DeviceConfig) (*Handler, error) { tun: tun, tnet: tnet, - - local: local, - cache: make(map[string]entry), }, nil } @@ -173,22 +154,6 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte return err } - var addr netip.Addr - if ob.Target.Address.Family().IsDomain() { - ip, err := h.resolveRemote(ob.Target.Address.String()) - if err != nil { - return errors.New("failed to resolve domain").Base(err) - } - addr, _ = netip.AddrFromSlice(ip) - } else { - addr, _ = netip.AddrFromSlice(ob.Target.Address.IP()) - } - - addrPort := netip.AddrPortFrom(addr, ob.Target.Port.Value()) - if !addrPort.IsValid() { - return errors.New("invalid target ", ob.Target) - } - var newCtx context.Context var newCancel context.CancelFunc if session.TimeoutOnlyFromContext(ctx) { @@ -217,10 +182,10 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte var err error if sessionPolicy.Timeouts.Handshake != 0 { timeoutCtx, timeoutCancel := context.WithTimeout(ctx, sessionPolicy.Timeouts.Handshake) - conn, err = h.tnet.DialContextTCPAddrPort(timeoutCtx, addrPort) + conn, err = h.tnet.DialContext(timeoutCtx, "tcp", ob.Target.NetAddr()) timeoutCancel() } else { - conn, err = h.tnet.DialContextTCPAddrPort(ctx, addrPort) + conn, err = h.tnet.Dial("tcp", ob.Target.NetAddr()) } if err != nil { return errors.New("failed to create TCP connection").Base(err) @@ -229,15 +194,14 @@ func (h *Handler) Process(ctx context.Context, link *transport.Link, dialer inte reader = buf.NewReader(conn) writer = buf.NewWriter(conn) case net.Network_UDP: - conn, err := h.tnet.DialUDPAddrPort(netip.AddrPort{}, addrPort) + conn, err := h.tnet.Dial("udp", ob.Target.NetAddr()) if err != nil { return errors.New("failed to create UDP connection").Base(err) } defer conn.Close() c := &udpConnClient{ - PacketConn: conn.(*internet.PacketConnWrapper).PacketConn, - resolveFunc: h.resolveRemote, - dest: gonet.UDPAddrFromAddrPort(addrPort), + PacketConn: conn.(*internet.PacketConnWrapper).PacketConn, + dest: conn.RemoteAddr().(*net.UDPAddr), } reader = c writer = c @@ -372,87 +336,48 @@ func (h *Handler) init(ctx context.Context) error { } func (h *Handler) resolveLocal(host string) (net.IP, error) { - 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 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 h.tnet.LookupHost(host) - }) -} - -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 - } - h.cacheMu.Lock() - if entry, ok := h.cache[host]; ok { - if time.Now().Before(entry.time) { - h.cacheMu.Unlock() - return entry.got[dice.Roll(len(entry.got))], nil - } - delete(h.cache, host) - } - h.cacheMu.Unlock() - ips, ttl, err := lookupIP(host) + ips, _, err := h.dns.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true}) if err != nil { return nil, err } - if len(ips) == 0 { - return nil, dns.ErrEmptyResponse - } - var got4, got6 []net.IP - for _, ip := range ips { - if ip.To4() != nil { - got4 = append(got4, ip) - } else { - got6 = append(got6, ip) + got := ips + if h.streamSettings.SocketSettings != nil { + var got4, got6 []net.IP + for _, ip := range ips { + if ip.To4() != nil { + got4 = append(got4, ip) + } else { + got6 = append(got6, ip) + } } - } - var got []net.IP - switch strategy { - case DeviceConfig_FORCE_IP: - got = ips - return ips[dice.Roll(len(ips))], nil - case DeviceConfig_FORCE_IP4: - got = got4 - case DeviceConfig_FORCE_IP6: - got = got6 - case DeviceConfig_FORCE_IP46: - got = got4 - if len(got) == 0 { - got = got6 - } - case DeviceConfig_FORCE_IP64: - got = got6 - if len(got) == 0 { + switch h.streamSettings.SocketSettings.DomainStrategy { + case internet.DomainStrategy_AS_IS, internet.DomainStrategy_USE_IP, internet.DomainStrategy_FORCE_IP: + got = ips + case internet.DomainStrategy_USE_IP4, internet.DomainStrategy_FORCE_IP4: got = got4 + case internet.DomainStrategy_USE_IP6, internet.DomainStrategy_FORCE_IP6: + got = got6 + case internet.DomainStrategy_USE_IP46, internet.DomainStrategy_FORCE_IP46: + got = got4 + if len(got) == 0 { + got = got6 + } + case internet.DomainStrategy_USE_IP64, internet.DomainStrategy_FORCE_IP64: + got = got6 + if len(got) == 0 { + got = got4 + } + } + if len(got) == 0 { + return nil, dns.ErrEmptyResponse } - default: - panic(strategy) } - if len(got) == 0 { - return nil, dns.ErrEmptyResponse - } - entry := entry{ - got: got, - time: time.Now().Add(time.Duration(ttl) * time.Second), - } - h.cacheMu.Lock() - h.cache[host] = entry - h.cacheMu.Unlock() return got[dice.Roll(len(got))], nil } type udpConnClient struct { net.PacketConn - resolveFunc func(host string) (net.IP, error) - dest *net.UDPAddr + dest *net.UDPAddr } func (c *udpConnClient) ReadMultiBuffer() (buf.MultiBuffer, error) { @@ -479,15 +404,8 @@ func (c *udpConnClient) WriteMultiBuffer(mb buf.MultiBuffer) error { dst := c.dest if b.UDP != nil { if b.UDP.Address.Family().IsDomain() { - ip, err := c.resolveFunc(b.UDP.Address.String()) - if err != nil { - errors.LogErrorInner(context.Background(), err, "drop packet to ", b.UDP, " with size ", len(b.Bytes())) - b.Release() - continue - } - dst = &net.UDPAddr{ - IP: ip, - Port: int(b.UDP.Port), + if b.UDP.Port != net.Port(dst.Port) { + dst = &net.UDPAddr{IP: dst.IP, Port: int(b.UDP.Port)} } } else { dst = b.UDP.RawNetAddr().(*net.UDPAddr) @@ -524,3 +442,59 @@ func (c *PacketCounterConnection) WriteTo(p []byte, addr net.Addr) (n int, err e } return } + +type entry struct { + saddr []string + deadline time.Time +} + +type cache struct { + running bool + m map[string]entry + mu sync.Mutex +} + +func (c *cache) run() { + if c.running { + return + } + c.running = true + c.m = make(map[string]entry) + go c.gc() +} + +func (c *cache) gc() { + ticker := time.NewTicker(time.Minute) + for { + now := <-ticker.C + c.mu.Lock() + for key, entry := range c.m { + if now.After(entry.deadline) { + delete(c.m, key) + } + } + c.mu.Unlock() + } +} + +func (c *cache) LookupHost(host string) []string { + c.mu.Lock() + defer c.mu.Unlock() + c.run() + if entry, ok := c.m[host]; ok { + if time.Now().Before(entry.deadline) { + return entry.saddr + } + delete(c.m, host) + } + return nil +} + +func (c *cache) Cache(host string, saddr []string, ttl uint32) { + c.mu.Lock() + defer c.mu.Unlock() + c.m[host] = entry{ + saddr: saddr, + deadline: time.Now().Add(time.Second * time.Duration(ttl)), + } +} diff --git a/proxy/wireguard/config.pb.go b/proxy/wireguard/config.pb.go index 4f434d1fc..9b36168d7 100644 --- a/proxy/wireguard/config.pb.go +++ b/proxy/wireguard/config.pb.go @@ -22,61 +22,6 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type DeviceConfig_DomainStrategy int32 - -const ( - DeviceConfig_FORCE_IP DeviceConfig_DomainStrategy = 0 - DeviceConfig_FORCE_IP4 DeviceConfig_DomainStrategy = 1 - DeviceConfig_FORCE_IP6 DeviceConfig_DomainStrategy = 2 - DeviceConfig_FORCE_IP46 DeviceConfig_DomainStrategy = 3 - DeviceConfig_FORCE_IP64 DeviceConfig_DomainStrategy = 4 -) - -// Enum value maps for DeviceConfig_DomainStrategy. -var ( - DeviceConfig_DomainStrategy_name = map[int32]string{ - 0: "FORCE_IP", - 1: "FORCE_IP4", - 2: "FORCE_IP6", - 3: "FORCE_IP46", - 4: "FORCE_IP64", - } - DeviceConfig_DomainStrategy_value = map[string]int32{ - "FORCE_IP": 0, - "FORCE_IP4": 1, - "FORCE_IP6": 2, - "FORCE_IP46": 3, - "FORCE_IP64": 4, - } -) - -func (x DeviceConfig_DomainStrategy) Enum() *DeviceConfig_DomainStrategy { - p := new(DeviceConfig_DomainStrategy) - *p = x - return p -} - -func (x DeviceConfig_DomainStrategy) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (DeviceConfig_DomainStrategy) Descriptor() protoreflect.EnumDescriptor { - return file_proxy_wireguard_config_proto_enumTypes[0].Descriptor() -} - -func (DeviceConfig_DomainStrategy) Type() protoreflect.EnumType { - return &file_proxy_wireguard_config_proto_enumTypes[0] -} - -func (x DeviceConfig_DomainStrategy) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use DeviceConfig_DomainStrategy.Descriptor instead. -func (DeviceConfig_DomainStrategy) EnumDescriptor() ([]byte, []int) { - return file_proxy_wireguard_config_proto_rawDescGZIP(), []int{1, 0} -} - type PeerConfig struct { state protoimpl.MessageState `protogen:"open.v1"` PublicKey string `protobuf:"bytes,1,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` @@ -154,19 +99,18 @@ func (x *PeerConfig) GetAllowedIps() []string { } type DeviceConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` - Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"` - Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"` - Users []*protocol.User `protobuf:"bytes,5,rep,name=users,proto3" json:"users,omitempty"` - Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"` - Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,omitempty"` - 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 + state protoimpl.MessageState `protogen:"open.v1"` + SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` + Endpoint []string `protobuf:"bytes,2,rep,name=endpoint,proto3" json:"endpoint,omitempty"` + Peers []*PeerConfig `protobuf:"bytes,3,rep,name=peers,proto3" json:"peers,omitempty"` + Users []*protocol.User `protobuf:"bytes,5,rep,name=users,proto3" json:"users,omitempty"` + Mtu int32 `protobuf:"varint,4,opt,name=mtu,proto3" json:"mtu,omitempty"` + Reserved []byte `protobuf:"bytes,6,opt,name=reserved,proto3" json:"reserved,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 } func (x *DeviceConfig) Reset() { @@ -241,13 +185,6 @@ func (x *DeviceConfig) GetReserved() []byte { return nil } -func (x *DeviceConfig) GetDomainStrategy() DeviceConfig_DomainStrategy { - if x != nil { - return x.DomainStrategy - } - return DeviceConfig_FORCE_IP -} - func (x *DeviceConfig) GetIsClient() bool { if x != nil { return x.IsClient @@ -283,7 +220,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\"\xee\x03\n" + + "allowedIps\"\xb4\x02\n" + "\fDeviceConfig\x12\x1d\n" + "\n" + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12\x1a\n" + @@ -291,20 +228,11 @@ const file_proxy_wireguard_config_proto_rawDesc = "" + "\x05peers\x18\x03 \x03(\v2 .xray.proxy.wireguard.PeerConfigR\x05peers\x120\n" + "\x05users\x18\x05 \x03(\v2\x1a.xray.common.protocol.UserR\x05users\x12\x10\n" + "\x03mtu\x18\x04 \x01(\x05R\x03mtu\x12\x1a\n" + - "\breserved\x18\x06 \x01(\fR\breserved\x12Z\n" + - "\x0fdomain_strategy\x18\a \x01(\x0e21.xray.proxy.wireguard.DeviceConfig.DomainStrategyR\x0edomainStrategy\x12\x1b\n" + + "\breserved\x18\x06 \x01(\fR\breserved\x12\x1b\n" + "\tis_client\x18\b \x01(\bR\bisClient\x12\"\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" + - "\tFORCE_IP6\x10\x02\x12\x0e\n" + - "\n" + - "FORCE_IP46\x10\x03\x12\x0e\n" + - "\n" + - "FORCE_IP64\x10\x04B^\n" + + " \x03(\tR\x03DNSB^\n" + "\x18com.xray.proxy.wireguardP\x01Z)github.com/xtls/xray-core/proxy/wireguard\xaa\x02\x14Xray.Proxy.WireGuardb\x06proto3" var ( @@ -319,23 +247,20 @@ func file_proxy_wireguard_config_proto_rawDescGZIP() []byte { return file_proxy_wireguard_config_proto_rawDescData } -var file_proxy_wireguard_config_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_proxy_wireguard_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_proxy_wireguard_config_proto_goTypes = []any{ - (DeviceConfig_DomainStrategy)(0), // 0: xray.proxy.wireguard.DeviceConfig.DomainStrategy - (*PeerConfig)(nil), // 1: xray.proxy.wireguard.PeerConfig - (*DeviceConfig)(nil), // 2: xray.proxy.wireguard.DeviceConfig - (*protocol.User)(nil), // 3: xray.common.protocol.User + (*PeerConfig)(nil), // 0: xray.proxy.wireguard.PeerConfig + (*DeviceConfig)(nil), // 1: xray.proxy.wireguard.DeviceConfig + (*protocol.User)(nil), // 2: xray.common.protocol.User } var file_proxy_wireguard_config_proto_depIdxs = []int32{ - 1, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig - 3, // 1: xray.proxy.wireguard.DeviceConfig.users:type_name -> xray.common.protocol.User - 0, // 2: xray.proxy.wireguard.DeviceConfig.domain_strategy:type_name -> xray.proxy.wireguard.DeviceConfig.DomainStrategy - 3, // [3:3] is the sub-list for method output_type - 3, // [3:3] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 0, // 0: xray.proxy.wireguard.DeviceConfig.peers:type_name -> xray.proxy.wireguard.PeerConfig + 2, // 1: xray.proxy.wireguard.DeviceConfig.users:type_name -> xray.common.protocol.User + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_proxy_wireguard_config_proto_init() } @@ -348,14 +273,13 @@ func file_proxy_wireguard_config_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proxy_wireguard_config_proto_rawDesc), len(file_proxy_wireguard_config_proto_rawDesc)), - NumEnums: 1, + NumEnums: 0, NumMessages: 2, NumExtensions: 0, NumServices: 0, }, GoTypes: file_proxy_wireguard_config_proto_goTypes, DependencyIndexes: file_proxy_wireguard_config_proto_depIdxs, - EnumInfos: file_proxy_wireguard_config_proto_enumTypes, MessageInfos: file_proxy_wireguard_config_proto_msgTypes, }.Build() File_proxy_wireguard_config_proto = out.File diff --git a/proxy/wireguard/config.proto b/proxy/wireguard/config.proto index 628059827..7aec161ff 100644 --- a/proxy/wireguard/config.proto +++ b/proxy/wireguard/config.proto @@ -17,13 +17,6 @@ message PeerConfig { } message DeviceConfig { - enum DomainStrategy { - FORCE_IP = 0; - FORCE_IP4 = 1; - FORCE_IP6 = 2; - FORCE_IP46 = 3; - FORCE_IP64 = 4; - } string secret_key = 1; repeated string endpoint = 2; repeated PeerConfig peers = 3; @@ -31,7 +24,6 @@ message DeviceConfig { int32 mtu = 4; bytes reserved = 6; - DomainStrategy domain_strategy = 7; bool is_client = 8; bool no_kernel_tun = 9; repeated string DNS = 10; diff --git a/proxy/wireguard/netstack.go b/proxy/wireguard/netstack.go index 813f46790..e5a15cfb9 100644 --- a/proxy/wireguard/netstack.go +++ b/proxy/wireguard/netstack.go @@ -15,6 +15,8 @@ import ( "net" "net/netip" "os" + "regexp" + "strconv" "strings" "syscall" "time" @@ -219,6 +221,7 @@ type Net struct { DialUDPAddrPort func(laddr, raddr netip.AddrPort) (net.Conn, error) dnsServers []netip.Addr hasV4, hasV6 bool + cache cache } func convertToFullAddr(endpoint netip.AddrPort) (tcpip.FullAddress, tcpip.NetworkProtocolNumber) { @@ -246,9 +249,12 @@ var ( errServerTemporarilyMisbehaving = errors.New("server misbehaving") errCanceled = errors.New("operation was canceled") errTimeout = errors.New("i/o timeout") + errNumericPort = errors.New("port must be numeric") + errNoSuitableAddress = errors.New("no suitable address found") + errMissingAddress = errors.New("missing address") ) -func (net *Net) LookupHost(host string) (addrs []net.IP, ttl uint32, err error) { +func (net *Net) LookupHost(host string) (addrs []string, err error) { return net.LookupContextHost(context.Background(), host) } @@ -567,9 +573,12 @@ 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) ([]net.IP, uint32, error) { +func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]string, error) { + if saddr := tnet.cache.LookupHost(host); saddr != nil { + return saddr, nil + } if host == "" || (!tnet.hasV6 && !tnet.hasV4) { - return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} } zlen := len(host) if strings.IndexByte(host, ':') != -1 { @@ -578,11 +587,11 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, } } if ip, err := netip.ParseAddr(host[:zlen]); err == nil { - return []net.IP{ip.AsSlice()}, 0, nil + return []string{ip.String()}, nil } if !isDomainName(host) { - return nil, 0, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} + return nil, &net.DNSError{Err: errNoSuchHost.Error(), Name: host, IsNotFound: true} } type result struct { p dnsmessage.Parser @@ -683,11 +692,137 @@ func (tnet *Net) LookupContextHost(ctx context.Context, host string) ([]net.IP, } if len(addrs) == 0 && lastErr != nil { - return nil, 0, lastErr + return nil, lastErr } - ips := make([]net.IP, 0, len(addrs)) + saddrs := make([]string, 0, len(addrs)) for _, ip := range addrs { - ips = append(ips, ip.AsSlice()) + saddrs = append(saddrs, ip.String()) } - return ips, ttl, nil + tnet.cache.Cache(host, saddrs, ttl) + return saddrs, nil +} + +func partialDeadline(now, deadline time.Time, addrsRemaining int) (time.Time, error) { + if deadline.IsZero() { + return deadline, nil + } + timeRemaining := deadline.Sub(now) + if timeRemaining <= 0 { + return time.Time{}, errTimeout + } + timeout := timeRemaining / time.Duration(addrsRemaining) + const saneMinimum = 2 * time.Second + if timeout < saneMinimum { + if timeRemaining < saneMinimum { + timeout = timeRemaining + } else { + timeout = saneMinimum + } + } + return now.Add(timeout), nil +} + +var protoSplitter = regexp.MustCompile(`^(tcp|udp|ping)(4|6)?$`) + +func (tnet *Net) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + if ctx == nil { + panic("nil context") + } + var acceptV4, acceptV6 bool + matches := protoSplitter.FindStringSubmatch(network) + if matches == nil { + return nil, &net.OpError{Op: "dial", Err: net.UnknownNetworkError(network)} + } else if len(matches[2]) == 0 { + acceptV4 = true + acceptV6 = true + } else { + acceptV4 = matches[2][0] == '4' + acceptV6 = !acceptV4 + } + var host string + var port int + if matches[1] == "ping" { + host = address + } else { + var sport string + var err error + host, sport, err = net.SplitHostPort(address) + if err != nil { + return nil, &net.OpError{Op: "dial", Err: err} + } + port, err = strconv.Atoi(sport) + if err != nil || port < 0 || port > 65535 { + return nil, &net.OpError{Op: "dial", Err: errNumericPort} + } + } + allAddr, err := tnet.LookupContextHost(ctx, host) + if err != nil { + return nil, &net.OpError{Op: "dial", Err: err} + } + var addrs []netip.AddrPort + for _, addr := range allAddr { + ip, err := netip.ParseAddr(addr) + if err == nil && ((ip.Is4() && acceptV4) || (ip.Is6() && acceptV6)) { + addrs = append(addrs, netip.AddrPortFrom(ip, uint16(port))) + } + } + if len(addrs) == 0 && len(allAddr) != 0 { + return nil, &net.OpError{Op: "dial", Err: errNoSuitableAddress} + } + + var firstErr error + for i, addr := range addrs { + select { + case <-ctx.Done(): + err := ctx.Err() + if err == context.Canceled { + err = errCanceled + } else if err == context.DeadlineExceeded { + err = errTimeout + } + return nil, &net.OpError{Op: "dial", Err: err} + default: + } + + dialCtx := ctx + if deadline, hasDeadline := ctx.Deadline(); hasDeadline { + partialDeadline, err := partialDeadline(time.Now(), deadline, len(addrs)-i) + if err != nil { + if firstErr == nil { + firstErr = &net.OpError{Op: "dial", Err: err} + } + break + } + if partialDeadline.Before(deadline) { + var cancel context.CancelFunc + dialCtx, cancel = context.WithDeadline(ctx, partialDeadline) + defer cancel() + } + } + + var c net.Conn + switch matches[1] { + case "tcp": + c, err = tnet.DialContextTCPAddrPort(dialCtx, addr) + case "udp": + c, err = tnet.DialUDPAddrPort(netip.AddrPort{}, addr) + case "ping": + err = errors.New("not support") + // c, err = tnet.DialPingAddr(netip.Addr{}, addr.Addr()) + } + if err == nil { + return c, nil + } + if firstErr == nil { + firstErr = err + } + } + if firstErr == nil { + firstErr = &net.OpError{Op: "dial", Err: errMissingAddress} + } + return nil, firstErr +} + +func (tnet *Net) Dial(network, address string) (net.Conn, error) { + return tnet.DialContext(context.Background(), network, address) }