diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a2470a8..578fa2c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,36 @@ its first tagged release. ## [Unreleased] +### Changed + +- **Both ingest ports bind by default, and both are published by default.** + The RTMP listener used to bind only when some enabled source was configured + for it, while SRT bound unconditionally. That was not a policy — it was two + histories preserved side by side: `ffmpeg -listen 1` had held 1935 only while + an RTMP source existed, and the SRT listener had always been up. The asymmetry + showed on a fresh install that had chosen no ingest mode at all, which still + opened 6000 while refusing to open 1935 on the grounds that nothing there + spoke the protocol. + + It also disagreed with this project's own instructions: `docs/HARDWARE.md` and + `docs/TROUBLESHOOTING.md` have always said to run + `-p 6000:6000/udp -p 1935:1935`, so we documented publishing a port that might + not be listening. datarhei Restreamer opens both, and so do we now. + + `install.sh` matches: it asks for an RTMP port the way it asks for an SRT one + rather than a yes/no, defaults to publishing 1935, and takes `--rtmp-port 0` + to decline it. **The port is the switch on both sides** — the server treats 0 + as off too — instead of a yes/no in the installer and a port in the settings + meaning the same thing two different ways. + + What this adds is narrow: a host with **no firewall at all** now has 1935 + reachable, where the source list used to close it by accident. Everywhere else + the ufw rule and the compose publish still decide, which is what they always + claimed to do. Both listeners refuse an unknown token or key in constant time, + both require the source to be ready before admitting anything, and a + connection that says nothing dies on the handshake timeout. + + ## [0.4.0] — 2026-08-07 A minor bump. Nothing here breaks a stored config, but two behaviours change in diff --git a/internal/engine/manager.go b/internal/engine/manager.go index c2fae758..cdea98a4 100644 --- a/internal/engine/manager.go +++ b/internal/engine/manager.go @@ -228,27 +228,33 @@ func (m *Manager) reconcileSharedIngest() { s := srtserver.New(m.log, addr, m.lookupToken) return s, s.Start() }) - // The RTMP port is bound only when a source actually uses RTMP. + // BOTH LISTENERS BIND, ALWAYS. The port is the switch, not the source list. // - // SRT's listener is unconditional because it IS the SRT ingest and a source - // can be pointed at it at any moment. RTMP's was not: before this package, - // `ffmpeg -listen 1` bound 1935 only while an RTMP source was configured, - // so making it unconditional would newly expose a port on every install — - // including a fresh one that has not chosen an ingest mode yet — for a - // protocol nothing there speaks. Port 0 disables it explicitly, and no RTMP - // source has the same effect. + // RTMP used to bind only when some enabled source was configured for it, + // which preserved what `ffmpeg -listen 1` did before this package existed. + // SRT bound unconditionally, which preserved what IT did. Neither was a + // policy: they were two different histories, and the asymmetry showed. A + // fresh install that has not chosen an ingest mode still opened 6000 — the + // exact thing the old comment here justified NOT doing for 1935. + // + // Symmetry is also what the ecosystem does and what this project already + // documents: datarhei Restreamer publishes 1935 and 6000 together, and + // docs/HARDWARE.md and docs/TROUBLESHOOTING.md have always told operators to + // run `-p 6000:6000/udp -p 1935:1935`. Binding only one of them made our own + // install instructions describe a port that might not be listening. + // + // The exposure this adds is small and bounded. Both listeners refuse an + // unknown token or key in constant time, both require Target.Ready before + // admitting anything, and a connection that opens and says nothing dies on + // the handshake timeout. What changes is that a host with no firewall now + // has 1935 reachable where the source list used to close it by accident — + // so `install.sh`'s RTMP prompt, which controls the ufw rule and the + // compose publish, is now the only thing that decides reachability. It was + // always the thing that SAID it did. + // + // Port 0 remains the explicit off switch for either protocol. rtmpPort := st.Listeners.RTMPPort - wantRTMP := true - if rows, err := m.store.ListSources(); err != nil { - // Read failure: leave the listener as it is rather than tearing it down - // on a transient error. An RTMP encoder that is mid-broadcast must not - // lose its socket because a settings read blipped. - m.log.Warn("cannot list sources for the rtmp listener gate; leaving it as-is", "err", err) - wantRTMP = rtmpAddr != "" - } else { - wantRTMP = anyRTMPSource(rows) - } - rtmp, rtmpAddr = reconcileListener(m.log, "rtmp", rtmpPort, wantRTMP, rtmp, rtmpAddr, + rtmp, rtmpAddr = reconcileListener(m.log, "rtmp", rtmpPort, true, rtmp, rtmpAddr, (*rtmpserver.Server).Stop, func(addr string) (*rtmpserver.Server, error) { s := rtmpserver.New(m.log, addr, m.lookupStreamKey) @@ -261,20 +267,6 @@ func (m *Manager) reconcileSharedIngest() { m.mu.Unlock() } -// anyRTMPSource reports whether any enabled source expects the RTMP listener. -// -// Disabled sources do not count: a disabled source has no engine, so it has no -// subscriber, so a publisher reaching it would be admitted into a stream nobody -// reads — which is the failure Target.Ready exists to prevent. -func anyRTMPSource(rows []*db.Source) bool { - for _, s := range rows { - if s.Enabled && s.Ingest.Mode == db.IngestRTMP { - return true - } - } - return false -} - // reconcileListener brings one shared listener to match its configured port, // returning what is bound afterwards and at which address. // diff --git a/internal/engine/manager_test.go b/internal/engine/manager_test.go index c0850e59..dc023ca0 100644 --- a/internal/engine/manager_test.go +++ b/internal/engine/manager_test.go @@ -233,8 +233,9 @@ func TestIngestLiveAndGPUBusyAggregateAcrossProgrammes(t *testing.T) { } // Neither listener is optional -- each IS the ingest for its protocol -- so the -// thing worth pinning is that both come up on their own, and that a port one of -// them cannot use leaves it down rather than bound to something arbitrary. +// thing worth pinning is that both come up on their own, whatever the sources +// happen to be configured for, and that a port one of them cannot use leaves it +// down rather than bound to something arbitrary. func TestBothListenersBindWithoutBeingAskedTo(t *testing.T) { m, store := managerFixture(t) // Free ports rather than the 6000/1935 defaults: a unit test that binds a @@ -252,18 +253,22 @@ func TestBothListenersBindWithoutBeingAskedTo(t *testing.T) { if err := m.Start(context.Background()); err != nil { t.Fatalf("Start: %v", err) } - // SRT's listener IS the SRT ingest and binds unconditionally: a source can - // be pointed at it at any moment. + // BOTH bind, and this fixture has no RTMP source at all — that is the point. + // + // This test used to assert the opposite for RTMP, on the reasoning that + // binding it unconditionally would newly expose a port on an install where + // nothing spoke the protocol. The hole in that argument was visible in the + // line above it: SRT bound unconditionally on exactly the same install, + // including a fresh one that had chosen no ingest mode. The asymmetry was + // two histories preserved side by side rather than a decision, and it made + // the project's own install instructions — which publish both ports — describe + // something that might not be listening. if !m.ListenerBound(db.IngestSRT) { t.Error("the SRT listener did not bind; every SRT source is unreachable") } - // RTMP's does NOT, and the asymmetry is deliberate. Before the shared - // listener existed, `ffmpeg -listen 1` bound 1935 only while an RTMP source - // was configured. Binding it unconditionally would newly expose a port on - // every install — including a fresh one that has not chosen an ingest mode — - // for a protocol nothing there speaks. This fixture has no RTMP source. - if m.ListenerBound(db.IngestRTMP) { - t.Error("the RTMP listener bound with no RTMP source configured; that is a port nobody asked for") + if !m.ListenerBound(db.IngestRTMP) { + t.Error("the RTMP listener did not bind. Both ports are open by default now; " + + "the port setting is the switch, and 0 is how an operator declines one") } // Pull dials out and has no listener to be gated by. Answering yes here // would tell an operator a token protects an ingest no publisher reaches. diff --git a/scripts/install.sh b/scripts/install.sh index 5f73d9d7..fbba5343 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -55,7 +55,10 @@ MODE="" # docker | binary TLS_MODE="off" # off | selfsigned | acme DOMAIN_NAME="" ACME_EMAIL="" -ENABLE_RTMP="no" +# Both ingest ports are published by default, matching what the server now +# binds and what this project's own docs have always told people to run +# (`-p 6000:6000/udp -p 1935:1935`). Set --rtmp-port 0 to decline it. +ENABLE_RTMP="yes" CONFIGURE_FIREWALL="no" COMPOSE_CMD="" @@ -571,7 +574,11 @@ gather_configuration() { header "=== Ports ===" [ "$HTTP_PORT_SET" = true ] || ask "Web UI port (tcp)" "$HTTP_PORT" HTTP_PORT [ "$SRT_PORT_SET" = true ] || ask "SRT ingest port (UDP — this is the one people forget)" "$SRT_PORT" SRT_PORT - [ "$RTMP_SET" = true ] || ask_yn "Also expose RTMP on ${RTMP_PORT}/tcp? Only needed for encoders that cannot do SRT" "n" ENABLE_RTMP + [ "$RTMP_SET" = true ] || ask "RTMP ingest port (tcp — 0 to decline it)" "$RTMP_PORT" RTMP_PORT + # The port IS the switch, server-side too: internal/engine binds both + # listeners and treats 0 as off. Asking a yes/no here and a port there meant + # two different ways to say the same thing. + case "$RTMP_PORT" in 0|"") ENABLE_RTMP="no" ;; *) ENABLE_RTMP="yes" ;; esac warn_if_taken "$HTTP_PORT" tcp "web UI" warn_if_taken "$SRT_PORT" udp "SRT ingest" @@ -1079,7 +1086,8 @@ Options: --mode docker|binary install mode (default: ask, then docker) --http-port N web UI port (default 8080) --srt-port N SRT ingest port, UDP (default 6000) - --rtmp also expose RTMP (default off) + --rtmp-port N RTMP ingest port, tcp (default 1935; 0 declines it) + --rtmp accepted for compatibility; RTMP is published by default --tls off|selfsigned|acme TLS mode. Not passing it takes the interactive default, which is SELFSIGNED -- including under --yes. @@ -1127,6 +1135,8 @@ parse_args() { --srt-port) [ $# -ge 2 ] || die "missing value for --srt-port"; SRT_PORT="$2"; SRT_PORT_SET=true; shift 2 ;; --srt-port=*) SRT_PORT="${1#*=}"; SRT_PORT_SET=true; shift ;; --rtmp) ENABLE_RTMP="yes"; RTMP_SET=true; shift ;; + --rtmp-port) [ $# -ge 2 ] || die "missing value for --rtmp-port"; RTMP_PORT="$2"; RTMP_SET=true; shift 2 ;; + --rtmp-port=*) RTMP_PORT="${1#*=}"; RTMP_SET=true; shift ;; --tls) [ $# -ge 2 ] || die "missing value for --tls"; TLS_MODE="$2"; TLS_SET=true; shift 2 ;; --tls=*) TLS_MODE="${1#*=}"; TLS_SET=true; shift ;; --hostname) [ $# -ge 2 ] || die "missing value for --hostname"; DOMAIN_NAME="$2"; shift 2 ;;