Metadata-Version: 2.4
Name: frp-jump
Version: 0.3.5
Summary: P2P-with-relay-fallback ssh/tcp tunnels between Linux boxes, built on frp
Author: Alexander Degtyarev
Author-email: Alexander Degtyarev <a.degtyarev@struhe.com>
License-Expression: MIT
License-File: LICENSE
Requires-Dist: typer>=0.15
Requires-Dist: rich>=13.9
Requires-Dist: tomli-w>=1.1
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic-settings>=2.15.0
Requires-Dist: fastapi>=0.115 ; extra == 'server'
Requires-Dist: uvicorn[standard]>=0.32 ; extra == 'server'
Requires-Dist: sqlmodel>=0.0.22 ; extra == 'server'
Requires-Dist: cryptography>=43.0 ; extra == 'server'
Requires-Python: >=3.12
Provides-Extra: server
Description-Content-Type: text/markdown

# frp-jump

Connect Linux boxes (including Wiren Board controllers) to each other over
the internet using the SSH/TCP clients you already have — no manual port
juggling, and no caring whether the link ended up peer-to-peer or relayed
through your server.

- **Tunneling** is [frp](https://github.com/fatedier/frp) (frpc/frps),
  driven through an abstract `TunnelDriver`/`RelayDriver` interface — see
  [`docs/architecture.md`](docs/architecture.md).
- **P2P with relay fallback** is frp's own `xtcp` + `fallbackTo` feature: a
  device pair first tries a direct (hole-punched) connection, and
  transparently falls back to relaying through your server if that doesn't
  complete within a timeout.
- **Identity**: a person is identified by an SSH public key (`users
  add-key`), not an email/password account. There is no WebUI — admin
  actions are `frp-jump-server` commands run over SSH to the box.
- **Encryption**: a private CA (run by your server) issues an mTLS cert to
  every enrolled device, so nothing unenrolled can reach the relay at all.
  A per-pair secret gates every connection, and the tunneled traffic
  itself — not just the control channel — is encrypted too.
- **Self-service**: once a person's first device is enrolled, they manage
  everything else themselves from the CLI (adding devices, connecting to
  each other), scoped to devices they own. Disabling a device or deleting a
  grant takes effect on its next sync, not instantly.
- **UX goal**: after setup, `ssh <name>` just works with your stock ssh
  client, whether the path underneath is p2p or relayed.

## Installing the CLI

Two supported ways to install, plus a manual fallback. Either one gets you
the same `frp-jump-client`/`frp-jump-server` CLIs; pick whichever fits how
you manage the box.

### Option A: pip

Published on PyPI as [`frp-jump`](https://pypi.org/project/frp-jump/),
Python 3.12+ required. It splits into two lean pieces sharing one package,
so a device install doesn't pull in the server's dependencies:

```sh
python3 -m venv .venv    # Debian/Ubuntu: `apt install python3-venv` first

# on the server box:
.venv/bin/pip install 'frp-jump[server]'   # pulls in fastapi/uvicorn/sqlmodel
.venv/bin/frp-jump-server ...

# on every device you want to connect (including headless/IoT ones):
.venv/bin/pip install frp-jump             # lean: no server-only deps
.venv/bin/frp-jump-client ...
```

Put `.venv/bin` on `PATH`, or call the binaries by their full path.

### Option B: apt repository (Debian/Ubuntu, including Wiren Board)

A self-contained `.deb` (own Python 3.12 runtime, no venv/pip involved) via
a signed apt repo hosted on GitHub Pages — add it once, then `apt upgrade`
picks up new releases like any other package:

```sh
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://aadegtyarev.github.io/frp-jump/frp-jump-archive-keyring.asc \
  | sudo gpg --dearmor -o /etc/apt/keyrings/frp-jump.gpg
echo "deb [signed-by=/etc/apt/keyrings/frp-jump.gpg] https://aadegtyarev.github.io/frp-jump stable main" \
  | sudo tee /etc/apt/sources.list.d/frp-jump.list

sudo apt update
sudo apt install frp-jump-client
```

Only `frp-jump-client` is packaged as a `.deb` today — run the server side
via pip.

### Fallback: download a `.deb` directly

Each [GitHub Release](https://github.com/aadegtyarev/frp-jump/releases)
also attaches the `.deb` for each architecture (amd64/arm64/armhf)
individually — useful for an air-gapped box, or if you'd rather not add the
apt repo. You're then on your own for upgrades (repeat the download each
release):

```sh
wget https://github.com/aadegtyarev/frp-jump/releases/download/vX.Y.Z/frp-jump-client_X.Y.Z_arm64.deb
sudo apt install ./frp-jump-client_X.Y.Z_arm64.deb
```

Every command has full `--help` text with runnable examples — start there
if anything below is unclear (`frp-jump-client <command> --help`).

## Quick start

On the **server** (a box with a public IP/domain):

```sh
sudo frp-jump-server install-service --relay-public-addr tunnel.example.com
```

That one command creates a dedicated, unprivileged `frp-jump` system user,
bootstraps the CA/database under its own `/var/lib/frp-jump`, and installs
+ starts a hardened systemd unit — see "systemd" below. To do it by hand
instead:

```sh
export FRP_JUMP_RELAY_PUBLIC_ADDR=tunnel.example.com   # or a bare IP
frp-jump-server init
frp-jump-server run          # foreground; wrap with systemd for real use
```

In another shell (or over SSH, any time later), register yourself:

```sh
frp-jump-server users add-key ~/.ssh/id_ed25519.pub --label me
```

Now enroll your first device — key-based needs no token, once your key is
registered; token-based works for a device you don't want to hand a
personal key to:

```sh
frp-jump-client enroll https://tunnel.example.com ~/.ssh/id_ed25519 --name laptop

# or, token-based:
frp-jump-server enroll-tokens create --user me --name wb01
frp-jump-client enroll https://tunnel.example.com <token-from-above>
```

Run as root, `enroll` also installs and starts the systemd service right
away; otherwise it tells you to run one of these next:

```sh
frp-jump-client install-service                                  # system-wide, needs root
frp-jump-client install-service --user                            # your own account, no root needed
frp-jump-client install-service --system-user frp-jump-client     # isolated account (created for you)
```

From there, everything is self-service — no more admin action needed for
that person, ever, even to add a tenth device or reconfigure who talks to
whom:

```sh
frp-jump-client devices add-token               # mint a token to chain-enroll
                                                 # another of your own devices
frp-jump-client devices list                    # devices you own
frp-jump-client connect wb01:22                 # tunnel to port 22 on "wb01"
                                                 # (22/2222 are well-known ssh
                                                 # ports; anything else is a
                                                 # plain tcp tunnel, or --ssh)
frp-jump-client status                          # what's exposed/consumed,
                                                 # local addresses once synced
frp-jump-client doctor                          # something seems wrong? run
                                                 # this first
frp-jump-client disconnect wb01                 # tear it back down
frp-jump-client devices disable old-laptop      # lost it? block its connections
frp-jump-client devices delete old-laptop       # gone for good, frees the name
```

Once synced (each side's agent polls every `agent_poll_interval_seconds`,
default 2s — override per-run with `frp-jump-client run --poll-interval N`):

```sh
ssh wb01                     # just works -- `connect`'s local profile name
                              # doubles as the ssh_config Host alias
```

An admin can see everyone's devices and connections (including a compact
relayed-traffic figure) without touching anyone's box:

```sh
frp-jump-server users list
frp-jump-server users show me
frp-jump-server devices list
frp-jump-server devices disable wb01 --user me    # force-disconnect + block,
                                                   # reversible with `enable`
```

## Configuration

Everything configurable lives in one place:
[`src/frp_jump/common/settings.py`](src/frp_jump/common/settings.py) — set
via `FRP_JUMP_<FIELD>` environment variables, or a TOML file
(`$FRP_JUMP_CONFIG_FILE`, else the first of `./frp-jump.toml`,
`~/.config/frp-jump/config.toml`, `/etc/frp-jump/config.toml` that exists).
Env vars win over the file.

The only setting with no sane default is `relay_public_addr` — the address
other devices dial to reach your relay; `frp-jump-server init`/`run` refuse
to start without it.

## Ports and firewalls

**On the server**, open these inbound (both plain TCP, no exotic
protocols):

| Port | Setting | What it's for |
| --- | --- | --- |
| 8443/tcp | `api_port` | The agent-facing HTTPS API (enroll, heartbeat, desired-state, self-service) |
| 7000/tcp | `relay_bind_port` | frps control channel, dialed by every enrolled device's frpc |

`frps_admin_port` (7500) is bound to `127.0.0.1` only and never needs a
firewall rule.

**On every client device**, allow *outbound* to those same two ports on
your relay. p2p (`xtcp`) hole-punching also needs outbound UDP to work —
a firewall that blocks it (or restricts frpc's ephemeral source ports)
won't break anything outright, but every connection will silently fall
back to relay instead of going peer-to-peer, since the relay fallback
only needs the already-open TCP control channel above. The consumer's own
locally-bound port (`agent_local_port_range_*`, default 40000-40999) is
only ever bound to `127.0.0.1`, so it never needs a firewall rule either.

## systemd

**Client** — `frp-jump-client install-service [--user | --system-user
NAME]` generates and enables the unit for you (see "Quick start" above).
Which mode to pick:

- A device that *consumes* an SSH grant needs the agent running as the
  actual human (so it can maintain their real `~/.ssh/config`) — use
  `--user`, or point `FRP_JUMP_SSH_CONFIG_PATH` at that person's config
  explicitly.
- A device that only *exposes* services (e.g. a Wiren Board controller),
  or consumes without needing `ssh <name>` to just work, is fine as a
  system service or under `--system-user` — an isolated, least-privilege
  account created for you.

Reach for [`packaging/systemd/`](packaging/systemd/) directly only for a
manual or packaged (`.deb`) install.

**Server** — `frp-jump-server install-service [--system-user NAME]
[--relay-public-addr ADDR]` is the equivalent one-shot setup: creates the
system account, bootstraps the CA/database, writes
`/etc/frp-jump/<name>.env`, and installs a hardened unit
(`ProtectSystem=strict`, `NoNewPrivileges=yes`, one writable data
directory) — matching
[`packaging/systemd/frp-jump-server.service`](packaging/systemd/frp-jump-server.service)
if you'd rather set it up by hand.

## Development

```sh
uv sync
uv run pytest tests/unit -q        # fast, no network
uv run ruff check .
uv run pytest tests/integration -m integration -q   # downloads real frp binaries; loopback only
```

The integration test proves the whole chain works over loopback (real frp
binaries, real mTLS, real xtcp-timeout-then-stcp-fallback), but it can't
prove real NAT hole-punching across two separate networks — check that
manually on real devices.

## Layout

```
src/frp_jump/
  common/     PKI (private CA), opaque tokens, settings, DB models
  driver/     TunnelDriver/RelayDriver abstraction; driver/frp/ = the frp
              implementation (config rendering, binary download+checksum,
              process supervision)
  server/     control-plane: registry (CRUD), the agent-facing API,
              bootstrap (`server init`) -- no WebUI, admin is CLI-only
  agent/      runs on every device: enroll (token or SSH key), the sync
              loop, local profiles, ssh_config management, systemd
              install (`agent/service_install.py`)
  cli/        `frp-jump-server ...` / `frp-jump-client ...`
```

## Contributing

See [`CONTRIBUTING.md`](CONTRIBUTING.md). Changes are tracked in
[`CHANGELOG.md`](CHANGELOG.md).

## License

[MIT](LICENSE)
