Skip to content

Repository files navigation

airlock

CI

A torrent finishes. Before you can open it, airlock has already moved it somewhere you can't, looked inside it, and decided whether to give it back.

That badge means the suite passed on a Windows runner that is not the author's machine. It does not mean the pipeline was exercised against a real torrent client, a real download, or a real antivirus decision — see What CI checks, and the much larger thing it does not.

It is a completion hook for Windows torrent clients (µTorrent, qBittorrent). When a download completes, the files are moved into quarantine first, marked the way a browser download is marked, blocked from running, expanded if they are archives, scanned with Microsoft Defender, and released back to your downloads folder only if nothing objected. If something did, you get a plain-text notice in the folder saying exactly why and exactly how to get the files out.

µTorrent / qBittorrent
        │  torrent finished
        ▼
   airlock ──▶ quarantine ──▶ mark ──▶ deny execute ──▶ extract ──▶ scan ──▶ decide
                                                                                  │
                                              ┌───────────────────────────────────┴────┐
                                              ▼                                        ▼
                                         released                            held, with a notice
                                     (exit code 0)                             (exit code 1)

It adds about a second to a typical download, and it does three things nothing else on your machine does.

Why you need this

1. Windows trusts a torrent more than it trusts your browser

Every file your browser or mail client saves gets a mark-of-the-web: a tiny tag that makes SmartScreen ask before an unknown .exe runs, makes Office open a document in Protected View, and makes Windows Script Host think twice about a .js. That tag is the single most effective consumer-grade defence Windows has, and it has been there since Internet Explorer 6.

No BitTorrent client writes it. A setup.exe that arrives over BitTorrent is treated by Windows as something you made — double-click it and it runs, with no prompt at all. The same file downloaded from a web page would have been challenged.

Without airlock: the repack's setup.exe runs the moment you double-click, no questions. With airlock: every file is marked as it comes out of quarantine, so SmartScreen and Protected View do their jobs on torrent downloads for the first time.

2. You cannot see inside an archive before you extract it

A torrent's file list is public before a byte transfers — but the contents of the .zip or .7z inside it are not. "Extract it and see what's in there" is precisely the operation an attacker counts on: an entry named ..\..\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\x.exe that lands outside the folder you extracted into, or 42 KB that unpacks to 4.5 PB and fills the drive, or a second archive inside the first that the scanner never opened.

Without airlock: you find out when you extract, in your live downloads folder. With airlock: archives are expanded in quarantine, under bounds, before anything is released — hostile entry names are refused, expansion is capped, nesting is limited, and every file that comes out is marked, blocked from running, and scanned as a file on disk, not as bytes inside a container the scanner may or may not have looked into.

3. Nothing tells you when the scanner stopped watching

Defender's real-time protection is a setting. It gets switched off by a third-party antivirus that lapsed, by a "performance" tweak, by an installer, by a Group Policy you never saw — and Windows switches it back on by itself, so neither state can be relied on. A pipeline that treated "no scanner answered" as "nothing was found" would report everything clean while doing nothing.

Without airlock: a download that nothing scanned looks exactly like one that passed. With airlock: the pipeline fails closed. A scanner that is missing, broken, or timed out holds the payload exactly as a detection does, and the notice says which. Release is the absence of reasons to hold, never a positive match — a check that fails to run leaves its reason in place instead of removing an obstacle.

What this buys you, concretely

real-time protection on (most machines) real-time protection off
mark-of-the-web on every file ✔ new — no client does this ✔
archives inspected before you extract them ✔ new — RTP sees the archive, not its contents until you extract ✔
extracted contents scanned on disk before release ✔ new ✔ the only scan that happens
fails closed when nothing could scan ✔ ✔ the whole point
a written record of every decision, and a notice when held ✔ ✔

And what it costs you, measured on this machine (see What it costs): a 1.6 GB movie file — about a second; a 624 MB game repack zip, 411 files extracted and scanned — five seconds; 1,000 small files — two seconds.

Who this is for, and who it is not for

For: anyone who downloads torrents on Windows and keeps what they download — movies, shows, music, books, games, software. Daily use is the design point: airlock tells you twice — a notification when it starts checking and one with the outcome — and leaves a note in the folder only when something is wrong. Nothing else to read, nothing to click.

Not yet for seeders who must keep seeding without interruption. airlock moves the payload into quarantine while it checks, and moves it back when it passes. Your client sees the files vanish for those seconds and reports "files missing"; the files come back on their own, the client's state does not — you resume or force-recheck the torrent. For download-and-keep use this is invisible. For a private tracker with ratio rules it is a real cost, and the honest answer today is: document it, and put an in-place mode on the roadmap. If seeding without interruption is non-negotiable for you, wait for that.

Requirements

  • Windows 10 or 11.
  • Microsoft Defender as the antivirus actually in use. Real-time protection is not required — airlock drives the on-demand scanner (MpCmdRun.exe), which works with RTP off. But Defender is the only thing airlock drives: on a machine where a third-party antivirus has taken over, Defender stands down, airlock detects that and says so, and payloads are held. See If another antivirus has taken over.
  • An NTFS destination. Quarantine relies on file ACLs and alternate data streams; a FAT32 or exFAT drive has neither, and a payload there is held for exactly that reason.
  • Your downloads folder not in Defender's exclusion list. Exclusions silence the on-demand scan too, and airlock cannot see them without administrator rights. Check with an elevated PowerShell: Get-MpPreference | Select-Object -ExpandProperty ExclusionPath.
  • Rust 1.93+ to build from source. A prebuilt binary is attached to each release — see Download — so a toolchain is optional.

Install

Download

airlock.exe and SHA256SUMS are attached to each release. They are built on a GitHub runner from the tagged commit, and the same test suite runs there before the binary is uploaded.

The binary is not code-signed, and Windows will challenge it — "Windows protected your PC", unknown publisher, the whole ceremony. That is SmartScreen doing precisely the job this program exists to make it do on your torrents, aimed at this program, and there is no honest way to complain about it. A code-signing certificate costs money this project does not have; what is offered instead is a build no human touched, and two ways to check the file you have is that build.

The hash, which tells you the file arrived intact:

(Get-FileHash airlock.exe -Algorithm SHA256).Hash.ToLower()
Get-Content SHA256SUMS   # the same hash, published beside it

The provenance, which tells you where it came from — that this exact binary was produced by this repository's release workflow from a tagged commit, and not by someone who merely published a file with a matching hash:

gh attestation verify airlock.exe --repo corecompiled/airlock

Then place it and register the notification id — the last five lines of the by-hand block below, skipping cargo build.

Build from source

From a clone of this repository:

.\install.ps1

That builds the release binary, copies it to %LOCALAPPDATA%\Programs\airlock\airlock.exe, registers the notification id, and prints the exact line to paste into your client. Or by hand:

cargo build --release
mkdir "$env:LOCALAPPDATA\Programs\airlock" -Force
copy target\release\airlock.exe "$env:LOCALAPPDATA\Programs\airlock\airlock.exe"
# notifications: register the AppUserModelId the binary notifies under
New-Item -Path 'HKCU:\Software\Classes\AppUserModelId\Airlock.Hook' -Force | Out-Null
Set-ItemProperty -Path 'HKCU:\Software\Classes\AppUserModelId\Airlock.Hook' -Name DisplayName -Value 'airlock'

Install it somewhere stable. target\ is git-ignored and cargo clean wipes it, so pointing a torrent client there is asking for a broken hook later. The path you choose is the path every hold notice will print back to you.

Wire it into a client

µTorrent — Options → Preferences → Advanced → Run Program, "Run this program when a torrent finishes":

"%LOCALAPPDATA%\Programs\airlock\airlock.exe" --payload-dir "%D" --payload-name "%F" --payload-kind "%K" --infohash "%I" --name "%N"

µTorrent describes a finished torrent as a directory plus a file name, and which of the two is the payload depends on the torrent: for a multi-file torrent %D is the torrent's own folder and is the payload, while %F names one file inside it. %K says which case applies, so all three are passed and the hook resolves them.

µTorrent only expands these % placeholders when it launches a program directly. Wrap the call in a .bat file and they arrive empty.

µTorrent's %D ends in a backslash — C:\Users\you\Downloads\ — and under Windows' normal command-line rules "…\" is an escaped quote, so "%D" would swallow every flag after it. airlock reads its own command line and takes a \" that closes a quoted argument as exactly that, so the line above works as written. 1.0.0 and 1.0.1 did not: on every single-file torrent they exited with "payload does not exist", left no notice, wrote their log into C:\Users\you\.airlock\ instead of the downloads folder, and the download landed unexamined. If you have that stray folder, it is safe to delete; upgrade. (1.1.0 moved the log to %LOCALAPPDATA%\airlock\ for exactly this reason — see Where things are kept.)

Trying that line by hand in PowerShell 5.1 will not reproduce what µTorrent does: it drops the empty "" argument to a native program entirely. The hook tolerates that — a --payload-name with no value reads as no name — but if you want a faithful test, run it through cmd.exe.

qBittorrent — Options → Downloads → "Run external program on torrent finished". Its %F is already the full content path:

"%LOCALAPPDATA%\Programs\airlock\airlock.exe" --payload "%F" --destination "%D" --infohash "%I" --name "%N"

What is verified, and what is not. µTorrent is the client this is tested against — real torrents, on a real machine, end to end. The qBittorrent line above is checked only as far as parsing: docs.rs runs it with its placeholders filled and asserts the binary accepts it. It has never been run against the real client. The specific unknown is whether qBittorrent's %D arrives with a trailing backslash the way µTorrent's does — airlock splits its own command line precisely so that either answer works, but "should survive" is not "was watched surviving", and mis-reading µTorrent's line is what broke 1.0.0 and 1.0.1. If you run qBittorrent, an issue saying it worked — or did not — is genuinely useful.

Three flags you may want, depending on what you download and how much you want to hear about it:

  • --allow-unsupported-archives if you download ISO or CAB releases, or RAR archives using the few features this build does not decode. See Archive formats for which, and for why RAR itself no longer needs it.
  • --allow-oversized-archives if you download very large repacks. See Large releases.
  • --quiet if you do not want the two desktop notifications described next. AIRLOCK_TOAST=off in the environment does the same.

What you'll see

Two desktop notifications per download. The first, the moment the check starts:

airlock is checking Some.Release — The files are in quarantine until the check finishes; your client may say "files missing" until then. They go back to C:\Users\You\Downloads.

and the second with the outcome — Released: Some.Release ("Nothing found in 1.2 s. The files are back in …"), Held: Some.Release (the first reason, and where the note is), or airlock could not check Some.Release if the hook itself failed. The outcome notification opens the folder when clicked. A 6.5 GB ISO takes seven minutes, all of it Defender, and without the first notification there was no way to tell that from the hook never having run — which is how 1.0.0 and 1.0.1 shipped broken. --quiet turns both off; the management commands below never show them. Notifications need the AppUserModelId that install.ps1 registers (see Install); without it the run is logged as "toast shown" and nothing appears.

A clean payload lands where the client would have put it, marked and scanned, and quarantine empties itself. Beyond the two notifications there is nothing to read.

A held payload does not. Because the files have moved out of the destination and torrent clients discard a hook's output, a plain-text notice is left in their place, named AIRLOCK HELD - <name> - <id>.txt so it sorts to the top of the folder:

AIRLOCK HELD THIS DOWNLOAD

    Some.Release

It finished downloading, but it was not released into this folder.
The files are in quarantine, where they cannot be run.

Why it was held:
  - a scanner identified Virus:DOS/EICAR_Test_File

The files are here:
    C:\Users\You\Downloads\.airlock\2dad0ac5c9ff4cf3839f366a885ffb18

The full report is here:
    C:\Users\You\Downloads\.airlock\reports\2dad0ac5c9ff4cf3839f366a885ffb18.json

1 file(s), 68 bytes. Quarantine id 2dad0ac5c9ff4cf3839f366a885ffb18.

If you decide the download is safe, release it with:

    "C:\Users\You\AppData\Local\Programs\airlock\airlock.exe" --destination "C:\Users\You\Downloads" --release-held 2dad0ac5c9ff4cf3839f366a885ffb18

That checks the files again first. If it holds them a second time
and you still want them, add --force.

Do not drag the files out of quarantine yourself. The block on
running them is set on the files rather than the folder, so they
carry it with them and will refuse to run, with nothing to say why.
Releasing them lifts it; moving them by hand does not.

Delete this notice once you have dealt with it.

The command is printed with the executable's full path, the destination and the id already filled in, because a command you have to assemble yourself is one you skip in favour of dragging the files out — which is the one way out that leaves them unable to run.

If the hook itself breaks, you get the same notice with different wording — "could not finish checking" rather than a verdict — because a crash in this program and a dangerous download call for different responses. And if a run dies outright — the machine sleeps mid-scan, the process is killed — the next run of any airlock command finds the abandoned entry and writes that notice for it, so a download can never simply vanish.

Managing quarantine

airlock --destination C:\Users\You\Downloads --list-held
airlock --destination C:\Users\You\Downloads --release-held <ID>
airlock --destination C:\Users\You\Downloads --release-held <ID> --force
airlock --destination C:\Users\You\Downloads --discard-held <ID>

--list-held ends with log: <path> — the per-user log every run writes to (see Where things are kept).

--release-held re-runs the whole pipeline over the quarantined files rather than just moving them, so a payload held only for its size comes out once you pass --allow-oversized-archives, and one held only for an ISO comes out with --allow-unsupported-archives, while a detection stays put. --force skips the re-check for a false positive, printing what it is overriding first.

Do not drag files out of .airlock by hand. Quarantined files carry an explicit deny-execute ACE, and a move within a volume carries that ACE along — you would get files that silently refuse to run. Only releasing through this program lifts it.

Archive formats

Containers are identified by their first bytes, never by extension, and then either opened under bounds or recorded as unexamined:

format opened? what happens
zip yes expanded under bounds, entries name-checked
7z yes expanded under bounds, entries name-checked
tar yes expanded under bounds, entries name-checked
gzip yes decompressed under bounds (and the tar inside, if any)
rar yes expanded under bounds, entries name-checked — RAR 1.5–4.x and RAR 5, stored and compressed, multi-volume sets, solid archives; see below for what stays held
iso9660 no held as unexamined, unless --allow-unsupported-archives
cab no held as unexamined, unless --allow-unsupported-archives
xz no held as unexamined, unless --allow-unsupported-archives
zstd no held as unexamined, unless --allow-unsupported-archives
bzip2 no held as unexamined, unless --allow-unsupported-archives

RAR matters, because most scene releases and game repacks are RAR sets, and it is opened: both generations of the format (RAR 1.5–4.x and RAR 5), stored and compressed members, solid archives, and multi-volume sets read through from the first volume (.part01.rar onwards, or .rar then .r00, .r01, …). The container is parsed in this tree and the compressed data is decoded by compcol, a pure-Rust, forbid(unsafe_code) decoder — see ARCHITECTURE.md for why that bar mattered. Every member that comes out is checked against the CRC the archive recorded for it; one that fails is withdrawn and reported, and the payload holds. What a RAR cannot do is be read without its later volumes: a set whose next part is missing is reported as incomplete, and held.

Some RAR archives still hold, for stated reasons rather than format gaps:

  • Encrypted — headers or members — holds, as every encrypted archive does.
  • Features this build does not decode are reported as unexamined with the feature named: the ARM executable filter, RAR 1.x/2.x compression, RAR 7 compression and dictionary sizes, a dictionary larger than the in-memory bound below, a RAR 5 member of undeclared size. These are rare in practice and --allow-unsupported-archives reaches them, on the same reasoning as the "no" rows: Defender's scan reads inside the archive itself.
  • A compressed member over 512 MiB (packed or unpacked) stops extraction as "too large", which --allow-oversized-archives waives and --max-rar-member-bytes moves. This is a memory bound, not a disk one: a compressed RAR member is held in memory while it decodes (all of it for RAR 1.5–4.x, a block's worth for RAR 5, and the format does not bound a block). The members of a solid RAR 5 archive share one decoder and count against it together. Stored members — which is what video releases are — stream and are not subject to it.

--allow-unsupported-archives releases a payload whose only unopened containers are formats from the "no" rows, or RAR archives using a feature named above, on the strength of the scan: Defender's on-demand scan reads inside archives itself (verified here — EICAR inside an unopened zip is detected under --no-extract; Microsoft lists RAR, ISO and CAB among the formats its engine unpacks), so the scan still covers the contents. What you give up is airlock's own checks on those contents — hostile entry names, bombs, nested archives — and the mark-of-the-web on files you later extract. The flag reaches nothing else: an encrypted, damaged, or too-deeply-nested archive, an incomplete volume set, a volume with no room, and a run with --no-extract all still hold.

Large releases

Repacked games routinely run to tens of gigabytes and stop extraction on a size bound, which holds them under the default policy. --allow-oversized-archives releases a payload whose extraction stopped only because it was too large.

It does not relax the expansion-ratio bound, so a zip bomb is still held — a repack is huge but already compressed, near 1:1, whereas 42.zip is 42 KB expanding to petabytes. The cost is real though: entries that were never unpacked were never name-checked either, leaving the scan over the quarantine tree as the remaining cover.

A compressed RAR member larger than 512 MiB is also "too large", because it is decoded in memory (see Archive formats); --max-rar-member-bytes raises that bound on a machine with the RAM for it, and the waiver covers it otherwise. Stored RAR members — video releases — are not affected.

Changing what gets held

The default policy holds on anything it could not rule out. Five flags change that, and only one of them makes the tool more conservative:

flag effect what it costs you
--strict also hold any payload containing a Windows executable holds installers and games, which torrents legitimately carry. For a destination that should only ever receive media.
--allow-unsupported-archives release when the only unopened containers are formats this build cannot open, or RAR archives using a feature it does not decode airlock's own checks on those contents; Defender's scan remains. See Archive formats
--allow-oversized-archives release when extraction stopped only on a size bound entries never unpacked were never name-checked; see Large releases
--allow-unscanned release even when no scanner could be used switches off the protection that matters most on a machine with real-time protection already disabled. A scan that ran and failed or timed out is still a hold. See If another antivirus has taken over
--no-extract skip archive extraction entirely every container is reported unexamined, so under the default policy the payload is held — this is not a way to release faster

A detection is held under every combination of these. There is no flag that releases a payload a scanner identified as a threat; --force on a specific quarantine id is the only override, and it names what it is overriding first.

--allow-unscanned relaxes only "no scanner ran". A scan that ran and found something, or ran and did not finish, is a different fact, and no waiver reaches it.

Extraction bounds can be moved rather than waived, which is usually the better answer for a machine that handles unusually large or deeply nested releases:

--max-total-bytes <BYTES>    # total extraction output allowed   (default 16 GiB)
--max-ratio <N>              # expansion of output over input     (default 200)
--max-depth <N>              # archives within archives           (default 3)
--extract-timeout <SECS>     # wall clock for extraction          (default 600)
--max-rar-member-bytes <BYTES>  # largest compressed RAR member decoded in memory (default 512 MiB)
--scan-timeout <SECS>        # wall clock for the scan            (default 1800)

The extraction budget is additionally clamped to what the volume can actually spare, keeping 2 GiB back, and not started at all with less than 256 MiB above that. If there is not enough room, archives are left unopened and reported as unexamined — which holds the payload, rather than releasing something nobody looked inside.

If another antivirus has taken over

Bitdefender, Kaspersky, Norton and the rest register with Windows Security Center, and Microsoft Defender stands down when one does — into passive mode, or off altogether. MpCmdRun.exe stays on disk either way, so finding the file is not evidence that a scan can happen.

airlock reads the values Defender keeps under its own registry key — PassiveMode, ForceDefenderPassiveMode, DisableAntiVirus, DisableAntiSpyware and IsServiceRunning — and records the answer in every report as one of normal, passive, disabled or unknown. What it does with each:

Defender is what airlock does
normal scans as usual
passive still asks for an on-demand scan, which Microsoft documents as working in that state. If the scan cannot run, the payload is held as unscanned rather than as a failed scan
disabled does not launch a scan that cannot succeed; the payload is held as never scanned
unknown scans anyway, and a failure holds as a failed scan — a waiver granted because the registry would not answer is a waiver granted on no evidence

That distinction is the entire point of reading the mode. "No scanner was available" is waivable with --allow-unscanned; "a scan ran and failed" is waivable by nothing. Without it, a machine that simply runs a different antivirus would have every download held with no flag that helped, and --force on each quarantine id as the only way out.

What --allow-unscanned means here: released without airlock's scan. The other product's real-time protection is whatever it is — airlock does not drive it, cannot ask it for a verdict and does not report on it. Quarantine, mark-of-the-web, deny-execute and archive inspection all still happen; only the scan step is waived.

This path has not been run on a real machine. No machine with a third-party antivirus was available to this project. Every branch of the table above is unit-tested, and on a Defender-normal machine airlock's reading is checked against Get-MpComputerStatus, which asks a different source. But the step from "Bitdefender is installed" to those particular registry values is taken from Microsoft's documentation, not from a measurement here. If you run such a machine and airlock gets it wrong, that is worth reporting — see SECURITY.md.

Reports

Every run that got as far as quarantine writes a JSON report, whatever it decided. --json prints it to stdout as well, and --report <PATH> puts it somewhere other than the vault.

{
  "schema": "airlock.report/3",
  "duration_ms": 5020,
  "decision": {
    "disposition": "hold",
    "reasons": [{ "reason": "unexamined_container", "detail": "... (iso9660) not examined: ..." }]
  }
}

The reason field is a stable identifier, so a script can tell why something was held without parsing prose. The complete list: threat_detected, scanner_unavailable, scan_failed, never_scanned, extraction_halted, refused_archive_entry, refused_archive_entries, unexamined_container, executable_present, provenance_not_recorded, execute_deny_failed, run_interrupted, and custom for a hook failure whose cause is in the detail.

Exit codes

The exit code is the integration contract, because a torrent client will not read prose.

code meaning
0 released to the destination, or a management command finished
1 held in quarantine; the report and the notice say why
2 the hook itself failed (including a bad command line); the payload was not released

Code 2 is deliberately distinct from 1. "Held because Defender found something" and "held because this program crashed" both leave the payload in quarantine, but only one of them is a bug. The code describes where the payload is: a report that could not be written after a successful release is a loud warning on stderr, not a 2.

Where things are kept

Quarantine lives in a hidden .airlock beside the destination, which puts it on the same volume — that is what lets intake and release be atomic renames instead of copies.

C:\Users\You\Downloads\.airlock\
    <id>\                       a held payload, plus extracted\ if archives were expanded,
                                and .airlock-in-progress while a run is still working on it
    reports\<id>.json           the full record of one payload's handling

The log is per user, not per folder:

%LOCALAPPDATA%\airlock\airlock.log      every run, rotated at 4 MB, two files deep

It lives there rather than in the vault because it has to exist before the command line has been understood — 1.0.0 and 1.0.1 named the log after --destination, and when µTorrent's line was mis-read the log went into a folder named by garbage — and because one fixed place is one a person can be told about. It is opened before the arguments are parsed, so a command line this program rejects is the first line in it rather than the one run with no log at all. airlock --destination <folder> --list-held prints the path. A run with no LOCALAPPDATA in its environment logs to stderr only and says so there. If you upgraded from 1.0.x, the old .airlock\airlock.log beside each destination is no longer written or read and is safe to delete.

The log level is set with the AIRLOCK_LOG environment variable (debug, off, and so on); the default is info.

What it costs you

Measured on the machine this was built on (Windows 11, NVMe, Defender 4.18, release build), running the hook by hand on copies of real downloads — and, for the ISO, letting µTorrent run it on the real thing — and reading the run time out of the report:

payload what airlock did time
a 1.6 GB .mkv quarantine, mark, deny-execute, scan, release about a second
a 6.5 GB Ubuntu .iso, invoked by µTorrent itself the same — but Defender scans the filesystem inside a disc image 7 min 12 s, all of it the scan; µTorrent shows "files missing" until then
a 624 MB game repack .zip the above, plus extracting 411 files (801 MB) and scanning them 5.0 s (2.8 s extracting, 1.0 s scanning)
1,000 small files quarantine, mark and deny-execute each, scan, release 2.1 s
a hostile zip (traversal entry) held at the first refused entry, nothing written outside quarantine 0.1 s
a zip bomb (196 KB → 200 MB) held at the 200:1 ratio bound after 39 MB 0.4 s
EICAR held, denied execution, notice written 0.1 s

The hook runs asynchronously from the client — your torrent client does not wait for it — so this is time until the files reappear, not time the client is blocked.

These numbers assume real-time protection is on, and the scan row is the reason. Scan duration here does not depend on how much there is to scan. Measured on this machine, one file of random bytes nothing had ever seen before: 106 ms at 4 MB, 105 ms at 100 MB, 104 ms at 1 GB, 107 ms at 3 GB. Flat — which at the top end is 28 GB/s, so Defender is not reading the payload. MpCmdRun.exe driven by hand, outside airlock, agrees: 315 ms for a fresh 1 GB. The likeliest explanation is that Defender is answering from what real-time protection already established as each file was written; what is certain is that it is not re-reading them.

Which means: with real-time protection off — the case where airlock's scan is the only scan that happens — nothing established those files were clean, Defender has to read them, and the table above is not your cost. How much more has not been measured here. The one row where Defender demonstrably read the content is the ISO, because real-time protection does not look inside a disc image: 6.5 GB in 7 min 12 s. Expect something closer to that shape. If you run with real-time protection off and measure it, that number would be worth an issue.

Building and testing

cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings

Unit tests do not prove this works. Exit codes say nothing about what is on the files, so a real change should be checked end to end: a clean payload releasing with no deny-execute ACE surviving, a hostile archive held with nothing escaping the extraction root, and an EICAR file detected and held.

That is not a formality. Every defect this project has found was found by running the thing and looking at the disk — a deny-execute ACE surviving a release, extraction output never getting one, a hold notice deleted by a concurrent run, a flag that released the archives it had declined to open, and — found the day before 1.0 — a real downloaded movie held because the file was read-only and the mark-of-the-web would not write onto it. In each case the unit tests were green throughout.

See CONTRIBUTING.md for how changes to this codebase are expected to be verified.

Status

1.3 (see CHANGELOG.md for the current patch) — recommended for daily download-and-keep use on Windows 10/11 with Microsoft Defender present. The command line, exit codes and report schema (airlock.report/3) are the contract from here on.

Known limitations, all stated in full in SECURITY.md: seeding is interrupted while a payload is checked; ISO/CAB/xz/zstd/bzip2 are identified but not opened, and a few RAR features (ARM filter, RAR 1.x/2.x and RAR 7 compression) are declined by name; a compressed RAR member over 512 MiB is decoded in memory or not at all; Defender exclusions cannot be detected without administrator rights; a third-party antivirus is detected but never driven, and that path has never been run on a machine that has one; the deny-execute ACE is a guard rail against a double-click, not a boundary against an adversary already running as you.

Roadmap

  • In-place mode for seeders — mark, deny-execute and scan at the final path, and move into quarantine only on a hold, so a clean download never disappears from the client.
  • Tell the client — after a release, ask qBittorrent/µTorrent (local WebUI API) to resume and recheck.
  • Streaming compressed RAR — lift the in-memory bound on compressed RAR members once the decoder can yield mid-block; the bound exists because of how the decoder buffers, not because of the format.

Documentation

file what's in it
ARCHITECTURE.md why it is built this way — the pipeline order, each policy decision and what it costs
SECURITY.md what this defends against, what it explicitly does not, and trust boundaries
CONTRIBUTING.md how changes here are expected to be verified
CHANGELOG.md notable changes, with the security-relevant ones marked

License

Dual-licensed under either of Apache License 2.0 or MIT license, at your option.

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages