Nodes
A Node is the central object in warpgate. It manages your identity, your servers,
your connections, and the traversal engine.
Most users want
Gate, notNode.Gateis a thin async-context wrapper that gives a node a stable nickname-derived identity, callsnode.start()for you, exposes aLinkper inbound peer, and tears everything down on__aexit__. See quickstart.md for the Gate-first flow. Reach forNodedirectly when you need custom message callbacks, manual plugin selection, multiple nodes per process, or full control over the startup phase order.
Creating a Node
from warpgate import Node
node = Node() # defaults
node = Node(port=12345) # explicit listen port
Constructor parameters
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
list |
|
Pre-loaded interfaces. Auto-discovered if empty. |
|
str or list |
|
Restrict listening to specific IP(s). |
|
int |
|
TCP/UDP listen port. |
|
socketpair |
|
Custom stop signal pair. Auto-created if None. |
|
dict |
|
Configuration dictionary. |
Starting a Node
node = await Node().start()
start() runs these phases in order:
1. load_network_interfaces — discover NICs and addresses
2. load_machine_identity — load/generate ECDSA key, derive listen port
3. load_cryptography_and_auth — set node_id from public key
4. ─── concurrent ───────────────────────────────────────────────
initialize_system_clock — sync NTP (if enabled)
load_p2p_stun_clients — load STUN clients per interface (if enabled)
setup_router_and_signal — connect to MQTT signal broker
─────────────────────────────────────────────────────────────
5. initialize_punch_coordination — log NTP skew
6. start_maintenance_tasks — background idle-pipe cleanup
7. listen_on_ifs — bind TCP/UDP servers on all interfaces
8. build_node_address — serialize identity + addresses → addr_bytes
9. start_background_port_forwarding — launch UPnP task (if enabled)
10. setup_nickname_service — initialise PNP client
11. setup_traversal_plugins — auto-load plugins from plugins/ directory
12. finalize_port_forwarding — await UPnP result
start() returns self, so you can chain: node = await Node().start().
Optional parameters to start():
node = await Node().start(
sys_clock=my_clock, # reuse an existing SysClock
out=True, # print progress to stdout
cout=my_logger, # custom print function
)
Node address
addr_bytes = node.address() # bytes
This is the serialised node address — a compact encoding of:
ECDSA public key (for authentication)
Machine ID (for stable port derivation)
All interfaces: NIC IP, external IP, NAT type, port
Share it with peers out-of-band. They pass it to auto_connect or node.connect().
# Write your address to a file:
with open("my_addr.bin", "wb") as f:
f.write(node.address())
# Another machine reads it and connects:
with open("my_addr.bin", "rb") as f:
peer_addr = f.read()
pipe, _ = await auto_connect(node, peer_addr)
Nicknames
Register a human-readable name in the PNP (Peer Name Protocol) system:
full = await node.nickname("alice")
print(full) # "alice.p2p"
Names use a TLD suffix derived from the configured PNP server set
(currently .p2p for the default single-server config — see
pnp_get_tld in nickname.py). Once registered, peers can connect
by passing the full name:
pipe, _ = await auto_connect(node, "alice.p2p")
If the name is already registered to a different keypair, the server
rejects the put on its signature check and node.nickname(...) raises.
When enable_nickname=True (the default) the node also auto-registers
its own derived name during start() — see setup_nickname_service.
Receiving messages
To handle messages arriving on any of the node’s server pipes, register a callback:
async def on_message(msg, client_tup, pipe):
print("Got:", msg, "from:", client_tup)
node.add_msg_cb(on_message)
The callback fires for every message received on every server socket. client_tup
is (ip, port), pipe is the server-side pipe for the connection.
Supported address families
node.supported() # [IP4], [IP6], or [IP4, IP6]
Returns which address families (IPv4, IPv6) are available across all interfaces.
Stopping a Node
await node.close()
This cancels background tasks, closes all server sockets, shuts down STUN clients, stops the MQTT router, and releases the process pool used by hole punching.
As a context manager:
async with Node(conf=NODE_TEST_CONF) as node:
await node.start()
pipe, _ = await auto_connect(node, peer_addr)
...
# node.close() called automatically
Configuration
NODE_CONF (production defaults)
NODE_CONF = {
"reuse_addr": False, # don't reuse ports between restarts
"enable_upnp": True, # try UPnP port forwarding on startup
"sig_pipe_no": 1, # number of MQTT signal connections per peer
"install_path": None, # data directory (auto-detected if None)
"init_clock_skew": True, # sync NTP for punch timing
"enable_punching": True, # load STUN clients and punch plugin
"enable_nickname": True, # auto-register nickname on start
"enable_stun_clients": True, # load STUN clients
}
NODE_TEST_CONF (fast tests)
NODE_TEST_CONF = {
"reuse_addr": False,
"enable_upnp": False, # skip UPnP
"sig_pipe_no": 0, # no MQTT (no signal needed for same-machine tests)
"install_path": None,
"init_clock_skew": False, # skip NTP
"enable_punching": True,
"enable_nickname": False, # skip nickname registration
"enable_stun_clients": False, # skip STUN
}
Use NODE_TEST_CONF for unit/integration tests and for same-machine demos where
real NAT traversal isn’t needed.
Customising
from aionetiface import dict_child
from warpgate.node.node_defs import NODE_TEST_CONF
MY_CONF = dict_child({"enable_upnp": True}, NODE_TEST_CONF)
node = await Node(conf=MY_CONF).start()
dict_child(overrides, parent) creates a new dict that inherits from parent
and applies overrides on top.
Node identity and port
The listen port is derived deterministically from the machine ID:
node.listen_port = field_wrap(dhash(machine_id), [10000, 60000])
This means the same machine always picks the same port, which helps UPnP mappings persist between restarts. You can override it:
node = Node(port=12345)
The machine ID itself is derived from hardware identifiers (MAC address, etc.) so it is stable across reboots.