MHSanaei/3x-ui · warning
connection has no TCP addresses
Error message
connection has no TCP addresses
What it means
Returned by writeProxyProtocolHeader when the already-dialed connection's LocalAddr or RemoteAddr does not type-assert to *net.TCPAddr. PROXY protocol headers must describe a real (src, dst) TCP pair, so a non-TCP or already-replaced address makes header generation impossible. In practice the scanner dials TCP, so this fires mainly on wrapped/mock connections or Unix-domain fallbacks.
Source
Thrown at internal/web/service/reality_scan.go:414
return -1
}
return 1
}
return a.LatencyMs - b.LatencyMs
})
}
// writeProxyProtocolHeader emits a PROXY protocol header describing the local
// connection so a target that requires it (Nginx `proxy_protocol`, matching a
// REALITY inbound's xver) accepts the probe instead of resetting it. xver 1
// sends the human-readable v1 header; xver 2 sends the binary v2 header. The
// addresses come from the already-dialed connection, so they are always a
// consistent, real (src, dst) pair.
func writeProxyProtocolHeader(conn net.Conn, xver int) error {
local, lok := conn.LocalAddr().(*net.TCPAddr)
remote, rok := conn.RemoteAddr().(*net.TCPAddr)
if !lok || !rok {
return fmt.Errorf("connection has no TCP addresses")
}
if xver >= 2 {
return writeProxyProtocolV2(conn, local, remote)
}
return writeProxyProtocolV1(conn, local, remote)
}
func writeProxyProtocolV1(conn net.Conn, local, remote *net.TCPAddr) error {
fam := "TCP4"
if local.IP.To4() == nil || remote.IP.To4() == nil {
fam = "TCP6"
}
header := fmt.Sprintf("PROXY %s %s %s %d %d\r\n", fam, local.IP.String(), remote.IP.String(), local.Port, remote.Port)
_, err := conn.Write([]byte(header))
return err
}
func writeProxyProtocolV2(conn net.Conn, local, remote *net.TCPAddr) error {View on GitHub (pinned to ad32144c42)
Solutions
- Ensure the REALITY scan probe always dials with net.Dialer (TCP) before writeProxyProtocolHeader is called
- If testing, provide a fake conn whose LocalAddr/RemoteAddr return *net.TCPAddr values
- Skip proxy-protocol header emission (treat as non-fatal) when the address family cannot be determined, since the probe can still run without xver
Example fix
// before
if err := writeProxyProtocolHeader(conn, xver); err != nil {
return err
}
// after — probe still usable without the header
if err := writeProxyProtocolHeader(conn, xver); err != nil {
logger.Warning("reality scan: skipping proxy header:", err)
}
Defensive patterns
Strategy: type-guard
Type guard
func isTCPConn(conn net.Conn) bool {
_, lok := conn.LocalAddr().(*net.TCPAddr)
_, rok := conn.RemoteAddr().(*net.TCPAddr)
return lok && rok
}
Try / catch
if err := writeProxyProtocolHeader(conn, xver); err != nil {
if strings.Contains(err.Error(), "no TCP addresses") {
logger.Warning("reality scan: cannot emit proxy header on non-TCP conn")
} else {
return err
}
}
Prevention
- Always net.Dial TCP directly for REALITY probes that need proxy-protocol headers
- Guard address-type assertions at the boundary instead of deep in writers
- In tests, supply conns whose addresses are real *net.TCPAddr values
When it happens
Trigger: The conn passed in is a net.Conn wrapper whose LocalAddr()/RemoteAddr() return a non-*net.TCPAddr type (e.g. UnixAddr, or a custom conn from a test double); the underlying socket was converted to a non-TCP transport.
Common situations: Unit tests injecting fake connections into the REALITY scanner; future refactors that route the probe through a socket redialer or an in-memory pipe; scanning code path changed to dial via a Unix-socket outbound.
Related errors
- node port must be 1-65535
- HTTP %d from remote panel
- remote response exceeds size limit
- outbound subscription response body exceeds size limit
- blocked private/internal address %s
AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15).
Data as JSON: /api/errors/e03cbacb62da374e.
Report an issue: GitHub.