Sniff: Add SniffingObject.ignoreClientIp (bool) option to override client-requested IP when routeOnly is enabled

- Added `ignoreClientIp` option in the sniffing module, which forces internal DNS resolution to override the client-requested IP when `routeOnly` is enabled.
- Resolves Issue #4335 by addressing routing failures caused by DoH (DNS over HTTPS) interfering with DNS resolution in transparent proxy scenarios.
- Eliminates the need to block common DoH providers when using geoip-based routing, ensuring accurate routing without additional configurations.

Use case:
When Xray acts as a transparent proxy gateway with geoip-based routing, `routeOnly` must be set to `true`. However, if the client enables DoH (e.g., Chrome automatically enabling DoH when detecting OS-level support), DNS resolution may be polluted, causing critical routing failures.

Previously, users had to block common DoH providers to prevent such issues—an impractical and cumbersome solution. With `ignoreClientIp` enabled, Xray no longer trusts the client-requested IP and instead performs internal DNS resolution, ensuring correct routing behavior without the need for DoH blocking.
pull/4423/head
Meo597 2025-02-22 10:42:48 +08:00
parent be43f66b63
commit 685e29bd76
5 changed files with 41 additions and 0 deletions

View File

@ -9,6 +9,7 @@ import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/dice"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
@ -290,6 +291,19 @@ func (d *DefaultDispatcher) Dispatch(ctx context.Context, destination net.Destin
}
if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP {
ob.RouteTarget = destination
if sniffingRequest.IgnoreClientIp {
ips, err := d.dns.LookupIP(domain, dns.IPOption{
IPv4Enable: ob.OriginalTarget.Address.Family().IsIPv4(),
IPv6Enable: !ob.OriginalTarget.Address.Family().IsIPv4(),
FakeEnable: false,
})
if len(ips) == 0 || err != nil {
errors.LogWarning(ctx, "failed to resolve domain:", domain, ", Falling back to client-requested IP:", destination.Address.String())
} else {
destination.Address = net.IPAddress(ips[dice.Roll(len(ips))])
ob.Target = destination
}
}
} else {
ob.Target = destination
}
@ -344,6 +358,19 @@ func (d *DefaultDispatcher) DispatchLink(ctx context.Context, destination net.De
}
if sniffingRequest.RouteOnly && protocol != "fakedns" && protocol != "fakedns+others" && !isFakeIP {
ob.RouteTarget = destination
if sniffingRequest.IgnoreClientIp {
ips, err := d.dns.LookupIP(domain, dns.IPOption{
IPv4Enable: ob.OriginalTarget.Address.Family().IsIPv4(),
IPv6Enable: !ob.OriginalTarget.Address.Family().IsIPv4(),
FakeEnable: false,
})
if len(ips) == 0 || err != nil {
errors.LogWarning(ctx, "failed to resolve domain:", domain, ", Falling back to client-requested IP:", destination.Address.String())
} else {
destination.Address = net.IPAddress(ips[dice.Roll(len(ips))])
ob.Target = destination
}
}
} else {
ob.Target = destination
}

View File

@ -192,6 +192,7 @@ type SniffingConfig struct {
// message.
MetadataOnly bool `protobuf:"varint,4,opt,name=metadata_only,json=metadataOnly,proto3" json:"metadata_only,omitempty"`
RouteOnly bool `protobuf:"varint,5,opt,name=route_only,json=routeOnly,proto3" json:"route_only,omitempty"`
IgnoreClientIp bool `protobuf:"varint,6,opt,name=ignore_client_ip,json=ignoreClientIp,proto3" json:"ignoreClientIp,omitempty"`
}
func (x *SniffingConfig) Reset() {
@ -259,6 +260,13 @@ func (x *SniffingConfig) GetRouteOnly() bool {
return false
}
func (x *SniffingConfig) GetIgnoreClientIp() bool {
if x != nil {
return x.IgnoreClientIp
}
return false
}
type ReceiverConfig struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache

View File

@ -103,6 +103,7 @@ func (w *tcpWorker) callback(conn stat.Connection) {
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
content.SniffingRequest.IgnoreClientIp = w.sniffingConfig.IgnoreClientIp
}
ctx = session.ContextWithContent(ctx, content)
@ -326,6 +327,7 @@ func (w *udpWorker) callback(b *buf.Buffer, source net.Destination, originalDest
content.SniffingRequest.OverrideDestinationForProtocol = w.sniffingConfig.DestinationOverride
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
content.SniffingRequest.IgnoreClientIp = w.sniffingConfig.IgnoreClientIp
}
ctx = session.ContextWithContent(ctx, content)
if err := w.proxy.Process(ctx, net.Network_UDP, conn, w.dispatcher); err != nil {
@ -477,6 +479,7 @@ func (w *dsWorker) callback(conn stat.Connection) {
content.SniffingRequest.ExcludeForDomain = w.sniffingConfig.DomainsExcluded
content.SniffingRequest.MetadataOnly = w.sniffingConfig.MetadataOnly
content.SniffingRequest.RouteOnly = w.sniffingConfig.RouteOnly
content.SniffingRequest.IgnoreClientIp = w.sniffingConfig.IgnoreClientIp
}
ctx = session.ContextWithContent(ctx, content)

View File

@ -80,6 +80,7 @@ type SniffingRequest struct {
Enabled bool
MetadataOnly bool
RouteOnly bool
IgnoreClientIp bool
}
// Content is the metadata of the connection content.

View File

@ -55,6 +55,7 @@ type SniffingConfig struct {
DomainsExcluded *StringList `json:"domainsExcluded"`
MetadataOnly bool `json:"metadataOnly"`
RouteOnly bool `json:"routeOnly"`
IgnoreClientIp bool `json:"ignoreClientIp"`
}
// Build implements Buildable.
@ -92,6 +93,7 @@ func (c *SniffingConfig) Build() (*proxyman.SniffingConfig, error) {
DomainsExcluded: d,
MetadataOnly: c.MetadataOnly,
RouteOnly: c.RouteOnly,
IgnoreClientIp: c.IgnoreClientIp,
}, nil
}