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

  1. Ensure the REALITY scan probe always dials with net.Dialer (TCP) before writeProxyProtocolHeader is called
  2. If testing, provide a fake conn whose LocalAddr/RemoteAddr return *net.TCPAddr values
  3. 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

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


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/e03cbacb62da374e. Report an issue: GitHub.