When calling Register() immediately after ServeBackground() returns, the REGISTER transaction fails with:
client transcation failed to request connection: listen udp 127.0.0.1:35256: bind: address already in use
Note: "transcation" in the error string is a typo in sipgo (transaction).
Root cause:
In diago.serve(), the ListenReadyFuncCtxValue callback fires after net.ListenUDP succeeds (server.go:129) but before srv.tp.ServeUDP() runs (server.go:130), which is where pool.Add happens (transport_udp.go:61):
// server.go ListenAndServe
udpConn, err := net.ListenUDP(network, laddr) // binds port
listenReadyCtx(ctx, network, udpConn.LocalAddr().String()) // fires readyCh here
return srv.tp.ServeUDP(udpConn) // pool.Add happens inside here
So ServeBackground returns while the connection pool is still empty. When Register() runs, ClientRequestConnection calls GetConnection(laddr) -> returns nil -> calls CreateConnection -> tries ListenPacket on the same port -> OS rejects it.
The race is acknowledged in comments at diago.go:874:
// Checking ports with ListenPorts is racy with ListenAndServe
// In case UDP, we want to have this port reused.
bindPort = tran.BindPort
Reproduction:
dg := diago.NewDiago(ua, diago.WithTransport(diago.Transport{
Transport: "udp",
BindHost: "127.0.0.1",
BindPort: 0, // ephemeral
RewriteContact: true,
}))
ctx, cancel := context.WithCancel(context.Background())
dg.ServeBackground(ctx, handler)
// This can fail with "address already in use"
dg.Register(ctx, registrar, diago.RegisterOptions{...})
Suggested fix:
Move the readyCh signal so it fires after pool.Add rather than after ListenUDP. One approach: have ServeUDP/TransportUDP.Serve accept an optional ready callback that fires after pool.Add:
// transport_udp.go
func (t *TransportUDP) Serve(conn net.PacketConn, handler MessageHandler) error {
c := &UDPConnection{...}
t.pool.Add(c.PacketAddr, c) // add to pool first
// signal ready here, after pool is populated
t.readListenerConnection(c, c.PacketAddr, handler)
return nil
}
Or alternatively, listenReadyCtx could be called from within ServeUDP after pool.Add.
Workaround:
Retry Register() when the error contains "address already in use":
for attempt := 1; attempt <= 3; attempt++ {
err := dg.Register(ctx, registrar, opts)
if err == nil {
break
}
if strings.Contains(err.Error(), "address already in use") && attempt < 3 {
time.Sleep(time.Duration(attempt*50) * time.Millisecond)
continue
}
return err
}
Versions: diago v0.27.0, sipgo v1.2.0
When calling
Register()immediately afterServeBackground()returns, the REGISTER transaction fails with:Root cause:
In
diago.serve(), theListenReadyFuncCtxValuecallback fires afternet.ListenUDPsucceeds (server.go:129) but beforesrv.tp.ServeUDP()runs (server.go:130), which is wherepool.Addhappens (transport_udp.go:61):So
ServeBackgroundreturns while the connection pool is still empty. WhenRegister()runs,ClientRequestConnectioncallsGetConnection(laddr)-> returns nil -> callsCreateConnection-> triesListenPacketon the same port -> OS rejects it.The race is acknowledged in comments at diago.go:874:
Reproduction:
Suggested fix:
Move the
readyChsignal so it fires afterpool.Addrather than afterListenUDP. One approach: haveServeUDP/TransportUDP.Serveaccept an optional ready callback that fires afterpool.Add:Or alternatively,
listenReadyCtxcould be called from withinServeUDPafterpool.Add.Workaround:
Retry
Register()when the error contains "address already in use":Versions: diago v0.27.0, sipgo v1.2.0