Quickstart

This guide shows two peers exchanging a message. It’s the smallest useful warpgate program and a good base for your own code.

Two peers, one message

import asyncio
from warpgate import Gate, peer
from aionetiface import SUB_ALL


async def echo(link):
    async for msg in link:
        await link.send(b"echo:" + msg)


async def alice():
    async with Gate("alice") as gate:
        await gate.listen(echo)            # blocks; calls echo(link) per peer


async def bob():
    async with Gate("bob") as gate:
        pipe, _ = await gate.connect(peer.find("alice"))
        pipe.subscribe(SUB_ALL)
        await pipe.send(b"hi")
        print(await pipe.recv(SUB_ALL))    # b"echo:hi"


asyncio.run(asyncio.gather(alice(), bob()))

Both peers register on the public nickname server, race every available traversal strategy until one connects, and exchange a message.

What’s happening

Gate("alice")

Gate is the high-level wrapper around Node. Constructing it with a name does three things:

  1. Loads (or generates + persists) an ECDSA keypair under ~/aionetiface/alice.json. Same name on the same machine → same key. Different name (or no name) → fresh identity.

  2. Starts a Node underneath: discovers NICs, opens listen sockets, classifies the local NAT, syncs NTP, connects to MQTT brokers.

  3. Registers alice.p2p on the namebump nickname server so peers can resolve the name to the node’s full address (with all per-interface paths and broker hints).

If you don’t pass a name, Gate() derives a deterministic one from the host’s NIC MAC addresses — useful for unattended nodes.

gate.listen(handler)

Blocks accepting inbound peers. For each peer that connects, handler(link) is scheduled as a background task. Each link is a HandlerPipe exposing async for msg in link (read) and await link.send(msg) (write).

async def echo(link):
    async for msg in link:
        await link.send(b"echo:" + msg)

await gate.listen(echo)

peer.find("alice")

Returns a PeerHandle — an opaque token saying “the peer registered as alice.<active_tld>”. When the input has no TLD, peer.find auto-appends the active one (.p2p for the default single-server PNP config). No network call yet; the actual nickname resolution happens inside gate.connect.

gate.connect(target)

Resolves the handle (one namebump GET), then runs auto_connect:

  1. Builds every valid (plugin, address-family, route-type) combo between the two peers.

  2. Launches every combo concurrently.

  3. Cancels the losers as soon as one wins.

Returns (pipe, plugin) — the live Pipe and the plugin instance that won, or (None, None) on failure. Use pipe.send, pipe.subscribe, and pipe.recv from there (see connections.md).

Plugins tried, in phase order: direct_connect, reverse_connect, tcp_punch, udp_punch, random_probe, then turn as a last-resort relay.

Bytes-only addressing (no nickname server)

If you don’t want to depend on the public nickname server, share raw address bytes out-of-band:

import asyncio
from warpgate import Gate
from aionetiface import SUB_ALL


async def echo(link):
    async for msg in link:
        await link.send(b"echo:" + msg)


async def alice():
    async with Gate() as gate:
        addr = gate.node.address()         # bytes
        share_with_bob(addr)               # however you like
        await gate.listen(echo)


async def bob(alice_addr_bytes):
    async with Gate() as gate:
        pipe, _ = await gate.connect(alice_addr_bytes)
        pipe.subscribe(SUB_ALL)
        await pipe.send(b"hi")
        print(await pipe.recv(SUB_ALL))

gate.connect accepts either a PeerHandle (resolved via namebump), a <name>.p2p string (likewise), or raw bytes (the addr serialised by node.address()).

Skipping the Gate wrapper

For full control over message callbacks, manual plugin selection, explicit interface lists, etc., drop down to the Node API. See nodes.md and connections.md.

import asyncio
from warpgate import Node
from warpgate.node.auto_connect import auto_connect
from aionetiface import SUB_ALL


async def main():
    alice = await Node().start()
    bob   = await Node().start()
    pipe,    _ = await auto_connect(alice, bob.address())
    bob_pipe, _ = await auto_connect(bob, alice.address())
    bob_pipe.subscribe(SUB_ALL)
    await pipe.send(b"hi")
    print(await bob_pipe.recv(SUB_ALL))     # b"hi"
    await pipe.close(); await bob_pipe.close()
    await alice.close(); await bob.close()


asyncio.run(main())

Test-mode startup

Real Gate / Node startup contacts STUN servers, MQTT brokers, and the nickname server. In tests you usually want none of that:

from warpgate import Node
from warpgate.node.node_defs import NODE_TEST_CONF

node = await Node(conf=NODE_TEST_CONF).start()

NODE_TEST_CONF disables UPnP, NTP, STUN, and PNP, so the node comes up in milliseconds and only talks loopback.