Skip to content

docs: webrtc-direct-v2#715

Open
dozyio wants to merge 6 commits into
libp2p:masterfrom
dozyio:docs-webrtc-direct-v2
Open

docs: webrtc-direct-v2#715
dozyio wants to merge 6 commits into
libp2p:masterfrom
dozyio:docs-webrtc-direct-v2

Conversation

@dozyio

@dozyio dozyio commented Apr 23, 2026

Copy link
Copy Markdown
  • Adds webrtc-direct-v2 spec

@dozyio

dozyio commented Apr 25, 2026

Copy link
Copy Markdown
Author

Draft JS implementation libp2p/js-libp2p#3480

murat-dogan pushed a commit to murat-dogan/node-datachannel that referenced this pull request Apr 26, 2026
Passes through `rtc::IceUdpMuxRequest::localUfrag` from libdatachannel to
the JS `onUnhandledStunRequest` callback. Needed by the libp2p
WebRTC-Direct v2 transport design (libp2p/specs#715).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread webrtc/webrtc-direct.md Outdated
Comment on lines +189 to +190
6. _B_ sets this inferred offer as remote description, generates an answer, and
sets it as local description.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

B's answer ice-ufrag/ice-pwd aren't specified here, but must be libp2p+webrtc+v2/<client_pwd> (the password A set for B in the synthetic answer) — otherwise the connection fails.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3587339. Step 6 now spells this out: before generating its answer, B MUST set its own ice-ufrag and ice-pwd to server_ufrag (the libp2p+webrtc+v2/<client_pwd> value A embedded). A signs its checks with that value and expects B's STUN responses under it, so ICE can't complete otherwise, exactly as you flagged. Thanks!

Comment thread webrtc/webrtc-direct.md Outdated
Comment on lines +178 to +180
4. _A_ starts sending STUN connectivity checks. _B_ parses STUN `USERNAME` as
`server_ufrag:client_ufrag` and validates that `server_ufrag` has the prefix
`libp2p+webrtc+v2/`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

v1 has no equivalent prefix check, so a v1 server's handling of a v2/ ufrag is undefined. Each version should validate its prefix and drop on mismatch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 3587339. Version selection is now explicit: B reads the version from the server_ufrag prefix, where libp2p+webrtc+v1/ selects v1 and libp2p+webrtc+v2/ selects v2. The libp2p+webrtc+ namespace is reserved for versions, so B MUST reject a server_ufrag whose version it doesn't recognize (or that carries no recognized prefix) rather than assume v1. A v2/ ufrag reaching a server that doesn't speak v2 is then dropped instead of undefined.

@tabcat

tabcat commented Jun 1, 2026

Copy link
Copy Markdown
Member

@MarcoPolo

@johannamoran

This comment was marked as off-topic.

The initial v2 draft described the happy path but left enough
unspecified that two implementations could diverge. Pin the details
down so a browser and a server interoperate from the spec alone:

- server credentials: B MUST set its own ICE ufrag and password to
  server_ufrag, or the browser rejects B's STUN responses and ICE
  never completes (the step that was missing).
- USERNAME parsing: split on the first ':', validate both halves as
  ice-char within the length bounds, and reject the rest, which keeps
  CR/LF out of the SDP B builds.
- client password: recover it by stripping the prefix from
  server_ufrag, read the browser's own value back from
  localDescription, abort if it is missing, and keep server_ufrag
  within the ICE length limit.
- version dispatch: recognize the v1 and v2 prefixes explicitly and
  reject an unknown version instead of assuming v1.
- ICE and DTLS roles: B is ICE Lite (controlled); the answer is
  a=setup:passive, so A is the DTLS client and B the DTLS server.
- c= line: it carries an address family and IP, not a port
  (c=IN <IP4|IP6> <ip>); fixed in v1 too.
- RFC references: ice-char grammar and lengths (RFC 8839 5.4),
  USERNAME format (RFC 8445 7.2.2), randomness (RFC 8445 5.3).
- Chrome context: link the WebRTC-NoSdpMangleUfrag trial, the WebRTC
  tracker issue, the discuss-webrtc notice, and the getStats side
  channel that motivated v2.
- guidance: new connections SHOULD use v2; v1 will stop working in
  browsers, but servers SHOULD keep accepting it during migration.
- versioning note: new versions ride the ufrag prefix; a new
  multiaddr is only for a change the prefix cannot carry.
lidel added a commit to libp2p/go-libp2p that referenced this pull request Jun 19, 2026
Chrome is removing the SDP munging that v1 browser-to-server dials
rely on (the WebRTC-NoSdpMangleUfrag field trial), so the listener
now also speaks the v2 flow, which carries the client's ICE password
in the server ufrag instead of munging. Implements libp2p/specs#715.

- listener: parse both halves of the STUN USERNAME, dispatch on the
  ufrag prefix (v1, v2, or reject an unknown version), recover the v2
  client password, and set pion's ICE credentials to match.
- udpmux: surface the server and client ufrag separately and key the
  mux on the server (local) ufrag, which is what pion looks up.
- validation: reject STUN USERNAME fragments outside the RFC 8839
  ice-char set or length bounds before they reach SDP.
- dialer: add opt-in WithDialerVersion(2) that emits the v2 wire
  format for go-to-go and testing; v1 stays the default.
- tests: parsing/validation units plus end-to-end v1 and v2 dials
  against a single listener.
lidel added a commit to dozyio/js-libp2p that referenced this pull request Jun 20, 2026
Refine the initial v2 implementation to match the now-precise spec
(libp2p/specs#715) so it can't diverge from go-libp2p or other peers.

- listener: dispatch on the ufrag prefix explicitly (v1, v2, or
  reject an unknown version) instead of treating anything non-v2 as
  v1.
- validation: reject STUN USERNAME fragments outside the RFC 8839
  ice-char set or length bounds before they reach SDP, and validate
  the recovered v2 client password.
- dialer: fail fast when the local ICE password can't be read back,
  instead of dialing with an unprefixed ufrag the server rejects.
- cleanup: drop the dead createOffer guard and the dead ice-pwd
  zero-padding (both unreachable; the padding could also desync the
  inferred offer from go-libp2p).
- tests: cover the credential validation and the unprefixed-ufrag
  rejection.

@lidel lidel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for getting this rolling, @dozyio! The v2 design here mostly lgtm.

An air-tight transport spec is hard to write without real code and a second implementation to test against, so I wrote a GO server and used it to find the spots where the v2 text still left room for two implementations to diverge. I pushed 3587339, which fills those gaps so an implementer can build an interoperable v2 server and browser dialer from the spec alone.

Implementations:

Spec gaps filled as much as possible, so LLM-driven implementations avoid footguns:

  • Server's own ICE credentials: B must set its ice-ufrag/ice-pwd to server_ufrag (the libp2p+webrtc+v2/<client_pwd> value A embedded), or A rejects B's STUN responses and ICE never completes. This is the one that bit first.
  • USERNAME parsing and validation: split on the first :, validate both halves as ICE ufrags (ice-char, length bounds), and reject the rest, which keeps CR/LF out of the SDP B builds.
  • Client password: recover it by stripping the prefix from server_ufrag, read the browser's own value back from localDescription, abort if it's missing, and keep server_ufrag within the ICE length limit.
  • Version dispatch: recognize the v1 and v2 prefixes explicitly and reject an unknown version instead of assuming v1.
  • ICE and DTLS roles: B is ICE Lite (controlled); the answer is a=setup:passive, so A is the DTLS client and B the DTLS server.
  • c= line: it carries an address family and IP, not a port (c=IN <IP4|IP6> <ip>); fixed in v1 too.
  • RFC references: ice-char grammar and lengths (RFC 8839 section 5.4), USERNAME format (RFC 8445 section 7.2.2), randomness (RFC 8445 section 5.3).
  • Chrome context: linked the WebRTC-NoSdpMangleUfrag trial, the WebRTC tracker issue, the discuss-webrtc notice, and the getStats side channel that motivated v2.
  • Guidance: new connections SHOULD use v2; v1 will stop working in browsers, but servers SHOULD keep accepting it during migration.
  • Versioning note: new versions ride the ufrag prefix; a new multiaddr is only needed for a change the prefix can't carry.

I also tested it end to end: the JS client in a real Google Chrome (Chrome-for-Testing, with the WebRTC-NoSdpMangleUfrag field trial on, so munging is disabled) dialing the GO server over v2. v2 connects and completes the Noise handshake, and v1 fails under the trial exactly as expected.

So.. I'm confident the spec at 3587339 is good to go, but second set of eyes + landing both GO and JS first would be good before merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

4 participants