TCP stream source (server mode) never recovers when the sender disappears without closing the connection
Summary
A tcp stream source in server mode stays stuck forever if its sender goes away without a FIN/RST (sender host reboots, loses power, laptop sleeps, Wi-Fi/VPN drops). snapserver keeps waiting on the dead connection, never accepts the sender's new connection, and the stream stays idle until snapserver is restarted.
Related: #995 (TCP keepalive for client connections). This report is about stream sources, where the consequence is a permanently dead source rather than a lingering client entry.
Environment
- snapserver 0.31.0 (Debian trixie, armhf, Raspberry Pi), source:
tcp://10.100.0.2:4955?name=VPS&mode=server - Sender: a remote host over WireGuard, pushing 44100:16:2 PCM
- Checked the code in v0.35.0 and
develop: the relevant paths are unchanged, so current versions should behave the same.
Steps to reproduce
- Configure a server-mode TCP source, e.g.
source = tcp://0.0.0.0:4953?name=TCP&mode=server. - From another host, stream PCM into it, e.g.
socat -u /dev/zero TCP:<server>:4953(silence is enough to keep it connected). - Make the sender vanish without the server seeing a close. The simplest way to simulate a reboot is on the sender host:
iptables -I OUTPUT -p tcp -d <server> --dport 4953 -j DROP pkill socat # its FIN/RST is dropped, so the server never sees it sleep 5 iptables -D OUTPUT -p tcp -d <server> --dport 4953 -j DROP - Start the sender again:
socat -u /dev/zero TCP:<server>:4953.
Expected
snapserver notices the old connection is dead (or prefers the new one) and starts reading from the new connection.
Actual
- The stream stays
idleindefinitely (No data since ... ms in stream 'TCP', switching to idleis logged once, then nothing). ss -tn '( sport = :4953 )'on the server shows the original connection stillESTABwith an empty receive queue (owned by snapserver). The new connection is alsoESTAB, with a fillingRecv-Q, but it is never accepted. In our case a sender retrying every few seconds piled up ~200 such connections.- Only restarting snapserver recovers.
Cause
TcpStream::connect()(server mode) arms a singleacceptor_->async_accept(...)and then only reads fromstream_.- A new accept is only armed after
AsioStream::do_read()hits a read error or EOF, which callsdisconnect()and thenconnect()100 ms later. - A peer that vanished without FIN/RST never produces a read error. The server only reads, so it never sends anything that would draw a RST from the rebooted peer, and there is no read timeout.
SO_KEEPALIVEis not set on the accepted socket, so the kernel never probes the connection either.- The idle timer (
check_state()) only callssetState(kIdle); it doesn't close the stale socket.
Possible fixes (either would solve it)
- Enable TCP keepalive on source sockets. Set it after accept in server mode and after connect in client mode. Ideally make it configurable through a URI parameter such as
keepalive=<idle seconds>:After the peer reboots, the first probe gets a RST, the read fails, and the existingstream_->set_option(boost::asio::socket_base::keep_alive(true)); #ifdef __linux__ int fd = stream_->native_handle(); int idle = 20, intvl = 5, cnt = 3; setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl)); setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt)); #endifdisconnect()→connect()path accepts the new connection. - Let a new connection replace the current one in server mode. Keep the acceptor armed while connected. When a new connection arrives, close the old socket and switch to the new one. This also covers peers that reconnect before any keepalive would fire.
Option 2 matches how a single-producer source is usually meant to work: the newest sender wins. Option 1 is the smaller change and also helps client mode.
Workaround we use for now
An LD_PRELOAD shim that wraps socket() to set SO_KEEPALIVE/TCP_KEEPIDLE=20/TCP_KEEPINTVL=5/TCP_KEEPCNT=3 on every TCP socket. Accepted sockets inherit these from the listening socket. With it, the reproduction above recovers about 11 s after the network comes back, without restarting snapserver. I'm happy to test a patch.
Source: snapcast/snapcast