Feature/Add Dodge module: opt-in traffic analysis defense framework#5021
Open
ewitwer wants to merge 60 commits into
Open
Feature/Add Dodge module: opt-in traffic analysis defense framework#5021ewitwer wants to merge 60 commits into
ewitwer wants to merge 60 commits into
Conversation
…d skipping byte ranges
…lective buffering is used
…uest failure and retry path)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 asdash.mss.js- auto-detected atattachView()time, loaded as a separate script, and wired in via overrides. The module adds 13 new files insrc/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
dash.all.min.jsonlydash.all.min.js+dash.dodge.min.jsdash.all.min.js+dash.dodge.min.jsNo 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.
ManifestLoaderdetects the JSON format, validates it via theDefenseRegistrysingleton, and extracts the embedded MPD for normalDashParserprocessing. The defense schedules are stored separately and consulted by theDashHandleroverride 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
labelmatches a representation ID from the MPD, and streams can optionally be scoped to aperiodindex. Cycles in thedataarray specify a segmentindex, optionalrangefor partial downloads, andbuffer/paddingflags that control when all pending data is buffered for playback - nothing is ever buffered unless the defense designer explicitly sets thebufferflag - 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
bufferfield ondatacycles 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 aqualityfield (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.strictModesetting prevents accidental undefended playback:'representation'(default)'manifest''representation'but also refuses to play if the source URL is not an extended manifest (no vanilla MPDs)'max''manifest'but also rejects manifests containing thumbnails, non-fragmented text, or XLinkfalseDisabling strict mode entirely is possible but not recommended. While
strictMode: falsemay 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'sstreamsarray 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 ofstrictMode: 'max') wherever possible. WhenstrictMode: '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). WithstrictMode: '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/nrrare 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.jssetsdashjs.DodgeHandleron the globaldashjsobject at load timeMediaPlayer._detectDodge()auto-detects the module (parallel to_detectMss()) and wires it in duringattachView()mediaPlayer.extend(..., true):DashHandler,BufferController,ScheduleController,GapController,FetchLoader,XHRLoaderEvents.extend():PADDING_LOADED,INIT_FRAGMENT_PARTIAL,MEDIA_FRAGMENT_PARTIAL'dash.dodge': './src/dodge/index.js'New Files (
src/dodge/)index.js-- Webpack entry point; registersDodgeHandleron the globaldashjsobjectDodgeHandler.js-- Main orchestrator: registers overrides, interceptsFRAGMENT_LOADING_COMPLETEDevents, manages partial segment combination and scheduling. Also emits diagnostic warnings intryProcessExtendedManifestDefenseRegistry.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 andfullon every init cycle (last occurrence perqualitygroup, or the last cycle, via a backward scan) and every data cycle (last non-padding occurrence per segmentindexbefore the segment is buffered)events/DodgeEvents.js-- Three module-specific eventserrors/DodgeErrors.js-- Strict mode error code (300), following the MSS errors patternoverrides/DodgeDashHandlerOverride.js-- Replaces request generation functions with cycle-based logic; handles init, data, padding, and trailing cyclesoverrides/DodgeBufferControllerOverride.js-- Mock buffer management for the trailing phase and to account for segment duration variance throughout playback. Also handles quality override media chunksoverrides/DodgeScheduleControllerOverride.js-- Enforces random walk delays on segment scheduling; prevents timer clearing during trailingoverrides/DodgeGapControllerOverride.js-- Suppresses gap jumps during trailing to prevent spurious seeksoverrides/DodgeFetchLoaderOverride.js/DodgeXHRLoaderOverride.js-- Apply HTTP-level request padding before delegating to parent loadersutils/SegmentsUtils.js-- Private copy of template functions (replaceIDForTemplate,replaceTokenForTemplate, etc.) that were removed in v5.2.0utils/RequestPadding.js-- Applies HTTP-level request padding by adding apaddingquery parameter sized to normalize the total wire size of requestsCore 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,dodgeHandlerpassed to ManifestLoader config and cleaned up inreset(), and added a few playback statistics functionssrc/streaming/ManifestLoader.js-- CallsdodgeHandler.tryProcessExtendedManifest()before parsing, swaps data and base URI on success, returns early on strict mode abortsrc/dash/DashHandler.js-- Four no-op stub methods added to the instance object (updateDefendedStreamInfo,getIsDefended,getIsTrailing,getRemainingInitCycles) soFactoryMaker.merge()can replace themsrc/streaming/StreamProcessor.js-- PassesplaybackControllerandadaptertoDashHandlerconfig; passesdashHandlertoScheduleControllerconfig; callsupdateDefendedStreamInfo(representation)before init and segment requests; bypasses theappendInitSegmentFromCacheshort-circuit while a Dodge defense has remaining init cycles, so every init cycle (including alternate inits) flows throughgetInitRequestsrc/streaming/controllers/BufferController.js--mockBuffervariable +setMockBuffer()method, mock buffer added to reported buffer level; two no-op stubs (onPaddingLoaded,onBufferCycleLoaded);_onInitFragmentLoadedand_onMediaFragmentLoadedexposed in instance for override interception (alternate-init caching and quality-override chunk routing);appendToBuffer()andgetInitChunkFromCache()public methods added;INIT_FRAGMENT_LOADED/MEDIA_FRAGMENT_LOADEDevent handlers routed through dispatch wrappers so FactoryMaker can merge the instance methodssrc/streaming/controllers/ScheduleController.js--_shouldClearScheduleTimer()exposed in instance object for override interceptionsrc/streaming/controllers/GapController.js--_shouldJumpGap()exposed in instance object for override interceptionsrc/streaming/controllers/PlaybackController.js--getTimeSinceStreamEnd()added with a stall time accumulator approachsrc/streaming/SourceBufferSink.js-- Buffer measurement trace for precise per-segment buffer contribution trackingsrc/streaming/vo/DataChunk.js-- Adds ahomeRepresentationIdfield (defaultnull) soDodgeHandlercan tag init and media chunks belonging to an alternate representation; used byDodgeBufferControllerOverrideto route alternate-representation init segments into a Dodge-owned cache instead of appending to the home SourceBuffersrc/core/Settings.js--dodgesettings block with defaults for scheduling delays, request/URL padding, and strict modebuild/webpack/common/webpack.common.base.cjs--dash.dodgeentry added to bothprodEntriesanddevEntriesTests
This version of Dodge has gone through the same experiments as in the PoPETS 2026 paper, and:
Unit Tests (416 tests)
DefenseRegistryMock.js)REQUIREMENTS.mdtraceability index mapping requirements to individual test cases with statisticsFunctional 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 intest/functional/config/test-configurations/streams/dodge.json. 141 tests total (test counts below reflect the total once eachdescribeis 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, collectsFRAGMENT_LOADING_STARTEDevents during playback, and compares the request stream cycle-by-cycle against the manifest for both video and audio:fullflag matches precomputed value (last non-padding occurrence of each segment index)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) -- Usesbbb_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 thequalityfield. 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 inDodgeBufferControllerOverride(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 thatstrictMode: 'manifest'allows normal defended playback when an extended manifest is loaded.strict-mode-rejects-mpd.js(2 tests) -- Verifies thatstrictMode: '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 athumbnail_tileAdaptationSet. Rejected in'max', warned in'representation', allowed infalse.xlink-detection.js(4 tests) -- Extended manifest embedding an MPD withxlink:href. Rejected in'max', warned in'representation', allowed infalse.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.Build and Usage
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 builddoes not take subjectively longer than an equivalent build of upstream dash.js v5.2.0.Bundle impact:
dash.all.min.jsgrows 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 indash.dodge.min.js(70,636 B / 68.98 KiB), which is only loaded when opted into.