Skip to content

Commit 1010338

Browse files
committed
EC: TCP keepalive on both ends so half-open connections get torn down (amule-project#757)
Without keepalive, if amuled's FIN or RST never reaches amulegui / amulecmd / amuleweb (peer crashed, network blip, process killed, packet dropped), the kernel sits on the half-open connection for the default ~2h TCP retransmit timeout. CECSocket::OnLost never fires, the GUI shows a connected status that's actually dead, and the daemon side keeps the CECServerSocket + its m_ec_notifier reference live for the same duration. Symmetric SO_KEEPALIVE + TCP_KEEPIDLE/INTVL/CNT (idle=30s, interval=10s, count=3 → ~60s half-open detection) on every EC socket, applied: * Client side from CECMuleSocket::InternalConnect on a successful CLibSocket::Connect (catches sync clients like amulecmd). * Async client side from CRemoteConnect::OnConnect, which fires when the async_connect handler runs on amulegui / amuleweb (where InternalConnect returns before the underlying connect completes, so we have to wait for the actual establishment). * Server side from CExternalConnListener::OnAccept right after AcceptWith succeeds, on the freshly-created CECServerSocket. The plumbing: * LibSocketAsio.cpp: SetTcpKeepalive() static helper modelled on SetCloexecOnSocket — POSIX uses setsockopt(SOL_SOCKET, SO_KEEPALIVE) + the three TCP_KEEP* knobs (TCP_KEEPALIVE as the idle spelling on macOS/*BSD, no INTVL/CNT there), Windows uses WSAIoctl(SIO_KEEPALIVE_VALS) which exposes idle + interval but not count. * CAsioSocketImpl::EnableTcpKeepalive() applies the helper to m_socket->native_handle() when the socket is open. * CLibSocket::EnableTcpKeepalive() delegates to the impl. * CECMuleSocket::ApplyEcKeepalive() bakes in the EC-tuned timings so the three call sites stay one-liners. Refs amule-project#757.
1 parent 2ef9763 commit 1010338

6 files changed

Lines changed: 137 additions & 3 deletions

File tree

src/ExternalConn.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,14 @@ void CExternalConnListener::OnAccept()
471471
// non-blocking accept (although if we got here, there
472472
// should ALWAYS be a pending connection).
473473
if (AcceptWith(*sock, false)) {
474+
// Apply EC keepalive on the freshly-accepted server-side
475+
// socket so amuled detects a half-open EC client (gui
476+
// process killed, network blip, FIN lost) symmetrically
477+
// with what the client just enabled on its end. Without
478+
// this, the kernel sits on the dead connection for the
479+
// default ~2h TCP retransmit timeout, holding the
480+
// CECServerSocket and its m_ec_notifier reference.
481+
sock->ApplyEcKeepalive();
474482
AddLogLineN(_("New external connection accepted"));
475483
} else {
476484
delete sock;

src/LibSocket.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,18 @@ class CLibSocket
116116
wxString GetPeer();
117117
uint32 GetPeerInt();
118118

119+
// Turn on TCP keepalive with per-socket timings so a half-open
120+
// connection (peer gone, FIN/RST lost or never sent) gets torn
121+
// down at the TCP layer instead of sitting idle forever. Used by
122+
// the EC sockets on both ends — see CECMuleSocket / CECServerSocket.
123+
// Idle seconds before the kernel starts probing, the interval
124+
// between probes, and how many probes before declaring the peer
125+
// dead. Effective only on POSIX (TCP_KEEPIDLE / TCP_KEEPINTVL /
126+
// TCP_KEEPCNT) and Windows (SIO_KEEPALIVE_VALS; only idle +
127+
// interval are settable, count uses the system default). No-op if
128+
// the underlying socket is not open.
129+
void EnableTcpKeepalive(int idleSec, int probeIntervalSec, int probeCount);
130+
119131
// Handlers
120132
virtual void OnConnect(int) {}
121133
virtual void OnSend(int) {}

src/LibSocketAsio.cpp

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,14 @@
7272
#include "ScopedPtr.h"
7373
#include <common/Macros.h>
7474

75-
#ifndef __WINDOWS__
76-
#include <fcntl.h> // FD_CLOEXEC
75+
#ifdef __WINDOWS__
76+
// SIO_KEEPALIVE_VALS + struct tcp_keepalive for SetTcpKeepalive.
77+
// winsock2.h is already brought in transitively by boost::asio.
78+
#include <mstcpip.h>
79+
#else
80+
#include <fcntl.h> // FD_CLOEXEC
81+
#include <netinet/tcp.h> // TCP_KEEPIDLE / TCP_KEEPINTVL / TCP_KEEPCNT
82+
#include <sys/socket.h> // SO_KEEPALIVE
7783
#endif
7884

7985
using namespace boost::asio;
@@ -104,6 +110,56 @@ static inline void SetCloexecOnSocket(Handle native)
104110
#endif
105111
}
106112

113+
114+
// Turn on TCP keepalive with per-socket timings. Used by the EC sockets
115+
// to detect a half-open connection (peer gone, FIN/RST lost or never
116+
// sent — common after a network blip, OOM-kill, etc.) instead of
117+
// sitting idle until the default ~2h TCP retransmit timeout kicks in.
118+
//
119+
// POSIX: SO_KEEPALIVE plus the three TCP-layer timing knobs. Linux
120+
// names (TCP_KEEPIDLE / TCP_KEEPINTVL / TCP_KEEPCNT) are the canonical
121+
// set; macOS / *BSD use TCP_KEEPALIVE for the idle time and inherit
122+
// the system defaults for interval and count, which is acceptable as
123+
// a fallback.
124+
//
125+
// Windows: SIO_KEEPALIVE_VALS via WSAIoctl. The Windows surface only
126+
// exposes idle and interval; the probe count uses the system default
127+
// (typically 10 on modern Windows).
128+
template <typename Handle>
129+
static inline void SetTcpKeepalive(Handle native, int idleSec, int intervalSec, int count)
130+
{
131+
#ifdef __WINDOWS__
132+
struct tcp_keepalive ka = {};
133+
ka.onoff = 1;
134+
ka.keepalivetime = static_cast<ULONG>(idleSec) * 1000;
135+
ka.keepaliveinterval = static_cast<ULONG>(intervalSec) * 1000;
136+
DWORD bytesReturned = 0;
137+
(void) count; // SIO_KEEPALIVE_VALS doesn't expose count
138+
::WSAIoctl(native, SIO_KEEPALIVE_VALS, &ka, sizeof(ka),
139+
NULL, 0, &bytesReturned, NULL, NULL);
140+
#else
141+
int yes = 1;
142+
::setsockopt(native, SOL_SOCKET, SO_KEEPALIVE, &yes, sizeof(yes));
143+
#ifdef TCP_KEEPIDLE
144+
::setsockopt(native, IPPROTO_TCP, TCP_KEEPIDLE, &idleSec, sizeof(idleSec));
145+
#elif defined(TCP_KEEPALIVE)
146+
// macOS / *BSD spelling — idle-only, no separate INTVL/CNT knobs.
147+
::setsockopt(native, IPPROTO_TCP, TCP_KEEPALIVE, &idleSec, sizeof(idleSec));
148+
#endif
149+
#ifdef TCP_KEEPINTVL
150+
::setsockopt(native, IPPROTO_TCP, TCP_KEEPINTVL, &intervalSec, sizeof(intervalSec));
151+
#else
152+
(void) intervalSec;
153+
#endif
154+
#ifdef TCP_KEEPCNT
155+
::setsockopt(native, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));
156+
#else
157+
(void) count;
158+
#endif
159+
#endif
160+
}
161+
162+
107163
// Number of threads in the Asio thread pool
108164
const int CAsioService::m_numberOfThreads = 4;
109165

@@ -228,6 +284,17 @@ class CAsioSocketImpl : public std::enable_shared_from_this<CAsioSocketImpl>
228284
return m_OK;
229285
}
230286

287+
// Apply TCP keepalive timings to the underlying socket if it's open.
288+
// Caller is expected to invoke this after a successful connect (client
289+
// side) or accept (server side) so the kernel native_handle is live.
290+
void EnableTcpKeepalive(int idleSec, int probeIntervalSec, int probeCount)
291+
{
292+
if (!m_socket || !m_socket->is_open()) {
293+
return;
294+
}
295+
SetTcpKeepalive(m_socket->native_handle(), idleSec, probeIntervalSec, probeCount);
296+
}
297+
231298
bool IsDestroying() const
232299
{
233300
return m_destroying.load(std::memory_order_acquire);
@@ -748,6 +815,12 @@ bool CLibSocket::IsOk() const
748815
}
749816

750817

818+
void CLibSocket::EnableTcpKeepalive(int idleSec, int probeIntervalSec, int probeCount)
819+
{
820+
m_aSocket->EnableTcpKeepalive(idleSec, probeIntervalSec, probeCount);
821+
}
822+
823+
751824
wxString CLibSocket::GetPeer()
752825
{
753826
return m_aSocket->GetPeer();

src/libs/ec/cpp/ECMuleSocket.cpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,36 @@ bool CECMuleSocket::ConnectSocket(amuleIPV4Address& address)
5151
}
5252

5353

54+
// EC-connection keepalive timings. With these values, a half-open
55+
// connection (peer crashed / network blip / FIN lost) is torn down at
56+
// the TCP layer in ~60s instead of sitting idle until the default
57+
// ~2h TCP retransmit timeout, so CECSocket::OnLost fires and the GUI
58+
// can flip to "Connection lost" instead of looking wedged. Same
59+
// constants used by CECServerSocket on the amuled side so detection
60+
// is symmetric. Numbers picked to balance responsiveness against the
61+
// keepalive packet overhead (one probe per 10s after 30s idle).
62+
namespace { const int EC_KEEPALIVE_IDLE_SEC = 30; }
63+
namespace { const int EC_KEEPALIVE_INTERVAL_SEC = 10; }
64+
namespace { const int EC_KEEPALIVE_PROBE_COUNT = 3; }
65+
5466
bool CECMuleSocket::InternalConnect(uint32_t ip, uint16_t port, bool wait) {
5567
amuleIPV4Address addr;
5668
addr.Hostname(Uint32toStringIP(ip));
5769
addr.Service(port);
58-
return CLibSocket::Connect(addr, wait);
70+
bool ok = CLibSocket::Connect(addr, wait);
71+
if (ok) {
72+
// Asio opens the socket fd during connect / async_connect, so
73+
// setsockopt is valid here regardless of sync vs async mode.
74+
ApplyEcKeepalive();
75+
}
76+
return ok;
77+
}
78+
79+
void CECMuleSocket::ApplyEcKeepalive() {
80+
CLibSocket::EnableTcpKeepalive(
81+
EC_KEEPALIVE_IDLE_SEC,
82+
EC_KEEPALIVE_INTERVAL_SEC,
83+
EC_KEEPALIVE_PROBE_COUNT);
5984
}
6085

6186
int CECMuleSocket::InternalGetLastError()

src/libs/ec/cpp/ECMuleSocket.h

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ class CECMuleSocket : public CECSocket, public CLibSocket {
4646
virtual void OnSend(int) { OnOutput(); }
4747
virtual void OnReceive(int) { OnInput(); }
4848

49+
// Apply EC-tuned TCP keepalive (idle=30s / probe=10s / count=3 →
50+
// ~60s half-open detection). Called automatically from
51+
// InternalConnect after a successful client-side connect; subclasses
52+
// that take over OnConnect (CRemoteConnect) and the server-side
53+
// accept path (ExternalConn.cpp::OnAccept on amuled) call this
54+
// explicitly so detection is symmetric on both ends of every EC
55+
// connection.
56+
void ApplyEcKeepalive();
57+
4958
private:
5059
bool InternalConnect(uint32_t ip, uint16_t port, bool wait);
5160

src/libs/ec/cpp/RemoteConnect.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,13 @@ void CRemoteConnect::WriteDoneAndQueueEmpty()
180180
}
181181

182182
void CRemoteConnect::OnConnect() {
183+
// Apply the EC-tuned TCP keepalive timings now that the underlying
184+
// asio socket is fully connected on the async path (sync clients
185+
// got it inside CECMuleSocket::InternalConnect already; this is
186+
// the amulegui / amuleweb side where InternalConnect returns before
187+
// the connect actually completes).
188+
ApplyEcKeepalive();
189+
183190
if (m_notifier) {
184191
wxASSERT(m_ec_state == EC_CONNECT_SENT);
185192
CECLoginPacket login_req(m_client, m_version, m_canZLIB, m_canUTF8numbers, m_canNotify);

0 commit comments

Comments
 (0)