diff --git a/app/webrtc/whip_lifecycle.go b/app/webrtc/whip_lifecycle.go index a7c57eb..8fcbce0 100644 --- a/app/webrtc/whip_lifecycle.go +++ b/app/webrtc/whip_lifecycle.go @@ -2,6 +2,7 @@ package webrtc import ( "fmt" + "net" appcfg "github.com/datarhei/core/v16/restream/app" ) @@ -103,7 +104,7 @@ func (s *Subsystem) onWHIPProcessStop(id string) { // allocAdjacentPortPair finds two consecutive free loopback UDP ports // (V, V+1) and returns V. It retries up to allocAttempts times because -// the kernel may hand us an odd-ended pair whose +1 neighbor is taken. +// the kernel may hand us a port whose +1 neighbor is already taken. // // The caller owns the returned port numbers; FFmpeg will bind them // immediately on process start via the ConfigIO input legs. @@ -115,14 +116,14 @@ func allocAdjacentPortPair() (int, error) { lastErr = err continue } - // Verify the audio port (videoPort+1) is also free. - // We do a quick bind-and-release on videoPort+1. audioPort := videoPort + 1 if audioPort > 65535 { - lastErr = fmt.Errorf("video port %d would require audio port > 65535", videoPort) + lastErr = fmt.Errorf("video port %d would push audio port above 65535", videoPort) continue } - // Try to claim videoPort+1 momentarily to verify availability. + // Verify the audio port (videoPort+1) is also free by attempting + // a momentary bind. TOCTOU race is accepted; FFmpeg will fail-fast + // on the actual bind and the process will restart cleanly. if err := checkPortFree(audioPort); err != nil { lastErr = err continue @@ -135,27 +136,15 @@ func allocAdjacentPortPair() (int, error) { return 0, fmt.Errorf("after %d attempts: %w", allocAttempts, lastErr) } -// checkPortFree does a quick UDP bind-and-release on the given loopback -// port. Returns nil if the port appears free, non-nil if it is in use. -// There is an inherent TOCTOU race between this check and FFmpeg's bind; -// callers should handle the bind failure gracefully (process restart). +// checkPortFree attempts a momentary UDP bind on the given loopback +// port. Returns nil if the port appears available, non-nil otherwise. func checkPortFree(port int) error { - import_net_listen := func() error { - // Inline the check without importing net again (already in portalloc.go - // via the package-level Alloc function). We re-use Alloc's pattern: - // bind :port, get error, close. - // - // This file is in the same package so we can call the unexported helper. - // However, since we only have Alloc() (which uses :0), we implement - // this using net directly here. - return nil + c, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}) + if err != nil { + return fmt.Errorf("port %d not free: %w", port, err) } - _ = import_net_listen - // Use net directly since this package already imports it via portalloc.go. - // We can't import "net" twice, but since this file is in the same package - // as portalloc.go which imports "net", the package-level net functions are - // available to us via the Alloc() pattern. We implement the check manually: - return checkUDPPortFree(port) + _ = c.Close() + return nil } // buildWHIPInputLegs produces the two FFmpeg input ConfigIO legs for