Securing Your Server

Read time 3 minutes
On this page

Nalix encrypts a connection through a handshake, not through configuration you write by hand — turn it on with one call and the framework negotiates a shared key per connection. This shows how to turn on encryption and run TCP, UDP, and WebSocket listeners side by side.

Full source: samples/SecureMultiTransportHelloWorld

What you’ll build

A server that requires a secure handshake before it processes any request, listening on TCP, UDP, and WebSocket at once.

Turn on secure connections

// samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Server/Program.cs
await using NetworkApplication app = NetworkApplication.CreateBuilder()
    .UseLogger(logger)
    .UseSecureConnections()
    .UseSystemControl()
    .MapHandlers(typeof(HelloHandlers))
    .MapTcp<DefaultProtocol>().OnPort(TcpPort).Bind()
    .MapUdp<DefaultProtocol>().OnPort(UdpPort).Bind()
    .MapWebSocket<DefaultProtocol>().OnPort(WebSocketPort).WithPath("/ws").Bind()
    .Build();

await app.RunAsync(cts.Token);

UseSecureConnections() sets up the handshake handlers and generates a certificate automatically the first time you run the server, if one doesn’t already exist. Every request is rejected until the client completes the handshake.

UseSecureConnections() also enables UseSystemControl() for you (calling both is harmless): the client’s first handshake step — the TOFU PUBLIC_KEY_REQUEST answered with SessionTofu — is served by the system control handlers. Before this was automatic, forgetting UseSystemControl() made unpinned clients time out waiting for SessionTofu.

Full source: samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Server/Program.cs

The client handshakes over TCP first

// samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Client/Program.cs
SessionState sharedState = new();

using TcpSession tcpSession = new(tcpOptions, sharedState);
await tcpSession.ConnectAsync(Host, TcpPort);

await tcpSession.HandshakeAsync();
Console.WriteLine($"SessionToken : {sharedState.SessionToken}");
Console.WriteLine($"Encryption   : {sharedState.EncryptionEnabled}");

HelloRequestPacket tcpRequest = new();
HelloResponsePacket tcpResponse = await tcpSession.RequestAsync<HelloResponsePacket>(
    tcpRequest,
    RequestOptions.Default.WithTimeout(5_000));

HandshakeAsync() negotiates a shared secret and issues a session token. SessionState holds that token and secret so you can reuse it on a second transport.

Full source: samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Client/Program.cs

UDP reuses the TCP session’s identity

using UdpSession udpSession = new(udpOptions, sharedState);
await udpSession.ConnectAsync(Host, UdpPort);

HelloRequestPacket udpRequest = new();
HelloResponsePacket udpResponse = await udpSession.RequestAsync<HelloResponsePacket>(
    udpRequest,
    RequestOptions.Default.WithTimeout(5_000));

You must complete the TCP handshake before sending anything over UDP. UDP packets are authenticated using the session token and a message authentication code derived from the shared secret negotiated during the TCP handshake — without that handshake, the server has no way to verify a UDP packet came from your session.

Full source: samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Client/Program.cs

WebSocket is independent

using WebSocketSession wsSession = new(wsOptions, wsTransportOptions);
await wsSession.ConnectAsync(Host, WebSocketPort);

HelloRequestPacket wsRequest = new();
HelloResponsePacket wsResponse = await wsSession.RequestAsync<HelloResponsePacket>(
    wsRequest,
    RequestOptions.Default.WithTimeout(5_000));

WebSocket opens its own connection and does not share the TCP session’s token or secret.

Full source: samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Client/Program.cs

Restricting who can call a handler

Declare permission, timeout, and rate-limit rules directly on a handler method:

[PacketOpcode(0x2001)]
[PacketPermission(PermissionLevel.USER)]
[PacketTimeout(5000)]
[PacketRateLimit(requestsPerSecond: 10)]
public ValueTask<AccountResponse> GetProfile(IPacketContext<ProfileRequest> context)
{
    // Only runs if permission, timeout, and rate limit checks pass
}

These are enforced by built-in middleware before your handler body runs — see Handlers and Middleware.

Run it

dotnet build samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.sln

Terminal 1 — start the server:

dotnet run --project samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Server

Terminal 2 — run the client:

dotnet run --project samples/SecureMultiTransportHelloWorld/SecureMultiTransportHelloWorld.Client

What you should see

Connected over TCP.
TCP handshake completed. Secure state is ready.
  SessionToken : 822530074371686401
  Encryption   : True
TCP replied: Hello from Nalix!

Sending UDP hello...
UDP replied: Hello from Nalix!
TCP connection is still alive: True

Connecting WebSocket...
WebSocket replied: Hello from Nalix!

Next steps