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.
- frp_jump-0.1.1/LICENSE +21 -0
- frp_jump-0.1.1/PKG-INFO +206 -0
- frp_jump-0.1.1/README.md +184 -0
- frp_jump-0.1.1/pyproject.toml +61 -0
- frp_jump-0.1.1/pyproject.toml.orig +53 -0
- frp_jump-0.1.1/src/frp_jump/__init__.py +0 -0
- frp_jump-0.1.1/src/frp_jump/agent/__init__.py +0 -0
- frp_jump-0.1.1/src/frp_jump/agent/enroll.py +40 -0
- frp_jump-0.1.1/src/frp_jump/agent/hosts.py +47 -0
- frp_jump-0.1.1/src/frp_jump/agent/poller.py +218 -0
- frp_jump-0.1.1/src/frp_jump/agent/state.py +79 -0
- frp_jump-0.1.1/src/frp_jump/cli/__init__.py +0 -0
- frp_jump-0.1.1/src/frp_jump/cli/client_cmds.py +155 -0
- frp_jump-0.1.1/src/frp_jump/cli/main.py +17 -0
- frp_jump-0.1.1/src/frp_jump/cli/server_cmds.py +125 -0
- frp_jump-0.1.1/src/frp_jump/common/__init__.py +0 -0
- frp_jump-0.1.1/src/frp_jump/common/crypto.py +35 -0
- frp_jump-0.1.1/src/frp_jump/common/models.py +138 -0
- frp_jump-0.1.1/src/frp_jump/common/pki.py +221 -0
- frp_jump-0.1.1/src/frp_jump/common/settings.py +100 -0
- frp_jump-0.1.1/src/frp_jump/driver/__init__.py +0 -0
- frp_jump-0.1.1/src/frp_jump/driver/base.py +116 -0
- frp_jump-0.1.1/src/frp_jump/driver/frp/__init__.py +0 -0
- frp_jump-0.1.1/src/frp_jump/driver/frp/binaries.py +154 -0
- frp_jump-0.1.1/src/frp_jump/driver/frp/config.py +133 -0
- frp_jump-0.1.1/src/frp_jump/driver/frp/driver.py +136 -0
- frp_jump-0.1.1/src/frp_jump/driver/frp/process.py +55 -0
- frp_jump-0.1.1/src/frp_jump/server/__init__.py +0 -0
- frp_jump-0.1.1/src/frp_jump/server/api.py +142 -0
- frp_jump-0.1.1/src/frp_jump/server/app.py +37 -0
- frp_jump-0.1.1/src/frp_jump/server/auth.py +100 -0
- frp_jump-0.1.1/src/frp_jump/server/bootstrap.py +108 -0
- frp_jump-0.1.1/src/frp_jump/server/db.py +46 -0
- frp_jump-0.1.1/src/frp_jump/server/registry.py +413 -0
- frp_jump-0.1.1/src/frp_jump/server/templates/base.html +82 -0
- frp_jump-0.1.1/src/frp_jump/server/templates/dashboard.html +176 -0
- frp_jump-0.1.1/src/frp_jump/server/templates/login.html +11 -0
- frp_jump-0.1.1/src/frp_jump/server/templates/result.html +14 -0
- 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.
|
frp_jump-0.1.1/PKG-INFO
ADDED
|
@@ -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)
|
frp_jump-0.1.1/README.md
ADDED
|
@@ -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
|