diff --git a/APPIMAGE.md b/APPIMAGE.md index e6c71eb..cf32ba8 100644 --- a/APPIMAGE.md +++ b/APPIMAGE.md @@ -738,11 +738,11 @@ bundling format inside a `.deb` — legitimate, but then use `/opt` + `$ORIGIN` - **apt gets strict in 26.04, not 24.04.** apt 3.2 uses Sequoia; `apt-key` is gone; `Signed-By` must point at a dearmored keyring in `/usr/share/keyrings`, never `/etc/apt/trusted.gpg.d` (which would trust our key for *all* repositories). -- **Desktop-file naming**: `main.cpp:67` uses app id `network.ur.urnetwork` but the +- **Desktop-file naming**: `main.cpp:67` uses app id `com.bringyour.network` but the desktop file is `urnetwork.desktop`. Nothing is broken today (`StartupWMClass` covers window association), but D-Bus activation — the clean way to deliver a `urnetwork://` URI to a running instance — requires the names to match. - Rename to `network.ur.urnetwork.desktop`. + Rename to `com.bringyour.network.desktop`. ### 10d. The recommended alternative, if the direction changes @@ -903,7 +903,7 @@ working with no error anywhere — a changed type is a decode error, and a chang in from `postinst` (IVPN's latent bug). **`install.sh` must run `update-desktop-database` and `gtk-update-icon-cache` itself** — see the §5 callout for why this one silently passes testing and reaches users broken. -- [ ] Rename the desktop file to `network.ur.urnetwork.desktop` to match +- [ ] Rename the desktop file to `com.bringyour.network.desktop` to match `main.cpp:67`'s app id. Harmless today, but D-Bus activation — the clean way to hand a `urnetwork://` URI to a running instance — requires the names to match. - [ ] Autostart via a root-owned **inert template** (`/etc/urnetwork/autostart/`) that diff --git a/MIGRATION.md b/MIGRATION.md index 30b9876..0d8b627 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -37,12 +37,12 @@ a known follow-up, do not silently pretend loopback is private. | `/usr/lib/urnetwork/libURnetworkSdk.so` | daemon pkg | rpath `$ORIGIN` | | `/usr/bin/urnetwork` | daemon pkg | **launcher script**, the stable `Exec=` target | | `/lib/systemd/system/urnetworkd.service` | daemon pkg | `/lib`, in every release's load path | -| `/usr/share/applications/network.ur.urnetwork.desktop` | daemon pkg | filename **must** match `main.cpp`'s app id | -| `/usr/share/icons/hicolor/{48x48,256x256}/apps/urnetwork.png` | daemon pkg | | +| `/usr/share/applications/com.bringyour.network.desktop` | daemon pkg | filename **must** match `main.cpp`'s app id | +| `/usr/share/icons/hicolor/{48x48,256x256}/apps/com.bringyour.network.png` | daemon pkg | | | `/usr/share/urnetwork/world-110m.json` | daemon pkg | globe land outlines | | `/usr/share/urnetwork/icons/urnetwork-tray-*.png` | daemon pkg | tray art | | `/usr/share/locale//LC_MESSAGES/urnetwork.mo` | daemon pkg | gettext catalogs | -| `/etc/urnetwork/autostart/network.ur.urnetwork.desktop` | daemon pkg | **inert template**, GUI symlinks it | +| `/etc/urnetwork/autostart/com.bringyour.network.desktop` | daemon pkg | **inert template**, GUI symlinks it | | `/etc/NetworkManager/conf.d/95-urnetwork.conf` | daemon pkg | `unmanaged-devices=interface-name:urnet0` | | `/etc/udev/rules.d/85-urnetwork-unmanaged.rules` | daemon pkg | `ENV{NM_UNMANAGED}="1"` | | `~/.local/lib/urnetwork/URnetwork.AppImage` | **user** | never packaged; must be user-writable | @@ -57,27 +57,51 @@ prints a one-line install hint and exits 127. ## Artifact filenames (normative — the pipeline greps for these) ``` -urnetwork-daemon__.deb arch = amd64 | arm64 +urnetwork-daemon__.deb arch = amd64 | arm64 urnetwork-daemon--.install.tar.gz +urnetwork-daemon--.pkg.tar.zst pacmanarch = x86_64 | aarch64 URnetwork--.AppImage URnetwork--.AppImage.zsync ``` +The pacman package is named with pacman's own arch spelling, for the same reason the +`.rpm` is named with rpm's: a package whose filename disagrees with the arch in its own +metadata is the confusing artifact. Its `` is the release version verbatim — +pacman does not parse filenames (`pacman -U ./file` reads `.PKGINFO`), so the file is +free to be named for the release while the metadata carries the folded, pacman-legal +`pkgver` (see `pkg_fields()` in `packaging/make-arch.sh`). `make-arch.sh` prints the +canonical `---.pkg.tar.zst` on every build and emits it +instead under `UR_ARCH_CANONICAL_NAME=1`, which is what a `repo-add` repository expects. + +**The `.rpm` still has no row here.** rpm forbids `-` in both Version and Release while +this line's `` contains two, so no legal rpm filename can carry the version +string verbatim. Its asset name is therefore settled by the release workflow rather +than by this contract, and that remains an open item. + `` = `$EXTERNAL_WARP_VERSION`. The tarball's **single top-level directory** is `urnetwork-daemon/`, containing `install.sh`, `uninstall.sh`, `VERSION`, and a `payload/` tree mirroring the installed paths above. ### Packaging script names + invocation (pinned 2026-08-05) -The pipeline calls these three by exact path. **These names are normative**; the +The pipeline calls these by exact path. **These names are normative**; the original contract pinned only the output filenames, which left the pipeline guessing: ``` linux/packaging/make-deb.sh linux/packaging/make-install-tarball.sh linux/packaging/make-appimage.sh +linux/packaging/make-rpm.sh (added later; Fedora/RHEL/openSUSE) +linux/packaging/make-arch.sh (added later; Arch/CachyOS/EndeavourOS/Manjaro) ``` +The two later ones take the SAME four-variable environment as the original three and +are deliberately not special: each is one more wrapper around the single staging tree, +so the daemon inside every package is the same bytes by construction rather than by +review. The pipeline may treat either as optional (`UR_REQUIRE_RPM` / +`UR_REQUIRE_ARCH_PKG`) — a new package that cannot build must never take the +already-contracted assets off a release with it. + Each is invoked with this environment and **must write its normative artifact filename into `$OUT_DIR`**: diff --git a/PLAN.md b/PLAN.md index 49c92b0..dedbcf5 100644 --- a/PLAN.md +++ b/PLAN.md @@ -302,7 +302,7 @@ than being the one platform that differs. refreshed by **dpkg triggers**, no dependency or maintainer script needed; §9.7.1 covers `x-scheme-handler/urnetwork`. ⚠️ Triggers fire only for files **dpkg** installs, so never symlink one in from `postinst` (IVPN's latent bug). Residual: - `main.cpp:67`'s app id `network.ur.urnetwork` does not match + `main.cpp:67`'s app id `com.bringyour.network` does not match `urnetwork.desktop`; harmless today, but D-Bus activation requires them to match. - **R8 — GeoClue ≥ 2.7.0 is required for the location override** [confirmed]: Ubuntu 22.04 (2.5.7) and Debian 12 (2.6.0) can **never** support it. Those users diff --git a/app/meson.build b/app/meson.build index 2fad86a..ccda761 100644 --- a/app/meson.build +++ b/app/meson.build @@ -305,30 +305,30 @@ endif # host_integration=false (Flatpak) drops it with the rest: the GUI never calls # polkit — the daemon does, against a subject the GUI cannot influence. if host_integration -install_data('..' / 'packaging' / 'polkit' / 'network.ur.urnetwork.policy', +install_data('..' / 'packaging' / 'polkit' / 'com.bringyour.network.policy', install_dir : get_option('datadir') / 'polkit-1' / 'actions') endif -# desktop file: the filename MUST match main.cpp's app id network.ur.urnetwork +# desktop file: the filename MUST match main.cpp's app id com.bringyour.network # (D-Bus activation requires it; APPIMAGE.md §11d) -install_data('packaging/network.ur.urnetwork.desktop', +install_data('packaging/com.bringyour.network.desktop', install_dir : get_option('datadir') / 'applications') # AppStream metadata — required by Flathub, read by GNOME Software / Discover # for the .deb and .rpm too. The component id matches the .desktop basename. -install_data('packaging/network.ur.urnetwork.metainfo.xml', +install_data('packaging/com.bringyour.network.metainfo.xml', install_dir : get_option('datadir') / 'metainfo') -# hicolor icons (Icon=urnetwork in the desktop file) -install_data('packaging/icons/hicolor/48x48/apps/urnetwork.png', +# hicolor icons (Icon=com.bringyour.network in the desktop file) +install_data('packaging/icons/hicolor/48x48/apps/com.bringyour.network.png', install_dir : get_option('datadir') / 'icons' / 'hicolor' / '48x48' / 'apps') -install_data('packaging/icons/hicolor/256x256/apps/urnetwork.png', +install_data('packaging/icons/hicolor/256x256/apps/com.bringyour.network.png', install_dir : get_option('datadir') / 'icons' / 'hicolor' / '256x256' / 'apps') # autostart INERT TEMPLATE: root-owned under /etc/urnetwork/autostart/, where # no desktop reads it; the GUI symlinks it into ~/.config/autostart atomically if host_integration -install_data('packaging/autostart/network.ur.urnetwork.desktop', +install_data('packaging/autostart/com.bringyour.network.desktop', install_dir : '/etc/urnetwork/autostart') endif diff --git a/app/packaging/autostart/network.ur.urnetwork.desktop b/app/packaging/autostart/com.bringyour.network.desktop similarity index 96% rename from app/packaging/autostart/network.ur.urnetwork.desktop rename to app/packaging/autostart/com.bringyour.network.desktop index ac7d01f..31b99cd 100644 --- a/app/packaging/autostart/network.ur.urnetwork.desktop +++ b/app/packaging/autostart/com.bringyour.network.desktop @@ -2,7 +2,7 @@ Type=Application Name=URnetwork Comment=URnetwork VPN -Icon=urnetwork +Icon=com.bringyour.network Exec=urnetwork # TryExec self-disables this entry once the daemon package (and with it the # launcher) is removed, so a stale symlink cannot error at every login. diff --git a/app/packaging/network.ur.urnetwork.desktop b/app/packaging/com.bringyour.network.desktop similarity index 93% rename from app/packaging/network.ur.urnetwork.desktop rename to app/packaging/com.bringyour.network.desktop index 63e8327..e592abc 100644 --- a/app/packaging/network.ur.urnetwork.desktop +++ b/app/packaging/com.bringyour.network.desktop @@ -2,7 +2,7 @@ Type=Application Name=URnetwork Comment=URnetwork VPN -Icon=urnetwork +Icon=com.bringyour.network # /usr/bin/urnetwork is the launcher script the daemon package installs; it # finds the user's AppImage (search order in MIGRATION.md) and execs it with # the same arguments. Keeping Exec= on a stable, package-managed path is what @@ -13,7 +13,7 @@ Terminal=false Categories=Network;Security; Keywords=VPN;URnetwork;proxy;privacy; StartupNotify=true -StartupWMClass=network.ur.urnetwork +StartupWMClass=com.bringyour.network # Register the urnetwork:// scheme so OAuth/SSO callbacks and wallet deep # links route back to the app. Shipped as a plain dpkg file so the # desktop-file-utils dpkg trigger refreshes mimeinfo.cache automatically diff --git a/app/packaging/network.ur.urnetwork.metainfo.xml b/app/packaging/com.bringyour.network.metainfo.xml similarity index 93% rename from app/packaging/network.ur.urnetwork.metainfo.xml rename to app/packaging/com.bringyour.network.metainfo.xml index 409e1e0..6770f1e 100644 --- a/app/packaging/network.ur.urnetwork.metainfo.xml +++ b/app/packaging/com.bringyour.network.metainfo.xml @@ -2,7 +2,7 @@ - network.ur.urnetwork + com.bringyour.network URnetwork Private, decentralized VPN powered by a peer network @@ -45,7 +45,7 @@

- network.ur.urnetwork.desktop + com.bringyour.network.desktop https://ur.io https://github.com/urnetwork/urnetwork-linux/issues diff --git a/app/packaging/icons/hicolor/256x256/apps/urnetwork.png b/app/packaging/icons/hicolor/256x256/apps/com.bringyour.network.png similarity index 100% rename from app/packaging/icons/hicolor/256x256/apps/urnetwork.png rename to app/packaging/icons/hicolor/256x256/apps/com.bringyour.network.png diff --git a/app/packaging/icons/hicolor/48x48/apps/urnetwork.png b/app/packaging/icons/hicolor/48x48/apps/com.bringyour.network.png similarity index 100% rename from app/packaging/icons/hicolor/48x48/apps/urnetwork.png rename to app/packaging/icons/hicolor/48x48/apps/com.bringyour.network.png diff --git a/app/snap/snapcraft.yaml b/app/snap/snapcraft.yaml index 43a049f..ced14b0 100644 --- a/app/snap/snapcraft.yaml +++ b/app/snap/snapcraft.yaml @@ -49,7 +49,7 @@ apps: urnetwork: command: usr/bin/urnetwork extensions: [gnome] # GTK4/libadwaita runtime, themes, portals, wayland/x11 - common-id: network.ur.urnetwork + common-id: com.bringyour.network desktop: usr/share/applications/urnetwork.desktop plugs: - network # outbound sockets (SDK platform + provider traffic) diff --git a/app/src/ControlProtocol.hpp b/app/src/ControlProtocol.hpp index 078d762..985f566 100644 --- a/app/src/ControlProtocol.hpp +++ b/app/src/ControlProtocol.hpp @@ -108,14 +108,14 @@ inline constexpr const char* kControlGroupName = "urnetwork"; // policy file absent -> byte-for-byte today's behaviour: 0660 // root:urnetwork and AuthorizeControlPeer. inline constexpr const char* kPolkitPolicyPath = - "/usr/share/polkit-1/actions/network.ur.urnetwork.policy"; + "/usr/share/polkit-1/actions/com.bringyour.network.policy"; // The immutable-host twin. ostree/bootc machines mount /usr read-only, so the // tarball installer maps its whole payload under /usr/local — and polkit has // read both directories since 124. ControlServer::PolicyPath() probes this one // FIRST; without it the daemon looks in the one place the file cannot be on // Bazzite/Silverblue/Kinoite/SteamOS and silently falls back to the group. inline constexpr const char* kPolkitPolicyPathLocal = - "/usr/local/share/polkit-1/actions/network.ur.urnetwork.policy"; + "/usr/local/share/polkit-1/actions/com.bringyour.network.policy"; // HelloReply::auth_mode — which authority this daemon actually latched at // start. The GUI needs it because the "add yourself to the urnetwork group, @@ -125,16 +125,16 @@ inline constexpr const char* kAuthModePolkit = "polkit"; inline constexpr const char* kAuthModeGroup = "group"; // The four polkit action ids, namespaced to the app id. Shipped in -// packaging/polkit/network.ur.urnetwork.policy (0644 root:root — polkit +// packaging/polkit/com.bringyour.network.policy (0644 root:root — polkit // ignores group- or world-writable action files). They are checked by // urnetworkd, never by the GUI: the subject is built from SO_PEERCRED on the // connection being served, so a client can neither nominate its own subject // nor skip the check by not asking. -inline constexpr const char* kActionControlTunnel = "network.ur.urnetwork.control-tunnel"; +inline constexpr const char* kActionControlTunnel = "com.bringyour.network.control-tunnel"; inline constexpr const char* kActionManageKillSwitch = - "network.ur.urnetwork.manage-kill-switch"; -inline constexpr const char* kActionTakeOverTunnel = "network.ur.urnetwork.take-over-tunnel"; -inline constexpr const char* kActionReadLog = "network.ur.urnetwork.read-log"; + "com.bringyour.network.manage-kill-switch"; +inline constexpr const char* kActionTakeOverTunnel = "com.bringyour.network.take-over-tunnel"; +inline constexpr const char* kActionReadLog = "com.bringyour.network.read-log"; // The SDK's built-in default device-RPC address (sdk/device_rpc.go:109, // deviceRpcDefaultAddress = "127.0.0.1:12025"). Kept as a NAMED CONSTANT ONLY, diff --git a/app/src/MainWindow.cpp b/app/src/MainWindow.cpp index 22e496e..9a424b6 100644 --- a/app/src/MainWindow.cpp +++ b/app/src/MainWindow.cpp @@ -517,7 +517,7 @@ void MainWindow::BuildChrome() { if (auto logo = BrandLogoTexture()) { icon->set(logo); } else { - icon->set_from_icon_name("urnetwork"); + icon->set_from_icon_name(kAppIconName); } icon->set_pixel_size(20); brand->append(*icon); diff --git a/app/src/SecretServiceRpcSessionStore.cpp b/app/src/SecretServiceRpcSessionStore.cpp index 325fe40..664286a 100644 --- a/app/src/SecretServiceRpcSessionStore.cpp +++ b/app/src/SecretServiceRpcSessionStore.cpp @@ -12,7 +12,13 @@ namespace { constexpr int kSecretPayloadVersion = 1; constexpr size_t kMaxSecretPayloadBytes = 256 * 1024; -constexpr const char* kApplicationAttribute = "network.ur.urnetwork"; +// The keyring attribute that scopes our secrets. Renamed with the app ID, so +// an entry written by an older build is not found by this one and the app +// falls back to a fresh RPC session — the same one-time cost as the Flatpak +// data path moving. Deliberately NOT dual-read: keeping the old attribute +// alive would leave the previous app identity holding live key material in the +// user's keyring with nothing left to clean it up. +constexpr const char* kApplicationAttribute = "com.bringyour.network"; const SecretSchema kRpcSessionSchema = [] { // Zero-initialize libsecret's reserved ABI fields explicitly. A short C diff --git a/app/src/UrTheme.cpp b/app/src/UrTheme.cpp index db6d024..44573d0 100644 --- a/app/src/UrTheme.cpp +++ b/app/src/UrTheme.cpp @@ -369,7 +369,8 @@ Glib::RefPtr BrandLogoTexture() { const std::string dir = BrandIconDir(); if (dir.empty()) return cached; for (const char* size : {"256x256", "48x48"}) { - const std::string path = dir + "/hicolor/" + size + "/apps/urnetwork.png"; + const std::string path = + dir + "/hicolor/" + size + "/apps/" + kAppIconName + ".png"; if (!g_file_test(path.c_str(), G_FILE_TEST_IS_REGULAR)) continue; try { cached = Gdk::Texture::create_from_filename(path); @@ -378,7 +379,7 @@ Glib::RefPtr BrandLogoTexture() { g_warning("brand logo: %s failed to load: %s", path.c_str(), e.what()); } } - g_warning("brand logo: no urnetwork.png under %s", dir.c_str()); + g_warning("brand logo: no %s.png under %s", kAppIconName, dir.c_str()); return cached; } diff --git a/app/src/UrTheme.hpp b/app/src/UrTheme.hpp index 7341792..de5e00f 100644 --- a/app/src/UrTheme.hpp +++ b/app/src/UrTheme.hpp @@ -34,10 +34,20 @@ void LoadBrandFonts(); // Install the brand CSS provider once per display. Safe to call repeatedly. void EnsureBrandCss(); +// The app icon's basename AND its icon-theme name: the packaging installs +// /icons/hicolor//apps/.png, and the .desktop's +// Icon= key names the same string. It lives here as ONE constant because it was +// previously spelled as a literal in two places -- the by-path load in +// UrTheme.cpp and the by-name fallback in MainWindow.cpp -- and the rename to +// the reverse-DNS app id caught only the packaging, so both lookups missed and +// the title-bar logo went blank with no error. Flatpak additionally REQUIRES +// this to equal the app id, or it exports no icon at all. +inline constexpr const char* kAppIconName = "com.bringyour.network"; + // Register the app's hicolor icon directory with the default icon theme so -// the icon NAME "urnetwork" resolves (window icon, tray, .desktop matching) +// the icon NAME "com.bringyour.network" resolves (window icon, tray, .desktop matching) // even when the app runs from a build tree or a relocated AppImage. Without -// this, set_from_icon_name("urnetwork") silently renders a BLANK image — +// this, set_from_icon_name("com.bringyour.network") silently renders a BLANK image — // which is exactly how the title-bar logo came out empty. Idempotent. void RegisterBrandIcons(); diff --git a/app/src/main.cpp b/app/src/main.cpp index 6eb543f..dc33838 100644 --- a/app/src/main.cpp +++ b/app/src/main.cpp @@ -78,7 +78,7 @@ int main(int argc, char** argv) { // the window with the app (and the hide-to-tray window keeps its identity). // HANDLES_OPEN: the single instance receives urnetwork:// deep links (wallet // callbacks) via signal_open — the .desktop registers x-scheme-handler/urnetwork. - auto app = Gtk::Application::create("network.ur.urnetwork", + auto app = Gtk::Application::create("com.bringyour.network", Gio::Application::Flags::HANDLES_OPEN); // Hold the application so it survives with only the tray (window hidden). @@ -98,7 +98,7 @@ int main(int argc, char** argv) { // missing face fails silently to the fallback font (windows parity). urnw::LoadBrandFonts(); urnw::EnsureBrandCss(); - // the icon NAME "urnetwork" must resolve for the window icon and the + // the icon NAME kAppIconName must resolve for the window icon and the // tray, wherever the app runs from urnw::RegisterBrandIcons(); diff --git a/docs/DISTRO-SUPPORT.md b/docs/DISTRO-SUPPORT.md index 019940c..f4bec27 100644 --- a/docs/DISTRO-SUPPORT.md +++ b/docs/DISTRO-SUPPORT.md @@ -344,8 +344,8 @@ the kill switch is available), the `.deb` exists, and the daemon's floor is exac | | Arch (rolling) | |---|---| -| Channel today | **T** (tarball) + **A** | -| Channel it should be | AUR `PKGBUILD` producing a native package | +| Channel today | **P** (`.pkg.tar.zst`, `packaging/make-arch.sh`, nfpm `-p archlinux`) + **T** (tarball) + **A** | +| Channel it should be | as today. An AUR `PKGBUILD` would be a *fourth* independent copy of the installed-path table; the first-party binary package is built from the same `assemble_daemon_root()` tree as the `.deb` and `.rpm`, so it cannot drift from them. An AUR recipe remains reasonable as a discovery channel, not as a replacement. | | init | systemd **[I]** | | LSM | none by default **[I]** — the SELinux branch no-ops (`getenforce` absent) **[M-src]** | | Firewall | nftables available, nothing enabled by default **[I]** | diff --git a/docs/TESTING-CACHYOS.md b/docs/TESTING-CACHYOS.md index 3cc0da3..5450d01 100644 --- a/docs/TESTING-CACHYOS.md +++ b/docs/TESTING-CACHYOS.md @@ -1,9 +1,25 @@ # Testing URnetwork on CachyOS — the tester's procedure **Target:** CachyOS (Arch-based), x86_64, systemd, cgroup v2, a desktop session. -**Channels covered:** the daemon **install tarball**, the GUI **AppImage**, the GUI **Flatpak**. -**Not covered:** `.deb` and `.rpm` — Arch has neither. On this distro the tarball is the -daemon's only channel, so if the tarball is broken there is no fallback. +**Channels covered:** the daemon **pacman package** (`.pkg.tar.zst`) and the daemon +**install tarball**, the GUI **AppImage**, the GUI **Flatpak**. +**Not covered:** `.deb` and `.rpm` — Arch has neither. + +> **THE TARBALL IS NO LONGER THE ONLY DAEMON CHANNEL HERE.** Releases now carry +> `urnetwork-daemon--x86_64.pkg.tar.zst`, built by `packaging/make-arch.sh` +> from the same staging tree as the `.deb` and the `.rpm`, so pacman owns and tracks +> the files. Prefer it: it declares `nftables` and `fuse2` (a minimal Arch install has +> neither, and the tarball can only complain about them after the fact), it keeps +> `/etc` files as `.pacnew`/`.pacsave`, and `pacman -R` removes it cleanly. +> +> **Never install both on one machine.** Each channel refuses to overwrite the other's +> files, on purpose: two owners of the same paths means the next `pacman -Syu` would +> half-replace the install. If you already ran the tarball installer, run +> `sudo /usr/lib/urnetwork/uninstall.sh` before `pacman -U`. +> +> If a release you are testing has no `.pkg.tar.zst` (the package is still optional in +> the pipeline while it settles), fall back to the tarball and everything below still +> applies. --- @@ -356,7 +372,8 @@ Repo: `Ryanmello07/urnetwork-linux`. Current beta at time of writing: | Asset | You need it for | |---|---| -| `urnetwork-daemon--amd64.install.tar.gz` | **The daemon. The only daemon channel on Arch.** | +| `urnetwork-daemon--x86_64.pkg.tar.zst` | **The daemon, native pacman package — prefer this.** `sudo pacman -U ./`. May be absent from older builds. | +| `urnetwork-daemon--amd64.install.tar.gz` | **The daemon, portable channel.** The fallback when the release has no pacman package, and the right choice on SteamOS (read-only `/usr`). | | `URnetwork--amd64.AppImage` | The GUI, AppImage channel. | | `URnetwork--amd64.AppImage.zsync` | Update control file. Attached for mirroring only — GitHub Releases answers zsync's multi-range requests with HTTP 501, so the in-app updater uses the self-hosted copy. You do not need it to test. | | `urnetwork-daemon__amd64.deb` | **Ignore.** No use on Arch. | @@ -1094,7 +1111,7 @@ cd urnetwork-linux ### 10.2 Run it ```bash -flatpak run network.ur.urnetwork +flatpak run com.bringyour.network ``` ### 10.3 What is different from the AppImage — and what to test because of it @@ -1121,7 +1138,7 @@ inode, and every `connect()` fails with `EACCES` — which historically got repo as a group-membership problem. The app now detects this and says so instead. To test it: ```bash -flatpak run network.ur.urnetwork & # leave it running +flatpak run com.bringyour.network & # leave it running sudo systemctl restart urnetworkd # now press Connect in the Flatpak GUI ``` @@ -1187,7 +1204,7 @@ Plus, separately: * **The full `--selftest-egress` output and its exit code** (§5.2). Always. * **The app's own log** — the `connect:` lines. From a terminal launch that is `gui.log` from §7.1; otherwise `journalctl --user -b | grep -E 'urnetwork|connect:'`, or for the Flatpak, - `flatpak run network.ur.urnetwork 2>&1 | tee flatpak-gui.log`. + `flatpak run com.bringyour.network 2>&1 | tee flatpak-gui.log`. * **The installer's full output** (§4 dry-run and §6 real run). **Use `stat -c %C`, never `ls -Z`.** On a machine with no SELinux, `ls -Z` prints `?` and @@ -1208,7 +1225,7 @@ tunnel addresses (`169.254.x.x`) are fine and are useful to us. sudo systemctl stop urnetworkd sudo /usr/lib/urnetwork/uninstall.sh # add --purge to also remove state + group rm -f ~/.local/lib/urnetwork/URnetwork.AppImage -flatpak uninstall --user network.ur.urnetwork +flatpak uninstall --user com.bringyour.network ``` Then confirm the machine is back to normal: diff --git a/docs/linux_agent_help.md b/docs/linux_agent_help.md index 5606c9d..4508e4a 100644 --- a/docs/linux_agent_help.md +++ b/docs/linux_agent_help.md @@ -1803,7 +1803,7 @@ single-process model existed only under Snap and is dead). ~17.8k lines of C++ a `urnetwork_sdk.hpp` wrapper as Windows. Holds `urnet::DeviceRemote` + a control-socket client. Ships as a **user AppImage in a user-writable path** with a `/usr/bin/urnetwork` launcher-script indirection; never installed by a package (meson `install_rpath - '$ORIGIN/../lib'`). App id `network.ur.urnetwork`; GApplication HANDLES_OPEN deep links; + '$ORIGIN/../lib'`). App id `com.bringyour.network`; GApplication HANDLES_OPEN deep links; hide-to-tray; forces `ADW_COLOR_SCHEME_FORCE_DARK`; runtime-resolved `bindtextdomain`. - **`urnetworkd`** — root daemon, **no GTK of any kind** (glib GMainLoop + gio + json + SDK; `-static-libstdc++ -static-libgcc`). Holds `DeviceLocal(enable_rpc=true)`, opens @@ -1898,8 +1898,8 @@ replacing it, keeping its version/SDK-match enforcement. `urnetworkd.service` (Type=notify, After/Wants `network-pre.target`, `RuntimeDirectory=urnetwork` 0750, `StateDirectory`/`LogsDirectory` 0700, `NoNewPrivileges`, `ProtectHome`, `Restart=on-failure`/`RestartSec=2`), - `network.ur.urnetwork.desktop` (`Exec=urnetwork %u`, - `MimeType=x-scheme-handler/urnetwork;`, `StartupWMClass=network.ur.urnetwork`), + `com.bringyour.network.desktop` (`Exec=urnetwork %u`, + `MimeType=x-scheme-handler/urnetwork;`, `StartupWMClass=com.bringyour.network`), `urnetwork-launcher` (installed `/usr/bin/urnetwork`; search order `$URNETWORK_APPIMAGE` → `~/.local/lib/urnetwork/URnetwork.AppImage` → `~/Applications/URnetwork*.AppImage` newest → `/usr/lib/urnetwork/URnetwork.AppImage` → `urnetwork-gui` on PATH; exit 127 with install @@ -1947,7 +1947,7 @@ Files that ARE snap support (delete): - `linux:app/snap/snapcraft.yaml` — the whole file (114 lines). Already carries a SUPERSEDED banner ("NOT BUILT, NOT SHIPPED... describes the SINGLE-PROCESS architecture... Do not resurrect as-is"); it is also **broken** — it organizes `packaging/urnetwork.desktop`, - which was renamed to `network.ur.urnetwork.desktop`, and its `version: "0.0.0"` sed + which was renamed to `com.bringyour.network.desktop`, and its `version: "0.0.0"` sed stamping mechanism is gone. It was retained only because "Snap Store as secondary channel" was left open (`APPIMAGE.md` §8 item 5); that door is now closed. - `linux:app/.gitignore` lines 8–11: `*.snap`, `parts/`, `prime/`, `stage/`. @@ -2139,7 +2139,7 @@ EOL ~yearly — plan an annual runtime bump. - polkit: actions as XML `.policy` in `/usr/share/polkit-1/actions/`; the daemon checks each privileged request via `CheckAuthorization` (CLI form `pkcheck`), passing request variables so admins can write rules. Proposed action split (tiering idiom is standard; names are our - proposal): `network.ur.urnetwork.connect`/`.disconnect` → `allow_active=yes`; + proposal): `com.bringyour.network.connect`/`.disconnect` → `allow_active=yes`; `.install-daemon`, `.set-killswitch`, `.clear-blocking` → `auth_admin_keep`. Simpler alternative used by Mullvad/Tailscale: socket file mode + group membership (already the linux: model — group `urnetwork`); reasonable default, with polkit/pkexec reserved for the diff --git a/packaging/arch/nfpm.yaml b/packaging/arch/nfpm.yaml new file mode 100644 index 0000000..7181686 --- /dev/null +++ b/packaging/arch/nfpm.yaml @@ -0,0 +1,360 @@ +# nfpm (https://nfpm.goreleaser.com) package definition for the URnetwork +# daemon .pkg.tar.zst -- the Arch-family native channel (Arch, CachyOS, +# EndeavourOS, Manjaro), and the third wrapper around the ONE staging tree +# packaging/lib/common.sh assemble_daemon_root() produces. +# +# WHY nfpm AND NOT A PKGBUILD +# --------------------------- +# The .deb and the .rpm are already nfpm, over the same assemble_daemon_root() +# output, and that shared function is the only reason "the three daemon +# packages ship the same payload" is a fact rather than a hope. A PKGBUILD +# would be a fourth, independent copy of the installed-path table -- exactly +# what packaging/rpm/nfpm.yaml's header refuses for rpmbuild, and for the same +# reason. nfpm 2.47.0 (the UR_NFPM_VERSION pin in +# .github/workflows/beta-build.yml) lists archlinux among its packagers, and +# its output is a real .pkg.tar.zst with .PKGINFO, .MTREE and .INSTALL that +# pacman installs, upgrades and removes. +# +# It also means this package needs no Arch machine to build: nfpm is pure Go, +# its zstd is pure Go, and the CI daemon job builds this inside the same +# ubuntu:22.04 container that produces the .deb and the .rpm. +# +# WHAT THAT COSTS, STATED PLAINLY +# ------------------------------- +# * There is no makepkg, no namcap and no PKGBUILD to submit to the AUR. +# This is a first-party binary package for direct `pacman -U`, not an AUR +# submission. An AUR recipe would be a separate, later piece of work. +# * nfpm's archlinux packager has NO optdepends field, and it SILENTLY +# DISCARDS the shared `recommends:`/`suggests:` keys -- measured on 2.47.0: +# a config carrying `recommends: [polkit]` emitted `Recommends:` in the +# .deb and NOTHING in the .PKGINFO, with no warning. Do not add a +# `recommends:` block here expecting an advisory line; it produces no +# bytes. (It would buy little anyway: PKGBUILD(5) is explicit that +# "optdepends are ... for informational purposes only and are not utilized +# by pacman during dependency resolution" -- there is no Arch analogue of +# APT::Install-Recommends or dnf's install_weak_deps.) The polkit story is +# told by scripts/post_install instead, which is strictly better than a +# metadata line nobody reads. +# * The dependency STRINGS are passed through verbatim and unvalidated. +# Debian alternation ("libfuse2t64 | libfuse2") and Debian version syntax +# ("polkit (>= 124)") emit as literal `depend =` tokens that pacman then +# cannot resolve -- an UNINSTALLABLE package that builds without a +# complaint. The depends list below is therefore REWRITTEN for Arch, never +# copied from packaging/deb/nfpm.yaml. Bare names, or `pkg>=ver`, only. +# +# Do not run nfpm against this file directly: this is a TEMPLATE, exactly like +# its two siblings. make-arch.sh assembles the package root, relocates the +# systemd unit out of /lib, substitutes ${PKG_VERSION} / ${PKG_RELEASE} / +# ${PKG_ARCH} / ${PKG_MTIME} / ${NFPM_ARCH} / ${PKGROOT} itself (nfpm does not +# expand environment variables in contents[].src -- verified for the .deb +# against nfpm v2.47), and names the output per the asset contract. +name: urnetwork-daemon +# The Debian arch spelling on the way in; nfpm maps amd64 -> x86_64 and +# arm64 -> aarch64 for the archlinux packager (measured: `arch: amd64` emitted +# `arch = x86_64` in .PKGINFO). The archlinux: block below states the pacman +# spelling explicitly all the same, so make-arch.sh's filename and this +# package's own metadata come from ONE variable and cannot disagree. +arch: "${NFPM_ARCH}" +platform: linux +# ONE TIMESTAMP FOR EVERY FILE, AND IT IS NOT COSMETIC. +# +# nfpm writes two independent time fields: the tar header mtime of each member, +# and .MTREE's `time=` for the same path. Left to itself it takes the tar mtime +# from the moment it writes the archive and the .MTREE time from the SOURCE +# file's ModTime -- which make-arch.sh created seconds earlier, when +# assemble_daemon_root copied it. pacman sets each installed file's mtime from +# the TAR header and then checks it against the .MTREE copy in its local +# database, so the two disagreeing by even one second makes +# +# pacman -Qkk urnetwork-daemon +# -> 144 total files, 24 altered files ("Modification time mismatch") +# +# FOREVER, on every installed machine. Measured exactly that way on real pacman +# before this line existed -- and measured as CLEAN in a build where the copy +# and the archive write happened to land in the same second, which is what +# makes it a nasty one: it is intermittent, and the clean build is the lucky +# one. For a VPN daemon `pacman -Qkk` is precisely the check a careful user +# runs, and "24 altered files" on a package they just installed is the wrong +# answer to give them. +# +# make-arch.sh substitutes one value here (SOURCE_DATE_EPOCH when set, so the +# package participates in reproducible builds; otherwise the build time) and +# asserts afterwards that every .MTREE entry carries it. +mtime: "${PKG_MTIME}" +# THE VERSION FOLD, AND WHY IT IS A THIRD DIFFERENT ANSWER. +# +# The pipeline's VERSION is "2026.8.20-1024376890-beta". Each packager mangles +# it differently and no two of the three scripts here derive their version +# fields the same way: +# .deb splits at the LAST hyphen -> Version: 2026.8.20-1024376890-beta +# .rpm splits at the FIRST hyphen -> 2026.8.20 + 0.1024376890.beta +# .pkg splits at NEITHER: pacman's pkgver may not contain '-' at all (it is +# the pkgver/pkgrel separator), so make-arch.sh folds the WHOLE string +# to dots and pins pkgrel to the literal 1. +# pkgver = 2026.8.20.1024376890.beta pkgrel = 1 +# +# Both other shapes were measured and both are wrong: +# * rpm-style split (version=2026.8.20, release="1024376890.beta") -> nfpm +# coerces a non-integer release to 1 SILENTLY and emits +# `pkgver = 2026.8.20-1`. The build id and the beta marker are simply gone, +# every build of a given day becomes the same pacman version, and +# `pacman -U` of a newer build reports "is up to date -- reinstalling". +# That is the tarball's problem back again, wearing a pacman badge. +# * the raw VERSION -> `pkgver = 2026.8.20-1024376890-beta-1`, which nfpm +# writes happily and pacman then REJECTS outright: "package version +# contains invalid characters" / "invalid or corrupted package". +# alpm compares dot-separated segments numerically, so the folded form orders +# correctly across both the day and the month rollover (verified with vercmp: +# 2026.8.9.x < 2026.8.20.y < 2026.9.1.z). +version: "${PKG_VERSION}" +release: "${PKG_RELEASE}" +# version_schema none for the same reason packaging/rpm/nfpm.yaml carries it: +# keep make-arch.sh's fold verbatim. Left at nfpm's semver default the filename +# and the .PKGINFO DISAGREE -- measured: file "...2026.8.161020679030_beta-1..." +# with `pkgver = 2026.8.16-1` inside it. +version_schema: none +maintainer: "URnetwork " +vendor: "URnetwork" +homepage: "https://ur.io" +# Ground rule 8: the code is MPL-2.0, used verbatim in the deb License:, the +# rpm's license: and the Flathub project_license. +license: "MPL-2.0" +description: | + Privileged system half of URnetwork for Linux: owns /dev/net/tun, routes and + DNS, exposes a control socket under /run/urnetwork whose privileged verbs are + gated by polkit (falling back to the urnetwork group where polkit is absent), + and starts idle -- no tunnel comes up until an authorized desktop client + asks. The GUI is the separate, unprivileged URnetwork AppImage, which lives + in a user-writable path so it can self-update; this package installs the + stable /usr/bin/urnetwork launcher plus the desktop entry, urnetwork:// + scheme handler, icons and autostart template that point at it. + +# NO GTK (or any GUI) dependency -- the daemon must install on a headless box. +depends: + # C runtime floor. The SDK is cross-built against glibc 2.35 (jammy), + # sdk/cgo/Makefile target gnu.2.35, and the .deb and .rpm both declare that + # floor by hand because nfpm runs no dependency generator. + # + # ON A ROLLING DISTRIBUTION THIS LINE IS ALWAYS SATISFIED (Arch is on 2.4x), + # and it is kept anyway so all three package definitions state the same floor + # in the same place. app/tests/glibc-floor-gate.sh (meson test `glibc-floor`) + # is what couples the declaration to the actual binary. + - "glibc>=2.35" + # Tunnel.cpp builds the tun link, the capture routes and the policy rules + # with `ip`, and the kill switch / egress self-exclusion with `nft`. BOTH are + # marked required=true in the daemon's own preflight (daemon/main.cpp), so + # without either the package installs, the service starts, and the first + # Connect fails at the first exec. + # + # iproute2 is in Arch's `base` meta-package and nftables is NOT -- so the + # nftables line is the one that does real work here. It is also the exact gap + # the .deb shipped with for a while (declared by the rpm, missing from the + # deb), which is what a declared dependency exists to prevent. + # + # Arch spells it iproute2, like Debian and openSUSE; only Fedora says + # `iproute`. Do not copy the rpm's name. + - "iproute2" + - "nftables" + # groupadd, used by scripts/pre_install. Arch's `shadow` is Fedora's + # `shadow-utils`; there is no `addgroup` on Arch at all, which is why the + # Debian postinst's addgroup-first shape is not reproduced in this package. + - "shadow" + # The unit, systemctl, and everything scripts/{post_install,post_upgrade, + # pre_remove} drive. Also what provides the alpm hooks in + # /usr/share/libalpm/hooks that daemon-reload on any change under + # usr/lib/systemd/system. + - "systemd" + # THE ONE GENUINE WIN OF SHIPPING A NATIVE PACKAGE (APPIMAGE.md 11d): the GUI + # AppImage needs libfuse.so.2 at runtime and every AppImage-only vendor walks + # its users through installing it by hand. This package ships + # /usr/bin/urnetwork and the .desktop entry that launch that AppImage, so an + # Arch box without fuse2 gets a menu entry that dies on a missing library. + # + # HARD, matching the .deb (which makes libfuse2 a Depends) rather than the + # .rpm (which makes libfuse.so.2 a Recommends). That difference is forced, + # not chosen: dnf and zypper install weak dependencies by default, so the rpm + # gets the same practical outcome from a Recommends -- pacman has no weak + # dependencies at all, so on Arch the choice is "hard" or "silently absent". + # fuse2 is 456 KB and pulls only fuse-common and glibc; nothing in a base + # Arch install provides it (verified against the archlinux image: absent). + - "fuse2" +# POLKIT IS DELIBERATELY NOT DECLARED, and this is the one place the three +# packages genuinely diverge. State the reasoning so nobody "fixes" it: +# +# * The .deb declares it as a RECOMMENDS with an explicit rationale -- the +# package must still install on a headless box, and apt installs Recommends +# by default so a desktop gets it anyway. +# * The .rpm declares it NOWHERE, in depends or recommends. So "no polkit +# line" is already the established behaviour of one of the two native +# packages, and this one matches it. +# * pacman has no weak-dependency mechanism to express the .deb's actual +# intent, and nfpm cannot emit optdepends even for the advisory line +# (see the header). The only two options are a HARD depend or nothing. +# * A hard depend would drag polkit and its pam/systemd/glib2/duktape/expat +# chain (~1.9 MB) onto every headless Arch install to buy nothing there, +# and would contradict the headless rationale the .deb states in its own +# words. +# * It is SAFE to omit because the fallback is real and fails open, not shut: +# ControlServer::PolkitPolicyPresent() requires BOTH the action file AND +# PolkitRuntimePresent() (which probes /usr/bin/pkcheck and +# /usr/lib/polkit-1/polkitd -- Arch's layout is covered), so an Arch box +# with no polkit latches Group mode and authorizes uid 0 plus the +# `urnetwork` group, exactly as it always did. A machine that shipped the +# action file with no polkit runtime is precisely the fail-shut trap that +# two-fact check exists to prevent. +# * On any Arch DESKTOP -- which is every machine that will run the GUI -- +# polkit is already present through GNOME/KDE/Cinnamon, so the polkit path +# is taken and there is no group-then-log-out first run. Only a minimal +# Arch install gets the group fallback, which is the same honest outcome +# the .rpm already ships to a minimal Fedora. +# +# scripts/post_install detects which of the two is in force and says so. + +contents: + # ONE /usr tree, and NOTHING under /lib. Two Arch facts drive this, and the + # first one is a hard install failure rather than a style preference: + # + # 1. /lib IS A SYMLINK TO usr/lib, OWNED BY THE `filesystem` PACKAGE. A + # package that carries any member under lib/ aborts the transaction: + # error: failed to commit transaction (conflicting files) + # urnetwork-daemon: /lib exists in filesystem (owned by filesystem) + # and nothing is installed. There is no YAML-level dodge -- nfpm emits an + # implicit parent-directory member for every dst, so declaring the unit as + # a single file at /lib/systemd/system/urnetworkd.service produces the + # lib/ member just the same. make-arch.sh RELOCATES the unit in ${PKGROOT} + # before nfpm runs, exactly as make-rpm.sh does, and asserts that nothing + # else was staged under /lib. + # + # 2. usr/lib/systemd/system IS THE PATH ARCH'S OWN HOOKS WATCH. + # /usr/share/libalpm/hooks/30-systemd-daemon-reload-system.hook targets + # `usr/lib/systemd/system/*`; a unit shipped anywhere else would not fire + # it. (Those hooks are PostTransaction, i.e. AFTER post_install and + # post_upgrade, which is why the scripts still run their own + # `systemctl daemon-reload` before starting anything -- the hook is not a + # substitute for the reload in the scriptlet.) + # The unit is NOT marked config: /usr/lib/systemd/system is the vendor + # unit directory, admin overrides live in /etc/systemd/system (drop-ins, + # `systemctl edit`), and pacman never writes there -- so "never overwrite + # an admin-edited unit" is satisfied by the path, with no scriptlet logic + # that could get it wrong. + # + # A GLOB, NOT `type: tree`, AND THAT IS NOT COSMETIC. With `type: tree` nfpm + # writes Go's unmasked fs.FileMode for every directory it walks into .MTREE + # -- `mode=20000000755`, i.e. fs.ModeDir|0755 formatted with %o and never + # masked to permission bits. Measured on the pristine nfpm output: 83 such + # entries, one `warning: directory permissions differ on / filesystem: + # 755 package: 755` per directory at install time (both sides printing 755 + # is the tell), and `pacman -Qkk urnetwork-daemon` reporting those + # directories as altered forever. For a VPN daemon that is exactly the check + # a security-minded user runs. The glob form emits the same 140 members with + # byte-identical per-file modes and zero folded entries -- verified by + # diffing `tar tvf` of both packages (mode, size and path identical; only + # .MTREE differs). + # + # DO NOT "fix" the directory modes with `file_info: {mode: 0755}` on a tree + # entry instead: that silences the warnings by clobbering EVERY FILE to 0755 + # -- the 0644 unit, the SDK library and the polkit .policy included -- + # destroying the mode discipline assemble_daemon_root() asserts. + # + # The .desktop file and the hicolor icons ride as plain pacman-shipped files + # ON PURPOSE, exactly as they ride as plain dpkg and rpm files in the other + # two packages: /usr/share/libalpm/hooks/ ships desktop-file-utils' and + # gtk-update-icon-cache's own hooks targeting usr/share/applications/ and + # usr/share/icons/, so mimeinfo.cache (what an x-scheme-handler/urnetwork + # lookup actually consults) and the icon cache refresh with zero scriptlet + # code. Never call update-desktop-database from post_install. + # + # The polkit action file rides inside this same /usr tree, put there by + # assemble_daemon_root(), which also asserts its 0644 mode before the package + # is built. It gets no second explicit entry here: a duplicated member is a + # packaging bug, not a stronger guarantee. + - src: "${PKGROOT}/usr/**/*" + dst: /usr + # Admin-editable integration files. `type: config` maps to pacman's + # `backup = etc/...` array, which is the exact analogue of a dpkg conffile + # and of rpm's %config(noreplace): an edited file survives an upgrade and the + # package's version lands beside it as .pacnew, and on removal the edited + # file is kept as .pacsave. + - src: "${PKGROOT}/etc/urnetwork/autostart/com.bringyour.network.desktop" + dst: /etc/urnetwork/autostart/com.bringyour.network.desktop + type: config + - src: "${PKGROOT}/etc/NetworkManager/conf.d/95-urnetwork.conf" + dst: /etc/NetworkManager/conf.d/95-urnetwork.conf + type: config + - src: "${PKGROOT}/etc/udev/rules.d/85-urnetwork-unmanaged.rules" + dst: /etc/udev/rules.d/85-urnetwork-unmanaged.rules + type: config + +# THE SIX HOOKS, AND WHY ALL SIX MUST BE DECLARED HERE. +# +# pacman's .INSTALL is not a set of Debian maintainer scripts and not a set of +# rpm scriptlets: it defines shell FUNCTIONS, and pacman dispatches to them by +# TRANSACTION KIND, with version strings as arguments and no overlap: +# +# fresh install pre_install "$new" post_install "$new" +# upgrade pre_upgrade "$new" "$old" post_upgrade "$new" "$old" +# downgrade pre_upgrade "$new" "$old" post_upgrade "$new" "$old" +# reinstall pre_upgrade "$v" "$v" post_upgrade "$v" "$v" +# removal pre_remove "$old" post_remove "$old" +# +# The install pair NEVER runs on an upgrade, and the remove pair never runs on +# one either -- so unlike dpkg (whose postinst re-runs its whole `configure` +# branch on upgrade) and unlike rpm (whose %post runs with $1 >= 2), Arch gives +# an upgrade NOTHING unless pre_upgrade/post_upgrade exist. +# +# And they only exist if they are declared in THIS BLOCK. nfpm's generic +# `scripts:` maps to pre_install/post_install/pre_remove/post_remove and +# NOTHING ELSE -- `nfpm jsonschema` shows `Scripts` with exactly those four +# keys and `ArchLinuxScripts` with exactly preupgrade/postupgrade, both +# additionalProperties:false, and there is no fallback wiring between them. +# Measured: a package carrying only the four generic scripts installs fine and +# then runs ABSOLUTELY NOTHING on `pacman -U` of a newer build -- the binary is +# replaced under a still-running daemon that keeps executing the old code, with +# no daemon-reload and no restart, and pacman reports success. +# +# The same trap is a forward-compatibility trap: because post_install never +# runs on upgrade, anything ADDED to it in a future release is invisible to +# every existing user forever. Whatever must hold after every transaction has +# to be idempotent and reachable from BOTH -- which is why post_upgrade here +# calls post_install rather than restating it. (Both function bodies land in +# one sourced .INSTALL file, so that call is just a shell function call; it is +# the same shape a hand-written PKGBUILD .install uses.) +scripts: + preinstall: scripts/pre_install + postinstall: scripts/post_install + preremove: scripts/pre_remove + postremove: scripts/post_remove +archlinux: + # Explicit, not inferred. The top-level `arch:` already produces the right + # value, but make-arch.sh needs the pacman spelling for the artifact name and + # this keeps the two from ever disagreeing. + arch: "${PKG_ARCH}" + # REQUIRED, and easy to lose by copying either sibling config: the top-level + # `maintainer:` does NOT feed .PKGINFO's packager field. Measured -- with + # maintainer set and this key absent, `pacman -Qi` reports + # "Packager : Unknown Packager". + packager: "URnetwork " + scripts: + preupgrade: scripts/pre_upgrade + postupgrade: scripts/post_upgrade + +# --------------------------------------------------------------------------- +# PORTABILITY NOTE -- WHAT THIS PACKAGE IS AND IS NOT +# --------------------------------------------------------------------------- +# This is the ARCH-FAMILY package: Arch, CachyOS, EndeavourOS, Manjaro. Every +# dependency name above was checked against the real repositories (glibc, +# iproute2, shadow, systemd in core; nftables, fuse2 in extra), so it resolves +# on any of them. +# +# IT IS NOT FOR SteamOS, which is Arch-family but immutable. SteamOS has a +# read-only /usr; `pacman -U` there needs `steamos-readonly disable` and is +# reverted by the next system update. Immutable Arch hosts take the install +# tarball instead, which handles a read-only /usr by installing under +# /usr/local. packaging/distro-smoke.sh says so on a SteamOS host. +# +# NO conflicts:/provides:/replaces: are declared. There is no urnetwork package +# in the Arch repositories or the AUR today, so there is nothing to conflict +# with by name; the real collision is with a tarball install of the same files, +# and pacman cannot see that. packaging/tarball/install.sh and uninstall.sh +# already guard the other direction -- both refuse to touch paths that +# `pacman -Qo` says a package owns. diff --git a/packaging/arch/scripts/post_install b/packaging/arch/scripts/post_install new file mode 100644 index 0000000..9e7e097 --- /dev/null +++ b/packaging/arch/scripts/post_install @@ -0,0 +1,178 @@ +#!/bin/sh +# post_install() for urnetwork-daemon -- and, via post_upgrade, the ONE place +# that converges this machine after EVERY pacman transaction. +# +# PACMAN ARGUMENTS: "$1" is the version just installed. "$2" is unset. +# post_install runs ONLY on a fresh install -- never on an upgrade, never on a +# downgrade, and never on a reinstall (all three go through the pre_upgrade / +# post_upgrade pair instead). That is the single biggest difference from the +# other two packages: packaging/deb/scripts/postinst re-runs its whole +# `configure` branch on every upgrade, and packaging/rpm/scripts/post runs on +# upgrade with $1 >= 2. Arch runs neither. +# +# WHICH IS WHY post_upgrade CALLS THIS FUNCTION rather than restating it. +# nfpm emits every hook into ONE .INSTALL file which pacman sources whole +# before calling anything, so `post_install "$1"` from post_upgrade is an +# ordinary shell function call -- the same shape a hand-written PKGBUILD +# .install uses (`post_upgrade() { post_install "$@"; }`). The consequence to +# keep in mind when editing: ANYTHING ADDED BELOW MUST BE IDEMPOTENT, because +# it will run again on every upgrade, downgrade and reinstall. That is the +# property that makes it safe, and it is also the only defence against the +# forward-compatibility trap -- work added to an install-only hook on Arch is +# invisible to every existing user forever. +# +# UR_ARCH_PHASE is how the two callers differ. post_upgrade sets it to +# "upgrade" before calling; a real install leaves it unset. It is NOT derived +# from "$1"/"$2" on purpose: the version strings alone cannot distinguish an +# upgrade from a downgrade from a reinstall, and every attempt to read meaning +# out of them is the same class of mistake as branching on 'configure'. +# +# Deliberately absent, mirroring both siblings: anything touching +# /usr/share/applications or /usr/share/icons. Those ship as plain +# pacman-owned files precisely so the desktop-file-utils and +# gtk-update-icon-cache alpm hooks in /usr/share/libalpm/hooks refresh +# mimeinfo.cache -- the thing an x-scheme-handler/urnetwork lookup actually +# consults -- and the icon cache, with zero scriptlet code. +set -e + +UNIT='urnetworkd.service' +UR_PHASE="${UR_ARCH_PHASE:-install}" +RESTART_MARKER='/run/urnetwork.pacman-restart' + +# --- which authority this machine will run under, said out loud -------------- +# +# DETECTED BY LOOKING FOR POLKIT, NOT FOR OUR OWN ACTION FILE. pacman has +# already extracted /usr/share/polkit-1/actions/com.bringyour.network.policy by +# the time this runs (and created that directory on the way), so testing the +# file or the directory would report "polkit" on a box that has never had it. +# +# This mirrors the daemon's own two-fact test exactly: +# ControlServer::PolkitRuntimePresent() probes /usr/bin/pkcheck and +# /usr/lib/polkit-1/polkitd among others, and PolkitPolicyPresent() requires +# BOTH that and the action file -- so a machine with the file and no runtime +# falls back to the group rather than latching an authority it cannot ask. +# Unlike the .deb, this package declares no polkit dependency at all (see +# nfpm.yaml for the four reasons), so on Arch this branch is genuinely load +# bearing rather than a formality. +UR_HAS_POLKIT=0 +if command -v pkaction >/dev/null 2>&1 || command -v pkcheck >/dev/null 2>&1; then + UR_HAS_POLKIT=1 +elif [ -f /usr/share/polkit-1/actions/org.freedesktop.policykit.policy ]; then + UR_HAS_POLKIT=1 +else + for d in /usr/lib/polkit-1 /usr/libexec/polkit-1 /usr/lib64/polkit-1; do + if [ -x "${d}/polkitd" ]; then UR_HAS_POLKIT=1; break; fi + done +fi +# Said once, on a fresh install. An upgrade repeats none of this chatter. +if [ "${UR_PHASE}" = install ]; then + if [ "${UR_HAS_POLKIT}" = 1 ]; then + echo "urnetwork: permission is granted through polkit -- no group change and no log-out are needed." + if [ -n "$(getent group urnetwork 2>/dev/null | cut -d: -f4)" ]; then + echo "urnetwork: the 'urnetwork' group still has members from an earlier version. Nothing to undo -- membership is simply no longer consulted where polkit is present." + fi + else + echo "urnetwork: no polkit found; urnetworkd falls back to the 'urnetwork' group." + echo "urnetwork: run 'sudo usermod -aG urnetwork ', then log out and back in." + echo "urnetwork: or 'sudo pacman -S polkit' and reinstall this package, which removes that requirement." + fi +fi + +# --- the polkit action file's mode ------------------------------------------- +# polkit IGNORES a group- or world-writable .policy file, SILENTLY: the actions +# revert to their built-in defaults and the only symptom is an administrator +# password prompt on every Connect, long after the install. pacman ships ours +# 0644 root:root from the package tree (assemble_daemon_root asserts that at +# build time); assert it here rather than assume it, and check the directory +# too -- polkit does not police the directory, and whoever can write it can +# drop in a file that redefines our defaults. +UR_POLICY=/usr/share/polkit-1/actions/com.bringyour.network.policy +if [ -f "${UR_POLICY}" ]; then + chmod 0644 "${UR_POLICY}" 2>/dev/null || : + chown root:root "${UR_POLICY}" 2>/dev/null || : + UR_POLICY_DIR_MODE="$(stat -c '%a' /usr/share/polkit-1/actions 2>/dev/null || echo '')" + case "${UR_POLICY_DIR_MODE}" in + '') : ;; + *[2367]|*[2367]?) + echo "urnetwork: WARNING /usr/share/polkit-1/actions is group- or world-writable (mode ${UR_POLICY_DIR_MODE}); anyone who can write there can redefine URnetwork's polkit defaults." >&2 ;; + esac +fi + +# --- /run/urnetwork ----------------------------------------------------------- +# The unit's RuntimeDirectory=urnetwork (RuntimeDirectoryPreserve=yes) is the +# real owner and re-applies the mode on every start; pre-create it so the +# control path exists even before the first start. The daemon narrows it to +# 0750 root:urnetwork itself when it finds no polkit action file. +if [ -d /run ]; then + if [ "${UR_HAS_POLKIT}" = 1 ]; then + install -d -m 0755 -o root -g root /run/urnetwork 2>/dev/null || : + else + install -d -m 0750 -o root -g urnetwork /run/urnetwork 2>/dev/null || : + fi +fi + +# --- NetworkManager / udev markings ------------------------------------------- +# The conf.d marking must be live before the first tunnel comes up, and both +# files are `backup =` conffiles that an upgrade may replace -- which is a +# second reason this block has to be reachable from post_upgrade. Neither call +# touches existing connections. +if command -v nmcli >/dev/null 2>&1; then + nmcli general reload conf >/dev/null 2>&1 || : +fi +if command -v udevadm >/dev/null 2>&1; then + udevadm control --reload >/dev/null 2>&1 || : +fi + +# --- systemd ------------------------------------------------------------------ +# THE daemon-reload IS NOT REDUNDANT with Arch's own +# /usr/share/libalpm/hooks/30-systemd-daemon-reload-system.hook. That hook is +# PostTransaction -- it runs AFTER this function -- so starting the unit here +# without reloading first would start it against the stale in-memory unit. +if [ ! -d /run/systemd/system ]; then + # No booted systemd (a chroot, a container image build, pacstrap). Nothing + # to enable or start; the unit is on disk and will be picked up at boot. + return 0 +fi +systemctl --system daemon-reload >/dev/null 2>&1 || : + +if [ "${UR_PHASE}" = install ]; then + # ENABLE, AND ONLY EVER HERE. + # + # Arch has no preset mechanism to lean on: its + # /usr/lib/systemd/system-preset/99-default.preset ends in `disable *`, and + # unlike Fedora there is no convention of shipping a package preset file -- + # so `systemctl preset` would leave the unit disabled and this package + # would install a service that never starts, at install or at boot. + # + # Enabling on the INITIAL INSTALL ONLY is how an admin's later `systemctl + # disable urnetworkd` survives every upgrade. It needs no state file: the + # enablement symlink lives in /etc/systemd/system/multi-user.target.wants/, + # which pacman does not touch on upgrade, and post_upgrade deliberately + # does not re-enable. This is the Arch expression of the .deb's + # deb-systemd-helper "enable once" state file and the .rpm's + # `[ $1 -eq 1 ]` preset guard -- same guarantee, no equivalent machinery + # required. + systemctl enable "${UNIT}" >/dev/null 2>&1 || : + # STARTING FROM A SCRIPTLET is correct here for the same reason Debian + # Policy 9.3.3.1 permits it for the .deb and the .rpm does it too: + # urnetworkd STARTS IDLE. It brings up no tunnel, changes no routes and + # installs no firewall rules until a client authenticates on the control + # socket and asks. The alternative is a `pacman -S` that leaves a machine + # where the app truthfully reports "the service is not running" until the + # next reboot. + systemctl start "${UNIT}" >/dev/null 2>&1 || : +else + # UPGRADE / DOWNGRADE / REINSTALL: start again ONLY if pre_upgrade stopped + # a unit that was actually running. An upgrade must never start a daemon an + # admin had stopped, and must never re-enable one they had disabled. + if [ -f "${RESTART_MARKER}" ]; then + rm -f "${RESTART_MARKER}" 2>/dev/null || : + systemctl start "${UNIT}" >/dev/null 2>&1 || : + fi +fi + +# `return 0`, never `exit 0` -- see the note at the end of pre_install. This +# one matters in practice rather than in theory: post_upgrade calls this +# function and then returns, and an `exit` here would end pacman's scriptlet +# shell mid-call. +return 0 diff --git a/packaging/arch/scripts/post_remove b/packaging/arch/scripts/post_remove new file mode 100644 index 0000000..0af13c3 --- /dev/null +++ b/packaging/arch/scripts/post_remove @@ -0,0 +1,58 @@ +#!/bin/sh +# post_remove() for urnetwork-daemon. +# +# PACMAN ARGUMENT: "$1" is the version that was removed. "$2" is unset. Runs +# only on a real removal, never on an upgrade. +# +# WHAT DELIBERATELY DOES *NOT* HAPPEN HERE, and why it differs from +# packaging/deb/scripts/postrm. +# +# dpkg has two removal states and the .deb uses both: on `remove` it masks the +# unit, and only on `purge` does it delete /var/lib/urnetwork (device identity +# and key material), /etc/urnetwork, an URnetwork-authored /etc/geolocation, +# and the urnetwork group. +# +# PACMAN HAS NO PURGE. `pacman -R` and even `pacman -Rns` are the equivalent of +# `apt remove`, not of `apt purge`: pacman removes the files it owns, keeps +# modified `backup =` files as .pacsave, and has no second state in which a +# package is entitled to delete user data or a system group other files may +# still be group-owned by. So this script removes none of it -- and says +# exactly how to, rather than silently keeping key material or silently +# deleting it. packaging/rpm/scripts/postun reaches the same conclusion for the +# same reason. +# +# There is also no mask step. deb-systemd-helper masks a unit while it is +# removed-but-not-purged so nothing restarts a service whose binary is gone; +# with no purge state there is nothing to unmask later, and pre_remove has +# already disabled the unit, so masking would only leave a permanent +# /etc/systemd/system/urnetworkd.service -> /dev/null symlink behind after the +# package that created it is gone. +set -e + +# The unit file is gone; tell systemd. Arch's own +# /usr/share/libalpm/hooks/30-systemd-daemon-reload-system.hook fires on +# Remove for usr/lib/systemd/system/* too, but it is PostTransaction and this +# costs nothing, so correct behaviour never depends on the hook being present. +if [ -d /run/systemd/system ]; then + systemctl --system daemon-reload >/dev/null 2>&1 || : +fi + +# /run/urnetwork survives a stop on purpose (RuntimeDirectoryPreserve=yes in +# the unit, so a crash-restart comes back armed rather than open). With the +# daemon gone it is a leftover holding a marker nothing will ever read again. +if [ -d /run/urnetwork ]; then + rm -rf /run/urnetwork 2>/dev/null || : +fi +rm -f /run/urnetwork.pacman-restart 2>/dev/null || : + +echo "urnetwork: removed. Device credentials and settings were KEPT (pacman has no 'purge')." +echo "urnetwork: to erase them too: sudo rm -rf /var/lib/urnetwork /etc/urnetwork" +echo "urnetwork: the 'urnetwork' group was kept: sudo groupdel urnetwork" +# Only ever mention the location override when URnetwork demonstrably authored +# it: the writer stamps the file with a marker line, and no other actor on the +# system reverts it. +if [ -f /etc/geolocation ] && grep -qi 'urnetwork' /etc/geolocation 2>/dev/null; then + echo "urnetwork: /etc/geolocation still holds an URnetwork location override: sudo rm -f /etc/geolocation" +fi + +return 0 diff --git a/packaging/arch/scripts/post_upgrade b/packaging/arch/scripts/post_upgrade new file mode 100644 index 0000000..962f0bc --- /dev/null +++ b/packaging/arch/scripts/post_upgrade @@ -0,0 +1,44 @@ +#!/bin/sh +# post_upgrade() for urnetwork-daemon -- the start half of the upgrade dance, +# and the reason nothing in post_install is unreachable on Arch. +# +# PACMAN ARGUMENTS: "$1" incoming version, "$2" outgoing version. Runs on +# UPGRADE, DOWNGRADE and REINSTALL alike (see pre_upgrade); never on a fresh +# install. +# +# THE ENTIRE BODY IS A DELEGATION, ON PURPOSE. post_install holds the work that +# must be true after every transaction -- the polkit authority detection, the +# 0644 assertion on the action file, the /run/urnetwork pre-create, the +# NetworkManager and udev reloads (both of those files are `backup =` +# conffiles an upgrade may have replaced), and the systemd daemon-reload. +# Restating any of it here would guarantee the two copies drift; worse, the +# NEXT thing added to post_install would silently never reach an existing user, +# because Arch does not run the install hooks on an upgrade the way dpkg and +# rpm both do. +# +# This works because nfpm emits every hook into ONE .INSTALL file and pacman +# sources the whole file before calling any function, so post_install is +# already defined here. It is also the idiomatic PKGBUILD .install shape +# (`post_upgrade() { post_install "$@"; }`); the only addition is the phase +# variable, so post_install can tell the two callers apart without trying to +# infer it from the version strings. +# +# WHAT post_install DOES DIFFERENTLY IN THIS PHASE, and why: +# * it does NOT re-enable the unit -- enabling exactly once, on the initial +# install, is what makes an admin's `systemctl disable` survive upgrades on +# a distribution with no deb-systemd-helper state file and no preset +# convention; +# * it starts the unit only if pre_upgrade recorded that it was running, so +# an upgrade never starts a daemon an admin had stopped; +# * it stays quiet -- the first-run polkit/group guidance is printed on a +# fresh install only. +set -e + +# Set BEFORE the call: post_install reads it as ${UR_ARCH_PHASE:-install}, and +# both functions run in the same shell process, so a plain variable is enough. +UR_ARCH_PHASE=upgrade +export UR_ARCH_PHASE + +post_install "$1" + +return 0 diff --git a/packaging/arch/scripts/pre_install b/packaging/arch/scripts/pre_install new file mode 100644 index 0000000..a0bb1dd --- /dev/null +++ b/packaging/arch/scripts/pre_install @@ -0,0 +1,47 @@ +#!/bin/sh +# pre_install() for urnetwork-daemon -- the Arch install hook. +# +# PACMAN ARGUMENT: "$1" is the version being installed ("2026.8.20.1024376890 +# .beta-1"). There is no "$2" AT ALL -- not empty, UNSET (measured: nargs=1, +# "${2-UNSET}" -> UNSET), so a bare $2 aborts the scriptlet under `set -u`. +# +# THIS IS NOT A DEBIAN MAINTAINER SCRIPT AND NOT AN RPM SCRIPTLET. pacman never +# passes 'configure', 'remove' or 'purge', and never passes 0/1/2 either. Any +# `case "$1" in configure)` or `[ "$1" -eq 1 ]` branch carried over from +# packaging/deb/scripts/ or packaging/rpm/scripts/ would simply never match, +# and the failure is SILENT -- the package installs with exit code 0 and does +# nothing. Each function here IS its own branch; that is the whole point of +# there being six of them. +# +# WHAT THIS DOES: creates the `urnetwork` system group, and nothing else. +# +# It is here rather than in post_install for the ordering reason Fedora's +# guidelines give and packaging/rpm/scripts/pre already follows: post_install +# pre-creates /run/urnetwork with `-g urnetwork` on a machine with no polkit, +# so the group has to exist before any file that references it. pacman runs +# pre_install before extracting a single file, which is exactly the right slot. +# +# THE GROUP IS THE FALLBACK AUTHORIZER, AND NOBODY IS PUT IN IT. Where polkit +# is present the socket is authorized per-action against the peer's SO_PEERCRED +# uid and membership is inert. Where it is absent, urnetworkd binds the socket +# 0660 root:urnetwork and consults this group. The `usermod -aG` + "log out and +# back in" pair that used to follow is deliberately gone: supplementary groups +# are applied at LOGIN, so it made a correct install unusable until the user +# ended their session. See packaging/deb/scripts/postinst, which this mirrors. +# +# Created on install and DELIBERATELY NEVER REMOVED -- see post_remove. +set -e + +# `groupadd --system`, not Debian's `addgroup`: there is no addgroup on Arch. +# groupadd comes from `shadow`, which this package depends on. +if ! getent group urnetwork >/dev/null 2>&1; then + groupadd --system urnetwork >/dev/null 2>&1 || : +fi + +# `return 0`, NEVER `exit 0`. nfpm inlines this file VERBATIM inside +# `function pre_install() { ... }` in the generated .INSTALL, so an `exit` +# terminates pacman's whole scriptlet shell rather than this function -- +# harmless as the very last statement, fatal for any function that calls +# another (post_upgrade calls post_install, and measured, a trailing `exit 0` +# in the callee silently swallowed everything after the call). +return 0 diff --git a/packaging/arch/scripts/pre_remove b/packaging/arch/scripts/pre_remove new file mode 100644 index 0000000..d7a43cf --- /dev/null +++ b/packaging/arch/scripts/pre_remove @@ -0,0 +1,51 @@ +#!/bin/sh +# pre_remove() for urnetwork-daemon. +# +# PACMAN ARGUMENT: "$1" is the version being removed. "$2" is unset. +# Runs ONLY on a real removal -- NOT on an upgrade, and not on a downgrade or a +# reinstall either. That is the opposite of dpkg, where prerm runs on both +# remove and upgrade and packaging/deb/scripts/prerm relies on it. Nothing here +# needs an "am I being upgraded?" guard, because pacman never calls this +# function in that case. +# +# `disable --now`, AND IT MUST HAPPEN HERE RATHER THAN IN post_remove. This is +# a real safety property of this package, not a formality, and it is the same +# argument packaging/rpm/scripts/preun makes at length: +# +# The unit's ExecStopPost= is +# /usr/lib/urnetwork/urnetworkd --revert-unless-armed +# i.e. THE DAEMON'S OWN BINARY is what removes `table inet urnetwork`, the +# policy rules and the capture routes on every stop path. pre_remove runs +# BEFORE pacman deletes a single file, so that binary still exists when +# systemd runs it. Move the stop to post_remove and the binary is already +# gone, ExecStopPost fails 203/EXEC, and a removal can leave the kill-switch +# ruleset in the kernel with nothing left on the machine able to lift it -- +# a box off the network after `pacman -R`. Do not move this. +# +# `systemctl` directly: there is no deb-systemd-helper and no deb-systemd-invoke +# on Arch, so there is no enable-state file to consult and no policy-rc.d to +# respect. Those two mechanisms have no Arch equivalent and are deliberately +# dropped rather than faked. +set -e + +UNIT='urnetworkd.service' + +if [ -d /run/systemd/system ]; then + # THE ARMED KILL SWITCH IS THE ONE THING A REMOVAL CANNOT FIX FOR ITSELF. + # /run/urnetwork/kill-switch-armed is the marker that says "this machine + # was armed when the daemon last stopped", and --revert-unless-armed + # deliberately PRESERVES the block floor in that case so a crash does not + # open a window. That design turns a removal into a trap: the floor stays + # in the kernel and the only binary that knows how to remove it is about to + # be deleted. Checked while the marker and the binary both still exist, and + # only reported -- silently lifting a firewall the user asked for would be + # its own bug, and the recovery is one command. + if [ -f /run/urnetwork/kill-switch-armed ]; then + echo "urnetwork: WARNING -- the kill switch is ARMED on this machine." >&2 + echo "urnetwork: removing the package leaves that nftables floor in place. Lift it with:" >&2 + echo "urnetwork: sudo nft delete table inet urnetwork" >&2 + fi + systemctl --no-reload disable --now "${UNIT}" >/dev/null 2>&1 || : +fi + +return 0 diff --git a/packaging/arch/scripts/pre_upgrade b/packaging/arch/scripts/pre_upgrade new file mode 100644 index 0000000..be60219 --- /dev/null +++ b/packaging/arch/scripts/pre_upgrade @@ -0,0 +1,62 @@ +#!/bin/sh +# pre_upgrade() for urnetwork-daemon -- the stop half of the upgrade dance. +# +# PACMAN ARGUMENTS: "$1" is the INCOMING version, "$2" the OUTGOING one. +# Read them as incoming/outgoing, NOT as new/old: this same function runs on a +# DOWNGRADE (measured: arg1=2026.8.11-1 arg2=2026.8.12-1, i.e. arg1 lower) and +# on a REINSTALL of the identical version (arg1 == arg2). Any "upgrading from +# older than X" logic keyed on "$2 is old, $1 is new" is wrong in two of the +# three cases, so this script reads neither. +# +# THE REINSTALL CASE IS THE ONE TO REMEMBER: `pacman -U` of the same file, and +# `pacman -S` of an already-installed package, both route HERE and not through +# pre_install/post_install. Without this hook declared, a tester's "just +# reinstall the package" is a silent no-op -- which is very likely how the +# omission would first be noticed in the field. +# +# WHY THE STOP IS HERE AND NOT AFTER THE FILES MOVE. +# +# pacman has not replaced a single file yet when this runs, so +# /usr/lib/urnetwork/urnetworkd is still the OLD binary -- and the unit's +# ExecStopPost=-/usr/lib/urnetwork/urnetworkd --revert-unless-armed +# is what removes `table inet urnetwork`, the policy rules and the capture +# routes on every stop path. Stopping now means that teardown is performed by +# the exact binary that built the ruleset. This is the .deb's discipline +# verbatim (packaging/deb/scripts/prerm: "stop the unit on BOTH remove and +# upgrade ... The daemon holds a live tun fd; the upgrade discipline is stop +# (old binary) -> replace files -> start (new binary)"). The .rpm cannot do +# this because rpm has no pre-replacement slot for the OLD package; pacman +# does, so this package takes it. +# +# THE OTHER HALF OF THE ARCH DIFFERENCE: pre_remove/post_remove do NOT run on +# an upgrade either. On Debian the discipline is split across two scripts -- +# prerm stops, postinst starts. Here there is no such split to inherit: the +# entire stop-then-start dance has to live in pre_upgrade/post_upgrade, or it +# does not happen at all. +set -e + +UNIT='urnetworkd.service' +RESTART_MARKER='/run/urnetwork.pacman-restart' + +# Record whether the daemon was RUNNING, so post_upgrade can put it back the +# way it found it and no other way. There is no `try-restart` shortcut +# available: by the time post_upgrade runs we will have stopped the unit +# ourselves, so "is it active now" no longer answers "was it active before". +# +# /run is a tmpfs owned by root and is guaranteed writable here -- this whole +# block only runs when systemd is booted, which means we are root on a live +# system. A stale marker (an interrupted transaction) can only cause an extra +# start of a daemon that comes up idle, which is the safe direction. +rm -f "${RESTART_MARKER}" 2>/dev/null || : +if [ -d /run/systemd/system ]; then + if systemctl is-active --quiet "${UNIT}" 2>/dev/null; then + : > "${RESTART_MARKER}" 2>/dev/null || : + fi + # `stop`, never `disable`: enablement is post_install's business exactly + # once, and an upgrade that disabled and re-enabled would quietly undo an + # admin's decision. Stopping tears the tunnel down through the daemon's own + # shutdown path; it comes back idle, exactly like a reboot. + systemctl stop "${UNIT}" >/dev/null 2>&1 || : +fi + +return 0 diff --git a/packaging/deb/nfpm.yaml b/packaging/deb/nfpm.yaml index f2d8605..4877706 100644 --- a/packaging/deb/nfpm.yaml +++ b/packaging/deb/nfpm.yaml @@ -97,7 +97,7 @@ contents: # APPIMAGE.md 11d). # # The polkit action file rides inside this same /usr tree, at - # usr/share/polkit-1/actions/network.ur.urnetwork.policy, put there by + # usr/share/polkit-1/actions/com.bringyour.network.policy, put there by # linux/packaging/lib/common.sh (assemble_daemon_root), which also asserts # its 0644 mode before the package is built. It is deliberately NOT given a # second explicit contents entry: nfpm would then emit the path twice, and @@ -112,8 +112,8 @@ contents: dst: /lib type: tree # Admin-editable integration files: conffiles, preserved on upgrade. - - src: "${PKGROOT}/etc/urnetwork/autostart/network.ur.urnetwork.desktop" - dst: /etc/urnetwork/autostart/network.ur.urnetwork.desktop + - src: "${PKGROOT}/etc/urnetwork/autostart/com.bringyour.network.desktop" + dst: /etc/urnetwork/autostart/com.bringyour.network.desktop type: config - src: "${PKGROOT}/etc/NetworkManager/conf.d/95-urnetwork.conf" dst: /etc/NetworkManager/conf.d/95-urnetwork.conf diff --git a/packaging/deb/scripts/postinst b/packaging/deb/scripts/postinst index ff6ebae..cc2e41b 100644 --- a/packaging/deb/scripts/postinst +++ b/packaging/deb/scripts/postinst @@ -57,7 +57,7 @@ case "$1" in # Which authority this machine will run under, said out loud. # # DETECTED BY LOOKING FOR POLKIT, NOT FOR OUR OWN ACTION FILE: dpkg has - # already unpacked /usr/share/polkit-1/actions/network.ur.urnetwork + # already unpacked /usr/share/polkit-1/actions/com.bringyour.network # .policy by the time this runs (and created that directory on the way), # so testing the file or the directory would report "polkit" on a box # that has never had it. polkitd is a Recommends, so apt pulls it in by @@ -88,7 +88,7 @@ case "$1" in # from the package tree; assert it rather than assume it, and check the # directory too (polkit does not police the directory, and whoever can # write it can drop in a file that redefines our defaults). - UR_POLICY=/usr/share/polkit-1/actions/network.ur.urnetwork.policy + UR_POLICY=/usr/share/polkit-1/actions/com.bringyour.network.policy if [ -f "${UR_POLICY}" ]; then chmod 0644 "${UR_POLICY}" 2>/dev/null || true chown root:root "${UR_POLICY}" 2>/dev/null || true diff --git a/packaging/distro-smoke.sh b/packaging/distro-smoke.sh index fdd923c..56eb03b 100755 --- a/packaging/distro-smoke.sh +++ b/packaging/distro-smoke.sh @@ -305,14 +305,18 @@ fi # ========================================================================== head2 "2. how URnetwork gets installed here" # ========================================================================== -# The release publishes exactly four assets today and their names are a -# contract (the in-app service checker parses them): +# The release asset names are a contract (the in-app service checker parses +# them): # urnetwork-daemon--.install.tar.gz # urnetwork-daemon__.deb -# URnetwork--.AppImage (+ .zsync) -# There is NO .rpm and NO flatpak bundle in the release yet. On an rpm distro -# the tarball installer is the whole story, and this check says so rather than -# implying a package that does not exist. +# urnetwork-daemon-..rpm +# urnetwork-daemon--.pkg.tar.zst +# URnetwork--.AppImage (+ .zsync, + .flatpak) +# All four daemon packages are the same payload out of one staging tree +# (packaging/lib/common.sh assemble_daemon_root), so "which channel" is a +# question about THIS HOST, not about which build is newer. Whether a given +# release actually carries a given package is still a fact about the RELEASE, +# which is why the branches below hedge rather than promise. PKG_CHANNEL='' if have dpkg; then @@ -334,7 +338,15 @@ elif have apk; then check 2.1 BLOCKER "native package format" "apk (Alpine): musl, and no musl build exists" elif have pacman; then PKG_CHANNEL='pacman' - check 2.1 WARN "native package format" "pacman present; no Arch package is published -- use the tarball" + check 2.1 ok "native package format" "pacman present -> urnetwork-daemon--$(uname -m).pkg.tar.zst, if the release carries one" + note "Install it with: sudo pacman -U ./urnetwork-daemon--$(uname -m).pkg.tar.zst" + note "It declares nftables and fuse2, which a minimal Arch install does not have and" + note "which the tarball can only tell you about after the fact. If the release has no" + note "pacman package, the install tarball works here too -- but never both on one" + note "machine: each refuses to overwrite the other's files." + if [ -r /usr/lib/os-release ] && grep -qi '^ID=steamos' /usr/lib/os-release 2>/dev/null; then + check 2.1 WARN "native package format" "SteamOS: /usr is read-only, 'pacman -U' needs 'steamos-readonly disable' and the next system update reverts it -- use the install tarball, which installs under /usr/local" + fi elif have zypper; then PKG_CHANNEL='zypper' check 2.1 WARN "native package format" "zypper present; no .rpm is published -- use the tarball" @@ -347,7 +359,7 @@ if have flatpak; then FP_REMOTES="${FP_REMOTES%% }" check 2.2 ok "flatpak" "present; remotes: ${FP_REMOTES:-}" note "No .flatpak bundle is published in the release yet either -- the manifest" - note "at packaging/flatpak/network.ur.urnetwork.yml builds one locally." + note "at packaging/flatpak/com.bringyour.network.yml builds one locally." else check 2.2 n/a "flatpak" "not installed (only matters for the GUI, never the daemon)" fi @@ -371,8 +383,10 @@ elif [ -n "${FUSE2}" ]; then else check 2.3 WARN "AppImage runtime (libfuse2)" "libfuse.so.2 not found -- the GUI AppImage will not mount" note "Fix per family: apt install libfuse2t64 (or libfuse2) | dnf install fuse-libs" - note "Or run the AppImage with --appimage-extract-and-run. The .deb declares this" - note "dependency for you; the tarball and the bare AppImage cannot." + note " pacman -S fuse2" + note "Or run the AppImage with --appimage-extract-and-run. The .deb and the pacman" + note "package declare this dependency for you; the tarball and the bare AppImage" + note "cannot." fi # The layout the tarball installer WILL choose, by the same rules install.sh @@ -922,8 +936,8 @@ fi GUI_FOUND='' [ -x /usr/bin/urnetwork ] && GUI_FOUND="/usr/bin/urnetwork" [ -x /usr/local/bin/urnetwork ] && GUI_FOUND="${GUI_FOUND} /usr/local/bin/urnetwork" -if have flatpak && flatpak list --app --columns=application 2>/dev/null | grep -qx 'network.ur.urnetwork'; then - GUI_FOUND="${GUI_FOUND} flatpak:network.ur.urnetwork" +if have flatpak && flatpak list --app --columns=application 2>/dev/null | grep -qx 'com.bringyour.network'; then + GUI_FOUND="${GUI_FOUND} flatpak:com.bringyour.network" fi if [ -n "${GUI_FOUND}" ]; then check 9.6 ok "GUI" "${GUI_FOUND# }" diff --git a/packaging/flatpak/network.ur.urnetwork.yml b/packaging/flatpak/com.bringyour.network.yml similarity index 97% rename from packaging/flatpak/network.ur.urnetwork.yml rename to packaging/flatpak/com.bringyour.network.yml index 7bc1238..7be0e0a 100644 --- a/packaging/flatpak/network.ur.urnetwork.yml +++ b/packaging/flatpak/com.bringyour.network.yml @@ -23,8 +23,8 @@ # LOCAL INSTALL (the current target — Flathub submission comes later): # flatpak install -y flathub org.gnome.Platform//49 org.gnome.Sdk//49 # flatpak run org.flatpak.Builder --force-clean --user --install \ -# build-flatpak packaging/flatpak/network.ur.urnetwork.yml -# flatpak run network.ur.urnetwork +# build-flatpak packaging/flatpak/com.bringyour.network.yml +# flatpak run com.bringyour.network # # FOR FLATHUB, two changes are required and are deliberately NOT made here: # 1. `sources: [{type: dir, path: ../..}]` must become a git/archive source @@ -34,7 +34,7 @@ # redistribution; that audit (§7.17) must land before submission. # # SPDX-License-Identifier: MPL-2.0 -id: network.ur.urnetwork +id: com.bringyour.network runtime: org.gnome.Platform runtime-version: '49' sdk: org.gnome.Sdk @@ -192,9 +192,9 @@ modules: # no AppImage here and no /usr/bin to put a launcher in, so the Flatpak # runs the GUI binary directly. - desktop-file-edit --set-key=Exec --set-value="urnetwork-gui %u" - /app/share/applications/network.ur.urnetwork.desktop + /app/share/applications/com.bringyour.network.desktop - desktop-file-edit --remove-key=TryExec - /app/share/applications/network.ur.urnetwork.desktop + /app/share/applications/com.bringyour.network.desktop sources: # LOCAL SOURCE — replace with a git/archive source before any Flathub # submission (see the header). The path is relative to this manifest. diff --git a/packaging/lib/common.sh b/packaging/lib/common.sh index 2f58e19..f40bdae 100644 --- a/packaging/lib/common.sh +++ b/packaging/lib/common.sh @@ -124,11 +124,11 @@ assemble_daemon_root() { # --- static integration files: canonical sources in app/packaging ------- local src="${APP_PACKAGING_DIR}" local f - for f in urnetwork-launcher urnetworkd.service network.ur.urnetwork.desktop \ - autostart/network.ur.urnetwork.desktop 95-urnetwork.conf \ + for f in urnetwork-launcher urnetworkd.service com.bringyour.network.desktop \ + autostart/com.bringyour.network.desktop 95-urnetwork.conf \ 85-urnetwork-unmanaged.rules \ - icons/hicolor/48x48/apps/urnetwork.png \ - icons/hicolor/256x256/apps/urnetwork.png; do + icons/hicolor/48x48/apps/com.bringyour.network.png \ + icons/hicolor/256x256/apps/com.bringyour.network.png; do [ -f "${src}/${f}" ] || die "packaging source missing: ${src}/${f}" done @@ -140,17 +140,17 @@ assemble_daemon_root() { cp "${src}/urnetworkd.service" "${root}/lib/systemd/system/urnetworkd.service" install -d "${root}/usr/share/applications" - cp "${src}/network.ur.urnetwork.desktop" "${root}/usr/share/applications/" + cp "${src}/com.bringyour.network.desktop" "${root}/usr/share/applications/" install -d "${root}/usr/share/icons/hicolor/48x48/apps" \ "${root}/usr/share/icons/hicolor/256x256/apps" - cp "${src}/icons/hicolor/48x48/apps/urnetwork.png" \ - "${root}/usr/share/icons/hicolor/48x48/apps/urnetwork.png" - cp "${src}/icons/hicolor/256x256/apps/urnetwork.png" \ - "${root}/usr/share/icons/hicolor/256x256/apps/urnetwork.png" + cp "${src}/icons/hicolor/48x48/apps/com.bringyour.network.png" \ + "${root}/usr/share/icons/hicolor/48x48/apps/com.bringyour.network.png" + cp "${src}/icons/hicolor/256x256/apps/com.bringyour.network.png" \ + "${root}/usr/share/icons/hicolor/256x256/apps/com.bringyour.network.png" install -d "${root}/etc/urnetwork/autostart" - cp "${src}/autostart/network.ur.urnetwork.desktop" "${root}/etc/urnetwork/autostart/" + cp "${src}/autostart/com.bringyour.network.desktop" "${root}/etc/urnetwork/autostart/" install -d "${root}/etc/NetworkManager/conf.d" cp "${src}/95-urnetwork.conf" "${root}/etc/NetworkManager/conf.d/" @@ -174,11 +174,11 @@ assemble_daemon_root() { # world-writable .policy file, so a wrong mode here does not fail loudly, # it silently drops every action back to its built-in default. The mode # normalization below sets it; the assertion after it proves it. - [ -f "${PACKAGING_DIR}/polkit/network.ur.urnetwork.policy" ] || \ - die "packaging source missing: ${PACKAGING_DIR}/polkit/network.ur.urnetwork.policy" + [ -f "${PACKAGING_DIR}/polkit/com.bringyour.network.policy" ] || \ + die "packaging source missing: ${PACKAGING_DIR}/polkit/com.bringyour.network.policy" install -d "${root}/usr/share/polkit-1/actions" - cp "${PACKAGING_DIR}/polkit/network.ur.urnetwork.policy" \ - "${root}/usr/share/polkit-1/actions/network.ur.urnetwork.policy" + cp "${PACKAGING_DIR}/polkit/com.bringyour.network.policy" \ + "${root}/usr/share/polkit-1/actions/com.bringyour.network.policy" # Normalize modes: directories 0755; everything except the two # executables 0644 (shared libraries ship 0644 on Debian). @@ -191,7 +191,7 @@ assemble_daemon_root() { # and skips the file), and the visible symptom would be "every Connect asks # for an admin password" long after the build. local policy_mode - policy_mode="$(_file_mode_octal "${root}/usr/share/polkit-1/actions/network.ur.urnetwork.policy")" + policy_mode="$(_file_mode_octal "${root}/usr/share/polkit-1/actions/com.bringyour.network.policy")" [ "${policy_mode}" = '644' ] || \ die "polkit action file is mode ${policy_mode}, must be 644 (polkit ignores a group- or world-writable .policy)" } diff --git a/packaging/make-appimage.sh b/packaging/make-appimage.sh index a283e0d..e064097 100755 --- a/packaging/make-appimage.sh +++ b/packaging/make-appimage.sh @@ -188,22 +188,22 @@ done < <(find "${STAGING}/usr/share/locale" -type f -name 'urnetwork.mo' 2>/dev/ # Desktop entry: same app-id filename as the system one (wayland app_id -> # icon association), but Exec points at the bundled binary -- inside the # AppDir the launcher script does not exist. AppRun is what actually runs. -DESKTOP_SRC="${APP_PACKAGING_DIR}/network.ur.urnetwork.desktop" +DESKTOP_SRC="${APP_PACKAGING_DIR}/com.bringyour.network.desktop" [ -f "${DESKTOP_SRC}" ] || die "missing ${DESKTOP_SRC}" install -d "${APPDIR}/usr/share/applications" sed -e 's/^Exec=urnetwork /Exec=urnetwork-gui /' \ -e 's/^TryExec=urnetwork$/TryExec=urnetwork-gui/' \ - "${DESKTOP_SRC}" > "${APPDIR}/usr/share/applications/network.ur.urnetwork.desktop" -printf 'X-AppImage-Version=%s\n' "${VERSION}" >> "${APPDIR}/usr/share/applications/network.ur.urnetwork.desktop" -cp "${APPDIR}/usr/share/applications/network.ur.urnetwork.desktop" "${APPDIR}/network.ur.urnetwork.desktop" + "${DESKTOP_SRC}" > "${APPDIR}/usr/share/applications/com.bringyour.network.desktop" +printf 'X-AppImage-Version=%s\n' "${VERSION}" >> "${APPDIR}/usr/share/applications/com.bringyour.network.desktop" +cp "${APPDIR}/usr/share/applications/com.bringyour.network.desktop" "${APPDIR}/com.bringyour.network.desktop" install -d "${APPDIR}/usr/share/icons/hicolor/256x256/apps" "${APPDIR}/usr/share/icons/hicolor/48x48/apps" -install -m 0644 "${APP_PACKAGING_DIR}/icons/hicolor/256x256/apps/urnetwork.png" \ - "${APPDIR}/usr/share/icons/hicolor/256x256/apps/urnetwork.png" -install -m 0644 "${APP_PACKAGING_DIR}/icons/hicolor/48x48/apps/urnetwork.png" \ - "${APPDIR}/usr/share/icons/hicolor/48x48/apps/urnetwork.png" -install -m 0644 "${APP_PACKAGING_DIR}/icons/hicolor/256x256/apps/urnetwork.png" "${APPDIR}/urnetwork.png" -cp "${APPDIR}/urnetwork.png" "${APPDIR}/.DirIcon" +install -m 0644 "${APP_PACKAGING_DIR}/icons/hicolor/256x256/apps/com.bringyour.network.png" \ + "${APPDIR}/usr/share/icons/hicolor/256x256/apps/com.bringyour.network.png" +install -m 0644 "${APP_PACKAGING_DIR}/icons/hicolor/48x48/apps/com.bringyour.network.png" \ + "${APPDIR}/usr/share/icons/hicolor/48x48/apps/com.bringyour.network.png" +install -m 0644 "${APP_PACKAGING_DIR}/icons/hicolor/256x256/apps/com.bringyour.network.png" "${APPDIR}/com.bringyour.network.png" +cp "${APPDIR}/com.bringyour.network.png" "${APPDIR}/.DirIcon" install -m 0755 "${SCRIPT_DIR}/appimage/AppRun" "${APPDIR}/AppRun" diff --git a/packaging/make-arch.sh b/packaging/make-arch.sh new file mode 100755 index 0000000..49483dd --- /dev/null +++ b/packaging/make-arch.sh @@ -0,0 +1,508 @@ +#!/bin/bash +# Build urnetwork-daemon--.pkg.tar.zst -- the ARCH-FAMILY native +# channel (Arch, CachyOS, EndeavourOS, Manjaro), and the format this project +# answered with a tarball because nothing else existed. +# +# Same shape, same input contract and same entry-point convention as +# packaging/make-deb.sh and packaging/make-rpm.sh: called with VERSION, ARCH +# (amd64|arm64), STAGING_DIR (the `meson install --destdir` tree) and OUT_DIR +# in the environment. Flags override the environment for manual runs: +# make-arch.sh [--staging ] [--arch ] [--out ] [--version ] +# +# WHY nfpm AND NOT A PKGBUILD: the long answer is in the header of +# packaging/arch/nfpm.yaml. The short one is the same as the .rpm's: all three +# native packages are assembled from ONE `meson install --destdir` staging tree +# through packaging/lib/common.sh assemble_daemon_root(), so they cannot drift. +# A PKGBUILD would be a fourth, independent copy of the installed-path table -- +# and it would also need an Arch machine or a container to run makepkg, which +# neither the macOS build server nor the workflow's ubuntu:22.04 container is. +# nfpm is pure Go down to its zstd, so this runs anywhere the .deb does. +# +# THREE THINGS THIS SCRIPT DOES THAT make-deb.sh DOES NOT +# ------------------------------------------------------- +# 1. It MOVES THE UNIT to /usr/lib/systemd/system, like make-rpm.sh, but for a +# harder reason than rpm's: on Arch /lib is a SYMLINK to usr/lib owned by +# the `filesystem` package, and a .pkg.tar.zst carrying any member under +# lib/ does not merely offend a guideline -- pacman ABORTS the transaction +# with "/lib exists in filesystem (owned by filesystem)" and installs +# nothing. It is also the path Arch's own alpm hooks watch +# (30-systemd-daemon-reload-system.hook targets usr/lib/systemd/system/*). +# 2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not +# contain '-' at all and nfpm silently destroys the other obvious shapes. +# See pkg_fields(), which is a THIRD different answer from make-deb.sh's +# last-hyphen split and make-rpm.sh's first-hyphen split. +# 3. It PINS ONE TIMESTAMP AND VERIFIES THE .MTREE, because `pacman -Qkk` -- +# the integrity check a careful user runs against a VPN daemon -- is broken +# by two separate nfpm behaviours and both are silent: +# * `type: tree` writes Go's unmasked fs.FileMode (mode=20000000755) for +# every directory, so every directory reports as altered forever. The +# config uses a glob instead; this script asserts the fix held. +# * the tar header mtime and .MTREE's `time=` come from different clocks, +# so a one-second gap between assembling the root and writing the +# archive makes every FILE report as altered. This script pins one +# timestamp (SOURCE_DATE_EPOCH when set) and asserts every .MTREE entry +# carries it. That is intermittent otherwise -- a build where both land +# in the same second passes by luck -- which is exactly why it is +# asserted rather than reviewed. +# +# ENVIRONMENT KNOBS (all optional) +# UR_ARCH_CANONICAL_NAME=1 name the output the canonical pacman +# ---.pkg.tar.zst +# instead of the release-asset name. For building +# a real pacman repo with repo-add; NOT for CI +# (see "THE OUTPUT NAME"). +# UR_SIGN_KEY= as in make-deb.sh/make-rpm.sh: a DETACHED .asc +# beside the artifact. NOTE this is NOT a pacman +# package signature -- see the note at the bottom. +# +# WHAT THIS SCRIPT CANNOT TELL YOU. It builds and inspects a package; it never +# installs one. Every function in packaging/arch/scripts/ is unexecuted until a +# real pacman runs it. Do not read a green build here as a green install. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source-path=SCRIPTDIR +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/lib/common.sh" + +usage() { + sed -n '2,11p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' + exit "${1:-0}" +} + +STAGING="${STAGING_DIR:-}" +ARCH="${ARCH:-}" +OUT="${OUT_DIR:-}" +VERSION="${VERSION:-${EXTERNAL_WARP_VERSION:-}}" +while [ $# -gt 0 ]; do + case "$1" in + --staging) STAGING="${2:?}"; shift 2 ;; + --arch) ARCH="${2:?}"; shift 2 ;; + --out) OUT="${2:?}"; shift 2 ;; + --version) VERSION="${2:?}"; shift 2 ;; + -h|--help) usage 0 ;; + *) die "unknown argument: $1 (see --help)" ;; + esac +done + +[ -n "${STAGING}" ] || die "STAGING_DIR (or --staging) is required: the meson install --destdir tree" +[ -n "${OUT}" ] || die "OUT_DIR (or --out) is required" +[ -n "${VERSION}" ] || die "VERSION (or --version) is required" + +# The ASSET arch spelling stays Debian (amd64/arm64) on the way IN -- it is the +# contract at the top of .github/workflows/beta-build.yml, every packaging +# script here takes the same --arch values, and common.sh's check_payload_arch +# understands only those two. The PACMAN arch spelling is separate, and it is +# what goes in .PKGINFO and in the artifact name. +case "${ARCH}" in + amd64) PKG_ARCH='x86_64' ;; + arm64) PKG_ARCH='aarch64' ;; + *) die "ARCH (or --arch) must be amd64 or arm64 (got '${ARCH:-}')" ;; +esac + +command -v nfpm >/dev/null 2>&1 || die \ + "nfpm not found -- install with: brew install nfpm (or: go install github.com/goreleaser/nfpm/v2/cmd/nfpm@latest)" + +STAGING="$(cd "${STAGING}" && pwd)" +mkdir -p "${OUT}" +OUT="$(cd "${OUT}" && pwd)" + +# --------------------------------------------------------------------------- +# VERSION -> pacman pkgver / pkgrel +# --------------------------------------------------------------------------- +# THE THIRD DIFFERENT ANSWER, AND IT HAS TO BE. The .deb's '~' lesson and the +# .rpm's Version/Release lesson recur here a third time, and no two of the +# three scripts derive their version fields the same way: +# +# make-deb.sh splits at the LAST hyphen -> 2026.8.20-1024376890-beta +# make-rpm.sh splits at the FIRST hyphen -> 2026.8.20 + 0.1024376890.beta +# make-arch.sh splits at NEITHER -> 2026.8.20.1024376890.beta-1 +# +# pacman's pkgver may not contain '-' AT ALL: '-' is the pkgver/pkgrel +# separator and pacman splits at the LAST one, rejecting anything else with +# "package version contains invalid characters / invalid or corrupted package". +# So the whole VERSION is folded to dots and pkgrel is pinned to the literal 1; +# the build code lives inside pkgver, so pkgrel never has to move. +# +# THE TWO SHAPES THAT LOOK RIGHT AND ARE NOT (both measured on nfpm 2.47.0): +# * version=2026.8.20, release="1024376890.beta" -- the rpm shape. nfpm +# coerces a non-integer release to 1 SILENTLY and emits +# `pkgver = 2026.8.20-1`. The build code and the beta marker are gone, the +# package still builds and still installs, and every build of a given day +# becomes the same pacman version: `pacman -U` of a newer build then says +# "is up to date -- reinstalling" instead of upgrading. A filename that +# lies about its own contents is worse than a build failure. +# * the raw VERSION with version_schema: none -- emits +# `pkgver = 2026.8.20-1024376890-beta-1`, which nfpm writes without +# complaint and pacman refuses to install at all. +# +# THE ALPHABET IS rpm's MINUS '~', and that is deliberate, not an oversight: +# * alpm does not treat '~' the dpkg way. `vercmp 1.0~beta-1 1.0-1` -> 1, so +# on Arch '~' sorts ABOVE -- the exact inverse of the lesson recorded in +# packaging/deb/nfpm.yaml. Carrying the trick over would import something +# that does the opposite of what its comment says. +# * nfpm strips '~', ':' and spaces from the FILENAME it generates while +# writing them verbatim into .PKGINFO, so "1.0~beta" and "1.0 beta" collide +# on one filename in a single output directory. +# +# ORDERING, verified with vercmp against the real pipeline shapes (alpm +# compares each dot-separated segment numerically, so both rollovers work): +# 2026.8.9.1020000000.beta-1 < 2026.8.20.1024376890.beta-1 +# 2026.8.20.1024351940.beta-1 < 2026.8.20.1024376890.beta-1 +# 2026.8.20.1024376890.beta-1 < 2026.9.1.1030000000.beta-1 +# +# ONE HONEST ASYMMETRY WITH THE .rpm, written down rather than left to be +# rediscovered: `vercmp 2026.8.20.1024376890.beta-1 2026.8.20.1024376890-1` +# -> 1, i.e. the '.beta' suffix sorts ABOVE the same version without it, the +# inverse of make-rpm.sh's deliberate "0."-prefixed Release. It is harmless +# because the build code is monotonic and unique per pipeline run, so no two +# artifacts ever differ only by that suffix. And attribute it correctly: the +# rpm's "0." trick is unavailable because NFPM coerces the release to an +# integer, NOT because pacman requires one -- pacman accepts a fractional +# pkgrel (Arch's own convention allows 1.1 for a minor bump). If ordering ever +# needs a hard reset, nfpm's archlinux packager does support `epoch:` and emits +# it correctly; that, not a fractional pkgrel, is the escape hatch. +pkg_fields() { + local v="$1" ver + ver="$(printf '%s' "${v}" | LC_ALL=C sed -e 's/[^0-9A-Za-z.+_]/./g' -e 's/\.\{2,\}/./g' -e 's/^\.//' -e 's/\.$//')" + [ -n "${ver}" ] || die "cannot derive a pacman pkgver from '${v}'" + printf '%s 1' "${ver}" +} +PKG_VERSION=''; PKG_RELEASE='' +# `die` inside a command substitution only kills the SUBSHELL, so check the +# result rather than trusting set -e to have stopped us (make-rpm.sh's lesson). +read -r PKG_VERSION PKG_RELEASE <<<"$(pkg_fields "${VERSION}")" || true +[ -n "${PKG_VERSION}" ] && [ -n "${PKG_RELEASE}" ] || \ + die "could not derive a pacman pkgver/pkgrel from VERSION='${VERSION}'" +case "${PKG_VERSION}" in + *-*) die "pkgver '${PKG_VERSION}' still contains '-' -- pacman would reject the package outright (pkgver may not contain the pkgver/pkgrel separator)" ;; +esac +log "version: ${VERSION} -> pacman pkgver=${PKG_VERSION} pkgrel=${PKG_RELEASE}" + +# --------------------------------------------------------------------------- +# Package root +# --------------------------------------------------------------------------- +# ${TMPDIR%/}: macOS TMPDIR ends in '/', and the doubled slash breaks nfpm's +# glob matching of contents[].src -- which this config depends on far more than +# its siblings do, since the whole /usr tree is a glob here. +TMP_BASE="${TMPDIR:-/tmp}"; TMP_BASE="${TMP_BASE%/}" +PKGROOT="$(mktemp -d "${TMP_BASE}/urnetwork-arch-root.XXXXXX")" +trap 'rm -rf "${PKGROOT}"' EXIT + +assemble_daemon_root "${STAGING}" "${PKGROOT}" +check_payload_arch "${PKGROOT}" "${ARCH}" + +# --- the unit moves out of /lib, and this one is a HARD BLOCKER ------------- +# common.sh stages the unit at lib/systemd/system, which is right for dpkg and +# fatal for pacman. Measured in a real Arch container with the unit left there: +# error: failed to commit transaction (conflicting files) +# urnetwork-daemon: /lib exists in filesystem (owned by filesystem) +# and nothing was installed -- because /lib is a SYMLINK to usr/lib owned by +# the `filesystem` package. (pacman tolerates dir-vs-dir overlap: after a +# successful install /usr/lib/systemd/system is co-owned by a dozen packages. +# A symlink is the case it refuses.) +# +# There is no YAML-level dodge: nfpm emits an implicit parent-directory member +# for every dst, so declaring the unit as one plain file at +# /lib/systemd/system/urnetworkd.service still produces lib/, lib/systemd/ and +# lib/systemd/system/ members. The relocation has to happen HERE, in +# ${PKGROOT}, before nfpm runs. +# +# IT MUST NOT BE FIXED IN common.sh. packaging/deb/nfpm.yaml declares +# `src: ${PKGROOT}/lib, type: tree`, and nfpm exits 1 with "Add tree: lstat +# .../lib: no such file or directory" the moment that directory stops being +# staged -- so moving this into assemble_daemon_root would break the .deb. +# Per-script remap keeps this additive, exactly as the rpm target already is. +# +# NOT marked config anywhere: /usr/lib/systemd/system is the vendor unit +# directory, admin overrides live in /etc/systemd/system (drop-ins, `systemctl +# edit`), and pacman never writes there. +[ -f "${PKGROOT}/lib/systemd/system/urnetworkd.service" ] || \ + die "assemble_daemon_root did not stage lib/systemd/system/urnetworkd.service -- packaging/lib/common.sh changed shape" +install -d "${PKGROOT}/usr/lib/systemd/system" +install -m 0644 "${PKGROOT}/lib/systemd/system/urnetworkd.service" \ + "${PKGROOT}/usr/lib/systemd/system/urnetworkd.service" +rm -f "${PKGROOT}/lib/systemd/system/urnetworkd.service" +# Nothing else may be dropped on the floor. `! -type d` rather than the rpm's +# `-type f`, because a symlink or an empty subtree common.sh might add later +# would slip past a file-only test and be silently deleted by the rm below. +LIB_LEFTOVERS="$(find "${PKGROOT:?}/lib" -mindepth 1 ! -type d 2>/dev/null || true)" +[ -z "${LIB_LEFTOVERS}" ] || die "assemble_daemon_root staged files under /lib that this script does not know how to remap: +${LIB_LEFTOVERS} +Decide where they belong under /usr (pacman REFUSES any package member under +/lib -- it is a symlink owned by the filesystem package) and extend the remap +above." +rm -rf "${PKGROOT:?}/lib" + +# --------------------------------------------------------------------------- +# ONE TIMESTAMP FOR THE WHOLE PACKAGE +# --------------------------------------------------------------------------- +# See the `mtime:` comment in packaging/arch/nfpm.yaml for what goes wrong +# without this: nfpm takes each tar header's mtime from when it writes the +# archive and each .MTREE `time=` from the source file's ModTime, pacman sets +# the installed file's mtime from the former and checks it against the latter, +# and a one-second gap between assemble_daemon_root's copy and nfpm's write +# makes `pacman -Qkk` report every file as altered, permanently. Pinning one +# value collapses both fields onto it. +# +# SOURCE_DATE_EPOCH is honoured because it is the reproducible-builds +# convention and because it is the only way two builds of the same input can +# produce the same bytes here. `date -u -d @N` is GNU and `date -u -r N` is +# BSD; this script also runs on the macOS build server, so try both rather +# than assume (make-rpm.sh learned the same lesson about sed's '\n'). +PKG_MTIME_EPOCH="${SOURCE_DATE_EPOCH:-$(date -u +%s)}" +case "${PKG_MTIME_EPOCH}" in + ''|*[!0-9]*) die "SOURCE_DATE_EPOCH must be a whole number of seconds (got '${PKG_MTIME_EPOCH}')" ;; +esac +PKG_MTIME="$(date -u -d "@${PKG_MTIME_EPOCH}" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \ + || date -u -r "${PKG_MTIME_EPOCH}" '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || true)" +[ -n "${PKG_MTIME}" ] || die "neither 'date -u -d @N' (GNU) nor 'date -u -r N' (BSD) works on this host -- cannot pin the package timestamp" +log "timestamp: ${PKG_MTIME} (epoch ${PKG_MTIME_EPOCH}${SOURCE_DATE_EPOCH:+, from SOURCE_DATE_EPOCH})" + +# --------------------------------------------------------------------------- +# Render the concrete nfpm config and build +# --------------------------------------------------------------------------- +# nfpm does not expand environment variables in contents[].src (verified for +# the .deb against nfpm v2.47, the version pinned in beta-build.yml), so +# substitute here. +NFPM_CONF="${PKGROOT}/.nfpm.yaml" +sed -e "s|\${PKG_VERSION}|${PKG_VERSION}|g" \ + -e "s|\${PKG_RELEASE}|${PKG_RELEASE}|g" \ + -e "s|\${PKG_ARCH}|${PKG_ARCH}|g" \ + -e "s|\${PKG_MTIME}|${PKG_MTIME}|g" \ + -e "s|\${NFPM_ARCH}|${ARCH}|g" \ + -e "s|\${PKGROOT}|${PKGROOT}|g" \ + "${SCRIPT_DIR}/arch/nfpm.yaml" > "${NFPM_CONF}" + +# THE OUTPUT NAME. +# +# The release-asset contract (top of .github/workflows/beta-build.yml) is that +# the FULL version string is discoverable in the asset name, because the +# in-app checker matches assets on the version it is looking for. Like the +# .rpm, this package keeps that information in its own punctuation and with its +# own arch spelling: +# +# urnetwork-daemon-2026.8.20-1024376890-beta-x86_64.pkg.tar.zst +# +# `pacman -U ./file.pkg.tar.zst` does not care about the filename -- pacman +# reads .PKGINFO, exactly as rpm reads the header -- so the asset name is free +# to serve the checker. +# +# A pacman REPOSITORY is the case that does care: repo-add records the file +# name it finds, and mirrors and humans expect the canonical +# ---.pkg.tar.zst. UR_ARCH_CANONICAL_NAME=1 +# emits that instead. The canonical name is printed on every build either way, +# so whoever builds the repo never has to derive it. +CANONICAL="urnetwork-daemon-${PKG_VERSION}-${PKG_RELEASE}-${PKG_ARCH}.pkg.tar.zst" +if [ "${UR_ARCH_CANONICAL_NAME:-0}" = 1 ]; then + PKG="${OUT}/${CANONICAL}" +else + PKG="${OUT}/urnetwork-daemon-${VERSION}-${PKG_ARCH}.pkg.tar.zst" +fi + +# cwd matters: nfpm resolves the config's relative script paths +# (scripts/post_install, ...) against the working directory. +(cd "${SCRIPT_DIR}/arch" && nfpm package -f "${NFPM_CONF}" -p archlinux -t "${PKG}") + +log "canonical pacman name (what repo-add expects): ${CANONICAL}" + +# --------------------------------------------------------------------------- +# Verification +# --------------------------------------------------------------------------- +# UNLIKE make-deb.sh and make-rpm.sh, this does NOT need the target +# distribution's tools. A .pkg.tar.zst is a plain zstd-compressed tar whose +# first three members are the metadata files, so tar alone reads everything +# that matters: .PKGINFO (the dependency and version metadata pacman actually +# reads), .INSTALL (the six hook functions) and the payload listing. There is +# no `pacman -Qp` equivalent needed and none available -- but there is also no +# excuse for skipping the assertions the way the rpm path has to when rpm is +# absent. +# +# The one external requirement is zstd, because nfpm's output is zstd and GNU +# tar shells out for it. Every fallback is tried before giving up. +PKG_TAR='' +if tar --zstd -tf "${PKG}" >/dev/null 2>&1; then + PKG_TAR='tar --zstd' +elif command -v bsdtar >/dev/null 2>&1 && bsdtar -tf "${PKG}" >/dev/null 2>&1; then + PKG_TAR='bsdtar' +fi + +if [ -n "${PKG_TAR}" ]; then + WORK="$(mktemp -d "${TMP_BASE}/urnetwork-arch-verify.XXXXXX")" + # shellcheck disable=SC2064 # WORK is expanded now on purpose + trap "rm -rf '${PKGROOT}' '${WORK}'" EXIT + ${PKG_TAR} -xf "${PKG}" -C "${WORK}" .PKGINFO .INSTALL + listing="${WORK}/listing" + ${PKG_TAR} -tf "${PKG}" > "${listing}" + + fail=0 + log "" + log "--- .PKGINFO ---" + cat "${WORK}/.PKGINFO" + + # The metadata pacman reads, asserted rather than eyeballed. + grep -qx "pkgname = urnetwork-daemon" "${WORK}/.PKGINFO" || { warn ".PKGINFO pkgname is not urnetwork-daemon"; fail=1; } + grep -qx "pkgver = ${PKG_VERSION}-${PKG_RELEASE}" "${WORK}/.PKGINFO" || { + warn ".PKGINFO pkgver is not '${PKG_VERSION}-${PKG_RELEASE}' -- nfpm re-parsed the version (is version_schema: none still set?)" + fail=1; } + grep -qx "arch = ${PKG_ARCH}" "${WORK}/.PKGINFO" || { warn ".PKGINFO arch is not ${PKG_ARCH}"; fail=1; } + # `maintainer:` does NOT feed this field; only archlinux.packager does, and + # losing it prints "Unknown Packager" in every `pacman -Qi`. + grep -q '^packager = URnetwork' "${WORK}/.PKGINFO" || { + warn ".PKGINFO packager is not set (archlinux.packager missing from the config -- 'Unknown Packager')"; fail=1; } + # The three conffiles must be pacman `backup =` entries, or an admin's edits + # are overwritten on every upgrade instead of landing as .pacnew. + for want in etc/urnetwork/autostart/com.bringyour.network.desktop \ + etc/NetworkManager/conf.d/95-urnetwork.conf \ + etc/udev/rules.d/85-urnetwork-unmanaged.rules ; do + grep -qx "backup = ${want}" "${WORK}/.PKGINFO" || { warn "${want} is not marked 'backup =' -- an upgrade would overwrite an admin's edits"; fail=1; } + done + # The dependencies that are the whole reason to ship a native package. + for want in iproute2 nftables systemd shadow fuse2; do + grep -qx "depend = ${want}" "${WORK}/.PKGINFO" || { warn "missing dependency: ${want}"; fail=1; } + done + # Debian syntax passes through nfpm VERBATIM and unvalidated, producing a + # package that builds cleanly and can never be installed. Catch it here. + if grep -E '^depend = ' "${WORK}/.PKGINFO" | grep -qE '\||\(|\)'; then + warn "a dependency uses Debian alternation ('a | b') or Debian version syntax ('(>= x)') -- pacman resolves those as literal package names and the package becomes uninstallable:" + grep -E '^depend = ' "${WORK}/.PKGINFO" | grep -E '\||\(|\)' >&2 + fail=1 + fi + + log "" + log "--- .INSTALL hooks ---" + # ALL SIX, and the two upgrade hooks are the ones that go missing silently. + # nfpm's generic `scripts:` maps ONLY to the four install/remove functions; + # pre_upgrade/post_upgrade come from the separate archlinux.scripts block, + # and without them every `pacman -U` upgrade, every downgrade and every + # reinstall runs NOTHING AT ALL while pacman reports success. + for fn in pre_install post_install pre_upgrade post_upgrade pre_remove post_remove; do + if grep -qE "^[[:space:]]*(function[[:space:]]+)?${fn}[[:space:]]*\(\)" "${WORK}/.INSTALL"; then + log " ${fn}: present" + else + warn " ${fn}: MISSING from .INSTALL" + # A plain `[ a ] || [ b ] && warn` here would be a set -e landmine: + # when neither test matches, the compound's status is 1 and the + # script would exit before reporting the other five hooks. + case "${fn}" in + pre_upgrade|post_upgrade) + warn " (declare it under archlinux.scripts in packaging/arch/nfpm.yaml -- the top-level scripts: block cannot emit it)" ;; + esac + fail=1 + fi + done + # The deb/rpm argument conventions do not exist here. A `case "$1" in + # configure)` or a `[ "$1" -eq 1 ]` copied from a sibling script never + # matches anything pacman passes, and the package then installs with exit + # code 0 having done nothing -- the exact silent failure that makes this + # worth asserting on the built artifact rather than trusting a review. + # + # COMMENT LINES ARE STRIPPED FIRST, and that is not fussiness: nfpm inlines + # each script file VERBATIM into its function, comments included, and + # packaging/arch/scripts/ discusses both wrong conventions at length in + # order to warn the next reader off them. Grepping the raw .INSTALL made + # this check fire on its own documentation. + _code="${WORK}/.INSTALL.code" + grep -v '^[[:space:]]*#' "${WORK}/.INSTALL" > "${_code}" || true + if grep -qE 'configure\)|"\$1" = "(remove|purge|configure)"' "${_code}"; then + warn ".INSTALL branches on dpkg's 'configure'/'remove'/'purge' -- pacman passes version strings and that branch can never match" + fail=1 + fi + if grep -qE '\[ *"?\$\{?1[:-]?[^ ]*"? *-(eq|ge|le|gt|lt) *[0-9]' "${_code}"; then + warn ".INSTALL compares \$1 numerically like an rpm scriptlet -- pacman passes version strings, not 0/1/2" + fail=1 + fi + + log "" + log "--- payload ($(grep -cv '/$' "${listing}") file entries) ---" + head -25 "${listing}" + + # The files whose absence is silent and fatal. A file count alone passes a + # package that is all locale catalogs and no daemon -- the shape of failure + # the Windows MSI shipped for months. + for want in \ + usr/lib/urnetwork/urnetworkd \ + usr/lib/urnetwork/libURnetworkSdk.so \ + usr/bin/urnetwork \ + usr/lib/systemd/system/urnetworkd.service \ + usr/share/polkit-1/actions/com.bringyour.network.policy \ + usr/share/urnetwork/world-110m.json ; do + grep -Fxq "${want}" "${listing}" || { warn "MISSING from the payload: ${want}"; fail=1; } + done + # THE HARD BLOCKER, asserted in the built artifact rather than trusted from + # the remap above. Widened past /lib on purpose: /bin, /sbin and /lib64 are + # every one of them filesystem-owned symlinks on Arch and would abort the + # transaction in exactly the same way. + if grep -Eq '^(bin|sbin|lib|lib64)/' "${listing}"; then + warn "the package ships members under a filesystem-owned symlink (/bin, /sbin, /lib or /lib64). pacman ABORTS the whole transaction with 'exists in filesystem (owned by filesystem)' and installs nothing:" + grep -E '^(bin|sbin|lib|lib64)/' "${listing}" >&2 + fail=1 + fi + + # THE .MTREE DIRECTORY MODES. nfpm's `type: tree` writes Go's unmasked + # fs.FileMode into .MTREE -- mode=20000000755, i.e. fs.ModeDir|0755 + # formatted with %o and never masked to permission bits. The visible + # results are one "warning: directory permissions differ on / + # filesystem: 755 package: 755" per directory at install time (both sides + # printing 755 is the tell) and `pacman -Qkk urnetwork-daemon` reporting + # those directories as altered FOREVER -- which, for a VPN daemon, is + # exactly the check a careful user runs. The config avoids it with a glob + # instead of a tree; assert that it stayed that way. + ${PKG_TAR} -xf "${PKG}" -C "${WORK}" .MTREE 2>/dev/null || true + if [ -f "${WORK}/.MTREE" ]; then + folded="$(gzip -cd "${WORK}/.MTREE" 2>/dev/null | grep -c 'mode=2[0-9]\{10\}' || true)" + if [ "${folded:-0}" -gt 0 ]; then + warn ".MTREE carries ${folded} directory entries with an unmasked Go file mode (mode=20000000755)." + warn "Every one of them makes pacman print a bogus 'directory permissions differ' warning at install" + warn "time and shows up as an altered file under 'pacman -Qkk' forever. This is what nfpm's" + warn "'type: tree' does; packaging/arch/nfpm.yaml must use the '\${PKGROOT}/usr/**/*' glob instead." + warn "Do NOT 'fix' it with file_info.mode on the tree entry -- that clobbers every FILE to 0755 too." + fail=1 + else + log "mtree: no unmasked directory modes" + fi + # THE OTHER HALF OF `pacman -Qkk`, and the half that is INTERMITTENT -- + # which is why it is asserted rather than reviewed. Every .MTREE entry + # must carry the one pinned timestamp; if any carries the source file's + # own ModTime instead, nfpm ignored `mtime:` and pacman will report + # every file as "Modification time mismatch" on every installed + # machine. A build where the copy and the archive write happen to land + # in the same second passes by luck, so a spot check would not do. + stray="$(gzip -cd "${WORK}/.MTREE" 2>/dev/null | grep -c "time=" || true)" + pinned="$(gzip -cd "${WORK}/.MTREE" 2>/dev/null | grep -c "time=${PKG_MTIME_EPOCH}\." || true)" + if [ "${stray:-0}" -ne "${pinned:-0}" ]; then + warn ".MTREE has ${stray} timestamped entries but only ${pinned} carry the pinned time=${PKG_MTIME_EPOCH}." + warn "nfpm did not honour 'mtime:' for all of them. pacman sets each file's mtime from the TAR" + warn "header and checks it against .MTREE, so every mismatched entry becomes a permanent" + warn "'Modification time mismatch' under 'pacman -Qkk urnetwork-daemon'." + fail=1 + else + log "mtree: all ${pinned} timestamps pinned to ${PKG_MTIME_EPOCH} (pacman -Qkk will be clean)" + fi + fi + + log "" + [ "${fail}" = 0 ] || die "package verification failed (see the warnings above)" + log "package verified." +else + warn "cannot read ${PKG##*/}: no zstd-capable tar on this host (install zstd, or bsdtar)." + warn "The package WAS built -- nfpm's zstd is pure Go -- but every payload, dependency and" + warn "hook assertion above was skipped. Do not ship an unverified build from a host like this." +fi + +log "" +log "NOT executed by this script, and only a real pacman can: the six functions in" +log "packaging/arch/scripts/. A green build here is not a green install." + +sha256_file "${PKG}" +maybe_sign "${PKG}" +# Said explicitly because the word "signed" means something different here than +# it does for the .deb. UR_SIGN_KEY produces a DETACHED .asc beside the file, +# which proves authorship to a human. It is NOT a pacman package signature: a +# pacman repo with SigLevel=Required wants a detached .sig made with the +# repo's key and listed in the database, and `pacman -U` of a local file +# ignores the .asc entirely. +if [ -z "${UR_SIGN_KEY:-}" ]; then + log "not signed. Note that even with UR_SIGN_KEY this produces a detached .asc, NOT the .sig a pacman repo with SigLevel=Required requires." +fi +log "built: ${PKG}" diff --git a/packaging/make-flatpak.sh b/packaging/make-flatpak.sh index c92da22..722431a 100755 --- a/packaging/make-flatpak.sh +++ b/packaging/make-flatpak.sh @@ -5,7 +5,7 @@ # /dev/net/tun, no CAP_NET_ADMIN and no way to install a system unit. The app # reaches the HOST's urnetworkd over /run/urnetwork/control.sock, which the # manifest exposes read-only (Trayscale's pattern). Install the daemon from the -# native .deb/.rpm — see packaging/flatpak/network.ur.urnetwork.yml for the +# native .deb/.rpm — see packaging/flatpak/com.bringyour.network.yml for the # full reasoning. # # Works on an immutable host (Bazzite/Silverblue): flatpak-builder itself runs @@ -20,8 +20,8 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -MANIFEST="${MANIFEST:-$REPO_ROOT/packaging/flatpak/network.ur.urnetwork.yml}" -APP_ID="network.ur.urnetwork" +MANIFEST="${MANIFEST:-$REPO_ROOT/packaging/flatpak/com.bringyour.network.yml}" +APP_ID="com.bringyour.network" RUNTIME_VERSION="${RUNTIME_VERSION:-49}" # The build dir MUST live inside the repo. flatpak-builder runs sandboxed and # gets a PRIVATE /tmp, so a build dir under the host's /tmp vanishes between diff --git a/packaging/polkit/network.ur.urnetwork.policy b/packaging/polkit/com.bringyour.network.policy similarity index 95% rename from packaging/polkit/network.ur.urnetwork.policy rename to packaging/polkit/com.bringyour.network.policy index b33e2ba..d6c0c5f 100644 --- a/packaging/polkit/network.ur.urnetwork.policy +++ b/packaging/polkit/com.bringyour.network.policy @@ -2,7 +2,7 @@ - + Connect or disconnect the URnetwork VPN Authentication is required to route this device's traffic through URnetwork. @@ -81,7 +81,7 @@ a site can require a password for THIS and not for Connect. The kill switch can cut every user on the box off the network, which is why it is worth its own knob. --> - + Change the URnetwork kill switch Authentication is required to change whether this device's traffic is blocked when the URnetwork connection drops. @@ -95,7 +95,7 @@ DIFFERENT uid started. This is the action that replaces "everyone in the urnetwork group is interchangeable": any active local user may run their own tunnel, nobody takes over someone else's without proving admin. --> - + Take over a URnetwork connection started by another user Authentication is required to take over the URnetwork connection another user started on this device. @@ -111,7 +111,7 @@ own. Same default trust as control-tunnel (if you can silently start a tunnel you can read its log), separate id so it can be tightened alone. The daemon ALSO refuses it across uids: see kCodeAuthNotTunnelOwner. --> - + Read the URnetwork service log Authentication is required to read the URnetwork service log. diff --git a/packaging/rpm/nfpm.yaml b/packaging/rpm/nfpm.yaml index 1e43b3b..4ba2282 100644 --- a/packaging/rpm/nfpm.yaml +++ b/packaging/rpm/nfpm.yaml @@ -153,8 +153,8 @@ contents: # Admin-editable integration files. %config(noreplace) is the rpm equivalent # of a dpkg conffile: an edited file is preserved and the package's version # lands beside it as .rpmnew. - - src: "${PKGROOT}/etc/urnetwork/autostart/network.ur.urnetwork.desktop" - dst: /etc/urnetwork/autostart/network.ur.urnetwork.desktop + - src: "${PKGROOT}/etc/urnetwork/autostart/com.bringyour.network.desktop" + dst: /etc/urnetwork/autostart/com.bringyour.network.desktop type: "config|noreplace" - src: "${PKGROOT}/etc/NetworkManager/conf.d/95-urnetwork.conf" dst: /etc/NetworkManager/conf.d/95-urnetwork.conf diff --git a/packaging/tarball/install.sh b/packaging/tarball/install.sh index 8b7ec98..6d855e0 100755 --- a/packaging/tarball/install.sh +++ b/packaging/tarball/install.sh @@ -162,7 +162,7 @@ DNS_WARNING='' # legacy `urnetwork` group check stays in force. POLKIT_PRESENT=0 POLKIT_VERSION='' -POLICY_REL='/usr/share/polkit-1/actions/network.ur.urnetwork.policy' +POLICY_REL='/usr/share/polkit-1/actions/com.bringyour.network.policy' log() { printf '%s\n' "$*"; } note() { printf -- '- %s\n' "$*"; } @@ -552,17 +552,22 @@ fi if command -v rpm >/dev/null 2>&1 && rpm -q "${PKG_NAME}" >/dev/null 2>&1; then die "${PKG_NAME} is installed and owned by rpm -- use dnf/zypper to upgrade or remove it first" fi -# pacman (Arch, CachyOS, Manjaro, EndeavourOS). There is no URnetwork package -# in the Arch repositories or the AUR today, so this is a guard against a -# FUTURE one rather than a live conflict -- and the day one lands, two owners -# of the same paths is exactly the silent-corruption failure dpkg is refused -# for above. +# pacman (Arch, CachyOS, Manjaro, EndeavourOS). THIS IS NOW A LIVE CONFLICT, +# not the hypothetical it was written as: packaging/make-arch.sh ships a real +# urnetwork-daemon .pkg.tar.zst, owning exactly the paths below. Two owners of +# the same paths is the same silent-corruption failure dpkg is refused for +# above -- pacman would not know these files changed, and the next +# `pacman -Syu` or `pacman -R` would half-replace or half-remove the install. # -# Queried by PATH, not by name. An AUR package could be called urnetwork, -# urnetwork-bin, urnetwork-daemon or urnetwork-git, and a name check would miss -# all but one; the path is the thing that actually collides. `pacman -Qo` exits -# non-zero and writes to stderr both when nothing owns the file and when the -# file does not exist, so both are absorbed. +# SteamOS is the one Arch-family host where the tarball is the RIGHT answer and +# this guard should stay quiet: its /usr is read-only, so the pacman package is +# never installed there and pacman owns none of these paths. +# +# Queried by PATH, not by name. Our own package is urnetwork-daemon, but an AUR +# package could equally be urnetwork, urnetwork-bin or urnetwork-git, and a +# name check would miss all but one; the path is the thing that actually +# collides. `pacman -Qo` exits non-zero and writes to stderr both when nothing +# owns the file and when the file does not exist, so both are absorbed. if command -v pacman >/dev/null 2>&1; then PACMAN_OWNER='' PACMAN_OWNED_PATH='' @@ -1339,7 +1344,7 @@ if [ "${POLKIT_PRESENT}" = 1 ]; then # # MEASURED on Bazzite: after a clean install the daemon correctly # reported `authorization: polkit`, and `pkaction --action-id - # network.ur.urnetwork.control-tunnel` answered "No action with action + # com.bringyour.network.control-tunnel` answered "No action with action # id" -- polkitd had been up since the previous boot, two days earlier. # Every authorization check would have been made against an action # polkit did not know, so Connect would have failed on a host the diff --git a/packaging/tarball/uninstall.sh b/packaging/tarball/uninstall.sh index 34a32f6..79c71d0 100755 --- a/packaging/tarball/uninstall.sh +++ b/packaging/tarball/uninstall.sh @@ -109,11 +109,19 @@ fi if command -v rpm >/dev/null 2>&1 && rpm -q "${PKG_NAME}" >/dev/null 2>&1; then die "${PKG_NAME} is owned by rpm -- remove it with: sudo dnf remove ${PKG_NAME}" fi -# ...and never remove a pacman-owned one either. Queried by PATH rather than by -# name for the same reason install.sh does it: a future Arch/AUR package could -# be called urnetwork, urnetwork-bin, urnetwork-daemon or urnetwork-git, and -# the file is what actually collides. `pacman -Qo` exits non-zero both when -# nothing owns the path and when the path does not exist. +# ...and never remove a pacman-owned one either -- which stopped being +# hypothetical when packaging/make-arch.sh landed: releases now carry a real +# urnetwork-daemon .pkg.tar.zst owning these exact paths, and the right removal +# for it is `pacman -R`, which runs the package's own pre_remove hook (stop and +# disable the unit WHILE the binary still exists, so its ExecStopPost can tear +# the nftables ruleset down). This script would delete the binary out from +# under pacman instead. +# +# Queried by PATH rather than by name for the same reason install.sh does it: +# our own package is urnetwork-daemon, but an AUR one could be urnetwork, +# urnetwork-bin or urnetwork-git, and the file is what actually collides. +# `pacman -Qo` exits non-zero both when nothing owns the path and when the path +# does not exist. if command -v pacman >/dev/null 2>&1 && [ -z "${PREFIX}" ]; then for _p in "${LIB_DIR}/urnetworkd" "${BIN_DIR}/urnetwork" \ /usr/lib/systemd/system/"${UNIT}" /lib/systemd/system/"${UNIT}"; do @@ -152,12 +160,12 @@ ${BIN_DIR}/urnetwork /lib/systemd/system/urnetworkd.service /usr/lib/systemd/system/urnetworkd.service /etc/systemd/system/urnetworkd.service -${SHARE_DIR}/applications/network.ur.urnetwork.desktop -${SHARE_DIR}/metainfo/network.ur.urnetwork.metainfo.xml -${SHARE_DIR}/icons/hicolor/48x48/apps/urnetwork.png -${SHARE_DIR}/icons/hicolor/256x256/apps/urnetwork.png -${SHARE_DIR}/polkit-1/actions/network.ur.urnetwork.policy -/etc/urnetwork/autostart/network.ur.urnetwork.desktop +${SHARE_DIR}/applications/com.bringyour.network.desktop +${SHARE_DIR}/metainfo/com.bringyour.network.metainfo.xml +${SHARE_DIR}/icons/hicolor/48x48/apps/com.bringyour.network.png +${SHARE_DIR}/icons/hicolor/256x256/apps/com.bringyour.network.png +${SHARE_DIR}/polkit-1/actions/com.bringyour.network.policy +/etc/urnetwork/autostart/com.bringyour.network.desktop /etc/NetworkManager/conf.d/95-urnetwork.conf /etc/udev/rules.d/85-urnetwork-unmanaged.rules" # plus whole directories swept below: ${SHARE_DIR}/urnetwork, locale .mo