netbirdio/netbird · error

failed to forward local WG packet (%d) to remote turn conn:

Error message

failed to forward local WG packet (%d) to remote turn conn: %w

What it means

Returned by WGEBPFProxy.readAndForwardPacket when writing a locally captured WireGuard packet to the TURN (relay) connection registered for that source port fails. The turnConnStore maps an allocated local port to a net.Conn; the write is the last hop of the path local WG -> eBPF redirect -> proxy socket -> relay. Failure means that particular relay connection is broken or closed while still registered.

Source

Thrown at client/iface/wgproxy/ebpf/proxy.go:200

func (p *WGEBPFProxy) readAndForwardPacket(buf []byte) error {
	n, addr, err := p.conn.ReadFromUDP(buf)
	if err != nil {
		return fmt.Errorf("failed to read UDP packet from WG: %w", err)
	}

	p.turnConnMutex.Lock()
	conn, ok := p.turnConnStore[uint16(addr.Port)]
	p.turnConnMutex.Unlock()
	if !ok {
		if p.ctx.Err() == nil {
			log.Debugf("turn conn not found by port because conn already has been closed: %d", addr.Port)
		}
		return nil
	}

	if _, err := conn.Write(buf[:n]); err != nil {
		return fmt.Errorf("failed to forward local WG packet (%d) to remote turn conn: %w", addr.Port, err)
	}
	return nil
}

func (p *WGEBPFProxy) storeTurnConn(turnConn net.Conn) (uint16, error) {
	p.turnConnMutex.Lock()
	defer p.turnConnMutex.Unlock()

	np, err := p.nextFreePort()
	if err != nil {
		return np, err
	}
	p.turnConnStore[np] = turnConn
	return np, nil
}

func (p *WGEBPFProxy) removeTurnConn(turnConnID uint16) {
	p.turnConnMutex.Lock()

View on GitHub (pinned to 93e97f4bf1)

Solutions

  1. Check relay availability and TLS/port reachability from the host to the configured relay endpoints
  2. Confirm the agent reconnects: the ICE/relay layer should notice the dead conn, call removeTurnConn and re-establish; if the error loops forever, restart the agent
  3. Verify agent clock/certificate health if relay auth is failing (the underlying %w error carries the cause)
  4. Upgrade agent if an old build kept stale conns in turnConnStore after relay failover
Defensive patterns

Strategy: try-catch

Try / catch

if _, err := conn.Write(buf); err != nil {
    var netErr net.Error
    if errors.As(err, &netErr) || errors.Is(err, net.ErrClosed) {
        p.removeTurnConn(port)
        // signal the ICE/relay layer to re-establish this relay conn
        p.onConnDead(port)
        return nil // drop packet, path is being rebuilt
    }
    return fmt.Errorf("forward: %w", err)
}

Prevention

When it happens

Trigger: Relay (TURN) server dropped the connection or restarted; NAT middlebox tore down the mapping; the peer closed its side; or the conn was closed locally but removeTurnConn has not run yet, so a packet still arrives for the stale port and Write hits a closed conn.

Common situations: Relay restarts or failover; long-lived relayed sessions through aggressive NATs; the moment a relayed peer disconnects while its queued packets are still being drained. Logged via log.Errorf in a loop, so it can repeat until the conn is removed.

Related errors


AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16). Data as JSON: /api/errors/612d8bde4470f700. Report an issue: GitHub.