From 074e777ba2469d4becbb646ae8a775a35817ce77 Mon Sep 17 00:00:00 2001 From: Ryanmello07 <67509637+Ryanmello07@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:05:38 -0700 Subject: [PATCH 1/2] rename: app id network.ur.urnetwork -> com.bringyour.network The Android and Apple clients ship under `com.bringyour.network`. Linux was the only platform on a different reverse-DNS id, and every place the id is written down had to be told which one to use. This makes Linux match, and it has to be done in one change because the id is a join key: the GTK application id, the .desktop basename, the AppStream component id, the polkit action namespace, the icon-theme name and the Flatpak app id must all agree or the desktop stops recognising the app. WHAT MOVES, AND WHY IT IS ALL ONE COMMIT main.cpp Gtk::Application::create() -- the GApplication id *.desktop filename, Icon=, StartupWMClass= metainfo.xml filename, , polkit .policy filename + all four action ids, matched in ControlProtocol.hpp so the daemon asks about the actions the file actually declares icons hicolor basenames; Flatpak refuses to export an icon whose name is not the app id flatpak manifest filename + id + the desktop-file-edit paths deb/rpm/tarball/ the installed paths, the conffile entries, and the AppImage/snap uninstaller's stale-path list Splitting these would leave an intermediate commit where, for example, the .desktop names an icon that does not exist, or the daemon checks polkit actions the shipped .policy does not declare -- both of which fail silently at runtime rather than at build time. TWO THINGS THAT ARE NOT PURE SEARCH-AND-REPLACE 1. `UrTheme::kAppIconName`. The icon name was spelled as a literal in two places -- the by-path load in UrTheme.cpp and the by-name fallback in MainWindow.cpp. Renaming the packaging alone left both lookups pointing at a file that no longer existed, and `set_from_icon_name()` renders a blank image without raising anything, so the title-bar logo simply went empty. It is now one constant that the packaging and both call sites share. 2. The libsecret keyring attribute in SecretServiceRpcSessionStore.cpp moves with the id. This is deliberately NOT dual-read: an entry written by an older build is no longer found, the app falls back to a fresh RPC session (the same one-time cost as the Flatpak data path moving), and the previous app identity is not left holding live key material in the user's keyring with nothing to clean it up. No behaviour changes beyond those two. `network.ur.urnetwork` no longer appears anywhere in the tree. --- APPIMAGE.md | 6 ++-- MIGRATION.md | 6 ++-- PLAN.md | 2 +- app/meson.build | 16 +++++----- ....desktop => com.bringyour.network.desktop} | 2 +- ....desktop => com.bringyour.network.desktop} | 4 +-- ...xml => com.bringyour.network.metainfo.xml} | 6 ++-- ...rnetwork.png => com.bringyour.network.png} | Bin ...rnetwork.png => com.bringyour.network.png} | Bin app/snap/snapcraft.yaml | 2 +- app/src/ControlProtocol.hpp | 14 ++++---- app/src/MainWindow.cpp | 2 +- app/src/SecretServiceRpcSessionStore.cpp | 8 ++++- app/src/UrTheme.cpp | 5 +-- app/src/UrTheme.hpp | 14 ++++++-- app/src/main.cpp | 4 +-- docs/TESTING-CACHYOS.md | 8 ++--- docs/linux_agent_help.md | 10 +++--- packaging/deb/nfpm.yaml | 6 ++-- packaging/deb/scripts/postinst | 4 +-- packaging/distro-smoke.sh | 6 ++-- ...rnetwork.yml => com.bringyour.network.yml} | 10 +++--- packaging/lib/common.sh | 30 +++++++++--------- packaging/make-appimage.sh | 20 ++++++------ packaging/make-flatpak.sh | 6 ++-- ...rk.policy => com.bringyour.network.policy} | 10 +++--- packaging/rpm/nfpm.yaml | 4 +-- packaging/tarball/install.sh | 4 +-- packaging/tarball/uninstall.sh | 12 +++---- 29 files changed, 119 insertions(+), 102 deletions(-) rename app/packaging/autostart/{network.ur.urnetwork.desktop => com.bringyour.network.desktop} (96%) rename app/packaging/{network.ur.urnetwork.desktop => com.bringyour.network.desktop} (93%) rename app/packaging/{network.ur.urnetwork.metainfo.xml => com.bringyour.network.metainfo.xml} (93%) rename app/packaging/icons/hicolor/256x256/apps/{urnetwork.png => com.bringyour.network.png} (100%) rename app/packaging/icons/hicolor/48x48/apps/{urnetwork.png => com.bringyour.network.png} (100%) rename packaging/flatpak/{network.ur.urnetwork.yml => com.bringyour.network.yml} (97%) rename packaging/polkit/{network.ur.urnetwork.policy => com.bringyour.network.policy} (95%) 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..31f6c4f 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 | 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/TESTING-CACHYOS.md b/docs/TESTING-CACHYOS.md index 3cc0da3..f3a9705 100644 --- a/docs/TESTING-CACHYOS.md +++ b/docs/TESTING-CACHYOS.md @@ -1094,7 +1094,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 +1121,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 +1187,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 +1208,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/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..b8a064b 100755 --- a/packaging/distro-smoke.sh +++ b/packaging/distro-smoke.sh @@ -347,7 +347,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 @@ -922,8 +922,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-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..2164aa6 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' "$*"; } @@ -1339,7 +1339,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..359c741 100755 --- a/packaging/tarball/uninstall.sh +++ b/packaging/tarball/uninstall.sh @@ -152,12 +152,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 From bc9824fd5c4d8743958f32f4cdf056047dfd2ce6 Mon Sep 17 00:00:00 2001 From: Ryanmello07 <67509637+Ryanmello07@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:11:50 -0700 Subject: [PATCH 2/2] packaging: a native Arch package, so pacman owns and tracks the daemon Arch, CachyOS, EndeavourOS and Manjaro had exactly one daemon channel: the install tarball. That means nothing on the machine knows the daemon is there. pacman cannot list it, cannot verify it, cannot upgrade it, and `pacman -R` cannot remove it -- and the tarball can only complain about missing nftables or fuse2 after it has already written files. This adds urnetwork-daemon--.pkg.tar.zst, built the same way the .deb and .rpm are. WHY nfpm AND NOT A PKGBUILD. All the native packages are assembled from ONE `meson install --destdir` staging tree through packaging/lib/common.sh's assemble_daemon_root(), so the daemon inside each is the same bytes by construction rather than by review. A PKGBUILD would be a fourth independent copy of the installed-path table, and it would need an Arch machine or a container to run makepkg -- which neither the build server nor the workflow's ubuntu:22.04 container is. nfpm is pure Go down to its zstd, so this builds anywhere the .deb does. An AUR recipe remains reasonable later as a discovery channel; it is not a replacement for a first-party binary package. THREE THINGS make-arch.sh DOES THAT make-deb.sh DOES NOT, each because pacman fails in a way the other formats do not: 1. It MOVES THE UNIT to /usr/lib/systemd/system. 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. 2. It FOLDS THE WHOLE VERSION INTO pkgver, because pacman's pkgver may not contain '-' at all and nfpm silently mangles the other obvious shapes. The file is still named for the release version verbatim: pacman does not parse filenames, it reads .PKGINFO. 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, both silent. `type: tree` writes Go's unmasked fs.FileMode for every directory, so every directory reports as altered forever; and the tar header mtime and the .MTREE `time=` come from different clocks. Both are worked around, and the script asserts the workarounds held rather than trusting them. The install/remove hooks mirror the .deb's maintainer scripts, with the pacman ordering difference that matters: pre_remove stops and disables the unit while the binary still exists, so its ExecStopPost can tear the nftables ruleset down. NOT FOR SteamOS, even though SteamOS is Arch-family. Its /usr is read-only, `pacman -U` there needs `steamos-readonly disable`, and the next system update reverts it. Immutable Arch hosts stay on the install tarball, which installs under /usr/local; packaging/distro-smoke.sh says so on a SteamOS host. The tarball installer and uninstaller already refused to touch paths that `pacman -Qo` reports as owned. That guard was written as protection against a hypothetical future AUR package; it is now a live conflict, and both comments are updated to say so rather than leaving the next reader to assume the check is theoretical. --- MIGRATION.md | 28 +- docs/DISTRO-SUPPORT.md | 4 +- docs/TESTING-CACHYOS.md | 25 +- packaging/arch/nfpm.yaml | 360 ++++++++++++++++++++ packaging/arch/scripts/post_install | 178 ++++++++++ packaging/arch/scripts/post_remove | 58 ++++ packaging/arch/scripts/post_upgrade | 44 +++ packaging/arch/scripts/pre_install | 47 +++ packaging/arch/scripts/pre_remove | 51 +++ packaging/arch/scripts/pre_upgrade | 62 ++++ packaging/distro-smoke.sh | 32 +- packaging/make-arch.sh | 508 ++++++++++++++++++++++++++++ packaging/tarball/install.sh | 25 +- packaging/tarball/uninstall.sh | 18 +- 14 files changed, 1408 insertions(+), 32 deletions(-) create mode 100644 packaging/arch/nfpm.yaml create mode 100644 packaging/arch/scripts/post_install create mode 100644 packaging/arch/scripts/post_remove create mode 100644 packaging/arch/scripts/post_upgrade create mode 100644 packaging/arch/scripts/pre_install create mode 100644 packaging/arch/scripts/pre_remove create mode 100644 packaging/arch/scripts/pre_upgrade create mode 100755 packaging/make-arch.sh diff --git a/MIGRATION.md b/MIGRATION.md index 31f6c4f..0d8b627 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -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/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 f3a9705..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. | 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/distro-smoke.sh b/packaging/distro-smoke.sh index b8a064b..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" @@ -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 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/tarball/install.sh b/packaging/tarball/install.sh index 2164aa6..6d855e0 100755 --- a/packaging/tarball/install.sh +++ b/packaging/tarball/install.sh @@ -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='' diff --git a/packaging/tarball/uninstall.sh b/packaging/tarball/uninstall.sh index 359c741..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