Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 169 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,186 @@
# nm-sshuttle

Make an [sshuttle](https://github.com/sshuttle/sshuttle) tunnel behave like a
first-class VPN connection on a GNOME desktop: a toggle in Quick Settings,
next to WireGuard, backed by a systemd unit. The tunnel should survive
suspend, roaming and SSH key unlock without manual work.

sshuttle needs only SSH access and Python on the remote side, and it uses the
system `ssh` client, so it works through ProxyJump hosts where WireGuard is
not an option.

## Status

**M3, usable, done.** The plugin runs its whole lifecycle (M2):
reconnects with backoff, suspend, roaming, captive portals, a health check,
the lock screen and the fail-closed guard. The profile commands are on PATH,
RPM packages (Fedora 43, 44) and deb packages (Debian 13, Ubuntu 26.04) are
built in CI, and the VM tests run in CI too. The
[user guide](docs/user-guide.md) covers installing, creating a profile and
troubleshooting. Suspend and the GNOME checks passed by hand on a Fedora 44
notebook; still open from M2 is DNS without systemd-resolved (design §7).
[![CI](https://github.com/dennisklein/nm-sshuttle/actions/workflows/ci.yml/badge.svg)](https://github.com/dennisklein/nm-sshuttle/actions/workflows/ci.yml)
[![VM tests](https://github.com/dennisklein/nm-sshuttle/actions/workflows/vm-tests.yml/badge.svg)](https://github.com/dennisklein/nm-sshuttle/actions/workflows/vm-tests.yml)

**An [sshuttle](https://github.com/sshuttle/sshuttle) tunnel as a first-class
VPN on the GNOME desktop.** A toggle in Quick Settings, next to WireGuard,
backed by a NetworkManager VPN plugin. It needs only SSH access and Python on
the far side, and it uses your own `ssh`, so it works through `ProxyJump`
hosts where WireGuard is not an option.

- **A real VPN toggle.** The tunnel shows up in GNOME Quick Settings and in
`nmcli`, like any other NetworkManager VPN. Profiles can be created in
GNOME Settings or with the `nm-sshuttle` command.
- **It stays up.** Once switched on, it reconnects by itself after suspend,
roaming, a captive portal, a dropped connection or a locked screen, with
backoff, and it notices a tunnel that is up but stuck.
- **It fails closed.** While the tunnel is down, traffic to the tunnelled
networks is blocked instead of leaking onto the local network.
- **Your SSH setup, unchanged.** `~/.ssh/config`, `ProxyJump`, `known_hosts`
and your agent work as usual. Only `ssh` runs as you; sshuttle runs as
root in its own systemd unit, and no sudoers rule is needed.
- **DNS that follows the tunnel.** Send lookups for your company's domains
to its resolvers (split DNS through systemd-resolved), or all of them.
- **IPv4 and IPv6, TCP and UDP.** `method=tproxy` carries UDP such as QUIC
too.
- **Several tunnels at once.** Up to 8 profiles can be active together;
profiles that would fight over the same traffic or DNS are refused.

## Install

Fedora 43 and 44 can install from the signed package repository, which also
brings updates:

```console
$ sudo rpm --import https://dennisklein.github.io/nm-sshuttle/RPM-GPG-KEY-nm-sshuttle
$ sudo dnf install https://dennisklein.github.io/nm-sshuttle/nm-sshuttle-release.noarch.rpm
$ sudo dnf install nm-sshuttle nm-sshuttle-gnome
```

`nm-sshuttle-gnome` adds sshuttle to GNOME Settings' VPN list; leave it out
to use the command line only. Restart Settings afterwards (log out, or
`pkill gnome-control-center`).

Debian 13 and Ubuntu 26.04 packages are built by CI (download them from a
run's artifacts), and the plugin installs from source with
`meson setup build --prefix=/usr && sudo meson install -C build &&
sudo nm-sshuttle post-install`. The
[user guide](docs/user-guide.md#installing) has the details.

You need NetworkManager 1.42 or later, nftables, sshuttle 1.3.1 or later and,
for the tunnel's DNS, systemd-resolved. The package pulls in the rest.

## Quick start

1. Put the way to your far side in `~/.ssh/config`, and make sure a key logs
you in without a prompt from ssh itself (a key in your agent, or a key
without a passphrase):

```
Host corp
HostName gw.corp.example
User alice
ProxyJump bastion.corp.example
```

2. Create a profile as yourself, and test it. `check` makes the login the
plugin will make, so it may ask you to accept a host key once:

```console
$ nm-sshuttle add corp --remote corp --subnets 10.0.0.0/8 \
--dns split --dns-servers 10.1.0.53 --dns-domains '~corp.example'
$ nm-sshuttle check corp
```

With `nm-sshuttle-gnome` installed, the same form is in GNOME Settings:
Network, **+** next to VPN, **sshuttle**.

3. Switch it on in Quick Settings, or:

```console
$ nmcli connection up corp
```

It stays on until you switch it off. If it does not come up, GNOME says
only "Connection failed"; the reason is in
`journalctl -b -u 'nm-sshuttle*'`.

## Common setups

Each of these is one `nm-sshuttle add`; the
[user guide](docs/user-guide.md#creating-a-profile) lists every option.

**Split DNS for a company domain.** Tunnel the network, send lookups for the
domain to its resolver, leave everything else alone. The leading `~` only
routes lookups; a plain `corp.example` also completes short names, so `git`
becomes `git.corp.example`:

```console
$ nm-sshuttle add corp --remote corp --subnets 10.0.0.0/8 \
--dns split --dns-servers 10.1.0.53 --dns-domains corp.example
$ nm-sshuttle check corp
$ nmcli connection up corp # or the VPN toggle in Quick Settings
```

- [`nm_sshuttle/`](nm_sshuttle) is the plugin (design §4.9): the VPN D-Bus
service, its lifecycle state machine, the NetworkManager and logind
watchers, the guard table and `nmss0`, and the commands the systemd units
and users run. [`data/`](data) holds the files it installs, among them
the NetworkManager `conf.d` snippet that keeps `nmss0` unmanaged.
- [`gnome/`](gnome) is the editor for GNOME Settings (C, GTK4): "Add VPN"
offers sshuttle and the Identity tab edits every profile key. It is its
own package, `nm-sshuttle-gnome`. Its validation reads the same
[test cases](tests/profile-cases.json) as the plugin's.
- [`packaging/rpm/`](packaging/rpm) and [`debian/`](debian) build the
Fedora and Debian/Ubuntu packages. [`packaging/`](packaging) also builds
and signs the Fedora dnf repository on GitHub Pages
(<https://dennisklein.github.io/nm-sshuttle/>) from release tags.
- [`docs/design.md`](docs/design.md) is the design: the research findings,
the options considered, the architecture, the risks and the roadmap.
- [`test-vm/`](test-vm) runs the plugin's scenarios in a throwaway Fedora
44 VM, from a Fedora host or in CI (the VM tests workflow).
- [`lab/`](lab) is a self-contained test lab. It checks the sshuttle and
kernel behaviour the design relies on (network namespaces; run as root in a
throwaway VM or container).
- [`spike/`](spike) is the M1 spike: a bare VPN plugin run through the open
risks in a Fedora 44 VM with GNOME.
- [`docs/upstream/`](docs/upstream) holds draft bug reports for
NetworkManager, GNOME Shell and sshuttle, found along the way and not yet
filed.

## Building and testing
**IPv4 and IPv6, UDP included.** `--subnets` takes both families. Add
`--method tproxy` to carry UDP, such as QUIC; it needs `iptables` installed:

```console
$ nm-sshuttle add corp --remote corp --subnets 10.0.0.0/8,2001:db8:10::/48 \
--method tproxy --dns split --dns-servers 10.1.0.53 --dns-domains '~corp.example'
```

**A second tunnel next to the first.** Exclude the nested network from the
larger profile and give it a profile of its own:

```console
$ nm-sshuttle add corp --remote corp --subnets 10.0.0.0/8 --exclude 10.50.0.0/16
$ nm-sshuttle add lab --remote lab --subnets 10.50.0.0/16
```

**A jump host behind `ProxyCommand`.** ssh cannot tell which address it
connects to first, so name it, and the tunnel will not swallow it:
`--gateway 203.0.113.7`.

**Notice a stuck tunnel.** `--probe 10.1.0.10:22` makes the plugin connect
to a server on the far side that speaks first, such as an ssh server, every
10 seconds, and restart the tunnel after two failures.

**Let traffic out while the tunnel is down.** `--fail-closed no` turns the
block off.

## How it works

```
Quick Settings / nmcli ──► NetworkManager ──► nm-sshuttle (VPN plugin, root)
│ guard · DNS link · health
▼
nm-sshuttle-tunnel@N.service
sshuttle (root) ──► ssh (as you) ──► far side
```

The plugin is a small Python service that tells NetworkManager the VPN is
persistent, supervises the sshuttle unit, and watches NetworkManager, logind
and the tunnel itself to decide when to reconnect. An nftables guard table
keeps the tunnelled networks fail-closed, and a per-tunnel dummy link
(`nmss0`, `nmss1` …) hands the VPN's DNS servers and routing domains to
systemd-resolved. The [design](docs/design.md) has the research behind it,
the options considered and the architecture.

## Documentation

- [User guide](docs/user-guide.md): installing, SSH setup, every profile
option, GNOME Settings, several VPNs at once, troubleshooting, limits.
- [Design](docs/design.md): findings, architecture, risks and roadmap.
The [designs of the later additions](docs/later/README.md) (IPv6, tproxy,
several tunnels, the GNOME editor) are next to it.
- [Packaging](packaging/README.md): the RPM and deb packages, the signed dnf
repository and how to cut a release.
- [Upstream reports](docs/upstream/README.md): drafts of bugs found along
the way in NetworkManager, GNOME Shell and sshuttle.

## Development

```console
$ python3 -m pytest # needs python3-gobject and dbus-daemon for the D-Bus test
$ python3 -m pytest # needs python3-gobject and dbus-daemon for the D-Bus tests
$ meson setup build --prefix=/usr
$ sudo meson install -C build
$ sudo nm-sshuttle post-install
$ meson test -C build
$ test-vm/vm.sh # on a Fedora 44 host with KVM; see test-vm/README.md
```

The editor for GNOME Settings is built when `libnm` and GTK4 are installed
with their development files (`-Dgnome-editor=enabled` requires them). Its
form is tested under a virtual display:
`xvfb-run -a meson test -C build --suite gnome-editor`.

`post-install` reloads NetworkManager's configuration so that `nmss0` is
unmanaged, unless an nm-sshuttle VPN is active; then the setting takes
effect at NetworkManager's next start. The packages run it themselves.

## Proposed shape, in short

- A thin NetworkManager VPN service plugin (`vpn-type sshuttle`), written in
Python, that runs as a D-Bus-activated systemd service.
- sshuttle runs in its own systemd unit as root. Only `ssh` runs as the
desktop user, so `~/.ssh/config`, ProxyJump, `known_hosts` and the
session's agent work as usual, and no sudoers rule is needed.
- The plugin is persistent: once switched on, it reconnects by itself after
suspend, roaming and drops. While the tunnel is down, traffic to the
tunnelled networks fails closed instead of leaking.
- Split DNS uses a per-tunnel dummy link handed to systemd-resolved.
- [`nm_sshuttle/`](nm_sshuttle) is the plugin and the `nm-sshuttle` command;
[`data/`](data) holds the files it installs.
- [`gnome/`](gnome) is the editor for GNOME Settings (C, GTK4). It is built
when `libnm` and GTK4 are installed with their development files
(`-Dgnome-editor=enabled` requires them) and is tested under a virtual
display: `xvfb-run -a meson test -C build --suite gnome-editor`.
- [`test-vm/`](test-vm) runs the plugin's scenarios in a throwaway Fedora 44
VM with SELinux enforcing, from a Fedora host or in CI.
- [`lab/`](lab) checks the sshuttle and kernel behaviour the design relies
on, in network namespaces. Run it as root in a throwaway VM or container.
- [`spike/`](spike) is the early spike that retired the design's open risks
on Fedora 44 with GNOME.

Report security problems privately, as [SECURITY.md](SECURITY.md) describes.

## License

Expand Down
52 changes: 39 additions & 13 deletions docs/design.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
# nm-sshuttle: design proposal

Status: M0 to M3 done (§7), updated 2026-10-11. The plugin is in
[`nm_sshuttle/`](../nm_sshuttle), packaged for Fedora, Debian and Ubuntu, and
its VM tests ([`test-vm/`](../test-vm)) pass in CI. The research code is the lab in
Status: M0 to M3 done and the "Later" items implemented (§7), updated
2026-10-11. The plugin is in [`nm_sshuttle/`](../nm_sshuttle) and the editor
for GNOME Settings in [`gnome/`](../gnome). They are packaged for Fedora,
Debian and Ubuntu (Fedora also from a signed dnf repository), and the VM
tests ([`test-vm/`](../test-vm)) pass in CI. The sections below describe the
design as proposed for v1; later work is marked where it changes them. The research code is the lab in
[`lab/`](../lab), which checks the sshuttle and kernel behaviour this design
relies on, and the spike in [`spike/`](../spike), which checks
NetworkManager, SELinux and GNOME on Fedora 44.

## 1. Recommendation in brief

Build a **small NetworkManager VPN service plugin in Python, without a GUI
editor**; a ten-line auth-dialog stub is its only GNOME part. It runs as a D-Bus-activated systemd service and supervises
editor** in v1; a ten-line auth-dialog stub is its only GNOME part. (The
[GNOME Settings editor](later/gnome-editor.md) came later, as a separate
package.) It runs as a D-Bus-activated systemd service and supervises
sshuttle, which runs in a second systemd unit.

- **The toggle.** GNOME Quick Settings shows a VPN toggle for every
Expand Down Expand Up @@ -586,7 +591,8 @@ DNS behaviour.

- **A** buys native editing in GNOME Settings at the cost of a C/GTK4
editor library (gnome-control-center `dlopen`s it) and libnma. It can be
added on top of B later without changing anything else.
added on top of B later without changing anything else; it was, as the
separate package `nm-sshuttle-gnome` ([editor](later/gnome-editor.md)).
- **C** is the smallest: a systemd unit plus a Quick Settings extension
such as *Custom Command Toggle* (GNOME 45–51). But the toggle sits outside
NM's VPN list, extensions break on GNOME upgrades, and the lifecycle work
Expand Down Expand Up @@ -653,6 +659,10 @@ supports-safe-private-file-access=true
auth-dialog=/usr/libexec/nm-sshuttle/nm-sshuttle-auth-dialog
```

With [several tunnels](later/multiple-tunnels.md) the installed file says
`supports-multiple-connections=true`, and the [editor](later/gnome-editor.md)
adds a `[libnm]` section that names the plugin library of the editor package.

The other installed files:

- a D-Bus policy in `/usr/share/dbus-1/system.d/`: only root may own the
Expand Down Expand Up @@ -1598,21 +1608,27 @@ minimal path under it; `ip` and `nft` there are untested.

```
nm_sshuttle/
service.py VPN D-Bus interface (Gio), about 250 lines
service.py VPN D-Bus interface (Gio)
supervisor.py state machine, backoff, health; pure logic, unit-testable
watch.py NM and logind D-Bus watchers (systemd's and NM's active
connection signals stay in service.py)
profile.py vpn.data parsing and validation
tunnel.py sshuttle argv, exec-tunnel, sweep, ssh-as-user
guard.py nft guard table, dummy link
cli.py add / check / list / remove (wraps nmcli); sweep and
cleanup, which the units run
data/ .name file, D-Bus policy and activation file, the two units,
claims.py what each active profile claims; the conflict rules
dispatcher.py gives each activation a slot (several tunnels)
const.py names, paths and NM constants
cli.py add / check / list / remove (wraps nmcli); sweep, cleanup
and post-install, which the units and packages run
data/ .name file, D-Bus policy and activation file, the units,
the conf.d snippet that leaves nmss0 unmanaged,
auth-dialog stub (shell)
gnome/ editor for GNOME Settings (C, GTK4), a separate package
```

That is roughly 1,200 lines of Python.
v1 was about 1,200 lines of Python. With the lifecycle work, IPv6, tproxy
and several tunnels it is about 4,200, and the editor adds about 1,200 lines
of C.

- **Runtime dependencies:** python3 ≥ 3.10, python3-gobject, sshuttle ≥
1.3.1, nftables, iproute2, util-linux (`setpriv`) and openssh-clients.
Expand Down Expand Up @@ -1873,9 +1889,9 @@ Later, also propose a hook for an externally provided firewall helper
README; and the VM tests in CI, on demand and weekly, on hosted runners
with KVM (about 12 min). Suspend and the GNOME checks passed by hand
on a notebook; DNS without resolved stays open under M2.
5. **Later:** GTK4 editor plugin for GNOME Settings, several tunnels at once
(template units), UDP through `tproxy`, IPv6 guard sets. Each has a
design in [`later/`](later/README.md). **IPv6 is in:** IPv6 subnets
5. **Later (all four implemented):** GTK4 editor plugin for GNOME Settings,
several tunnels at once (template units), UDP through `tproxy`, IPv6
guard sets. Each has a design in [`later/`](later/README.md). **IPv6 is in:** IPv6 subnets
and exclusions, the IPv6 guard sets, an IPv6 first hop and `gateway`,
and the IPv6 part of the network key, with unit tests, lab tests T11 to
T14 and VM scenarios V6-01 and V6-02. **So is `method=tproxy`:** UDP
Expand All @@ -1886,3 +1902,13 @@ Later, also propose a hook for an externally provided firewall helper
activation a slot and starts a plugin instance for it, names per slot,
the conflict rules between active profiles, and `cleanup --slot` and
`--all`, with unit and D-Bus tests and VM scenarios MT-01 to MT-03.
**And the editor for GNOME Settings:** a GTK4 library, packaged as
`nm-sshuttle-gnome`, whose validation reads the same test cases as the
plugin's, with unit tests and a form test under Xvfb. On the Fedora 44
notebook the protocol
([`test-vm/gnome-editor-protocol.md`](../test-vm/gnome-editor-protocol.md))
passed in GNOME Settings except 7.3 and 7.6 (Apply lagged one change
behind the IPv4 method); that is fixed, and the re-check of 7.3 to 7.7
is still open.
**Packages:** a signed dnf repository for Fedora 43 and 44 on GitHub
Pages, published from release tags ([`packaging/`](../packaging/README.md)).
4 changes: 3 additions & 1 deletion docs/later/gnome-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
Status: implemented in `gnome/`, with the changes listed under
"Differences from the design" at the end, and checked by the unit tests and a
form test under Xvfb. The checks in GNOME Settings itself (§Tests, "VM, by
hand") are still open. One of the "Later" items of the [design](../design.md)
hand") passed on a Fedora 44 notebook except 7.3 and 7.6 of the
[protocol](../../test-vm/gnome-editor-protocol.md) (Apply lagged one change
behind the IPv4 method). That is fixed; the re-check of 7.3 to 7.7 is open. One of the "Later" items of the [design](../design.md)
(§7); the index is in [README.md](README.md).

v1 has no editor plugin (§1, option B in §3). Profiles come from `nm-sshuttle
Expand Down
7 changes: 4 additions & 3 deletions docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ $ sudo apt install ./nm-sshuttle_*_all.deb
```

To create and edit profiles in GNOME Settings as well, install the editor,
which is a separate package (`nm-sshuttle-gnome`; `nm-sshuttle-gnome-*.rpm`,
`nm-sshuttle-gnome_*.deb` as files). Settings reads its list of VPN types
once, so restart it afterwards (log out, or `pkill gnome-control-center`).
which is a separate package (`sudo dnf install nm-sshuttle-gnome` from the
repository; `nm-sshuttle-gnome-*.rpm`, `nm-sshuttle-gnome_*.deb` as files).
Settings reads its list of VPN types once, so restart it afterwards (log out,
or `pkill gnome-control-center`).

From source:

Expand Down
Loading