Skip to content

Commit 54b04a7

Browse files
committed
Add support for ssh tunnelling to a jump server
This allows one to tunnel to a jumpserver right from the toolbox.
1 parent 607625d commit 54b04a7

File tree

6 files changed

+130
-1
lines changed

6 files changed

+130
-1
lines changed

Makefile.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ cd standalone-py/python/install
337337
cp . ../../../py39
338338
cd ../../../
339339
exec --fail-on-error ${PYTHON} ./get-pip.py
340-
exec --fail-on-error ${PYTHON} -m pip install wheel flit . ".[test]"
340+
exec --fail-on-error ${PYTHON} -m pip install wheel flit . ".[test]" ".[ssh-tunnel]"
341341
cm_run_task generate-resources
342342
'''
343343

console_backend/src/cli_options.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,14 @@ pub struct CliOptions {
203203
/// Enable QML Debugging and profiling.
204204
#[clap(long, hide = true)]
205205
pub qmldebug: bool,
206+
207+
/// SSH tunnel to a jumphost specified ([username]:[password]@some.fqdn)
208+
#[clap(long, hide = true)]
209+
pub ssh_tunnel: Option<PathBuf>,
210+
211+
/// SSH tunnel forward port of remote IP and port to localhost (some.fqdn:port)
212+
#[clap(long, hide = true)]
213+
pub ssh_remote_bind_address: Option<PathBuf>,
206214
}
207215

208216
impl CliOptions {

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ test = [
1919
"py2many >=0.3",
2020
"types-requests ~= 2.28.11",
2121
]
22+
ssh-tunnel = ["sshtunnel >= 0.4.0"]
2223

2324
[build-system]
2425
requires = [

swift-toolbox.pyproject

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"swiftnav_console/solution_position_tab.py",
2424
"swiftnav_console/solution_table.py",
2525
"swiftnav_console/solution_velocity_tab.py",
26+
"swiftnav_console/ssh_tunnel.py",
2627
"swiftnav_console/status_bar.py",
2728
"swiftnav_console/tracking_signals_tab.py",
2829
"swiftnav_console/tracking_sky_plot_tab.py",

swiftnav_console/main.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@
99
import sys
1010
import time
1111

12+
try:
13+
import sshtunnel # type: ignore # pylint: disable=unused-import
14+
from . import ssh_tunnel
15+
16+
FEATURE_SSHTUNNEL = True
17+
except ImportError:
18+
FEATURE_SSHTUNNEL = False
19+
1220
from typing import Optional, Tuple
1321

1422
import capnp # type: ignore
@@ -639,6 +647,11 @@ def handle_cli_arguments(args: argparse.Namespace, globals_: QObject):
639647
globals_.setProperty("width", args.width) # type: ignore
640648
if args.show_file_connection:
641649
globals_.setProperty("showFileConnection", True) # type: ignore
650+
try:
651+
if args.ssh_tunnel:
652+
ssh_tunnel.setup(args.ssh_tunnel, args.ssh_remote_bind_address)
653+
except AttributeError:
654+
pass
642655

643656

644657
def start_splash_linux():
@@ -702,11 +715,26 @@ def main(passed_args: Optional[Tuple[str, ...]] = None) -> int:
702715
parser.add_argument("--height", type=int)
703716
parser.add_argument("--width", type=int)
704717
parser.add_argument("--qmldebug", action="store_true")
718+
if FEATURE_SSHTUNNEL:
719+
parser.add_argument("--ssh-tunnel", type=str, default=None)
720+
parser.add_argument("--ssh-remote-bind-address", type=str, default=None)
705721

706722
args_main, unknown_args = parser.parse_known_args()
723+
for unknown_arg in unknown_args:
724+
for tunnel_arg in ("--ssh-tunnel", "--ssh-remote-bind-address"):
725+
if tunnel_arg in unknown_arg:
726+
parser.error(
727+
f"Option {tunnel_arg} unsupported.\n"
728+
"The --ssh-tunnel and --ssh-remote-bind-address "
729+
"arguments require the `sshtunnel` python module."
730+
)
731+
707732
if args_main.debug_with_no_backend and args_main.read_capnp_recording is None:
708733
parser.error("The --debug-with-no-backend argument requires the --read-capnp-recording argument.")
709734

735+
if FEATURE_SSHTUNNEL:
736+
ssh_tunnel.validate(args_main, parser)
737+
710738
found_help_arg = False
711739
for arg in unknown_args:
712740
if arg in HELP_CLI_ARGS:
@@ -846,6 +874,11 @@ def handle_qml_load_errors(obj, _url):
846874

847875
endpoint_main.shutdown()
848876
backend_msg_receiver.join()
877+
try:
878+
# Stop the sshtunnel server if there is one.
879+
sshtunnel_server.stop() # type: ignore
880+
except NameError:
881+
pass
849882

850883
return 0
851884

swiftnav_console/ssh_tunnel.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import argparse
2+
import sshtunnel # type: ignore
3+
4+
_default_tunnel_port = 22
5+
_default_remote_bind_port = 55555
6+
7+
8+
def validate(args: argparse.Namespace, parser: argparse.ArgumentParser):
9+
if not (args.ssh_remote_bind_address or args.ssh_tunnel):
10+
return
11+
12+
if bool(args.ssh_remote_bind_address) != bool(args.ssh_tunnel):
13+
parser.error(
14+
f"""The --ssh-tunnel and --ssh-remote-bind-address options must be used together.
15+
--ssh-tunnel option format: [user]:[password]@example.com[:port] (default port {_default_tunnel_port})
16+
--ssh-remote-bind-address format: example.com[:port] (default port {_default_remote_bind_port})
17+
"""
18+
)
19+
20+
e2_g1_str = "expected 2, got 1"
21+
try:
22+
try:
23+
(user_pw, host_port) = args.ssh_tunnel.split("@")
24+
except ValueError as e:
25+
if e2_g1_str not in str(e):
26+
raise
27+
(host_port,) = args.ssh_tunnel.split("@")
28+
try:
29+
(_, _) = user_pw.split(":")
30+
except ValueError as e:
31+
if e2_g1_str not in str(e):
32+
raise
33+
except UnboundLocalError:
34+
pass
35+
(_, _) = host_port.split(":")
36+
except ValueError as e:
37+
if e2_g1_str not in str(e):
38+
parser.error(
39+
f"""invalid --ssh-tunnel argument.
40+
Please use format: [user]:[password]@example.com[:port] (default port {_default_tunnel_port})"""
41+
)
42+
43+
try:
44+
(_, _) = args.ssh_remote_bind_address.split(":")
45+
except ValueError as e:
46+
if e2_g1_str not in str(e):
47+
parser.error(
48+
f"""invalid --ssh-remote-bind-address argument.
49+
Please use format: example.com[:port] (default port {_default_remote_bind_port})"""
50+
)
51+
52+
53+
def setup(tunnel_address: str, remote_bind_address: str):
54+
username_password = ""
55+
try:
56+
(username_password, host_port) = tunnel_address.split("@")
57+
except ValueError:
58+
host_port = tunnel_address
59+
password = None
60+
try:
61+
(username, password) = username_password.split(":")
62+
except ValueError:
63+
username = username_password
64+
port = str(_default_tunnel_port)
65+
try:
66+
(host, port) = host_port.split(":")
67+
except ValueError:
68+
host = host_port
69+
70+
remote_bind_port = str(_default_remote_bind_port)
71+
try:
72+
(remote_bind_host, remote_bind_port) = remote_bind_address.split(":")
73+
except ValueError:
74+
remote_bind_host = remote_bind_address
75+
76+
global sshtunnel_server # pylint: disable=global-variable-undefined
77+
# To debug this, set `logger` parameter to `sshtunnel.create_logger(None, "DEBUG")`
78+
sshtunnel_server = sshtunnel.SSHTunnelForwarder( # type: ignore
79+
(host, int(port)),
80+
ssh_username=username,
81+
ssh_password=password,
82+
local_bind_address=("127.0.0.1", int(remote_bind_port)),
83+
remote_bind_address=(remote_bind_host, int(remote_bind_port)),
84+
# logger=sshtunnel.create_logger(None, "DEBUG"),
85+
)
86+
sshtunnel_server.start() # type: ignore

0 commit comments

Comments
 (0)