tunnel-manager 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl
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.
Potentially problematic release.
This version of tunnel-manager might be problematic. Click here for more details.
- tunnel_manager/tunnel_manager_mcp.py +213 -2
- {tunnel_manager-1.0.3.dist-info → tunnel_manager-1.0.5.dist-info}/METADATA +295 -197
- tunnel_manager-1.0.5.dist-info/RECORD +11 -0
- tunnel_manager-1.0.3.dist-info/RECORD +0 -11
- {tunnel_manager-1.0.3.dist-info → tunnel_manager-1.0.5.dist-info}/WHEEL +0 -0
- {tunnel_manager-1.0.3.dist-info → tunnel_manager-1.0.5.dist-info}/entry_points.txt +0 -0
- {tunnel_manager-1.0.3.dist-info → tunnel_manager-1.0.5.dist-info}/licenses/LICENSE +0 -0
- {tunnel_manager-1.0.3.dist-info → tunnel_manager-1.0.5.dist-info}/top_level.txt +0 -0
|
@@ -8,9 +8,16 @@ import concurrent.futures
|
|
|
8
8
|
import yaml
|
|
9
9
|
import asyncio
|
|
10
10
|
from typing import Optional, Dict, List, Union
|
|
11
|
-
from tunnel_manager.tunnel_manager import Tunnel
|
|
12
|
-
from fastmcp import FastMCP, Context
|
|
13
11
|
from pydantic import Field
|
|
12
|
+
from fastmcp import FastMCP, Context
|
|
13
|
+
from fastmcp.server.auth.oidc_proxy import OIDCProxy
|
|
14
|
+
from fastmcp.server.auth import OAuthProxy, RemoteAuthProvider
|
|
15
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier, StaticTokenVerifier
|
|
16
|
+
from fastmcp.server.middleware.logging import LoggingMiddleware
|
|
17
|
+
from fastmcp.server.middleware.timing import TimingMiddleware
|
|
18
|
+
from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
|
|
19
|
+
from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
|
|
20
|
+
from tunnel_manager.tunnel_manager import Tunnel
|
|
14
21
|
|
|
15
22
|
# Initialize FastMCP
|
|
16
23
|
mcp = FastMCP(name="TunnelServer")
|
|
@@ -2120,6 +2127,83 @@ def tunnel_manager_mcp():
|
|
|
2120
2127
|
default=8000,
|
|
2121
2128
|
help="Port number for HTTP transport (default: 8000)",
|
|
2122
2129
|
)
|
|
2130
|
+
parser.add_argument(
|
|
2131
|
+
"--auth-type",
|
|
2132
|
+
default="none",
|
|
2133
|
+
choices=["none", "static", "jwt", "oauth-proxy", "oidc-proxy", "remote-oauth"],
|
|
2134
|
+
help="Authentication type for MCP server: 'none' (disabled), 'static' (internal), 'jwt' (external token verification), 'oauth-proxy', 'oidc-proxy', 'remote-oauth' (external) (default: none)",
|
|
2135
|
+
)
|
|
2136
|
+
# JWT/Token params
|
|
2137
|
+
parser.add_argument(
|
|
2138
|
+
"--token-jwks-uri", default=None, help="JWKS URI for JWT verification"
|
|
2139
|
+
)
|
|
2140
|
+
parser.add_argument(
|
|
2141
|
+
"--token-issuer", default=None, help="Issuer for JWT verification"
|
|
2142
|
+
)
|
|
2143
|
+
parser.add_argument(
|
|
2144
|
+
"--token-audience", default=None, help="Audience for JWT verification"
|
|
2145
|
+
)
|
|
2146
|
+
# OAuth Proxy params
|
|
2147
|
+
parser.add_argument(
|
|
2148
|
+
"--oauth-upstream-auth-endpoint",
|
|
2149
|
+
default=None,
|
|
2150
|
+
help="Upstream authorization endpoint for OAuth Proxy",
|
|
2151
|
+
)
|
|
2152
|
+
parser.add_argument(
|
|
2153
|
+
"--oauth-upstream-token-endpoint",
|
|
2154
|
+
default=None,
|
|
2155
|
+
help="Upstream token endpoint for OAuth Proxy",
|
|
2156
|
+
)
|
|
2157
|
+
parser.add_argument(
|
|
2158
|
+
"--oauth-upstream-client-id",
|
|
2159
|
+
default=None,
|
|
2160
|
+
help="Upstream client ID for OAuth Proxy",
|
|
2161
|
+
)
|
|
2162
|
+
parser.add_argument(
|
|
2163
|
+
"--oauth-upstream-client-secret",
|
|
2164
|
+
default=None,
|
|
2165
|
+
help="Upstream client secret for OAuth Proxy",
|
|
2166
|
+
)
|
|
2167
|
+
parser.add_argument(
|
|
2168
|
+
"--oauth-base-url", default=None, help="Base URL for OAuth Proxy"
|
|
2169
|
+
)
|
|
2170
|
+
# OIDC Proxy params
|
|
2171
|
+
parser.add_argument(
|
|
2172
|
+
"--oidc-config-url", default=None, help="OIDC configuration URL"
|
|
2173
|
+
)
|
|
2174
|
+
parser.add_argument("--oidc-client-id", default=None, help="OIDC client ID")
|
|
2175
|
+
parser.add_argument("--oidc-client-secret", default=None, help="OIDC client secret")
|
|
2176
|
+
parser.add_argument("--oidc-base-url", default=None, help="Base URL for OIDC Proxy")
|
|
2177
|
+
# Remote OAuth params
|
|
2178
|
+
parser.add_argument(
|
|
2179
|
+
"--remote-auth-servers",
|
|
2180
|
+
default=None,
|
|
2181
|
+
help="Comma-separated list of authorization servers for Remote OAuth",
|
|
2182
|
+
)
|
|
2183
|
+
parser.add_argument(
|
|
2184
|
+
"--remote-base-url", default=None, help="Base URL for Remote OAuth"
|
|
2185
|
+
)
|
|
2186
|
+
# Common
|
|
2187
|
+
parser.add_argument(
|
|
2188
|
+
"--allowed-client-redirect-uris",
|
|
2189
|
+
default=None,
|
|
2190
|
+
help="Comma-separated list of allowed client redirect URIs",
|
|
2191
|
+
)
|
|
2192
|
+
# Eunomia params
|
|
2193
|
+
parser.add_argument(
|
|
2194
|
+
"--eunomia-type",
|
|
2195
|
+
default="none",
|
|
2196
|
+
choices=["none", "embedded", "remote"],
|
|
2197
|
+
help="Eunomia authorization type: 'none' (disabled), 'embedded' (built-in), 'remote' (external) (default: none)",
|
|
2198
|
+
)
|
|
2199
|
+
parser.add_argument(
|
|
2200
|
+
"--eunomia-policy-file",
|
|
2201
|
+
default="mcp_policies.json",
|
|
2202
|
+
help="Policy file for embedded Eunomia (default: mcp_policies.json)",
|
|
2203
|
+
)
|
|
2204
|
+
parser.add_argument(
|
|
2205
|
+
"--eunomia-remote-url", default=None, help="URL for remote Eunomia server"
|
|
2206
|
+
)
|
|
2123
2207
|
|
|
2124
2208
|
args = parser.parse_args()
|
|
2125
2209
|
|
|
@@ -2127,6 +2211,133 @@ def tunnel_manager_mcp():
|
|
|
2127
2211
|
print(f"Error: Port {args.port} is out of valid range (0-65535).")
|
|
2128
2212
|
sys.exit(1)
|
|
2129
2213
|
|
|
2214
|
+
# Set auth based on type
|
|
2215
|
+
auth = None
|
|
2216
|
+
allowed_uris = (
|
|
2217
|
+
args.allowed_client_redirect_uris.split(",")
|
|
2218
|
+
if args.allowed_client_redirect_uris
|
|
2219
|
+
else None
|
|
2220
|
+
)
|
|
2221
|
+
|
|
2222
|
+
if args.auth_type == "none":
|
|
2223
|
+
auth = None
|
|
2224
|
+
elif args.auth_type == "static":
|
|
2225
|
+
# Internal static tokens (hardcoded example)
|
|
2226
|
+
auth = StaticTokenVerifier(
|
|
2227
|
+
tokens={
|
|
2228
|
+
"test-token": {"client_id": "test-user", "scopes": ["read", "write"]},
|
|
2229
|
+
"admin-token": {"client_id": "admin", "scopes": ["admin"]},
|
|
2230
|
+
}
|
|
2231
|
+
)
|
|
2232
|
+
elif args.auth_type == "jwt":
|
|
2233
|
+
if not (args.token_jwks_uri and args.token_issuer and args.token_audience):
|
|
2234
|
+
print(
|
|
2235
|
+
"Error: jwt requires --token-jwks-uri, --token-issuer, --token-audience"
|
|
2236
|
+
)
|
|
2237
|
+
sys.exit(1)
|
|
2238
|
+
auth = JWTVerifier(
|
|
2239
|
+
jwks_uri=args.token_jwks_uri,
|
|
2240
|
+
issuer=args.token_issuer,
|
|
2241
|
+
audience=args.token_audience,
|
|
2242
|
+
)
|
|
2243
|
+
elif args.auth_type == "oauth-proxy":
|
|
2244
|
+
if not (
|
|
2245
|
+
args.oauth_upstream_auth_endpoint
|
|
2246
|
+
and args.oauth_upstream_token_endpoint
|
|
2247
|
+
and args.oauth_upstream_client_id
|
|
2248
|
+
and args.oauth_upstream_client_secret
|
|
2249
|
+
and args.oauth_base_url
|
|
2250
|
+
and args.token_jwks_uri
|
|
2251
|
+
and args.token_issuer
|
|
2252
|
+
and args.token_audience
|
|
2253
|
+
):
|
|
2254
|
+
print(
|
|
2255
|
+
"Error: oauth-proxy requires --oauth-upstream-auth-endpoint, --oauth-upstream-token-endpoint, --oauth-upstream-client-id, --oauth-upstream-client-secret, --oauth-base-url, --token-jwks-uri, --token-issuer, --token-audience"
|
|
2256
|
+
)
|
|
2257
|
+
sys.exit(1)
|
|
2258
|
+
token_verifier = JWTVerifier(
|
|
2259
|
+
jwks_uri=args.token_jwks_uri,
|
|
2260
|
+
issuer=args.token_issuer,
|
|
2261
|
+
audience=args.token_audience,
|
|
2262
|
+
)
|
|
2263
|
+
auth = OAuthProxy(
|
|
2264
|
+
upstream_authorization_endpoint=args.oauth_upstream_auth_endpoint,
|
|
2265
|
+
upstream_token_endpoint=args.oauth_upstream_token_endpoint,
|
|
2266
|
+
upstream_client_id=args.oauth_upstream_client_id,
|
|
2267
|
+
upstream_client_secret=args.oauth_upstream_client_secret,
|
|
2268
|
+
token_verifier=token_verifier,
|
|
2269
|
+
base_url=args.oauth_base_url,
|
|
2270
|
+
allowed_client_redirect_uris=allowed_uris,
|
|
2271
|
+
)
|
|
2272
|
+
elif args.auth_type == "oidc-proxy":
|
|
2273
|
+
if not (
|
|
2274
|
+
args.oidc_config_url
|
|
2275
|
+
and args.oidc_client_id
|
|
2276
|
+
and args.oidc_client_secret
|
|
2277
|
+
and args.oidc_base_url
|
|
2278
|
+
):
|
|
2279
|
+
print(
|
|
2280
|
+
"Error: oidc-proxy requires --oidc-config-url, --oidc-client-id, --oidc-client-secret, --oidc-base-url"
|
|
2281
|
+
)
|
|
2282
|
+
sys.exit(1)
|
|
2283
|
+
auth = OIDCProxy(
|
|
2284
|
+
config_url=args.oidc_config_url,
|
|
2285
|
+
client_id=args.oidc_client_id,
|
|
2286
|
+
client_secret=args.oidc_client_secret,
|
|
2287
|
+
base_url=args.oidc_base_url,
|
|
2288
|
+
allowed_client_redirect_uris=allowed_uris,
|
|
2289
|
+
)
|
|
2290
|
+
elif args.auth_type == "remote-oauth":
|
|
2291
|
+
if not (
|
|
2292
|
+
args.remote_auth_servers
|
|
2293
|
+
and args.remote_base_url
|
|
2294
|
+
and args.token_jwks_uri
|
|
2295
|
+
and args.token_issuer
|
|
2296
|
+
and args.token_audience
|
|
2297
|
+
):
|
|
2298
|
+
print(
|
|
2299
|
+
"Error: remote-oauth requires --remote-auth-servers, --remote-base-url, --token-jwks-uri, --token-issuer, --token-audience"
|
|
2300
|
+
)
|
|
2301
|
+
sys.exit(1)
|
|
2302
|
+
auth_servers = [url.strip() for url in args.remote_auth_servers.split(",")]
|
|
2303
|
+
token_verifier = JWTVerifier(
|
|
2304
|
+
jwks_uri=args.token_jwks_uri,
|
|
2305
|
+
issuer=args.token_issuer,
|
|
2306
|
+
audience=args.token_audience,
|
|
2307
|
+
)
|
|
2308
|
+
auth = RemoteAuthProvider(
|
|
2309
|
+
token_verifier=token_verifier,
|
|
2310
|
+
authorization_servers=auth_servers,
|
|
2311
|
+
base_url=args.remote_base_url,
|
|
2312
|
+
)
|
|
2313
|
+
mcp.auth = auth
|
|
2314
|
+
if args.eunomia_type != "none":
|
|
2315
|
+
from eunomia_mcp import create_eunomia_middleware
|
|
2316
|
+
|
|
2317
|
+
if args.eunomia_type == "embedded":
|
|
2318
|
+
if not args.eunomia_policy_file:
|
|
2319
|
+
print("Error: embedded Eunomia requires --eunomia-policy-file")
|
|
2320
|
+
sys.exit(1)
|
|
2321
|
+
middleware = create_eunomia_middleware(policy_file=args.eunomia_policy_file)
|
|
2322
|
+
mcp.add_middleware(middleware)
|
|
2323
|
+
elif args.eunomia_type == "remote":
|
|
2324
|
+
if not args.eunomia_remote_url:
|
|
2325
|
+
print("Error: remote Eunomia requires --eunomia-remote-url")
|
|
2326
|
+
sys.exit(1)
|
|
2327
|
+
middleware = create_eunomia_middleware(
|
|
2328
|
+
use_remote_eunomia=args.eunomia_remote_url
|
|
2329
|
+
)
|
|
2330
|
+
mcp.add_middleware(middleware)
|
|
2331
|
+
|
|
2332
|
+
mcp.add_middleware(
|
|
2333
|
+
ErrorHandlingMiddleware(include_traceback=True, transform_errors=True)
|
|
2334
|
+
)
|
|
2335
|
+
mcp.add_middleware(
|
|
2336
|
+
RateLimitingMiddleware(max_requests_per_second=10.0, burst_capacity=20)
|
|
2337
|
+
)
|
|
2338
|
+
mcp.add_middleware(TimingMiddleware())
|
|
2339
|
+
mcp.add_middleware(LoggingMiddleware())
|
|
2340
|
+
|
|
2130
2341
|
if args.transport == "stdio":
|
|
2131
2342
|
mcp.run(transport="stdio")
|
|
2132
2343
|
elif args.transport == "http":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tunnel-manager
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Create SSH Tunnels to your remote hosts and host as an MCP Server for Agentic AI!
|
|
5
5
|
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: fastmcp>=2.
|
|
15
|
+
Requires-Dist: fastmcp>=2.12.4
|
|
16
16
|
Requires-Dist: paramiko>=4.0.0
|
|
17
17
|
Dynamic: license-file
|
|
18
18
|
|
|
@@ -38,7 +38,7 @@ Dynamic: license-file
|
|
|
38
38
|

|
|
39
39
|

|
|
40
40
|
|
|
41
|
-
*Version: 1.0.
|
|
41
|
+
*Version: 1.0.5*
|
|
42
42
|
|
|
43
43
|
This project provides a Python-based `Tunnel` class for secure SSH connections and file transfers, integrated with a FastMCP server (`tunnel_manager_mcp.py`) to expose these capabilities as tools for AI-driven workflows. The implementation supports both standard SSH (e.g., for local networks) and Teleport's secure access platform, leveraging the `paramiko` library for SSH operations.
|
|
44
44
|
|
|
@@ -84,135 +84,49 @@ This project provides a Python-based `Tunnel` class for secure SSH connections a
|
|
|
84
84
|
<details>
|
|
85
85
|
<summary><b>Usage:</b></summary>
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Download a file
|
|
114
|
-
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
|
|
115
|
-
|
|
116
|
-
# Setup passwordless SSH with RSA
|
|
117
|
-
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_rsa", key_type="rsa")
|
|
118
|
-
|
|
119
|
-
# Copy SSH config
|
|
120
|
-
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
|
|
121
|
-
|
|
122
|
-
# Rotate SSH key with RSA
|
|
123
|
-
tunnel.rotate_ssh_key("/path/to/new_rsa_key", key_type="rsa")
|
|
124
|
-
|
|
125
|
-
# Close the connection
|
|
126
|
-
tunnel.close()
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### Using Ed25519 Keys
|
|
130
|
-
```python
|
|
131
|
-
from tunnel_manager.tunnel_manager import Tunnel
|
|
132
|
-
|
|
133
|
-
# Initialize with a remote host (assumes ~/.ssh/config or explicit params)
|
|
134
|
-
tunnel = Tunnel(
|
|
135
|
-
remote_host="192.168.1.10",
|
|
136
|
-
username="admin",
|
|
137
|
-
password="mypassword",
|
|
138
|
-
identity_file="/path/to/id_ed25519",
|
|
139
|
-
certificate_file="/path/to/cert", # Optional for Teleport
|
|
140
|
-
proxy_command="tsh proxy ssh %h", # Optional for Teleport
|
|
141
|
-
ssh_config_file="~/.ssh/config",
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
# Connect and run a command
|
|
145
|
-
tunnel.connect()
|
|
146
|
-
out, err = tunnel.run_command("ls -la /tmp")
|
|
147
|
-
print(f"Output: {out}\nError: {err}")
|
|
148
|
-
|
|
149
|
-
# Upload a file
|
|
150
|
-
tunnel.send_file("/local/file.txt", "/remote/file.txt")
|
|
151
|
-
|
|
152
|
-
# Download a file
|
|
153
|
-
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
|
|
154
|
-
|
|
155
|
-
# Setup passwordless SSH with Ed25519
|
|
156
|
-
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_ed25519", key_type="ed25519")
|
|
157
|
-
|
|
158
|
-
# Copy SSH config
|
|
159
|
-
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
|
|
160
|
-
|
|
161
|
-
# Rotate SSH key with Ed25519
|
|
162
|
-
tunnel.rotate_ssh_key("/path/to/new_ed25519_key", key_type="ed25519")
|
|
163
|
-
|
|
164
|
-
# Close the connection
|
|
165
|
-
tunnel.close()
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## Tunnel Manager CLI Usage
|
|
169
|
-
The `tunnel_manager.py` script provides a CLI for managing SSH operations across hosts defined in an Ansible-style YAML inventory file. Below are examples for each command, targeting different inventory groups (`all`, `homelab`, `poweredge`). The CLI now supports both RSA and Ed25519 keys via the `--key-type` flag for relevant commands (default: `ed25519`).
|
|
170
|
-
|
|
171
|
-
**Inventory File Example (`inventory.yml`)**:
|
|
172
|
-
```yaml
|
|
173
|
-
all:
|
|
174
|
-
hosts:
|
|
175
|
-
r510:
|
|
176
|
-
ansible_host: 192.168.1.10
|
|
177
|
-
ansible_user: admin
|
|
178
|
-
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
179
|
-
r710:
|
|
180
|
-
ansible_host: 192.168.1.11
|
|
181
|
-
ansible_user: admin
|
|
182
|
-
ansible_ssh_pass: mypassword
|
|
183
|
-
gr1080:
|
|
184
|
-
ansible_host: 192.168.1.14
|
|
185
|
-
ansible_user: admin
|
|
186
|
-
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
|
|
187
|
-
homelab:
|
|
188
|
-
hosts:
|
|
189
|
-
r510:
|
|
190
|
-
ansible_host: 192.168.1.10
|
|
191
|
-
ansible_user: admin
|
|
192
|
-
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
193
|
-
r710:
|
|
194
|
-
ansible_host: 192.168.1.11
|
|
195
|
-
ansible_user: admin
|
|
196
|
-
ansible_ssh_pass: mypassword
|
|
197
|
-
gr1080:
|
|
198
|
-
ansible_host: 192.168.1.14
|
|
199
|
-
ansible_user: admin
|
|
200
|
-
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
|
|
201
|
-
poweredge:
|
|
202
|
-
hosts:
|
|
203
|
-
r510:
|
|
204
|
-
ansible_host: 192.168.1.10
|
|
205
|
-
ansible_user: admin
|
|
206
|
-
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
207
|
-
r710:
|
|
208
|
-
ansible_host: 192.168.1.11
|
|
209
|
-
ansible_user: admin
|
|
210
|
-
ansible_ssh_pass: mypassword
|
|
211
|
-
```
|
|
87
|
+
### CLI
|
|
88
|
+
| Short Flag | Long Flag | Description | Required | Default Value |
|
|
89
|
+
|------------|----------------------|----------------------------------------------------------|----------|---------------|
|
|
90
|
+
| -h | --help | Show usage for the script | No | None |
|
|
91
|
+
| | --log-file | Log to specified file (default: console output) | No | Console |
|
|
92
|
+
| | setup-all | Setup passwordless SSH for all hosts in inventory | Yes* | None |
|
|
93
|
+
| | --inventory | YAML inventory path | Yes | None |
|
|
94
|
+
| | --shared-key-path | Path to shared private key | No | ~/.ssh/id_shared |
|
|
95
|
+
| | --key-type | Key type (rsa or ed25519) | No | ed25519 |
|
|
96
|
+
| | --group | Inventory group to target | No | all |
|
|
97
|
+
| | --parallel | Run operation in parallel | No | False |
|
|
98
|
+
| | --max-threads | Max threads for parallel execution | No | 5 |
|
|
99
|
+
| | run-command | Run a shell command on all hosts in inventory | Yes* | None |
|
|
100
|
+
| | --remote-command | Shell command to run | Yes | None |
|
|
101
|
+
| | copy-config | Copy SSH config to all hosts in inventory | Yes* | None |
|
|
102
|
+
| | --local-config-path | Local SSH config path | Yes | None |
|
|
103
|
+
| | --remote-config-path | Remote path for SSH config | No | ~/.ssh/config |
|
|
104
|
+
| | rotate-key | Rotate SSH keys for all hosts in inventory | Yes* | None |
|
|
105
|
+
| | --key-prefix | Prefix for new key paths (appends hostname) | No | ~/.ssh/id_ |
|
|
106
|
+
| | --key-type | Key type (rsa or ed25519) | No | ed25519 |
|
|
107
|
+
| | send-file | Upload a file to all hosts in inventory | Yes* | None |
|
|
108
|
+
| | --local-path | Local file path to upload | Yes | None |
|
|
109
|
+
| | --remote-path | Remote destination path | Yes | None |
|
|
110
|
+
| | receive-file | Download a file from all hosts in inventory | Yes* | None |
|
|
111
|
+
| | --remote-path | Remote file path to download | Yes | None |
|
|
112
|
+
| | --local-path-prefix | Local directory path prefix to save files | Yes | None |
|
|
212
113
|
|
|
213
|
-
|
|
114
|
+
### Notes
|
|
115
|
+
One of the commands (`setup-all`, `run-command`, `copy-config`, `rotate-key`, `send-file`, `receive-file`) must be specified as the first argument to `tunnel_manager.py`. Each command has required arguments that must be specified with flags:
|
|
116
|
+
- `setup-all`: Requires `--inventory`.
|
|
117
|
+
- `run-command`: Requires `--inventory` and `--remote-command`.
|
|
118
|
+
- `copy-config`: Requires `--inventory` and `--local-config-path`.
|
|
119
|
+
- `rotate-key`: Requires `--inventory`.
|
|
120
|
+
- `send-file`: Requires `--inventory`, `--local-path`, and `--remote-path`.
|
|
121
|
+
- `receive-file`: Requires `--inventory`, `--remote-path`, and `--local-path-prefix`.
|
|
214
122
|
|
|
215
|
-
###
|
|
123
|
+
### Additional Notes
|
|
124
|
+
- Ensure `ansible_host` values in `inventory.yml` are resolvable IPs or hostnames.
|
|
125
|
+
- Update `ansible_ssh_private_key_file` in the inventory after running `rotate-key`.
|
|
126
|
+
- Use `--log-file` for file-based logging or omit for console output.
|
|
127
|
+
- The `--parallel` option speeds up operations but may overload resources; adjust `--max-threads` as needed.
|
|
128
|
+
- The `receive-file` command saves files to `local_path_prefix/<hostname>/<filename>` to preserve original filenames and avoid conflicts.
|
|
129
|
+
- Ed25519 keys are recommended for better security and performance over RSA, but RSA is supported for compatibility with older systems.
|
|
216
130
|
|
|
217
131
|
#### 1. Setup Passwordless SSH
|
|
218
132
|
Set up passwordless SSH for hosts in the inventory, distributing a shared key. Use `--key-type` to specify RSA or Ed25519 (default: ed25519).
|
|
@@ -304,70 +218,267 @@ Download a file from all hosts in the specified group, saving to host-specific s
|
|
|
304
218
|
tunnel-manager --log-file download_poweredge.log receive-file --inventory inventory.yml --remote-path /home/user/myfile.txt --local-path-prefix ./downloads --group poweredge
|
|
305
219
|
```
|
|
306
220
|
|
|
307
|
-
###
|
|
308
|
-
| Short Flag | Long Flag | Description | Required | Default Value |
|
|
309
|
-
|------------|----------------------|----------------------------------------------------------|----------|---------------|
|
|
310
|
-
| -h | --help | Show usage for the script | No | None |
|
|
311
|
-
| | --log-file | Log to specified file (default: console output) | No | Console |
|
|
312
|
-
| | setup-all | Setup passwordless SSH for all hosts in inventory | Yes* | None |
|
|
313
|
-
| | --inventory | YAML inventory path | Yes | None |
|
|
314
|
-
| | --shared-key-path | Path to shared private key | No | ~/.ssh/id_shared |
|
|
315
|
-
| | --key-type | Key type (rsa or ed25519) | No | ed25519 |
|
|
316
|
-
| | --group | Inventory group to target | No | all |
|
|
317
|
-
| | --parallel | Run operation in parallel | No | False |
|
|
318
|
-
| | --max-threads | Max threads for parallel execution | No | 5 |
|
|
319
|
-
| | run-command | Run a shell command on all hosts in inventory | Yes* | None |
|
|
320
|
-
| | --remote-command | Shell command to run | Yes | None |
|
|
321
|
-
| | copy-config | Copy SSH config to all hosts in inventory | Yes* | None |
|
|
322
|
-
| | --local-config-path | Local SSH config path | Yes | None |
|
|
323
|
-
| | --remote-config-path | Remote path for SSH config | No | ~/.ssh/config |
|
|
324
|
-
| | rotate-key | Rotate SSH keys for all hosts in inventory | Yes* | None |
|
|
325
|
-
| | --key-prefix | Prefix for new key paths (appends hostname) | No | ~/.ssh/id_ |
|
|
326
|
-
| | --key-type | Key type (rsa or ed25519) | No | ed25519 |
|
|
327
|
-
| | send-file | Upload a file to all hosts in inventory | Yes* | None |
|
|
328
|
-
| | --local-path | Local file path to upload | Yes | None |
|
|
329
|
-
| | --remote-path | Remote destination path | Yes | None |
|
|
330
|
-
| | receive-file | Download a file from all hosts in inventory | Yes* | None |
|
|
331
|
-
| | --remote-path | Remote file path to download | Yes | None |
|
|
332
|
-
| | --local-path-prefix | Local directory path prefix to save files | Yes | None |
|
|
221
|
+
### Tunnel Manager Inventory
|
|
333
222
|
|
|
334
|
-
|
|
335
|
-
One of the commands (`setup-all`, `run-command`, `copy-config`, `rotate-key`, `send-file`, `receive-file`) must be specified as the first argument to `tunnel_manager.py`. Each command has required arguments that must be specified with flags:
|
|
336
|
-
- `setup-all`: Requires `--inventory`.
|
|
337
|
-
- `run-command`: Requires `--inventory` and `--remote-command`.
|
|
338
|
-
- `copy-config`: Requires `--inventory` and `--local-config-path`.
|
|
339
|
-
- `rotate-key`: Requires `--inventory`.
|
|
340
|
-
- `send-file`: Requires `--inventory`, `--local-path`, and `--remote-path`.
|
|
341
|
-
- `receive-file`: Requires `--inventory`, `--remote-path`, and `--local-path-prefix`.
|
|
223
|
+
**Inventory File Example (`inventory.yml`)**:
|
|
342
224
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
225
|
+
```yaml
|
|
226
|
+
all:
|
|
227
|
+
hosts:
|
|
228
|
+
r510:
|
|
229
|
+
ansible_host: 192.168.1.10
|
|
230
|
+
ansible_user: admin
|
|
231
|
+
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
232
|
+
r710:
|
|
233
|
+
ansible_host: 192.168.1.11
|
|
234
|
+
ansible_user: admin
|
|
235
|
+
ansible_ssh_pass: mypassword
|
|
236
|
+
gr1080:
|
|
237
|
+
ansible_host: 192.168.1.14
|
|
238
|
+
ansible_user: admin
|
|
239
|
+
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
|
|
240
|
+
homelab:
|
|
241
|
+
hosts:
|
|
242
|
+
r510:
|
|
243
|
+
ansible_host: 192.168.1.10
|
|
244
|
+
ansible_user: admin
|
|
245
|
+
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
246
|
+
r710:
|
|
247
|
+
ansible_host: 192.168.1.11
|
|
248
|
+
ansible_user: admin
|
|
249
|
+
ansible_ssh_pass: mypassword
|
|
250
|
+
gr1080:
|
|
251
|
+
ansible_host: 192.168.1.14
|
|
252
|
+
ansible_user: admin
|
|
253
|
+
ansible_ssh_private_key_file: "~/.ssh/id_rsa"
|
|
254
|
+
poweredge:
|
|
255
|
+
hosts:
|
|
256
|
+
r510:
|
|
257
|
+
ansible_host: 192.168.1.10
|
|
258
|
+
ansible_user: admin
|
|
259
|
+
ansible_ssh_private_key_file: "~/.ssh/id_ed25519"
|
|
260
|
+
r710:
|
|
261
|
+
ansible_host: 192.168.1.11
|
|
262
|
+
ansible_user: admin
|
|
263
|
+
ansible_ssh_pass: mypassword
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Replace IPs, usernames, and passwords with your actual values.
|
|
350
267
|
|
|
351
|
-
## FastMCP Server
|
|
352
|
-
The FastMCP server exposes the `Tunnel` functionality as AI-accessible tools. Start the server with:
|
|
353
268
|
|
|
269
|
+
### MCP CLI
|
|
270
|
+
|
|
271
|
+
| Short Flag | Long Flag | Description |
|
|
272
|
+
|------------|------------------------------------|-----------------------------------------------------------------------------|
|
|
273
|
+
| -h | --help | Display help information |
|
|
274
|
+
| -t | --transport | Transport method: 'stdio', 'http', or 'sse' [legacy] (default: stdio) |
|
|
275
|
+
| -s | --host | Host address for HTTP transport (default: 0.0.0.0) |
|
|
276
|
+
| -p | --port | Port number for HTTP transport (default: 8000) |
|
|
277
|
+
| | --auth-type | Authentication type: 'none', 'static', 'jwt', 'oauth-proxy', 'oidc-proxy', 'remote-oauth' (default: none) |
|
|
278
|
+
| | --token-jwks-uri | JWKS URI for JWT verification |
|
|
279
|
+
| | --token-issuer | Issuer for JWT verification |
|
|
280
|
+
| | --token-audience | Audience for JWT verification |
|
|
281
|
+
| | --oauth-upstream-auth-endpoint | Upstream authorization endpoint for OAuth Proxy |
|
|
282
|
+
| | --oauth-upstream-token-endpoint | Upstream token endpoint for OAuth Proxy |
|
|
283
|
+
| | --oauth-upstream-client-id | Upstream client ID for OAuth Proxy |
|
|
284
|
+
| | --oauth-upstream-client-secret | Upstream client secret for OAuth Proxy |
|
|
285
|
+
| | --oauth-base-url | Base URL for OAuth Proxy |
|
|
286
|
+
| | --oidc-config-url | OIDC configuration URL |
|
|
287
|
+
| | --oidc-client-id | OIDC client ID |
|
|
288
|
+
| | --oidc-client-secret | OIDC client secret |
|
|
289
|
+
| | --oidc-base-url | Base URL for OIDC Proxy |
|
|
290
|
+
| | --remote-auth-servers | Comma-separated list of authorization servers for Remote OAuth |
|
|
291
|
+
| | --remote-base-url | Base URL for Remote OAuth |
|
|
292
|
+
| | --allowed-client-redirect-uris | Comma-separated list of allowed client redirect URIs |
|
|
293
|
+
| | --eunomia-type | Eunomia authorization type: 'none', 'embedded', 'remote' (default: none) |
|
|
294
|
+
| | --eunomia-policy-file | Policy file for embedded Eunomia (default: mcp_policies.json) |
|
|
295
|
+
| | --eunomia-remote-url | URL for remote Eunomia server |
|
|
296
|
+
|
|
297
|
+
### Using as an MCP Server
|
|
298
|
+
|
|
299
|
+
The MCP Server can be run in two modes: `stdio` (for local testing) or `http` (for networked access). To start the server, use the following commands:
|
|
300
|
+
|
|
301
|
+
#### Run in stdio mode (default):
|
|
354
302
|
```bash
|
|
355
|
-
|
|
303
|
+
tunnel-manager-mcp --transport "stdio"
|
|
356
304
|
```
|
|
357
305
|
|
|
358
|
-
|
|
306
|
+
#### Run in HTTP mode:
|
|
359
307
|
```bash
|
|
360
|
-
|
|
308
|
+
tunnel-manager-mcp --transport "http" --host "0.0.0.0" --port "8000"
|
|
361
309
|
```
|
|
362
310
|
|
|
363
|
-
|
|
311
|
+
### Tunnel Class
|
|
312
|
+
The `Tunnel` class can be used standalone for SSH operations. Examples:
|
|
364
313
|
|
|
365
|
-
|
|
366
|
-
|
|
314
|
+
#### Using RSA Keys
|
|
315
|
+
```python
|
|
316
|
+
from tunnel_manager.tunnel_manager import Tunnel
|
|
367
317
|
|
|
368
|
-
|
|
318
|
+
# Initialize with a remote host (assumes ~/.ssh/config or explicit params)
|
|
319
|
+
tunnel = Tunnel(
|
|
320
|
+
remote_host="192.168.1.10",
|
|
321
|
+
username="admin",
|
|
322
|
+
password="mypassword",
|
|
323
|
+
identity_file="/path/to/id_rsa",
|
|
324
|
+
certificate_file="/path/to/cert", # Optional for Teleport
|
|
325
|
+
proxy_command="tsh proxy ssh %h", # Optional for Teleport
|
|
326
|
+
ssh_config_file="~/.ssh/config",
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
# Connect and run a command
|
|
330
|
+
tunnel.connect()
|
|
331
|
+
out, err = tunnel.run_command("ls -la /tmp")
|
|
332
|
+
print(f"Output: {out}\nError: {err}")
|
|
333
|
+
|
|
334
|
+
# Upload a file
|
|
335
|
+
tunnel.send_file("/local/file.txt", "/remote/file.txt")
|
|
336
|
+
|
|
337
|
+
# Download a file
|
|
338
|
+
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
|
|
339
|
+
|
|
340
|
+
# Setup passwordless SSH with RSA
|
|
341
|
+
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_rsa", key_type="rsa")
|
|
342
|
+
|
|
343
|
+
# Copy SSH config
|
|
344
|
+
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
|
|
345
|
+
|
|
346
|
+
# Rotate SSH key with RSA
|
|
347
|
+
tunnel.rotate_ssh_key("/path/to/new_rsa_key", key_type="rsa")
|
|
348
|
+
|
|
349
|
+
# Close the connection
|
|
350
|
+
tunnel.close()
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Using Ed25519 Keys
|
|
354
|
+
```python
|
|
355
|
+
from tunnel_manager.tunnel_manager import Tunnel
|
|
356
|
+
|
|
357
|
+
# Initialize with a remote host (assumes ~/.ssh/config or explicit params)
|
|
358
|
+
tunnel = Tunnel(
|
|
359
|
+
remote_host="192.168.1.10",
|
|
360
|
+
username="admin",
|
|
361
|
+
password="mypassword",
|
|
362
|
+
identity_file="/path/to/id_ed25519",
|
|
363
|
+
certificate_file="/path/to/cert", # Optional for Teleport
|
|
364
|
+
proxy_command="tsh proxy ssh %h", # Optional for Teleport
|
|
365
|
+
ssh_config_file="~/.ssh/config",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# Connect and run a command
|
|
369
|
+
tunnel.connect()
|
|
370
|
+
out, err = tunnel.run_command("ls -la /tmp")
|
|
371
|
+
print(f"Output: {out}\nError: {err}")
|
|
372
|
+
|
|
373
|
+
# Upload a file
|
|
374
|
+
tunnel.send_file("/local/file.txt", "/remote/file.txt")
|
|
375
|
+
|
|
376
|
+
# Download a file
|
|
377
|
+
tunnel.receive_file("/remote/file.txt", "/local/downloaded.txt")
|
|
378
|
+
|
|
379
|
+
# Setup passwordless SSH with Ed25519
|
|
380
|
+
tunnel.setup_passwordless_ssh(local_key_path="~/.ssh/id_ed25519", key_type="ed25519")
|
|
381
|
+
|
|
382
|
+
# Copy SSH config
|
|
383
|
+
tunnel.copy_ssh_config("/local/ssh_config", "~/.ssh/config")
|
|
384
|
+
|
|
385
|
+
# Rotate SSH key with Ed25519
|
|
386
|
+
tunnel.rotate_ssh_key("/path/to/new_ed25519_key", key_type="ed25519")
|
|
387
|
+
|
|
388
|
+
# Close the connection
|
|
389
|
+
tunnel.close()
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Deploy MCP Server as a Service
|
|
393
|
+
|
|
394
|
+
The MCP server can be deployed using Docker, with configurable authentication, middleware, and Eunomia authorization.
|
|
395
|
+
|
|
396
|
+
#### Using Docker Run
|
|
397
|
+
|
|
398
|
+
```bash
|
|
399
|
+
docker pull knucklessg1/tunnel-manager:latest
|
|
400
|
+
|
|
401
|
+
docker run -d \
|
|
402
|
+
--name tunnel-manager-mcp \
|
|
403
|
+
-p 8004:8004 \
|
|
404
|
+
-e HOST=0.0.0.0 \
|
|
405
|
+
-e PORT=8004 \
|
|
406
|
+
-e TRANSPORT=http \
|
|
407
|
+
-e AUTH_TYPE=none \
|
|
408
|
+
-e EUNOMIA_TYPE=none \
|
|
409
|
+
knucklessg1/tunnel-manager:latest
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
For advanced authentication (e.g., JWT, OAuth Proxy, OIDC Proxy, Remote OAuth) or Eunomia, add the relevant environment variables:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
docker run -d \
|
|
416
|
+
--name tunnel-manager-mcp \
|
|
417
|
+
-p 8004:8004 \
|
|
418
|
+
-e HOST=0.0.0.0 \
|
|
419
|
+
-e PORT=8004 \
|
|
420
|
+
-e TRANSPORT=http \
|
|
421
|
+
-e AUTH_TYPE=oidc-proxy \
|
|
422
|
+
-e OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration \
|
|
423
|
+
-e OIDC_CLIENT_ID=your-client-id \
|
|
424
|
+
-e OIDC_CLIENT_SECRET=your-client-secret \
|
|
425
|
+
-e OIDC_BASE_URL=https://your-server.com \
|
|
426
|
+
-e ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/* \
|
|
427
|
+
-e EUNOMIA_TYPE=embedded \
|
|
428
|
+
-e EUNOMIA_POLICY_FILE=/app/mcp_policies.json \
|
|
429
|
+
knucklessg1/tunnel-manager:latest
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
#### Using Docker Compose
|
|
433
|
+
|
|
434
|
+
Create a `docker-compose.yml` file:
|
|
435
|
+
|
|
436
|
+
```yaml
|
|
437
|
+
services:
|
|
438
|
+
tunnel-manager-mcp:
|
|
439
|
+
image: knucklessg1/tunnel-manager:latest
|
|
440
|
+
environment:
|
|
441
|
+
- HOST=0.0.0.0
|
|
442
|
+
- PORT=8004
|
|
443
|
+
- TRANSPORT=http
|
|
444
|
+
- AUTH_TYPE=none
|
|
445
|
+
- EUNOMIA_TYPE=none
|
|
446
|
+
ports:
|
|
447
|
+
- 8004:8004
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
For advanced setups with authentication and Eunomia:
|
|
451
|
+
|
|
452
|
+
```yaml
|
|
453
|
+
services:
|
|
454
|
+
tunnel-manager-mcp:
|
|
455
|
+
image: knucklessg1/tunnel-manager:latest
|
|
456
|
+
environment:
|
|
457
|
+
- HOST=0.0.0.0
|
|
458
|
+
- PORT=8004
|
|
459
|
+
- TRANSPORT=http
|
|
460
|
+
- AUTH_TYPE=oidc-proxy
|
|
461
|
+
- OIDC_CONFIG_URL=https://provider.com/.well-known/openid-configuration
|
|
462
|
+
- OIDC_CLIENT_ID=your-client-id
|
|
463
|
+
- OIDC_CLIENT_SECRET=your-client-secret
|
|
464
|
+
- OIDC_BASE_URL=https://your-server.com
|
|
465
|
+
- ALLOWED_CLIENT_REDIRECT_URIS=http://localhost:*,https://*.example.com/*
|
|
466
|
+
- EUNOMIA_TYPE=embedded
|
|
467
|
+
- EUNOMIA_POLICY_FILE=/app/mcp_policies.json
|
|
468
|
+
ports:
|
|
469
|
+
- 8004:8004
|
|
470
|
+
volumes:
|
|
471
|
+
- ./mcp_policies.json:/app/mcp_policies.json
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
Run the service:
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
docker-compose up -d
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
#### Configure `mcp.json` for AI Integration
|
|
369
481
|
|
|
370
|
-
Configure `mcp.json`
|
|
371
482
|
```json
|
|
372
483
|
{
|
|
373
484
|
"mcpServers": {
|
|
@@ -398,23 +509,10 @@ Configure `mcp.json`
|
|
|
398
509
|
}
|
|
399
510
|
}
|
|
400
511
|
```
|
|
512
|
+
</details>
|
|
401
513
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
docker pull knucklessg1/tunnel-manager:latest
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
Modify the `compose.yml`
|
|
408
|
-
```yaml
|
|
409
|
-
services:
|
|
410
|
-
tunnel-manager:
|
|
411
|
-
image: knucklessg1/tunnel-manager:latest
|
|
412
|
-
environment:
|
|
413
|
-
- HOST=0.0.0.0
|
|
414
|
-
- PORT=8021
|
|
415
|
-
ports:
|
|
416
|
-
- 8021:8021
|
|
417
|
-
```
|
|
514
|
+
<details>
|
|
515
|
+
<summary><b>Installation Instructions:</b></summary>
|
|
418
516
|
|
|
419
517
|
### Install Python Package
|
|
420
518
|
```bash
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
tests/test_tunnel.py,sha256=Dj6z1EM50uGqEJA0GBDP6L8ZPUMRAJNT7dAnwkuSgNU,2461
|
|
2
|
+
tunnel_manager/__init__.py,sha256=cqAitAkoCcEkaCQcP7Y8tngiUK7pU6SIMlmpABShh9g,807
|
|
3
|
+
tunnel_manager/__main__.py,sha256=Z1uxNLjwIjJpvu97bXrvsawnghJScA52E2wtAgg5MLo,152
|
|
4
|
+
tunnel_manager/tunnel_manager.py,sha256=DZn2Zs0OPxB_2wWqkro--UbFLdoe8kivaeLvKZKWANM,39384
|
|
5
|
+
tunnel_manager/tunnel_manager_mcp.py,sha256=W8glkvrtD0jxdddkOgEAz4YZMoKfqOYsNB2jdrTrqSY,84492
|
|
6
|
+
tunnel_manager-1.0.5.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
|
7
|
+
tunnel_manager-1.0.5.dist-info/METADATA,sha256=6B1XWaT318I_xGl2cxwbnuJTztrOmC88fynozPQI_P4,25585
|
|
8
|
+
tunnel_manager-1.0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
tunnel_manager-1.0.5.dist-info/entry_points.txt,sha256=hYtm4jvOAew8CbeqqUBH2nXY51mSQwhF4GhU0yclV6c,154
|
|
10
|
+
tunnel_manager-1.0.5.dist-info/top_level.txt,sha256=W4J-lyPPNeOS696f0LneZsP2MVERR8HE9UHbAFQ550A,21
|
|
11
|
+
tunnel_manager-1.0.5.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
tests/test_tunnel.py,sha256=Dj6z1EM50uGqEJA0GBDP6L8ZPUMRAJNT7dAnwkuSgNU,2461
|
|
2
|
-
tunnel_manager/__init__.py,sha256=cqAitAkoCcEkaCQcP7Y8tngiUK7pU6SIMlmpABShh9g,807
|
|
3
|
-
tunnel_manager/__main__.py,sha256=Z1uxNLjwIjJpvu97bXrvsawnghJScA52E2wtAgg5MLo,152
|
|
4
|
-
tunnel_manager/tunnel_manager.py,sha256=DZn2Zs0OPxB_2wWqkro--UbFLdoe8kivaeLvKZKWANM,39384
|
|
5
|
-
tunnel_manager/tunnel_manager_mcp.py,sha256=_TVDIH7fZpNsADbCKpMU8uzTUU41l9YqKNuK6dvaxl4,76157
|
|
6
|
-
tunnel_manager-1.0.3.dist-info/licenses/LICENSE,sha256=Z1xmcrPHBnGCETO_LLQJUeaSNBSnuptcDVTt4kaPUOE,1060
|
|
7
|
-
tunnel_manager-1.0.3.dist-info/METADATA,sha256=Es2jT6ZIlWs1miyKjAw6mK3K0V59ZkLIHtR81YFVh10,20683
|
|
8
|
-
tunnel_manager-1.0.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
tunnel_manager-1.0.3.dist-info/entry_points.txt,sha256=hYtm4jvOAew8CbeqqUBH2nXY51mSQwhF4GhU0yclV6c,154
|
|
10
|
-
tunnel_manager-1.0.3.dist-info/top_level.txt,sha256=W4J-lyPPNeOS696f0LneZsP2MVERR8HE9UHbAFQ550A,21
|
|
11
|
-
tunnel_manager-1.0.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|