netbirdio/netbird · error
proxy not started
Error message
proxy not started
What it means
Returned by WGUDPProxy.InjectPacket when remoteConn is nil, and by WGUDPProxy.CloseConn when cancel is nil. Both fields are only set by a successful AddTurnConn (it dials the local WG port and stores ctx/cancel/conns). The proxy is explicitly documented as not thread safe, and there is no constructor-time state, so this error is a lifecycle guard: the method was called before AddTurnConn ever ran or before it succeeded.
Source
Thrown at client/iface/wgproxy/udp/proxy.go:164
p.srcFakerConn = srcFakerConn
p.sendPkg = p.srcFakerConn.SendPkg
}
// InjectPacket writes b to the remote peer over the underlying transport.
func (p *WGUDPProxy) InjectPacket(b []byte) error {
if p.remoteConn == nil {
return errors.New("proxy not started")
}
if _, err := p.remoteConn.Write(b); err != nil {
return err
}
return nil
}
// CloseConn close the localConn
func (p *WGUDPProxy) CloseConn() error {
if p.cancel == nil {
return fmt.Errorf("proxy not started")
}
return p.close()
}
func (p *WGUDPProxy) close() error {
var result *multierror.Error
p.closeMu.Lock()
defer p.closeMu.Unlock()
// prevent double close
if p.closed {
return nil
}
p.closeListener.SetCloseListener(nil)
p.closed = true
View on GitHub (pinned to 93e97f4bf1)
Solutions
- Ensure AddTurnConn succeeded (check its error) before any InjectPacket/CloseConn/Work call on the same proxy instance
- Order teardown as: stop using the conn, then CloseConn once; guard with your own started flag if multiple goroutines can trigger close
- Remember NewWGUDPProxy only allocates - Work() and EndpointAddr() no-op silently until AddTurnConn populates the fields
Example fix
// before
proxy := NewWGUDPProxy(wgPort, mtu)
_ = proxy.InjectPacket(pkt) // "proxy not started"
// after
proxy := NewWGUDPProxy(wgPort, mtu)
if err := proxy.AddTurnConn(ctx, nil, remoteConn); err != nil {
return fmt.Errorf("add turn conn: %w", err)
}
if err := proxy.InjectPacket(pkt); err != nil {
return fmt.Errorf("inject: %w", err)
} Defensive patterns
Strategy: validation
Validate before calling
// call order contract: AddTurnConn must succeed first
if err := proxy.AddTurnConn(ctx, nil, remoteConn); err != nil {
return fmt.Errorf("start proxy: %w", err)
}
// only now inject/close
if err := proxy.InjectPacket(pkt); err != nil { ... } Type guard
// wrap the proxy to make the started state explicit for callers
type startedUDPProxy struct{ p *udp.WGUDPProxy }
func startProxy(ctx context.Context, p *udp.WGUDPProxy, rc net.Conn) (*startedUDPProxy, error) {
if err := p.AddTurnConn(ctx, nil, rc); err != nil {
return nil, err
}
return &startedUDPProxy{p}, nil
} Try / catch
if err := proxy.InjectPacket(b); err != nil {
if errors.Is(err, errProxyNotStarted) || strings.Contains(err.Error(), "proxy not started") {
return fmt.Errorf("lifecycle bug: proxy used before AddTurnConn: %w", err)
}
return err
} Prevention
- Treat NewWGUDPProxy as allocation only; every other method requires a successful AddTurnConn
- Do not share one WGUDPProxy across goroutines - the type is documented not thread safe
- In teardown paths, guard CloseConn so it only runs for proxies that were actually started
When it happens
Trigger: Calling InjectPacket or CloseConn on a WGUDPProxy obtained from NewWGUDPProxy before AddTurnConn; calling them after AddTurnConn failed its dial (fields stay nil); races where a second AddTurnConn overwrites state while another goroutine proxies.
Common situations: Integrations/embedders (client/embed) driving the UDP proxy manually; test code exercising close paths; ICE agent code paths that tear down candidates before the proxy is fully wired. In shipped code it surfaces as a programming-order bug rather than an environment issue.
Related errors
- service is not up
- client not initialized
- engine not started
- invalid rule type
- ICEBind has not been initialized yet
AI-assisted analysis of netbirdio/netbird@93e97f4bf1 (2026-08-16).
Data as JSON: /api/errors/76cdcbafb745ec3c.
Report an issue: GitHub.