This chapter explores sessions and connections in STT - how peers establish communication channels, maintain them, and handle errors. You'll learn the full lifecycle from connection establishment to graceful shutdown.
Agnostic Note: Sessions transport encrypted bytes. STT doesn't care if you're streaming video, sending files, or running custom protocols. The session just provides a secure, multiplexed byte channel. YOU define what flows through it.
Key concepts:
- Connection: Network-level link (UDP socket or WebSocket)
- Session: Encrypted, authenticated communication channel on top of connection
- Lifecycle: Setup → Active → Teardown
STT separates network connectivity from logical sessions:
+--------------------------------------------------+
| Session (Logical) |
| - Encrypted communication |
| - Stream multiplexing |
| - Application-level channel |
+--------------------------------------------------+
↓
+--------------------------------------------------+
| Connection (Network) |
| - UDP socket or WebSocket |
| - IP addresses and ports |
| - Physical network link |
+--------------------------------------------------+
Analogy:
- Connection = Phone line (physical infrastructure)
- Session = Conversation (what you're actually saying)
You can have the phone line connected but no conversation happening.
Flexibility:
- Sessions can resume across transports (UDP → WebSocket)
- Sessions can survive temporary connection loss via resumption tokens
0.2.0a0 (unreleased): Includes session continuity and multi-session management features
UDP (User Datagram Protocol) is the default transport:
from seigr_toolset_transmissions.node import STTNode
node = STTNode(
node_id=b"Alice",
port=8080,
transport='udp' # Default
)
node.start()Properties:
- Connectionless: No TCP handshake overhead
- Low latency: Minimal protocol overhead
- No congestion control: Application must handle (STT does this)
- Packet loss possible: STT detects and retransmits
Use UDP when:
- Low latency critical (real-time streaming)
- Direct connectivity available (no restrictive firewalls)
- You control both endpoints
WebSocket provides connection over HTTP:
node = STTNode(
node_id=b"Alice",
port=8080,
transport='websocket'
)
node.start()Properties:
- Connection-oriented: TCP underneath
- Firewall-friendly: Uses HTTP port 80/443
- Proxy traversal: Works through HTTP proxies
- Slightly higher latency: TCP + HTTP overhead
Use WebSocket when:
- Connecting through corporate firewalls
- NAT traversal needed (with TURN server)
- Browser compatibility (future JavaScript client)
Trade-off: ~10-20ms extra latency vs UDP, but better connectivity success rate.
node = STTNode(
node_id=b"Alice",
port=8080,
transport='udp',
bind_address='0.0.0.0', # Listen on all interfaces
reuse_port=True, # Allow multiple processes on same port
recv_buffer_size=2097152 # 2MB socket buffer
)bind_address: Which network interface to listen on
'0.0.0.0': All interfaces (default)'127.0.0.1': Localhost only (testing)'192.168.1.10': Specific interface
recv_buffer_size: OS-level socket buffer (handles bursts)
Before connecting, initialize node:
# Alice's side
alice_node = STTNode(
node_id=b"Alice-12345",
port=8080,
shared_seed=b"correct-horse-battery-staple"
)
alice_node.start() # Starts listeningNode is now listening for incoming connections on port 8080.
Initiate connection to peer:
# Alice connects to Bob at 10.0.1.5:8080
session = await alice_node.connect(
peer_address=('10.0.1.5', 8080),
peer_node_id=b"Bob-67890"
)Handshake process (see Chapter 5 for details):
- Alice sends HELLO (her nonce + node_id)
- Bob sends CHALLENGE (his nonce + encrypted challenge)
- Alice sends AUTH_PROOF (proves she has correct seed)
- Bob sends FINAL (confirms authentication)
Duration: Typically 20-50ms (4 round-trips)
Result: session object ready for use.
Session is established - can now send/receive data:
# Open a stream for file transfer
stream = session.open_stream(purpose='file_transfer')
# Send data
await stream.send(b"Hello Bob!")
# Receive data
data = await stream.receive()During active phase:
- Streams can be opened/closed dynamically
- Data flows bidirectionally
- Keep-alives maintain session (automatic)
- Encryption/decryption handled transparently
Graceful shutdown:
# Close specific stream
await stream.close()
# Close entire session
await session.close()
# Shutdown node (closes all sessions)
await alice_node.stop()Orderly teardown:
- Application calls
session.close() - STT sends
SESSION_CLOSEframe to peer - Peer acknowledges
- Both sides clean up resources
- Connection closed
Ungraceful shutdown (power loss, network failure):
- Peer detects missing keep-alives (timeout after ~30 seconds)
- Session marked as dead
- Resources cleaned up
- No data loss for confirmed sends (STT tracks acknowledgments)
One node can handle multiple sessions (though typically one-to-one):
# Alice's node
alice_node = STTNode(node_id=b"Alice", port=8080, shared_seed=b"seed_default")
alice_node.start()
# Connect to Bob
session_bob = await alice_node.connect(('10.0.1.5', 8080), b"Bob")
# Simultaneously connect to Carol (if needed)
session_carol = await alice_node.connect(('10.0.1.8', 8081), b"Carol")
# Both sessions active concurrentlyEach session:
- Independent encryption (separate derived keys)
- Independent streams
- Independent lifecycle
Practical use cases:
- Client connecting to multiple servers simultaneously
- Server handling multiple clients simultaneously (server mode)
Note: Peer discovery is implemented by applications built on top of STT transport.
Session ID uniquely identifies each session:
print(session.session_id.hex())
# Output: '0123456789abcdef' (8 bytes, hex-encoded)Derivation (from handshake):
session_id = (nonce_xor + node_xor)[:8]Properties:
- Unique per handshake (random nonces)
- Same for both peers (deterministic calculation)
- Used internally for logging, debugging
You rarely need to access session_id directly - STT handles it.
Internal states (FSM - Finite State Machine):
INIT → HANDSHAKING → ESTABLISHED → CLOSING → CLOSED
State transitions:
INIT: Session object created, not yet connectedHANDSHAKING: Handshake in progress (4 messages)ESTABLISHED: Authenticated, ready for dataCLOSING: Graceful shutdown initiatedCLOSED: Session terminated, resources freed
Check state:
from seigr_toolset_transmissions.session import SessionState
if session.state == SessionState.ESTABLISHED:
# Safe to send data
await stream.send(data)
else:
# Not ready yet
print(f"Session not ready: {session.state}")State changes are automatic - STT manages transitions.
SessionManager handles multiple simultaneous sessions:
# Get all active sessions
sessions = node.session_manager.get_active_sessions()
print(f"Currently connected to {len(sessions)} peers")
# Get specific session
session = node.session_manager.get_session(session_id)
# Check session count
count = node.session_manager.get_session_count()Automatic cleanup:
# SessionManager automatically removes closed sessions
removed = await node.session_manager.cleanup_closed_sessions()
print(f"Cleaned up {removed} closed sessions")Note: Applications can implement their own session organization strategies on top of SessionManager.
STT sends periodic keep-alives to detect dead connections:
# Configured at node level (defaults)
node = STTNode(
node_seed=b"my_seed" * 8,
shared_seed=b"shared" * 8
)
# Keep-alive happens automatically in the backgroundHow it works:
- Every 10 seconds, STT sends
KEEP_ALIVEframe - Peer responds with
KEEP_ALIVE_ACK - If no response after 30 seconds, session declared dead
- Application notified via callback/event
Why needed?
- Detect crashed peers (no graceful shutdown)
- Detect network failures (cable unplugged)
- Refresh NAT mappings (keeps firewall holes open)
Overhead: ~10 bytes every 10 seconds per session (negligible)
Timeouts at different levels:
Handshake timeout:
session = await alice_node.connect(
peer_address=('10.0.1.5', 8080),
peer_node_id=b"Bob",
timeout=5.0 # Give up after 5 seconds
)
# Raises TimeoutError if handshake not completeStream send timeout:
await stream.send(data, timeout=10.0)
# Raises TimeoutError if not acknowledged in 10 secondsKeep-alive timeout (configured above)
Best practices:
- Handshake: 5-10 seconds (allows for slow networks)
- Stream send: Depends on data size (1 second per MB reasonable)
- Keep-alive: 30 seconds (balance between detection speed and false positives)
Common failures:
try:
session = await node.connect(('10.0.1.5', 8080), b"Bob")
except ConnectionRefusedError:
print("Peer not listening (wrong IP or port)")
except ConnectionTimeoutError:
print("Handshake timeout (peer slow/dead/wrong seed)")
except AuthenticationError:
print("Wrong shared_seed (authentication failed)")
except Exception as e:
print(f"Unexpected error: {e}")Retry logic:
import asyncio
max_retries = 3
for attempt in range(max_retries):
try:
session = await node.connect(('10.0.1.5', 8080), b"Bob", timeout=5.0)
break # Success
except ConnectionTimeoutError:
if attempt < max_retries - 1:
await asyncio.sleep(2) # Wait 2 seconds
else:
raise # Give upDuring active session:
try:
await stream.send(large_data)
except SessionClosedError:
print("Session closed while sending (peer disconnected)")
except StreamClosedError:
print("Stream closed (application-level close)")
except TimeoutError:
print("Send timeout (no acknowledgment)")Handle gracefully:
if session.state == SessionState.ESTABLISHED:
try:
await stream.send(data)
except SessionClosedError:
# Session died unexpectedly
logger.error("Session lost, reconnecting...")
session = await node.connect(peer_address, peer_node_id)
# Retry send on new sessionSTT provides some automatic recovery:
Retransmissions:
- Lost UDP packets automatically retransmitted (transparent to application)
- Configurable retry limits
Reordering:
- Out-of-order frames automatically reordered (streams guarantee order)
Corruption detection:
- Checksums detect corrupted frames (discarded, retransmitted)
What STT does NOT auto-recover:
- Session-level failures (peer crash) - application must reconnect
- Authentication failures (wrong seed) - application must fix configuration
- Programming errors (invalid data) - application bug
# Register callback for session events
def on_session_closed(session_id, reason):
print(f"Session {session_id.hex()} closed: {reason}")
# Trigger reconnection logic
node.on_session_closed(on_session_closed)Reasons:
'graceful': Application calledsession.close()'timeout': Keep-alive timeout (peer dead/unreachable)'error': Protocol error (invalid frames)'auth_failure': Authentication failed (wrong seed)
Good practice for reconnections:
import asyncio
async def connect_with_retry(node, peer_address, peer_node_id):
"""Connect with exponential backoff retry."""
delay = 1.0 # Start with 1 second
max_delay = 60.0
while True:
try:
session = await node.connect(peer_address, peer_node_id, timeout=10.0)
return session # Success
except (ConnectionTimeoutError, ConnectionRefusedError):
print(f"Connection failed, retrying in {delay:.1f}s...")
await asyncio.sleep(delay)
delay = min(delay * 2, max_delay) # Double delay, cap at 60sWhy exponential backoff?
- Avoids overwhelming network/server (if peer down)
- Reduces resource usage during extended outages
- Standard practice in distributed systems
STT 0.2.0a0 includes seed-based resumption:
from seigr_toolset_transmissions.session import CryptoSessionContinuity
# Create continuity manager
continuity = CryptoSessionContinuity(stc_wrapper, resumption_timeout=86400)
# Create resumable session
session_id, resume_token = continuity.create_resumable_session(
peer_node_id=b"peer",
node_seed=node.seed,
shared_seed=shared_secret
)
# Initial session on WiFi/UDP
session = await node.connect(('192.168.1.100', 8000), peer_id)
# ... network change (WiFi → LTE) ...
# Resume on different transport/IP
resumed = continuity.resume_session(
resume_token,
new_transport_type='websocket',
new_peer_addr=('10.0.0.50', 9000),
stc_wrapper=stc
)How it works:
- Session identity derived from cryptographic seeds
- Resume across IP changes (WiFi → LTE)
- Resume across transports (UDP ↔ WebSocket)
- Resume across devices (same seeds = same session)
- Zero-knowledge proofs verify identity without revealing seeds
Session identity is based on cryptographic seeds rather than network connection IDs.
OS-level socket buffers:
node = STTNode(
node_id=b"Alice",
recv_buffer_size=4194304, # 4MB receive buffer
send_buffer_size=4194304 # 4MB send buffer
)Larger buffers:
- Handle bursts better (sudden influx of packets)
- Reduce packet loss on high-latency networks
- Cost: More memory per connection
Smaller buffers:
- Lower memory usage
- Risk: Dropped packets under load
Rule of thumb: buffer_size = bandwidth * latency * 2
- Example: 100 Mbps 100ms 2 = 2.5 MB
STT uses asyncio - efficient for many concurrent sessions:
# Server handling 1000+ concurrent sessions
node = STTNode(node_id=b"Server", port=8080)
node.start()
# Sessions handled concurrently (event loop)
# No thread-per-session overheadScalability:
- 100 sessions: No problem (modern hardware)
- 1,000 sessions: Achievable (server-grade)
- 10,000+ sessions: Requires tuning (OS limits, large buffers)
Current 0.2.0a0 (unreleased) tested up to ~500 concurrent sessions.
Larger frames = higher throughput, lower overhead:
stream = session.open_stream(
max_frame_size=65536 # 64 KB frames (vs default 16 KB)
)Trade-off:
- Larger frames: Better throughput, higher latency per frame
- Smaller frames: Lower latency, more overhead
Recommendation:
- File transfer: 64 KB frames
- Real-time chat: 4 KB frames
- Video streaming: 16-32 KB frames (balance)
Server streaming to multiple clients:
# Server accepts multiple connections
server = STTNode(node_seed=server_seed, shared_seed=shared_seed, port=8080)
await server.start(server_mode=True) # Enable server mode
# Server automatically accepts incoming connections
# Clients can now connect
# Broadcast to all active sessions
await server.send_to_all(b"Broadcast message", stream_id=1)
# Or send to specific sessions
session_ids = [session.session_id for session in server.get_active_sessions()]
await server.send_to_sessions(session_ids[:2], b"Targeted message")Use case: Video streaming server to many viewers
Connecting to peers:
# Connect to peer using IP address and port
session = await node.connect_udp(
peer_host="192.168.1.100",
peer_port=8080
)
# Or connect to multiple peers
peers = [
("192.168.1.100", 8080),
("192.168.1.101", 8080),
("192.168.1.102", 8080),
]
sessions = []
for host, port in peers:
session = await node.connect_udp(host, port)
sessions.append(session)
print(f"Connected to {len(sessions)} peers")Note: Applications built on STT can implement their own peer discovery mechanisms (DHT, mDNS, centralized registry, etc.) and use connect_udp() to establish connections.
Current approach: Manual peer configuration with pre-shared seeds and known IP:port addresses.
# Client sends request, waits for response
stream = session.open_stream()
await stream.send(b"GET /resource")
response = await stream.receive()
await stream.close()# Both peers send/receive concurrently
async def send_loop(stream):
while True:
data = get_data_to_send()
await stream.send(data)
await asyncio.sleep(0.1)
async def receive_loop(stream):
while True:
data = await stream.receive()
process_data(data)
# Run concurrently
await asyncio.gather(
send_loop(stream),
receive_loop(stream)
)# Use separate streams for different purposes
stream_video = session.open_stream(stream_id=1, purpose='video')
stream_audio = session.open_stream(stream_id=2, purpose='audio')
stream_chat = session.open_stream(stream_id=3, purpose='chat')
# Send on each independently
await stream_video.send(video_frame)
await stream_audio.send(audio_sample)
await stream_chat.send(chat_message) Session Lifecycle in STT
+--------------------------------------------------+
| 1. INIT |
| STTNode created, listening on port |
+--------------------------------------------------+
↓
+--------------------------------------------------+
| 2. HANDSHAKING |
| 4-message handshake (HELLO, CHALLENGE, |
| AUTH_PROOF, FINAL) |
| Duration: ~20-50ms |
+--------------------------------------------------+
↓
+--------------------------------------------------+
| 3. ESTABLISHED |
| Streams can be opened/closed |
| Data flows bidirectionally |
| Keep-alives maintain session |
| Duration: Seconds to hours |
+--------------------------------------------------+
↓
+--------------------------------------------------+
| 4. CLOSING |
| Graceful shutdown (SESSION_CLOSE frame) |
| Resources freed |
+--------------------------------------------------+
↓
+--------------------------------------------------+
| 5. CLOSED |
| Session terminated |
| Can create new session if needed |
+--------------------------------------------------+
-
What's the difference between a connection and a session?
- Connection is network-level (UDP/WebSocket); session is logical encrypted channel
-
Can one STT node handle multiple sessions simultaneously?
- Yes - SessionManager tracks multiple concurrent sessions
-
How does STT detect dead peers?
- Keep-alive frames every 10 seconds; timeout after 30 seconds silence
-
Is XOR used for session encryption?
- No - XOR only for session ID mixing; STC handles encryption
-
Can a session survive connection loss?
- Not implemented: connection migration not implemented
-
What happens if handshake fails due to wrong seed?
- AuthenticationError raised; session not established
Problem: Connection refused
Solution: Check peer is listening, verify IP/port correct, check firewall
Problem: Handshake timeout
Solution: Check seeds match, verify network reachability, check NAT/firewall
Problem: Session randomly disconnecting
Solution: Check keep-alive settings, verify stable network, check peer logs
Problem: High latency
Solution: Use UDP instead of WebSocket, check network conditions, tune buffer sizes
Problem: Authentication failed
Solution: Verify both peers using identical shared_seed
- Chapter 7: Streams and Multiplexing (using established sessions efficiently)
- Chapter 8: Transport Layer (deep dive into UDP vs WebSocket)
- Chapter 11: Error Handling (comprehensive error recovery patterns)
Key Takeaways:
- Connection (network) ≠ Session (logical encrypted channel)
- Handshake establishes authenticated session (~20-50ms)
- Keep-alives detect dead connections (30-second timeout)
- Multiple concurrent sessions supported (current and future)
- STT provides automatic retransmission, reordering, corruption detection
- Applications built on STT can implement peer discovery and content routing