Browse Source

Outbound: Add `targetStrategy`; Fix mux does not close `link.Reader`; Fix `origin` does not work on UDP; Add logs (#5006)

patterniha 4 months ago
parent
commit
f557bf7da4

+ 5 - 2
app/dns/dnscommon.go

@@ -42,12 +42,15 @@ func (r *IPRecord) getIPs() ([]net.IP, uint32, error) {
 	if r == nil {
 		return nil, 0, errRecordNotFound
 	}
-	untilExpire := time.Until(r.Expire)
+	untilExpire := time.Until(r.Expire).Seconds()
 	if untilExpire <= 0 {
 		return nil, 0, errRecordNotFound
 	}
 
-	ttl := uint32(untilExpire/time.Second) + uint32(1)
+	ttl := uint32(untilExpire) + 1
+	if ttl == 1 {
+		r.Expire = time.Now().Add(time.Second) // To ensure that two consecutive requests get the same result
+	}
 	if r.RCode != dnsmessage.RCodeSuccess {
 		return nil, ttl, dns_feature.RCodeError(r.RCode)
 	}

+ 43 - 28
app/proxyman/config.pb.go

@@ -449,11 +449,12 @@ type SenderConfig struct {
 	unknownFields protoimpl.UnknownFields
 
 	// Send traffic through the given IP. Only IP is allowed.
-	Via               *net.IPOrDomain        `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
-	StreamSettings    *internet.StreamConfig `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
-	ProxySettings     *internet.ProxyConfig  `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"`
-	MultiplexSettings *MultiplexingConfig    `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
-	ViaCidr           string                 `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
+	Via               *net.IPOrDomain         `protobuf:"bytes,1,opt,name=via,proto3" json:"via,omitempty"`
+	StreamSettings    *internet.StreamConfig  `protobuf:"bytes,2,opt,name=stream_settings,json=streamSettings,proto3" json:"stream_settings,omitempty"`
+	ProxySettings     *internet.ProxyConfig   `protobuf:"bytes,3,opt,name=proxy_settings,json=proxySettings,proto3" json:"proxy_settings,omitempty"`
+	MultiplexSettings *MultiplexingConfig     `protobuf:"bytes,4,opt,name=multiplex_settings,json=multiplexSettings,proto3" json:"multiplex_settings,omitempty"`
+	ViaCidr           string                  `protobuf:"bytes,5,opt,name=via_cidr,json=viaCidr,proto3" json:"via_cidr,omitempty"`
+	TargetStrategy    internet.DomainStrategy `protobuf:"varint,6,opt,name=target_strategy,json=targetStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"target_strategy,omitempty"`
 }
 
 func (x *SenderConfig) Reset() {
@@ -521,6 +522,13 @@ func (x *SenderConfig) GetViaCidr() string {
 	return ""
 }
 
+func (x *SenderConfig) GetTargetStrategy() internet.DomainStrategy {
+	if x != nil {
+		return x.TargetStrategy
+	}
+	return internet.DomainStrategy(0)
+}
+
 type MultiplexingConfig struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -779,7 +787,7 @@ var file_app_proxyman_config_proto_rawDesc = []byte{
 	0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x73, 0x65, 0x72, 0x69, 0x61, 0x6c, 0x2e, 0x54, 0x79, 0x70, 0x65,
 	0x64, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x53,
 	0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x10, 0x0a, 0x0e, 0x4f, 0x75, 0x74, 0x62, 0x6f,
-	0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xcb, 0x02, 0x0a, 0x0c, 0x53, 0x65,
+	0x75, 0x6e, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x9d, 0x03, 0x0a, 0x0c, 0x53, 0x65,
 	0x6e, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x0a, 0x03, 0x76, 0x69,
 	0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x63,
 	0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, 0x6f,
@@ -800,23 +808,28 @@ var file_app_proxyman_config_proto_rawDesc = []byte{
 	0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x11, 0x6d, 0x75, 0x6c, 0x74, 0x69,
 	0x70, 0x6c, 0x65, 0x78, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x19, 0x0a, 0x08,
 	0x76, 0x69, 0x61, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
-	0x76, 0x69, 0x61, 0x43, 0x69, 0x64, 0x72, 0x22, 0xa4, 0x01, 0x0a, 0x12, 0x4d, 0x75, 0x6c, 0x74,
-	0x69, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18,
-	0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
-	0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x63,
-	0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x63,
-	0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x78, 0x75,
-	0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x03, 0x20,
-	0x01, 0x28, 0x05, 0x52, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72,
-	0x65, 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x50, 0x72, 0x6f, 0x78,
-	0x79, 0x55, 0x44, 0x50, 0x34, 0x34, 0x33, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x78,
-	0x75, 0x64, 0x70, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x44, 0x50, 0x34, 0x34, 0x33, 0x42, 0x55,
-	0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70, 0x2e, 0x70,
-	0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x50, 0x01, 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75,
-	0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d,
-	0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61,
-	0x6e, 0xaa, 0x02, 0x11, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50, 0x72, 0x6f,
-	0x78, 0x79, 0x6d, 0x61, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x76, 0x69, 0x61, 0x43, 0x69, 0x64, 0x72, 0x12, 0x50, 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65,
+	0x74, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e,
+	0x32, 0x27, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72,
+	0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x44, 0x6f, 0x6d, 0x61, 0x69,
+	0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65,
+	0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x22, 0xa4, 0x01, 0x0a, 0x12, 0x4d, 0x75,
+	0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x78, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67,
+	0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x63, 0x6f,
+	0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52,
+	0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f,
+	0x78, 0x75, 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x43, 0x6f, 0x6e, 0x63, 0x75,
+	0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x28, 0x0a, 0x0f, 0x78, 0x75, 0x64, 0x70, 0x50, 0x72,
+	0x6f, 0x78, 0x79, 0x55, 0x44, 0x50, 0x34, 0x34, 0x33, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x0f, 0x78, 0x75, 0x64, 0x70, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x55, 0x44, 0x50, 0x34, 0x34, 0x33,
+	0x42, 0x55, 0x0a, 0x15, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x61, 0x70, 0x70,
+	0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x50, 0x01, 0x5a, 0x26, 0x67, 0x69, 0x74,
+	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61,
+	0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x61, 0x70, 0x70, 0x2f, 0x70, 0x72, 0x6f, 0x78, 0x79,
+	0x6d, 0x61, 0x6e, 0xaa, 0x02, 0x11, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x41, 0x70, 0x70, 0x2e, 0x50,
+	0x72, 0x6f, 0x78, 0x79, 0x6d, 0x61, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
 var (
@@ -850,6 +863,7 @@ var file_app_proxyman_config_proto_goTypes = []any{
 	(*internet.StreamConfig)(nil),                            // 13: xray.transport.internet.StreamConfig
 	(*serial.TypedMessage)(nil),                              // 14: xray.common.serial.TypedMessage
 	(*internet.ProxyConfig)(nil),                             // 15: xray.transport.internet.ProxyConfig
+	(internet.DomainStrategy)(0),                             // 16: xray.transport.internet.DomainStrategy
 }
 var file_app_proxyman_config_proto_depIdxs = []int32{
 	0,  // 0: xray.app.proxyman.AllocationStrategy.type:type_name -> xray.app.proxyman.AllocationStrategy.Type
@@ -866,11 +880,12 @@ var file_app_proxyman_config_proto_depIdxs = []int32{
 	13, // 11: xray.app.proxyman.SenderConfig.stream_settings:type_name -> xray.transport.internet.StreamConfig
 	15, // 12: xray.app.proxyman.SenderConfig.proxy_settings:type_name -> xray.transport.internet.ProxyConfig
 	8,  // 13: xray.app.proxyman.SenderConfig.multiplex_settings:type_name -> xray.app.proxyman.MultiplexingConfig
-	14, // [14:14] is the sub-list for method output_type
-	14, // [14:14] is the sub-list for method input_type
-	14, // [14:14] is the sub-list for extension type_name
-	14, // [14:14] is the sub-list for extension extendee
-	0,  // [0:14] is the sub-list for field type_name
+	16, // 14: xray.app.proxyman.SenderConfig.target_strategy:type_name -> xray.transport.internet.DomainStrategy
+	15, // [15:15] is the sub-list for method output_type
+	15, // [15:15] is the sub-list for method input_type
+	15, // [15:15] is the sub-list for extension type_name
+	15, // [15:15] is the sub-list for extension extendee
+	0,  // [0:15] is the sub-list for field type_name
 }
 
 func init() { file_app_proxyman_config_proto_init() }

+ 1 - 0
app/proxyman/config.proto

@@ -84,6 +84,7 @@ message SenderConfig {
   xray.transport.internet.ProxyConfig proxy_settings = 3;
   MultiplexingConfig multiplex_settings = 4;
   string via_cidr = 5;
+  xray.transport.internet.DomainStrategy target_strategy = 6;
 }
 
 message MultiplexingConfig {

+ 1 - 1
app/proxyman/inbound/worker.go

@@ -325,7 +325,7 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
 
 			ctx = session.ContextWithInbound(ctx, &session.Inbound{
 				Source:  source,
-				Local:   net.DestinationFromAddr(w.hub.Addr()),
+				Local:   net.DestinationFromAddr(w.hub.Addr()), // Due to some limitations, in UDP connections, localIP is always equal to listen interface IP
 				Gateway: net.UDPDestination(w.address, w.port),
 				Tag:     w.tag,
 			})

+ 27 - 14
app/proxyman/outbound/handler.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"crypto/rand"
 	goerrors "errors"
+	"github.com/xtls/xray-core/common/dice"
 	"io"
 	"math/big"
 	gonet "net"
@@ -177,6 +178,25 @@ func (h *Handler) Tag() string {
 func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
 	outbounds := session.OutboundsFromContext(ctx)
 	ob := outbounds[len(outbounds)-1]
+	content := session.ContentFromContext(ctx)
+	if h.senderSettings != nil && h.senderSettings.TargetStrategy.HasStrategy() && ob.Target.Address.Family().IsDomain() && (content == nil || !content.SkipDNSResolve) {
+		ips, err := internet.LookupForIP(ob.Target.Address.Domain(), h.senderSettings.TargetStrategy, nil)
+		if err != nil {
+			errors.LogInfoInner(ctx, err, "failed to resolve ip for target ", ob.Target.Address.Domain())
+			if h.senderSettings.TargetStrategy.ForceIP() {
+				err := errors.New("failed to resolve ip for target ", ob.Target.Address.Domain()).Base(err)
+				session.SubmitOutboundErrorToOriginator(ctx, err)
+				common.Interrupt(link.Writer)
+				common.Interrupt(link.Reader)
+				return
+			}
+
+		} else {
+			unchangedDomain := ob.Target.Address.Domain()
+			ob.Target.Address = net.IPAddress(ips[dice.Roll(len(ips))])
+			errors.LogInfo(ctx, "target: ", unchangedDomain, " resolved to: ", ob.Target.Address.String())
+		}
+	}
 	if ob.Target.Network == net.Network_UDP && ob.OriginalTarget.Address != nil && ob.OriginalTarget.Address != ob.Target.Address {
 		link.Reader = &buf.EndpointOverrideReader{Reader: link.Reader, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address}
 		link.Writer = &buf.EndpointOverrideWriter{Writer: link.Writer, Dest: ob.Target.Address, OriginalDest: ob.OriginalTarget.Address}
@@ -188,6 +208,7 @@ func (h *Handler) Dispatch(ctx context.Context, link *transport.Link) {
 				session.SubmitOutboundErrorToOriginator(ctx, err)
 				errors.LogInfo(ctx, err.Error())
 				common.Interrupt(link.Writer)
+				common.Interrupt(link.Reader)
 			}
 		}
 		if ob.Target.Network == net.Network_UDP && ob.Target.Port == 443 {
@@ -287,26 +308,18 @@ func (h *Handler) Dial(ctx context.Context, dest net.Destination) (stat.Connecti
 				ob.Gateway = ParseRandomIP(addr, h.senderSettings.ViaCidr)
 
 			case domain == "origin":
-
 				if inbound := session.InboundFromContext(ctx); inbound != nil {
-					if inbound.Conn != nil {
-						origin, _, err := net.SplitHostPort(inbound.Conn.LocalAddr().String())
-						if err == nil {
-							ob.Gateway = net.ParseAddress(origin)
-							errors.LogDebug(ctx, "use receive package ip as snedthrough: ", origin)
-						}
+					if inbound.Local.IsValid() && inbound.Local.Address.Family().IsIP() {
+						ob.Gateway = inbound.Local.Address
+						errors.LogDebug(ctx, "use inbound local ip as sendthrough: ", inbound.Local.Address.String())
 					}
 				}
 			case domain == "srcip":
 				if inbound := session.InboundFromContext(ctx); inbound != nil {
-					if inbound.Conn != nil {
-						clientaddr, _, err := net.SplitHostPort(inbound.Conn.RemoteAddr().String())
-						if err == nil {
-							ob.Gateway = net.ParseAddress(clientaddr)
-							errors.LogDebug(ctx, "use client src ip as snedthrough: ", clientaddr)
-						}
+					if inbound.Source.IsValid() && inbound.Source.Address.Family().IsIP() {
+						ob.Gateway = inbound.Source.Address
+						errors.LogDebug(ctx, "use inbound source ip as sendthrough: ", inbound.Source.Address.String())
 					}
-
 				}
 			//case addr.Family().IsDomain():
 			default:

+ 34 - 7
infra/conf/xray.go

@@ -260,13 +260,14 @@ func (c *InboundDetourConfig) Build() (*core.InboundHandlerConfig, error) {
 }
 
 type OutboundDetourConfig struct {
-	Protocol      string           `json:"protocol"`
-	SendThrough   *string          `json:"sendThrough"`
-	Tag           string           `json:"tag"`
-	Settings      *json.RawMessage `json:"settings"`
-	StreamSetting *StreamConfig    `json:"streamSettings"`
-	ProxySettings *ProxyConfig     `json:"proxySettings"`
-	MuxSettings   *MuxConfig       `json:"mux"`
+	Protocol       string           `json:"protocol"`
+	SendThrough    *string          `json:"sendThrough"`
+	Tag            string           `json:"tag"`
+	Settings       *json.RawMessage `json:"settings"`
+	StreamSetting  *StreamConfig    `json:"streamSettings"`
+	ProxySettings  *ProxyConfig     `json:"proxySettings"`
+	MuxSettings    *MuxConfig       `json:"mux"`
+	TargetStrategy string           `json:"targetStrategy"`
 }
 
 func (c *OutboundDetourConfig) checkChainProxyConfig() error {
@@ -282,6 +283,32 @@ func (c *OutboundDetourConfig) checkChainProxyConfig() error {
 // Build implements Buildable.
 func (c *OutboundDetourConfig) Build() (*core.OutboundHandlerConfig, error) {
 	senderSettings := &proxyman.SenderConfig{}
+	switch strings.ToLower(c.TargetStrategy) {
+	case "asis", "":
+		senderSettings.TargetStrategy = internet.DomainStrategy_AS_IS
+	case "useip":
+		senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP
+	case "useipv4":
+		senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP4
+	case "useipv6":
+		senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP6
+	case "useipv4v6":
+		senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP46
+	case "useipv6v4":
+		senderSettings.TargetStrategy = internet.DomainStrategy_USE_IP64
+	case "forceip":
+		senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP
+	case "forceipv4":
+		senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP4
+	case "forceipv6":
+		senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP6
+	case "forceipv4v6":
+		senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP46
+	case "forceipv6v4":
+		senderSettings.TargetStrategy = internet.DomainStrategy_FORCE_IP64
+	default:
+		return nil, errors.New("unsupported target domain strategy: ", c.TargetStrategy)
+	}
 	if err := c.checkChainProxyConfig(); err != nil {
 		return nil, err
 	}

+ 7 - 7
transport/internet/config.go

@@ -100,30 +100,30 @@ func (m SocketConfig_TProxyMode) IsEnabled() bool {
 	return m != SocketConfig_Off
 }
 
-func (s DomainStrategy) hasStrategy() bool {
+func (s DomainStrategy) HasStrategy() bool {
 	return strategy[s][0] != 0
 }
 
-func (s DomainStrategy) forceIP() bool {
+func (s DomainStrategy) ForceIP() bool {
 	return strategy[s][0] == 2
 }
 
-func (s DomainStrategy) preferIP4() bool {
+func (s DomainStrategy) PreferIP4() bool {
 	return strategy[s][1] == 4 || strategy[s][1] == 0
 }
 
-func (s DomainStrategy) preferIP6() bool {
+func (s DomainStrategy) PreferIP6() bool {
 	return strategy[s][1] == 6 || strategy[s][1] == 0
 }
 
-func (s DomainStrategy) hasFallback() bool {
+func (s DomainStrategy) HasFallback() bool {
 	return strategy[s][2] != 0
 }
 
-func (s DomainStrategy) fallbackIP4() bool {
+func (s DomainStrategy) FallbackIP4() bool {
 	return strategy[s][2] == 4
 }
 
-func (s DomainStrategy) fallbackIP6() bool {
+func (s DomainStrategy) FallbackIP6() bool {
 	return strategy[s][2] == 6
 }

+ 10 - 10
transport/internet/dialer.go

@@ -85,20 +85,20 @@ var (
 	obm       outbound.Manager
 )
 
-func lookupIP(domain string, strategy DomainStrategy, localAddr net.Address) ([]net.IP, error) {
+func LookupForIP(domain string, strategy DomainStrategy, localAddr net.Address) ([]net.IP, error) {
 	if dnsClient == nil {
 		return nil, errors.New("DNS client not initialized").AtError()
 	}
 
 	ips, _, err := dnsClient.LookupIP(domain, dns.IPOption{
-		IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && strategy.preferIP4(),
-		IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && strategy.preferIP6(),
+		IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && strategy.PreferIP4(),
+		IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && strategy.PreferIP6(),
 	})
 	{ // Resolve fallback
-		if (len(ips) == 0 || err != nil) && strategy.hasFallback() && localAddr == nil {
+		if (len(ips) == 0 || err != nil) && strategy.HasFallback() && localAddr == nil {
 			ips, _, err = dnsClient.LookupIP(domain, dns.IPOption{
-				IPv4Enable: strategy.fallbackIP4(),
-				IPv6Enable: strategy.fallbackIP6(),
+				IPv4Enable: strategy.FallbackIP4(),
+				IPv6Enable: strategy.FallbackIP6(),
 			})
 		}
 	}
@@ -113,7 +113,7 @@ func canLookupIP(dst net.Destination, sockopt *SocketConfig) bool {
 	if dst.Address.Family().IsIP() {
 		return false
 	}
-	return sockopt.DomainStrategy.hasStrategy()
+	return sockopt.DomainStrategy.HasStrategy()
 }
 
 func redirect(ctx context.Context, dst net.Destination, obt string, h outbound.Handler) net.Conn {
@@ -249,17 +249,17 @@ func DialSystem(ctx context.Context, dest net.Destination, sockopt *SocketConfig
 	}
 
 	if canLookupIP(dest, sockopt) {
-		ips, err := lookupIP(dest.Address.String(), sockopt.DomainStrategy, src)
+		ips, err := LookupForIP(dest.Address.String(), sockopt.DomainStrategy, src)
 		if err != nil {
 			errors.LogErrorInner(ctx, err, "failed to resolve ip")
-			if sockopt.DomainStrategy.forceIP() {
+			if sockopt.DomainStrategy.ForceIP() {
 				return nil, err
 			}
 		} else if sockopt.HappyEyeballs == nil || sockopt.HappyEyeballs.TryDelayMs == 0 || sockopt.HappyEyeballs.MaxConcurrentTry == 0 || len(ips) < 2 || len(sockopt.DialerProxy) > 0 || dest.Network != net.Network_TCP {
 			dest.Address = net.IPAddress(ips[dice.Roll(len(ips))])
 			errors.LogInfo(ctx, "replace destination with "+dest.String())
 		} else {
-			return TcpRaceDial(ctx, src, ips, dest.Port, sockopt)
+			return TcpRaceDial(ctx, src, ips, dest.Port, sockopt, dest.Address.String())
 		}
 	}
 

+ 5 - 1
transport/internet/happy_eyeballs.go

@@ -2,6 +2,7 @@ package internet
 
 import (
 	"context"
+	"github.com/xtls/xray-core/common/errors"
 	"github.com/xtls/xray-core/common/net"
 	"time"
 )
@@ -12,7 +13,7 @@ type result struct {
 	index int
 }
 
-func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Port, sockopt *SocketConfig) (net.Conn, error) {
+func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Port, sockopt *SocketConfig, domain string) (net.Conn, error) {
 	if len(ips) < 2 {
 		panic("at least 2 ips is required to race dial")
 	}
@@ -30,6 +31,7 @@ func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Po
 	activeNum := uint32(0)
 	timer := time.NewTimer(0)
 	var winConn net.Conn
+	errors.LogDebug(ctx, "happy eyeballs racing dial for ", domain, " with IPs ", ips)
 	for {
 		select {
 		case r := <-resultCh:
@@ -54,6 +56,7 @@ func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Po
 					timer.Stop()
 					if winConn == nil {
 						winConn = r.conn
+						errors.LogDebug(ctx, "happy eyeballs established connection for ", domain, " with IP ", ips[r.index])
 					} else {
 						r.conn.Close()
 					}
@@ -69,6 +72,7 @@ func TcpRaceDial(ctx context.Context, src net.Address, ips []net.IP, port net.Po
 					continue
 				}
 				if activeNum == 0 {
+					errors.LogDebugInner(ctx, r.err, "happy eyeballs no connection established for ", domain)
 					return nil, r.err
 				}
 				timer.Stop()