Nostr events used by the transport
This describes the current local implementation, not a claim of verified Rust interoperability.
Kind 37195: overlay advert
Illustrative event matching the shape emitted by publishOverlayAdvert().
The ID, key, signature, and relay URL below are placeholders, not a valid signed
event or a recommended public relay.
{
"id": "<64-hex-event-hash>",
"pubkey": "<64-hex-x-only-FIPS-public-key>",
"created_at": 1804377600,
"kind": 37195,
"tags": [
["d", "fips-overlay-v1"],
["protocol", "fips-overlay-v1"],
["version", "1"],
["expiration", "1804381200"]
],
"content": "{\"identifier\":\"fips-overlay-v1\",\"version\":1,\"endpoints\":[{\"transport\":\"udp\",\"addr\":\"nat\"}],\"signalRelays\":[\"wss://relay.example.com\"],\"stunServers\":[\"stun.l.google.com:19302\",\"stun.cloudflare.com:3478\"]}",
"sig": "<128-hex-schnorr-signature>"
}
Decoded content, for readability:
{
"identifier": "fips-overlay-v1",
"version": 1,
"endpoints": [{ "transport": "udp", "addr": "nat" }],
"signalRelays": ["wss://relay.example.com"],
"stunServers": ["stun.l.google.com:19302", "stun.cloudflare.com:3478"]
}
- The FIPS identity signs the advert, not the human master or bot identity.
pubkeyis Nostr's x-only public key; the roster/invite carries the compressed FIPS key, including its prefix.- The
dtag identifies the addressable advert for that author. Consumers must verify signatures and consider the latest applicable nonexpired event. udp/natmeans traversal negotiation is needed. It is not a public IP.signalRelaysidentifies signaling locations;stunServersidentifies STUN services. Neither means that a public UDP connection has already succeeded.- The current expiry is one hour, with republication attempted every half hour. Publication requires at least one relay acceptance, with an eight-second timeout.
Publication versus invite generation
The runtime publishes on startup and periodically. Signed invite/acceptance completion additionally persists the relay, starts its incoming subscription, and requires advert acceptance on that relay before returning the code. Subscription health is not yet verified. See pairing.
Remote advert lookup remains pending: resolve() uses the local roster key.
Existing dialing does not yet depend on fetching the advert.
STUN and private candidate exchange
For each traversal attempt, both peers independently open a UDP socket and query STUN. They may use different STUN servers; they do not need accounts or a shared STUN session. The server reports each socket's externally observed endpoint.
Those endpoints are sent in encrypted kind 21059 offer/answer payloads as
reflexiveAddress, alongside localAddresses. They are not published in our
kind 37195 advert. Each peer then probes the other's candidates using the
same socket. A mapping learned against STUN can still be unsuitable for the
peer destination, so traversal is best effort.
Current diagnostics
/fips ping <id>returns a pong with RTT, orPing failed: <error>.- Ping uses separate deadlines: connection establishment (30 seconds), encrypted send (10 seconds), and waiting for pong after send completes (20 seconds). RTT starts immediately before sending, excluding connection setup. Only the expected peer can satisfy a pending pong. Runtime restart cancels outstanding ping requests. A timeout returns an error rather than an unhandled rejection.
- Ping failures name the broad stage, with nested attempt IDs and relay rejection, answer-timeout, or punching-timeout details where available. A generic outer connection deadline still groups signaling/traversal/FMP; use the logs below to distinguish them. Send may include FSP session establishment.
- A ping deadline removes its waiter and timer and prevents a late connection result from initiating the ping send. The underlying transport API does not expose per-attempt cancellation: already-running transport operations can finish later, with their rejections handled. Transport-level abort/cleanup is follow-up work.
/fips statusexposes runtime state and configured signal relays; it does not confirm relay health or peer reachability.- Peer state changes update roster status and last-seen information.
- Core logs include runtime startup failures, malformed app envelopes, unknown senders, and master-key mismatches.
- The runtime wires a sanitized logger into both the UDP transport and FIPS core. Normal info/warning logs show signaling subscription requests and relay URLs, EOSE/closure notifications, received event IDs, decryption/rejection categories, roster authorization, matched or ignored answers, STUN mapping success/failure and candidate counts, UDP punching results, and FMP peer state.
- Advert, offer, and answer publications require at least one accepting relay. Each destination has an eight-second acknowledgement deadline. Rejections and failures are logged per relay, and an all-failed publication returns its relay reasons to the initiating ping. Acceptance is not proof of peer delivery.
FIPS attempt=<12-hex-prefix>correlates an offer and answer across both backend logs. Event IDs and peer key prefixes distinguish publication and endpoints. No candidate IP/port values or decrypted payloads are intentionally logged; error reasons are bounded and address/key/blob-like material is redacted.- Enable
DEBUG=1before starting both instances for extra core FMP/FSP handshake and routing diagnostics. Normal stage logs use the standard info logger; ensure info logging is enabled andLOGis not0when collecting them. - VPS probe diagnostics report a bounded socket error
code(orUNKNOWN),source=callback|throw, one-basedcandidate=<index>/<count>, and an addressscope:loopback,private(RFC1918),link-local,shared(CGNAT),special(0/8 or multicast/reserved 224/4 and above),public(other IPv4), ornon-ipv4. These are diagnostic categories, not destination validation or proof of public routability. Socket error events separately logsource=socketand their code because no candidate can be attributed reliably. Raw error messages, destination addresses/ports and payloads are not logged. On Linux an asynchronous ICMP error can surface on a later send, so an error's candidate index associates it with that callback, not necessarily the original failing destination. - UDP punching keeps all candidates eligible every 200 ms for the existing
10-second window after recognized destination/network send errors:
ECONNREFUSED,EHOSTUNREACH,ENETUNREACH,EHOSTDOWN,ENETDOWN,EACCES,EPERM,EADDRNOTAVAIL,EMSGSIZE,ENOBUFS,EAGAIN,EWOULDBLOCK,ENOTFOUND, andEAI_AGAIN. This applies to thrown errors, send callbacks and unattributed socket error events. Unknown/unclassified errors and socket closure still abort conservatively. No candidate is filtered or permanently disabled, preserving loopback/LAN use and retries while a remote socket becomes ready. - Error warnings retain their code/source/index/scope fields and now say
action=continueoraction=abort. During an attempt, retryable warnings are deduplicated by operation/source/code/candidate and capped at 12; a fatal error is always reported. The generic socket listener defers to the punching listener to avoid duplicate warnings on every socket error. Late callbacks after attempt completion are ignored and cannot restart cleanup or change the outcome. - Punch startup logs duration, interval, retry policy and each candidate's scope.
Each completed attempt logs
UDP punch summarywithoutcome=success|timeout|aborted, elapsed time, rounds, probe-send attempts, locally queued sends, probe/socket/ACK error counts, retryable error events and suppressed warnings. Queued sends are not proof of delivery; counts are observations up to completion, excluding late callbacks. Successful punching also logs the received packet kind and scope before the FMP handshake. These changes do not add ports or firewall privileges.
Collecting a failed attempt
- Update and deliberately restart both paired instances, preserving their databases.
- Run
/fips ping <id>on one side only. - Capture its error and
[fips]logs from both terminals. Search for the same attempt prefix; if the receiver logs no event, compare subscription URLs and the initiator's relay acknowledgement first. - If the receiver logs a rejected offer, inspect that reason. If it publishes an accepted answer but the initiator sees none, inspect the initiator subscription.
- Only move to NAT/firewall investigation once answer matching and UDP punching are reached. Successful punching still requires the FMP/FSP handshakes.
EOSE/closure callbacks are aggregate pool notifications, not continuous per-relay health checks. There is no persistent per-peer diagnostic history or dedicated diagnostics UI yet. An unknown peer is logged/rejected locally without sending it a denial message. Full signaling schemas, freshness/replay limits, rate limits, and transport cancellation remain in the implementation plan.