SDK Guide
Table of Contents
- Overview
- Quick Start (C++)
- Quick Start (C API)
- Identity Management
- Authentication
- Joining the Network
- WireGuard Tunnel
- Tree Operations
- Mesh P2P
- Best Practices
- Error Handling
- Platform Notes
Overview
The LemonadeNexusSDK is a C++ library with a C FFI layer for integrating the mesh VPN into any application. It handles identity, authentication, network joining, WireGuard tunnel management, and peer-to-peer mesh networking.
Quick Start (C++)
#include <LemonadeNexusSDK/LemonadeNexusClient.hpp>
// Connect to a server
lnsdk::ServerConfig config;
config.servers.push_back({"server.example.com", 9100, true}); // host, port, tls
lnsdk::LemonadeNexusClient client(config);
// Derive identity from username/password (deterministic: same creds = same keypair)
client.derive_identity("user@example.com", "password123");
// Join the mesh (auth + create node + allocate IP + WG config)
auto result = client.join_network("user@example.com", "");
if (result.ok) {
std::cout << "Node ID: " << result.value.node_id << "\n";
std::cout << "Tunnel IP: " << result.value.tunnel_ip << "\n";
// WireGuard tunnel is automatically configured
}
// Enable mesh P2P for direct client-to-client connections
lnsdk::MeshConfig mesh_cfg;
mesh_cfg.heartbeat_interval_sec = 5;
client.enable_mesh(mesh_cfg);
Quick Start (C API)
#include "lemonade_nexus.h"
// Create client (TLS)
ln_client_t* client = ln_create_tls("server.example.com", 9100);
// Generate or derive identity
ln_identity_t* id = ln_identity_from_seed(client, "user", "pass");
// Authenticate
char* auth_json = NULL;
ln_auth_ed25519(client, &auth_json);
ln_free(auth_json);
// Join network
char* join_json = NULL;
ln_join_network(client, &join_json);
// join_json contains: tunnel_ip, node_id, wg_endpoint, etc.
ln_free(join_json);
// Bring up WireGuard tunnel
char* tunnel_json = NULL;
ln_tunnel_up(client, NULL, &tunnel_json);
ln_free(tunnel_json);
// Clean up
ln_destroy(client);
Identity Management
// Option 1: Derive from credentials (deterministic)
client.derive_identity("username", "password");
// PBKDF2(SHA256, 100k iterations) → 32-byte seed → Ed25519 keypair
// Option 2: Random keypair
auto identity = client.generate_identity();
// Option 3: Load from disk
client.load_identity("/path/to/identity.json");
// Save to disk
client.save_identity("/path/to/identity.json");
// Get public key (base64)
auto pubkey = client.public_key_string();
// Returns "ed25519:base64..." format
Key derivation chain:
username + password
→ PBKDF2(SHA256, 100k rounds)
→ 32-byte seed
→ Ed25519 keypair (identity)
→ X25519 keypair (WireGuard, derived from Ed25519)
Authentication
// Ed25519 challenge-response (primary)
auto auth = client.authenticate_ed25519();
// Server sends nonce → client signs → server verifies
// Passkey registration (macOS Secure Enclave)
auto reg = client.register_passkey(credential_id, pub_x, pub_y);
// Passkey authentication
auto auth = client.authenticate_passkey(credential_id, auth_data, client_data, signature);
// Password auth
auto auth = client.authenticate("username", "password");
Joining the Network
auto result = client.join_network("username", "password");
The join flow is composite — one call does everything:
- Authenticate (Ed25519 challenge-response)
- Create endpoint node in the permission tree
- Allocate tunnel IP from IPAM (10.64.0.10+)
- Get server’s WireGuard public key and endpoint
- Configure WireGuard tunnel (bring up if possible)
Response fields:
| Field | Example | Description |
|——-|———|————-|
| node_id | 09ba6947... | Your unique node ID |
| tunnel_ip | 10.64.0.10/32 | Your WireGuard tunnel IP |
| server_tunnel_ip | 10.64.0.1 | Server’s tunnel IP |
| server_private_fqdn | private.<id>.<region>.seip.<domain> | Server’s private HTTPS hostname |
| wg_server_pubkey | base64... | Server’s WireGuard X25519 key |
| wg_endpoint | 67.x.x.x:51940 | Server’s WireGuard endpoint |
After joining, private API calls (tree, IPAM, mesh) route through HTTPS over the WireGuard tunnel via the server_private_fqdn.
WireGuard Tunnel
// Tunnel is usually brought up automatically during join_network()
// Manual control:
WireGuardConfig config;
config.private_key = "base64...";
config.public_key = "base64...";
config.tunnel_ip = "10.64.0.10/32";
config.server_public_key = "base64...";
config.server_endpoint = "67.x.x.x:51940";
config.keepalive = 5; // 5-second persistent keepalive
auto result = client.bring_up_tunnel(config);
auto result = client.bring_down_tunnel();
// Get config as JSON (for external tunnel management on iOS/Android)
auto json = client.wireguard_config_json();
Tree Operations
All tree operations go through the private API (HTTPS over WG tunnel):
// Read a node
auto node = client.get_tree_node("node-id");
// List children
auto children = client.get_children("parent-id");
// Create a child node
auto result = client.create_child_node("parent-id", "endpoint");
// Update a node
auto result = client.update_node("node-id", hostname);
// Delete a node
auto result = client.delete_node("node-id");
Mesh P2P
Enable peer-to-peer connections between clients:
MeshConfig cfg;
cfg.peer_refresh_interval_sec = 30; // Poll server for peer updates
cfg.heartbeat_interval_sec = 5; // Report endpoint to server
cfg.prefer_direct = true; // Prefer direct P2P over relay
client.enable_mesh(cfg);
// Set callback for mesh state changes
client.set_mesh_callback([](const MeshTunnelStatus& status) {
std::cout << "Peers: " << status.peer_count
<< " Online: " << status.online_count << "\n";
});
// Get current mesh status
auto status = client.mesh_status();
// status.is_up, status.peer_count, status.online_count, status.peers[]
client.disable_mesh();
Best Practices
- Always derive identity before auth — PBKDF2 is deterministic, same credentials = same keypair across devices
- Store identity in Keychain — never write keys to plaintext files
- Use TLS for the initial connection —
ln_create_tls()notln_create() - Let the SDK handle key conversion — Ed25519 → X25519 is automatic
- Don’t construct deltas directly — use server endpoints (
create_child_node,update_node, etc.) - Enable mesh for P2P connectivity between clients
- Handle join failure gracefully — retry with exponential backoff
- Session tokens expire — re-authenticate if you get HTTP 401
- Clean up on exit — call
bring_down_tunnel()anddisable_mesh() - Use the private FQDN — after join, all sensitive API calls route through HTTPS over the WG tunnel
Error Handling
All C++ methods return Result<T>:
auto result = client.join_network("user", "pass");
if (!result.ok) {
std::cerr << "Error: " << result.error << "\n";
std::cerr << "HTTP status: " << result.http_status << "\n";
return;
}
auto& join = result.value;
C API error codes:
| Code | Constant | Meaning |
|——|———-|———|
| 0 | LN_OK | Success |
| 1 | LN_ERR_NULL_ARG | Null pointer argument |
| 2 | LN_ERR_CONNECT | Connection failed |
| 3 | LN_ERR_AUTH | Authentication failed |
| 4 | LN_ERR_PARSE | JSON parse error |
| 5 | LN_ERR_INTERNAL | Internal error |
Platform Notes
| Platform | WireGuard Backend | Notes |
|---|---|---|
| Linux | Kernel module (preferred), BoringTun fallback | Best performance with kernel WG |
| macOS | BoringTun via privilege-escalated helper | AppleScript password dialog for utun creation |
| Windows | wireguard-nt kernel driver (auto-downloaded) | Requires admin privileges |
| iOS | Config-only | App uses NEPacketTunnelProvider, SDK generates WG config |
| Android | Config-only | App uses VpnService, SDK generates WG config |
On mobile platforms, the SDK generates the WireGuard configuration string and the host app manages the VPN lifecycle through OS APIs.