# The Linux app, in depth

This is the deep dive: every major surface of the Linux app, what it
controls, its defaults, and how to fix it when something breaks. It assumes
you have already installed and connected. If not, start with
[getting started](/docs/getting-started-linux). For how the network behind
the app works, read the [overview](/docs/overview).

The Linux client is a GTK4/libadwaita desktop app over a root systemd
daemon, shipped as an AppImage plus a `.deb`. All of it is open source at
[github.com/urnetwork/linux](https://github.com/urnetwork/linux).

## Settings at a glance

The defaults protect you without configuration. The connection controls live
in the connect drawer; provide mode sits in the home controls; the
device-location override sits above the connected-providers globe.

| Setting | Default | Effect | Main cost |
|---|---|---|---|
| Connection mode | Auto | picks the provider windows; Auto runs quality and speed together | Web or Streaming narrow to one window |
| Strong Anonymization | On | hides your source IP from the provider | more latency |
| Post Quantum Encryption | On | encrypts from the app to the provider | skips a provider it cannot seal to |
| Kill switch | Off | stops local fallback when the tunnel is down | no traffic while providers fail |
| Fixed IP | Off | one provider, one exit address | no failover |
| Block ads and trackers | Off | filters ad and tracker traffic | filtering can break some sites |
| Custom DNS | encrypted DoH via the tunnel | DNS stays encrypted and inside the tunnel | loosening it is the trade |
| Provide mode | Never | shares spare bandwidth as an exit for others | strangers' traffic egresses from your IP |
| Device location override | Off | apps that ask GeoClue see your longest-held provider's city | needs GeoClue 2.7+; affects every app on the machine |
| Split rules (domain/IP) | empty | listed destinations use your local connection | that traffic is unprotected |

Caveats that go with the table:

- Turning **Strong Anonymization** off is direct mode: the client talks
  straight to the provider and gets faster, and that provider then sees your
  real address. The default protects your identity. Spend it deliberately.
- The kill switch also decides what happens to a packet the security filter
  drops (see provide mode): on, it is blocked outright; off, it leaves by
  your normal route.
- Per-app split tunneling is not available on Linux. The split rules are
  domain- and IP-level.
- Deleting your account is not on Linux either. Do that in the
  [web app](https://ur.io/app), which manages the same account.

## The daemon split

Two programs, two privilege levels:

- **`urnetworkd`** is a root daemon managed by `urnetworkd.service`, with no
  GUI dependencies at all. It embeds the SDK's `DeviceLocal`, opens and owns
  `/dev/net/tun`, pumps packets through the SDK's IO loop, and applies
  routes and DNS. The `.deb` or the install tarball installs it.
- **`urnetwork`** is the desktop app, gtkmm-4.0 and libadwaita, running as
  your normal user and shipped as an AppImage. It holds a `DeviceRemote`: a
  view of the daemon's device, not a device of its own.

They talk over two channels:

1. **Device RPC.** The app's `DeviceRemote` drives the daemon's
   `DeviceLocal` over the SDK's mutually authenticated TLS RPC on loopback,
   `127.0.0.1:12025`, the same transport the Windows app uses.
2. **The control socket.** A unix-domain socket at
   `/run/urnetwork/control.sock` carries tunnel lifecycle: bring the tunnel
   up, tear it down, set provide mode, report daemon state.

The unix socket is the real authorization boundary. Loopback TCP is
reachable by any local process, which is why the device RPC is mutually
authenticated with certificates. But nothing starts a tunnel without passing
the unix socket, where the kernel itself tells the daemon which user is
asking (`SO_PEERCRED`, checked before a single byte of the request is
parsed, and authorizing on user id, never process id). The directory is
`0750 root:urnetwork`, the socket `0660`, and the rule is: root, or a member
of the `urnetwork` group.

Moving the device RPC onto a unix transport too is a recorded follow-up in
the SDK; the design notes state the loopback limitation instead of treating
loopback as private.

### The two halves must match

Linux is the first URnetwork platform where the app and the tunnel update
independently: the AppImage self-updates as you, the daemon updates through
apt. So the app checks two things on connect: the control protocol version,
and that both halves carry the exact same SDK build. A mismatch is refused
up front with a distinct message (*service not running*, *service out of
date*, *app out of date*, *builds differ*) because each has a different fix,
and a silently half-working tunnel is worse than an error.

### How the pieces install

Both halves come from the release page: `.deb`, `.install.tar.gz` and
`.AppImage`, amd64 and arm64, with a glibc 2.35 floor that means Ubuntu
22.04 or Debian 12 and newer. There is no store listing.

The package owns everything privileged and stable: the daemon at
`/usr/lib/urnetwork/urnetworkd`, its systemd unit, the desktop entry, icons,
the world map data the globe draws from, translations, and a launcher at
`/usr/bin/urnetwork`.

The GUI AppImage is never installed by the package. It lives in your home
directory (`~/.local/lib/urnetwork/URnetwork.AppImage`), user-writable so it
can replace itself without root. The launcher searches a fixed list of
locations (`$URNETWORK_APPIMAGE`, then `~/.local/lib/urnetwork/`, then
`~/Applications/`, then `/usr/lib/urnetwork/`, then any `urnetwork-gui` on
`$PATH`) and runs the first hit; with none installed it prints a one-line
hint telling you where to get one.

The daemon runs with the privilege it needs and gives up what it does not:
`NoNewPrivileges`, `ProtectHome` (it is headless and must never read user
home directories), `PrivateTmp`, root-only state and log directories. It is
ordered against `network-pre.target`, the slot a VPN needs so it can act
before the network comes up, and it is always-running rather than
socket-activated, because a lazily started unit cannot participate in that
ordering at all.

## Routes

When the tunnel comes up, the daemon does not replace your default route. It
outbids it, the way `wg-quick` does:

- It adds routes through the tunnel interface (`urnet0`) covering the whole
  IPv4 space **except** the private LAN ranges: 10.0.0.0/8, 172.16.0.0/12,
  192.168.0.0/16.
- Those are more specific than your existing `0.0.0.0/0` default, so they
  win without touching it. Teardown is just removing them; your original
  routing is never modified.
- Excluding the private ranges keeps your printer, NAS, and local services
  working while you are connected, the same behavior as the Android, Apple,
  and Windows clients.

NetworkManager is told to keep its hands off: `urnet0` is marked unmanaged
via a NetworkManager config drop-in and a udev rule, so the desktop's
network manager never fights the daemon over the interface.

## DNS

DNS on the tunnel link is set through **systemd-resolved** (`resolvectl`),
the standard resolver on Ubuntu desktops: the link's resolvers are pointed
at the network's, with `~.` so queries route through it, and everything is
reverted on teardown. Not every system runs resolved, so the behavior is
best-effort by design:

- **With systemd-resolved:** DNS for the tunnel link is captured and
  restored.
- **Without it:** the tunnel still routes, but DNS on that link is not
  captured. Your queries keep using whatever resolver the system had.

One more subtlety: a DHCP-supplied search domain (`lan`, `home`) is a longer
match than `~.` and will beat it for names in that domain. If you run an
unusual resolver setup, check where your DNS actually goes.

The app's own **Custom DNS** sheet is the more meaningful control. It edits
the SDK's resolver settings, whose default is encrypted DNS over HTTPS
resolved through the tunnel. It offers DoH and unencrypted resolution,
remote or local, editable server lists per family, a local-DNS fallback
switch that races a local resolver while the tunnel starts so lookups do not
stall, regional recommendations for places where the strictest configuration
is known to break, and one action to restore the secure defaults.

## The tray

The status icon is a **StatusNotifierItem** with a `com.canonical.dbusmenu`
menu, spoken directly over GDBus: the protocol KDE and most panels support
natively, and GNOME supports through the AppIndicator extension. With no
tray host present, the app shows no icon rather than failing.

Closing the window hides to the tray and keeps the tunnel running. The menu
carries connect/disconnect, show, and quit. Quit is the real exit here: it
ends the session and brings the tunnel down. Day to day you want the close
button, not quit.

## The window

Sign-in first, then the home view: the connect control, the provider globe,
and the drawer. Sign-in takes an email or phone code, **Create account**, an
auth code generated on a device you are already signed in on, or a wallet
signature through Solana (Phantom, Solflare) or Bittensor. A legacy **Try
Guest Mode** button also sits there. It mints an ordinary permanent account
on the server, but this app throws away the recovery seedphrase that comes
back with it, so use one of the other paths if you want to be able to
recover the account later.

**The provider globe** is drawn from a world topology bundled with the app.
No map tiles are fetched, so nothing about the map you are looking at leaves
your machine.

**The connect drawer** carries the session:

- **The connection controls card.** The selected location, a connection mode
  of **Auto**, **Web**, or **Streaming**, and four switches: **Fixed IP**,
  **Strong Anonymization**, **Post Quantum Encryption**, and the **kill
  switch**. The mode picks which provider window runs: **Web** is the
  quality window (2–6 providers at once), **Streaming** the speed window
  (1–2), and **Auto** runs both side by side, which is why an ordinary
  session commonly has 3–8 providers open, not one. Strong Anonymization is
  what forbids a direct client-to-provider path, so no provider ever sees
  your real address; switching it off is the direct-mode trade from the
  table above. Post Quantum Encryption seals the session itself (see the
  identity panel below).
- **Client and local statistics** cards with live charts, each opening a
  detail sheet.
- **Custom DNS** status, opening the resolver editor above.
- **Block ads and trackers.**
- **Plan and usage.** Your tier, the used/pending/available bar, the daily
  allowance and referral rows, **Get UR Pro**, and redeeming a balance code.
- **The post-quantum identity panel** (below).

**Sheets**, each a real screen rather than a stub:

- **Locations.** The SDK's bucketed sections: your connected network peers
  first, then best-available, then countries with live provider counts.
  Typing in the search box adds top matches plus regions, cities and
  devices. A city is something you search for here, not something you
  scroll to.
- **Connected providers.** A globe over one row per provider currently in
  your window, oldest connection first, with client id, city/region/country,
  coordinates, duration, and an inline remove.
- **Contracts.** The live transfer contracts behind your session, one stack
  per peer, send and receive kept separate.
- **Split rules.** Domain- and IP-level rules that force specific traffic to
  your local connection, over a live feed of what is being acted on.
- **Redeem code.** Apply a balance code, with the history of ones you have
  redeemed.
- **Upgrade.** UR Pro monthly or yearly through Stripe checkout, embedded in
  the app where the system has WebKitGTK, and falling back to your browser
  otherwise, because a payment path must never hard-fail.

There is no separate Account, Wallet, or Leaderboard screen on Linux today.
Plan and balance live in the drawer and the redeem sheet, and account
settings live in the web app at [ur.io](https://ur.io). Sign out is in the
home view.

## The device-location override

Linux gets a real version of what Android calls "sync device location": apps
that ask the system where you are can be told the coordinates of the
provider you have been connected to the longest, instead of your actual
location. The toggle sits above the connected-providers globe, with a setup
guide, and it is off until you turn it on.

The mechanism is narrow. GeoClue, the service desktop apps ask for location,
has a static source that reads a fixed position from `/etc/geolocation` and
watches the file for changes. That file is the only injection point and it
lives under `/etc`, so the write is done by the daemon over the same control
channel; the GUI never touches it and never needs root.

The guide states its limits directly. It needs GeoClue 2.7.0 or newer, which
is not on every system; Ubuntu 22.04 and Debian 12 ship an older version and
can never satisfy it. And it moves only what GeoClue reports: GNOME Settings
and Maps, Firefox, and sandboxed Flatpak and Snap apps follow it. Chrome and
KDE Plasma never ask GeoClue at all, anything that works your location out
from your IP address is untouched, and Firefox falls back to its own lookup
if the system answers slowly. It also changes the location reported to every
app on the machine, not just URnetwork. Everything else in the app works
regardless.

## Provide mode

Providing is off by default, and the home controls offer **Auto**,
**Always**, **Network** (your own account's devices only), or **Never**.
Providers participate in the UR protocol; [ur.xyz](https://ur.xyz) documents
how rewards work.

What protects you when you share is the connect engine's open-source
`ip_security` layer. It runs deep packet inspection on your own egress and
drops DMCA-class traffic (stateful BitTorrent and file-sharing signatures,
opaque-protocol drops) and CFAA-class traffic (attack and intrusion
patterns) before it leaves your machine. The same filter also runs on your
own send path as an ordinary user, whether or not you ever turn providing
on, so your traffic is judged on your machine before it reaches any
provider.

On a match the packet is dropped, with no destination, domain or contents
recorded. A BitTorrent signature on traffic you carry for someone else
additionally raises an abuse flag to the operator holding only the sending
device's id and a boolean, and the operator ships no handler for it today.
An opaque-encrypted flow is a plain drop that reports nothing at all.
Sealing does not bypass the filter, because it runs where traffic exits. And
the kill switch decides the fate of a dropped-class packet from your own
machine: off, it goes out your local connection; on, it stops.

## The post-quantum identity panel

The drawer shows this device's session identity: an identicon, the canonical
key hash and client id (click to copy), a deck of the provider identities
you have established verified sessions with, the full provider-identities
list, and a share dialog for comparing your fingerprint out of band.

What the setting does: **Post Quantum Encryption** ships on, and while it is
on your client seals traffic end to end to the provider over a session
negotiated with **X25519MLKEM768** (hybrid classical plus ML-KEM). That
sealed leg is what makes the middle hop blind: the operator carries bytes it
cannot read. It is fail-closed. While the setting is on the client will not
send or accept application data in the clear, so a provider it cannot
establish that session with is skipped rather than used unsealed: the cost
is a lost provider, not lost encryption, and you cannot be silently
downgraded. Every current provider build enables the responder side, so the
skip is rare. Switch the setting off and traffic can take the standard
TLS-to-platform path again. Identity signatures remain Ed25519 and the
cipher is AES-256-GCM. "Post-quantum" means this key exchange, nothing
broader.

## What the network is doing underneath

Your traffic takes four legs: you → extender → operator → provider →
internet. The extender is a reachability hop that forwards your encrypted
session without reading it; as your first hop, it does see your address.
The [overview](/docs/overview) explains the full path, why it stops at four
legs, and the provider windows.

What matters for this app's controls is the split across the two relay
parties. The provider never learns who you are, because the operator sits in
between. The operator cannot read what you send, because Post Quantum
Encryption seals the session end to end to the provider by default. Both
hold on a stock install, so no single party holds both your identity and
your activity. The three states the two switches produce:

| Mode | Operator sees | Provider sees | Default and availability |
|---|---|---|---|
| **Relayed sealed** | your account and source connection, which providers you are on, and ciphertext with its timing and volume | destinations it egresses and a device/contract id, **not** your real IP | the shipped default: Post Quantum Encryption, on in the connect drawer |
| **Relayed standard** | your account and source connection, which providers you are on, and the destinations and packet bytes inside | destinations it egresses and a device/contract id, **not** your real IP | only with Post Quantum Encryption switched off |
| **Direct** | less relay involvement | **your real IP** and the destinations it egresses | opt-in: switch off **Strong Anonymization** |

The [threat model](/docs/threat-model) works these rows against named
adversaries and is explicit about where each one fails.

On the operator's side there is little to keep: connection records hold a
keyed one-way hash (scrambled with one secret key) of your IP block rather
than the address, the contract ledger holds client ids and byte counts with
no destination, host, URL or domain anywhere, and the data path logs
nothing. A geographic lookup does record an approximate city per connection;
what you reach through it is not recorded at all. No independent audit
covers the protocol or the operator's server code (the two 2025 third-party
assessments cover other surfaces: a penetration test of the web application
and API, and a passed Leviathan MASA AL2 assessment of the Android app, not
the Linux client). The checks that exist instead are the open source itself,
continuously checkable by anyone, and the exit design: egress is a
decentralized fleet of separately operated providers rather than one
custodian holding everything. Limit: how independent that fleet really is
has never been measured, and nothing stops the operator running providers of
its own ([threat model](/docs/threat-model) §6.1).

## Limits

- **The kill switch ships off.** It is a real toggle in the connect drawer,
  but until you turn it on, traffic falls back to your normal connection
  whenever no provider is up instead of stopping. The drawer applies it live
  over the device RPC and the daemon restores your choice at the next start.
- **IPv4 only through the tunnel.** IPv6 is not routed through it, so on an
  IPv6-enabled network a v6-capable site can still see your real address.
  Disable IPv6 on the connection if you need everything to exit through a
  provider.
- **No per-app split tunneling.** Rules are domain- and IP-level.
- **No Google or Apple sign-in.**
- **DNS capture depends on systemd-resolved**, as described above.

## Troubleshooting

### "The URnetwork system service is not running"

Check the daemon and its socket:

```
systemctl status urnetworkd
journalctl -u urnetworkd -f
```

The daemon narrates what it configures and what it restores, so this is the
first place to look for tunnel bring-up, route, and DNS problems. GUI logs
stay in your user session, separate from the privileged half, which is the
point of the split.

If the daemon is running and the app still cannot reach it, you are probably
not in the `urnetwork` group: `id -nG` will tell you. Add yourself with
`sudo usermod -aG urnetwork "$USER"` and log out and back in.

### "The service is out of date" / "different builds"

Update both halves to the same release: the daemon by installing the newer
`.deb` (or by re-running `install.sh` from the newer tarball), and the
AppImage in `~/.local/lib/urnetwork/`. The check exists so a half-upgraded
install says so instead of misbehaving quietly.

### The AppImage won't start

Ubuntu 22.04+ does not ship `libfuse2` by default and AppImages need it. The
`.deb` declares that dependency, so installing the daemon package usually
resolves it; otherwise install `libfuse2` (or `libfuse2t64` on newer
releases). Also check the file is executable.

### No tray icon

Your desktop has no StatusNotifierItem host. On GNOME, install the
AppIndicator extension; KDE and most panels support it natively. The app
works fine without one: closing the window just hides it, and you reopen
from the app menu.

### `urnetwork://` links do nothing

Wallet sign-in returns through a `urnetwork://` link, which needs the
desktop database refreshed after install. Run
`update-desktop-database ~/.local/share/applications` (or the system path
with sudo). The packaged install triggers this for you; a manual install may
not have.

### The interface exists but nothing routes

Check that NetworkManager is not managing `urnet0`. The package ships a
config drop-in and a udev rule marking it unmanaged, and both need a reload
(`nmcli general reload conf`, `udevadm control --reload`), which the
installer does.

## Elsewhere

The [overview](/docs/overview) explains the network end to end: providers,
contracts, what each party can and cannot see, and the encryption. The
[FAQ](/docs/faq) answers the common questions. Other platforms:
[Android](/docs/getting-started-android), [iOS](/docs/getting-started-ios),
[macOS](/docs/getting-started-macos),
[Windows](/docs/getting-started-windows), and the
[browser](/docs/getting-started-browser).
