Releases: Ekkh1300/NetPilot
Release list
NetPilot 1.2.3
What is in this release
Every artefact is built from this tree and its checksums are in SHA256SUMS.txt.
Security
Command injection through the phone pairing token, on all three platforms. A phone that had
paired - six digits, then a 256-bit token - could have arbitrary commands run as administrator on
the PC. The proxy address it sent was interpolated straight into a PowerShell script that ran
elevated. The daemon had the same shape on Linux and macOS, and its HTTP API needs no
authentication at all, so there the value came from anyone on the network.
Fixed by validating that a proxy address can only be an address, and by passing the value to
the script as an environment variable rather than interpolating it, so PowerShell cannot re-parse
it as code. The daemon passes one argument per element now, and every interface name, resolver
address and service name is validated first.
Every machine-rooted path in the repository is gone - 24 of them. Two were constants in
shipped code, and both silently disabled a feature on every install except the developer's: the
flag marking an active share pointed at a directory that exists on one machine, so the recovery
that restores your own network settings after a crash never ran; and the file the CI installer
job reads could not exist anywhere but that machine.
The pairing token is encrypted at rest with a key in the Android Keystore. It was a bare
string in a JSON file, which is readable by anything on a rooted phone, by any code running under
the app's UID, and by an adb backup. Existing installs keep working and are re-sealed on first
load.
The LAN proxy caps concurrent connections. It binds 0.0.0.0 and held each for up to 20 seconds,
so a few hundred idle opens from one host exhausted the phone's file descriptors and took the
tunnel with them. No credentials were needed.
Correctness
- The tunnel now answers every DNS query in a segment, not just the first. Pipelining is legal
over TCP and is what the length prefix exists to support; the second query used to sit
unanswered, with the client already ACKed, waiting on a resolver that was working correctly. - DNS replies are matched against the transaction id. Any packet arriving on the socket used to be
accepted as the answer, including a spoofed one from anywhere on the path. - A TCP flow's receive buffer is bounded. A client that never acknowledged anything grew it until
the phone ran out of memory. - The Android foreground service uses the right type, and the tunnel reader closes its file
descriptors on every exit path. - Settings are fsync'd before and after the rename, and a failed write is logged instead of
swallowed.
Verification
The injection fix is measured end to end, not just unit tested. The pre-fix script shape prints
OK and writes a marker file; the shipped binary refuses the same payload with
mv_bad_proxy and writes nothing. Tools/Test-Injection.ps1 drives the real executable over
the real HTTP API.
125 Windows, 107 Core, 82 Daemon and 177 Android tests pass, plus lint.
Not verified
The Linux and macOS GUI binaries have never been rendered - there is no emulator and no
device available - so those are in the separate v1.2.3-linux candidate release rather than
here. The daemon is covered by CI on real ubuntu-24.04 and macos-14 runners.
One thing you must do
The Android signing key that was briefly committed to the public repository has been replaced.
The Play Console still has to be moved to the new key - see docs/KEYSTORE-ROTATION.md.
Until it is, a Play upload will be signed with a key Play Console does not know.
NetPilot 1.2.3 - Linux and macOS (release candidate)
Linux and macOS
Built from the same tree as v1.2.3, which carries the command-injection fixes. Both artefacts
are headless-capable; the daemon runs without a display and the GUI needs one.
What is here
netpilot-daemon-*- the daemon for linux-x64, linux-arm64, osx-x64 and osx-arm64. This is
the headless build and the one with CI coverage on realubuntu-24.04andmacos-14
runners.netpilot-gui-*- the Avalonia GUI for linux-x64 and linux-arm64.
Please read this before reporting a problem
The GUI has never been rendered. No emulator or device was available on the machine that built
it, so it compiles, links and packages, and has not been seen on screen. That is why these are a
candidate rather than the main release. Everything testable is tested - the shared core, the
daemon's validation and argument handling, and the platform-independent screens - but the native
windowing layer is unverified. A first run may need a display server or a working session bus;
if it does not start, that is the untested part and it is worth saying so in an issue rather than
assuming a bug in the feature itself.
The daemon is a different matter: its blocking was measured with setpriv against a local
listener on real hardware, so the firewall and throttling paths are known to work.
What changed for these platforms
- Command injection, same as the main release. A resolver address or a proxy host arrived from
the HTTP API, which needs no authentication, and was interpolated into a command line the target
program then re-split. Every value is now validated as the kind of thing it must be, and passed
as one argument per element. - Linux DNS via
resolvectl, macOS vianetworksetup. Two of the macOS calls were also plain
broken: one built a single string holding fivegsettingscommands for one invocation, which
could only ever have failed; another passed the literal text{service}because it used an
ordinary string where an interpolated one was meant. - Every machine-rooted path removed, including the ones inside the release scripts, which is what
let the installer job fail on CI.
Checksums for all five files are in the main release's SHA256SUMS.txt; the values match the
ones computed here.
NetPilot 1.2.2 for Linux - graphical build (release candidate)
NetPilot 1.2.2 for Linux — graphical build
The Windows desktop app is WPF, which does not exist on Linux, so this is a separate program:
written in Avalonia, drawing the same interface and sharing the same logic with the Windows
build through NetPilot.Core.
What has actually been verified
Verified on a real Linux kernel (ubuntu-24.04, full root): the shared logic's 107 tests pass
there, and the daemon's privileged paths — nftables rules, per-uid blocking measured with
setpriv, tc shaping — are exercised on every push by .github/workflows/daemon.yml.
Verified by running this window: it was launched, screenshotted and driven on a Windows
machine. Avalonia renders the same interface on all three platforms, so the screenshots show the
real thing — layout, bindings, live charts, the diagnostics page — rather than a promise. Every
defect listed below was found that way.
Not verified: that this window opens on a real Linux desktop. No Linux machine was available
to run it on. The only untested part is the native windowing underneath, which is Avalonia's own
code rather than ours — but "Avalonia is well tested" is not "we watched it work here". Treat
this as a release candidate.
Install
tar -xzf netpilot-linux-x64.tar.gz # or linux-arm64
cd netpilot-linux-x64
./NetPilot # start the window
sudo ./NetPilot # needed for firewall rules and bandwidth limitslinux-x64 for Intel/AMD, linux-arm64 for Raspberry Pi 4/5 and other 64-bit ARM.
One executable and its runtime; nothing to install. There is no .desktop entry or icon yet, so
the first launch is from a terminal.
What it does here
| Feature | Linux |
|---|---|
| Dashboard with live throughput and per-adapter counters | yes |
| Phone Tunnel → PC — pair the Android app with this machine | yes |
| DNS servers per interface | read |
| Adapters | yes |
| What this machine can do — the honest capability list | yes |
| Diagnostics — the log, and a one-click support report | yes |
| Block one application | yes, with root (nftables, by uid) |
| Bandwidth limit per application | yes, with root (tc, upload only) |
| Per-application traffic attribution | no |
| Per-application download limiting | no |
The two root-only rows apply per user, not per executable: meta skuid is what the kernel
can match on, so a rule covers everything running as that account. The app says so rather than
implying something finer-grained than the kernel can deliver.
The diagnostics page
Everything the app knows about itself in one place: the log, whether it is trustworthy (written /
dropped / queued), and a Copy for support button that puts the lot on the clipboard.
Pairing codes, bearer tokens and hardware addresses are replaced before anything reaches the
file, so the log can be pasted into a thread as it is. That is not a promise — redaction happens
before any sink sees the text, in one place, and there are tests asserting a bearer token and a
six-digit code never appear in the output.
Bugs this build found, by being run
Every one of these compiled cleanly and started cleanly:
System.Text.Jsondrops public fields.MvLinkwas declared with fields, so/status
answered with an array of empty objects while the process printed a healthy banner.- XAML cannot bind to a field. The same type change made every adapter render as a blank tile.
Fixed at the root: the shared contract types are properties now. - A custom-drawn Avalonia control is not re-rendered when a bound property changes. The
throughput number updated every two seconds and the chart stayed an empty baseline. - Double-encoded UTF-8. "Phone Tunnel → PC" rendered as mojibake, because an edit had read a
BOM-less file as Latin-1. Every non-ASCII character in the sources is a\uescape now, which
cannot be misread. - A log file cannot be read while it is being written.
File.ReadAllLinesopens with
FileShare.Read, which forbids other handles from writing — and the app's own writer holds the
file for exactly that. The diagnostics page would have failed on the log it exists to show. - A per-category log level could only make things quieter. The global threshold was checked
first, so "trace this subsystem while everything else stays at Info" silently did nothing. Shutdownpermanently killed logging. Disposing the queue made every later entry throw and
be swallowed, so the log simply stopped after the first call — which is what happens on a
settings change.
Not in this release
- 12 of the Windows app's 18 pages (DNS benchmark, Smart DNS, scheduled limits, history,
profiles, tools, settings). - Per-application download limiting, on either platform.
- Per-process traffic attribution — no base system offers it without eBPF.
- A desktop entry, an icon, and any package format (.deb, AppImage, Flatpak).
Licence: MIT.
NetPilot 1.2.2
NetPilot 1.2.2
The tunnel works but crawls — fixed. Plus the "Stop Service" button no longer fails silently.
Fixed — the tunnel was fragmenting every packet (Android)
The tunnel interface was created with MTU 4096. Nothing on a real network carries that:
the phone wrote packets up to 4 KB into the tunnel, they were IP-fragmented on the first hop,
and throughput collapsed into fragment reassembly. From the user's side it looked like "the
sharing works but the speed is bad".
The MTU is now 1400 — what WireGuard and the other mainstream Android tunnels use,
because it survives Wi-Fi, cellular, and a second encapsulation (a VPN inside the tunnel)
without fragmenting.
This is the single change most likely to be felt immediately. If the tunnel still feels slow
after this, the next things to look at are the relay's copy buffer and TCP_NODELAY on the
proxy's client socket.
Changed — "Stop Service" explains itself (Windows)
A failed Start used to be swallowed: the page simply sat at "stopped" with no reason, which
looks exactly like a dead button. Now:
- a bind that fails because the port is momentarily still held is retried (8 × 250 ms);
- and if it still cannot start, the reason is shown instead of nothing.
Note: on this machine the port is released instantly, so the original complaint could not be
reproduced here — the visibility fix stands on its own, but the underlying cause on the
reported machines is not yet confirmed. A screenshot of what the button does on one of those
machines would settle it.
Assets
| File | What it is |
|---|---|
NetPilot-Windows-1.2.2-Setup.exe |
Installer, self-contained. Run as administrator. |
NetPilot-1.2.2-release.apk |
Android release build, versionCode 4, minSdk 26, universal APK. |
NetPilot-Windows-1.2.2.exe |
The desktop exe alone, for machines with the .NET 8 runtime. |
Verification
62 Windows tests (4 new, driving the same Start/Stop calls the button makes), 92 Android unit
tests, lint.
Licence: MIT.
NetPilot daemon 1.2.2 - Linux and macOS (release candidate)
NetPilot daemon 1.2.2 — Linux and macOS
A separate netpilotd binary for Linux and macOS. The desktop app is WPF, which does not exist
on those platforms, so this shares its logic with the Windows build through NetPilot.Core.
What has actually been verified
These are release candidates, not a preview: the binaries here are the ones that passed
continuous testing on real Linux and real macOS machines (GitHub Actions runners with a real
kernel, real nftables, real networksetup). Every commit that goes into master runs that
suite.
Verified on a real Linux kernel (ubuntu-24.04, full root):
- the daemon starts, serves
/api/v1/*, and detects the machine's own interfaces and counters - an unprivileged run reports that it cannot enforce rules rather than pretending
nftaccepts the ruleset the backend generates, and the drop rule appears in the kernel- blocking a uid measurably cuts that uid off while the host keeps working — tested against
a listener on the machine, withsetpriv, in both directions (block, then unblock) tcinstalls the HTB qdisc and the per-uid flower filter, and the daemon verifies the qdisc
is really there instead of trusting the exit code- the runner's firewall is left clean afterwards
Verified on a real Mac (macos-14 arm64):
- link detection, with correct interface names and byte counters
- the VPN state reads
offon a machine with no VPN, and never falsely claimsactive - per-app blocking and shaping refuse, with the real reason — checked where
pfexists - DNS is read through the same
networksetuppath the app drives - 71 tests of the shared logic, run on macOS as well as Linux and Windows
Not verified: anything that needs real hardware or a real user — a USB-tethered phone, a
Wi-Fi hotspot, a second machine actually sharing over the tunnel, and bandwidth shaping under
real load. If you try those, this is a release candidate, not a finished product.
Feature reality
| Feature | Linux | macOS |
|---|---|---|
| Link detection + traffic counters | yes | yes |
| DNS control | yes | yes |
| System proxy (phone tunnel) | yes (GNOME) | yes |
| PC VPN state (3-way) | yes | yes |
| Snapshot / restore | DNS + proxy | DNS + proxy |
| Per-app blocking | yes (nftables, by uid) | no |
| Per-app upload limit | yes (tc, by uid) | no |
| Per-app download limit | no | no |
| Per-process traffic | no | no |
| Graphical interface | no | no |
macOS cannot block or throttle a single application: pf has no process matcher, and per-flow
shaping needs a Network Extension with Apple's approval. The daemon refuses with that reason
instead of offering a switch that does nothing.
On Linux, rules apply per user (uid) — that is what the kernel's matchers can do — not per
executable path.
Install
tar -xzf netpilotd-<rid>.tar.gz
./netpilotd # status only
sudo ./netpilotd # firewall rules and bandwidth limits become availablenft and tc need root. Without it the daemon starts, says so plainly, and serves status — it
does not pretend to enforce anything.
curl -s http://localhost:8787/api/v1/ping
curl -s http://localhost:8787/api/v1/status | jqPairing with the Android app
The phone speaks the same ten endpoints as on Windows, so no Android change is needed. Open
Phone Tunnel → PC on the phone, point it at this machine's address and port 8787, and pair
with the code the daemon prints. The phone cannot tell a daemon from the Windows app apart.
Known gaps
- No graphical interface. Everything is the API;
docs/PORTING.mdexplains what a GUI port
would involve and why it is a separate project. - Per-process traffic attribution: unavailable on both platforms (
/prochas no per-process
network counter, andlsofreports open sockets rather than bytes). - Per-target download shaping: not implemented on either.
- Only the
gsettingspath for the Linux system proxy is wired up; KDE and XFCE sessions need
the environment variable instead. - Hotspot detection on Linux needs the driver to expose it. Where none does, the phone link is
classified aslan, which still works.
Full detail: docs/LINUX-MACOS.md ·
docs/PORTING.md
Licence: MIT.
NetPilot 1.2.1
NetPilot 1.2.1
Patch release. One bug fix, reported by users: on some machines the phone-tunnel page claimed
the PC's VPN was connected while it was not — and, because an "active" PC VPN also blocks
sharing, it stopped that feature from working at all.
Fixed — the PC VPN indicator stopped lying
Root cause. A VPN client's virtual network card is reported Up by the driver on almost
every machine that merely has the client installed — and Windows has even self-assigned it an
APIPA address (169.254.x.x) while it was doing nothing. The detector treated driver-level
"Up" as a live tunnel.
This machine shows it exactly:
HotspotShield Network Adapter status=Disconnected ipv4=169.254.149.195
On machines where such a card reports Up instead, the page said "PC VPN: connected".
The rule now. An established tunnel carries a usable IPv4 address; an adapter that is up
but has none — or only an APIPA one — is a client that is installed and not tunnelling:
| Adapter state | Reported |
|---|---|
Windows VPN profile reports Connected |
Active (authoritative) |
| VPN adapter up with a usable address | Active |
| VPN adapter up without one | Unknown — a new, neutral third state |
| nothing VPN-looking up | Inactive |
Why a third state instead of just "inactive". Two of the three used to be lumped together,
and the guess was wrong in both directions. Unknown is shown in grey and does not block
sharing — only a confirmed active VPN does.
Also in this release
/api/v1/statusgainedpcVpnState(active/inactive/unknown).pcVpnActive
stays, so older phone builds keep working.
Verification
58 Windows tests (up from 43), including a new suite that pins the address rule and checks
the invariant against this machine's real adapters, plus the 92 Android unit tests and lint.
Assets
| File | What it is |
|---|---|
NetPilot-Windows-1.2.1-Setup.exe |
Installer, self-contained. Run as administrator. |
NetPilot-1.2.1-release.apk |
Android release build, versionCode 3, minSdk 26, universal APK. |
NetPilot-Windows-1.2.1.exe |
The desktop exe alone, for machines with the .NET 8 runtime. |
Licence: MIT.
NetPilot 1.2.0
NetPilot 1.2.0 — first public release.
Assets
| File | What it is |
|---|---|
NetPilot-Windows-1.2.0-Setup.exe |
Windows installer, self-contained (no .NET needed on the target machine). Run it as administrator. |
NetPilot-1.2.0-release.apk |
Android release build. versionName 1.2.0 / versionCode 2, universal (arm64-v8a, armeabi-v7a, x86, x86_64), minSdk 26. |
NetPilot-Windows-1.2.0.exe |
The desktop exe alone, for machines that already have the .NET 8 runtime. Prefer the installer. |
SHA256SUMS.txt |
SHA-256 of every file above. |
There is no debug build in this release on purpose: it keeps println logging on and is not meant for users.
Android
- Network health dashboard — one score for DNS quality, latency, packet loss and link
stability, with a live traffic chart and running totals. - DNS manager — curated resolvers plus your own, each benchmarked for response time, with
a one-tap restore of whatever was configured before. - Live monitor — download/upload charts from a one-second to a one-hour window.
- Per-app usage — who is actually downloading, sorted by volume.
- Internet limiter — a download/upload cap per app, or block it outright. The caps are
enforced in the tunnel, not just displayed. - Scheduled limits, profiles with export/import, network tools (ping, nslookup,
cache flush), and an adapter inventory. - Local VPN tunnel — DNS-only or full-tunnel mode, failing open to the normal path so the
phone never loses connectivity. - Phone Tunnel → PC — hand the phone's VPN tunnel to a Windows PC over shared Wi-Fi, the
phone's hotspot, USB tethering, oradb reverse.
Bilingual English/Persian with proper RTL. No location, files or camera permissions; usage
access is optional and only feeds the per-app usage table.
Windows
The same toolset as a desktop app, plus the Phone Tunnel → PC page that pairs with the
Android app and lets the desktop browse through the phone. Upload caps are real Windows QoS
policies; download caps are enforced with a token bucket. Network state is snapshotted before
any change and can be restored.
Fixed in this release
Android
- The desktop never actually rode the phone's VPN: the LAN proxy's sockets were excluded
along with the rest of the app's package, so traffic left through the phone's normal
interface. They are now put back into the tunnel withVpnService.protect(). - DNS lookups were tried one resolver after another, so a dead first resolver slowed down
every lookup. All resolvers are now queried at once and the first real answer wins. - The proxy's 20 s idle timeout cut HTTPS connections and websockets mid-session; now 120 s.
Windows
- Starting the bridge deadlocked the UI thread on a PowerShell child. The listener came up
and the firewall rule was created, but the accept loop never started, so every request from
the phone hung until it timed out — reported as "PC unreachable". - Restoring the network reported success even when every adapter had failed.
- The proxy reachability probe gave a live proxy on 127.0.0.1 a false "unreachable" when the
host also resolved to ::1. - The internet limiter silently stopped applying limits after a restart.
Verification
92 Android unit tests, 43 Windows unit and integration tests — the latter include suites that
change real DNS servers and real Windows QoS policies and then put them back — plus an
end-to-end installer test. NetPilot/Tools/verify.ps1 runs everything and fails if the suite
changed anything on the machine.
Signing note
The release keystore is deliberately not in this repository. To build a signed APK, copy
NetPilotMobile/keystore.properties.example to keystore.properties and fill it in, or
export NETPILOT_STORE_FILE, NETPILOT_STORE_PASSWORD, NETPILOT_KEY_ALIAS and
NETPILOT_KEY_PASSWORD. Without either, assembleRelease still builds an unsigned APK.
Licence: MIT.