frp-jump 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. frp_jump-0.1.1/LICENSE +21 -0
  2. frp_jump-0.1.1/PKG-INFO +206 -0
  3. frp_jump-0.1.1/README.md +184 -0
  4. frp_jump-0.1.1/pyproject.toml +61 -0
  5. frp_jump-0.1.1/pyproject.toml.orig +53 -0
  6. frp_jump-0.1.1/src/frp_jump/__init__.py +0 -0
  7. frp_jump-0.1.1/src/frp_jump/agent/__init__.py +0 -0
  8. frp_jump-0.1.1/src/frp_jump/agent/enroll.py +40 -0
  9. frp_jump-0.1.1/src/frp_jump/agent/hosts.py +47 -0
  10. frp_jump-0.1.1/src/frp_jump/agent/poller.py +218 -0
  11. frp_jump-0.1.1/src/frp_jump/agent/state.py +79 -0
  12. frp_jump-0.1.1/src/frp_jump/cli/__init__.py +0 -0
  13. frp_jump-0.1.1/src/frp_jump/cli/client_cmds.py +155 -0
  14. frp_jump-0.1.1/src/frp_jump/cli/main.py +17 -0
  15. frp_jump-0.1.1/src/frp_jump/cli/server_cmds.py +125 -0
  16. frp_jump-0.1.1/src/frp_jump/common/__init__.py +0 -0
  17. frp_jump-0.1.1/src/frp_jump/common/crypto.py +35 -0
  18. frp_jump-0.1.1/src/frp_jump/common/models.py +138 -0
  19. frp_jump-0.1.1/src/frp_jump/common/pki.py +221 -0
  20. frp_jump-0.1.1/src/frp_jump/common/settings.py +100 -0
  21. frp_jump-0.1.1/src/frp_jump/driver/__init__.py +0 -0
  22. frp_jump-0.1.1/src/frp_jump/driver/base.py +116 -0
  23. frp_jump-0.1.1/src/frp_jump/driver/frp/__init__.py +0 -0
  24. frp_jump-0.1.1/src/frp_jump/driver/frp/binaries.py +154 -0
  25. frp_jump-0.1.1/src/frp_jump/driver/frp/config.py +133 -0
  26. frp_jump-0.1.1/src/frp_jump/driver/frp/driver.py +136 -0
  27. frp_jump-0.1.1/src/frp_jump/driver/frp/process.py +55 -0
  28. frp_jump-0.1.1/src/frp_jump/server/__init__.py +0 -0
  29. frp_jump-0.1.1/src/frp_jump/server/api.py +142 -0
  30. frp_jump-0.1.1/src/frp_jump/server/app.py +37 -0
  31. frp_jump-0.1.1/src/frp_jump/server/auth.py +100 -0
  32. frp_jump-0.1.1/src/frp_jump/server/bootstrap.py +108 -0
  33. frp_jump-0.1.1/src/frp_jump/server/db.py +46 -0
  34. frp_jump-0.1.1/src/frp_jump/server/registry.py +413 -0
  35. frp_jump-0.1.1/src/frp_jump/server/templates/base.html +82 -0
  36. frp_jump-0.1.1/src/frp_jump/server/templates/dashboard.html +176 -0
  37. frp_jump-0.1.1/src/frp_jump/server/templates/login.html +11 -0
  38. frp_jump-0.1.1/src/frp_jump/server/templates/result.html +14 -0
  39. frp_jump-0.1.1/src/frp_jump/server/web.py +271 -0
frp_jump-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Degtyarev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: frp-jump
3
+ Version: 0.1.1
4
+ Summary: P2P-with-relay-fallback ssh/http/tcp tunnels between Linux boxes, built on frp
5
+ Author: Alexander Degtyarev
6
+ Author-email: Alexander Degtyarev <a.degtyarev@struhe.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: typer>=0.15
10
+ Requires-Dist: rich>=13.9
11
+ Requires-Dist: fastapi>=0.115
12
+ Requires-Dist: uvicorn[standard]>=0.32
13
+ Requires-Dist: jinja2>=3.1
14
+ Requires-Dist: python-multipart>=0.0.17
15
+ Requires-Dist: sqlmodel>=0.0.22
16
+ Requires-Dist: cryptography>=43.0
17
+ Requires-Dist: tomli-w>=1.1
18
+ Requires-Dist: httpx>=0.27
19
+ Requires-Dist: pydantic-settings>=2.15.0
20
+ Requires-Python: >=3.12
21
+ Description-Content-Type: text/markdown
22
+
23
+ # frp-jump
24
+
25
+ Connect Linux boxes (including Wiren Board controllers) to each other over
26
+ the internet using the SSH/HTTP/TCP clients you already have — no manual
27
+ port juggling, and no caring whether the link ended up peer-to-peer or
28
+ relayed through your server.
29
+
30
+ - **Tunneling** is [frp](https://github.com/fatedier/frp) (frpc/frps),
31
+ driven through an abstract `TunnelDriver`/`RelayDriver` interface — see
32
+ [`docs/architecture.md`](docs/architecture.md).
33
+ - **P2P with relay fallback** is frp's own `xtcp` + `fallbackTo` feature: a
34
+ device pair first tries a direct (hole-punched) connection, and
35
+ transparently falls back to relaying through your server if that doesn't
36
+ complete within a timeout. Not something this project implements itself.
37
+ - **Security**: a private CA (run by your server) issues an mTLS cert to
38
+ every enrolled device, so nothing unenrolled can reach the relay at all;
39
+ a per-pair secret means even enrolled devices can't reach each other's
40
+ services without an explicit grant; the WebUI is behind passwordless
41
+ magic-link auth (no passwords, no SMTP — links are generated by the
42
+ server and you hand-deliver them yourself); only an admin can add
43
+ devices, wire up services/grants, or revoke them — an invited non-admin
44
+ user can view the dashboard but not change it. Revoking a device or
45
+ grant takes effect on its next sync, not instantly — see
46
+ [`docs/architecture.md`](docs/architecture.md) for what that does and
47
+ doesn't cover.
48
+ - **UX goal**: after setup, `ssh <name>` and `http://127.0.0.1:<port>` just
49
+ work with stock clients, whether the path underneath is p2p or relayed.
50
+
51
+ ## Installing the CLI
52
+
53
+ Not published to PyPI — install on **every** machine that will run either
54
+ `frp-jump server ...` or `frp-jump client ...` (the server box, and each
55
+ device you connect), Python 3.12+ required (already present on any recent
56
+ Debian/Ubuntu, including Wiren Board controllers). Two ways:
57
+
58
+ **From a tagged release** (built by `.github/workflows/release.yml`,
59
+ shows up under this repo's *Releases* tab):
60
+
61
+ ```sh
62
+ python3 -m venv .venv # needs the venv module: on Debian/Ubuntu that's
63
+ # a separate package, `apt install python3-venv`
64
+ gh release download v0.1.0 --repo aadegtyarev/frp-jump --pattern '*.whl' -O frp_jump.whl
65
+ .venv/bin/pip install frp_jump.whl
66
+ ```
67
+
68
+ Needs the [`gh` CLI](https://cli.github.com/) authenticated
69
+ (`gh auth login`) since the repo is private — fine on your own machine,
70
+ probably not worth installing just for a headless device (see below).
71
+
72
+ **Directly from a git tag** (no release/wheel involved, works anywhere
73
+ you can already `git clone` the repo — e.g. via the same **read-only
74
+ deploy key** pattern used for node1 in `docs/architecture.md`; `gh repo
75
+ deploy-key add` to mint one per device):
76
+
77
+ ```sh
78
+ python3 -m venv .venv
79
+ .venv/bin/pip install "git+ssh://git@github.com/aadegtyarev/frp-jump.git@v0.1.0"
80
+ ```
81
+
82
+ Either way you end up with `frp-jump` in that venv — put `.venv/bin` on
83
+ `PATH`, or call `.venv/bin/frp-jump` directly. The rest of this README
84
+ just says `frp-jump`.
85
+
86
+ To cut a new release: `git tag vX.Y.Z && git push origin vX.Y.Z` (bump
87
+ `version` in `pyproject.toml` first) — the workflow builds and publishes
88
+ it automatically.
89
+
90
+ ## Quick start
91
+
92
+ On the **server** (a box with a public IP/domain):
93
+
94
+ ```sh
95
+ export FRP_JUMP_RELAY_PUBLIC_ADDR=tunnel.example.com # or a bare IP
96
+ frp-jump server init --admin-email you@example.com
97
+ # -> prints an admin login link: open it in a browser to sign in
98
+ frp-jump server run
99
+ ```
100
+
101
+ Lost that link, or need to sign in from somewhere else? It's not your
102
+ only way in — mint a fresh one any time:
103
+
104
+ ```sh
105
+ frp-jump server login-link you@example.com # new login link
106
+ frp-jump server login-link friend@example.com --invite # invite someone else
107
+ ```
108
+
109
+ (Under the systemd deployment below, `server login-link` needs the same
110
+ `FRP_JUMP_*` env vars as `server run`, which it won't pick up on its own
111
+ outside of systemd's `EnvironmentFile` — see
112
+ [`packaging/scripts/frp-jump-login-link`](packaging/scripts/frp-jump-login-link)
113
+ for a copy-pasteable wrapper.)
114
+
115
+ In the WebUI: **Add device** for each box you want to connect — this is
116
+ also where the enroll token comes from, there's no other source for it.
117
+ It prints a one-time `frp-jump client enroll <url> <token>` command; run
118
+ that *on the device itself* (not on the server). Then define a
119
+ **service** on the exposing device (e.g. `wb01-ssh`, protocol `ssh`, local
120
+ port `22`) and a **grant** wiring a consuming device to it.
121
+
122
+ On **each device** (after [installing the CLI](#installing-the-cli) there
123
+ too):
124
+
125
+ ```sh
126
+ frp-jump client enroll https://tunnel.example.com <token-from-webui>
127
+ frp-jump client run # foreground; wrap with systemd for real use
128
+ ```
129
+
130
+ Once the consuming device has synced (its agent polls every
131
+ `agent_poll_interval_seconds`, default 30s):
132
+
133
+ ```sh
134
+ ssh wb01-ssh # just works — see `frp-jump client status` for
135
+ # the local address of a non-ssh grant
136
+ ```
137
+
138
+ ## Configuration
139
+
140
+ Everything configurable lives in one place:
141
+ [`src/frp_jump/common/settings.py`](src/frp_jump/common/settings.py) — set
142
+ via `FRP_JUMP_<FIELD>` environment variables, or a TOML file
143
+ (`$FRP_JUMP_CONFIG_FILE`, else the first of `./frp-jump.toml`,
144
+ `~/.config/frp-jump/config.toml`, `/etc/frp-jump/config.toml` that
145
+ exists). Env vars win over the file. Nothing else in the codebase
146
+ hardcodes a port, TTL, or version pin.
147
+
148
+ The only setting with no sane default is `relay_public_addr` — the
149
+ address other devices dial to reach your relay; `server init`/`server run`
150
+ refuse to start without it.
151
+
152
+ ## systemd
153
+
154
+ See [`packaging/systemd/`](packaging/systemd/). The client unit's comments
155
+ explain a real gotcha: a device that *consumes* an SSH grant needs the
156
+ agent running as the actual human (so it can maintain their real
157
+ `~/.ssh/config`) — a `--user` unit, not a system one, unless you point
158
+ `FRP_JUMP_SSH_CONFIG_PATH` at that user's config explicitly. A
159
+ device that only *exposes* services (e.g. a Wiren Board controller) is
160
+ fine as a system service.
161
+
162
+ On the server, also install
163
+ [`packaging/scripts/frp-jump-login-link`](packaging/scripts/frp-jump-login-link)
164
+ to `/usr/local/bin/` (`chmod 755`, `root:root`) — a one-line wrapper
165
+ around `server login-link` that loads the systemd unit's env file for
166
+ you, so minting a fresh login/invite link doesn't mean hand-assembling
167
+ `env $(sudo cat /etc/frp-jump/server.env | xargs) sudo -u frp-jump ...`
168
+ every time.
169
+
170
+ ## Development
171
+
172
+ ```sh
173
+ uv sync
174
+ uv run pytest tests/unit -q # fast, no network
175
+ uv run ruff check .
176
+ uv run pytest tests/integration -m integration -q # downloads real frp binaries; loopback only
177
+ ```
178
+
179
+ The integration test proves the whole chain works over loopback (real frp
180
+ binaries, real mTLS, real xtcp-timeout-then-stcp-fallback), but it can't
181
+ prove real NAT hole-punching across two separate networks — that's the one
182
+ thing to manually check on your own machines after this lands.
183
+
184
+ ## Layout
185
+
186
+ ```
187
+ src/frp_jump/
188
+ common/ PKI (private CA), opaque tokens, settings, DB models
189
+ driver/ TunnelDriver/RelayDriver abstraction; driver/frp/ = the frp
190
+ implementation (config rendering, binary download+checksum,
191
+ process supervision)
192
+ server/ control-plane: registry (CRUD), auth (magic links), the
193
+ agent-facing API, the WebUI, bootstrap (`server init`)
194
+ agent/ runs on every device: enroll, the sync loop, ssh_config
195
+ management
196
+ cli/ `frp-jump server ...` / `frp-jump client ...`
197
+ ```
198
+
199
+ ## Contributing
200
+
201
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). Changes are tracked in
202
+ [`CHANGELOG.md`](CHANGELOG.md).
203
+
204
+ ## License
205
+
206
+ [MIT](LICENSE)
@@ -0,0 +1,184 @@
1
+ # frp-jump
2
+
3
+ Connect Linux boxes (including Wiren Board controllers) to each other over
4
+ the internet using the SSH/HTTP/TCP clients you already have — no manual
5
+ port juggling, and no caring whether the link ended up peer-to-peer or
6
+ relayed through your server.
7
+
8
+ - **Tunneling** is [frp](https://github.com/fatedier/frp) (frpc/frps),
9
+ driven through an abstract `TunnelDriver`/`RelayDriver` interface — see
10
+ [`docs/architecture.md`](docs/architecture.md).
11
+ - **P2P with relay fallback** is frp's own `xtcp` + `fallbackTo` feature: a
12
+ device pair first tries a direct (hole-punched) connection, and
13
+ transparently falls back to relaying through your server if that doesn't
14
+ complete within a timeout. Not something this project implements itself.
15
+ - **Security**: a private CA (run by your server) issues an mTLS cert to
16
+ every enrolled device, so nothing unenrolled can reach the relay at all;
17
+ a per-pair secret means even enrolled devices can't reach each other's
18
+ services without an explicit grant; the WebUI is behind passwordless
19
+ magic-link auth (no passwords, no SMTP — links are generated by the
20
+ server and you hand-deliver them yourself); only an admin can add
21
+ devices, wire up services/grants, or revoke them — an invited non-admin
22
+ user can view the dashboard but not change it. Revoking a device or
23
+ grant takes effect on its next sync, not instantly — see
24
+ [`docs/architecture.md`](docs/architecture.md) for what that does and
25
+ doesn't cover.
26
+ - **UX goal**: after setup, `ssh <name>` and `http://127.0.0.1:<port>` just
27
+ work with stock clients, whether the path underneath is p2p or relayed.
28
+
29
+ ## Installing the CLI
30
+
31
+ Not published to PyPI — install on **every** machine that will run either
32
+ `frp-jump server ...` or `frp-jump client ...` (the server box, and each
33
+ device you connect), Python 3.12+ required (already present on any recent
34
+ Debian/Ubuntu, including Wiren Board controllers). Two ways:
35
+
36
+ **From a tagged release** (built by `.github/workflows/release.yml`,
37
+ shows up under this repo's *Releases* tab):
38
+
39
+ ```sh
40
+ python3 -m venv .venv # needs the venv module: on Debian/Ubuntu that's
41
+ # a separate package, `apt install python3-venv`
42
+ gh release download v0.1.0 --repo aadegtyarev/frp-jump --pattern '*.whl' -O frp_jump.whl
43
+ .venv/bin/pip install frp_jump.whl
44
+ ```
45
+
46
+ Needs the [`gh` CLI](https://cli.github.com/) authenticated
47
+ (`gh auth login`) since the repo is private — fine on your own machine,
48
+ probably not worth installing just for a headless device (see below).
49
+
50
+ **Directly from a git tag** (no release/wheel involved, works anywhere
51
+ you can already `git clone` the repo — e.g. via the same **read-only
52
+ deploy key** pattern used for node1 in `docs/architecture.md`; `gh repo
53
+ deploy-key add` to mint one per device):
54
+
55
+ ```sh
56
+ python3 -m venv .venv
57
+ .venv/bin/pip install "git+ssh://git@github.com/aadegtyarev/frp-jump.git@v0.1.0"
58
+ ```
59
+
60
+ Either way you end up with `frp-jump` in that venv — put `.venv/bin` on
61
+ `PATH`, or call `.venv/bin/frp-jump` directly. The rest of this README
62
+ just says `frp-jump`.
63
+
64
+ To cut a new release: `git tag vX.Y.Z && git push origin vX.Y.Z` (bump
65
+ `version` in `pyproject.toml` first) — the workflow builds and publishes
66
+ it automatically.
67
+
68
+ ## Quick start
69
+
70
+ On the **server** (a box with a public IP/domain):
71
+
72
+ ```sh
73
+ export FRP_JUMP_RELAY_PUBLIC_ADDR=tunnel.example.com # or a bare IP
74
+ frp-jump server init --admin-email you@example.com
75
+ # -> prints an admin login link: open it in a browser to sign in
76
+ frp-jump server run
77
+ ```
78
+
79
+ Lost that link, or need to sign in from somewhere else? It's not your
80
+ only way in — mint a fresh one any time:
81
+
82
+ ```sh
83
+ frp-jump server login-link you@example.com # new login link
84
+ frp-jump server login-link friend@example.com --invite # invite someone else
85
+ ```
86
+
87
+ (Under the systemd deployment below, `server login-link` needs the same
88
+ `FRP_JUMP_*` env vars as `server run`, which it won't pick up on its own
89
+ outside of systemd's `EnvironmentFile` — see
90
+ [`packaging/scripts/frp-jump-login-link`](packaging/scripts/frp-jump-login-link)
91
+ for a copy-pasteable wrapper.)
92
+
93
+ In the WebUI: **Add device** for each box you want to connect — this is
94
+ also where the enroll token comes from, there's no other source for it.
95
+ It prints a one-time `frp-jump client enroll <url> <token>` command; run
96
+ that *on the device itself* (not on the server). Then define a
97
+ **service** on the exposing device (e.g. `wb01-ssh`, protocol `ssh`, local
98
+ port `22`) and a **grant** wiring a consuming device to it.
99
+
100
+ On **each device** (after [installing the CLI](#installing-the-cli) there
101
+ too):
102
+
103
+ ```sh
104
+ frp-jump client enroll https://tunnel.example.com <token-from-webui>
105
+ frp-jump client run # foreground; wrap with systemd for real use
106
+ ```
107
+
108
+ Once the consuming device has synced (its agent polls every
109
+ `agent_poll_interval_seconds`, default 30s):
110
+
111
+ ```sh
112
+ ssh wb01-ssh # just works — see `frp-jump client status` for
113
+ # the local address of a non-ssh grant
114
+ ```
115
+
116
+ ## Configuration
117
+
118
+ Everything configurable lives in one place:
119
+ [`src/frp_jump/common/settings.py`](src/frp_jump/common/settings.py) — set
120
+ via `FRP_JUMP_<FIELD>` environment variables, or a TOML file
121
+ (`$FRP_JUMP_CONFIG_FILE`, else the first of `./frp-jump.toml`,
122
+ `~/.config/frp-jump/config.toml`, `/etc/frp-jump/config.toml` that
123
+ exists). Env vars win over the file. Nothing else in the codebase
124
+ hardcodes a port, TTL, or version pin.
125
+
126
+ The only setting with no sane default is `relay_public_addr` — the
127
+ address other devices dial to reach your relay; `server init`/`server run`
128
+ refuse to start without it.
129
+
130
+ ## systemd
131
+
132
+ See [`packaging/systemd/`](packaging/systemd/). The client unit's comments
133
+ explain a real gotcha: a device that *consumes* an SSH grant needs the
134
+ agent running as the actual human (so it can maintain their real
135
+ `~/.ssh/config`) — a `--user` unit, not a system one, unless you point
136
+ `FRP_JUMP_SSH_CONFIG_PATH` at that user's config explicitly. A
137
+ device that only *exposes* services (e.g. a Wiren Board controller) is
138
+ fine as a system service.
139
+
140
+ On the server, also install
141
+ [`packaging/scripts/frp-jump-login-link`](packaging/scripts/frp-jump-login-link)
142
+ to `/usr/local/bin/` (`chmod 755`, `root:root`) — a one-line wrapper
143
+ around `server login-link` that loads the systemd unit's env file for
144
+ you, so minting a fresh login/invite link doesn't mean hand-assembling
145
+ `env $(sudo cat /etc/frp-jump/server.env | xargs) sudo -u frp-jump ...`
146
+ every time.
147
+
148
+ ## Development
149
+
150
+ ```sh
151
+ uv sync
152
+ uv run pytest tests/unit -q # fast, no network
153
+ uv run ruff check .
154
+ uv run pytest tests/integration -m integration -q # downloads real frp binaries; loopback only
155
+ ```
156
+
157
+ The integration test proves the whole chain works over loopback (real frp
158
+ binaries, real mTLS, real xtcp-timeout-then-stcp-fallback), but it can't
159
+ prove real NAT hole-punching across two separate networks — that's the one
160
+ thing to manually check on your own machines after this lands.
161
+
162
+ ## Layout
163
+
164
+ ```
165
+ src/frp_jump/
166
+ common/ PKI (private CA), opaque tokens, settings, DB models
167
+ driver/ TunnelDriver/RelayDriver abstraction; driver/frp/ = the frp
168
+ implementation (config rendering, binary download+checksum,
169
+ process supervision)
170
+ server/ control-plane: registry (CRUD), auth (magic links), the
171
+ agent-facing API, the WebUI, bootstrap (`server init`)
172
+ agent/ runs on every device: enroll, the sync loop, ssh_config
173
+ management
174
+ cli/ `frp-jump server ...` / `frp-jump client ...`
175
+ ```
176
+
177
+ ## Contributing
178
+
179
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). Changes are tracked in
180
+ [`CHANGELOG.md`](CHANGELOG.md).
181
+
182
+ ## License
183
+
184
+ [MIT](LICENSE)
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "frp-jump"
3
+ version = "0.1.1"
4
+ description = "P2P-with-relay-fallback ssh/http/tcp tunnels between Linux boxes, built on frp"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ dependencies = [
10
+ "typer>=0.15",
11
+ "rich>=13.9",
12
+ "fastapi>=0.115",
13
+ "uvicorn[standard]>=0.32",
14
+ "jinja2>=3.1",
15
+ "python-multipart>=0.0.17",
16
+ "sqlmodel>=0.0.22",
17
+ "cryptography>=43.0",
18
+ "tomli-w>=1.1",
19
+ "httpx>=0.27",
20
+ "pydantic-settings>=2.15.0",
21
+ ]
22
+
23
+ [[project.authors]]
24
+ name = "Alexander Degtyarev"
25
+ email = "a.degtyarev@struhe.com"
26
+
27
+ [project.scripts]
28
+ frp-jump = "frp_jump.cli.main:app"
29
+
30
+ [build-system]
31
+ requires = ["uv_build>=0.11.14,<0.12.0"]
32
+ build-backend = "uv_build"
33
+
34
+ [dependency-groups]
35
+ dev = [
36
+ "pytest>=8.3",
37
+ "pytest-asyncio>=0.24",
38
+ "ruff>=0.7",
39
+ ]
40
+
41
+ [tool.ruff]
42
+ line-length = 100
43
+ target-version = "py312"
44
+
45
+ [tool.ruff.lint]
46
+ select = [
47
+ "E",
48
+ "F",
49
+ "I",
50
+ "UP",
51
+ "B",
52
+ ]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ markers = ["integration: downloads/spins up real frp binaries and hits the network; excluded by default"]
57
+ addopts = [
58
+ "-m",
59
+ "not integration",
60
+ ]
61
+ asyncio_mode = "auto"
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "frp-jump"
3
+ version = "0.1.1"
4
+ description = "P2P-with-relay-fallback ssh/http/tcp tunnels between Linux boxes, built on frp"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Alexander Degtyarev", email = "a.degtyarev@struhe.com" }
10
+ ]
11
+ requires-python = ">=3.12"
12
+ dependencies = [
13
+ "typer>=0.15",
14
+ "rich>=13.9",
15
+ "fastapi>=0.115",
16
+ "uvicorn[standard]>=0.32",
17
+ "jinja2>=3.1",
18
+ "python-multipart>=0.0.17",
19
+ "sqlmodel>=0.0.22",
20
+ "cryptography>=43.0",
21
+ "tomli-w>=1.1",
22
+ "httpx>=0.27",
23
+ "pydantic-settings>=2.15.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ frp-jump = "frp_jump.cli.main:app"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.11.14,<0.12.0"]
31
+ build-backend = "uv_build"
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "pytest>=8.3",
36
+ "pytest-asyncio>=0.24",
37
+ "ruff>=0.7",
38
+ ]
39
+
40
+ [tool.ruff]
41
+ line-length = 100
42
+ target-version = "py312"
43
+
44
+ [tool.ruff.lint]
45
+ select = ["E", "F", "I", "UP", "B"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]
49
+ markers = [
50
+ "integration: downloads/spins up real frp binaries and hits the network; excluded by default",
51
+ ]
52
+ addopts = ["-m", "not integration"]
53
+ asyncio_mode = "auto"
File without changes
File without changes
@@ -0,0 +1,40 @@
1
+ """One-time enrollment: trade an admin-issued enroll token for this
2
+ device's identity (cert, api token, relay connection info), and persist it.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ import httpx
10
+
11
+ from frp_jump.agent.state import AgentState, save
12
+
13
+
14
+ class EnrollError(RuntimeError):
15
+ pass
16
+
17
+
18
+ def enroll(*, control_url: str, token: str, data_dir: Path) -> AgentState:
19
+ control_url = control_url.rstrip("/")
20
+ try:
21
+ resp = httpx.post(f"{control_url}/api/agent/enroll", json={"token": token}, timeout=30.0)
22
+ except httpx.HTTPError as exc:
23
+ raise EnrollError(f"could not reach {control_url}: {exc}") from exc
24
+ if resp.status_code != 200:
25
+ raise EnrollError(f"enroll failed ({resp.status_code}): {resp.text}")
26
+
27
+ body = resp.json()
28
+ state = AgentState(
29
+ device_id=body["device_id"],
30
+ device_name=body["device_name"],
31
+ control_url=control_url,
32
+ relay_addr=body["server_addr"],
33
+ relay_port=body["server_port"],
34
+ api_token=body["api_token"],
35
+ cert_pem=body["cert_pem"],
36
+ key_pem=body["key_pem"],
37
+ ca_cert_pem=body["ca_cert_pem"],
38
+ )
39
+ save(data_dir, state)
40
+ return state
@@ -0,0 +1,47 @@
1
+ """Make consumed SSH grants reachable via `ssh <service-name>`.
2
+
3
+ We never edit ``~/.ssh/config`` beyond ensuring a single ``Include`` line
4
+ at its top; every ``Host`` block frp-jump manages lives in its own file,
5
+ fully regenerated on every apply so it always matches the current grants.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ _MANAGED_HEADER = "# managed by frp-jump -- do not edit, changes are overwritten on the next sync\n"
13
+
14
+
15
+ def managed_config_path(data_dir: Path) -> Path:
16
+ return data_dir / "ssh_config"
17
+
18
+
19
+ def render_ssh_config(entries: list[tuple[str, int]]) -> str:
20
+ """``entries``: (service_name, local_port) for each consumed SSH grant."""
21
+ lines = [_MANAGED_HEADER]
22
+ for name, port in entries:
23
+ lines.append(f"\nHost {name}\n HostName 127.0.0.1\n Port {port}\n")
24
+ return "".join(lines)
25
+
26
+
27
+ def write_ssh_config(data_dir: Path, entries: list[tuple[str, int]]) -> Path:
28
+ path = managed_config_path(data_dir)
29
+ path.parent.mkdir(parents=True, exist_ok=True)
30
+ path.write_text(render_ssh_config(entries))
31
+ return path
32
+
33
+
34
+ def ensure_include(ssh_config_path: Path, managed_path: Path) -> bool:
35
+ """Idempotently prepend ``Include <managed_path>`` to ``ssh_config_path``.
36
+
37
+ Prepended (not appended) so frp-jump's ``Host`` entries are matched
38
+ before any conflicting ones the user already has -- ssh_config uses
39
+ first-match-wins per keyword. Returns True if it had to add the line.
40
+ """
41
+ include_line = f"Include {managed_path}\n"
42
+ ssh_config_path.parent.mkdir(parents=True, exist_ok=True)
43
+ existing = ssh_config_path.read_text() if ssh_config_path.exists() else ""
44
+ if str(managed_path) in existing:
45
+ return False
46
+ ssh_config_path.write_text(include_line + existing)
47
+ return True