mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
fix(xray): allow a private DoH server on the port Xray dials (#6918)
The DNS allow rule opened port 53 for a private server given as https:// or h2c://, but Xray dials remote DoH on the URL's port, 443 by default, so the rule never matched and the private-IP block dropped the queries. Remote DoH now takes its port from the URL and ignores an object entry's port field, which Xray ignores as well; the +local forms skip routing and keep their previous handling.
This commit is contained in:
@@ -130,16 +130,8 @@ type dnsServerEndpoint struct {
|
||||
port int
|
||||
}
|
||||
|
||||
// privateDnsServerEndpoint extracts a literal, private/internal (ip, port)
|
||||
// endpoint from a dns.servers entry, or ok=false if the entry is a domain
|
||||
// name, a special Xray keyword (localhost, fakedns, ...), or resolves to a
|
||||
// public IP.
|
||||
//
|
||||
// A dns.servers entry is either a bare string or an object with an
|
||||
// "address" field (see frontend/src/schemas/dns.ts DnsServerEntrySchema);
|
||||
// the object form may also carry an explicit "port" (default 53 there,
|
||||
// per DnsServerObjectInnerSchema), which takes precedence over any port
|
||||
// embedded in the address itself.
|
||||
// privateDnsServerEndpoint returns the private (ip, port) of a dns.servers string or object entry; an
|
||||
// object's "port" wins over the address's own except for remote DoH, which Xray dials from its URL.
|
||||
func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
|
||||
var address string
|
||||
explicitPort := 0
|
||||
@@ -159,7 +151,7 @@ func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
|
||||
if host == "" {
|
||||
return dnsServerEndpoint{}, false
|
||||
}
|
||||
if explicitPort > 0 {
|
||||
if explicitPort > 0 && !isRemoteDoh(address) {
|
||||
port = explicitPort
|
||||
}
|
||||
|
||||
@@ -175,18 +167,26 @@ func privateDnsServerEndpoint(entry any) (dnsServerEndpoint, bool) {
|
||||
return dnsServerEndpoint{ip: ip.String(), port: port}, true
|
||||
}
|
||||
|
||||
// splitAddressHostPort extracts the bare host and port (defaulting to 53)
|
||||
// from an Xray-core DNS server address string. Those may carry a URI
|
||||
// scheme (tcp://, tcp+local://, https://, https+local://, quic://,
|
||||
// quic+local://) and, for DoH, a path and/or a bracketed IPv6 host — all
|
||||
// of that is stripped down to host[:port] before parsing.
|
||||
// isRemoteDoh reports a routed DoH address (https://, h2c://), which Xray dials from the URL alone.
|
||||
func isRemoteDoh(address string) bool {
|
||||
scheme, _, ok := strings.Cut(strings.TrimSpace(address), "://")
|
||||
return ok && (strings.EqualFold(scheme, "https") || strings.EqualFold(scheme, "h2c"))
|
||||
}
|
||||
|
||||
// splitAddressHostPort strips an Xray DNS address's scheme, DoH path and IPv6 brackets down to host
|
||||
// and port, defaulting to 443 for remote DoH (https://, h2c://) and 53 otherwise.
|
||||
func splitAddressHostPort(address string) (host string, port int) {
|
||||
address = strings.TrimSpace(address)
|
||||
if address == "" {
|
||||
return "", 0
|
||||
}
|
||||
|
||||
port = 53
|
||||
if idx := strings.Index(address, "://"); idx != -1 {
|
||||
// Remote DoH goes out on the HTTPS port, h2c included; the +local forms skip routing.
|
||||
if isRemoteDoh(address) {
|
||||
port = 443
|
||||
}
|
||||
address = address[idx+3:]
|
||||
}
|
||||
// Drop a DoH path, e.g. "1.1.1.1/dns-query".
|
||||
@@ -194,7 +194,6 @@ func splitAddressHostPort(address string) (host string, port int) {
|
||||
address = address[:idx]
|
||||
}
|
||||
|
||||
port = 53
|
||||
host = address
|
||||
if strings.HasPrefix(host, "[") {
|
||||
// Bracketed IPv6, with or without a port: "[::1]" / "[::1]:53".
|
||||
|
||||
@@ -183,6 +183,34 @@ func TestEnsureDnsServerRouting_StripsSchemePortAndPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDnsServerRouting_RemoteDohUsesHttpsPort(t *testing.T) {
|
||||
in := `{
|
||||
"dns": {"servers": [
|
||||
"https://10.255.255.1/dns-query",
|
||||
"h2c://10.255.255.2/dns-query",
|
||||
"https://10.255.255.3:8443/dns-query"
|
||||
]},
|
||||
"routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
|
||||
}`
|
||||
out, err := EnsureDnsServerRouting(in)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
rules := rulesFromRaw(t, out)
|
||||
if len(rules) != 3 {
|
||||
t.Fatalf("got %d rules, want 2 allow rules + block: %v", len(rules), rules)
|
||||
}
|
||||
if p, _ := rules[0]["port"].(string); p != "443" {
|
||||
t.Fatalf("rules[0] port = %v, want \"443\"", rules[0]["port"])
|
||||
}
|
||||
if ips := readRuleIPs(rules[0]["ip"]); len(ips) != 2 || ips[0] != "10.255.255.1" || ips[1] != "10.255.255.2" {
|
||||
t.Fatalf("rules[0] ip = %v, want [10.255.255.1 10.255.255.2]", ips)
|
||||
}
|
||||
if p, _ := rules[1]["port"].(string); p != "8443" {
|
||||
t.Fatalf("rules[1] port = %v, want \"8443\"", rules[1]["port"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDnsServerRouting_SkipsSpecialAndDomainAddresses(t *testing.T) {
|
||||
in := `{
|
||||
"dns": {"servers": ["localhost", "fakedns", "dns.google", "8.8.8.8"]},
|
||||
@@ -465,3 +493,20 @@ func TestEnsureDnsServerRouting_InvalidJsonReturnsAsIs(t *testing.T) {
|
||||
t.Fatalf("expected raw passthrough on error, got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Xray dials a DoH server from its URL alone; the object's "port" (53 in entries saved by older
|
||||
// forms) never reaches it, so the allow rule must still open the URL's port.
|
||||
func TestEnsureDnsServerRouting_RemoteDohIgnoresObjectPort(t *testing.T) {
|
||||
in := `{
|
||||
"dns": {"servers": [{"address": "https://10.255.255.4/dns-query", "port": 53, "domains": ["lan"]}]},
|
||||
"routing": {"rules": [{"type":"field","outboundTag":"blocked","ip":["geoip:private"]}]}
|
||||
}`
|
||||
out, err := EnsureDnsServerRouting(in)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
rules := rulesFromRaw(t, out)
|
||||
if p, _ := rules[0]["port"].(string); p != "443" {
|
||||
t.Fatalf("rules[0] port = %v, want \"443\"", rules[0]["port"])
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user