@blockcast/mmt-container 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockcast/mmt-container",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MMTP/LOC container parsing for MMT over MoQ and SSM",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -169,10 +169,10 @@
169
169
  "test:watch": "vitest"
170
170
  },
171
171
  "dependencies": {
172
- "@blockcast/fec-worker": "^2.0.0",
173
- "@blockcast/mmt-base": "^2.0.0",
174
- "@blockcast/mmt-fec": "^2.0.0",
175
- "@blockcast/mmt-manifest-verify": "^2.0.0",
172
+ "@blockcast/fec-worker": "^2.0.1",
173
+ "@blockcast/mmt-base": "^2.0.1",
174
+ "@blockcast/mmt-fec": "^2.0.1",
175
+ "@blockcast/mmt-manifest-verify": "^2.0.1",
176
176
  "@noble/curves": "^1.9.7",
177
177
  "@noble/hashes": "^1.8.0",
178
178
  "ajv": "^8.17.1",
@@ -16,6 +16,7 @@ import {
16
16
  MMTP_PACKET_ID_MAX,
17
17
  MMTP_SIGNALING_PACKET_ID,
18
18
  readMMTPPacketId,
19
+ sameIpAddress,
19
20
  } from '@blockcast/mmt-base'
20
21
  import { isBcpV2RecordCandidate } from '@blockcast/mmt-manifest-verify'
21
22
 
@@ -41,6 +42,12 @@ export interface MulticastTransportConfig {
41
42
  * are "MHM2" passes ahead of the packet_id read and the allow-list, and is
42
43
  * never counted toward a packet_id. On an unprotected join it is an
43
44
  * unparseable MMTP packet, as before.
45
+ *
46
+ * A protected join admits only datagrams whose reported sender is the
47
+ * flow's S (`sourceAddress`, or `senderAddress` on ASM), compared as IP
48
+ * addresses, before anything hashes them (multicast-02 7.2.7). A datagram
49
+ * with no reported sender is dropped, and a join that cannot report one
50
+ * (no S, or an ATSC 3.0 tuner) is declined `invalid-request`.
44
51
  */
45
52
  protected?: boolean
46
53
  }
@@ -148,7 +155,8 @@ export interface MulticastTransportStats {
148
155
  * one (G, port, packet_id); the first sender whose packet is accepted owns
149
156
  * the sub-flow until the next join, and every other sender is dropped here,
150
157
  * before any gap, duplicate or FEC ledger sees it. A join or a silence
151
- * failback releases the lock.
158
+ * failback releases the lock. On a protected join it also counts every
159
+ * datagram whose sender is not the flow's S, or is not reported.
152
160
  */
153
161
  packetsDroppedForeignSender: number
154
162
  }
@@ -215,7 +223,14 @@ function hasMulticastGateway(): boolean {
215
223
  return typeof getMulticastGlobals().multicast?.subscribe === 'function'
216
224
  }
217
225
 
218
- /** The `bytes` event's sender (multicast-contract 1.3.0); undefined when unknown. */
226
+ /**
227
+ * The `bytes` event's sender (multicast-contract 1.3.0); undefined when unknown.
228
+ *
229
+ * The provider filters (S, G, port) and owns the multicast-02 7.2.7
230
+ * destination MUST: the event carries no destination address, so libmmt
231
+ * cannot discard a datagram whose IP destination is not G (pim BLO-51627).
232
+ * On a protected flow libmmt checks the sender itself, against S.
233
+ */
219
234
  function senderOf(event: CustomEvent<unknown>): string | undefined {
220
235
  const sourceAddress = (event as { sourceAddress?: unknown }).sourceAddress
221
236
  return typeof sourceAddress === 'string' ? sourceAddress : undefined
@@ -343,6 +358,10 @@ export class MulticastTransport<TPacketContext = never> {
343
358
  readonly #acceptedBytesByPacketId = new Map<number, { bytes: number }>()
344
359
  #packetIds: ReadonlySet<number> | null = null
345
360
  #protected = false
361
+ // A protected join's S, and the last sender spelling that matched it, so a
362
+ // matching sender parses once per spelling rather than per datagram.
363
+ #protectedSource: string | undefined = undefined
364
+ #matchedSource: string | undefined = undefined
346
365
  // The sender S this join's accepted packets came from, once known; see
347
366
  // packetsDroppedForeignSender. Cleared by every join and by a silence
348
367
  // failback, so a replacement publisher is not locked out.
@@ -406,6 +425,11 @@ export class MulticastTransport<TPacketContext = never> {
406
425
  // failure before it runs, so the catch classifies a decline by where the
407
426
  // join stopped, never by what the error says.
408
427
  let failure: MulticastJoinDeclineReason = 'invalid-request'
428
+ const protectedSource = config.protected === true ? config.sourceAddress ?? config.senderAddress : undefined
429
+ if (config.protected === true && (protectedSource === undefined || config.atsc3)) {
430
+ console.warn('[MulticastTransport] protected join refused: the path reports no per-datagram sender to check against S')
431
+ return this.#declined(generation, 'invalid-request')
432
+ }
409
433
  try {
410
434
  const packetIds = config.packetIds?.length ? new Set(config.packetIds) : null
411
435
  console.log(
@@ -454,8 +478,9 @@ export class MulticastTransport<TPacketContext = never> {
454
478
  // costs batching on every any-source join, including one on a pre-1.3.0
455
479
  // provider that sends no sender and so gets no lock for it; keep it
456
480
  // unless the provider can say it carries the sender.
481
+ // A protected join checks every datagram's sender, so it listens to `bytes` too.
457
482
  const anySource = !config.sourceAddress && !config.senderAddress && !config.atsc3
458
- const packetListener: PacketListener = !anySource && subscriptionDeliversPacketBatches(subscription)
483
+ const packetListener: PacketListener = !anySource && protectedSource === undefined && subscriptionDeliversPacketBatches(subscription)
459
484
  ? {
460
485
  type: 'packets',
461
486
  listener: (event) => {
@@ -473,6 +498,8 @@ export class MulticastTransport<TPacketContext = never> {
473
498
  this.#packetListener = packetListener
474
499
  this.#packetIds = packetIds
475
500
  this.#protected = config.protected === true
501
+ this.#protectedSource = protectedSource
502
+ this.#matchedSource = undefined
476
503
  this.#providerFilter = {
477
504
  acknowledged: packetIds,
478
505
  sent: 0,
@@ -1009,6 +1036,8 @@ export class MulticastTransport<TPacketContext = never> {
1009
1036
  this.#delivery = undefined
1010
1037
  this.#packetIds = null
1011
1038
  this.#protected = false
1039
+ this.#protectedSource = undefined
1040
+ this.#matchedSource = undefined
1012
1041
  this.#providerFilter = null
1013
1042
  this.#context = undefined
1014
1043
  this.#withdrawnPacketIds.clear()
@@ -1016,8 +1045,18 @@ export class MulticastTransport<TPacketContext = never> {
1016
1045
 
1017
1046
  /** The ingress filter; an accepted packet is counted here, once. */
1018
1047
  #acceptPacket(packet: Uint8Array, sourceAddress?: string): boolean {
1048
+ const source = this.#protectedSource
1049
+ if (source !== undefined && (sourceAddress === undefined || sourceAddress !== this.#matchedSource)) {
1050
+ if (sourceAddress === undefined || !sameIpAddress(sourceAddress, source)) {
1051
+ this.#stats.packetsDroppedForeignSender++
1052
+ return false
1053
+ }
1054
+ this.#matchedSource = sourceAddress
1055
+ }
1019
1056
  // Absent means unknown (a pre-1.3.0 provider, a tuner): never a new sender.
1020
- const owner = this.#lockedTo?.() ?? this.#sender
1057
+ // A protected flow's S check above replaces the first-seen lock, which
1058
+ // compares spellings and would refuse S written another way.
1059
+ const owner = source === undefined ? this.#lockedTo?.() ?? this.#sender : undefined
1021
1060
  if (sourceAddress !== undefined && owner !== undefined && sourceAddress !== owner) {
1022
1061
  this.#stats.packetsDroppedForeignSender++
1023
1062
  return false