@torrent-tv/proxy 2.89.5 → 2.89.7
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/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +338 -338
- package/CHANGELOG.md +16 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +762 -753
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +475 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +68 -68
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +240 -240
- package/test/helpers/manager.js +62 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +87 -87
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-completeness.test.js +37 -1
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +160 -160
- package/test/quality-variants.test.js +1270 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +64 -64
- package/test/same-height-switch.test.js +663 -663
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +222 -221
- package/test/segment-serve-wiring.test.js +9 -3
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +111 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/supply-margin.test.js +190 -190
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +508 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +92 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file System health metrics for proxy scoring.
|
|
3
|
-
*
|
|
4
|
-
* Collects lightweight OS-level metrics that allow the registry server to
|
|
5
|
-
* score and rank proxy clients when a browser requests playback.
|
|
6
|
-
* All values are cheap to read and require no background work.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import os from "node:os";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Snapshot of system health at a point in time.
|
|
13
|
-
*
|
|
14
|
-
* `cpuLoad` — 1-minute load average divided by the number of logical CPUs.
|
|
15
|
-
* 0 means idle, 1 means fully utilised, >1 means overloaded.
|
|
16
|
-
* Suitable as input to `Math.max(0, 1 - Math.min(1, cpuLoad))` for a
|
|
17
|
-
* normalised "CPU availability" score.
|
|
18
|
-
*
|
|
19
|
-
* `memFree` — fraction of total system RAM that could still be given out
|
|
20
|
-
* (0–1). The reading is the storage component's (`storage/machine-memory.js`
|
|
21
|
-
* says why that is not the same as free memory), handed in.
|
|
22
|
-
*
|
|
23
|
-
* `uptime` — process uptime in whole seconds (useful for preferring
|
|
24
|
-
* already-warmed proxies over freshly started ones).
|
|
25
|
-
*
|
|
26
|
-
* @typedef {Object} HealthMetrics
|
|
27
|
-
* @property {number} cpuLoad - 1-min load avg / cpu-count. 0 = idle, 1 = saturated, >1 = overloaded.
|
|
28
|
-
* @property {number} memFree - Memory an allocation could obtain, as a fraction of total RAM (0–1).
|
|
29
|
-
* @property {number} uptime - Process uptime in seconds.
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Collect current system health metrics.
|
|
34
|
-
*
|
|
35
|
-
* All three values are rounded to three decimal places to avoid unnecessary
|
|
36
|
-
* diff noise when serialising to JSON across the tunnel.
|
|
37
|
-
*
|
|
38
|
-
* @param {object} params
|
|
39
|
-
* @param {() => number} params.availableMemoryBytes - What the machine could
|
|
40
|
-
* give an allocation right now; the storage component's reading.
|
|
41
|
-
* @returns {HealthMetrics}
|
|
42
|
-
*/
|
|
43
|
-
export function collectHealthMetrics({ availableMemoryBytes }) {
|
|
44
|
-
const cpuCount = os.cpus().length || 1;
|
|
45
|
-
const cpuLoad = os.loadavg()[0] / cpuCount;
|
|
46
|
-
const memFree = availableMemoryBytes() / os.totalmem();
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
cpuLoad: Math.round(cpuLoad * 1000) / 1000,
|
|
50
|
-
memFree: Math.round(memFree * 1000) / 1000,
|
|
51
|
-
uptime: Math.floor(process.uptime())
|
|
52
|
-
};
|
|
53
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file System health metrics for proxy scoring.
|
|
3
|
+
*
|
|
4
|
+
* Collects lightweight OS-level metrics that allow the registry server to
|
|
5
|
+
* score and rank proxy clients when a browser requests playback.
|
|
6
|
+
* All values are cheap to read and require no background work.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Snapshot of system health at a point in time.
|
|
13
|
+
*
|
|
14
|
+
* `cpuLoad` — 1-minute load average divided by the number of logical CPUs.
|
|
15
|
+
* 0 means idle, 1 means fully utilised, >1 means overloaded.
|
|
16
|
+
* Suitable as input to `Math.max(0, 1 - Math.min(1, cpuLoad))` for a
|
|
17
|
+
* normalised "CPU availability" score.
|
|
18
|
+
*
|
|
19
|
+
* `memFree` — fraction of total system RAM that could still be given out
|
|
20
|
+
* (0–1). The reading is the storage component's (`storage/machine-memory.js`
|
|
21
|
+
* says why that is not the same as free memory), handed in.
|
|
22
|
+
*
|
|
23
|
+
* `uptime` — process uptime in whole seconds (useful for preferring
|
|
24
|
+
* already-warmed proxies over freshly started ones).
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} HealthMetrics
|
|
27
|
+
* @property {number} cpuLoad - 1-min load avg / cpu-count. 0 = idle, 1 = saturated, >1 = overloaded.
|
|
28
|
+
* @property {number} memFree - Memory an allocation could obtain, as a fraction of total RAM (0–1).
|
|
29
|
+
* @property {number} uptime - Process uptime in seconds.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Collect current system health metrics.
|
|
34
|
+
*
|
|
35
|
+
* All three values are rounded to three decimal places to avoid unnecessary
|
|
36
|
+
* diff noise when serialising to JSON across the tunnel.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} params
|
|
39
|
+
* @param {() => number} params.availableMemoryBytes - What the machine could
|
|
40
|
+
* give an allocation right now; the storage component's reading.
|
|
41
|
+
* @returns {HealthMetrics}
|
|
42
|
+
*/
|
|
43
|
+
export function collectHealthMetrics({ availableMemoryBytes }) {
|
|
44
|
+
const cpuCount = os.cpus().length || 1;
|
|
45
|
+
const cpuLoad = os.loadavg()[0] / cpuCount;
|
|
46
|
+
const memFree = availableMemoryBytes() / os.totalmem();
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
cpuLoad: Math.round(cpuLoad * 1000) / 1000,
|
|
50
|
+
memFree: Math.round(memFree * 1000) / 1000,
|
|
51
|
+
uptime: Math.floor(process.uptime())
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -1,231 +1,231 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file NAT classification via STUN (minimal, dependency-free client).
|
|
3
|
-
*
|
|
4
|
-
* Determines whether the proxy's home NAT preserves its external UDP port
|
|
5
|
-
* across destinations (endpoint-independent / "cone") or assigns a new port
|
|
6
|
-
* per destination ("symmetric"). This decides whether the fixed-UDP-port +
|
|
7
|
-
* UPnP mapping (the WebRTC reachability of proxy 2.9.18) is sufficient:
|
|
8
|
-
*
|
|
9
|
-
* - endpoint-independent → a static mapping works; no port prediction needed.
|
|
10
|
-
* - symmetric → the mapped port differs per viewer; WebRTC needs
|
|
11
|
-
* port prediction (a later roadmap step).
|
|
12
|
-
*
|
|
13
|
-
* Method: from ONE local UDP socket, send a STUN Binding Request to TWO
|
|
14
|
-
* different public STUN servers (different destinations) and compare the
|
|
15
|
-
* reported external port. Same port → endpoint-independent; different →
|
|
16
|
-
* symmetric. This is the modern, reliable test — unlike RFC 3489's CHANGE-
|
|
17
|
-
* REQUEST classification, it needs no special STUN-server support (works with
|
|
18
|
-
* Google/Cloudflare STUN). The two queries MUST share one socket: a fresh
|
|
19
|
-
* socket would get its own NAT mapping and make even a cone NAT look symmetric.
|
|
20
|
-
*
|
|
21
|
-
* Strictly best-effort: never throws; returns `klass: "unknown"` if the probes
|
|
22
|
-
* fail (STUN blocked, offline). Used for diagnostics/telemetry and to gate the
|
|
23
|
-
* future WebRTC port-prediction work.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import dgram from "node:dgram";
|
|
27
|
-
import crypto from "node:crypto";
|
|
28
|
-
|
|
29
|
-
const STUN_MAGIC_COOKIE = 0x2112a442;
|
|
30
|
-
const STUN_BINDING_REQUEST = 0x0001;
|
|
31
|
-
const ATTR_XOR_MAPPED_ADDRESS = 0x0020;
|
|
32
|
-
const ATTR_MAPPED_ADDRESS = 0x0001;
|
|
33
|
-
|
|
34
|
-
// Public STUN servers from DIFFERENT operators, so the destination genuinely
|
|
35
|
-
// differs between the two queries (required for the symmetric test).
|
|
36
|
-
const DEFAULT_STUN_SERVERS = [
|
|
37
|
-
{ host: "stun.l.google.com", port: 19302 },
|
|
38
|
-
{ host: "stun.cloudflare.com", port: 3478 },
|
|
39
|
-
{ host: "stun1.l.google.com", port: 19302 }
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
const QUERY_TIMEOUT_MS = 4000;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* @typedef {object} NatObservation
|
|
46
|
-
* @property {string} server - "host:port" that was queried.
|
|
47
|
-
* @property {string} ip - Reflexive external IP reported.
|
|
48
|
-
* @property {number} port - Reflexive external port reported.
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @typedef {object} NatClassification
|
|
53
|
-
* @property {"endpoint-independent" | "symmetric" | "unknown"} klass
|
|
54
|
-
* @property {string | null} externalIp - External IP (from the first success).
|
|
55
|
-
* @property {NatObservation[]} observations
|
|
56
|
-
* @property {number | null} portDelta - port(2nd) - port(1st) when symmetric, else null.
|
|
57
|
-
*/
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Build a 20-byte STUN Binding Request with a random 96-bit transaction id.
|
|
61
|
-
*
|
|
62
|
-
* @returns {Buffer}
|
|
63
|
-
*/
|
|
64
|
-
function buildBindingRequest() {
|
|
65
|
-
const msg = Buffer.alloc(20);
|
|
66
|
-
msg.writeUInt16BE(STUN_BINDING_REQUEST, 0);
|
|
67
|
-
msg.writeUInt16BE(0, 2); // message length (no attributes)
|
|
68
|
-
msg.writeUInt32BE(STUN_MAGIC_COOKIE, 4);
|
|
69
|
-
crypto.randomFillSync(msg, 8, 12); // transaction id
|
|
70
|
-
return msg;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Parse the reflexive address from a STUN response, preferring
|
|
75
|
-
* XOR-MAPPED-ADDRESS and falling back to MAPPED-ADDRESS. IPv4 only.
|
|
76
|
-
*
|
|
77
|
-
* @param {Buffer} msg
|
|
78
|
-
* @returns {{ ip: string, port: number } | null}
|
|
79
|
-
*/
|
|
80
|
-
function parseMappedAddress(msg) {
|
|
81
|
-
if (msg.length < 20) {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
let offset = 20;
|
|
85
|
-
while (offset + 4 <= msg.length) {
|
|
86
|
-
const type = msg.readUInt16BE(offset);
|
|
87
|
-
const length = msg.readUInt16BE(offset + 2);
|
|
88
|
-
const valueStart = offset + 4;
|
|
89
|
-
if (valueStart + length > msg.length) {
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (type === ATTR_XOR_MAPPED_ADDRESS || type === ATTR_MAPPED_ADDRESS) {
|
|
94
|
-
const family = msg.readUInt8(valueStart + 1);
|
|
95
|
-
if (family === 0x01) {
|
|
96
|
-
// IPv4
|
|
97
|
-
const xored = type === ATTR_XOR_MAPPED_ADDRESS;
|
|
98
|
-
const rawPort = msg.readUInt16BE(valueStart + 2);
|
|
99
|
-
const port = xored ? rawPort ^ (STUN_MAGIC_COOKIE >>> 16) : rawPort;
|
|
100
|
-
const addrBytes = [
|
|
101
|
-
msg.readUInt8(valueStart + 4),
|
|
102
|
-
msg.readUInt8(valueStart + 5),
|
|
103
|
-
msg.readUInt8(valueStart + 6),
|
|
104
|
-
msg.readUInt8(valueStart + 7)
|
|
105
|
-
];
|
|
106
|
-
const cookieBytes = [
|
|
107
|
-
(STUN_MAGIC_COOKIE >>> 24) & 0xff,
|
|
108
|
-
(STUN_MAGIC_COOKIE >>> 16) & 0xff,
|
|
109
|
-
(STUN_MAGIC_COOKIE >>> 8) & 0xff,
|
|
110
|
-
STUN_MAGIC_COOKIE & 0xff
|
|
111
|
-
];
|
|
112
|
-
const ipParts = addrBytes.map((b, i) => (xored ? b ^ cookieBytes[i] : b));
|
|
113
|
-
return { ip: ipParts.join("."), port };
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Attributes are padded to 4-byte boundaries.
|
|
118
|
-
offset = valueStart + length + ((4 - (length % 4)) % 4);
|
|
119
|
-
}
|
|
120
|
-
return null;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Send one Binding Request to `host:port` over the given socket and resolve
|
|
125
|
-
* with the reflexive address, or null on timeout / parse failure.
|
|
126
|
-
*
|
|
127
|
-
* @param {import("node:dgram").Socket} socket
|
|
128
|
-
* @param {string} host
|
|
129
|
-
* @param {number} port
|
|
130
|
-
* @returns {Promise<{ ip: string, port: number } | null>}
|
|
131
|
-
*/
|
|
132
|
-
function queryStun(socket, host, port) {
|
|
133
|
-
return new Promise((resolve) => {
|
|
134
|
-
let settled = false;
|
|
135
|
-
const onMessage = (msg) => {
|
|
136
|
-
if (settled) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
const parsed = parseMappedAddress(msg);
|
|
140
|
-
if (!parsed) {
|
|
141
|
-
return; // ignore unrelated datagrams; let the timeout fire if needed
|
|
142
|
-
}
|
|
143
|
-
settled = true;
|
|
144
|
-
socket.off("message", onMessage);
|
|
145
|
-
resolve(parsed);
|
|
146
|
-
};
|
|
147
|
-
socket.on("message", onMessage);
|
|
148
|
-
socket.send(buildBindingRequest(), port, host, (err) => {
|
|
149
|
-
if (err && !settled) {
|
|
150
|
-
settled = true;
|
|
151
|
-
socket.off("message", onMessage);
|
|
152
|
-
resolve(null);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
const timer = setTimeout(() => {
|
|
156
|
-
if (!settled) {
|
|
157
|
-
settled = true;
|
|
158
|
-
socket.off("message", onMessage);
|
|
159
|
-
resolve(null);
|
|
160
|
-
}
|
|
161
|
-
}, QUERY_TIMEOUT_MS);
|
|
162
|
-
timer.unref?.();
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Classify the host's NAT by comparing the external port seen by two different
|
|
168
|
-
* STUN servers over a single local socket.
|
|
169
|
-
*
|
|
170
|
-
* @param {{ servers?: Array<{ host: string, port: number }> }} [options]
|
|
171
|
-
* @returns {Promise<NatClassification>}
|
|
172
|
-
*/
|
|
173
|
-
export async function classifyNat({ servers = DEFAULT_STUN_SERVERS } = {}) {
|
|
174
|
-
/** @type {NatClassification} */
|
|
175
|
-
const unknown = { klass: "unknown", externalIp: null, observations: [], portDelta: null };
|
|
176
|
-
|
|
177
|
-
const socket = dgram.createSocket("udp4");
|
|
178
|
-
try {
|
|
179
|
-
await new Promise((resolve, reject) => {
|
|
180
|
-
socket.once("error", reject);
|
|
181
|
-
socket.bind(0, () => {
|
|
182
|
-
socket.off("error", reject);
|
|
183
|
-
resolve(undefined);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
} catch {
|
|
187
|
-
try {
|
|
188
|
-
socket.close();
|
|
189
|
-
} catch {
|
|
190
|
-
// ignore
|
|
191
|
-
}
|
|
192
|
-
return unknown;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/** @type {NatObservation[]} */
|
|
196
|
-
const observations = [];
|
|
197
|
-
try {
|
|
198
|
-
for (const server of servers) {
|
|
199
|
-
const result = await queryStun(socket, server.host, server.port);
|
|
200
|
-
if (result) {
|
|
201
|
-
observations.push({ server: `${server.host}:${server.port}`, ip: result.ip, port: result.port });
|
|
202
|
-
}
|
|
203
|
-
// Stop once we have two observations from two different destinations.
|
|
204
|
-
if (observations.length >= 2) {
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
} finally {
|
|
209
|
-
try {
|
|
210
|
-
socket.close();
|
|
211
|
-
} catch {
|
|
212
|
-
// ignore
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (observations.length < 2) {
|
|
217
|
-
return { ...unknown, observations };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const [first, second] = observations;
|
|
221
|
-
const externalIp = first.ip;
|
|
222
|
-
if (first.port === second.port) {
|
|
223
|
-
return { klass: "endpoint-independent", externalIp, observations, portDelta: null };
|
|
224
|
-
}
|
|
225
|
-
return {
|
|
226
|
-
klass: "symmetric",
|
|
227
|
-
externalIp,
|
|
228
|
-
observations,
|
|
229
|
-
portDelta: second.port - first.port
|
|
230
|
-
};
|
|
231
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file NAT classification via STUN (minimal, dependency-free client).
|
|
3
|
+
*
|
|
4
|
+
* Determines whether the proxy's home NAT preserves its external UDP port
|
|
5
|
+
* across destinations (endpoint-independent / "cone") or assigns a new port
|
|
6
|
+
* per destination ("symmetric"). This decides whether the fixed-UDP-port +
|
|
7
|
+
* UPnP mapping (the WebRTC reachability of proxy 2.9.18) is sufficient:
|
|
8
|
+
*
|
|
9
|
+
* - endpoint-independent → a static mapping works; no port prediction needed.
|
|
10
|
+
* - symmetric → the mapped port differs per viewer; WebRTC needs
|
|
11
|
+
* port prediction (a later roadmap step).
|
|
12
|
+
*
|
|
13
|
+
* Method: from ONE local UDP socket, send a STUN Binding Request to TWO
|
|
14
|
+
* different public STUN servers (different destinations) and compare the
|
|
15
|
+
* reported external port. Same port → endpoint-independent; different →
|
|
16
|
+
* symmetric. This is the modern, reliable test — unlike RFC 3489's CHANGE-
|
|
17
|
+
* REQUEST classification, it needs no special STUN-server support (works with
|
|
18
|
+
* Google/Cloudflare STUN). The two queries MUST share one socket: a fresh
|
|
19
|
+
* socket would get its own NAT mapping and make even a cone NAT look symmetric.
|
|
20
|
+
*
|
|
21
|
+
* Strictly best-effort: never throws; returns `klass: "unknown"` if the probes
|
|
22
|
+
* fail (STUN blocked, offline). Used for diagnostics/telemetry and to gate the
|
|
23
|
+
* future WebRTC port-prediction work.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import dgram from "node:dgram";
|
|
27
|
+
import crypto from "node:crypto";
|
|
28
|
+
|
|
29
|
+
const STUN_MAGIC_COOKIE = 0x2112a442;
|
|
30
|
+
const STUN_BINDING_REQUEST = 0x0001;
|
|
31
|
+
const ATTR_XOR_MAPPED_ADDRESS = 0x0020;
|
|
32
|
+
const ATTR_MAPPED_ADDRESS = 0x0001;
|
|
33
|
+
|
|
34
|
+
// Public STUN servers from DIFFERENT operators, so the destination genuinely
|
|
35
|
+
// differs between the two queries (required for the symmetric test).
|
|
36
|
+
const DEFAULT_STUN_SERVERS = [
|
|
37
|
+
{ host: "stun.l.google.com", port: 19302 },
|
|
38
|
+
{ host: "stun.cloudflare.com", port: 3478 },
|
|
39
|
+
{ host: "stun1.l.google.com", port: 19302 }
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const QUERY_TIMEOUT_MS = 4000;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {object} NatObservation
|
|
46
|
+
* @property {string} server - "host:port" that was queried.
|
|
47
|
+
* @property {string} ip - Reflexive external IP reported.
|
|
48
|
+
* @property {number} port - Reflexive external port reported.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {object} NatClassification
|
|
53
|
+
* @property {"endpoint-independent" | "symmetric" | "unknown"} klass
|
|
54
|
+
* @property {string | null} externalIp - External IP (from the first success).
|
|
55
|
+
* @property {NatObservation[]} observations
|
|
56
|
+
* @property {number | null} portDelta - port(2nd) - port(1st) when symmetric, else null.
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a 20-byte STUN Binding Request with a random 96-bit transaction id.
|
|
61
|
+
*
|
|
62
|
+
* @returns {Buffer}
|
|
63
|
+
*/
|
|
64
|
+
function buildBindingRequest() {
|
|
65
|
+
const msg = Buffer.alloc(20);
|
|
66
|
+
msg.writeUInt16BE(STUN_BINDING_REQUEST, 0);
|
|
67
|
+
msg.writeUInt16BE(0, 2); // message length (no attributes)
|
|
68
|
+
msg.writeUInt32BE(STUN_MAGIC_COOKIE, 4);
|
|
69
|
+
crypto.randomFillSync(msg, 8, 12); // transaction id
|
|
70
|
+
return msg;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse the reflexive address from a STUN response, preferring
|
|
75
|
+
* XOR-MAPPED-ADDRESS and falling back to MAPPED-ADDRESS. IPv4 only.
|
|
76
|
+
*
|
|
77
|
+
* @param {Buffer} msg
|
|
78
|
+
* @returns {{ ip: string, port: number } | null}
|
|
79
|
+
*/
|
|
80
|
+
function parseMappedAddress(msg) {
|
|
81
|
+
if (msg.length < 20) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
let offset = 20;
|
|
85
|
+
while (offset + 4 <= msg.length) {
|
|
86
|
+
const type = msg.readUInt16BE(offset);
|
|
87
|
+
const length = msg.readUInt16BE(offset + 2);
|
|
88
|
+
const valueStart = offset + 4;
|
|
89
|
+
if (valueStart + length > msg.length) {
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (type === ATTR_XOR_MAPPED_ADDRESS || type === ATTR_MAPPED_ADDRESS) {
|
|
94
|
+
const family = msg.readUInt8(valueStart + 1);
|
|
95
|
+
if (family === 0x01) {
|
|
96
|
+
// IPv4
|
|
97
|
+
const xored = type === ATTR_XOR_MAPPED_ADDRESS;
|
|
98
|
+
const rawPort = msg.readUInt16BE(valueStart + 2);
|
|
99
|
+
const port = xored ? rawPort ^ (STUN_MAGIC_COOKIE >>> 16) : rawPort;
|
|
100
|
+
const addrBytes = [
|
|
101
|
+
msg.readUInt8(valueStart + 4),
|
|
102
|
+
msg.readUInt8(valueStart + 5),
|
|
103
|
+
msg.readUInt8(valueStart + 6),
|
|
104
|
+
msg.readUInt8(valueStart + 7)
|
|
105
|
+
];
|
|
106
|
+
const cookieBytes = [
|
|
107
|
+
(STUN_MAGIC_COOKIE >>> 24) & 0xff,
|
|
108
|
+
(STUN_MAGIC_COOKIE >>> 16) & 0xff,
|
|
109
|
+
(STUN_MAGIC_COOKIE >>> 8) & 0xff,
|
|
110
|
+
STUN_MAGIC_COOKIE & 0xff
|
|
111
|
+
];
|
|
112
|
+
const ipParts = addrBytes.map((b, i) => (xored ? b ^ cookieBytes[i] : b));
|
|
113
|
+
return { ip: ipParts.join("."), port };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Attributes are padded to 4-byte boundaries.
|
|
118
|
+
offset = valueStart + length + ((4 - (length % 4)) % 4);
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Send one Binding Request to `host:port` over the given socket and resolve
|
|
125
|
+
* with the reflexive address, or null on timeout / parse failure.
|
|
126
|
+
*
|
|
127
|
+
* @param {import("node:dgram").Socket} socket
|
|
128
|
+
* @param {string} host
|
|
129
|
+
* @param {number} port
|
|
130
|
+
* @returns {Promise<{ ip: string, port: number } | null>}
|
|
131
|
+
*/
|
|
132
|
+
function queryStun(socket, host, port) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
let settled = false;
|
|
135
|
+
const onMessage = (msg) => {
|
|
136
|
+
if (settled) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const parsed = parseMappedAddress(msg);
|
|
140
|
+
if (!parsed) {
|
|
141
|
+
return; // ignore unrelated datagrams; let the timeout fire if needed
|
|
142
|
+
}
|
|
143
|
+
settled = true;
|
|
144
|
+
socket.off("message", onMessage);
|
|
145
|
+
resolve(parsed);
|
|
146
|
+
};
|
|
147
|
+
socket.on("message", onMessage);
|
|
148
|
+
socket.send(buildBindingRequest(), port, host, (err) => {
|
|
149
|
+
if (err && !settled) {
|
|
150
|
+
settled = true;
|
|
151
|
+
socket.off("message", onMessage);
|
|
152
|
+
resolve(null);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const timer = setTimeout(() => {
|
|
156
|
+
if (!settled) {
|
|
157
|
+
settled = true;
|
|
158
|
+
socket.off("message", onMessage);
|
|
159
|
+
resolve(null);
|
|
160
|
+
}
|
|
161
|
+
}, QUERY_TIMEOUT_MS);
|
|
162
|
+
timer.unref?.();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Classify the host's NAT by comparing the external port seen by two different
|
|
168
|
+
* STUN servers over a single local socket.
|
|
169
|
+
*
|
|
170
|
+
* @param {{ servers?: Array<{ host: string, port: number }> }} [options]
|
|
171
|
+
* @returns {Promise<NatClassification>}
|
|
172
|
+
*/
|
|
173
|
+
export async function classifyNat({ servers = DEFAULT_STUN_SERVERS } = {}) {
|
|
174
|
+
/** @type {NatClassification} */
|
|
175
|
+
const unknown = { klass: "unknown", externalIp: null, observations: [], portDelta: null };
|
|
176
|
+
|
|
177
|
+
const socket = dgram.createSocket("udp4");
|
|
178
|
+
try {
|
|
179
|
+
await new Promise((resolve, reject) => {
|
|
180
|
+
socket.once("error", reject);
|
|
181
|
+
socket.bind(0, () => {
|
|
182
|
+
socket.off("error", reject);
|
|
183
|
+
resolve(undefined);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
} catch {
|
|
187
|
+
try {
|
|
188
|
+
socket.close();
|
|
189
|
+
} catch {
|
|
190
|
+
// ignore
|
|
191
|
+
}
|
|
192
|
+
return unknown;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** @type {NatObservation[]} */
|
|
196
|
+
const observations = [];
|
|
197
|
+
try {
|
|
198
|
+
for (const server of servers) {
|
|
199
|
+
const result = await queryStun(socket, server.host, server.port);
|
|
200
|
+
if (result) {
|
|
201
|
+
observations.push({ server: `${server.host}:${server.port}`, ip: result.ip, port: result.port });
|
|
202
|
+
}
|
|
203
|
+
// Stop once we have two observations from two different destinations.
|
|
204
|
+
if (observations.length >= 2) {
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
} finally {
|
|
209
|
+
try {
|
|
210
|
+
socket.close();
|
|
211
|
+
} catch {
|
|
212
|
+
// ignore
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (observations.length < 2) {
|
|
217
|
+
return { ...unknown, observations };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const [first, second] = observations;
|
|
221
|
+
const externalIp = first.ip;
|
|
222
|
+
if (first.port === second.port) {
|
|
223
|
+
return { klass: "endpoint-independent", externalIp, observations, portDelta: null };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
klass: "symmetric",
|
|
227
|
+
externalIp,
|
|
228
|
+
observations,
|
|
229
|
+
portDelta: second.port - first.port
|
|
230
|
+
};
|
|
231
|
+
}
|