fix(whip): clean up lifecycle — proper net import and checkPortFree implementation
Some checks failed
ci / race tests (push) Blocked by required conditions
ci / WebRTC smoke (5-viewer fanout) (push) Blocked by required conditions
ci / WebRTC latency p95 gate (push) Blocked by required conditions
ci / vet + build (push) Has been cancelled

This commit is contained in:
Zac Gaetano 2026-05-09 16:34:20 -04:00
parent b1057756d2
commit a22b8c68f0

View file

@ -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