room

package module
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 36 Imported by: 0

README

go-room

Pure-Go rooms: copies of an app find each other with no account, then talk directly over WebRTC data channels.

import room "github.com/StephenSHorton/go-room" // package room
  • Signaling (presence, offers and answers) is sealed with a key derived from a secret the members share, and goes over one or more carriers. A room is one Signaler that publishes each envelope on every carrier and keeps the first copy that arrives, so one carrier being down changes nothing. The carriers:
    • the LAN: sealed UDP datagrams to each local subnet's broadcast address, then unicast. Two devices on one network find each other with nothing else running and no internet.
    • a hub: a small TLS signaling server that one of your own machines runs (a home server). Devices prove their key to it, and it routes sealed bytes by topic. It comes with a TURN server and router port mapping, so devices away from home reach it.
    • public MQTT brokers (EMQX and HiveMQ, in package go-room/mqtt), as Toru uses. An app that doesn't import that package never links an MQTT client.
  • Peers are pion PeerConnections, one per pair of members, with STUN for hole punching and an optional TURN relay. Each connection is bound to both sides' ed25519 keys by a signed-fingerprint handshake. The app gets one reliable, ordered message channel per peer and as many byte streams as it opens. The peer layer doesn't know which carriers its signaler has.
  • No cgo, no helper process. It builds for Windows, macOS and Linux.

It was extracted from Toru's rooms (internal/stream/room.go and ice.go). The code alphabet, scrypt parameters, sealing and envelope JSON are Toru's, so Toru can move onto it without a wire change. Irori's device mesh is the second user.

Pieces

NewCode, NormalizeCode Room codes like ABCD-EFGH-JKMN-PQRS: 16 symbols from a 31-letter alphabet with no 0/O or 1/I/L, about 79 bits. Typed in any case, with or without dashes.
SecretFromCode(app, code) scrypt(code, salt = app, N=2^15, r=8, p=1) gives a 32-byte sealing key and a topic app/<hex>. Use it for anything a person types.
SecretFromKey(app, ikm, info) HKDF-SHA256 version for high-entropy keys, for example a per-person topic derived from a secret every device holds, plus an epoch. Never feed it a typed code.
Open(secret, self, opts, carriers...) The live room over every carrier given. It returns at once; carriers connect and reconnect in the background. Send(to, type, body), with to set to "" for everyone. OnMsg gets every envelope that isn't one of Mesh's own rtc.* types. OnConnect fires on each carrier (re)connect, so you can announce yourself. OnHeard(from, addr) reports where an authentic envelope came from on the LAN. Connected() counts carriers that are up.
Carrier, Leg What a carrier implements. Join(topic, deliver, up) gives a Leg with Publish(to, payload), Seen(member, addr), Up() and Close(). A carrier moves sealed bytes and never holds a key: sealing, filtering, de-duplication and the age check are Open's.
NewMemHub() An in-process fake hub for tests: Carrier(), or Open(...) as shorthand for room.Open(..., h.Carrier()).
OpenLAN(LANConfig) The LAN carrier: one UDP socket on a fixed port (Irori: 38419) for every room. See LAN. NewMemLAN() is an in-process broadcast domain for tests.
NewHub(HubConfig), Serve(ln) The hub server; see Home server. Local() carries the server's own rooms in process. OpenGuest, CloseGuest and Drop manage pairing windows and revokes.
DialHub(HubDialConfig) A hub client. It dials every address it has at once, keeps the first that completes the handshake, and reconnects by itself (1 s doubling to 60 s). Carrier(), Up(), Addr(), Server(), OpenGuest, CloseGuest, TURN(ctx).
NewTURN(TURNConfig) A TURN server on pion/turn, with credentials minted by Issue(user) and IssueGuest(until).
SplitTCP(ln) One TCP port for the hub and TURN over TCP, split by each connection's first byte.
portmap.Start(Config) Router port mapping over UPnP-IGD or NAT-PMP, renewed until Close removes it.
mqtt.OpenSignal(secret, self, opts, mqtt.Options{}) A room over the public brokers, one carrier per broker set. mqtt.Carrier puts them beside other carriers.
NewRelay(cfg, peer, onChange) The ICE list: DefaultSTUN, plus TURN while short-lived credentials are good. They come from CredentialURL (the TURN REST scheme) or from Fetch (for example HubClient.TURN). It refreshes at 80% of the ttl and keeps working credentials through fetch failures until they expire. Servers() includes DefaultSTUN; TURN() is the list without it, for an app with STUN of its own. Status() is none, pending, ok or error.
NewMesh(sig, cfg) Connections to other members. Connect(peer) offers if this side is the offerer (default: the lower id), and otherwise sends rtc.want so the other side offers. It is idempotent, with a backoff from 1 s up to 60 s after failed attempts. OnPeer fires once a peer is authenticated and accepted on both sides, OnPeerDown when one goes away. After a drop neither side asked for (the connection failed), Mesh reconnects by itself, about six tries over a minute; after a Close, a bye or a refusal it doesn't. Call Connect again whenever you see the peer in the room. When a newer connection to a member replaces an up one, OnPeerDown gets ErrReplaced for the old *Peer just before OnPeer for the new one, so key per-peer state on the *Peer. Errors to tell apart: ErrClosed, ErrTooBig, ErrRefused, ErrReplaced, *ByeError, *LocalCloseError.
Peer Messages() is a *Channel with Send and Recv of whole messages up to MaxMessage (default 4 MiB). OpenStream(ctx, label) and OnStream give byte streams, each io.ReadWriteCloser on its own data channel. A stream is raw bytes with no message boundaries, so frame any request you send on it yourself. OpenStream waits while MaxStreams (default 4) of your streams are open; the receiving side takes up to twice that, because it frees a slot only when its handler closes the stream. Close unblocks a Write stuck on a reader that stopped reading, and WriteContext takes a context. Pub(), Fingerprints(), Path() (direct or relay), RTT(), Close(reason).
LoopbackOnly() A Config.Tune that keeps pion on 127.0.0.1, for tests. Pair it with ICE: func() []ICEServer { return nil }.

Messages and stream writes go out in SCTP messages of at most 16 KiB, because pion's default cap is 64 KiB. Writers block while more than 1 MiB is queued on a channel and resume once it drains to 256 KiB. SCTP keeps channels independent, so a big stream never holds up the message channel. Data channels are detached (SettingEngine.DetachDataChannels), and mDNS is off.

LAN

OpenLAN binds the wildcard address on its port, because broadcasts reach only a wildcard bind on Windows.

  • Everyone: an envelope for everyone goes to the directed broadcast address of each up interface with a private IPv4 address (192.168.1.255 for 192.168.1.20/24). It never goes to 255.255.255.255, which Windows sends out of one interface only.
  • Public networks: on Windows, interfaces whose network is on the Public firewall profile get nothing, so a laptop on café Wi-Fi sends no datagrams there. If the Network List Manager can't be read, nothing is skipped.
  • One member: an envelope for one member is unicast to the address its last authentic envelope came from. Until one is known it is broadcast.
  • IPv4 only.

Each datagram is "GR1" || tag(8) || nonce(24) || secretbox, at most 8 KiB. The tag is the first 8 bytes of SHA-256("go-room/lan/1" || topic || u64(unix seconds / 300)). A receiver finds the room by it without trying every key, and since it changes every 5 minutes, a listener can't follow one device from network to network by a fixed value. Receivers accept the previous, current and next window's tag and drop anything else unopened. Only the first copy of an envelope that arrives with an address teaches the leg (and OnHeard) where its sender is, so a host that resends a captured datagram from its own address can't redirect that member's unicast. They open at most MaxRate datagrams a second (default 200), and drop their own broadcasts coming back.

There is no mDNS. Port 5353 belongs to the OS responders, a service record names the service in clear text, and the sealed datagram does discovery and signaling at once.

Home server

One of your own always-on machines can be the server your devices meet at. It runs a hub for signaling, a TURN server for networks that block direct paths, and port mapping so devices away from home reach both. Data still flows device to device; the hub only introduces them.

One port number N serves everything. SplitTCP sends TCP connections that start with 0x16 (TLS) to the hub and 0x00/0x01 (STUN) to TURN over TCP, and closes anything else. UDP N carries STUN and TURN.

The hub wire. TLS 1.3 over TCP, ALPN go-room-hub/1, then frames of u32 length || u8 type || body, at most 64 KiB: hello, welcome, sub, unsub, pub, msg, guest.open, guest.close, turn.get, turn.creds, ping, pong and err. The certificate is self-signed, has no names, is made fresh at every start, and is never pinned. Instead, both ends sign the TLS exporter (EXPORTER-go-room-hub, 32 bytes) with their device keys:

client = ed25519(device key, "go-room/hub/client/1" || 0x00 || x || len(id) || id)
server = ed25519(device key, "go-room/hub/server/1" || 0x00 || x || len(id) || id)
  • The client signs first, and the hub checks it with HubConfig.Authorize(id, pub) before it says anything.
  • A bad signature, an unknown key, a guest topic that isn't open, or a malformed or late hello closes the connection without a reply.
  • The client checks the hub's welcome with HubDialConfig.Expect(id, pub).
  • x is unique to the TLS session, so a man in the middle can't relay either signature.

Who may use which topic.

  • Members may subscribe to the topics AllowTopic allows and to open guest topics. By default AllowTopic allows the topics the server's own rooms have open through Local().
  • Guests are devices pairing with only a code. A guest gets the one topic its hello names, and only while a member has it open with guest.open: at most 10 minutes, and at most 2 per member.
  • The bridge: Bridge carries open guest topics between the server's LAN and the hub, so a new device at home reaches a member away from home.
  • Closing: closing a guest topic drops its guests and revokes their TURN credentials. Drop(id) closes a revoked member and its guest topics.
  • Limits (HubLimits): 64 members, 8 guests, 4 handshakes in flight per IP (an IPv6 /64 counts as one) and 64 in all, with a backoff after a failed handshake, 8 topics per connection, 50 pub a second. A member's guest topics outlive its last connection by a short grace. The hub keeps nothing.

TURN. NewTURN runs pion/turn on the UDP socket and the TCP side of SplitTCP, with relayed sockets on the server's LAN address. Those sockets are never mapped on the router.

  • Credentials are the TURN REST scheme: "<expiry>:<handle>" and base64(HMAC-SHA1(key, username)), where a member's handle is its user masked with a keystream from the key and the expiry, so the username names no stable id. The 32-byte key never leaves the server. They last 12 hours and are handed out only over the hub (turn.get).
  • Integrity first: pion asks the auth handler before it checks MESSAGE-INTEGRITY, so a client's address is learned only from the OnAuth event of a verified request.
  • Revocation: Authorize(user) runs on every authenticated request, allocation refreshes included, so a revoked device loses its relay at its next refresh.
  • Peers: by default, members may relay to the server's own private subnets and to relayed addresses on the server. Guests may relay only to relayed addresses. Neither may relay to loopback, link-local, multicast or the internet.
  • Caps: 8 allocations per user, 32 in all, 4 across every guest; a revoked guest's allocations close. TURN over TCP takes 64 connections (8 per IP) and closes one that reads nothing for 5 minutes.
  • There is no turns:, because what TURN carries is DTLS already.

Port mapping (go-room/portmap).

  • UPnP-IGD goes through goupnp: WANIPConnection 2, then 1, then WANPPPConnection 1. Discovery keeps only an IGD at the default gateway's own address.
  • NAT-PMP is go-room's own client, sent to the default gateway from jackpal/gateway.
  • Leases default to 1 h and renew at half of what was granted. A router that grants only permanent leases gets those. Close removes the mappings; Config.Stale removes what an earlier run left (after a crash) if it still points at this machine. The external address is re-read every 3 minutes.
  • Conflicts: a conflict on the external port takes another one.
  • CGNAT: Status gives the router's external address and says whether it is CGNAT (100.64.0.0/10) or private space. There a mapping succeeds but reaches nobody.

Windows Firewall. The app's LAN socket, hub, TURN and pion's own sockets listen on real interfaces, which is the point of them. Windows asks once to allow the app on private networks, and that one program rule covers every port. If it is denied, nothing reaches the server and the LAN carrier hears nothing.

Security model

What the carriers see. A hub, a broker or anyone on the LAN sees a topic, which is a hash-derived name that identifies nobody, and sealed bytes (NaCl secretbox, XSalsa20-Poly1305, with a fresh 24-byte nonce per envelope). They also see your IP address, the times you publish and the sizes of your messages. On the LAN, a tag that changes every 5 minutes takes the topic's place. A hub also sees which members are connected, and from where. MQTT client ids are a prefix plus random bytes, never the member id. Nobody without the room secret can read, forge or alter an envelope. A carrier can drop, delay or replay envelopes: duplicates are dropped by id, and envelopes more than MaxAge (default 2 minutes) old or ahead are dropped. That also means a machine whose clock is more than 2 minutes off can't signal at all.

Codes. A room code is the secret. Anyone who has it can join that room, read its signaling and send envelopes into it. Treat it like a one-use password. About 79 bits, behind scrypt, makes guessing one from a captured topic or message impractical. It does not stop someone who saw the code.

What go-room authenticates.

  • Envelopes: membership of the room, meaning the sender holds the room key. Envelope.From is whatever a member claims. It is not proof of who sent it.
  • Peers: every connection starts with an auth message in each direction on the message channel, before anything else:
    auth { v:1, deviceId, devicePub, sig }
    sig = ed25519(identity key, "go-room/auth/1" || 0x00 || role ('O' offerer | 'A' answerer)
                  || sha256(offerer's DTLS cert) || sha256(answerer's DTLS cert)
                  || sha256(topic) || len(deviceId) || deviceId)
    
    The fingerprints are read from the connected DTLS transport, not from the SDP that crossed a carrier. Each side checks the other's signature against the devicePub it sent, and checks that deviceId is the id signaling used. Then it asks your Accept(peer, pub). The two sides exchange the verdicts ({v:1, ok}), and the peer is up only when both accepted, so the app's first message on Messages() is the third on the channel. Accept runs inside the 10 s handshake and gets no fingerprints: a pairing SAS runs after OnPeer, over Messages(), and until it matches the app should treat the peer as unconfirmed. The auth message also carries a random per-connection token (see below). A relayed or replayed signature is useless: it names this connection's DTLS certificates, whose private keys are fresh for every connection and never leave the two machines.

What a room-key holder can still do. Anyone with the room secret (a code holder during a pairing, or a device that was revoked but still has an old key while that topic stays open) can read every member's SDP, and so their candidate addresses, and see who is present. They can spoil attempts that haven't finished the handshake: a forged rtc.answer that wins the race, or a forged rtc.bye with the attempt's session id, costs that attempt and a backoff. They can't take down a connection that is up: a newer rtc.offer is answered beside it and replaces it only after the new connection passes the handshake, so it needs that member's device key and your Accept; an offer not newer on the offerer's clock than the live one's is dropped (that also catches a carrier replaying an old offer); rtc.want closes an up connection only when ICE has stopped hearing the peer; and rtc.bye closes it only when it carries the token from that side's auth message, which travelled inside DTLS.

What it doesn't authenticate.

  • Whether a key belongs to who you think. go-room proves "this connection is held by whoever holds the private key for pub". Accept is where you decide whether that pub may be peer: check it against a key you pinned or certified earlier.
  • A first contact. When two sides have no keys for each other yet, as in a pairing, the code holder in the middle problem is yours to close. Someone who knows the code can sit between the two sides and run two DTLS sessions. Compare a short authentication string over both keys and Peer.Fingerprints() on both screens. They differ under a man in the middle.
  • Signaling metadata, such as who is present. Presence is just envelopes, so any room member can claim to be anyone there. Only a peer that passes the handshake is authenticated.
  • Traffic analysis by carriers, STUN servers or a TURN relay. A TURN relay sees encrypted DTLS packets and both ends' addresses.

A hub reached from the internet. With a port mapped, a scanner finds two things:

  • a TLS 1.3 server whose certificate has no names and changes at every start, and which says nothing without a valid hello;
  • a STUN server that answers Binding with the scanner's own address and refuses Allocate (401, realm relay).

It doesn't learn whose server it is, which devices exist, or any topic. A guest topic is open for at most 10 minutes and named by a hash of a pairing code. A certified device can send UDP through TURN to hosts on the server's LAN until it is revoked; that is the price of relaying to LAN devices.

Example

secret, _ := room.SecretFromCode("myapp-room-v1", code)
lan, _ := room.OpenLAN(room.LANConfig{Port: 38419})
hub := room.DialHub(room.HubDialConfig{
    Self:     self,
    Identity: &ident,
    Addrs:    func() []string { return serverAddrs() }, // its LAN and public addresses
    Expect:   func(id string, pub ed25519.PublicKey) error { return checkServer(id, pub) },
})
sig, _ := room.Open(secret, self, room.SignalOptions{
    OnConnect: func() { announce() },                          // sig.Send("", "hello", nil)
    OnMsg:     func(e room.Envelope) { mesh.Connect(e.From) }, // on "hello"
}, lan.Carrier(), hub.Carrier())
relay := room.NewRelay(room.RelayConfig{Fetch: hub.TURN}, self, nil)
relay.Start()
mesh, _ := room.NewMesh(sig, room.Config{
    Self:     self,
    Identity: room.KeyIdentity(privateKey),
    Accept:   func(peer string, pub ed25519.PublicKey) error { return checkKnown(peer, pub) },
    ICE:      relay.TURN, // the home server's STUN and TURN, no public STUN
})
mesh.OnPeer(func(p *room.Peer) {
    go func() {
        for {
            msg, err := p.Messages().Recv(ctx)
            if err != nil {
                return // p went down; OnPeerDown says why
            }
            handle(p.ID(), msg)
        }
    }()
    p.OnStream(func(label string, s *room.Stream) { serve(label, s) })
})

The server side is NewHub and Serve on the hub half of SplitTCP, NewTURN on the TURN half and the UDP socket, the server's own rooms on hub.Local() beside the LAN, and portmap.Start for the router.

More code:

  • mqtt/example_test.go is a broker version that compiles.
  • mesh_test.go runs two peers in one process over MemHub and loopback.
  • hub_test.go and lan_test.go run the same over a real hub and LAN sockets on 127.0.0.1.

Notes

  • One Mesh per Signaler. That is why a room is one Signaler over many carriers, not one per carrier: two signalers on one topic would make two connections to the same member. To listen on two topics at once, for example while a key rotates, open two signalers and two meshes. Whether two members that both reached the new topic also stay connected on the old one is the app's choice (Allow).
  • Secret.Topic is app + "/" + hex. An app with an existing wire format can rewrite the prefix and keep the suffix. Toru's is toru/v1/.
  • Config.ICE nil means DefaultSTUN; a func that returns nil means no servers.
  • pion never reuses a data channel id within one connection, so a connection can open about 32,000 streams in its lifetime.

Checks (run locally - no CI)

gofmt -l .                              # prints nothing
go vet ./...
go test ./... -count=1 -timeout 300s
GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go build ./...
GOOS=linux go build ./...

Tests run on 127.0.0.1 only:

  • MemHub and MemLAN signaling;
  • the LAN carrier on 127.0.0.1 sockets with 127.0.0.1 targets (no broadcast, no wildcard bind, no multicast);
  • the hub and TURN on 127.0.0.1 listeners, and LoopbackOnly peers;
  • the credential endpoint on httptest, and fake UPnP and NAT-PMP gateways on 127.0.0.1;
  • the mqtt test, which dials only a closed port on 127.0.0.1.

Nothing contacts the public brokers or STUN servers or binds a LAN address, so a fresh test binary doesn't raise a Windows Firewall prompt.

Licence

MIT. See LICENSE.

Documentation

Overview

Package room lets two or more copies of an app find each other with no account, and then talk directly over WebRTC data channels.

Signaling (presence, offers and answers) is sealed with a key derived from a shared secret and goes over one or more carriers: the local network (OpenLAN), a hub on one of the app's own machines (NewHub, DialHub, with NewTURN and package portmap beside it), or public MQTT brokers (package mqtt). Every carrier sees only random bytes on a random-looking topic. Once two members have found each other, Mesh opens a pion PeerConnection between them, binds it to each side's ed25519 identity with a signed-fingerprint handshake, and hands the app one reliable ordered message channel plus on-demand byte streams.

It was extracted from Toru's rooms (internal/stream/room.go and ice.go).

Index

Constants

View Source
const (
	RelayNone    = "none"    // no relay set up: STUN only
	RelayPending = "pending" // fetching the first credentials
	RelayOK      = "ok"      // TURN is in the list
	RelayError   = "error"   // credentials couldn't be fetched (Detail says why)
)

Relay states, for a "Direct only" note and diagnostics.

View Source
const (
	MaxLANDatagram = 8 << 10 // bytes, header included
	DefaultLANRate = 200     // datagrams opened per second

)

LAN limits and defaults.

View Source
const (
	MsgWant   = "rtc.want"   // to the offerer: please offer to me
	MsgOffer  = "rtc.offer"  // offerer → answerer: session, sdp
	MsgAnswer = "rtc.answer" // answerer → offerer: session, sdp
	MsgBye    = "rtc.bye"    // either way: session, reason
)

Envelope types Mesh sends and owns.

View Source
const (
	DefaultMaxMessage     = 4 << 20
	DefaultMaxStreams     = 4
	DefaultAuthTimeout    = 10 * time.Second
	DefaultConnectTimeout = 30 * time.Second
)

Defaults for Config's zero values.

View Source
const (
	DefaultTURNTTL     = 12 * time.Hour
	DefaultTURNRealm   = "relay"
	DefaultMaxPerUser  = 8
	DefaultMaxTotal    = 32
	DefaultMaxGuests   = 4  // allocations across every guest
	DefaultMaxTCP      = 64 // TURN-over-TCP connections
	DefaultMaxTCPPerIP = 8
)

TURN defaults.

View Source
const CodeLen = 16

CodeLen is the number of symbols in a room code: 16 of 31 is about 79 bits.

View Source
const DefaultMaxAge = 2 * time.Minute

DefaultMaxAge drops anything older (or further ahead) than this: a carrier replaying a backlog after a reconnect, or a clock far off.

View Source
const HubALPN = "go-room-hub/1"

HubALPN is the hub's TLS application protocol.

View Source
const MaxGuestTTL = 10 * time.Minute

MaxGuestTTL caps how long a guest topic stays open.

View Source
const MaxHubFrame = 64 << 10

MaxHubFrame caps one frame, type byte included.

View Source
const RTCPrefix = "rtc."

RTCPrefix starts the envelope types Mesh owns ("rtc.want", "rtc.offer", "rtc.answer", "rtc.bye"). They never reach SignalOptions.OnMsg.

View Source
const SyncLabel = "sync"

SyncLabel is the label of every peer's message channel (pre-negotiated, stream id 0). OpenStream refuses it.

Variables

View Source
var DefaultSTUN = []ICEServer{
	{URLs: []string{"stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"}},
	{URLs: []string{"stun:stun.cloudflare.com:3478"}},
}

DefaultSTUN are public STUN servers (no account) for finding each machine's public address. Data never passes through them.

View Source
var ErrBadCode = errors.New("that isn't a room code (16 letters and numbers, like ABCD-EFGH-JKMN-PQRS)")

ErrBadCode is returned for a string that isn't a room code.

View Source
var ErrClosed = errors.New("room: closed")

ErrClosed is returned by anything used after Close.

View Source
var ErrNotConnected = errors.New("not connected to the room yet")

ErrNotConnected is Send's error while no carrier is up.

View Source
var ErrRefused = errors.New("room: refused by peer")

ErrRefused means the other side's Accept turned this side down.

View Source
var ErrReplaced = errors.New("room: replaced by a new connection")

ErrReplaced is why an up peer went down (OnPeerDown) when a newer connection to the same member came up in its place: OnPeer for the new one follows. Mesh doesn't reconnect after it; there is nothing to fix.

View Source
var ErrTooBig = errors.New("room: message too big")

ErrTooBig is returned for a message over the channel's cap.

View Source
var ErrTooBigDatagram = errors.New("room: envelope too big for a LAN datagram")

ErrTooBigDatagram is Publish's error for a payload that doesn't fit one LAN datagram.

Functions

func CleanICEURLs

func CleanICEURLs(in []string) []string

CleanICEURLs keeps the well-formed stun:, turn: and turns: entries.

func Decode

func Decode[T any](env Envelope) (T, error)

Decode unmarshals an envelope's body.

func LoopbackOnly

func LoopbackOnly() func(*webrtc.SettingEngine)

LoopbackOnly keeps pion on 127.0.0.1: UDP4 only, loopback candidates included, every other address filtered out (no LAN host candidates, so a fresh test binary raises no Windows Firewall prompt). Pair it with an ICE list that returns nil, so no STUN gathering socket is opened.

func NewCode

func NewCode() string

NewCode returns a fresh random room code, formatted XXXX-XXXX-XXXX-XXXX.

func NormalizeCode

func NormalizeCode(in string) (string, bool)

NormalizeCode accepts a code typed or pasted any way (lower case, spaces, missing dashes) and returns its canonical form.

func OnPublicNetwork added in v0.2.0

func OnPublicNetwork(ip netip.Addr) bool

OnPublicNetwork reports whether ip is this machine's address on a network Windows has on the Public firewall profile (always false on other systems), where the LAN carrier stays silent and Windows Firewall blocks what a server listens for.

func SplitTCP added in v0.2.0

func SplitTCP(ln net.Listener) (hub, turn net.Listener)

SplitTCP shares one TCP listener between the hub and TURN over TCP, by each connection's first byte: 0x16 (a TLS handshake record) goes to the hub, 0x00 or 0x01 (a STUN message) to TURN, anything else is closed. TURN ChannelData only ever follows on a connection TURN already has.

Each side is closed on its own; ln closes once both are.

func WebRTCServers

func WebRTCServers(list []ICEServer) []webrtc.ICEServer

WebRTCServers converts a list to the shape pion takes.

Types

type ByeError

type ByeError struct{ Reason string }

ByeError is why a peer went down when the other side closed it.

func (*ByeError) Error

func (e *ByeError) Error() string

type Carrier added in v0.2.0

type Carrier interface {
	// Join starts carrying topic. deliver gets every payload that arrives
	// for it, with the address it came from when the carrier knows one (the
	// zero AddrPort otherwise); up runs each time the leg (re)connects.
	// Neither may block for long.
	Join(topic string, deliver func(payload []byte, from netip.AddrPort), up func()) (Leg, error)
}

A Carrier moves sealed payloads for topics. It never holds a key: Open seals, opens, filters and de-duplicates, so every carrier gets the same checks. A Carrier may serve many rooms at once.

type Channel

type Channel struct {
	// contains filtered or unexported fields
}

Channel is a peer's one reliable, ordered message channel ("sync"). Each message is sent as a 4-byte big-endian length, then the bytes, cut into SCTP messages of at most 16 KiB and joined again on arrival.

func (*Channel) Recv

func (c *Channel) Recv(ctx context.Context) ([]byte, error)

Recv returns the next whole message.

func (*Channel) Send

func (c *Channel) Send(ctx context.Context, msg []byte) error

Send sends one whole message. It blocks while the channel is backed up. If ctx ends part way through a message the channel can't be resynced, so the peer is closed.

type Config

type Config struct {
	// Self is this member's id; it must match the Signaler's.
	Self string
	// Identity signs this side of each connection's auth handshake.
	Identity Identity
	// Accept is the app's decision on a peer, called once both auth
	// signatures verified: pub is the key the peer proved it holds. A nil
	// return lets the peer up. Required: during a pairing it may accept any
	// key (and record it); afterwards it should check pub against what it
	// knows about peer.
	Accept func(peer string, pub ed25519.PublicKey) error
	// Allow is asked before any PeerConnection is made for peer, for a local
	// Connect, an incoming offer or an incoming want. nil allows everyone in
	// the room.
	Allow func(peer string) bool
	// Offerer reports whether this side makes the offer to peer. Both sides
	// must agree. nil: the lower id offers.
	Offerer func(peer string) bool
	// ICE is read for each new connection (Relay.Servers). nil means
	// DefaultSTUN; a func returning nil means no servers (loopback tests).
	ICE func() []ICEServer
	// Tune adjusts the pion SettingEngine after go-room's own settings
	// (tests: LoopbackOnly()).
	Tune func(*webrtc.SettingEngine)
	// RelayOnly makes every connection go through a TURN relay from ICE
	// (ICE transport policy "relay"): tests that prove a relayed path, or
	// an app that must not reveal its addresses to peers.
	RelayOnly bool
	// MaxMessage caps one Channel message, both ways (default 4 MiB).
	MaxMessage int
	// MaxStreams caps the streams this side opens to one peer at a time
	// (default 4): OpenStream waits for a free slot. Incoming streams may run
	// to twice this, because the opener frees a slot on its own Close while
	// this side frees it only when its handler closes the stream; past that,
	// an incoming stream is closed at once (the opener reads io.EOF). Both
	// sides should use the same value.
	MaxStreams int
	// AuthTimeout bounds the handshake once the data channel is open
	// (default 10 s); ConnectTimeout bounds a whole attempt (default 30 s).
	AuthTimeout    time.Duration
	ConnectTimeout time.Duration
	// Logf, when set, gets diagnostics (never secrets).
	Logf func(format string, args ...any)
}

Config sets up a Mesh.

type Envelope

type Envelope struct {
	V    int             `json:"v"`
	ID   string          `json:"id"`
	From string          `json:"from"`
	To   string          `json:"to,omitempty"`
	Type string          `json:"type"`
	At   int64           `json:"at"` // unix ms
	Body json.RawMessage `json:"body,omitempty"`
}

Envelope is one signaling message. To == "" is for everyone. The JSON shape is Toru's, so Toru can move onto go-room without a wire change.

type Hub added in v0.2.0

type Hub struct {
	// contains filtered or unexported fields
}

Hub is a running signaling hub.

func NewHub added in v0.2.0

func NewHub(cfg HubConfig) (*Hub, error)

NewHub makes a hub; Serve starts taking connections.

func (*Hub) Close added in v0.2.0

func (h *Hub) Close()

Close stops every listener and connection and closes guest topics.

func (*Hub) CloseGuest added in v0.2.0

func (h *Hub) CloseGuest(topic string)

CloseGuest closes a guest topic and drops its guests; members still subscribed keep it only if AllowTopic allows it on its own.

func (*Hub) Drop added in v0.2.0

func (h *Hub) Drop(id string)

Drop closes every connection of member id and its guest topics (a revoke). Authorize must refuse id from now on, or it reconnects.

func (*Hub) Local added in v0.2.0

func (h *Hub) Local() Carrier

Local is the hub as a Carrier for the server's own rooms, in process: they are subscribers like any connection, without TLS. By default the topics they join are the ones members may subscribe to.

func (*Hub) OpenGuest added in v0.2.0

func (h *Hub) OpenGuest(topic string, ttl time.Duration) error

OpenGuest opens a guest topic for the server's own pairing window: guests naming it in their hello are admitted until ttl (at most MaxGuestTTL) or CloseGuest.

func (*Hub) Peers added in v0.2.0

func (h *Hub) Peers() []HubPeer

Peers lists the connected clients.

func (*Hub) Serve added in v0.2.0

func (h *Hub) Serve(ln net.Listener) error

Serve takes connections on ln (a TCP listener, or SplitTCP's hub side) until ln fails or the hub closes; after Close it returns nil.

type HubClient added in v0.2.0

type HubClient struct {
	// contains filtered or unexported fields
}

HubClient is a connection to a hub that reconnects by itself, 1 s, 2 s, 4 s ... up to 60 s apart.

func DialHub added in v0.2.0

func DialHub(cfg HubDialConfig) *HubClient

DialHub starts connecting to the hub in the background.

func (*HubClient) Addr added in v0.2.0

func (c *HubClient) Addr() string

Addr is the address that completed the handshake ("" while down).

func (*HubClient) Carrier added in v0.2.0

func (c *HubClient) Carrier() Carrier

Carrier is the hub connection as a Carrier.

func (*HubClient) Close added in v0.2.0

func (c *HubClient) Close()

Close disconnects and stops reconnecting.

func (*HubClient) CloseGuest added in v0.2.0

func (c *HubClient) CloseGuest(topic string)

CloseGuest closes a guest topic this member opened.

func (*HubClient) OpenGuest added in v0.2.0

func (c *HubClient) OpenGuest(ctx context.Context, topic string, ttl time.Duration) error

OpenGuest asks the hub to admit guests to topic (a pairing room) for ttl (at most MaxGuestTTL). It waits for the connection up to ctx, and is sent again on every reconnect until ttl runs out or CloseGuest.

func (*HubClient) Server added in v0.2.0

func (c *HubClient) Server() (id string, pub ed25519.PublicKey)

Server is the hub's device id and key from its last welcome.

func (*HubClient) TURN added in v0.2.0

func (c *HubClient) TURN(ctx context.Context) (TURNCreds, error)

TURN asks the hub for TURN credentials for this device (a guest's are narrower and last only while its topic is open). Pass it as RelayConfig.Fetch.

func (*HubClient) Up added in v0.2.0

func (c *HubClient) Up() bool

Up reports whether the hub connection is up.

type HubConfig added in v0.2.0

type HubConfig struct {
	// Self is the server device's id; Identity signs its welcome.
	Self     string
	Identity Identity
	// Authorize decides on a member that proved it holds pub's private key;
	// nil error admits it. Required.
	Authorize func(id string, pub ed25519.PublicKey) error
	// AllowTopic decides a member's subscriptions other than open guest
	// topics. nil: only topics the server's own rooms (Local) have open. It
	// runs with the hub's lock held: answer from the app's own state and
	// don't call back into the hub.
	AllowTopic func(c HubPeer, topic string) bool
	// TURN, when set, answers turn.get: members get Issue(id), guests
	// IssueGuest until their topic closes.
	TURN *TURNServer
	// PublicUDP, when set, is the public address of TURN's UDP port as the
	// router mapped it; it rides in turn.creds so a device that dialed the
	// public TCP address finds UDP when the router gave it another port.
	PublicUDP func() netip.AddrPort
	// Bridge, when set, carries open guest topics between this LAN and the
	// hub both ways, so a device at home with only a pairing code reaches a
	// member away from home.
	Bridge *LAN
	// Limits; the zero value is the defaults.
	Limits HubLimits
	// Logf, when set, gets diagnostics (never payloads or credentials).
	Logf func(format string, args ...any)
}

HubConfig sets up a hub.

type HubDialConfig added in v0.2.0

type HubDialConfig struct {
	// Self is this member's device id (unused for a guest).
	Self string
	// Identity signs the hello; nil makes this a guest.
	Identity *Identity
	// GuestTopic is a guest's one topic (its pairing room).
	GuestTopic string
	// Addrs lists host:port addresses for the hub. They are dialed in
	// parallel and the first to complete the handshake wins; it is read
	// again on every reconnect.
	Addrs func() []string
	// Expect checks the hub's welcome: the server device's id and the key
	// it proved. Required for members (Irori: a certified, unrevoked
	// device); optional for a guest, which records the key instead.
	Expect func(id string, pub ed25519.PublicKey) error
	// Dial opens the TCP connection (default net.Dialer; tests: 127.0.0.1).
	Dial func(ctx context.Context, addr string) (net.Conn, error)
	// TURNURIs gives the ICE URLs for TURN credentials from the hub reached
	// at addr. nil: stun:, turn:?transport=udp and turn:?transport=tcp at
	// addr itself, since a home server's TURN shares the hub's port number.
	TURNURIs func(addr string) []string
	// OnUp, when set, runs (on its own goroutine) each time the connection
	// comes up: Irori kicks its TURN credential loop there, so relay
	// credentials don't wait out a backoff that ran while the hub was down.
	OnUp func()
	// Logf, when set, gets diagnostics (never payloads or credentials).
	Logf func(format string, args ...any)
}

HubDialConfig sets up a connection to a hub.

type HubLimits added in v0.2.0

type HubLimits struct {
	Members         int           // member connections [64]
	Guests          int           // guest connections [8]
	HandshakesPerIP int           // handshakes in flight per remote IP (an IPv6 /64 is one) [4]
	Handshakes      int           // handshakes in flight in total [64]
	TopicsPerConn   int           // subscriptions per connection [8]
	GuestsPerMember int           // guest topics one member may have open [2]
	PubRate         float64       // pub frames per second per connection [50]
	PubBurst        float64       // [100]
	FailBackoff     time.Duration // refuse an IP this long after a failed handshake, doubling to 30 s [1 s]; negative: off
}

HubLimits bound what a hub on someone's desktop will take. Zero values are the defaults in brackets.

type HubPeer added in v0.2.0

type HubPeer struct {
	ID     string // the member's device id ("" for a guest)
	Pub    ed25519.PublicKey
	Guest  bool
	Remote netip.AddrPort
}

HubPeer is one connected client as the hub sees it.

type ICEServer

type ICEServer struct {
	URLs       []string `json:"urls"`
	Username   string   `json:"username,omitempty"`
	Credential string   `json:"credential,omitempty"`
}

ICEServer is one STUN or TURN server, in the shape RTCPeerConnection takes.

type Identity

type Identity struct {
	Pub  ed25519.PublicKey
	Sign func(msg []byte) []byte
}

Identity is this member's long-term ed25519 key, as a public key and a signing function, so the private key can stay wherever the app keeps it.

func KeyIdentity

func KeyIdentity(priv ed25519.PrivateKey) Identity

KeyIdentity is an Identity for a private key held in memory.

type LAN added in v0.2.0

type LAN struct {
	// contains filtered or unexported fields
}

LAN is one UDP socket carrying every room opened on it.

func OpenLAN added in v0.2.0

func OpenLAN(cfg LANConfig) (*LAN, error)

OpenLAN opens the LAN carrier's socket and starts reading.

func (*LAN) Carrier added in v0.2.0

func (l *LAN) Carrier() Carrier

Carrier returns the LAN as a Carrier.

func (*LAN) Close added in v0.2.0

func (l *LAN) Close()

Close stops reading and closes the socket and every leg.

func (*LAN) LocalAddr added in v0.2.0

func (l *LAN) LocalAddr() net.Addr

LocalAddr is the socket's address.

func (*LAN) Targets added in v0.2.0

func (l *LAN) Targets() []netip.AddrPort

Targets is where an envelope for everyone goes right now. Empty means the LAN carrier sends nothing: no private network, or (Windows) every one is on the Public profile.

type LANConfig added in v0.2.0

type LANConfig struct {
	// Port is the fixed UDP port every device uses (Irori: 38419).
	Port int
	// Conn, when set, is used instead of binding the wildcard address on
	// Port (tests: a MemLAN conn or a 127.0.0.1 socket). The LAN closes it.
	Conn net.PacketConn
	// Targets is where envelopes for everyone go. nil: the directed
	// broadcast address of each up private IPv4 interface on Port, skipping
	// networks on the Public firewall profile on Windows.
	Targets func() []netip.AddrPort
	// MaxRate caps datagrams opened per second (default DefaultLANRate); the
	// rest are dropped unopened.
	MaxRate int
	// Now is the clock for the tag window (tests).
	Now func() time.Time
	// Logf, when set, gets diagnostics (never payloads).
	Logf func(format string, args ...any)
}

LANConfig sets up the LAN carrier.

type Leg added in v0.2.0

type Leg interface {
	// Publish sends one sealed payload. to is the member it is for ("" =
	// everyone); a carrier may ignore it (a hub or a broker sends to every
	// subscriber), and the LAN carrier unicasts when it knows the address.
	Publish(to string, payload []byte) error
	// Seen tells the leg that a payload it delivered from addr opened as an
	// authentic envelope from member.
	Seen(member string, from netip.AddrPort)
	// Up reports whether the leg can publish right now.
	Up() bool
	// Close stops carrying the topic.
	Close()
}

A Leg is one topic on one Carrier.

type LocalCloseError

type LocalCloseError struct{ Reason string }

LocalCloseError is why a peer went down when this side closed it.

func (*LocalCloseError) Error

func (e *LocalCloseError) Error() string

type LocalSubnet added in v0.2.0

type LocalSubnet struct {
	Index  int          // interface index
	Prefix netip.Prefix // address and mask, e.g. 192.168.1.20/24
}

LocalSubnet is one up IPv4 interface address with a private address.

func LocalSubnets added in v0.2.0

func LocalSubnets() []LocalSubnet

LocalSubnets lists the up, non-loopback interfaces' private IPv4 addresses (RFC 1918). The LAN carrier broadcasts to these; the TURN server's default peer filter allows them.

type MemHub

type MemHub struct {

	// Tap, when set, sees every payload published (topic, sealed bytes):
	// tests use it to check what a carrier would see.
	Tap func(topic string, payload []byte)
	// contains filtered or unexported fields
}

MemHub is an in-process stand-in for a hub or broker, for tests: every Signaler opened on it publishes sealed bytes to the hub, and the hub hands them to every other leg subscribed to the same topic, each in order on its own goroutine. Sealing, filtering, de-duplication and the age check are the same code every carrier gets; only the transport differs.

func NewMemHub

func NewMemHub() *MemHub

NewMemHub returns an empty hub.

func (*MemHub) Carrier added in v0.2.0

func (h *MemHub) Carrier() Carrier

Carrier returns the hub as a Carrier, to open beside others.

func (*MemHub) Open

func (h *MemHub) Open(s Secret, self string, o SignalOptions) Signaler

Open joins the room for s as member self, with the hub as its only carrier: Open(s, self, o, h.Carrier()). OnConnect fires once, at once.

func (*MemHub) Publish

func (h *MemHub) Publish(topic string, payload []byte)

Publish delivers a raw payload on topic to every leg, as a broker would, for tests that replay or forge traffic.

type MemLAN added in v0.2.0

type MemLAN struct {

	// Drop, when set, is asked about every datagram (from, to); true loses
	// it, as a lossy network would.
	Drop func(from, to netip.AddrPort) bool
	// contains filtered or unexported fields
}

MemLAN is an in-process broadcast domain for tests: each Conn is a net.PacketConn with an address of your choosing, a datagram to one Conn's address reaches that Conn, and one to Broadcast() reaches every other. There are no sockets.

func NewMemLAN added in v0.2.0

func NewMemLAN() *MemLAN

NewMemLAN returns an empty broadcast domain.

func (*MemLAN) Broadcast added in v0.2.0

func (n *MemLAN) Broadcast() netip.AddrPort

Broadcast is a Targets entry that reaches every Conn on the domain.

func (*MemLAN) Conn added in v0.2.0

func (n *MemLAN) Conn(addr netip.AddrPort) net.PacketConn

Conn attaches a new packet conn at addr. It panics if addr is taken.

type Mesh

type Mesh struct {
	// contains filtered or unexported fields
}

Mesh makes and keeps authenticated connections to other members of one room. It owns the rtc.* envelopes on its Signaler; use one Mesh per Signaler. Closing the Mesh doesn't close the Signaler.

func NewMesh

func NewMesh(sig Signaler, cfg Config) (*Mesh, error)

NewMesh starts handling rtc.* envelopes on sig.

func (*Mesh) Close

func (m *Mesh) Close()

Close closes every connection (telling each peer) and stops handling the signaler's rtc.* envelopes. It waits for pion to finish closing, and for queued OnPeer/OnPeerDown callbacks to run; don't call it from one.

func (*Mesh) Connect

func (m *Mesh) Connect(peer string)

Connect makes sure there is (or soon will be) a connection to peer: it offers if this side is the offerer, and asks peer to offer (rtc.want) otherwise. It is idempotent while an attempt is running or the peer is up, and after a failed attempt it waits out a backoff (1 s doubling to 60 s) before offering again. Call it whenever peer is seen in the room.

func (*Mesh) OnPeer

func (m *Mesh) OnPeer(fn func(*Peer))

OnPeer sets the handler for a peer coming up: both auth signatures verified and Accept passed on both sides. Set it before Connect.

func (*Mesh) OnPeerDown

func (m *Mesh) OnPeerDown(fn func(peer string, err error))

OnPeerDown sets the handler for an up peer going away, with why. After a drop that neither side asked for (the connection failed), Mesh tries to reconnect on its own for about a minute; after a Close, a bye or a refusal it doesn't. Apps still call Connect when they see the peer again. When a newer connection replaces an up one, OnPeerDown for the old *Peer (ErrReplaced) comes just before OnPeer for the new one.

func (*Mesh) Peer

func (m *Mesh) Peer(id string) *Peer

Peer returns the connected peer with id, or nil.

func (*Mesh) Peers

func (m *Mesh) Peers() []*Peer

Peers returns every connected peer.

type Peer

type Peer struct {
	// contains filtered or unexported fields
}

Peer is one authenticated connection to another member.

func (*Peer) Close

func (p *Peer) Close(reason string)

Close closes the connection and every stream on it, and tells the peer why with an rtc.bye.

func (*Peer) Done

func (p *Peer) Done() <-chan struct{}

Done is closed once the peer is down.

func (*Peer) Err

func (p *Peer) Err() error

Err is why the peer went down (nil while up).

func (*Peer) Fingerprints

func (p *Peer) Fingerprints() (local, remote string)

Fingerprints are the SHA-256 fingerprints of this side's and the peer's DTLS certificates, as the connected transport saw them (not the SDP), in lower-case colon hex. Both sides see the same pair, swapped; a pairing SAS over both catches a man in the middle.

func (*Peer) ID

func (p *Peer) ID() string

ID is the peer's member id.

func (*Peer) Messages

func (p *Peer) Messages() *Channel

Messages is the peer's one reliable, ordered message channel.

func (*Peer) OnStream

func (p *Peer) OnStream(fn func(label string, s *Stream))

OnStream sets the handler for streams the peer opens. Streams that arrived before it was set are handed to it at once. Without a handler, streams wait (up to MaxStreams) until one is set.

func (*Peer) OpenStream

func (p *Peer) OpenStream(ctx context.Context, label string) (*Stream, error)

OpenStream opens a new byte stream to the peer. It waits for one of the MaxStreams slots, then for the channel to open.

func (*Peer) Path

func (p *Peer) Path() string

Path is "relay" when the selected ICE pair goes through TURN, "direct" otherwise ("" before a pair is selected).

func (*Peer) Pub

func (p *Peer) Pub() ed25519.PublicKey

Pub is the ed25519 key the peer proved it holds.

func (*Peer) RTT

func (p *Peer) RTT() time.Duration

RTT is the latest ICE round-trip time on the nominated pair (0 if none measured yet).

func (*Peer) Session

func (p *Peer) Session() string

Session is this connection's id (fresh for every attempt).

type Relay

type Relay struct {
	// contains filtered or unexported fields
}

Relay keeps the ICE list: STUN always, TURN while credentials are good.

func NewRelay

func NewRelay(cfg RelayConfig, peer string, onChange func()) *Relay

NewRelay reads the relay setup; peer is sent to the credential endpoint (the TURN REST username is usually "expiry:peer"). onChange, if set, runs after every fetch.

func (*Relay) Kick added in v0.2.0

func (r *Relay) Kick()

Kick fetches again now, with the backoff reset, unless the credentials in hand are good: call it when the source of credentials came back (a hub reconnected), so a backoff that grew while it was down isn't waited out.

func (*Relay) Servers

func (r *Relay) Servers() []ICEServer

Servers is the ICE list right now: STUN, then TURN while the credentials haven't expired. Pass it as Config.ICE.

func (*Relay) Start

func (r *Relay) Start()

Start begins fetching and refreshing credentials; a no-op with no CredentialURL or Fetch. It never blocks.

func (*Relay) Status

func (r *Relay) Status() RelayStatus

Status returns the relay's state.

func (*Relay) Stop

func (r *Relay) Stop()

Stop ends the refresh loop and waits for it.

func (*Relay) TURN added in v0.2.0

func (r *Relay) TURN() []ICEServer

TURN is the list without DefaultSTUN, for an app that brings its own (a home server): while the credentials haven't expired, their turn: and turns: URLs with the credentials, and any stun: URLs they came with as an entry of their own; nil otherwise.

type RelayConfig

type RelayConfig struct {
	// CredentialURL answers GET ?peer=<peer> with {username, credential (or
	// password), ttl, uris}. It must be https, except on loopback.
	CredentialURL string
	// TurnURLs, when set, replace the uris the credentials come with.
	TurnURLs []string
	// Fetch, when set, replaces the HTTP fetch from CredentialURL: the loop
	// calls it for fresh credentials (an app with a hub: HubClient.TURN).
	Fetch func(ctx context.Context) (TURNCreds, error)
	// Logf, when set, gets credential failures.
	Logf func(format string, args ...any)
}

RelayConfig sets up an optional TURN relay with short-lived credentials (the TURN REST scheme).

type RelayStatus

type RelayStatus struct {
	State   string `json:"state"`
	Detail  string `json:"detail,omitempty"`
	Expires int64  `json:"expires,omitempty"` // unix ms the current credentials stop working
}

RelayStatus is how the TURN relay is doing.

type Secret

type Secret struct {
	Key   [32]byte
	Topic string
}

Secret is what members of one room share: the key every envelope is sealed with, and the broker topic they meet on. Topic is not secret on its own (brokers see it), but it is derived from the same secret, so it names nobody. Callers may rewrite Topic's prefix (keeping the derived suffix) to stay on an existing wire format; Toru's is "toru/v1/".

func SecretFromCode

func SecretFromCode(app, code string) (Secret, error)

SecretFromCode derives a room's secret from a room code (any spelling NormalizeCode accepts): scrypt(code, salt = app), as Toru does, so guessing codes from a captured topic or message is expensive on top of the ~79-bit code space. The first 32 bytes are the sealing key; the topic is app + "/" + hex(the next 16).

func SecretFromKey

func SecretFromKey(app string, ikm, info []byte) Secret

SecretFromKey derives a room's secret from a high-entropy key (32 random bytes or more), such as a secret every device of one person holds: HKDF-SHA256(ikm, salt = app, info), 48 bytes, split as in SecretFromCode. It is fast, so never feed it something a person typed: use SecretFromCode.

type SignalOptions

type SignalOptions struct {
	// MaxAge drops envelopes older or further ahead than this (default
	// DefaultMaxAge).
	MaxAge time.Duration
	// OnMsg gets every envelope from another member, addressed to everyone
	// or to self, that isn't one of Mesh's own rtc.* types. It runs on the
	// signaler's receive goroutine; don't block it for long.
	OnMsg func(Envelope)
	// OnConnect fires whenever a carrier (re)connects, so callers can
	// announce themselves.
	OnConnect func()
	// OnHeard, when set, gets the sender and source address of an
	// authentic envelope that arrived with one (the LAN carrier): the first
	// copy of each envelope that came with an address, even when another
	// carrier delivered it first. How an app learns where a member is on the
	// local network. It runs on the carrier's receive goroutine.
	OnHeard func(from string, addr netip.AddrPort)
}

SignalOptions tune a Signaler. The zero value is Toru's behaviour.

type Signaler

type Signaler interface {
	// Send seals one envelope to member to ("" = everyone) and publishes it
	// on every carrier.
	Send(to, typ string, body any) error
	// Connected reports how many carriers are up right now.
	Connected() int
	// Self is this member's id.
	Self() string
	// Topic is the room's topic (from its Secret).
	Topic() string
	// Close leaves the room.
	Close()
	// contains filtered or unexported methods
}

Signaler is a live connection to one room over one or more carriers: Open, MemHub.Open (tests) or mqtt.OpenSignal (public brokers). The unexported method keeps implementations inside go-room, because Mesh relies on how they seal, filter and route.

func Open added in v0.2.0

func Open(s Secret, self string, o SignalOptions, carriers ...Carrier) (Signaler, error)

Open joins the room for s as member self over every carrier given. Each envelope is sealed once and published on all of them, and the first copy to arrive wins (the rest are dropped by id), so one carrier being down or slow changes nothing. It returns at once; carriers connect (and reconnect) in the background, and OnConnect fires on each.

type Stream

type Stream struct {
	// contains filtered or unexported fields
}

Stream is a byte stream over its own data channel, opened with Peer.OpenStream or received through Peer.OnStream. Writes go out in 16 KiB pieces under the same flow control as Channel. Close flushes what was written, then closes the channel; the other side's Read returns io.EOF. Close also unblocks a Write stuck on a reader that stopped reading (that Write returns ErrClosed, and Close then skips the flush). When the peer goes down, Read usually returns the peer's error instead, but a connection that dies at the wrong moment can still look like io.EOF: check lengths or hashes before trusting a stream as complete.

func (*Stream) Close

func (s *Stream) Close() error

Close flushes what was written (up to 30 s) and closes the stream. If it had to stop a blocked Write, it doesn't flush: the data is cut anyway.

func (*Stream) Label

func (s *Stream) Label() string

Label is the label the opener gave the stream.

func (*Stream) Read

func (s *Stream) Read(p []byte) (int, error)

Read reads bytes the other side wrote; io.EOF once it closed.

func (*Stream) Write

func (s *Stream) Write(p []byte) (int, error)

Write writes all of p, blocking while the channel is backed up. Close (or the peer going down) ends a blocked Write with ErrClosed.

func (*Stream) WriteContext

func (s *Stream) WriteContext(ctx context.Context, p []byte) (int, error)

WriteContext is Write that also gives up when ctx ends. What was written before that stays written; the stream stays usable.

type TURNConfig added in v0.2.0

type TURNConfig struct {
	// UDP is the UDP socket (port N); STUN Binding is answered there too.
	UDP net.PacketConn
	// TCP, when set, takes TURN over TCP (SplitTCP's TURN side).
	TCP net.Listener
	// RelayIP is where relayed sockets bind: the server's LAN address
	// (tests: 127.0.0.1). It must be IPv4.
	RelayIP netip.Addr
	// Key is the HMAC key credentials are made with; the app keeps it.
	Key [32]byte
	// Realm defaults to DefaultTURNRealm.
	Realm string
	// TTL is how long issued credentials last (default 12 h). A relayed
	// connection lasts only as long as the credentials it was made with.
	TTL time.Duration
	// Authorize is asked on every authenticated request with the user from
	// the username (a device id); non-nil refuses. Guest users are checked
	// by the server itself. nil admits every user with valid credentials.
	Authorize func(user string) error
	// AllowPeer decides which peer addresses a user may relay to, on every
	// permission and every packet out. nil: members to this host's private
	// subnets and to relayed addresses on this server; guests only to
	// relayed addresses. Loopback, link-local, multicast and public
	// addresses are never in the default.
	AllowPeer func(user string, ip netip.Addr) bool
	// MaxPerUser and MaxTotal cap allocations (defaults 8 and 32);
	// MaxGuests caps the allocations of every guest together (default 4),
	// so pairing guests never take the relay from members.
	MaxPerUser, MaxTotal, MaxGuests int
	// MaxTCP and MaxTCPPerIP cap TURN-over-TCP connections (defaults 64
	// and 8; an IPv6 /64 counts as one IP).
	MaxTCP, MaxTCPPerIP int
	// Logf, when set, gets diagnostics (never credentials).
	Logf func(format string, args ...any)
}

TURNConfig sets up a TURN server on sockets the app opened.

type TURNCreds added in v0.2.0

type TURNCreds struct {
	Username   string        `json:"username"`
	Credential string        `json:"credential"`
	TTL        time.Duration `json:"-"`
	URIs       []string      `json:"uris,omitempty"`
}

TURNCreds are one set of short-lived TURN credentials and the URLs they work on.

type TURNServer added in v0.2.0

type TURNServer struct {
	// contains filtered or unexported fields
}

TURNServer is a running TURN server.

func NewTURN added in v0.2.0

func NewTURN(cfg TURNConfig) (*TURNServer, error)

NewTURN starts the TURN server.

func (*TURNServer) Close added in v0.2.0

func (t *TURNServer) Close()

Close stops the server and closes its sockets.

func (*TURNServer) Issue added in v0.2.0

func (t *TURNServer) Issue(user string) TURNCreds

Issue mints credentials for user (a device id; no ':').

func (*TURNServer) IssueGuest added in v0.2.0

func (t *TURNServer) IssueGuest(until time.Time) TURNCreds

IssueGuest mints credentials for a pairing guest, valid until until (at most the TTL). A guest's allocation may send only to relayed addresses on this server, unless AllowPeer says otherwise.

Directories

Path Synopsis
Package mqtt carries go-room signaling over public MQTT brokers, as Toru does.
Package mqtt carries go-room signaling over public MQTT brokers, as Toru does.
Package portmap asks the home router to forward ports to this machine, over UPnP-IGD (through goupnp) or NAT-PMP (RFC 6886, go-room's own client), so devices away from home can reach a home server.
Package portmap asks the home router to forward ports to this machine, over UPnP-IGD (through goupnp) or NAT-PMP (RFC 6886, go-room's own client), so devices away from home can reach a home server.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL