Integrity requirements
Description
A WireGuard outbound whose peer endpoint is given as a domain name stops working after a short loss of upstream network connectivity on the host, and never recovers on its own — only restarting the Xray process brings it back.
Scenario: the host had a brief network interruption (the machine stayed up, power was fine, only the uplink was gone for several minutes). After connectivity was restored, Xray itself was clearly still running and healthy — other inbounds kept accepting connections and responding normally —but everything routed through the WireGuard outbound stayed dead indefinitely.
Reading the source, two things in proxy/wireguard seem to combine to prevent recovery:
-
Re-dial only triggers on a read error. In bind.go, netBindClient.Send only re-dials when endpoint.conn == nil, and conn is reset to nil only inside the receive goroutine when ReadFrom returns an error (connectTo). On a write error, Send returns the error but does not reset endpoint.conn, so no re-dial happens. A connected UDP socket typically does not produce a read error when the peer silently disappears during an outage (no ICMP), so the receive goroutine just blocks, conn stays non-nil, writes keep going into a dead socket, and the outbound never re-establishes.
-
The endpoint is resolved once and cached. createIPCRequest in client.go resolves the endpoint domain to an IP a single time and never re-resolves it for the lifetime of the device, so even a re-dial would reuse a possibly-stale address.
Suggested directions: on a write error in Send, also reset/close endpoint.conn so the next Send re-dials; and retain the endpoint domain to re-resolve it on re-dial.
Reproduction Method
- Start Xray with the config below (a local SOCKS inbound routed to a WireGuard outbound whose peer endpoint is a domain name).
- Confirm traffic works:
curl -x socks5h://127.0.0.1:1080 https://api.ipify.org.
- Cut the host's network for ~3-5 minutes (e.g. drop all egress with the firewall, or bring the uplink down), then restore it.
- Run the same curl again. It hangs/fails and never recovers; only restarting Xray restores the WireGuard outbound.
Client config
Details
No separate Xray client is needed to reproduce. Traffic is generated against the local SOCKS inbound of the single instance below, e.g.:
curl -x socks5h://127.0.0.1:1080 https://api.ipify.org
Server config
Details
{
"log": {
"loglevel": "debug",
"dnsLog": true
},
"inbounds": [
{
"tag": "socks-in",
"protocol": "socks",
"listen": "127.0.0.1",
"port": 1080,
"settings": {
"udp": true
}
}
],
"outbounds": [
{
"protocol": "wireguard",
"tag": "wg-out",
"settings": {
"secretKey": "SECRETKEY",
"address": [
"10.0.0.2/32"
],
"peers": [
{
"publicKey": "PUBLICKEY",
"endpoint": "wg.example.com:51820",
"allowedIPs": [
"0.0.0.0/0",
"::/0"
]
}
],
"domainStrategy": "ForceIPv4"
}
}
],
"routing": {
"rules": [
{
"inboundTag": [
"socks-in"
],
"outboundTag": "wg-out"
}
]
}
}
Client log
Details
Not applicable — reproduction uses only the local SOCKS inbound of the single instance above; there is no separate Xray client.
Server log
Details
Debug logs were not retained from the original incident.
Integrity requirements
Description
A WireGuard outbound whose peer
endpointis given as a domain name stops working after a short loss of upstream network connectivity on the host, and never recovers on its own — only restarting the Xray process brings it back.Scenario: the host had a brief network interruption (the machine stayed up, power was fine, only the uplink was gone for several minutes). After connectivity was restored, Xray itself was clearly still running and healthy — other inbounds kept accepting connections and responding normally —but everything routed through the WireGuard outbound stayed dead indefinitely.
Reading the source, two things in
proxy/wireguardseem to combine to prevent recovery:Re-dial only triggers on a read error. In
bind.go,netBindClient.Sendonly re-dials whenendpoint.conn == nil, andconnis reset tonilonly inside the receive goroutine whenReadFromreturns an error (connectTo). On a write error,Sendreturns the error but does not resetendpoint.conn, so no re-dial happens. A connected UDP socket typically does not produce a read error when the peer silently disappears during an outage (no ICMP), so the receive goroutine just blocks,connstays non-nil, writes keep going into a dead socket, and the outbound never re-establishes.The endpoint is resolved once and cached.
createIPCRequestinclient.goresolves the endpoint domain to an IP a single time and never re-resolves it for the lifetime of the device, so even a re-dial would reuse a possibly-stale address.Suggested directions: on a write error in
Send, also reset/closeendpoint.connso the nextSendre-dials; and retain the endpoint domain to re-resolve it on re-dial.Reproduction Method
curl -x socks5h://127.0.0.1:1080 https://api.ipify.org.Client config
Details
Server config
Details
Client log
Details
Server log
Details