nullgate 1.1.1__tar.gz → 1.2.0__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.
- {nullgate-1.1.1/src/nullgate.egg-info → nullgate-1.2.0}/PKG-INFO +41 -37
- {nullgate-1.1.1 → nullgate-1.2.0}/README.md +40 -36
- nullgate-1.2.0/src/nullgate/__init__.py +1 -0
- nullgate-1.2.0/src/nullgate/commands.py +246 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/session.py +13 -10
- {nullgate-1.1.1 → nullgate-1.2.0/src/nullgate.egg-info}/PKG-INFO +41 -37
- {nullgate-1.1.1 → nullgate-1.2.0}/tests/test_commands.py +44 -20
- nullgate-1.1.1/src/nullgate/__init__.py +0 -1
- nullgate-1.1.1/src/nullgate/commands.py +0 -179
- {nullgate-1.1.1 → nullgate-1.2.0}/LICENSE +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/pyproject.toml +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/setup.cfg +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/account.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/bridge.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/client_config.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/gateway.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/ingress.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/runtime.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/transports.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate/wsroute.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate.egg-info/SOURCES.txt +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate.egg-info/dependency_links.txt +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate.egg-info/entry_points.txt +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate.egg-info/requires.txt +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/src/nullgate.egg-info/top_level.txt +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/tests/test_account.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/tests/test_bridge.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/tests/test_gateway.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/tests/test_ingress.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/tests/test_install.py +0 -0
- {nullgate-1.1.1 → nullgate-1.2.0}/tests/test_wsroute.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nullgate
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Disposable SSH gateway into confined directory workspaces
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -18,7 +18,7 @@ Disposable SSH gateway into confined directory workspaces for remote containers,
|
|
|
18
18
|
|
|
19
19
|
- **Ingress Transports**: Publish endpoints via Upterm WebSocket relays (default), srv.us reverse tunnels, Cloudflare Worker relays, or Cloudflare Argo (`cloudflared`) tunnels.
|
|
20
20
|
- **Confinement**: SFTP and SCP workloads are restricted to the designated workspace root; parent traversals and out-of-root symlinks are blocked.
|
|
21
|
-
- **Host file visibility**: Relative transfer paths begin in the workspace root while absolute paths address host locations, matching shell behavior; add `--
|
|
21
|
+
- **Host file visibility**: Relative transfer paths begin in the workspace root while absolute paths address host locations, matching shell behavior; add `--restrict-file-transfer` for strict workspace-only transfers.
|
|
22
22
|
- **Direct Shell Access**: Authenticated PTY sessions run interactively under the local user credentials.
|
|
23
23
|
- **Synthetic UID Resilience**: Runs seamlessly in container environments missing user records in `/etc/passwd`.
|
|
24
24
|
- **Persistent Key Identities**: Host keys and identity seeds persist across restarts rather than being silently regenerated.
|
|
@@ -44,77 +44,77 @@ pip install nullgate
|
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
|
-
###
|
|
47
|
+
### Start
|
|
48
48
|
|
|
49
49
|
Launch the gateway service and ingress transport in the background:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
# Default Upterm transport on port 4822 sharing current directory
|
|
53
|
-
nullgate
|
|
53
|
+
nullgate start
|
|
54
54
|
|
|
55
55
|
# Custom path, port, and slot
|
|
56
|
-
nullgate
|
|
56
|
+
nullgate start /workspace --ssh-port 4822 --tunnel-slot 1
|
|
57
57
|
|
|
58
58
|
# Select specific transport provider
|
|
59
|
-
nullgate
|
|
60
|
-
nullgate
|
|
61
|
-
nullgate
|
|
62
|
-
nullgate
|
|
59
|
+
nullgate start . --provider upterm
|
|
60
|
+
nullgate start . --provider srvus
|
|
61
|
+
nullgate start . --provider cloudflare --relay-url https://relay.example.com
|
|
62
|
+
nullgate start . --provider cloudflared --public-hostname workspace.example.com --cloudflare-token "$TUNNEL_TOKEN"
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Options:
|
|
66
|
-
- `--
|
|
67
|
-
- `--
|
|
68
|
-
- `--hostname`: Public
|
|
69
|
-
- `--token`:
|
|
70
|
-
- `--
|
|
71
|
-
- `--no-
|
|
72
|
-
- `--
|
|
73
|
-
- `--no-
|
|
74
|
-
- `--
|
|
75
|
-
- `--no-
|
|
76
|
-
|
|
77
|
-
###
|
|
66
|
+
- `--provider`: Public connection provider (`upterm`, `srvus`, `cloudflare`, or `cloudflared`). Defaults to `upterm`.
|
|
67
|
+
- `--relay-url`: WebSocket relay URL used with `upterm` or `cloudflare`.
|
|
68
|
+
- `--public-hostname`: Public hostname used with `cloudflared`.
|
|
69
|
+
- `--cloudflare-token`: Cloudflare tunnel credential, or set `NULLGATE_CLOUDFLARED_TOKEN`.
|
|
70
|
+
- `--public-access`: Allow connections without credentials. This is unsafe on public endpoints.
|
|
71
|
+
- `--no-public-access`: Require authentication.
|
|
72
|
+
- `--tcp-forwarding`: Enable SSH TCP tunneling (`ssh -L`).
|
|
73
|
+
- `--no-tcp-forwarding`: Disable SSH TCP tunneling.
|
|
74
|
+
- `--restrict-file-transfer`: Keep SFTP and SCP inside the workspace.
|
|
75
|
+
- `--no-restrict-file-transfer`: Allow host-visible file transfers.
|
|
76
|
+
|
|
77
|
+
### Status
|
|
78
78
|
|
|
79
79
|
Display operational status, running PIDs, connection URLs, and security settings:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
nullgate
|
|
82
|
+
nullgate status
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
###
|
|
85
|
+
### Connect
|
|
86
86
|
|
|
87
87
|
Print client connection strings and OpenSSH client configuration directives:
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
nullgate
|
|
90
|
+
nullgate connect
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
###
|
|
93
|
+
### Logs
|
|
94
94
|
|
|
95
95
|
Stream or print daemon logs:
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
|
-
nullgate
|
|
99
|
-
nullgate
|
|
100
|
-
nullgate
|
|
101
|
-
nullgate
|
|
98
|
+
nullgate logs
|
|
99
|
+
nullgate logs -f
|
|
100
|
+
nullgate logs gateway
|
|
101
|
+
nullgate logs transport
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
###
|
|
104
|
+
### Stop
|
|
105
105
|
|
|
106
106
|
Terminate running gateway and transport background processes:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
nullgate
|
|
109
|
+
nullgate stop
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
###
|
|
112
|
+
### Restart
|
|
113
113
|
|
|
114
114
|
Restart daemon instances reusing active or saved workspace settings:
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
|
-
nullgate
|
|
117
|
+
nullgate restart
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### Upgrade
|
|
@@ -126,13 +126,17 @@ nullgate upgrade
|
|
|
126
126
|
nullgate upgrade --version 1.0.1
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
The earlier command names `open`, `shut`, `inspect`, `enter`, `trace`, and
|
|
130
|
+
`cycle` remain available as compatibility aliases. Their earlier option names
|
|
131
|
+
also remain accepted, but new scripts should use the names documented above.
|
|
132
|
+
|
|
129
133
|
## Authentication and Security Boundaries
|
|
130
134
|
|
|
131
135
|
- **Authentication Baseline**: Access requires authorized SSH keys in `~/.ssh/authorized_keys` or a high-entropy secret created in the runtime directory.
|
|
132
|
-
- **
|
|
133
|
-
- **Upterm Transport**: Relies on authorized SSH public keys or `--
|
|
136
|
+
- **Public access (`--public-access`)**: Bypasses authentication checks entirely; any incoming connection with network access is accepted.
|
|
137
|
+
- **Upterm Transport**: Relies on authorized SSH public keys or `--public-access` (Upterm does not support password authentication). The relay itself is trusted for inbound multiplexing.
|
|
134
138
|
- **Filesystem Confinement**: SFTP and SCP access cannot escape the designated workspace directory.
|
|
135
|
-
- **Host Transfer Mode (default)**: Relative SFTP and SCP paths begin in the workspace directory, while absolute paths refer to host filesystem locations subject to OS user permissions. Pass `--
|
|
139
|
+
- **Host Transfer Mode (default)**: Relative SFTP and SCP paths begin in the workspace directory, while absolute paths refer to host filesystem locations subject to OS user permissions. Pass `--restrict-file-transfer` to limit transfers to the workspace namespace and reject symlink escapes.
|
|
136
140
|
- **Interactive Shell Privileges**: Command and terminal sessions execute with the permissions of the host account rather than inside an OS chroot.
|
|
137
141
|
- **State Storage**: Daemon PIDs and logs are maintained in `gateway.pid`, `transport.pid`, `gateway.log`, and `transport.log`, with session records kept in `manifest.json` and `settings.json`.
|
|
138
142
|
|
|
@@ -140,7 +144,7 @@ nullgate upgrade --version 1.0.1
|
|
|
140
144
|
|
|
141
145
|
| Variable | Description |
|
|
142
146
|
|---|---|
|
|
143
|
-
| `NULLGATE_PASSWORD` | Gateway-process credential; normally managed internally by `nullgate
|
|
147
|
+
| `NULLGATE_PASSWORD` | Gateway-process credential; normally managed internally by `nullgate start` |
|
|
144
148
|
| `NULLGATE_STATE_DIR` | Directory holding runtime state and logs (defaults to `~/.local/state/nullgate`) |
|
|
145
149
|
| `NULLGATE_IDENTITY_SEED` | Seed value used for deterministic Ed25519 key derivation |
|
|
146
150
|
| `NULLGATE_TUNNEL_HOST` | Host address for srv.us ingress (default: `srv.us`) |
|
|
@@ -6,7 +6,7 @@ Disposable SSH gateway into confined directory workspaces for remote containers,
|
|
|
6
6
|
|
|
7
7
|
- **Ingress Transports**: Publish endpoints via Upterm WebSocket relays (default), srv.us reverse tunnels, Cloudflare Worker relays, or Cloudflare Argo (`cloudflared`) tunnels.
|
|
8
8
|
- **Confinement**: SFTP and SCP workloads are restricted to the designated workspace root; parent traversals and out-of-root symlinks are blocked.
|
|
9
|
-
- **Host file visibility**: Relative transfer paths begin in the workspace root while absolute paths address host locations, matching shell behavior; add `--
|
|
9
|
+
- **Host file visibility**: Relative transfer paths begin in the workspace root while absolute paths address host locations, matching shell behavior; add `--restrict-file-transfer` for strict workspace-only transfers.
|
|
10
10
|
- **Direct Shell Access**: Authenticated PTY sessions run interactively under the local user credentials.
|
|
11
11
|
- **Synthetic UID Resilience**: Runs seamlessly in container environments missing user records in `/etc/passwd`.
|
|
12
12
|
- **Persistent Key Identities**: Host keys and identity seeds persist across restarts rather than being silently regenerated.
|
|
@@ -32,77 +32,77 @@ pip install nullgate
|
|
|
32
32
|
|
|
33
33
|
## Usage
|
|
34
34
|
|
|
35
|
-
###
|
|
35
|
+
### Start
|
|
36
36
|
|
|
37
37
|
Launch the gateway service and ingress transport in the background:
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
# Default Upterm transport on port 4822 sharing current directory
|
|
41
|
-
nullgate
|
|
41
|
+
nullgate start
|
|
42
42
|
|
|
43
43
|
# Custom path, port, and slot
|
|
44
|
-
nullgate
|
|
44
|
+
nullgate start /workspace --ssh-port 4822 --tunnel-slot 1
|
|
45
45
|
|
|
46
46
|
# Select specific transport provider
|
|
47
|
-
nullgate
|
|
48
|
-
nullgate
|
|
49
|
-
nullgate
|
|
50
|
-
nullgate
|
|
47
|
+
nullgate start . --provider upterm
|
|
48
|
+
nullgate start . --provider srvus
|
|
49
|
+
nullgate start . --provider cloudflare --relay-url https://relay.example.com
|
|
50
|
+
nullgate start . --provider cloudflared --public-hostname workspace.example.com --cloudflare-token "$TUNNEL_TOKEN"
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Options:
|
|
54
|
-
- `--
|
|
55
|
-
- `--
|
|
56
|
-
- `--hostname`: Public
|
|
57
|
-
- `--token`:
|
|
58
|
-
- `--
|
|
59
|
-
- `--no-
|
|
60
|
-
- `--
|
|
61
|
-
- `--no-
|
|
62
|
-
- `--
|
|
63
|
-
- `--no-
|
|
64
|
-
|
|
65
|
-
###
|
|
54
|
+
- `--provider`: Public connection provider (`upterm`, `srvus`, `cloudflare`, or `cloudflared`). Defaults to `upterm`.
|
|
55
|
+
- `--relay-url`: WebSocket relay URL used with `upterm` or `cloudflare`.
|
|
56
|
+
- `--public-hostname`: Public hostname used with `cloudflared`.
|
|
57
|
+
- `--cloudflare-token`: Cloudflare tunnel credential, or set `NULLGATE_CLOUDFLARED_TOKEN`.
|
|
58
|
+
- `--public-access`: Allow connections without credentials. This is unsafe on public endpoints.
|
|
59
|
+
- `--no-public-access`: Require authentication.
|
|
60
|
+
- `--tcp-forwarding`: Enable SSH TCP tunneling (`ssh -L`).
|
|
61
|
+
- `--no-tcp-forwarding`: Disable SSH TCP tunneling.
|
|
62
|
+
- `--restrict-file-transfer`: Keep SFTP and SCP inside the workspace.
|
|
63
|
+
- `--no-restrict-file-transfer`: Allow host-visible file transfers.
|
|
64
|
+
|
|
65
|
+
### Status
|
|
66
66
|
|
|
67
67
|
Display operational status, running PIDs, connection URLs, and security settings:
|
|
68
68
|
|
|
69
69
|
```bash
|
|
70
|
-
nullgate
|
|
70
|
+
nullgate status
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
###
|
|
73
|
+
### Connect
|
|
74
74
|
|
|
75
75
|
Print client connection strings and OpenSSH client configuration directives:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
nullgate
|
|
78
|
+
nullgate connect
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
###
|
|
81
|
+
### Logs
|
|
82
82
|
|
|
83
83
|
Stream or print daemon logs:
|
|
84
84
|
|
|
85
85
|
```bash
|
|
86
|
-
nullgate
|
|
87
|
-
nullgate
|
|
88
|
-
nullgate
|
|
89
|
-
nullgate
|
|
86
|
+
nullgate logs
|
|
87
|
+
nullgate logs -f
|
|
88
|
+
nullgate logs gateway
|
|
89
|
+
nullgate logs transport
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
###
|
|
92
|
+
### Stop
|
|
93
93
|
|
|
94
94
|
Terminate running gateway and transport background processes:
|
|
95
95
|
|
|
96
96
|
```bash
|
|
97
|
-
nullgate
|
|
97
|
+
nullgate stop
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
###
|
|
100
|
+
### Restart
|
|
101
101
|
|
|
102
102
|
Restart daemon instances reusing active or saved workspace settings:
|
|
103
103
|
|
|
104
104
|
```bash
|
|
105
|
-
nullgate
|
|
105
|
+
nullgate restart
|
|
106
106
|
```
|
|
107
107
|
|
|
108
108
|
### Upgrade
|
|
@@ -114,13 +114,17 @@ nullgate upgrade
|
|
|
114
114
|
nullgate upgrade --version 1.0.1
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
The earlier command names `open`, `shut`, `inspect`, `enter`, `trace`, and
|
|
118
|
+
`cycle` remain available as compatibility aliases. Their earlier option names
|
|
119
|
+
also remain accepted, but new scripts should use the names documented above.
|
|
120
|
+
|
|
117
121
|
## Authentication and Security Boundaries
|
|
118
122
|
|
|
119
123
|
- **Authentication Baseline**: Access requires authorized SSH keys in `~/.ssh/authorized_keys` or a high-entropy secret created in the runtime directory.
|
|
120
|
-
- **
|
|
121
|
-
- **Upterm Transport**: Relies on authorized SSH public keys or `--
|
|
124
|
+
- **Public access (`--public-access`)**: Bypasses authentication checks entirely; any incoming connection with network access is accepted.
|
|
125
|
+
- **Upterm Transport**: Relies on authorized SSH public keys or `--public-access` (Upterm does not support password authentication). The relay itself is trusted for inbound multiplexing.
|
|
122
126
|
- **Filesystem Confinement**: SFTP and SCP access cannot escape the designated workspace directory.
|
|
123
|
-
- **Host Transfer Mode (default)**: Relative SFTP and SCP paths begin in the workspace directory, while absolute paths refer to host filesystem locations subject to OS user permissions. Pass `--
|
|
127
|
+
- **Host Transfer Mode (default)**: Relative SFTP and SCP paths begin in the workspace directory, while absolute paths refer to host filesystem locations subject to OS user permissions. Pass `--restrict-file-transfer` to limit transfers to the workspace namespace and reject symlink escapes.
|
|
124
128
|
- **Interactive Shell Privileges**: Command and terminal sessions execute with the permissions of the host account rather than inside an OS chroot.
|
|
125
129
|
- **State Storage**: Daemon PIDs and logs are maintained in `gateway.pid`, `transport.pid`, `gateway.log`, and `transport.log`, with session records kept in `manifest.json` and `settings.json`.
|
|
126
130
|
|
|
@@ -128,7 +132,7 @@ nullgate upgrade --version 1.0.1
|
|
|
128
132
|
|
|
129
133
|
| Variable | Description |
|
|
130
134
|
|---|---|
|
|
131
|
-
| `NULLGATE_PASSWORD` | Gateway-process credential; normally managed internally by `nullgate
|
|
135
|
+
| `NULLGATE_PASSWORD` | Gateway-process credential; normally managed internally by `nullgate start` |
|
|
132
136
|
| `NULLGATE_STATE_DIR` | Directory holding runtime state and logs (defaults to `~/.local/state/nullgate`) |
|
|
133
137
|
| `NULLGATE_IDENTITY_SEED` | Seed value used for deterministic Ed25519 key derivation |
|
|
134
138
|
| `NULLGATE_TUNNEL_HOST` | Host address for srv.us ingress (default: `srv.us`) |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.0"
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Command-line parser and entry point dispatch for Nullgate."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from typing import Sequence
|
|
8
|
+
|
|
9
|
+
from nullgate import session, transports
|
|
10
|
+
|
|
11
|
+
SUBCOMMANDS = (
|
|
12
|
+
"start",
|
|
13
|
+
"stop",
|
|
14
|
+
"status",
|
|
15
|
+
"connect",
|
|
16
|
+
"logs",
|
|
17
|
+
"restart",
|
|
18
|
+
"upgrade",
|
|
19
|
+
"open",
|
|
20
|
+
"shut",
|
|
21
|
+
"inspect",
|
|
22
|
+
"enter",
|
|
23
|
+
"trace",
|
|
24
|
+
"cycle",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="nullgate",
|
|
31
|
+
description="Disposable SSH gateway into confined directory workspaces.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"-V",
|
|
35
|
+
"--version",
|
|
36
|
+
action="version",
|
|
37
|
+
version=f"nullgate {session._version()}",
|
|
38
|
+
)
|
|
39
|
+
sub = parser.add_subparsers(dest="command", required=True, metavar="command")
|
|
40
|
+
|
|
41
|
+
start_parser = sub.add_parser(
|
|
42
|
+
"start",
|
|
43
|
+
aliases=["open"],
|
|
44
|
+
description="Start SSH access to a workspace.",
|
|
45
|
+
help="Start SSH access to a workspace (alias: open).",
|
|
46
|
+
)
|
|
47
|
+
start_parser.add_argument(
|
|
48
|
+
"workspace",
|
|
49
|
+
nargs="?",
|
|
50
|
+
help="Directory to make available (default: current directory).",
|
|
51
|
+
)
|
|
52
|
+
start_parser.add_argument(
|
|
53
|
+
"--ssh-port",
|
|
54
|
+
dest="port",
|
|
55
|
+
type=int,
|
|
56
|
+
default=None,
|
|
57
|
+
help=f"Local SSH server port (default: {session.DEFAULT_PORT}).",
|
|
58
|
+
)
|
|
59
|
+
start_parser.add_argument("--port", dest="port", type=int, help=argparse.SUPPRESS)
|
|
60
|
+
start_parser.add_argument(
|
|
61
|
+
"--tunnel-slot",
|
|
62
|
+
dest="slot",
|
|
63
|
+
type=int,
|
|
64
|
+
default=None,
|
|
65
|
+
help=f"srv.us tunnel slot (default: {session.DEFAULT_SLOT}).",
|
|
66
|
+
)
|
|
67
|
+
start_parser.add_argument("--slot", dest="slot", type=int, help=argparse.SUPPRESS)
|
|
68
|
+
start_parser.add_argument(
|
|
69
|
+
"--provider",
|
|
70
|
+
dest="transport",
|
|
71
|
+
choices=transports.VALID_TRANSPORTS,
|
|
72
|
+
default=None,
|
|
73
|
+
help=f"Public connection provider (default: {transports.DEFAULT_TRANSPORT}).",
|
|
74
|
+
)
|
|
75
|
+
start_parser.add_argument(
|
|
76
|
+
"--transport",
|
|
77
|
+
dest="transport",
|
|
78
|
+
choices=transports.VALID_TRANSPORTS,
|
|
79
|
+
help=argparse.SUPPRESS,
|
|
80
|
+
)
|
|
81
|
+
start_parser.add_argument(
|
|
82
|
+
"--public-access",
|
|
83
|
+
dest="accept",
|
|
84
|
+
action=argparse.BooleanOptionalAction,
|
|
85
|
+
default=None,
|
|
86
|
+
help="Allow connections without SSH credentials (unsafe on public endpoints).",
|
|
87
|
+
)
|
|
88
|
+
start_parser.add_argument(
|
|
89
|
+
"--accept",
|
|
90
|
+
dest="accept",
|
|
91
|
+
action=argparse.BooleanOptionalAction,
|
|
92
|
+
default=None,
|
|
93
|
+
help=argparse.SUPPRESS,
|
|
94
|
+
)
|
|
95
|
+
start_parser.add_argument(
|
|
96
|
+
"--tcp-forwarding",
|
|
97
|
+
dest="allow_tcp_forwarding",
|
|
98
|
+
action=argparse.BooleanOptionalAction,
|
|
99
|
+
default=None,
|
|
100
|
+
help="Allow SSH port forwarding.",
|
|
101
|
+
)
|
|
102
|
+
start_parser.add_argument(
|
|
103
|
+
"--allow-tcp-forwarding",
|
|
104
|
+
dest="allow_tcp_forwarding",
|
|
105
|
+
action=argparse.BooleanOptionalAction,
|
|
106
|
+
default=None,
|
|
107
|
+
help=argparse.SUPPRESS,
|
|
108
|
+
)
|
|
109
|
+
start_parser.add_argument(
|
|
110
|
+
"--restrict-file-transfer",
|
|
111
|
+
dest="confine_sftp",
|
|
112
|
+
action=argparse.BooleanOptionalAction,
|
|
113
|
+
default=None,
|
|
114
|
+
help="Keep SFTP and SCP transfers inside the workspace.",
|
|
115
|
+
)
|
|
116
|
+
start_parser.add_argument(
|
|
117
|
+
"--confine-sftp",
|
|
118
|
+
dest="confine_sftp",
|
|
119
|
+
action=argparse.BooleanOptionalAction,
|
|
120
|
+
default=None,
|
|
121
|
+
help=argparse.SUPPRESS,
|
|
122
|
+
)
|
|
123
|
+
start_parser.add_argument(
|
|
124
|
+
"--relay-url",
|
|
125
|
+
dest="endpoint",
|
|
126
|
+
help="WebSocket relay URL (for upterm or cloudflare providers).",
|
|
127
|
+
)
|
|
128
|
+
start_parser.add_argument("--endpoint", dest="endpoint", help=argparse.SUPPRESS)
|
|
129
|
+
start_parser.add_argument(
|
|
130
|
+
"--public-hostname",
|
|
131
|
+
dest="hostname",
|
|
132
|
+
help="Public hostname (for the cloudflared provider).",
|
|
133
|
+
)
|
|
134
|
+
start_parser.add_argument("--hostname", dest="hostname", help=argparse.SUPPRESS)
|
|
135
|
+
start_parser.add_argument(
|
|
136
|
+
"--cloudflare-token",
|
|
137
|
+
dest="token",
|
|
138
|
+
help="Cloudflare tunnel token (for the cloudflared provider).",
|
|
139
|
+
)
|
|
140
|
+
start_parser.add_argument("--token", dest="token", help=argparse.SUPPRESS)
|
|
141
|
+
|
|
142
|
+
sub.add_parser(
|
|
143
|
+
"stop",
|
|
144
|
+
aliases=["shut"],
|
|
145
|
+
description="Stop SSH access and its connection provider.",
|
|
146
|
+
help="Stop SSH access (alias: shut).",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
sub.add_parser(
|
|
150
|
+
"status",
|
|
151
|
+
aliases=["inspect"],
|
|
152
|
+
description="Show service health, connection details, and security settings.",
|
|
153
|
+
help="Show current status (alias: inspect).",
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
sub.add_parser(
|
|
157
|
+
"connect",
|
|
158
|
+
aliases=["enter"],
|
|
159
|
+
description="Show how to connect with SSH.",
|
|
160
|
+
help="Show SSH connection instructions (alias: enter).",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
logs_parser = sub.add_parser(
|
|
164
|
+
"logs",
|
|
165
|
+
aliases=["trace"],
|
|
166
|
+
description="View or follow service logs.",
|
|
167
|
+
help="View or follow service logs (alias: trace).",
|
|
168
|
+
)
|
|
169
|
+
logs_parser.add_argument(
|
|
170
|
+
"target",
|
|
171
|
+
nargs="?",
|
|
172
|
+
default="all",
|
|
173
|
+
choices=["all", "gateway", "transport"],
|
|
174
|
+
help="Target log stream to inspect (default all).",
|
|
175
|
+
)
|
|
176
|
+
logs_parser.add_argument(
|
|
177
|
+
"-f",
|
|
178
|
+
"--follow",
|
|
179
|
+
dest="f",
|
|
180
|
+
action="store_true",
|
|
181
|
+
help="Follow log stream output continuously.",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
sub.add_parser(
|
|
185
|
+
"restart",
|
|
186
|
+
aliases=["cycle"],
|
|
187
|
+
description="Restart SSH access using the saved settings.",
|
|
188
|
+
help="Restart using saved settings (alias: cycle).",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
upgrade_parser = sub.add_parser(
|
|
192
|
+
"upgrade",
|
|
193
|
+
description="Fetch and install latest Nullgate release.",
|
|
194
|
+
help="Fetch and install latest Nullgate release.",
|
|
195
|
+
)
|
|
196
|
+
upgrade_parser.add_argument(
|
|
197
|
+
"--version",
|
|
198
|
+
help="Install a specific published version instead of the latest stable release.",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
proxy_parser = sub.add_parser(
|
|
202
|
+
"proxy",
|
|
203
|
+
description=argparse.SUPPRESS,
|
|
204
|
+
)
|
|
205
|
+
proxy_parser.add_argument("url")
|
|
206
|
+
|
|
207
|
+
upterm_proxy = sub.add_parser(
|
|
208
|
+
"upterm-proxy",
|
|
209
|
+
description=argparse.SUPPRESS,
|
|
210
|
+
)
|
|
211
|
+
upterm_proxy.add_argument("url")
|
|
212
|
+
|
|
213
|
+
return parser
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
217
|
+
parser = build_parser()
|
|
218
|
+
args = parser.parse_args(argv)
|
|
219
|
+
|
|
220
|
+
handlers = {
|
|
221
|
+
"start": session.cmd_open,
|
|
222
|
+
"open": session.cmd_open,
|
|
223
|
+
"stop": session.cmd_shut,
|
|
224
|
+
"shut": session.cmd_shut,
|
|
225
|
+
"status": session.cmd_inspect,
|
|
226
|
+
"inspect": session.cmd_inspect,
|
|
227
|
+
"connect": session.cmd_enter,
|
|
228
|
+
"enter": session.cmd_enter,
|
|
229
|
+
"logs": session.cmd_trace,
|
|
230
|
+
"trace": session.cmd_trace,
|
|
231
|
+
"restart": session.cmd_cycle,
|
|
232
|
+
"cycle": session.cmd_cycle,
|
|
233
|
+
"upgrade": session.cmd_upgrade,
|
|
234
|
+
"proxy": transports.cmd_proxy,
|
|
235
|
+
"upterm-proxy": transports.cmd_upterm_proxy,
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
handler = handlers.get(args.command)
|
|
239
|
+
if handler is None:
|
|
240
|
+
parser.print_help(sys.stderr)
|
|
241
|
+
return 2
|
|
242
|
+
return handler(args)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
if __name__ == "__main__":
|
|
246
|
+
sys.exit(main())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Session lifecycle orchestration for Nullgate.
|
|
2
2
|
|
|
3
|
-
Provides handlers for
|
|
3
|
+
Provides handlers for start, stop, status, connect, logs, restart, and upgrade.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
@@ -96,14 +96,14 @@ def _print_url_row(color: _Color, known: bool, url: str) -> None:
|
|
|
96
96
|
if known:
|
|
97
97
|
print(f" url {url}")
|
|
98
98
|
else:
|
|
99
|
-
print(" url pending " + _Color(sys.stdout.isatty()).dim("(nullgate
|
|
99
|
+
print(" url pending " + _Color(sys.stdout.isatty()).dim("(nullgate logs transport -f)"))
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
def _print_auth_rows(color: _Color, state: Path) -> None:
|
|
103
103
|
accept = st.manifest_get("accept")
|
|
104
104
|
allow = st.manifest_get("allow_tcp_forwarding")
|
|
105
105
|
if accept == "1":
|
|
106
|
-
print(" auth none " + color.dim("(--
|
|
106
|
+
print(" auth none " + color.dim("(--public-access); anyone can connect"))
|
|
107
107
|
else:
|
|
108
108
|
password_file = state / "password"
|
|
109
109
|
if password_file.is_file():
|
|
@@ -116,12 +116,12 @@ def _print_auth_rows(color: _Color, state: Path) -> None:
|
|
|
116
116
|
if allow == "1":
|
|
117
117
|
print(" forward enabled")
|
|
118
118
|
else:
|
|
119
|
-
print(" forward disabled " + color.dim("(--
|
|
119
|
+
print(" forward disabled " + color.dim("(--tcp-forwarding)"))
|
|
120
120
|
confine = st.manifest_get("confine_sftp")
|
|
121
121
|
if not confine:
|
|
122
122
|
confine = st.read_settings().get("confine_sftp")
|
|
123
123
|
if confine == "1":
|
|
124
|
-
print(" sftp confined " + color.dim("(--
|
|
124
|
+
print(" sftp confined " + color.dim("(--restrict-file-transfer)"))
|
|
125
125
|
else:
|
|
126
126
|
print(" sftp host")
|
|
127
127
|
|
|
@@ -203,8 +203,8 @@ def _show_status() -> int:
|
|
|
203
203
|
_print_auth_rows(color, state)
|
|
204
204
|
if target is not None:
|
|
205
205
|
print()
|
|
206
|
-
print("
|
|
207
|
-
print(" " + color.dim("first time here: nullgate
|
|
206
|
+
print(" connect " + color.bold(f"ssh {target.user or current_username()}@{target.host}"))
|
|
207
|
+
print(" " + color.dim("first time here: nullgate connect"))
|
|
208
208
|
return 0 if (gateway_running and transport_running) else 1
|
|
209
209
|
|
|
210
210
|
|
|
@@ -269,7 +269,10 @@ def cmd_open(args: Any) -> int:
|
|
|
269
269
|
transports.prompt_authorized_key()
|
|
270
270
|
keys = Path(os.path.expanduser("~/.ssh/authorized_keys"))
|
|
271
271
|
if not keys.is_file() or not keys.stat().st_size:
|
|
272
|
-
die(
|
|
272
|
+
die(
|
|
273
|
+
"The upterm provider requires an SSH authorized key or "
|
|
274
|
+
"the --public-access flag"
|
|
275
|
+
)
|
|
273
276
|
(state / "password").unlink(missing_ok=True)
|
|
274
277
|
password = ""
|
|
275
278
|
else:
|
|
@@ -409,7 +412,7 @@ def cmd_enter(args: Any = None) -> int:
|
|
|
409
412
|
transport = st.manifest_get("transport") or transports.DEFAULT_TRANSPORT
|
|
410
413
|
target = transports.resolve_target(transport, state)
|
|
411
414
|
if target is None:
|
|
412
|
-
die("No published address yet; check nullgate
|
|
415
|
+
die("No published address yet; check `nullgate status`.")
|
|
413
416
|
color = _colorizer()
|
|
414
417
|
ssh_user = target.user or current_username()
|
|
415
418
|
print("Run once on the machine you are connecting from:")
|
|
@@ -451,7 +454,7 @@ def cmd_cycle(args: Any = None) -> int:
|
|
|
451
454
|
del args
|
|
452
455
|
st.setup_state()
|
|
453
456
|
if not st.read_settings():
|
|
454
|
-
die("No saved
|
|
457
|
+
die("No saved settings found; run `nullgate start` with the desired options first.")
|
|
455
458
|
cmd_shut()
|
|
456
459
|
return cmd_open(
|
|
457
460
|
argparse.Namespace(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nullgate
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Disposable SSH gateway into confined directory workspaces
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -18,7 +18,7 @@ Disposable SSH gateway into confined directory workspaces for remote containers,
|
|
|
18
18
|
|
|
19
19
|
- **Ingress Transports**: Publish endpoints via Upterm WebSocket relays (default), srv.us reverse tunnels, Cloudflare Worker relays, or Cloudflare Argo (`cloudflared`) tunnels.
|
|
20
20
|
- **Confinement**: SFTP and SCP workloads are restricted to the designated workspace root; parent traversals and out-of-root symlinks are blocked.
|
|
21
|
-
- **Host file visibility**: Relative transfer paths begin in the workspace root while absolute paths address host locations, matching shell behavior; add `--
|
|
21
|
+
- **Host file visibility**: Relative transfer paths begin in the workspace root while absolute paths address host locations, matching shell behavior; add `--restrict-file-transfer` for strict workspace-only transfers.
|
|
22
22
|
- **Direct Shell Access**: Authenticated PTY sessions run interactively under the local user credentials.
|
|
23
23
|
- **Synthetic UID Resilience**: Runs seamlessly in container environments missing user records in `/etc/passwd`.
|
|
24
24
|
- **Persistent Key Identities**: Host keys and identity seeds persist across restarts rather than being silently regenerated.
|
|
@@ -44,77 +44,77 @@ pip install nullgate
|
|
|
44
44
|
|
|
45
45
|
## Usage
|
|
46
46
|
|
|
47
|
-
###
|
|
47
|
+
### Start
|
|
48
48
|
|
|
49
49
|
Launch the gateway service and ingress transport in the background:
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
# Default Upterm transport on port 4822 sharing current directory
|
|
53
|
-
nullgate
|
|
53
|
+
nullgate start
|
|
54
54
|
|
|
55
55
|
# Custom path, port, and slot
|
|
56
|
-
nullgate
|
|
56
|
+
nullgate start /workspace --ssh-port 4822 --tunnel-slot 1
|
|
57
57
|
|
|
58
58
|
# Select specific transport provider
|
|
59
|
-
nullgate
|
|
60
|
-
nullgate
|
|
61
|
-
nullgate
|
|
62
|
-
nullgate
|
|
59
|
+
nullgate start . --provider upterm
|
|
60
|
+
nullgate start . --provider srvus
|
|
61
|
+
nullgate start . --provider cloudflare --relay-url https://relay.example.com
|
|
62
|
+
nullgate start . --provider cloudflared --public-hostname workspace.example.com --cloudflare-token "$TUNNEL_TOKEN"
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Options:
|
|
66
|
-
- `--
|
|
67
|
-
- `--
|
|
68
|
-
- `--hostname`: Public
|
|
69
|
-
- `--token`:
|
|
70
|
-
- `--
|
|
71
|
-
- `--no-
|
|
72
|
-
- `--
|
|
73
|
-
- `--no-
|
|
74
|
-
- `--
|
|
75
|
-
- `--no-
|
|
76
|
-
|
|
77
|
-
###
|
|
66
|
+
- `--provider`: Public connection provider (`upterm`, `srvus`, `cloudflare`, or `cloudflared`). Defaults to `upterm`.
|
|
67
|
+
- `--relay-url`: WebSocket relay URL used with `upterm` or `cloudflare`.
|
|
68
|
+
- `--public-hostname`: Public hostname used with `cloudflared`.
|
|
69
|
+
- `--cloudflare-token`: Cloudflare tunnel credential, or set `NULLGATE_CLOUDFLARED_TOKEN`.
|
|
70
|
+
- `--public-access`: Allow connections without credentials. This is unsafe on public endpoints.
|
|
71
|
+
- `--no-public-access`: Require authentication.
|
|
72
|
+
- `--tcp-forwarding`: Enable SSH TCP tunneling (`ssh -L`).
|
|
73
|
+
- `--no-tcp-forwarding`: Disable SSH TCP tunneling.
|
|
74
|
+
- `--restrict-file-transfer`: Keep SFTP and SCP inside the workspace.
|
|
75
|
+
- `--no-restrict-file-transfer`: Allow host-visible file transfers.
|
|
76
|
+
|
|
77
|
+
### Status
|
|
78
78
|
|
|
79
79
|
Display operational status, running PIDs, connection URLs, and security settings:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
nullgate
|
|
82
|
+
nullgate status
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
-
###
|
|
85
|
+
### Connect
|
|
86
86
|
|
|
87
87
|
Print client connection strings and OpenSSH client configuration directives:
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
|
-
nullgate
|
|
90
|
+
nullgate connect
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
###
|
|
93
|
+
### Logs
|
|
94
94
|
|
|
95
95
|
Stream or print daemon logs:
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
|
-
nullgate
|
|
99
|
-
nullgate
|
|
100
|
-
nullgate
|
|
101
|
-
nullgate
|
|
98
|
+
nullgate logs
|
|
99
|
+
nullgate logs -f
|
|
100
|
+
nullgate logs gateway
|
|
101
|
+
nullgate logs transport
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
###
|
|
104
|
+
### Stop
|
|
105
105
|
|
|
106
106
|
Terminate running gateway and transport background processes:
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
|
-
nullgate
|
|
109
|
+
nullgate stop
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
###
|
|
112
|
+
### Restart
|
|
113
113
|
|
|
114
114
|
Restart daemon instances reusing active or saved workspace settings:
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
|
-
nullgate
|
|
117
|
+
nullgate restart
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### Upgrade
|
|
@@ -126,13 +126,17 @@ nullgate upgrade
|
|
|
126
126
|
nullgate upgrade --version 1.0.1
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
+
The earlier command names `open`, `shut`, `inspect`, `enter`, `trace`, and
|
|
130
|
+
`cycle` remain available as compatibility aliases. Their earlier option names
|
|
131
|
+
also remain accepted, but new scripts should use the names documented above.
|
|
132
|
+
|
|
129
133
|
## Authentication and Security Boundaries
|
|
130
134
|
|
|
131
135
|
- **Authentication Baseline**: Access requires authorized SSH keys in `~/.ssh/authorized_keys` or a high-entropy secret created in the runtime directory.
|
|
132
|
-
- **
|
|
133
|
-
- **Upterm Transport**: Relies on authorized SSH public keys or `--
|
|
136
|
+
- **Public access (`--public-access`)**: Bypasses authentication checks entirely; any incoming connection with network access is accepted.
|
|
137
|
+
- **Upterm Transport**: Relies on authorized SSH public keys or `--public-access` (Upterm does not support password authentication). The relay itself is trusted for inbound multiplexing.
|
|
134
138
|
- **Filesystem Confinement**: SFTP and SCP access cannot escape the designated workspace directory.
|
|
135
|
-
- **Host Transfer Mode (default)**: Relative SFTP and SCP paths begin in the workspace directory, while absolute paths refer to host filesystem locations subject to OS user permissions. Pass `--
|
|
139
|
+
- **Host Transfer Mode (default)**: Relative SFTP and SCP paths begin in the workspace directory, while absolute paths refer to host filesystem locations subject to OS user permissions. Pass `--restrict-file-transfer` to limit transfers to the workspace namespace and reject symlink escapes.
|
|
136
140
|
- **Interactive Shell Privileges**: Command and terminal sessions execute with the permissions of the host account rather than inside an OS chroot.
|
|
137
141
|
- **State Storage**: Daemon PIDs and logs are maintained in `gateway.pid`, `transport.pid`, `gateway.log`, and `transport.log`, with session records kept in `manifest.json` and `settings.json`.
|
|
138
142
|
|
|
@@ -140,7 +144,7 @@ nullgate upgrade --version 1.0.1
|
|
|
140
144
|
|
|
141
145
|
| Variable | Description |
|
|
142
146
|
|---|---|
|
|
143
|
-
| `NULLGATE_PASSWORD` | Gateway-process credential; normally managed internally by `nullgate
|
|
147
|
+
| `NULLGATE_PASSWORD` | Gateway-process credential; normally managed internally by `nullgate start` |
|
|
144
148
|
| `NULLGATE_STATE_DIR` | Directory holding runtime state and logs (defaults to `~/.local/state/nullgate`) |
|
|
145
149
|
| `NULLGATE_IDENTITY_SEED` | Seed value used for deterministic Ed25519 key derivation |
|
|
146
150
|
| `NULLGATE_TUNNEL_HOST` | Host address for srv.us ingress (default: `srv.us`) |
|
|
@@ -122,7 +122,7 @@ def state_dir(state_base):
|
|
|
122
122
|
class CLILifecycleTests(unittest.TestCase):
|
|
123
123
|
def test_redesigned_defaults_and_boolean_overrides(self):
|
|
124
124
|
parser = commands.build_parser()
|
|
125
|
-
defaults = parser.parse_args(["
|
|
125
|
+
defaults = parser.parse_args(["start"])
|
|
126
126
|
self.assertEqual(session.DEFAULT_PORT, 4822)
|
|
127
127
|
self.assertEqual(transports.DEFAULT_TRANSPORT, "upterm")
|
|
128
128
|
self.assertIsNone(defaults.workspace)
|
|
@@ -133,13 +133,37 @@ class CLILifecycleTests(unittest.TestCase):
|
|
|
133
133
|
self.assertIsNone(defaults.confine_sftp)
|
|
134
134
|
|
|
135
135
|
restricted = parser.parse_args(
|
|
136
|
-
[
|
|
136
|
+
[
|
|
137
|
+
"start",
|
|
138
|
+
"--no-public-access",
|
|
139
|
+
"--no-tcp-forwarding",
|
|
140
|
+
"--restrict-file-transfer",
|
|
141
|
+
]
|
|
137
142
|
)
|
|
138
143
|
self.assertFalse(restricted.accept)
|
|
139
144
|
self.assertFalse(restricted.allow_tcp_forwarding)
|
|
140
|
-
self.
|
|
141
|
-
|
|
142
|
-
|
|
145
|
+
self.assertTrue(restricted.confine_sftp)
|
|
146
|
+
|
|
147
|
+
clearer = parser.parse_args(
|
|
148
|
+
[
|
|
149
|
+
"start",
|
|
150
|
+
"--ssh-port", "2222",
|
|
151
|
+
"--tunnel-slot", "2",
|
|
152
|
+
"--provider", "upterm",
|
|
153
|
+
"--relay-url", "wss://relay.example",
|
|
154
|
+
]
|
|
155
|
+
)
|
|
156
|
+
self.assertEqual(clearer.port, 2222)
|
|
157
|
+
self.assertEqual(clearer.slot, 2)
|
|
158
|
+
self.assertEqual(clearer.transport, "upterm")
|
|
159
|
+
self.assertEqual(clearer.endpoint, "wss://relay.example")
|
|
160
|
+
|
|
161
|
+
legacy = parser.parse_args(
|
|
162
|
+
["open", "--no-accept", "--no-allow-tcp-forwarding", "--confine-sftp"]
|
|
163
|
+
)
|
|
164
|
+
self.assertFalse(legacy.accept)
|
|
165
|
+
self.assertFalse(legacy.allow_tcp_forwarding)
|
|
166
|
+
self.assertTrue(legacy.confine_sftp)
|
|
143
167
|
|
|
144
168
|
upgrade = parser.parse_args(["upgrade"])
|
|
145
169
|
self.assertIsNone(upgrade.version)
|
|
@@ -318,13 +342,13 @@ class CLILifecycleTests(unittest.TestCase):
|
|
|
318
342
|
try:
|
|
319
343
|
code, out, err = run_cli(
|
|
320
344
|
[
|
|
321
|
-
"
|
|
345
|
+
"start",
|
|
322
346
|
str(root),
|
|
323
|
-
"--port",
|
|
347
|
+
"--ssh-port",
|
|
324
348
|
str(ssh_port),
|
|
325
|
-
"--slot",
|
|
349
|
+
"--tunnel-slot",
|
|
326
350
|
str(relay_forward),
|
|
327
|
-
"--
|
|
351
|
+
"--provider",
|
|
328
352
|
"srvus",
|
|
329
353
|
],
|
|
330
354
|
env=env,
|
|
@@ -336,28 +360,28 @@ class CLILifecycleTests(unittest.TestCase):
|
|
|
336
360
|
self.assertEqual(key.stat().st_mode & 0o777, 0o600)
|
|
337
361
|
self.assertTrue((state / "password").is_file())
|
|
338
362
|
|
|
339
|
-
code, out, err = run_cli(["
|
|
363
|
+
code, out, err = run_cli(["status"], env=env)
|
|
340
364
|
self.assertEqual(code, 0)
|
|
341
365
|
self.assertIn("gateway running pid", out)
|
|
342
366
|
self.assertIn("transport running pid", out)
|
|
343
367
|
self.assertIn(f"url https://{SRVUS_HOST}/", out)
|
|
344
368
|
self.assertNotIn("\x1b[", out)
|
|
345
|
-
self.assertIn(f"
|
|
369
|
+
self.assertIn(f"connect ssh {getpass.getuser()}@{SRVUS_HOST}", out)
|
|
346
370
|
self.assertIn("keys disabled", out)
|
|
347
|
-
self.assertNotIn("
|
|
371
|
+
self.assertNotIn("logs transport -f", out)
|
|
348
372
|
|
|
349
|
-
code, out, err = run_cli(["
|
|
373
|
+
code, out, err = run_cli(["connect"], env=env)
|
|
350
374
|
self.assertEqual(code, 0, err)
|
|
351
375
|
self.assertIn("openssl s_client", out)
|
|
352
376
|
self.assertIn("-quiet", out)
|
|
353
377
|
self.assertIn("-no_ign_eof", out)
|
|
354
378
|
self.assertIn("Host *.srv.us", out)
|
|
355
379
|
|
|
356
|
-
code, out, err = run_cli(["
|
|
380
|
+
code, out, err = run_cli(["logs", "transport"], env=env)
|
|
357
381
|
self.assertEqual(code, 0)
|
|
358
382
|
self.assertIn("remote forward ready", out)
|
|
359
383
|
|
|
360
|
-
code, out, err = run_cli(["
|
|
384
|
+
code, out, err = run_cli(["stop"], env=env)
|
|
361
385
|
self.assertEqual(code, 0)
|
|
362
386
|
self.assertIn("Nullgate stopped.", out)
|
|
363
387
|
finally:
|
|
@@ -367,7 +391,7 @@ class CLILifecycleTests(unittest.TestCase):
|
|
|
367
391
|
with contextlib.suppress(OSError):
|
|
368
392
|
os.kill(pid, 9)
|
|
369
393
|
|
|
370
|
-
code, out, err = run_cli(["
|
|
394
|
+
code, out, err = run_cli(["status"], env=env)
|
|
371
395
|
self.assertEqual(code, 1)
|
|
372
396
|
self.assertIn("gateway stopped", out)
|
|
373
397
|
self.assertIn("transport stopped", out)
|
|
@@ -483,7 +507,7 @@ class CLILifecycleTests(unittest.TestCase):
|
|
|
483
507
|
)
|
|
484
508
|
)
|
|
485
509
|
code, out, err = run_cli(["inspect"], env=env)
|
|
486
|
-
self.assertIn("auth none (--
|
|
510
|
+
self.assertIn("auth none (--public-access); anyone can connect", out)
|
|
487
511
|
|
|
488
512
|
def test_open_refuses_occupied_port(self):
|
|
489
513
|
with tempfile.TemporaryDirectory() as temp:
|
|
@@ -548,7 +572,7 @@ class CLILifecycleTests(unittest.TestCase):
|
|
|
548
572
|
env = make_env(home, state_base)
|
|
549
573
|
code, out, err = run_cli(["cycle"], env=env)
|
|
550
574
|
self.assertNotEqual(code, 0)
|
|
551
|
-
self.assertIn("No saved
|
|
575
|
+
self.assertIn("No saved settings found", err)
|
|
552
576
|
|
|
553
577
|
def test_inspect_non_tty_has_no_ansi(self):
|
|
554
578
|
with tempfile.TemporaryDirectory() as temp:
|
|
@@ -572,8 +596,8 @@ class CLILifecycleTests(unittest.TestCase):
|
|
|
572
596
|
code, out, err = run_cli(["inspect"], env=env)
|
|
573
597
|
self.assertNotIn("\x1b[", out)
|
|
574
598
|
self.assertIn("url ssh://glacier.vctx.io", out)
|
|
575
|
-
self.assertIn("first time here: nullgate
|
|
576
|
-
code, out, err = run_cli(["
|
|
599
|
+
self.assertIn("first time here: nullgate connect", out)
|
|
600
|
+
code, out, err = run_cli(["connect"], env=env)
|
|
577
601
|
self.assertEqual(code, 0, err)
|
|
578
602
|
self.assertIn("cloudflared must be installed", out)
|
|
579
603
|
self.assertNotIn("\x1b[", out)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.1"
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
"""Command-line parser and entry point dispatch for Nullgate."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
import sys
|
|
7
|
-
from typing import Sequence
|
|
8
|
-
|
|
9
|
-
from nullgate import session, transports
|
|
10
|
-
|
|
11
|
-
SUBCOMMANDS = ("open", "shut", "inspect", "enter", "trace", "cycle", "upgrade")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
15
|
-
parser = argparse.ArgumentParser(
|
|
16
|
-
prog="nullgate",
|
|
17
|
-
description="Disposable SSH gateway into confined directory workspaces.",
|
|
18
|
-
)
|
|
19
|
-
parser.add_argument(
|
|
20
|
-
"-V",
|
|
21
|
-
"--version",
|
|
22
|
-
action="version",
|
|
23
|
-
version=f"nullgate {session._version()}",
|
|
24
|
-
)
|
|
25
|
-
sub = parser.add_subparsers(dest="command", required=True, metavar="command")
|
|
26
|
-
|
|
27
|
-
open_parser = sub.add_parser(
|
|
28
|
-
"open",
|
|
29
|
-
description="Launch gateway daemon and ingress transport.",
|
|
30
|
-
help="Launch gateway daemon and ingress transport.",
|
|
31
|
-
)
|
|
32
|
-
open_parser.add_argument(
|
|
33
|
-
"workspace",
|
|
34
|
-
nargs="?",
|
|
35
|
-
help="Directory exposed by the gateway (default: current directory).",
|
|
36
|
-
)
|
|
37
|
-
open_parser.add_argument(
|
|
38
|
-
"--port",
|
|
39
|
-
type=int,
|
|
40
|
-
default=None,
|
|
41
|
-
help=f"Local SSH listen port (default: {session.DEFAULT_PORT}).",
|
|
42
|
-
)
|
|
43
|
-
open_parser.add_argument(
|
|
44
|
-
"--slot",
|
|
45
|
-
type=int,
|
|
46
|
-
default=None,
|
|
47
|
-
help=f"Ingress slot number (default: {session.DEFAULT_SLOT}).",
|
|
48
|
-
)
|
|
49
|
-
open_parser.add_argument(
|
|
50
|
-
"--transport",
|
|
51
|
-
choices=transports.VALID_TRANSPORTS,
|
|
52
|
-
default=None,
|
|
53
|
-
help=f"Ingress transport provider (default {transports.DEFAULT_TRANSPORT}).",
|
|
54
|
-
)
|
|
55
|
-
open_parser.add_argument(
|
|
56
|
-
"--accept",
|
|
57
|
-
action=argparse.BooleanOptionalAction,
|
|
58
|
-
default=None,
|
|
59
|
-
help="Permit any client without credentials; --no-accept restores authentication.",
|
|
60
|
-
)
|
|
61
|
-
open_parser.add_argument(
|
|
62
|
-
"--allow-tcp-forwarding",
|
|
63
|
-
action=argparse.BooleanOptionalAction,
|
|
64
|
-
default=None,
|
|
65
|
-
help="Allow SSH port forwarding; use --no-allow-tcp-forwarding to disable it.",
|
|
66
|
-
)
|
|
67
|
-
open_parser.add_argument(
|
|
68
|
-
"--confine-sftp",
|
|
69
|
-
action=argparse.BooleanOptionalAction,
|
|
70
|
-
default=None,
|
|
71
|
-
help="Confine SFTP and SCP to the workspace; --no-confine-sftp restores host visibility.",
|
|
72
|
-
)
|
|
73
|
-
open_parser.add_argument(
|
|
74
|
-
"--endpoint",
|
|
75
|
-
help="WebSocket ingress endpoint URL (for upterm or cloudflare transports).",
|
|
76
|
-
)
|
|
77
|
-
open_parser.add_argument(
|
|
78
|
-
"--hostname",
|
|
79
|
-
help="Public hostname (for cloudflared transport).",
|
|
80
|
-
)
|
|
81
|
-
open_parser.add_argument(
|
|
82
|
-
"--token",
|
|
83
|
-
help="Cloudflare Argo tunnel token (for cloudflared transport).",
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
sub.add_parser(
|
|
87
|
-
"shut",
|
|
88
|
-
description="Halt running gateway and transport services.",
|
|
89
|
-
help="Halt running gateway and transport services.",
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
sub.add_parser(
|
|
93
|
-
"inspect",
|
|
94
|
-
description="Display service health, access URLs, and authentication status.",
|
|
95
|
-
help="Display service health, access URLs, and authentication status.",
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
sub.add_parser(
|
|
99
|
-
"enter",
|
|
100
|
-
description="Show SSH connection strings and client setup commands.",
|
|
101
|
-
help="Show SSH connection strings and client setup commands.",
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
trace_parser = sub.add_parser(
|
|
105
|
-
"trace",
|
|
106
|
-
description="View or stream service operational logs.",
|
|
107
|
-
help="View or stream service operational logs.",
|
|
108
|
-
)
|
|
109
|
-
trace_parser.add_argument(
|
|
110
|
-
"target",
|
|
111
|
-
nargs="?",
|
|
112
|
-
default="all",
|
|
113
|
-
choices=["all", "gateway", "transport"],
|
|
114
|
-
help="Target log stream to inspect (default all).",
|
|
115
|
-
)
|
|
116
|
-
trace_parser.add_argument(
|
|
117
|
-
"-f",
|
|
118
|
-
"--follow",
|
|
119
|
-
dest="f",
|
|
120
|
-
action="store_true",
|
|
121
|
-
help="Follow log stream output continuously.",
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
sub.add_parser(
|
|
125
|
-
"cycle",
|
|
126
|
-
description="Restart services using active or saved configuration.",
|
|
127
|
-
help="Restart services using active or saved configuration.",
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
upgrade_parser = sub.add_parser(
|
|
131
|
-
"upgrade",
|
|
132
|
-
description="Fetch and install latest Nullgate release.",
|
|
133
|
-
help="Fetch and install latest Nullgate release.",
|
|
134
|
-
)
|
|
135
|
-
upgrade_parser.add_argument(
|
|
136
|
-
"--version",
|
|
137
|
-
help="Install a specific published version instead of the latest stable release.",
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
proxy_parser = sub.add_parser(
|
|
141
|
-
"proxy",
|
|
142
|
-
description=argparse.SUPPRESS,
|
|
143
|
-
)
|
|
144
|
-
proxy_parser.add_argument("url")
|
|
145
|
-
|
|
146
|
-
upterm_proxy = sub.add_parser(
|
|
147
|
-
"upterm-proxy",
|
|
148
|
-
description=argparse.SUPPRESS,
|
|
149
|
-
)
|
|
150
|
-
upterm_proxy.add_argument("url")
|
|
151
|
-
|
|
152
|
-
return parser
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def main(argv: Sequence[str] | None = None) -> int:
|
|
156
|
-
parser = build_parser()
|
|
157
|
-
args = parser.parse_args(argv)
|
|
158
|
-
|
|
159
|
-
handlers = {
|
|
160
|
-
"open": session.cmd_open,
|
|
161
|
-
"shut": session.cmd_shut,
|
|
162
|
-
"inspect": session.cmd_inspect,
|
|
163
|
-
"enter": session.cmd_enter,
|
|
164
|
-
"trace": session.cmd_trace,
|
|
165
|
-
"cycle": session.cmd_cycle,
|
|
166
|
-
"upgrade": session.cmd_upgrade,
|
|
167
|
-
"proxy": transports.cmd_proxy,
|
|
168
|
-
"upterm-proxy": transports.cmd_upterm_proxy,
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
handler = handlers.get(args.command)
|
|
172
|
-
if handler is None:
|
|
173
|
-
parser.print_help(sys.stderr)
|
|
174
|
-
return 2
|
|
175
|
-
return handler(args)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if __name__ == "__main__":
|
|
179
|
-
sys.exit(main())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|