zscams 2.0.1__py2.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.
- zscams/__init__.py +0 -0
- zscams/__main__.py +101 -0
- zscams/agent/__init__.py +0 -0
- zscams/agent/certificates/.gitkeep +0 -0
- zscams/agent/config.yaml +121 -0
- zscams/agent/keys/autoport.key +27 -0
- zscams/agent/src/__init__.py +0 -0
- zscams/agent/src/core/__init__.py +1 -0
- zscams/agent/src/core/api/backend/bootstrap.py +21 -0
- zscams/agent/src/core/api/backend/client.py +242 -0
- zscams/agent/src/core/api/backend/exceptions.py +10 -0
- zscams/agent/src/core/api/backend/update_machine_info.py +16 -0
- zscams/agent/src/core/prerequisites.py +36 -0
- zscams/agent/src/core/service_health_check.py +49 -0
- zscams/agent/src/core/services.py +86 -0
- zscams/agent/src/core/tunnel/__init__.py +144 -0
- zscams/agent/src/core/tunnel/tls.py +56 -0
- zscams/agent/src/core/tunnels.py +55 -0
- zscams/agent/src/services/__init__.py +0 -0
- zscams/agent/src/services/reverse_ssh.py +73 -0
- zscams/agent/src/services/ssh_forwarder.py +75 -0
- zscams/agent/src/services/system_monitor.py +264 -0
- zscams/agent/src/support/__init__.py +0 -0
- zscams/agent/src/support/configuration.py +53 -0
- zscams/agent/src/support/filesystem.py +41 -0
- zscams/agent/src/support/logger.py +40 -0
- zscams/agent/src/support/mac.py +18 -0
- zscams/agent/src/support/network.py +49 -0
- zscams/agent/src/support/openssl.py +100 -0
- zscams/libs/getmac/__init__.py +3 -0
- zscams/libs/getmac/__main__.py +123 -0
- zscams/libs/getmac/getmac.py +1900 -0
- zscams/libs/getmac/shutilwhich.py +67 -0
- zscams-2.0.1.dist-info/METADATA +118 -0
- zscams-2.0.1.dist-info/RECORD +37 -0
- zscams-2.0.1.dist-info/WHEEL +4 -0
- zscams-2.0.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Service launcher utilities for TLS Tunnel Client
|
|
3
|
+
|
|
4
|
+
- Generic solution: any service can receive parameters via `SERVICE_PARAMS` env variable
|
|
5
|
+
- Starts services asynchronously using asyncio
|
|
6
|
+
- Supports both Python scripts and executables
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from zscams.agent.src.support.logger import get_logger
|
|
13
|
+
from zscams.agent.src.core.prerequisites import check_prerequisites
|
|
14
|
+
from zscams.agent.src.core import running_services
|
|
15
|
+
|
|
16
|
+
logger = get_logger("service_launcher")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def start_service(service_cfg, config_dir=None):
|
|
20
|
+
"""
|
|
21
|
+
Launch an external script/service asynchronously.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
service_cfg (dict): Service configuration from config.yaml
|
|
25
|
+
- name: service name
|
|
26
|
+
- script: path to script/executable
|
|
27
|
+
- port: associated port (optional)
|
|
28
|
+
- args: list of extra arguments
|
|
29
|
+
- params: dict of arbitrary parameters (SERVICE_PARAMS)
|
|
30
|
+
config_dir (str, optional): Base directory to resolve relative script paths
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
asyncio.subprocess.Process or None
|
|
34
|
+
"""
|
|
35
|
+
base_dir = config_dir or os.getcwd()
|
|
36
|
+
script_path = os.path.join(base_dir, service_cfg["script"])
|
|
37
|
+
prereqs = service_cfg.get("prerequisites")
|
|
38
|
+
name = service_cfg["name"]
|
|
39
|
+
# Wait for prerequisites before starting
|
|
40
|
+
logger.info("Checking prerequisites for %s...", name)
|
|
41
|
+
ok = check_prerequisites(prereqs)
|
|
42
|
+
if not ok:
|
|
43
|
+
logger.error("Prerequisites not met for %s. Skipping start.", name)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if not os.path.exists(script_path):
|
|
47
|
+
logger.error("Service script not found: %s", script_path)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
env = os.environ.copy()
|
|
51
|
+
params = service_cfg.get("params")
|
|
52
|
+
if params:
|
|
53
|
+
# Pass generic parameters to the service via JSON environment variable
|
|
54
|
+
env["SERVICE_PARAMS"] = json.dumps(params)
|
|
55
|
+
|
|
56
|
+
cmd = ["python", script_path] + service_cfg.get("args", [])
|
|
57
|
+
logger.info(
|
|
58
|
+
"Starting service %s on port %d: %s",
|
|
59
|
+
service_cfg.get("name"),
|
|
60
|
+
service_cfg.get("port", 0),
|
|
61
|
+
cmd,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
process = await asyncio.create_subprocess_exec(*cmd, env=env)
|
|
66
|
+
running_services.add(name)
|
|
67
|
+
return process
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error("Failed to start service %s: %s", service_cfg.get("name"), e)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def start_all_services(services_cfg_list, config_dir=None):
|
|
74
|
+
"""
|
|
75
|
+
Start all services defined in the configuration concurrently.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
services_cfg_list (list): List of service configuration dictionaries
|
|
79
|
+
config_dir (str, optional): Base directory to resolve relative script paths
|
|
80
|
+
"""
|
|
81
|
+
tasks = []
|
|
82
|
+
for svc in services_cfg_list:
|
|
83
|
+
tasks.append(start_service(svc, config_dir=config_dir))
|
|
84
|
+
|
|
85
|
+
# Wait for all services to start
|
|
86
|
+
await asyncio.gather(*tasks)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Forwarding utilities for TLS Tunnel Client with:
|
|
3
|
+
- Auto-reconnect with exponential backoff
|
|
4
|
+
- Health checks
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from zscams.agent.src.support.logger import get_logger
|
|
9
|
+
|
|
10
|
+
READ_BUFFER_SIZE = 4096
|
|
11
|
+
|
|
12
|
+
logger = get_logger("tls_forward")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def forward(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
|
16
|
+
"""
|
|
17
|
+
Forward data from reader to writer until connection closes.
|
|
18
|
+
Handles Windows-specific connection resets gracefully.
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
while True:
|
|
22
|
+
data = await reader.read(READ_BUFFER_SIZE)
|
|
23
|
+
if not data:
|
|
24
|
+
break
|
|
25
|
+
writer.write(data)
|
|
26
|
+
await writer.drain()
|
|
27
|
+
except (ConnectionResetError, OSError) as err:
|
|
28
|
+
# Handle Windows WinError 64 and other disconnects gracefully
|
|
29
|
+
logger.debug("Connection closed by peer (expected): %s", err)
|
|
30
|
+
except Exception as err:
|
|
31
|
+
logger.error("Unexpected forwarding error: %s", err)
|
|
32
|
+
finally:
|
|
33
|
+
try:
|
|
34
|
+
writer.close()
|
|
35
|
+
await writer.wait_closed()
|
|
36
|
+
except Exception:
|
|
37
|
+
pass # ignore errors on close
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def handle_client(
|
|
41
|
+
local_reader, local_writer, remote_host, remote_port, sni_hostname, ssl_context
|
|
42
|
+
): # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
43
|
+
"""
|
|
44
|
+
Forward a single local connection to the remote TLS server with SNI.
|
|
45
|
+
Handles abrupt disconnects gracefully.
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
remote_reader, remote_writer = await asyncio.open_connection(
|
|
49
|
+
host=remote_host,
|
|
50
|
+
port=remote_port,
|
|
51
|
+
ssl=ssl_context,
|
|
52
|
+
server_hostname=sni_hostname,
|
|
53
|
+
)
|
|
54
|
+
logger.debug(
|
|
55
|
+
"New connection: local -> %s:%s (SNI=%s)",
|
|
56
|
+
remote_host,
|
|
57
|
+
remote_port,
|
|
58
|
+
sni_hostname,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
await asyncio.gather(
|
|
62
|
+
forward(local_reader, remote_writer), forward(remote_reader, local_writer)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
except (ConnectionResetError, OSError) as e:
|
|
66
|
+
logger.debug("Connection closed unexpectedly: %s", e)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
logger.error("Unexpected client handling error: %s", e)
|
|
69
|
+
finally:
|
|
70
|
+
try:
|
|
71
|
+
local_writer.close()
|
|
72
|
+
await local_writer.wait_closed()
|
|
73
|
+
logger.debug(
|
|
74
|
+
f"Connection closed: local -> {remote_host}:{remote_port} (SNI={sni_hostname})"
|
|
75
|
+
)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
logger.error("Unexpected client handling error: %s", e)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def start_tunnel(
|
|
81
|
+
local_port,
|
|
82
|
+
remote_host,
|
|
83
|
+
remote_port,
|
|
84
|
+
sni_hostname,
|
|
85
|
+
ssl_context,
|
|
86
|
+
reconnect_max_delay=60,
|
|
87
|
+
ready_event=None,
|
|
88
|
+
): # pylint: disable=too-many-arguments,too-many-positional-arguments
|
|
89
|
+
"""
|
|
90
|
+
Start a TCP listener on local_port and forward connections to the remote TLS server.
|
|
91
|
+
Automatically reconnects on failure with exponential backoff and performs periodic health checks.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
local_port (int)
|
|
95
|
+
remote_host (str)
|
|
96
|
+
remote_port (int)
|
|
97
|
+
sni_hostname (str)
|
|
98
|
+
ssl_context (ssl.SSLContext)
|
|
99
|
+
reconnect_max_delay (int): Maximum delay between reconnect attempts
|
|
100
|
+
"""
|
|
101
|
+
backoff = 1 # initial delay in seconds
|
|
102
|
+
|
|
103
|
+
while True:
|
|
104
|
+
try:
|
|
105
|
+
server = await asyncio.start_server(
|
|
106
|
+
lambda r, w: handle_client(
|
|
107
|
+
r, w, remote_host, remote_port, sni_hostname, ssl_context
|
|
108
|
+
),
|
|
109
|
+
"127.0.0.1",
|
|
110
|
+
local_port,
|
|
111
|
+
)
|
|
112
|
+
addr = server.sockets[0].getsockname()
|
|
113
|
+
logger.info(
|
|
114
|
+
"Listening on :%s -> %s:%s (SNI=%s)",
|
|
115
|
+
addr[1],
|
|
116
|
+
remote_host,
|
|
117
|
+
remote_port,
|
|
118
|
+
sni_hostname,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Health check task: logs tunnel is alive every 30 seconds
|
|
122
|
+
async def health_check():
|
|
123
|
+
while True:
|
|
124
|
+
await asyncio.sleep(600)
|
|
125
|
+
logger.info(
|
|
126
|
+
"Tunnel on local port %s is healthy and running", local_port
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
asyncio.create_task(health_check())
|
|
130
|
+
|
|
131
|
+
# Signal ready if event is provided
|
|
132
|
+
if ready_event:
|
|
133
|
+
ready_event.set()
|
|
134
|
+
|
|
135
|
+
async with server:
|
|
136
|
+
await server.serve_forever()
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.error("Tunnel error on local port %s: %s", local_port, e)
|
|
140
|
+
logger.info("Reconnecting in %s seconds...", backoff)
|
|
141
|
+
await asyncio.sleep(backoff)
|
|
142
|
+
backoff = min(backoff * 2, reconnect_max_delay) # exponential backoff
|
|
143
|
+
else:
|
|
144
|
+
backoff = 1 # reset backoff if successful
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import ssl
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from zscams.agent.src.support.configuration import RemoteConfig
|
|
5
|
+
from zscams.agent.src.support.filesystem import resolve_path
|
|
6
|
+
from zscams.agent.src.support.logger import get_logger
|
|
7
|
+
|
|
8
|
+
logger = get_logger("tls_utils")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_ssl_context(
|
|
12
|
+
remote_cfg: RemoteConfig, config_dir: Optional[str] = None
|
|
13
|
+
) -> ssl.SSLContext:
|
|
14
|
+
"""
|
|
15
|
+
Create an SSL context for TLS connections based on configuration.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
remote_cfg (dict): Remote server configuration including:
|
|
19
|
+
- verify_cert (bool)
|
|
20
|
+
- ca_cert (str)
|
|
21
|
+
- client_cert (str)
|
|
22
|
+
- client_key (str)
|
|
23
|
+
config_dir (str, optional): Directory to resolve relative paths from.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
ssl.SSLContext: Configured SSL context
|
|
27
|
+
"""
|
|
28
|
+
context = ssl.create_default_context()
|
|
29
|
+
|
|
30
|
+
ca_cert = resolve_path(remote_cfg.get("ca_cert"), config_dir)
|
|
31
|
+
client_cert = resolve_path(remote_cfg.get("client_cert"), config_dir)
|
|
32
|
+
client_key = resolve_path(remote_cfg.get("client_key"), config_dir)
|
|
33
|
+
|
|
34
|
+
if not remote_cfg.get("verify_cert", True):
|
|
35
|
+
context.check_hostname = False
|
|
36
|
+
context.verify_mode = ssl.CERT_NONE
|
|
37
|
+
logger.warning("Certificate verification is disabled!")
|
|
38
|
+
else:
|
|
39
|
+
if ca_cert and os.path.exists(ca_cert):
|
|
40
|
+
context.load_verify_locations(cafile=ca_cert)
|
|
41
|
+
logger.info("Loaded CA certificate from %s", ca_cert)
|
|
42
|
+
else:
|
|
43
|
+
logger.warning("CA certificate not found or not provided: %s", ca_cert)
|
|
44
|
+
|
|
45
|
+
if client_cert and client_key:
|
|
46
|
+
if os.path.exists(client_cert) and os.path.exists(client_key):
|
|
47
|
+
context.load_cert_chain(certfile=client_cert, keyfile=client_key)
|
|
48
|
+
logger.info(
|
|
49
|
+
"Loaded client certificate/key: %s / %s", client_cert, client_key
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
raise FileNotFoundError(
|
|
53
|
+
f"Client certificate or key not found: {client_cert}, {client_key}"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return context
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities to start TLS tunnels asynchronously
|
|
3
|
+
|
|
4
|
+
- Supports multiple forwards
|
|
5
|
+
- Signals readiness via asyncio.Event
|
|
6
|
+
- Auto-reconnect and logging
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from zscams.agent.src.core.tunnel import start_tunnel
|
|
11
|
+
from zscams.agent.src.support.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger("tunnel_launcher")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def start_all_tunnels(forwards_cfg_list, remote_host, remote_port, ssl_context):
|
|
17
|
+
"""
|
|
18
|
+
Start all TLS tunnels concurrently and wait for readiness.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
forwards_cfg_list (list): List of forward dictionaries from config
|
|
22
|
+
Each dict should have:
|
|
23
|
+
- local_port
|
|
24
|
+
- sni_hostname
|
|
25
|
+
remote_host (str): Remote host to connect to
|
|
26
|
+
remote_port (int): Remote port to connect to
|
|
27
|
+
ssl_context (ssl.SSLContext): SSL context for the TLS tunnel
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
list of asyncio.Tasks: tunnel tasks running indefinitely
|
|
31
|
+
"""
|
|
32
|
+
tasks = []
|
|
33
|
+
ready_events = []
|
|
34
|
+
|
|
35
|
+
for forward_cfg in forwards_cfg_list:
|
|
36
|
+
ready_event = asyncio.Event()
|
|
37
|
+
ready_events.append(ready_event)
|
|
38
|
+
tasks.append(
|
|
39
|
+
asyncio.create_task(
|
|
40
|
+
start_tunnel(
|
|
41
|
+
local_port=forward_cfg["local_port"],
|
|
42
|
+
remote_host=remote_host,
|
|
43
|
+
remote_port=remote_port,
|
|
44
|
+
sni_hostname=forward_cfg["sni_hostname"],
|
|
45
|
+
ssl_context=ssl_context,
|
|
46
|
+
reconnect_max_delay=60,
|
|
47
|
+
ready_event=ready_event,
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Wait for all tunnels to signal readiness
|
|
53
|
+
await asyncio.gather(*(event.wait() for event in ready_events))
|
|
54
|
+
logger.info("[*] %d tunnel(s) ready", len(tasks))
|
|
55
|
+
return tasks
|
|
File without changes
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from zscams.agent.src.support.logger import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger("autossh_service")
|
|
8
|
+
|
|
9
|
+
# Load service-specific params from environment
|
|
10
|
+
params_env = os.environ.get("SERVICE_PARAMS")
|
|
11
|
+
if not params_env:
|
|
12
|
+
logger.error("SERVICE_PARAMS environment variable not set")
|
|
13
|
+
sys.exit(1)
|
|
14
|
+
|
|
15
|
+
params = json.loads(params_env)
|
|
16
|
+
|
|
17
|
+
LOCAL_PORT = params.get("local_port", 4422)
|
|
18
|
+
SERVER_SSH_USER = params.get("server_ssh_user", "ssh_user")
|
|
19
|
+
REVERSE_PORT = params.get("reverse_port", 2222)
|
|
20
|
+
PRIVATE_KEY = params.get("private_key") # Path to RSA key
|
|
21
|
+
SSH_OPTIONS = params.get("ssh_options", [])
|
|
22
|
+
CHECK_INTERVAL = 120 #
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
async def run():
|
|
26
|
+
backoff = 1
|
|
27
|
+
max_backoff = 60
|
|
28
|
+
ssh_cmd = [
|
|
29
|
+
"ssh",
|
|
30
|
+
"-p",
|
|
31
|
+
f"{LOCAL_PORT}",
|
|
32
|
+
"-R",
|
|
33
|
+
f"*:{REVERSE_PORT}:localhost:22",
|
|
34
|
+
f"{SERVER_SSH_USER}@localhost",
|
|
35
|
+
"-N",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Use key file if provided
|
|
39
|
+
if PRIVATE_KEY:
|
|
40
|
+
ssh_cmd += ["-i", PRIVATE_KEY]
|
|
41
|
+
|
|
42
|
+
# Add additional SSH options
|
|
43
|
+
ssh_cmd += SSH_OPTIONS
|
|
44
|
+
|
|
45
|
+
logger.info(f"Starting reverse SSH tunnel: {' '.join(ssh_cmd)}")
|
|
46
|
+
|
|
47
|
+
while True:
|
|
48
|
+
try:
|
|
49
|
+
process = await asyncio.create_subprocess_exec(
|
|
50
|
+
*ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
51
|
+
)
|
|
52
|
+
logger.info(f"SSH tunnel started (PID={process.pid})")
|
|
53
|
+
stdout, stderr = await process.communicate()
|
|
54
|
+
if stdout:
|
|
55
|
+
logger.info(stdout.decode())
|
|
56
|
+
if stderr:
|
|
57
|
+
logger.warning(stderr.decode())
|
|
58
|
+
returncode = process.returncode
|
|
59
|
+
logger.warning(f"SSH tunnel exited with code {returncode}")
|
|
60
|
+
except Exception as e:
|
|
61
|
+
logger.error(f"SSH tunnel failed: {e}")
|
|
62
|
+
|
|
63
|
+
logger.info(f"Reconnecting in {backoff} seconds...")
|
|
64
|
+
await asyncio.sleep(backoff)
|
|
65
|
+
backoff = min(backoff * 2, max_backoff)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
try:
|
|
70
|
+
asyncio.run(run())
|
|
71
|
+
except KeyboardInterrupt:
|
|
72
|
+
logger.info("Exiting autossh_service.py")
|
|
73
|
+
sys.exit(0)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from zscams.agent.src.support.logger import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger("autossh_service")
|
|
8
|
+
|
|
9
|
+
# Load service-specific params from environment
|
|
10
|
+
params_env = os.environ.get("SERVICE_PARAMS")
|
|
11
|
+
if not params_env:
|
|
12
|
+
logger.error("SERVICE_PARAMS environment variable not set")
|
|
13
|
+
sys.exit(1)
|
|
14
|
+
|
|
15
|
+
params = json.loads(params_env)
|
|
16
|
+
|
|
17
|
+
FORWARDER_PORT = params.get("forwarder_port", 4422)
|
|
18
|
+
LOCAL_PORT = params.get("local_port", 4422)
|
|
19
|
+
REMOTE_PORT = params.get("remote_port", 4422)
|
|
20
|
+
REMOTE_HOST = params.get("remote_host", "localhost")
|
|
21
|
+
SERVER_SSH_USER = params.get("server_ssh_user", "ssh_user")
|
|
22
|
+
PRIVATE_KEY = params.get("private_key") # Path to RSA key
|
|
23
|
+
SSH_OPTIONS = params.get("ssh_options", [])
|
|
24
|
+
CHECK_INTERVAL = 120 #
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def run():
|
|
28
|
+
backoff = 1
|
|
29
|
+
max_backoff = 60
|
|
30
|
+
ssh_cmd = [
|
|
31
|
+
"ssh",
|
|
32
|
+
"-p",
|
|
33
|
+
f"{LOCAL_PORT}",
|
|
34
|
+
"-L",
|
|
35
|
+
f"{FORWARDER_PORT}:{REMOTE_HOST}:{REMOTE_PORT}",
|
|
36
|
+
f"{SERVER_SSH_USER}@localhost",
|
|
37
|
+
"-N", # No command execution
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Use key file if provided
|
|
41
|
+
if PRIVATE_KEY:
|
|
42
|
+
ssh_cmd += ["-i", PRIVATE_KEY]
|
|
43
|
+
|
|
44
|
+
# Add additional SSH options
|
|
45
|
+
ssh_cmd += SSH_OPTIONS
|
|
46
|
+
|
|
47
|
+
logger.info(f"Starting reverse SSH tunnel: {' '.join(ssh_cmd)}")
|
|
48
|
+
|
|
49
|
+
while True:
|
|
50
|
+
try:
|
|
51
|
+
process = await asyncio.create_subprocess_exec(
|
|
52
|
+
*ssh_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
53
|
+
)
|
|
54
|
+
logger.info(f"SSH tunnel started (PID={process.pid})")
|
|
55
|
+
stdout, stderr = await process.communicate()
|
|
56
|
+
if stdout:
|
|
57
|
+
logger.info(stdout.decode())
|
|
58
|
+
if stderr:
|
|
59
|
+
logger.warning(stderr.decode())
|
|
60
|
+
returncode = process.returncode
|
|
61
|
+
logger.warning(f"SSH tunnel exited with code {returncode}")
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error(f"SSH tunnel failed: {e}")
|
|
64
|
+
|
|
65
|
+
logger.info(f"Reconnecting in {backoff} seconds...")
|
|
66
|
+
await asyncio.sleep(backoff)
|
|
67
|
+
backoff = min(backoff * 2, max_backoff)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
try:
|
|
72
|
+
asyncio.run(run())
|
|
73
|
+
except KeyboardInterrupt:
|
|
74
|
+
logger.info("Exiting autossh_service.py")
|
|
75
|
+
sys.exit(0)
|