Releases: emdzej/airlock
Release list
airlock 0.3.1
[0.3.1] — 2026-07-14
Patch release: shipping the companion with a proper app icon so it
stops looking like a generic PkgInfo blank in Finder / Launchpad /
About dialogs.
Added
- App icon for the macOS companion. SVG source at
companion/mac/assets/AppIcon.svg(an airlock-hatch metaphor in
the site's green gradient), rendered toAppIcon.icnsvia a
make-icon.shhelper (librsvg +iconutil). Both files are
committed sobuild.shand CI don't need librsvg — regenerate
locally with./make-icon.shafter editing the SVG. Info.plist
getsCFBundleIconFile: AppIcon; the .app bundle picks it up in
Finder, Launchpad, and (if you unhide the Dock) the Dock. The
menu-bar icon stays as theexternaldrive.badge.wifiSF Symbol —
optimized for 22 pt template rendering.
Version surfaces
- Daemon
main.versionbumped to0.3.1. - Companion
CFBundleShortVersionString/MARKETING_VERSIONbumped
to0.3.1; Preferences window auto-picks it up from Info.plist. - No API or on-wire change — the daemon bump is only so
Version 0.3.1shows correctly in the web UI footer.
Full Changelog: 0.3.0...0.3.1
0.3.0
[0.3.0] — 2026-07-14
Adds a macOS menubar companion app, daemon-side plumbing to push live
drive updates over Server-Sent Events, and a VitePress product site
at airlock.emdzej.pl. No changes to the
existing 0.2.0 REST API — the companion app is a strict additive
layer.
Added — Mac companion app (companion/mac/)
- New menubar-only
AirlockCompanion.app(Swift Package +
LSUIElement). Discovers airlock instances via the new
_airlock._tcpBonjour service; lists their drives; per-drive
actions Mount / Mount and Open in Finder / Unmount /
Reveal in Finder / Copy SMB URL / Eject drive from
airlock; per-host Eject all drives and Open web UI. - Silent mount via
NetFSMountURLSyncfromNetFS.framework—
same DiskArbitration path Finder uses, but no window pops on
mount.NSWorkspace.open(smb://…)was the first draft; it worked
but always opened a window, making "Mount" and "Mount and Open"
indistinguishable. - Auto-unmount reconciliation: when a drive is ejected on the
daemon side (web UI, GPIO button, format), the Mac's local SMB
mount is dropped automatically. Skipped when the host is
currently offline so a Pi reboot doesn't yank live mounts. - Preferences window:
Auto-mount all discovered drives,
Open in Finder after mounting,Start at login(via
SMAppService.mainAppon macOS 13+), plus a footer with the app
version + a GitHub link. - Persistent host store: airlocks the app has seen appear in the
menu even when offline, withoffline · 2h ago. Prune after 30
days of not-seen. Restores drive list from cache on launch so the
menu is populated before the first SSE reconnect finishes. - Coloured host status dot: green live · grey offline · red
error, with the last error inline. - Live updates via SSE with exponential-backoff reconnect and
URLSessionDataDelegate-based frame parsing (bypasses the
URLSession.bytes buffering issue on macOS 13/14). - XcodeGen
project.ymlfor developers who want Xcode.
.xcodeprojstays gitignored. - Distributable DMG via
package.sh. The .app inside is
ad-hoc signed (codesign --force --deep --sign -) so macOS 15
Sequoia's stricter Gatekeeper — which no longer honours the old
right-click Open bypass on unsigned apps — treats it as a normal
first-run prompt instead of "damaged and can't be opened." Not
notarized (no Apple Developer Account); users still need one
xattr -dr com.apple.quarantine /Applications/AirlockCompanion.app
after copying, documented in the companion guide.
Added — daemon
GET /api/events— Server-Sent Events stream broadcasting the
current drive list on every mount-manager change. Initial snapshot
on connect, heartbeat comment lines every 30 s, event types are
wrapped in an envelope ({"type":"drives","drives":[...]}) so we
can add more event kinds without a breaking wire change.mount.Manager.AddListenerso multiple downstream consumers can
subscribe to drive-change events. The Samba writer and the SSE
broadcaster both hook in via this now instead of wrapping the
primary listener.- Additional
_airlock._tcpBonjour advertisement (port 80, TXT
api=1 path=/). Companion apps browse for exactly that instead of
filtering_smb._tcp/_http._tcp.
Added — release workflow
- New
companion-macjob onmacos-14. StampsInfo.plistwith
the release tag, builds the .app viaswift build, packages a
DMG viahdiutil, attachesAirlockCompanion-<tag>.dmg+
sha256 to the same GitHub release the daemon binary and pi-gen
image are already on.
Added — product site (docs/)
- airlock.emdzej.pl — VitePress site
built from the existingdocs/folder. New landing hero page, a
product overview, and a dedicated macOS companion guide sit next
to the existing user guide and install guide (same source, no
duplication). .github/workflows/site.ymlbuilds and publishes to GitHub Pages
on every push tomainthat touchesdocs/**, using
actions/deploy-pages@v4. Custom domain viadocs/public/CNAME;
DNS + Pages source (GitHub Actions) configured out-of-band on
the repo.
Version surfaces
- Web UI footer already links
Version 0.3.0→ the repo. - Companion app's Preferences window shows
Airlock Companion 0.3.0
at the bottom with a matching GitHub link. - Daemon
main.versionbumped to0.3.0; release workflow
overrides via-ldflags -X main.version=<tag>for tagged builds.
Full Changelog: 0.2.0...0.3.0
0.2.0
[0.2.0] — 2026-07-09
Hardware verification + hardening + boot-time tuning pass. All 0.1.0
functionality still applies; this release focuses on making the appliance
faster to boot, safer to compromise, and more visible in the Devices tab.
Added
- Dump a device to a disk image (new). Devices tab gets a Dump…
button; pick a compression (xz / gz / none) and the browser streams
the raw block device down as a downloadable image with a generated
filename (airlock-<label>-<date>.img[.xz|.gz]). Read-only op —
sync()first, thenos.Open("/dev/<parent>"), pipe through the
compressor (xz -c -T 0for parallel or stdlibcompress/gzip),
write straight to the response body. No unmount, so SMB sessions
aren't disturbed. Endpoint:GET /api/devices/{parent}/dump?compression=…. - fsck per partition (new). Devices tab gets a Check button on
every partition with a supported filesystem. Modal picks Check
(read-only) or Repair (auto-fix,-p/-a/-yper tool),
unmounts, runse2fsck/fsck.vfat/fsck.exfat/ntfsfix/
fsck.hfsplus, streams stdout+stderr live via SSE into a scrollable
log in the modal, remounts. Endpoint:
POST /api/partitions/{name}/fsck?mode=check|repair. stage-airlock/06-fast-boot: pi-gen sub-stage that bakes the
same service disables +dtoverlay=disable-btwe ship in
AIRLOCK_FAST_BOOT=1directly into the image. Fresh flashes boot in
~14 s without needing the installer flag.- Release workflow now builds
.img.xz: taggingx.y.znow runs
a second CI job (~45 min) that clones pi-gen inside GitHub Actions,
syncsstage-airlock/, produces the flashable image, and attaches
airlock-<tag>-linux-arm64.img.xz+ sha256 to the same GitHub
Release alongside the binary tarball + install bundle. - Flash OS images to a drive (new). Devices tab gets a Flash…
button; the modal takes a.img,.img.xz, or.img.gzfile, streams
the upload through the appropriate decompressor (xz -dcor stdlib
compress/gzip) straight onto/dev/<parent>in 4 MB blocks with
per-500 ms SSE progress events. Zero intermediate storage — a 3 GB Pi
OS image never lands on the airlock SD, only on the target. Adds
xz-utilsto the installer's package list and the pi-gen stage.
Endpoint:POST /api/devices/{parent}/flash?compression=none|xz|gz
(raw body,text/event-streamresponse). Safety-gated to USB
devices, uses the same quarantine mechanism as format so the daemon
doesn't auto-mount the fresh partition table mid-write. - Mount button on the Devices tab for USB partitions that have a
supported filesystem but aren't currently mounted by airlock. Backed
byPOST /api/partitions/{name}/mount— safety-gated to USB-attached
devices, firesudevadm trigger --action=addand the daemon's normal
event handler picks up the mount. AIRLOCK_FAST_BOOT=1installer flag. Disables services not needed
on a headless appliance (bluetooth, hciuart, triggerhappy, ModemManager,
unused Samba flavoursnmbd/winbind/samba-ad-dc,apt-daily
timers,e2scrub,dphys-swapfile,NetworkManager-wait-online,
rpi-eeprom-update); maskskeyboard-setupandconsole-setup;
disables cloud-init post-first-boot; addsdtoverlay=disable-btto
/boot/firmware/config.txtto skip Bluetooth radio init.AIRLOCK_DISABLE_WIFI=1installer flag. Addsdtoverlay=disable-wifi
after checking thateth0is up — refuses to run if not, so it can't
accidentally orphan a Wi-Fi-only Pi.AIRLOCK_HARDEN_USB=1installer flag plusscripts/modprobe-airlock.conf.
Blocks HID (keyboards / mice) and CDC-* (USB Ethernet / serial) driver
binding at attach time while leaving USB mass storage alone —
neutralizes "BadUSB" / "USB Rubber Ducky" attacks against the console.- systemd seccomp + prctl sandbox on
airlockd.service:
NoNewPrivileges,LockPersonality,RestrictSUIDSGID,
RestrictRealtime,RestrictNamespaces,
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK.
Mount-namespace-inducing directives are deliberately excluded (see
Fixed below). - New docs sections in
docs/install.md: Optional: faster boot
(with per-service explanation, measured impact, and a manual recipe),
Additional hardening (systemd sandbox + optional USB blocklist),
and Flashing OS images to a drive. - "Why the name" section in the README explaining the security
posture — Airlock as the isolation chamber between untrusted media
and your primary machine (nosuid,nodev,noexec, no autorun, BadUSB
neutralization on a headless box, kernel FS-parser CVEs land on the
Pi instead of your laptop). Explicit note that endpoint hygiene
(AV, sandbox) is still the user's job. - Devices tab payload now includes
can_mount,can_relabel, and
can_fsckflags per partition, so the UI shows only the actions
that make sense for each filesystem.
Fixed
- Duplicate mDNS advertisement removed. Samba's built-in mDNS
publisher was shipping a competing_device-info._tcp(and
_smb._tcp) record under the NetBIOS nameAIRLOCKwith
model=MacSamba, which shadowed the record our Avahi service
publishes. Suppressed viamulticast dns register = noin
smb.conf. The Avahi service still advertises
_device-info._tcpwithmodel=MacPro7,1@ECOLOR=226,226,224; the
actual icon Finder draws depends on macOS's undocumented icon
table, and reliably matching a specific "network storage"
appearance (e.g. TrueNAS's) requires macOS-side vendor recognition
we can't advertise. - SMB shares appeared empty when the systemd sandbox created a private
mount namespace.ProtectSystem=strict+ReadWritePathsbind-mounted
/mnt/airlockinto the service's own namespace as slave, so mounts
airlockd made were invisible to smbd running in the host namespace.
MountFlags=shareddidn't fix it (a slave mount only propagates
host→service, not back). Sandbox reduced to seccomp/prctl only —
no private mount namespace, mounts propagate correctly. - Format operation "device or resource busy" during
wipefs— added a
device quarantine so the daemon ignores auto-mount events for the
target device between unmount and mkfs. udevadm trigger --subsystem-match=blockno longer accidentally
matches the Pi's own boot media (mmcblk*) — the udev rule requires
ID_BUS=usb, and every mount pass additionally cross-checks
/proc/self/mountinfobefore touching a device.- Windows-friendly Copy button on the Mounts tab now sends
\\host\shareon Windows andsmb://host/shareeverywhere else.
Boot-time measurements (Pi 4 / Trixie reference hardware)
| Configuration | multi-user.target reached |
|---|---|
| Stock Pi OS Lite Trixie | 24.0 s |
+ AIRLOCK_FAST_BOOT=1 |
14.2 s |
+ AIRLOCK_DISABLE_WIFI=1 |
13.7 s |
airlock 0.1.0
[0.1.0] — 2026-07-09
First public tag. Airlock is a network card reader appliance for the
Raspberry Pi 4: plug in USB media, expose it over SMB and a small web UI,
manage it (browse / upload / format / relabel) from any device on your LAN.
Added — daemon
- Netlink udev subscription that auto-mounts USB block devices under
/mnt/airlock/<name>with per-filesystem mount options for FAT32,
exFAT, NTFS (kernelntfs3), ext2/3/4, HFS+ (read-only), and ISO9660. - Multi-partition drives: each partition becomes its own share; hybrid-ISO
layouts are handled by skipping partitions when the parent disk is
already mounted as a filesystem. - Read-only detection: media flagged read-only in
/sys/class/block/*/ro
(write-protect switch, iso9660, etc.) is mounted RO and refuses writes
via HTTP / SMB. - Safety guards against ever exposing the Pi's own boot media: udev rule
matches only USBsd*devices, and every mount pass cross-checks
/proc/self/mountinfofor prior mounts. - Dynamic Samba shares:
airlockdregenerates a single include file at
/etc/samba/smb.conf.d/airlock.confon each drive change and calls
smbcontrol reload-config— nosmbdrestart. - Startup state recovery: on daemon restart, stale
/mnt/airlock/*
mounts are force-unmounted andudevadm trigger --action=add --subsystem-match=blockre-fires ADD events for currently attached
devices. - Owner lookup: the guest Samba
force useris resolved at start from
UID 1000, so the same binary works on bothpi-gen-built images
(airlock) and generic Raspberry Pi OS installs (pi,emdzej, …). - Physical GPIO controls: button on GPIO 17 (BCM) triggers a global
eject; LED on GPIO 27 reflects state (off / solid / fast-blink busy)
via a small state machine. Fails soft when no chip / no wiring. - Avahi advertisement of
_smb._tcp,_http._tcpand
_device-info._tcp— the last one carriesmodel=TimeCapsule6,106
so macOS Finder renders the host as a network drive instead of a
generic computer. sync()-on-shutdown; no automatic unmount on shutdown so a systemd
restart never yanks a user's data.
Added — HTTP UI (http://<host>.local/)
- Mounts tab: current shares with per-share Eject, clickable
smb://…links, and a Copy button that puts the platform-appropriate
form on the clipboard (\\host\shareon Windows,smb://…elsewhere). - Devices tab: enumerates every USB block device via
lsblk -bJ,
showing model, vendor, serial, size, partition-table type, and every
partition (mounted or not). Per-device Eject / Format… actions,
per-partition Rename (volume label). - File browser at
/drives/{share}/: breadcrumbs, sort dirs-first,
download links, delete + rename with type-to-confirm dialogs, mkdir.
Upload accepts individual files, whole folders (webkitdirectory),
and drag-drop of files or folders — folder tree is preserved on the
target. One HTTP request per file with per-file progress; the progress
card has an expandable Details panel showing status of each item. - Format modal (Devices tab) with:
- filesystem picker (FAT32 / exFAT / NTFS / ext4) auto-selected by
device size (FAT32 <32 GB, exFAT ≥32 GB); - new-label field with per-FS length limits;
- type-to-confirm safety (matches an existing label, or the literal
wordFORMATif the drive is unlabeled); - SSE-streamed progress (
unmount → wipe → partition → rescan → mkfs → done); - device quarantine while the operation runs, so the daemon does not
auto-mount the fresh partition mid-mkfs.
- filesystem picker (FAT32 / exFAT / NTFS / ext4) auto-selected by
- Volume relabel per partition (Devices tab): unmounts, runs the
FS-specific tool (fatlabel,exfatlabel,ntfslabel,e2label),
triggers udev to remount under the new label. - Live polling: pages fetch state every 3 s and only touch the DOM when
the underlying JSON differs — no flashing reloads, scroll/hover state
preserved. - Version + GitHub link in the footer of every page.
Added — packaging & tooling
pi-gencustom stage (image/pi-gen/stage-airlock/) producing an
arm64 Bookworm image with a read-only root (overlayroot=tmpfs) and
all daemon dependencies preinstalled.- Installer for existing Raspberry Pi OS installs (
scripts/install.sh):
idempotent, downloads a matching arm64 binary from GitHub Releases,
drops the systemd unit / udev rule / Samba / Avahi configuration,
backs up any existingsmb.confbefore overwriting, enables and
starts everything. Environment overrides for version, custom URL,
local binary path, and install prefix. - Manual install guide (
docs/install.md) covering prerequisites,
quick-install one-liner, step-by-step manual install, macOS/Linux/
Windows client instructions, GPIO wiring diagram, uninstall, and
troubleshooting. - GitHub Actions CI (
.github/workflows/ci.yml):go vet,go test -race, host build, cross-compile arm64, andshellcheckon the
installer. - GitHub Actions Release (
.github/workflows/release.yml): triggered
by any<major>.<minor>.<patch>tag (novprefix required),
publishesairlockd-<tag>-linux-arm64.tar.gz+ sha256, an offline
install bundle, and the rawinstall.shon the GitHub Release.
Known limitations
- Not tested on Raspberry Pi 5 (would need
ChipNameconfig override
for GPIO — Pi 5 usesgpiochip4). - Read-only root is defined in the pi-gen stage but the full image
build (make image) has not yet been round-tripped on a fresh Pi. - Corporate networks with mDNS filtering or 802.1X-controlled Ethernet
are intentionally out of scope. Home / small-office LANs only. - Multi-partition creation, live resize, and encrypted volumes
(LUKS / BitLocker) are out of scope by design.
Full Changelog: https://github.com/emdzej/airlock/commits/0.1.0