JM Tech Power

jm-tech-power-official/JM Net

A private network across your own machines. Enroll a device and reach it by a stable 10.64.x.y address from anywhere, with no port forwarding. Your WireGuard private key is generated on the device and never leaves it.

公开 Networking Windows, macOS, Linux LGPL-3.0 (PySide6)
README.md

JM Net — a private network across your own machines

A WireGuard-based private network for your devices. Enroll a device, connect, and it gets its own address on a private range shared with your other machines.

  • Category: Networking
  • Version: 1.3.0
  • OS: Linux, macOS, Windows
  • Dependencies: PySide6 (Qt for Python), installed into an isolated venv;

WireGuard tools, installed separately (see below)

JM Net is a small desktop client built with PySide6. It talks to https://www.jmtechpower.com/api/v1/net/* to enroll this device and list the others on your network, and it drives WireGuard locally to bring the tunnel up and down. In Phase 1 every device's only peer is the hub — there is no mesh and no background agent, so the device list in the window is display-only.

What's new in 1.3.0

JM Net has moved to a new address range, and every device must enroll again. Your existing devices have been removed; open JM Net, sign in, and enroll each machine as you did the first time. Nothing else about the app has changed, and your private keys were never on the server to begin with.

Why the move: JM Net used to hand out addresses in 100.64.0.0/10. That range was picked to stay clear of the 192.168.x and 10.x addresses home routers use — but Tailscale claims all of it, and on Linux it installs a firewall rule that discards anything arriving from that range on an interface that is not Tailscale's own. If you ran both, JM Net connected, reported a healthy tunnel, and then quietly carried no traffic at all. There is no way to work around that from our side: the rule belongs to Tailscale and it wins.

The new range is 10.64.0.0/10, and — more to the point — the range is now a server setting rather than something written into the code, so if it ever has to move again it will be a configuration change and a re-enrollment, nothing more.

What's new in 1.2.5

Both of these were reported from a real Mac, and the first one is a fix for a fix that did not work.

  • **Connect no longer fails on macOS with ``jmnet0' already exists as

utun12'.** macOS will not let WireGuard name an interface, so JM Net was not recognising its own live tunnel: it reported "not connected", tried to connect again, and wg-quick refused because the tunnel was already up. It now identifies its tunnel from ifconfig — a utun carrying an address in 10.64.0.0/10. The previous attempt read /var/run/wireguard/<utunN>.name, which cannot work, because that directory is not readable by the app (it runs as you, not as root). See "On macOS the interface is called utunN`" below.

  • Connecting asks for your password exactly once. Installing the

root-owned configuration and bringing the interface up were two separate elevated steps, so one Connect click cost two prompts — and three or four while the bug above was sending it round the retry path. They are now one elevated step. A failed connect still distinguishes "could not install the configuration" (usually a declined prompt) from "could not bring the interface up" (a config wg-quick refused), because the two have different fixes. Your private key still never appears in a command line or in the elevated script — it travels in a file only you can read.

One caveat, stated rather than papered over: before this device has enrolled, another tunnel on the same Mac carrying an address in 10.64.0.0/10 would also be recognised as JM Net's. Once the device is enrolled the check matches the exact address the server gave it, so the ambiguity only exists in a window where there is no JM Net tunnel to confuse.

Install

WireGuard tools must be installed before you run the installer — JM Net is useless without them, and the installer checks and refuses to continue if they are missing.

  • Linux: sudo apt install wireguard-tools (or sudo dnf install wireguard-tools)
  • macOS: brew install wireguard-tools
  • Windows: download and install WireGuard from

https://www.wireguard.com/install/

Then:

unzip the package && ./install.sh # Linux / macOS jmnet # launch

On Windows, run install.ps1 in PowerShell instead:

.\install.ps1

The installer creates an isolated virtualenv just for JM Net (so its PySide6 dependency never touches your system Python) and installs a jmnet launcher on your PATH; on Linux it also adds a desktop entry so JM Net shows up in your applications menu.

  • Linux/macOS: app lives in ~/.local/share/jmnet, launcher at

~/.local/bin/jmnet.

  • Windows: app lives in %LOCALAPPDATA%\Programs\jmnet, jmnet.cmd on PATH.

Uninstall with ./uninstall.sh (or .\uninstall.ps1). It removes everything JM Net put on this machine, including its secrets, and on Linux/macOS it also revokes this device on the server so you do not have to go and do it by hand. In order:

  • the device row on the server — the uninstaller calls

DELETE /api/v1/net/devices/<id> with the token in your state file, so the address this machine was using goes back to your network instead of sitting there enrolled forever. This needs no root. If the token has expired, the server cannot be reached, or the call fails for any other reason, the uninstaller says so and carries on — it will never refuse to uninstall because of a failed web request, and it tells you where to finish the job (your device list at https://www.jmtechpower.com/net);

  • the tunnel and the WireGuard config (/etc/wireguard/jmnet0.conf, or

the staged %LOCALAPPDATA%\jmnet\jmnet0.conf on Windows), which holds this device's private key. Both need root on Linux/macOS and both happen in a single elevated step, so it is one polkit/sudo prompt for the whole uninstall, not one per operation. If you decline it, you are told separately which of the two did not happen and the exact command to finish it;

  • the app's state file (~/.config/jmnet/state.json, or

%APPDATA%\jmnet\state.json), which holds your API token and the cached address;

  • the app itself, the launcher and the desktop entry.

Running it twice is harmless: with nothing left it asks for nothing, prints nothing alarming, and exits 0.

Because nothing is left behind, uninstalling and reinstalling really does start fresh: the next Connect enrolls a new device rather than reusing a stale config — and, since the old row was revoked, without quietly using up another of your 254 addresses.

On Windows, uninstall.ps1 does not yet revoke the device; remove it from
your device list at https://www.jmtechpower.com/net .

Using JM Net

  • Connect — the first time, you'll be asked for an API token (from

https://www.jmtechpower.com/settings/tokens) and a name for this device. JM Net then generates a keypair, enrolls the device with the hub, writes a WireGuard config, and brings the tunnel up. That costs exactly one authorization prompt — a polkit/sudo prompt on Linux, an administrator prompt on macOS, a UAC prompt on Windows — because installing the root-owned config and bringing the interface up are composed into a single elevated step. If you are ever asked twice for one Connect click, that is a bug; please report it.

  • Disconnect — brings the tunnel down.
  • On macOS the interface is called utunN, not jmnet0. macOS will not

let WireGuard name an interface, so the kernel picks e.g. utun12. JM Net therefore recognises its own tunnel by reading ifconfig and looking for a utun carrying this device's own JM Net address — the exact one the server allocated at enrollment, not merely something in the range, so another VPN cannot be mistaken for it. (Before the device has enrolled there is no such address yet, and it falls back to matching the range 10.64.0.0/10; the worst that can happen then is one redundant connect attempt.) It deliberately does not read /var/run/wireguard, because that directory is not readable by the app (it runs as you, not as root), and an earlier attempt to detect the tunnel that way reported a live tunnel as disconnected.

  • The status line reports a working tunnel, not just an interface.

Bringing the interface up succeeds even when no WireGuard handshake is possible at all, so "Connected" is only shown when the hub confirms it has recently handshaked with this device. Otherwise you are told which of these it is:

  • the hub has never responded — the interface is up but nothing is

getting through; usually UDP 51821 is blocked on your network, or the hub endpoint is unreachable from where you are;

  • stalled — it handshaked once but the hub has not heard from this

device for over three minutes (keepalive is every 25s);

  • unverified — the tunnel is up but the server could not be asked, so

neither "working" nor "broken" would be honest.

  • Refresh — reloads the device list from the server. If the server is

unreachable, the list is shown as unavailable, but this does not mean your tunnel is down — the API is only needed to enroll and to list devices, never to keep an established tunnel running.

Troubleshooting

It says Connected, the handshake works, but nothing can reach anything

Almost always your local firewall, and almost always on Linux with ufw.

wg-quick brings the interface up; it does not manage the host firewall. With ufw active and the usual default-deny inbound policy the tunnel connects for real — the handshake completes, packets arrive and WireGuard decrypts them — and then the firewall drops them before they reach any application. Everything you can see says the tunnel is fine, because it is: the fault is one layer above it.

The fingerprint, and the thing that actually identified this the first time:

cat /sys/class/net/jmnet0/statistics/rx_packets

Run it, send four pings over the tunnel, run it again. If that counter goes up by four while ping reports 100% loss, the replies are arriving and being decrypted and something on this machine is dropping them afterwards.

Fix:

sudo ufw allow in on jmnet0

That allows inbound traffic only on the JM Net interface — traffic WireGuard has already cryptographically authenticated. It opens no port to the outside world.

JM Net adds this rule for you as part of the same elevated step as bringing the tunnel up (one prompt, not two) — but only when it can see that ufw is active and the rule is missing. ufw status needs root to read, so when the app cannot read it, it deliberately does nothing rather than guess about your firewall. In that case run the command above yourself.

If you use firewalld or plain nftables instead

Same fault, different firewall — and there is no single safe rule JM Net could add to someone else's ruleset, so it does not add one. What it does instead is tell you what it found, after you connect:

  • firewalld (detected with firewall-cmd --state, which needs no root):

sudo firewall-cmd --zone=trusted --add-interface=jmnet0 sudo firewall-cmd --permanent --zone=trusted --add-interface=jmnet0

The second line makes it survive a reboot. This trusts only the JM Net interface — traffic WireGuard has already authenticated — and opens no port to the outside world.

  • plain nftables with a default-drop input chain: you need a rule

accepting input on jmnet0 in your own input chain, roughly sudo nft insert rule inet filter input iifname "jmnet0" accept. Your table and chain are probably named something else, so check sudo nft list ruleset first, and put it in your persistent config rather than only in the live ruleset.

Reading an nftables ruleset needs root, so if JM Net cannot read yours it says exactly that — "could not look", not "you are fine". If no firewall is detected at all, it says nothing.

macOS and Windows do not need any of this: pf is permissive for this case by default, and the WireGuard for Windows installer registers its own firewall rules for the tunnel adapter.

How it works

  • Your private key never leaves this machine. It is generated locally,

written only into the WireGuard config, and never stored in the app's own state file or sent to the server. The server only ever sees your public key.

  • On Linux and macOS that config is /etc/wireguard/jmnet0.conf,

owned by root and mode 0600, so no other account on the machine can read it.

  • On Windows the protection is weaker, and it is worth being straight

about it. JM Net writes a staging copy under your own %LOCALAPPDATA%\jmnet, strips its inherited permissions with icacls so only you, Administrators and SYSTEM can read it, hands it to wireguard.exe /installtunnelservice — which copies it into WireGuard's own SYSTEM-only store — and then deletes the staging copy. It is not written to %PROGRAMDATA%, which grants Users read access by inheritance and would expose the key to every local account. None of the Windows path has been exercised on a real Windows machine, so the honest claim is "briefly readable only by you", not "root-owned".

  • Phase 1 routes only 10.64.0.0/10 — the JM Net address range — not

all of your internet traffic. Your ordinary browsing and everything else on this machine is untouched. The server enforces this too: the hub drops anything arriving on the tunnel that is not destined for another JM Net address, so editing AllowedIPs in the config by hand will not turn JM Net into an exit node or a route onto the server's other networks.

  • If your network goes over its monthly relay cap, the app surfaces that

in the status line rather than leaving you to wonder why devices stopped talking to each other.

Notes

  • Built with PySide6 (not PyQt).
  • The device list is display-only: because every device's only peer is the

hub, connecting or disconnecting another device never changes your own config, so there is nothing here to sync in the background.