Skip to content

Feature/Add Dodge module: opt-in traffic analysis defense framework#5021

Open
ewitwer wants to merge 60 commits into
Dash-Industry-Forum:developmentfrom
ewitwer:features/videoFingerprintingDefenses
Open

Feature/Add Dodge module: opt-in traffic analysis defense framework#5021
ewitwer wants to merge 60 commits into
Dash-Industry-Forum:developmentfrom
ewitwer:features/videoFingerprintingDefenses

Conversation

@ewitwer
Copy link
Copy Markdown

@ewitwer ewitwer commented Apr 22, 2026

Summary

This PR adds the Dodge module (dash.dodge.js), an opt-in extension that provides a framework with the building blocks for purely client-side defenses against video identification via traffic analysis. It follows the same plugin architecture as dash.mss.js - auto-detected at attachView() time, loaded as a separate script, and wired in via overrides. The module adds 13 new files in src/dodge/ (~3,400 lines), modifies 12 core files with minimal stubs and hooks, and includes 7 unit test files + 1 mock (~6,500 lines) with a requirements traceability document and 18 functional test files with 15 stream configuration entries for end-to-end browser testing. The design is based on a paper published at PoPETS 2026, with production-ready improvements and a few new features that experience has shown may lead to better defenses.

Motivation

Encrypted DASH traffic leaks segment size patterns, request timing, etc. that network observers can use to identify which video a user is watching - smaller content providers' entire catalogs can be fingerprinted successfully, and VPNs/wireless encryption do not help. Dodge lets content providers control which segments to fetch, at what byte ranges, in what order, and with what padding, so that traffic patterns no longer uniquely identify content. Dodge is purely client-side; no server or network infrastructure changes are required beyond hosting JSON configuration files (extended manifests), which are used in place of ordinary MPDs. The name is a play on words: to play videos normally is to "dash"; when defenses are needed to mitigate traffic analysis, we "dodge".

Operating Modes

Scripts loaded Source URL Result
dash.all.min.js only Plain MPD Standard DASH, completely unchanged
dash.all.min.js + dash.dodge.min.js Plain MPD Standard DASH, graceful degradation
dash.all.min.js + dash.dodge.min.js Extended manifest Dodge defense active

No API changes are needed. The user passes an extended manifest URL instead of an MPD URL; everything else works through the existing MediaPlayer.initialize() interface.

Extended Manifests

An extended manifest is a JSON file that wraps the original MPD and adds per-representation download schedules. ManifestLoader detects the JSON format, validates it via the DefenseRegistry singleton, and extracts the embedded MPD for normal DashParser processing. The defense schedules are stored separately and consulted by the DashHandler override during playback.

{
  "start": {
    "mpd": "<MPD xmlns='urn:mpeg:dash:schema:mpd:2011' ...>...</MPD>",
    "base_uri": "https://example.com/segments/"
  },
  "streams": [
    {
      "label": "video_1000k",
      "init": [
        {"range": "0-449"},
        {"range": "44-898"}
      ],
      "data": [
        {"index": 0, "range": "0-43999"},
        {"index": 0, "range": "16000-43999", "buffer": true},
        {"index": 1, "range": "0-43999"},
        {"index": 5, "range": "16000-43999", "padding": true},
        {"index": 2, "buffer": true}
      ]
    }
  ]
}

Each stream entry's label matches a representation ID from the MPD, and streams can optionally be scoped to a period index. Cycles in the data array specify a segment index, optional range for partial downloads, and buffer/padding flags that control when all pending data is buffered for playback - nothing is ever buffered unless the defense designer explicitly sets the buffer flag - and whether it is discarded as cover traffic, respectively. Padding can be downloaded after playable content (trailing padding) to increase a video's apparent duration, as seen by network observers.

New features in Dodge: The buffer field on data cycles can optionally be an array of segment indices (e.g., "buffer": [0, 1]) for selective buffering. Pending data is held until the cycle with the buffer array completes, then all listed indices are buffered together. Also, both data cycles and init cycles may carry a quality field (a representation ID string or numeric index) to fetch from a different representation in the same adaptation set. On data cycles, this lets defenses conceal a segment's true size by, for example, substituting a smaller version from an alternate quality level. On init cycles, it selects which representation's init segment is fetched and stored; downloading multiple init segments per representation is often necessary for playback of alternate representations.

Strict Mode (Fail-Safe)

The dodge.strictMode setting prevents accidental undefended playback:

Level Behavior
'representation' (default) Blocks undefended fragmented representations when an extended manifest is active; plain DASH still works
'manifest' Same as 'representation' but also refuses to play if the source URL is not an extended manifest (no vanilla MPDs)
'max' Same as 'manifest' but also rejects manifests containing thumbnails, non-fragmented text, or XLink
false Always falls back to vanilla dash.js (not recommended for production)

Disabling strict mode entirely is possible but not recommended. While strictMode: false may be useful as a testbed, it does not really increase convenience - in production, it is rather a substitute for proper and comprehensive extended manifest design - and defenses could easily be undermined, since representations that are present in the embedded MPD but not covered in the extended manifest's streams array are downloaded without any defense. Dodge warns when strict mode is disabled.

The 'representation' default is likely suitable for many use cases: undefended playback is possible, but when an extended manifest is used, playback does not proceed if an undefended fragmented representation is encountered. However, the MPDs embedded in extended manifests are allowed to have thumbnail tracks, non-fragmented text, and other elements (see below) that have not been evaluated in a traffic analysis context. Such requests are not controlled by Dodge; extended manifests should thus be crafted with care, and resource sizes of thumbnails, text files, etc. should be carefully controlled if used with extended manifests.

The 'manifest' option is included for use cases where the presence of a defense is critical and playback should not ever proceed without a defense. Realistically, this is probably often the case if a content provider is using defenses in the first place. Thus, this option is perfectly relevant for more typical scenarios as long as extended manifests are available for all videos that may be viewed in the player instance, and we recommend its use (or the use of strictMode: 'max') wherever possible. When strictMode: 'manifest' blocks playback, the player fires an error event with code 300 (DODGE_STRICT_MODE_ERROR_CODE).

The 'max' option is even safer: no thumbnail tracks, non-fragmented text, or XLink expansion - extended manifests whose embedded MPDs contain these elements are rejected. The wire sizes of these resource downloads could leak content-identifying information if not carefully controlled by the content provider, and this is probably difficult to get right. Defended pathways could be created for these features in the future if there is a need (e.g., they could be downloaded using cycles). With strictMode: 'max', the consequences of accidentally including undefended elements in extended manifests are smaller.

Recognizing their importance in the streaming ecosystem, Dodge does not disable DRM, CMCD, DVB reporting, or content steering. Instead, nor/nrr are not set in CMCD requests (everything else is), and Dodge warns when these features are used in strict mode. They are not likely to be useful vectors for passive traffic analysis attacks; however, for peace of mind in critical use cases, they can be disabled via the dash.js settings. We also note that init segment caching should likely not be enabled - it is disabled by default, and Dodge warns when it is enabled in strict mode - and disabling request retries and the background clock sync that occurs upon a 404 response could add plausible deniability in the face of active attacks; this can also be done in the settings.

Integration Approach

  • dash.dodge.min.js sets dashjs.DodgeHandler on the global dashjs object at load time
  • MediaPlayer._detectDodge() auto-detects the module (parallel to _detectMss()) and wires it in during attachView()
  • Six controller/loader overrides are registered via mediaPlayer.extend(..., true): DashHandler, BufferController, ScheduleController, GapController, FetchLoader, XHRLoader
  • Three custom events are registered via Events.extend(): PADDING_LOADED, INIT_FRAGMENT_PARTIAL, MEDIA_FRAGMENT_PARTIAL
  • Separate webpack entry: 'dash.dodge': './src/dodge/index.js'

New Files (src/dodge/)

  • index.js -- Webpack entry point; registers DodgeHandler on the global dashjs object
  • DodgeHandler.js -- Main orchestrator: registers overrides, intercepts FRAGMENT_LOADING_COMPLETED events, manages partial segment combination and scheduling. Also emits diagnostic warnings in tryProcessExtendedManifest
  • DefenseRegistry.js -- FactoryMaker singleton that validates extended manifests, stores them, and provides stream info lookups by representation ID and period index. Precomputes the maximum non-trailing cycle index and full on every init cycle (last occurrence per quality group, or the last cycle, via a backward scan) and every data cycle (last non-padding occurrence per segment index before the segment is buffered)
  • events/DodgeEvents.js -- Three module-specific events
  • errors/DodgeErrors.js -- Strict mode error code (300), following the MSS errors pattern
  • overrides/DodgeDashHandlerOverride.js -- Replaces request generation functions with cycle-based logic; handles init, data, padding, and trailing cycles
  • overrides/DodgeBufferControllerOverride.js -- Mock buffer management for the trailing phase and to account for segment duration variance throughout playback. Also handles quality override media chunks
  • overrides/DodgeScheduleControllerOverride.js -- Enforces random walk delays on segment scheduling; prevents timer clearing during trailing
  • overrides/DodgeGapControllerOverride.js -- Suppresses gap jumps during trailing to prevent spurious seeks
  • overrides/DodgeFetchLoaderOverride.js / DodgeXHRLoaderOverride.js -- Apply HTTP-level request padding before delegating to parent loaders
  • utils/SegmentsUtils.js -- Private copy of template functions (replaceIDForTemplate, replaceTokenForTemplate, etc.) that were removed in v5.2.0
  • utils/RequestPadding.js -- Applies HTTP-level request padding by adding a padding query parameter sized to normalize the total wire size of requests

Core Files Modified

All core changes are minimal stubs and hooks, except for a few new statistics functions:

  • src/streaming/MediaPlayer.js -- Added _detectDodge() (parallel to _detectMss()), deferred GapController singleton creation to after override registration, dodgeHandler passed to ManifestLoader config and cleaned up in reset(), and added a few playback statistics functions
  • src/streaming/ManifestLoader.js -- Calls dodgeHandler.tryProcessExtendedManifest() before parsing, swaps data and base URI on success, returns early on strict mode abort
  • src/dash/DashHandler.js -- Four no-op stub methods added to the instance object (updateDefendedStreamInfo, getIsDefended, getIsTrailing, getRemainingInitCycles) so FactoryMaker.merge() can replace them
  • src/streaming/StreamProcessor.js -- Passes playbackController and adapter to DashHandler config; passes dashHandler to ScheduleController config; calls updateDefendedStreamInfo(representation) before init and segment requests; bypasses the appendInitSegmentFromCache short-circuit while a Dodge defense has remaining init cycles, so every init cycle (including alternate inits) flows through getInitRequest
  • src/streaming/controllers/BufferController.js -- mockBuffer variable + setMockBuffer() method, mock buffer added to reported buffer level; two no-op stubs (onPaddingLoaded, onBufferCycleLoaded); _onInitFragmentLoaded and _onMediaFragmentLoaded exposed in instance for override interception (alternate-init caching and quality-override chunk routing); appendToBuffer() and getInitChunkFromCache() public methods added; INIT_FRAGMENT_LOADED / MEDIA_FRAGMENT_LOADED event handlers routed through dispatch wrappers so FactoryMaker can merge the instance methods
  • src/streaming/controllers/ScheduleController.js -- _shouldClearScheduleTimer() exposed in instance object for override interception
  • src/streaming/controllers/GapController.js -- _shouldJumpGap() exposed in instance object for override interception
  • src/streaming/controllers/PlaybackController.js -- getTimeSinceStreamEnd() added with a stall time accumulator approach
  • src/streaming/SourceBufferSink.js -- Buffer measurement trace for precise per-segment buffer contribution tracking
  • src/streaming/vo/DataChunk.js -- Adds a homeRepresentationId field (default null) so DodgeHandler can tag init and media chunks belonging to an alternate representation; used by DodgeBufferControllerOverride to route alternate-representation init segments into a Dodge-owned cache instead of appending to the home SourceBuffer
  • src/core/Settings.js -- dodge settings block with defaults for scheduling delays, request/URL padding, and strict mode
  • build/webpack/common/webpack.common.base.cjs -- dash.dodge entry added to both prodEntries and devEntries

Tests

This version of Dodge has gone through the same experiments as in the PoPETS 2026 paper, and:

Unit Tests (416 tests)

  • 7 test files covering: DefenseRegistry validation, DodgeHandler event routing and strict mode, DashHandler override cycle traversal (including SegmentBase, multi-period videos, and quality overrides), BufferController mock buffer, GapController trailing suppression, ScheduleController delay enforcement, and RequestPadding
  • 1 mock file (DefenseRegistryMock.js)
  • REQUIREMENTS.md traceability index mapping requirements to individual test cases with statistics
  • Regression safety: Existing dash.js tests are completely unaffected - stubs return no-op values, no control flow changes, no new dependencies
# Run Dodge unit tests only
npx mocha --timeout 10000 'test/unit/test/dodge/*.js'

# Run all unit tests (Dodge + existing)
npx mocha --timeout 10000 'test/unit/test/**/*.js'

Functional Tests

Browser-based tests using the existing Karma + Mocha + Chai infrastructure. These play real video content from CDNs through the Dodge module in a headless browser and verify end-to-end behavior. Test content (12 extended manifests with real CDN-hosted video) is in test/functional/content/dodge/. Stream configuration (15 stream entries) is in test/functional/config/test-configurations/streams/dodge.json. 141 tests total (test counts below reflect the total once each describe is multiplied across the vectors declared in that config).

Core playback and defense verification:

  • play-defended.js (12 tests) -- Basic playback with three extended manifests (undefended, constant-size, mimicry). Verifies playing state, defense activation, playback progression, and no critical errors.
  • defense-verification.js (30 tests) -- The core traffic verification tests. Fetches the extended manifest JSON, collects FRAGMENT_LOADING_STARTED events during playback, and compares the request stream cycle-by-cycle against the manifest for both video and audio:
    • Init cycles match the extended manifest
    • Segment index, byte range, buffer directive, and padding flag match the extended manifest
    • full flag matches precomputed value (last non-padding occurrence of each segment index)
    • URL and request padding are applied: query parameter specified in settings is present
    • Random walk scheduling delay is enforced between consecutive requests
    • Buffer level is positive during defended playback
  • graceful-degradation.js (4 tests) -- Verifies that Dodge does not interfere with vanilla DASH playback when a regular MPD is used outside strict mode.

Trailing phase, mock buffer, and quality override:

  • trailing.js (6 tests) -- Uses a short manifest (3 playable segments + padding) to verify that trailing activates, the mock buffer remains positive throughout, and the playback position does not jump to stream end during trailing (gap jump suppression).
  • extended-trailing.js (6 tests) -- Uses bbb_30fps_trailing_extended.exmfst.json (3 playable segments + 10 padding cycles) to verify that the trailing phase stays active for the full extended duration, the buffer level remains non-negative throughout, and more padding cycles are downloaded than in the short trailing test.
  • quality-override.js (6 tests) -- Extended manifest where cycles 1 and 3 of each video representation fetch from an alternate representation via the quality field. Verifies defense activation, that override cycles fetch the alternate representation's byte range and URL, that init cycles include an alternate-representation pre-fetch, and that playback completes past the override segments.
  • mock-buffer.js (5 tests) -- On the short trailing manifest, verifies that the reported buffer level stays non-negative during defended playback and after each data cycle, exercising the mock buffer bookkeeping in DodgeBufferControllerOverride (segment duration variance compensation).

Strict mode enforcement and side channel policy:

  • text-track-blocking.js (4 tests) -- Extended manifest with an undefended TTML subtitle track. Verifies video and audio play normally while zero text segment requests are made (blocked by strict mode).
  • representation-strict-mode.js (3 tests) -- Extended manifest with only video defended (no audio stream entry). Verifies video defense is active, no audio segment requests are made (blocked by representation-level strict mode), and video segments are fetched normally.
  • strict-mode-allows-exmfst.js (4 tests) -- Verifies that strictMode: 'manifest' allows normal defended playback when an extended manifest is loaded.
  • strict-mode-rejects-mpd.js (2 tests) -- Verifies that strictMode: 'manifest' blocks playback when a regular MPD (not extended manifest) is loaded.
  • side-channel-policy.js (27 tests, 9 its × 3 vectors) -- Verifies that 'max' mode does not modify dash.js settings (retries, background sync, content steering remain at defaults — max only warns). Non-strict mode also verified at defaults. Also verifies DRM manifests are accepted with a warning (DRM is a side-channel concern, not a manifest-rejection one).
  • thumbnail-detection.js (4 tests) -- Extended manifest embedding an MPD with a thumbnail_tile AdaptationSet. Rejected in 'max', warned in 'representation', allowed in false.
  • xlink-detection.js (4 tests) -- Extended manifest embedding an MPD with xlink:href. Rejected in 'max', warned in 'representation', allowed in false.

Seeking and multi-period playback:

  • multiperiod.js (4 tests) -- Two-period MPD with period-scoped defended stream entries. Verifies defense activation, period transition occurs, and segments are fetched from both periods.
  • seek.js (6 tests) -- Seeks forward to 60s during defended playback. Verifies playback resumes near the target, defense remains active, and playback progresses.
  • seek-backward.js (7 tests) -- Seeks forward to 60s, then backward to 10s, then performs three rapid successive seeks. Verifies defense remains active throughout, playback resumes near each target, and no critical errors.
  • seek-trailing.js (7 tests) -- Waits for the trailing phase on a short manifest, then seeks backward to 0s. Verifies the seek does not crash, defense remains active, buffer stays non-negative, and no critical errors.
# Run Dodge functional tests, Firefox (recommended for local testing)
npx karma start test/functional/config/karma.functional.conf.cjs \
  --configfile=local --streamsfile=dodge --single-run --browsers=FirefoxHeadless

# Run Dodge functional tests, Chrome
# Note: requires Google Chrome (not Chromium) for H.264/AAC codec support.
# Headless Chromium on most Linux distros is built without proprietary codecs,
# causing MEDIA_ERR_DECODE on BBB test content.
npx karma start test/functional/config/karma.functional.conf.cjs \
  --configfile=local --streamsfile=dodge --single-run --browsers=ChromeHeadless

Build and Usage

npm run build
# Outputs:
#   dist/modern/umd/dash.all.min.js     -- base player
#   dist/modern/umd/dash.dodge.min.js   -- Dodge module (new)
<script src="dash.all.min.js"></script>
<script src="dash.dodge.min.js"></script>
<script>
  var player = dashjs.MediaPlayer().create();
  player.initialize(document.querySelector("#videoPlayer"), "content.exmfst.json", true);
</script>

The Dodge module is auto-detected at attachView() time. Pass an extended manifest URL as the source; no other API changes are needed.

Build times are effectively unchanged. On the test machine (Fedora 43, 12th Gen Intel Core i7-12650H with 16 logical CPUs, 16 GiB RAM), npm run build does not take subjectively longer than an equivalent build of upstream dash.js v5.2.0.

Bundle impact: dash.all.min.js grows from 775,185 B to 778,357 B (+3,172 B / +3.10 KiB) - the base player only carries the small hooks and stubs described in "Core Files Modified". The Dodge module itself lives entirely in dash.dodge.min.js (70,636 B / 68.98 KiB), which is only loaded when opted into.

ewitwer added 30 commits March 31, 2026 17:32
@dsilhavy dsilhavy added this to the 5.3.0 milestone Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants