Skip to content

ServeBackground returns before UDP connection is added to sipgo's pool, causing "bind: address already in use" on Register #134

@jmarble

Description

@jmarble

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

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions