{"id":48501542,"url":"https://github.com/marcelocantos/pigeon","last_synced_at":"2026-05-29T07:18:41.841Z","repository":{"id":346112148,"uuid":"1188490610","full_name":"marcelocantos/pigeon","owner":"marcelocantos","description":"WebTransport relay library (Go + Swift + Kotlin + C + TypeScript) with E2E encryption. Backends behind NATs/firewalls; relay sees only ciphertext.","archived":false,"fork":false,"pushed_at":"2026-04-19T13:50:50.000Z","size":15428,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-04-19T14:35:38.868Z","etag":null,"topics":["aes-gcm","e2e-encryption","encryption","go","pairing","relay","swift","websocket","x25519"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/marcelocantos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-22T06:30:32.000Z","updated_at":"2026-04-19T13:50:54.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/marcelocantos/pigeon","commit_stats":null,"previous_names":["marcelocantos/tern","marcelocantos/pigeon"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/marcelocantos/pigeon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcelocantos%2Fpigeon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcelocantos%2Fpigeon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcelocantos%2Fpigeon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcelocantos%2Fpigeon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marcelocantos","download_url":"https://codeload.github.com/marcelocantos/pigeon/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marcelocantos%2Fpigeon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32288653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T06:26:00.361Z","status":"ssl_error","status_checked_at":"2026-04-26T06:25:58.791Z","response_time":129,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aes-gcm","e2e-encryption","encryption","go","pairing","relay","swift","websocket","x25519"],"created_at":"2026-04-07T15:00:39.116Z","updated_at":"2026-05-29T07:18:41.835Z","avatar_url":"https://github.com/marcelocantos.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pigeon\n\nPigeon is a WebTransport relay library and server (Go + Swift + Kotlin + C + TypeScript) that enables\nconnections between devices where the backend sits on a private network\nwith no ingress. The relay forwards opaque ciphertext over QUIC — it never\nsees plaintext traffic. Applications import pigeon's packages rather than\nimplementing relay, pairing, or crypto logic themselves.\n\n## Trust Model\n\nAll application traffic is end-to-end encrypted:\n\n- **Key exchange:** X25519 ECDH — each side generates an ephemeral key pair\n  and derives a shared secret.\n- **Symmetric encryption:** AES-256-GCM with monotonic counter nonces and\n  directional key derivation via HKDF-SHA256.\n- **MitM detection:** A 6-digit confirmation code derived from both public\n  keys is displayed on each device. Users verify the codes match during\n  the pairing ceremony.\n\nThe relay server handles only ciphertext and has no access to session keys.\n\n## How It Works\n\n1. A **backend** connects to `GET /register` via WebTransport. The relay\n   assigns a unique instance ID and sends it back as the first message.\n2. One or more **clients** connect to `GET /ws/\u003cinstance-id\u003e`. The relay\n   bridges traffic bidirectionally — both reliable streams and unreliable\n   datagrams — and maintains an independent bridge per client.\n3. Pairing and encryption happen above the relay layer, in the\n   application, using pigeon's crypto and protocol packages.\n\n## Go Library\n\n```bash\ngo get github.com/marcelocantos/pigeon\n```\n\n```go\nimport (\n    \"github.com/marcelocantos/pigeon\"\n    \"github.com/marcelocantos/pigeon/crypto\"\n    \"github.com/marcelocantos/pigeon/protocol\"\n    \"github.com/marcelocantos/pigeon/qr\"\n)\n```\n\n| Package    | Purpose                                                     |\n|------------|-------------------------------------------------------------|\n| root       | Client-side relay connectivity (register, connect, send/recv)|\n| `crypto/`  | X25519 key exchange, AES-256-GCM channel, confirmation code |\n| `protocol/`| Declarative state machine framework and pairing ceremony     |\n| `qr/`      | Terminal QR code rendering and LAN IP detection              |\n\n**Quick integration — relay + encrypted channel:**\n\n```go\n// Backend registers with the relay and waits for a paired client.\nlistener, instanceID, _ := pigeon.Register(ctx, \u0026pigeon.RegisterArgs{\n    Identity:  identity,                         // crypto.Identity (long-term keypair)\n    Pairing:   resolvePairingRecord,             // func(clientID string) (*crypto.PairingRecord, error)\n    Relay:     \"https://carrier-pigeon.fly.dev\",\n    Token:     os.Getenv(\"PIGEON_TOKEN\"),        // optional; wires through BearerTokenAuth\n    Datagrams: map[string]uint64{\"video\": 1},\n})\ndefer listener.Close()\nfmt.Println(\"Instance ID:\", instanceID) // share via QR code\nsession, _ := listener.Accept(ctx)     // one paired client per call\ndefer session.Close()\n\n// Client connects by instance ID (obtained from QR scan).\nsession, _ := pigeon.Connect(ctx, \u0026pigeon.ConnectArgs{\n    InstanceID: instanceID,\n    Record:     pairingRecord,                   // *crypto.PairingRecord from pairing ceremony\n    Identity:   identity,\n    Relay:      \"https://carrier-pigeon.fly.dev\",\n    Datagrams:  map[string]uint64{\"video\": 1},\n})\ndefer session.Close()\n\n// Reliable stream — send/receive encrypted messages.\nprimary := session.Primary()\nprimary.Send([]byte(\"hello\"))\ndata, _ := primary.Recv(ctx)\n\n// Unreliable datagrams (latency-sensitive data, e.g. video frames).\nvideo := session.Datagram(\"video\")\nvideo.Send(frame)\nframe, _ = video.Recv(ctx)\n```\n\n**Encrypted channel:**\n\n```go\n// Both sides generate an ephemeral key pair and exchange public keys.\nkp, _ := crypto.GenerateKeyPair()\n// ... send kp.Public.Bytes() to peer; receive peerPubBytes ...\npeerPub, _ := ecdh.X25519().NewPublicKey(peerPubBytes)\n\n// Derive directional session keys and open an encrypted channel.\nsendKey, _ := crypto.DeriveSessionKey(kp.Private, peerPub, []byte(\"client-to-server\"))\nrecvKey, _ := crypto.DeriveSessionKey(kp.Private, peerPub, []byte(\"server-to-client\"))\nch, _ := crypto.NewChannel(sendKey, recvKey)\n\n// Verify the pairing is MitM-free (show 6-digit codes on both devices).\ncode, _ := crypto.DeriveConfirmationCode(kp.Public, peerPub)\nfmt.Println(\"Confirmation code:\", code) // e.g. \"042857\"\n\n// Encrypt / decrypt messages sent through the relay.\nencrypted := ch.Encrypt([]byte(\"hello\"))\nplaintext, _ := ch.Decrypt(encrypted)\n```\n\n## Swift Package\n\nAdd the GitHub repo as an SPM dependency:\n\n```\nhttps://github.com/marcelocantos/pigeon\n```\n\nThe package provides the `Pigeon` library (iOS 16+, macOS 13+)\ncontaining `E2ECrypto.swift` (key exchange and encrypted channel),\n`PigeonRelay.swift` (relay connectivity), and the generated\n`PairingCeremonyMachine.swift`.\n\n```swift\n// Both sides exchange public key bytes through the relay.\nlet kp = E2EKeyPair()\n// ... send kp.publicKeyData; receive peerPubBytes ...\nlet sessionKey = try kp.deriveSessionKey(peerPublicKey: peerPubBytes,\n                                         info: Data(\"client-to-server\".utf8))\nlet channel = E2EChannel(sharedKey: sessionKey, isServer: false)\nlet encrypted = try channel.encrypt(plaintext)\nlet plaintext  = try channel.decrypt(ciphertext)\n```\n\n## Android/Kotlin Library\n\nAdd via [JitPack](https://jitpack.io) (Gradle):\n\n```kotlin\n// settings.gradle.kts\ndependencyResolutionManagement {\n    repositories {\n        maven(\"https://jitpack.io\")\n    }\n}\n\n// build.gradle.kts\ndependencies {\n    implementation(\"com.github.marcelocantos.pigeon:pigeon:v0.5.0\")\n}\n```\n\nRequires JDK 17+ / Android API 33+ (for X25519).\n\n```kotlin\n// Key exchange\nval kp = E2EKeyPair()\n// ... send kp.publicKeyData (32 bytes); receive peerPubBytes ...\nval sessionKey = kp.deriveSessionKey(peerPubBytes, \"client-to-server\".toByteArray())\n\n// Encrypted channel from shared key\nval channel = E2EChannel(sharedKey, isServer = false)\nval encrypted = channel.encrypt(plaintext)\nval plaintext = channel.decrypt(ciphertext)\n```\n\n## C Client Library\n\nPure C client library distributed as two files: `dist/pigeon.h` + `dist/pigeon.c`.\nZero heap allocations — all state lives in a `pigeon_ctx` struct sized at compile time.\n\n```c\n#include \"pigeon.h\"\n// Compile: clang -DPIGEON_CRYPTO_LIBSODIUM $(pkg-config --cflags --libs libsodium) pigeon.c your_app.c\n\npigeon_ctx ctx;\npigeon_init(\u0026ctx, \u0026transport);  // transport = your QUIC callbacks\n\npigeon_keypair kp;\npigeon_generate_keypair(\u0026kp);\n\npigeon_channel ch;\npigeon_channel_init_symmetric(\u0026ch, session_key, /*is_server=*/false);\npigeon_channel_encrypt(\u0026ch, plaintext, pt_len, out, out_len);\n```\n\nIncludes generated pairing state machine, crypto (X25519 + AES-256-GCM + HKDF),\nand wire framing. `PIGEON_MAX_MSG` is the sole build-time knob.\n\n## Pairing Ceremony\n\nThe full ceremony involves three actors — **server** (backend daemon),\n**mobile** (iOS client), and **CLI** (initiator):\n\n1. CLI sends `pair_begin` to server; server generates a one-time token,\n   connects to the relay (`/register`), and receives an instance ID.\n2. Server displays a QR code encoding the relay URL, token, and instance ID.\n3. Mobile scans the QR, connects to `/ws/{id}`, generates an X25519 key pair,\n   and sends `{token, pubkey}` to the server through the relay.\n4. Server verifies the token, performs ECDH, derives the session key, and sends\n   `pair_hello_ack {pubkey}` back. Mobile performs ECDH and derives the same key.\n5. Both sides independently compute the 6-digit confirmation code from the two\n   public keys. The server signals CLI to show the code; mobile shows it on screen.\n   The user verifies the codes match — a mismatch means a MitM is present.\n6. CLI submits the code the user entered. If correct, the server sends\n   `pair_complete {secret, key}` to mobile and `pair_status` to CLI. Pairing done.\n\n![Pairing ceremony state machines](docs/PairingCeremony.svg)\n\n## Persistent Pairing\n\nAfter the first pairing ceremony, save a `PairingRecord` for reconnection\nwithout re-scanning the QR code:\n\n```go\n// After first pairing — save this securely\nrecord := crypto.NewPairingRecord(backend.InstanceID(), relayURL, myKeyPair, peerPubKey)\ndata, _ := record.Marshal()\nos.WriteFile(\"pairing.json\", data, 0600)\n\n// On reconnect — load and derive channel\ndata, _ = os.ReadFile(\"pairing.json\")\nrecord, _ = crypto.UnmarshalPairingRecord(data)\nch, _ := record.DeriveChannel([]byte(\"client-to-server\"), []byte(\"server-to-client\"))\nconn, _ := pigeon.Connect(ctx, record.RelayURL, record.PeerInstanceID)\nconn.SetChannel(ch)\n```\n\nThe shared secret is never stored — it is re-derived on each reconnect from\nthe private key and peer public key via ECDH + HKDF. `PairingRecord` is\navailable on all platforms: Go (`crypto.PairingRecord`), Swift\n(`PairingRecord`), Kotlin (`PairingRecord`), and TypeScript\n(`PairingRecord` / `createPairingRecord` / `deriveChannelFromRecord`).\n\nFor the consumer-app integration path — pairing once, persisting via a\n`CredentialStore`, reconnecting through `ConnectWithArtifact`-equivalents,\nand re-pairing on expiry — see the **[Pairing Artifact Lifecycle\nguide](docs/pairing-lifecycle.md)**. It walks through both delivery flows\n(QR scan and developer-deploy via `xcrun`) with side-by-side Go, Swift,\nand Kotlin samples.\n\n## Channels\n\nNamed streaming channels and datagram channels provide independent,\nmultiplexed communication paths over a single connection.\n\n```go\n// Streaming channels — independent ordered streams\nch, _ := conn.OpenChannel(\"game-state\")\nch.Send(ctx, data)\n\npeerCh, _ := conn.AcceptChannel(ctx)\ndata, _ := peerCh.Recv(ctx)\n\n// Datagram channels — named, unreliable, both sides create by name\nvideo := conn.DatagramChannel(\"camera-front\")\nvideo.Send(frame)\nframe, _ := video.Recv(ctx)\n```\n\nEach streaming channel gets its own QUIC stream (no head-of-line\nblocking between channels). Datagram channels share the QUIC datagram\npipe with a 2-byte channel ID prefix for demuxing.\n\n## LAN Upgrade\n\nWhen both peers are on the same LAN, traffic transparently switches\nfrom the relay to a direct QUIC connection:\n\n```go\n// Backend: start a LAN server and register with the relay.\nlan, _ := pigeon.NewLANServer(\"\", nil)  // random port, self-signed cert\ndefer lan.Close()\n\nbackend, _ := pigeon.Register(ctx, relayURL, pigeon.Config{\n    LANServer: lan,\n})\nbackend.SetChannel(ch)  // triggers LAN address advertisement\n\n// Client: enable LAN upgrade.\nclient, _ := pigeon.Connect(ctx, relayURL, instanceID, pigeon.Config{\n    LAN: true,\n})\nclient.SetChannel(ch)\n// LAN upgrade happens automatically in the background.\n```\n\nThe LANServer is a standalone QUIC listener that can serve multiple\nclients. When a client receives the LAN offer (via the encrypted relay\nchannel), it dials the backend directly, verifies via a\nchallenge/response, and atomically swaps the Conn's transport. All\nsubsequent Send/Recv/SendDatagram/RecvDatagram go via LAN.\n\n## Fault Injection Testing\n\nThe `faultproxy` package provides a transparent UDP proxy for testing\nunder adverse network conditions:\n\n```go\nproxy, _ := faultproxy.New(relayAddr,\n    faultproxy.WithLatency(50*time.Millisecond, 20*time.Millisecond),\n    faultproxy.WithPacketLoss(0.05),\n    faultproxy.WithCorrupt(0.01),\n)\ndefer proxy.Close()\n// Connect to proxy.Addr() instead of the real relay.\n```\n\nSupports latency, jitter, packet loss, corruption, bandwidth\nthrottling, blackhole periods, sequence-aware drop (`WithDropAfter`,\n`WithDropWindow`), and programmable per-packet hooks (`WithPacketHook`).\n\n## Running the Relay Server\n\n```bash\ngo build -o pigeon ./cmd/pigeon\nPORT=443 ./pigeon                           # self-signed cert (development)\n./pigeon --cert cert.pem --key key.pem      # production TLS certificate\n```\n\nThe server is also deployable via Fly.io (`fly.toml` and `Dockerfile`\nare included).\n\n**Endpoints (HTTP/3 over WebTransport):**\n\n| Route              | Description                               |\n|--------------------|-------------------------------------------|\n| `GET /health`      | Health check (returns `{\"status\":\"ok\"}`)  |\n| `GET /register`    | Backend registers (WebTransport session)  |\n| `GET /ws/{id}`     | Client connects by instance ID            |\n\n## Configuration\n\n| Flag / Env var | Default | Description |\n|----------------|---------|-------------|\n| `--port` / `PORT` | `443` | Listening port (UDP + TCP) |\n| `--domain` | — | Domain for automatic Let's Encrypt TLS (e.g. `carrier-pigeon.fly.dev`) |\n| `--acme-email` | — | Email for Let's Encrypt account |\n| `--cert` | — | TLS certificate file (PEM); if `--domain` is not set |\n| `--key` | — | TLS private key file (PEM); used with `--cert` |\n| `PIGEON_TOKEN` | — | Bearer token required for `/register`; open if unset. Wires through the default `BearerTokenAuth` verifier; replace with any custom `pigeon.Auth` for more complex admission policies. |\n| `--version` | — | Print version and exit |\n| `--help-agent` | — | Print usage + agent guide |\n\nBuild-time version injection: `go build -ldflags \"-X main.version=v1.0.0\" ./cmd/pigeon`\n\nMax message frame size: 1 MiB (constant `maxMessageSize`).\n\n## Running Tests\n\n```bash\n# Go — relay, crypto, protocol, and E2E integration tests\ngo test ./...\n\n# Swift — crypto and state machine tests\nswift test\n```\n\n## Protocol Code Generation\n\nProtocols are defined in YAML (`protocol/pairing.yaml`) and used to\ngenerate Go, Swift, Kotlin, C, TypeScript, TLA+, and PlantUML outputs:\n\n```bash\ngo run ./cmd/protogen protocol/pairing.yaml\n```\n\n## Formal Model\n\nA TLA+ specification (`formal/PairingCeremony.tla`) models the pairing\nceremony with an active adversary. Verified security properties include:\n\n- No token reuse\n- MitM detection via confirmation code mismatch\n- Device secret secrecy\n- Authentication requires completed pairing\n- No nonce reuse\n\nRun the model checker:\n\n```bash\n./formal/tlc PairingCeremony\n```\n\n## Licence\n\nApache 2.0 — see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcelocantos%2Fpigeon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarcelocantos%2Fpigeon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarcelocantos%2Fpigeon/lists"}