fix(descriptor): rebuild SDP that Firefox accepts; release v5.8.1
Live cross-browser testing found three defects in the SBQ2 SDP template, all
invisible to Chrome and all fatal to Firefox.
The candidate lines omitted raddr/rport on srflx and relay candidates. RFC 8839
section 5.1 makes rel-addr and rel-port mandatory for non-host candidates even
though ICE never reads them; Chrome tolerates the omission and Firefox drops the
candidate. Relay-only connections to Firefox failed 0/8 against 8/8 for the
browser's own SDP. The STUN and TURN profiles hid it because a host pair
connected instead -- the relay candidates were never actually needed there.
The template also advertised ice-options:trickle without ever closing the
candidate set, though a descriptor is a complete one-shot set with no channel to
trickle over, and hard-coded the m= port and c= line to the 9 / 0.0.0.0 null
default candidate, which is the trickle convention for "nothing gathered yet".
Both are now correct: no trickle, an explicit a=end-of-candidates, and the most
publicly reachable candidate as the default.
All three are serializer-side and cost zero descriptor bytes; sizes are
unchanged at 98-149 bytes, QR version 6-8. Verified 48/48 across all 16
combinations of {Chrome, Firefox} squared and four network profiles, with every
relay-only pair now connecting over the relay.
This commit is contained in:
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## v5.8.1 — SBQ2 rebuilds SDP that Firefox accepts
|
||||
|
||||
Still nothing in the application calls the SBQ2 descriptor; this fixes defects in
|
||||
it found by running live connections between real browsers.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The rebuilt SDP omitted `raddr`/`rport` on srflx and relay candidates. RFC 8839
|
||||
§5.1 makes them mandatory for non-host candidates, and while Chrome tolerates
|
||||
the omission, **Firefox drops the candidate entirely**. Relay-only connections
|
||||
to Firefox failed 0/8 where the browser's own SDP succeeded 8/8. The STUN and
|
||||
TURN profiles hid it, because a host candidate pair connected instead.
|
||||
|
||||
- The template advertised `a=ice-options:trickle` and never closed the candidate
|
||||
set. A descriptor is a complete one-shot set with no channel to trickle over,
|
||||
so this promised candidates that could never arrive. Trickle is gone and
|
||||
`a=end-of-candidates` is emitted.
|
||||
|
||||
- The `m=` port and `c=` line were hard-coded to the `9` / `0.0.0.0` null default
|
||||
candidate, which is the trickle-ICE "nothing gathered yet" convention and false
|
||||
here. The most publicly reachable candidate is advertised instead — relay, then
|
||||
srflx, then host — falling back to the null form only when every candidate is
|
||||
mDNS, as Chrome does.
|
||||
|
||||
None of these change the descriptor: all three are serializer-side and cost zero
|
||||
bytes. Sizes are unchanged at 98–149 bytes, QR version 6–8.
|
||||
|
||||
Verified across all 16 combinations of {Chrome, Firefox} x {Chrome, Firefox} and
|
||||
four network profiles: **48/48 connections**, with every relay-only pair now
|
||||
connecting over the relay.
|
||||
|
||||
|
||||
## v5.8.0 — A connection descriptor that fits in a small QR code
|
||||
|
||||
No change to how messages are protected, and no change to how a connection is
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](#install-as-an-app)
|
||||
[](#security-model)
|
||||
[](#forward-secrecy)
|
||||
|
||||
Vendored
+1
-1
@@ -19984,7 +19984,7 @@ var SecureMasterKeyManager = class {
|
||||
var import_NotificationIntegration = __toESM(require_NotificationIntegration());
|
||||
|
||||
// package.json
|
||||
var version = "5.8.0";
|
||||
var version = "5.8.1";
|
||||
|
||||
// src/components/ui/Header.jsx
|
||||
var APP_VERSION = `v${version}`;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -7,7 +7,7 @@ this document describes.
|
||||
|
||||
| | |
|
||||
| --- | --- |
|
||||
| Release | v5.8.0 |
|
||||
| Release | v5.8.1 |
|
||||
| Protocol version | 4.1 |
|
||||
| Ratchet wire version | 1 |
|
||||
|
||||
@@ -214,5 +214,5 @@ worse than one that reports nothing.
|
||||
|
||||
## Scope
|
||||
|
||||
This describes the browser implementation as it stands in v5.8.0. It is not a
|
||||
This describes the browser implementation as it stands in v5.8.1. It is not a
|
||||
substitute for independent cryptographic review.
|
||||
|
||||
+29
-3
@@ -83,9 +83,35 @@ Foundation and priority are **not** transmitted. Priority only orders
|
||||
connectivity checks, and each peer computes its own local priorities anyway; the
|
||||
serializer re-derives RFC 8445 §5.1.2.1 values with `localPref = 65535 - index`,
|
||||
so the sender's ordering intent survives at zero cost. Foundations are grouped by
|
||||
kind and transport, satisfying both halves of §5.1.1.3. `raddr`/`rport` are
|
||||
diagnostics that ICE does not consume, and `generation`/`network-cost` are Chrome
|
||||
extensions.
|
||||
kind and transport, satisfying both halves of §5.1.1.3. `generation` and
|
||||
`network-cost` are Chrome extensions and are dropped.
|
||||
|
||||
### What the serializer must emit anyway
|
||||
|
||||
Three things are not in the descriptor but must appear in the rebuilt SDP. All
|
||||
three are constants or derived from data already present, so none costs a byte —
|
||||
and each was found by a live cross-browser test rather than by reading the spec.
|
||||
|
||||
- **`raddr` / `rport` on every srflx, prflx and relay candidate.** RFC 8839 §5.1
|
||||
makes rel-addr and rel-port mandatory for non-host candidates even though ICE's
|
||||
own algorithm never reads them. An earlier draft left them out as
|
||||
"diagnostics". Chrome accepts such a line; **Firefox drops the candidate.**
|
||||
Measured: relay-only connections to Firefox went 0/8 while the browser's own
|
||||
SDP went 8/8, and the fault was invisible in the STUN and TURN profiles because
|
||||
a host pair connected instead. `0.0.0.0`/`0` (or `::`/`0`) is emitted — the
|
||||
same placeholder Chrome uses when it has no base address to disclose.
|
||||
|
||||
- **No `a=ice-options:trickle`, and an explicit `a=end-of-candidates`.** A
|
||||
descriptor is a complete one-shot candidate set; there is no channel to trickle
|
||||
over. Advertising trickle promises candidates that can never arrive and leaves
|
||||
the peer waiting for them.
|
||||
|
||||
- **A real default candidate on the `m=` port and `c=` line.** `m=application 9`
|
||||
with `c=IN IP4 0.0.0.0` is the trickle convention for "nothing gathered yet"
|
||||
(RFC 8840 §4.1) and is simply false here. The serializer advertises the most
|
||||
publicly reachable candidate — relay, then srflx, then host, which is the order
|
||||
Chrome uses and the reverse of ICE priority — falling back to the null form
|
||||
only when every candidate is mDNS, which is also what Chrome does.
|
||||
|
||||
### TLV extension area
|
||||
|
||||
|
||||
+22
-22
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786031423211">
|
||||
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1786051485967">
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
@@ -90,7 +90,7 @@
|
||||
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
|
||||
|
||||
<!-- Apple Touch Icons -->
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786031423211">
|
||||
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1786051485967">
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
|
||||
@@ -99,7 +99,7 @@
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786031423211">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1786051485967">
|
||||
|
||||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
@@ -183,7 +183,7 @@
|
||||
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
|
||||
both execute in document order after parsing, so React still runs before the
|
||||
app modules below, but the parser / first paint is no longer blocked. -->
|
||||
<script defer src="config/ice-servers.js?v=1786031423211"></script>
|
||||
<script defer src="config/ice-servers.js?v=1786051485967"></script>
|
||||
<script defer src="libs/react/react.production.min.js"></script>
|
||||
<script defer src="libs/react-dom/react-dom.production.min.js"></script>
|
||||
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
|
||||
@@ -191,8 +191,8 @@
|
||||
Its CSS is loaded async via load-async-css.js (not paint-critical). -->
|
||||
<script defer src="libs/prism/prism.js"></script>
|
||||
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1786031423211">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786031423211">
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1786051485967">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1786051485967">
|
||||
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
|
||||
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
|
||||
first screen). Inter latin 400/700 cover body text and headings/buttons. -->
|
||||
@@ -200,31 +200,31 @@
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<link rel="stylesheet" href="/assets/fonts/inter/inter.css">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1786031423211">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1786031423211">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1786031423211">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1786051485967">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1786051485967">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1786051485967">
|
||||
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
|
||||
<script defer src="src/scripts/load-async-css.js?v=1786031423211"></script>
|
||||
<script defer src="src/scripts/load-async-css.js?v=1786051485967"></script>
|
||||
<noscript>
|
||||
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
|
||||
<link rel="stylesheet" href="libs/prism/prism.css">
|
||||
</noscript>
|
||||
<script defer src="src/scripts/fa-check.js?v=1786031423211"></script>
|
||||
<script defer src="src/scripts/fa-check.js?v=1786051485967"></script>
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script defer src="src/utils/updateManager.js?v=1786031423211"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1786031423211"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1786031423211"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1786031423211"></script>
|
||||
<script defer src="src/utils/updateManager.js?v=1786051485967"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1786051485967"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1786051485967"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1786051485967"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1786031423211"></script>
|
||||
<script type="module" src="dist/app.js?v=1786031423211"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1786051485967"></script>
|
||||
<script type="module" src="dist/app.js?v=1786051485967"></script>
|
||||
|
||||
<script defer src="src/scripts/pwa-register.js?v=1786031423211"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1786031423211" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1786031423211" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1786031423211"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786031423211">
|
||||
<script defer src="src/scripts/pwa-register.js?v=1786051485967"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1786051485967" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1786051485967" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1786051485967"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1786051485967">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1786031423211",
|
||||
"buildVersion": "1786031423211",
|
||||
"appVersion": "5.8.0",
|
||||
"buildTime": "2026-08-06T15:50:23.257Z",
|
||||
"buildId": "1786031423211-3212138",
|
||||
"gitHash": "3212138",
|
||||
"version": "1786051485967",
|
||||
"buildVersion": "1786051485967",
|
||||
"appVersion": "5.8.1",
|
||||
"buildTime": "2026-08-06T21:24:46.003Z",
|
||||
"buildId": "1786051485967-6e82cfc",
|
||||
"gitHash": "6e82cfc",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-08-06T15:50:23.258Z"
|
||||
"generatedAt": "2026-08-06T21:24:46.005Z"
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "5.8.0",
|
||||
"version": "5.8.1",
|
||||
"description": "Secure P2P Communication Application with End-to-End Encryption",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
|
||||
@@ -659,6 +659,37 @@ function renderAddr(kind, addr) {
|
||||
*/
|
||||
export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
const isOffer = desc.type === TYPE.OFFER;
|
||||
|
||||
// The m-line port and c-line carry the DEFAULT CANDIDATE, and they must be
|
||||
// real whenever we have one.
|
||||
//
|
||||
// `m=application 9` with `c=IN IP4 0.0.0.0` is the trickle-ICE convention
|
||||
// for "no candidate yet" (RFC 8840 §4.1). A descriptor is never trickle —
|
||||
// the candidate set is complete by construction — so a null default
|
||||
// candidate here is simply a lie, and Firefox acts on it: with a relay-only
|
||||
// Chrome offer, rebuilding with 9/0.0.0.0 connected 0/4 while the browser's
|
||||
// own SDP connected 4/4. Advertising the highest-priority routable candidate
|
||||
// instead, exactly as Chrome and Firefox do, costs zero descriptor bytes
|
||||
// because that candidate is already in the list.
|
||||
//
|
||||
// An mDNS candidate cannot appear here (c= needs a literal address), so if
|
||||
// that is all we have we fall back to the null form — which is also what
|
||||
// Chrome emits in that case.
|
||||
// Preference order is relay > srflx > host — the most publicly reachable
|
||||
// address, which is what the default candidate is for, and the order Chrome
|
||||
// itself uses (its STUN-profile SDP names the srflx address and its TURN
|
||||
// profile names the relay). That is the reverse of ICE priority, which
|
||||
// ranks host first.
|
||||
const DEFAULT_RANK = { relay: 0, srflx: 1, host: 2 };
|
||||
const def = desc.candidates
|
||||
.filter((c) => c.kind !== KIND.HOST_MDNS && c.tcptype === 0)
|
||||
.sort((x, y) => DEFAULT_RANK[KIND_TYPE[x.kind]] - DEFAULT_RANK[KIND_TYPE[y.kind]])[0];
|
||||
const defIsV6 = def && KIND_FAMILY[def.kind] === 'v6';
|
||||
const mPort = def ? def.port : 9;
|
||||
const cLine = def
|
||||
? `c=IN IP${defIsV6 ? '6' : '4'} ${renderAddr(def.kind, def.addr)}`
|
||||
: 'c=IN IP4 0.0.0.0';
|
||||
|
||||
const lines = [
|
||||
'v=0',
|
||||
`o=- ${sessionId} 2 IN IP4 127.0.0.1`,
|
||||
@@ -666,8 +697,8 @@ export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
't=0 0',
|
||||
'a=group:BUNDLE 0',
|
||||
'a=msid-semantic: WMS',
|
||||
'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'c=IN IP4 0.0.0.0',
|
||||
`m=application ${mPort} UDP/DTLS/SCTP webrtc-datachannel`,
|
||||
cLine,
|
||||
'a=ice-ufrag:' + desc.ufrag,
|
||||
'a=ice-pwd:' + desc.pwd,
|
||||
// Deliberately NOT `a=ice-options:trickle`. A descriptor is a complete,
|
||||
@@ -696,6 +727,18 @@ export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
// transport satisfies both halves of that.
|
||||
const foundation = String(c.kind * 4 + c.tcptype + 1);
|
||||
let line = `a=candidate:${foundation} 1 ${transport} ${priority} ${renderAddr(c.kind, c.addr)} ${c.port} typ ${ctype}`;
|
||||
// rel-addr/rel-port are MANDATORY for srflx, prflx and relay candidates
|
||||
// (RFC 8839 §5.1), even though ICE's own algorithm never reads them.
|
||||
// Omitting them is not a harmless economy: Chrome accepts such a line,
|
||||
// Firefox drops the candidate outright. That cost 0/8 relay-only
|
||||
// connections to Firefox while the browser's own SDP made 8/8, and it
|
||||
// stayed invisible in the STUN and TURN profiles because a host pair
|
||||
// connected instead. The value is not transmitted — 0.0.0.0/0 is what
|
||||
// Chrome itself emits when it has no base address to disclose, and it
|
||||
// satisfies the grammar at zero descriptor bytes.
|
||||
if (ctype !== 'host') {
|
||||
line += KIND_FAMILY[c.kind] === 'v6' ? ' raddr :: rport 0' : ' raddr 0.0.0.0 rport 0';
|
||||
}
|
||||
if (transport === 'tcp') line += ` tcptype ${TCPTYPE[c.tcptype]}`;
|
||||
return line;
|
||||
});
|
||||
@@ -703,7 +746,7 @@ export function serializeSdp(desc, { sessionId = '1' } = {}) {
|
||||
// (RFC 8838 §14), so the peer stops waiting for more and can start failing
|
||||
// pairs promptly instead of sitting in checking until a timeout.
|
||||
candLines.push('a=end-of-candidates');
|
||||
const at = lines.indexOf('c=IN IP4 0.0.0.0') + 1;
|
||||
const at = lines.indexOf(cLine) + 1;
|
||||
lines.splice(at, 0, ...candLines);
|
||||
|
||||
return { type: isOffer ? 'offer' : 'answer', sdp: lines.join('\r\n') + '\r\n' };
|
||||
|
||||
@@ -11,7 +11,7 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
|
||||
// Build stamp — rewritten by scripts/post-build.js on every release so this file's
|
||||
// bytes change each deploy. That is what makes the browser detect a new Service Worker,
|
||||
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
|
||||
const SW_BUILD_VERSION = '1786031423211';
|
||||
const SW_BUILD_VERSION = '1786051485967';
|
||||
|
||||
// Load version from meta.json on install
|
||||
async function getAppVersion() {
|
||||
|
||||
@@ -191,6 +191,70 @@ for (const [browser, fixtures] of [['chrome', chrome], ['firefox', firefox]]) {
|
||||
assert.deepEqual([...re.candidates[2].addr].slice(12), [198, 51, 100, 9], 'NAT64 tail round-trips');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SDP grammar conformance of the rebuilt candidate lines
|
||||
//
|
||||
// Regression guard. rel-addr/rel-port are mandatory for srflx/prflx/relay
|
||||
// (RFC 8839 §5.1). Leaving them out is silently fine in Chrome and fatal in
|
||||
// Firefox, which drops the candidate: measured 0/8 relay-only connections to
|
||||
// Firefox versus 8/8 for the browser's own SDP, and invisible in the STUN and
|
||||
// TURN profiles because a host pair connected instead.
|
||||
// ---------------------------------------------------------------------------
|
||||
{
|
||||
const sdp = [
|
||||
'v=0', 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'a=candidate:1 1 udp 2113937151 192.168.1.9 40000 typ host',
|
||||
'a=candidate:2 1 udp 1677729535 203.0.113.4 40001 typ srflx raddr 192.168.1.9 rport 40000',
|
||||
'a=candidate:3 1 udp 50340351 144.172.96.126 40002 typ relay raddr 203.0.113.4 rport 40001',
|
||||
'a=candidate:4 1 udp 50341375 2001:db8::5 40003 typ relay',
|
||||
'a=ice-ufrag:abcd', 'a=ice-pwd:0123456789abcdef01234567',
|
||||
'a=fingerprint:sha-256 ' + new Array(32).fill('EE').join(':'), 'a=setup:actpass',
|
||||
].join('\r\n') + '\r\n';
|
||||
|
||||
const out = serializeSdp(decodeDescriptor(await build(sdp, TYPE.OFFER))).sdp;
|
||||
for (const line of out.split('\r\n').filter((l) => l.startsWith('a=candidate:'))) {
|
||||
const type = line.split(' typ ')[1].split(' ')[0];
|
||||
if (type === 'host') {
|
||||
assert.ok(!line.includes('raddr'), `host candidates must not carry raddr: ${line}`);
|
||||
} else {
|
||||
assert.match(line, / raddr (0\.0\.0\.0|::) rport 0/, `${type} candidate needs rel-addr/rel-port: ${line}`);
|
||||
}
|
||||
}
|
||||
|
||||
// The default candidate must be a real, routable address whenever one
|
||||
// exists — `m=... 9` with `c=IN IP4 0.0.0.0` is the trickle "none yet"
|
||||
// form, and a descriptor is never trickle.
|
||||
assert.ok(!out.includes('a=ice-options:trickle'), 'a complete candidate set must not advertise trickle');
|
||||
assert.match(out, /^a=end-of-candidates$/m, 'the candidate set is explicitly closed');
|
||||
|
||||
// relay beats srflx beats host: the default candidate is the most publicly
|
||||
// reachable one, matching what Chrome emits. Asserted on a v4-only set so
|
||||
// the expected winner is unambiguous.
|
||||
const v4only = [
|
||||
'v=0', 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'a=candidate:1 1 udp 2113937151 192.168.1.9 40000 typ host',
|
||||
'a=candidate:2 1 udp 1677729535 203.0.113.4 40001 typ srflx raddr 192.168.1.9 rport 40000',
|
||||
'a=candidate:3 1 udp 50340351 144.172.96.126 40002 typ relay raddr 203.0.113.4 rport 40001',
|
||||
'a=ice-ufrag:abcd', 'a=ice-pwd:0123456789abcdef01234567',
|
||||
'a=fingerprint:sha-256 ' + new Array(32).fill('EE').join(':'), 'a=setup:actpass',
|
||||
].join('\r\n') + '\r\n';
|
||||
const vOut = serializeSdp(decodeDescriptor(await build(v4only, TYPE.OFFER))).sdp;
|
||||
assert.match(vOut, /^m=application 40002 /m, 'm-line carries the relay port as default');
|
||||
assert.match(vOut, /^c=IN IP4 144\.172\.96\.126$/m, 'c-line carries the relay address as default');
|
||||
|
||||
// With only an mDNS candidate there is no literal address to advertise, so
|
||||
// the null form is correct — and it is what Chrome emits in that case too.
|
||||
const mdnsOnly = [
|
||||
'v=0', 'm=application 9 UDP/DTLS/SCTP webrtc-datachannel',
|
||||
'a=candidate:1 1 udp 2113937151 7d9c00c2-bcef-48b6-9166-428899e0582e.local 40000 typ host',
|
||||
'a=ice-ufrag:abcd', 'a=ice-pwd:0123456789abcdef01234567',
|
||||
'a=fingerprint:sha-256 ' + new Array(32).fill('EE').join(':'), 'a=setup:actpass',
|
||||
].join('\r\n') + '\r\n';
|
||||
const mOut = serializeSdp(decodeDescriptor(await build(mdnsOnly, TYPE.OFFER))).sdp;
|
||||
assert.match(mOut, /^m=application 9 /m);
|
||||
assert.match(mOut, /^c=IN IP4 0\.0\.0\.0$/m);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TCP candidates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user