[BUG] TunnelServer deletes session immediately after handshake, breaking TCP tunnel functionality
Problem Summary
TunnelServer deletes sessions immediately after handshake, causing TCP tunnels to receive different ports on each reconnection and making the system unstable.
Observed behavior:
- Client connects to TunnelServer → gets TCP port 40000
- Client reconnects → gets TCP port 40001
- Next reconnect → gets TCP port 40002
- Pattern continues indefinitely
Root cause: ClientConnectionHandler calls CleanupAsync() in the finally block after every handshake, even successful ones.
---
Architecture Context
Tunnel2 uses split-plane architecture:
- TunnelServer (Control Plane) - Manages handshake, JWT tokens, routing
- Client connects to
TunnelServer:12002 - Handshake is a short-lived connection
- Server returns: SessionId, JWT token, PublicTcpPort
- Connection closes after handshake
- ProxyEntry (Data Plane) - Routes traffic through active tunnels
- Client connects to
ProxyEntry:12001(uplink) - Uplink is a long-lived connection
- ProxyEntry creates TCP listener on
PublicTcpPort - ProxyEntry expects session to exist in PostgreSQL
---
Root Cause Analysis
File: tunnel2-server/src/Tunnel2.TunnelServer.ConsoleApp/ConnectionHandlers/ClientConnectionHandler.cs:101-116
finally
{
// Cleanup: session store, resource tracking, TCP ports, events
try
{
await _cleanupService.CleanupAsync(context); // ❌ ALWAYS called!
_logger.LogDebug("[PIPELINE] Cleanup completed for SessionId={SessionId}", context.SessionId);
}
catch (Exception ex)
{
_logger.LogError(ex, "[PIPELINE] Cleanup failed for SessionId={SessionId}", context.SessionId);
}
// Dispose TCP client
context.TcpClient?.Dispose();
}
Problem: CleanupAsync() is always invoked in the finally block, even after successful handshakes.
---
What CleanupAsync Does
File: tunnel2-server/src/Tunnel2.TunnelServer.Infrastructure/Services/ClientConnectionCleanupService.cs:49-134
When called, it:
- Deletes session from PostgreSQL (
DeactivateSessionAsync) - Releases TCP port (
ReleasePortAsync) - Removes TCP routing (
UnregisterAsync) - Publishes SessionClosed event to RabbitMQ
---
Evidence from Logs
{"@t":"2026-01-18T07:15:12.0528820Z","@mt":"[PUBLISH-STEP] Published SessionCreated: SessionId={SessionId}, Protocol={Protocol}, PublicTcpPort={Port}","Port":40001}
{"@t":"2026-01-18T07:15:12.0568699Z","@mt":"[CLEANUP] TCP resources released: Port={Port}, SessionId={SessionId}","Port":40001}
Timeline:
07:15:12.0528- Session created with port 4000107:15:12.0568- Same port released 40ms later!- Next handshake allocates port 40002
---
Expected Behavior
In split-plane architecture, session should persist after handshake:
- Client → TunnelServer (handshake)
- Create session in PostgreSQL
- Allocate TCP port
- Return SessionId + JWT + PublicTcpPort
- Close connection (short-lived)
- DO NOT delete session!
- Client → ProxyEntry (uplink)
- Attach uplink using JWT
- Create TCP listener on PublicTcpPort
- Session remains active
- Session cleanup should happen when:
- Uplink disconnects (ProxyEntry detects)
- Session expires (TTL)
- Explicit client disconnect
---
Current Workaround
None available. TCP tunnels are currently broken.
---
Possible Solutions
Option 1: Remove CleanupAsync from finally block (risky)
finally
{
// Only dispose TCP client, don't cleanup session
context.TcpClient?.Dispose();
}
Concern: Who will cleanup sessions if handshake fails?
Option 2: Conditional cleanup based on success
finally
{
if (!context.HandshakeSucceeded)
{
await _cleanupService.CleanupAsync(context);
}
context.TcpClient?.Dispose();
}
Option 3: Separate cleanup for handshake vs uplink
- Handshake cleanup: Only on failure
- Uplink cleanup: ProxyEntry triggers via RabbitMQ event
---
Impact
Critical:
- TCP tunnels completely broken
- Ports change on every reconnect
- ProxyEntry cannot find sessions
- Users cannot rely on stable TCP endpoints
---
Related Files
tunnel2-server/src/Tunnel2.TunnelServer.ConsoleApp/ConnectionHandlers/ClientConnectionHandler.cs:101-116tunnel2-server/src/Tunnel2.TunnelServer.Infrastructure/Services/ClientConnectionCleanupService.cstunnel2-server/src/Tunnel2.TunnelServer.Infrastructure/PipelineSteps/ProvisionSessionStep.cs
---
Commit History
Introduced in refactor: 4f4346e - refactor(phase-4): Refactor ClientConnectionHandler as pipeline orchestrator (691 LOC → 133 LOC, 81% reduction)
Likely unintended side-effect of pipeline refactoring.
---
Labels
bugcriticaltcp-tunnelscontrol-planesession-management
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗