From 346cd175279139c4b24408f04de43b560b26f01b Mon Sep 17 00:00:00 2001 From: nojhan Date: Tue, 9 Aug 2022 09:31:47 +0200 Subject: [PATCH 1/8] feat: show port forwarding types --- tunnelmon.py | 64 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/tunnelmon.py b/tunnelmon.py index d7d20e6..49c0156 100755 --- a/tunnelmon.py +++ b/tunnelmon.py @@ -38,7 +38,7 @@ import itertools class Tunnel: - def __init__(self, ssh_pid=None, in_port=None, via_host=None, target_host=None, out_port=None): + def __init__(self, ssh_pid=None, in_port=None, via_host=None, target_host=None, out_port=None, forward=None): # assert ssh_pid is not None self.ssh_pid = ssh_pid assert in_port is not None @@ -49,11 +49,18 @@ class Tunnel: self.target_host = target_host assert out_port is not None self.out_port = out_port + assert forward is not None + self.forwards = {'L':'local', 'R':'remote', 'D': 'dynamic'} + if forward in self.forwards: + self.forward = self.forwards[forward] + else: + self.forward = "unknown" self.connections = [] def repr_tunnel(self): - return "%i\t%i\t%s\t%s\t%i" % ( + return "%s\t%i\t%i\t%s\t%s\t%i" % ( + self.forward, self.ssh_pid, self.in_port, self.via_host, @@ -146,9 +153,9 @@ class TunnelsParser: # only a list of connections OR autossh processes # self.update() - self.re_forwarding = re.compile(r"-\w*[LRD]\w*\s*(\d+):(.*):(\d+)") + self.re_forwarding = re.compile(r"-\w*([LRD])\w*\s*(\d+):(.*):(\d+)") - self.header = 'TYPE\tSSH_PID\tIN_PORT\tVIA_HOST\tTARGET_HOST\tOUT_PORT' + self.header = 'TYPE\tFORWARD\tSSH_PID\tIN_PORT\tVIA_HOST\tTARGET_HOST\tOUT_PORT' def get_tunnel(self, pos): pid = list(self.tunnels.keys())[pos] @@ -163,7 +170,7 @@ class TunnelsParser: logging.debug(match) if match: assert len(match) == 1 - in_port, target_host, out_port = match[0] + forward, in_port, target_host, out_port = match[0] logging.debug("matches: %s", match) else: raise ValueError("is not a ssh tunnel") @@ -188,7 +195,7 @@ class TunnelsParser: via_host = cmd[i] break - return int(in_port), via_host, target_host, int(out_port) + return int(in_port), via_host, target_host, int(out_port), forward def update(self): """Gather and parse informations from the operating system""" @@ -206,21 +213,21 @@ class TunnelsParser: if process['name'] == 'ssh': logging.debug(process) try: - in_port, via_host, target_host, out_port = self.parse(cmd) + in_port, via_host, target_host, out_port, forward = self.parse(cmd) except ValueError: continue - logging.debug("%s %s %s %s", in_port, via_host, target_host, out_port) + logging.debug("%s %s %s %s %s", in_port, via_host, target_host, out_port, forward) # Check if this ssh tunnel is managed by autossh. parent = psutil.Process(process['ppid']) if parent.name() == 'autossh': # Add an autossh tunnel. pid = parent.pid # autossh pid - self.tunnels[pid] = AutoTunnel(pid, process['pid'], in_port, via_host, target_host, out_port) + self.tunnels[pid] = AutoTunnel(pid, process['pid'], in_port, via_host, target_host, out_port, forward) else: # Add a raw tunnel. pid = process['pid'] - self.tunnels[pid] = RawTunnel(pid, in_port, via_host, target_host, out_port) + self.tunnels[pid] = RawTunnel(pid, in_port, via_host, target_host, out_port, forward) for c in process['connections']: logging.debug(c) @@ -272,14 +279,32 @@ class CursesMonitor: # colors # FIXME different colors for different types of tunnels (auto or raw) + # 0: Black, + # 1: Blue, + # 2: Green, + # 3: Cyan, + # 4: Red, + # 5: Magenta, + # 6: Brown, + # 7: White ("Light Gray"), + # 8: Bright Black ("Gray"), + # 9: Bright Blue, + # 10: Bright Green, + # 11: Bright Cyan, + # 12: Bright Red, + # 13: Bright Magenta, + # 14: Yellow, + # 15: Bright White self.colors_tunnel = {'kind_auto': 4, 'kind_raw': 5, 'ssh_pid': 0, 'in_port': 3, - 'via_host': 2, 'target_host': 2, 'out_port': 3, 'tunnels_nb': 4, 'tunnels_nb_none': 1} + 'via_host': 2, 'target_host': 2, 'out_port': 3, 'tunnels_nb': 4, 'tunnels_nb_none': 1, + 'forward': 6} self.colors_highlight = {'kind_auto': 9, 'kind_raw': 9, 'ssh_pid': 9, 'in_port': 9, - 'via_host': 9, 'target_host': 9, 'out_port': 9, 'tunnels_nb': 9, 'tunnels_nb_none': 9} + 'via_host': 9, 'target_host': 9, 'out_port': 9, 'tunnels_nb': 9, 'tunnels_nb_none': 9, + 'forward': 9} self.colors_connection = {'ssh_pid': 0, 'autossh_pid': 0, 'status': 4, 'status_out': 1, 'local_address': 2, 'in_port': 3, 'foreign_address': 2, 'out_port': 3} - self.header = ("TYPE", "SSHPID", "INPORT", "VIA", "TARGET", "OUTPORT") + self.header = ("TYPE", "FORWARD", "SSHPID", "INPORT", "VIA", "TARGET", "OUTPORT") def do_Q(self): """Quit""" @@ -477,8 +502,6 @@ class CursesMonitor: self.cur_pid = -1 # header line - # header_msg = "TYPE\tINPORT\tVIA \tTARGET \tOUTPORT" - # if os.geteuid() == 0: header_msg = " ".join(self.format()).format(*self.header) header_msg += " CONNECTIONS" self.scr.addstr(header_msg, curses.color_pair(color)) @@ -545,11 +568,12 @@ class CursesMonitor: self.scr.addstr(' ', curses.color_pair(colors['kind_raw'])) # self.add_tunnel_info('ssh_pid', line) - self.add_tunnel_info('ssh_pid', line, 1) - self.add_tunnel_info('in_port', line, 2) - self.add_tunnel_info('via_host', line, 3) - self.add_tunnel_info('target_host', line, 4) - self.add_tunnel_info('out_port', line, 5) + self.add_tunnel_info('forward' , line, 1) + self.add_tunnel_info('ssh_pid' , line, 2) + self.add_tunnel_info('in_port' , line, 3) + self.add_tunnel_info('via_host' , line, 4) + self.add_tunnel_info('target_host', line, 5) + self.add_tunnel_info('out_port' , line, 6) nb = len(self.tp.get_tunnel(line).connections) if nb > 0: From 23d83914f81ef58f4238251e7cefc0c5c8ee90f2 Mon Sep 17 00:00:00 2001 From: nojhan Date: Tue, 9 Aug 2022 11:20:48 +0200 Subject: [PATCH 2/8] feat: colors by types - hide cursor - same headers in CLI and TUI --- tunnelmon.py | 142 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 109 insertions(+), 33 deletions(-) diff --git a/tunnelmon.py b/tunnelmon.py index 49c0156..635ce14 100755 --- a/tunnelmon.py +++ b/tunnelmon.py @@ -155,7 +155,7 @@ class TunnelsParser: self.re_forwarding = re.compile(r"-\w*([LRD])\w*\s*(\d+):(.*):(\d+)") - self.header = 'TYPE\tFORWARD\tSSH_PID\tIN_PORT\tVIA_HOST\tTARGET_HOST\tOUT_PORT' + self.header = 'TYPE\tFORWARD\tSSHPID\tINPORT\tVIA\tTARGET\tOUTPORT' def get_tunnel(self, pos): pid = list(self.tunnels.keys())[pos] @@ -258,6 +258,9 @@ class CursesMonitor: """Textual user interface to display up-to-date informations about current tunnels""" def __init__(self, scr): + # hide cursor + curses.curs_set(0) + # curses screen self.scr = scr @@ -278,31 +281,61 @@ class CursesMonitor: self.ui_delay = 0.05 # seconds between two screen update # colors - # FIXME different colors for different types of tunnels (auto or raw) - # 0: Black, - # 1: Blue, - # 2: Green, - # 3: Cyan, - # 4: Red, - # 5: Magenta, - # 6: Brown, - # 7: White ("Light Gray"), - # 8: Bright Black ("Gray"), - # 9: Bright Blue, - # 10: Bright Green, - # 11: Bright Cyan, - # 12: Bright Red, - # 13: Bright Magenta, - # 14: Yellow, - # 15: Bright White - self.colors_tunnel = {'kind_auto': 4, 'kind_raw': 5, 'ssh_pid': 0, 'in_port': 3, - 'via_host': 2, 'target_host': 2, 'out_port': 3, 'tunnels_nb': 4, 'tunnels_nb_none': 1, - 'forward': 6} - self.colors_highlight = {'kind_auto': 9, 'kind_raw': 9, 'ssh_pid': 9, 'in_port': 9, - 'via_host': 9, 'target_host': 9, 'out_port': 9, 'tunnels_nb': 9, 'tunnels_nb_none': 9, - 'forward': 9} - self.colors_connection = {'ssh_pid': 0, 'autossh_pid': 0, 'status': 4, 'status_out': 1, - 'local_address': 2, 'in_port': 3, 'foreign_address': 2, 'out_port': 3} + # 0:black, 1:red, 2:green, 3:yellow, 4:blue, 5:magenta, 6:cyan, and 7:white. + self.colors_tunnel = { + 'kind_auto' : curses.COLOR_CYAN, + 'kind_raw' : curses.COLOR_BLUE, + 'ssh_pid' : curses.COLOR_WHITE, + 'in_port' : curses.COLOR_YELLOW, + 'out_port' : curses.COLOR_YELLOW, + 'in_port_priv' : curses.COLOR_RED, + 'out_port_priv' : curses.COLOR_RED, + 'via_host' : curses.COLOR_GREEN, + 'target_host' : curses.COLOR_GREEN, + 'via_local' : curses.COLOR_BLUE, + 'target_local' : curses.COLOR_BLUE, + 'via_priv' : curses.COLOR_CYAN, + 'target_priv' : curses.COLOR_CYAN, + 'tunnels_nb' : curses.COLOR_RED, + 'tunnels_nb_none': curses.COLOR_RED, + 'forward_local' : curses.COLOR_BLUE, + 'forward_remote' : curses.COLOR_CYAN, + 'forward_dynamic': curses.COLOR_YELLOW, + 'forward_unknown': curses.COLOR_WHITE, + } + self.colors_highlight = { + 'kind_auto' : 9, + 'kind_raw' : 9, + 'ssh_pid' : 9, + 'in_port' : 9, + 'out_port' : 9, + 'in_port_priv' : 9, + 'out_port_priv' : 9, + 'via_host' : 9, + 'target_host' : 9, + 'via_local' : 9, + 'target_local' : 9, + 'via_priv' : 9, + 'target_priv' : 9, + 'tunnels_nb' : 9, + 'tunnels_nb_none': 9, + 'forward_local' : 9, + 'forward_remote' : 9, + 'forward_dynamic': 9, + 'forward_unknown': 9, + } + self.colors_connection = { + 'ssh_pid' : curses.COLOR_WHITE, + 'autossh_pid' : curses.COLOR_WHITE, + 'status' : curses.COLOR_CYAN, + 'status_out' : curses.COLOR_RED, + 'local_address' : curses.COLOR_BLUE, + 'foreign_address': curses.COLOR_GREEN, + 'in_port' : curses.COLOR_YELLOW, + 'out_port' : curses.COLOR_YELLOW, + 'in_port_priv' : curses.COLOR_RED, + 'out_port_priv' : curses.COLOR_RED, + } self.header = ("TYPE", "FORWARD", "SSHPID", "INPORT", "VIA", "TARGET", "OUTPORT") @@ -459,6 +492,7 @@ class CursesMonitor: # end of the loop def format(self): + """Prepare formating strings to pad with spaces up to the tolumn header width.""" reps = [self.tp.tunnels[t].repr_tunnel() for t in self.tp.tunnels] tuns = [t.split() for t in reps] tuns.append(self.header) @@ -556,25 +590,67 @@ class CursesMonitor: """Add line corresponding to the line-th autossh process""" self.scr.addstr('\n') + # Handle on the current tunnel object. + t = self.tp.get_tunnel(line) + + # Highlight selected line. colors = self.colors_tunnel if self.cur_line == line: colors = self.colors_highlight + # TYPE if type(self.tp.get_tunnel(line)) == AutoTunnel: + # Format 'auto' using the 0th column format string and the related color.. self.scr.addstr(self.format()[0].format('auto'), curses.color_pair(colors['kind_auto'])) + # Trailing space. self.scr.addstr(' ', curses.color_pair(colors['kind_auto'])) else: self.scr.addstr(self.format()[0].format('ssh'), curses.color_pair(colors['kind_raw'])) self.scr.addstr(' ', curses.color_pair(colors['kind_raw'])) - # self.add_tunnel_info('ssh_pid', line) - self.add_tunnel_info('forward' , line, 1) - self.add_tunnel_info('ssh_pid' , line, 2) - self.add_tunnel_info('in_port' , line, 3) - self.add_tunnel_info('via_host' , line, 4) - self.add_tunnel_info('target_host', line, 5) - self.add_tunnel_info('out_port' , line, 6) + # FORWARD + fwd = t.forward + self.scr.addstr(self.format()[1].format(fwd), curses.color_pair(colors['forward_'+fwd])) + self.scr.addstr(' ', curses.color_pair(colors['forward_'+fwd])) + # SSHPID + self.add_tunnel_info('ssh_pid' , line, 2) + + # INPORT + if t.in_port <= 1024: + self.scr.addstr(self.format()[3].format(t.in_port), curses.color_pair(colors['in_port_priv'])) + self.scr.addstr(' ', curses.color_pair(colors['in_port_priv'])) + else: + self.add_tunnel_info('in_port' , line, 3) + + # VIA + if any(re.match(p,t.via_host) for p in ["^127\..*", "::1", "localhost"]): # loopback + self.scr.addstr(self.format()[4].format(t.via_host), curses.color_pair(colors['via_local'])) + self.scr.addstr(' ', curses.color_pair(colors['via_local'])) + elif any(re.match(p,t.via_host) for p in ["^10\..*", "^172\.[123][0-9]*\..*", "^192\.0\.0\.[0-9]+", "^192\.168\..*", "64:ff9b:1:.*", "fc00::.*"]): # private network + self.scr.addstr(self.format()[4].format(t.via_host), curses.color_pair(colors['via_priv'])) + self.scr.addstr(' ', curses.color_pair(colors['via_priv'])) + else: + self.add_tunnel_info('via_host' , line, 4) + + # TARGET + if any(re.match(p,t.target_host) for p in ["^127\..*", "::1", "localhost"]): # loopback + self.scr.addstr(self.format()[5].format(t.target_host), curses.color_pair(colors['target_local'])) + self.scr.addstr(' ', curses.color_pair(colors['target_local'])) + elif any(re.match(p,t.target_host) for p in ["^10\..*", "^172\.[123][0-9]*\..*", "^192\.0\.0\.[0-9]+", "^192\.168\..*", "64:ff9b:1:.*", "fc00::.*"]): # private network + self.scr.addstr(self.format()[5].format(t.target_host), curses.color_pair(colors['target_priv'])) + self.scr.addstr(' ', curses.color_pair(colors['target_priv'])) + else: + self.add_tunnel_info('target_host' , line, 5) + + # OUTPORT + if t.out_port <= 1024: + self.scr.addstr(self.format()[6].format(t.out_port), curses.color_pair(colors['out_port_priv'])) + self.scr.addstr(' ', curses.color_pair(colors['out_port_priv'])) + else: + self.add_tunnel_info('out_port' , line, 6) + + # CONNECTIONS nb = len(self.tp.get_tunnel(line).connections) if nb > 0: # for each connection related to this process From 19f676ef0f4980bb06bff01298a012c5e77d512f Mon Sep 17 00:00:00 2001 From: nojhan Date: Tue, 9 Aug 2022 11:32:36 +0200 Subject: [PATCH 3/8] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. From 2ac2d0d9573baeb76bdf24afb1eda1498e17b7b4 Mon Sep 17 00:00:00 2001 From: nojhan Date: Tue, 9 Aug 2022 11:33:08 +0200 Subject: [PATCH 4/8] update documentation - update changelog to prepare 1.1 - add missing author - more info in README - screenshot --- AUTHORS | 5 +++++ CHANGELOG | 24 ------------------------ CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ README.md | 29 ++++++++++++++++++++++++++++- screenshot.png | Bin 0 -> 25852 bytes 5 files changed, 67 insertions(+), 25 deletions(-) delete mode 100644 CHANGELOG create mode 100644 CHANGELOG.md create mode 100644 screenshot.png diff --git a/AUTHORS b/AUTHORS index 6f3e279..cf8d695 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,3 +4,8 @@ E:nojhan@nojhan.net D:2009-06-23 C:Initial code, lead developper +N:Ghislain Picard +P:ghislainp +E:ghislain.picard@univ-grenoble-alpes.fr +D:2021-01-22 +C:Fixes on tunnels detection diff --git a/CHANGELOG b/CHANGELOG deleted file mode 100644 index 7109a0a..0000000 --- a/CHANGELOG +++ /dev/null @@ -1,24 +0,0 @@ -0.5: -* python3 -* complete rewrite for portability -* handle autossh tunnels *and* raw ssh tunnels -* better formatting - -0.4: -* adds IPv6 support -* bugfixes - -0.3: -* added a via_host field (show on which host the tunnel is build) -* don't try to show connections if the user is not root - -0.2: -* update from the deprecated popen3 functions to the subprocess module (need python >= 2.4) -* html help page - -0.1: -* command line interface -* curses interface -* list of active autossh instances -* associated active ssh connections - diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..402f281 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,34 @@ +1.1: "Ñìrikarñar" +- handle local, remote and dynamic port forwading tunnels, +- different colors for different types. + +1.0: "Ereshkigal" +- python 3.8, +- fix tunnels detection, +- fix logging, +- code formating. + +0.5: +- python3, +- complete rewrite for portability, +- handle autossh tunnels *and* raw ssh tunnels, +- better formatting. + +0.4: +- adds IPv6 support, +- bugfixes. + +0.3: +- added a via_host field (show on which host the tunnel is build), +- don't try to show connections if the user is not root. + +0.2: +- update from the deprecated popen3 functions to the subprocess module (need python >= 2.4), +- html help page. + +0.1: +- command line interface, +- curses interface, +- list of active autossh instances, +- associated active ssh connections. + diff --git a/README.md b/README.md index 49ac77e..f5093a0 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ tunnelmon -- Monitor and manage autoSSH tunnels ## DESCRIPTION -`tunnelmon` is an autossh tunnel monitor. It gives a user interface to monitor existing SSH tunnel that are managed with autossh. +`tunnelmon` is an autossh tunnel monitor. It gives a user interface to monitor existing SSH tunnel, and tunnels managed with autossh. It can print the current state of your tunnels or display them in an interactive text-based interface. `tunnelmon` is released under the GNU Public License v3. +![Screenshot](https://raw.github.com/nojhan/tunnelmon/master/screenshot.png) + ## INSTALLATION @@ -63,6 +65,30 @@ Keyboard commands: * `Q`: Quit tunnelmon. +## DISPLAY + +Tunnelmon displays a table where lines are [auto]ssh processes that sets up a tunnel. +Columns of the table indicates: +- TYPE: `auto` if the process is managed by autossh, `ssh` if it is a "raw" SSH tunnel; +- FORWARD: the type of port forwarding method (either `local`, `remote` or `dynamic`, see the SSH manual for details); +- SSHPID: the process identifier; +- INPORT: the client port; +- VIA: the client host; +- TARGET: the host address; +- OUTPORT: the host port. + +The interactive interface adds a CONNECTIONS columns that displays one vertical bar for each connection set up by the tunnel. + +If you ask for showing the connections list (typing `N` in the interactive interface, or not passing `-u` to the command line one), +Tunnelmon will show indented lines with the type of the connection, its status and the related address:port informations. + +In the interactive interface, different colors are used for: +- the tunnel type, +- the port forwarding methods, +- privileged and unprivileged ports, +- loopback, private and regular addresses. + + ## SSH Tunnels in a nutshell To open a tunnel to port 1234 of `server` through a `host` reached on port 4567: @@ -75,3 +101,4 @@ Autossh can restart tunnels for you, in case they crash: ``` autossh -f host -L4567:server:1234 ``` + diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..d81baed58f7d94bf9b89003667829e909585a33b GIT binary patch literal 25852 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 BkQD#` literal 0 HcmV?d00001 From 5efc38648539fb1738ab892fbb25cf21438cd17c Mon Sep 17 00:00:00 2001 From: nojhan Date: Fri, 19 Aug 2022 15:53:42 +0200 Subject: [PATCH 5/8] fix(doc) Mention some dependencies in the readme Fix #4 --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5093a0..9ac277b 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,10 @@ It can print the current state of your tunnels or display them in an interactive ## INSTALLATION `tunnelmon` targets Linux operating systems, and depends on: -* `openssh-client` -* `python` version 3.8 at least. +* `openssh-client`, +* `python` version 3.8 at least, you may also need to install the following python modules (for example via `pip`, but you may use any other package management system going along with your installation): + * `psutils` + * `curses` You may also want to install the recommend packages: * `autossh` From 8da6025673f7ccaadc752d3d9ab9577110a8ea82 Mon Sep 17 00:00:00 2001 From: nojhan Date: Tue, 10 Jan 2023 11:30:47 +0100 Subject: [PATCH 6/8] fix cmd parsing Was crashing when cmd was not an iterable. --- tunnelmon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tunnelmon.py b/tunnelmon.py index 635ce14..b3637d5 100755 --- a/tunnelmon.py +++ b/tunnelmon.py @@ -162,7 +162,10 @@ class TunnelsParser: return self.tunnels[pid] def parse(self, cmd): - cmdline = " ".join(cmd) + try: + cmdline = " ".join(cmd) + except TypeError: + cmdline = cmd logging.debug('autossh cmd line: %s', cmdline) logging.debug('forwarding regexp: %s', self.re_forwarding) From 2d95669928a3c3aca948690b3cb6c3e016d216cb Mon Sep 17 00:00:00 2001 From: nojhan Date: Thu, 20 Jul 2023 11:07:20 +0200 Subject: [PATCH 7/8] feat(log): separate sensitive logs - Adds the --log-sensitive option. - Tag with [SENSITIVE] in the logs. - Avoid formatting within logging calls. --- tunnelmon.py | 91 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 33 deletions(-) diff --git a/tunnelmon.py b/tunnelmon.py index b3637d5..e30cadc 100755 --- a/tunnelmon.py +++ b/tunnelmon.py @@ -36,6 +36,7 @@ import re import collections import itertools +log_sensitive = False class Tunnel: def __init__(self, ssh_pid=None, in_port=None, via_host=None, target_host=None, out_port=None, forward=None): @@ -167,14 +168,17 @@ class TunnelsParser: except TypeError: cmdline = cmd - logging.debug('autossh cmd line: %s', cmdline) - logging.debug('forwarding regexp: %s', self.re_forwarding) + if log_sensitive: + logging.debug("[SENSITIVE] autossh cmd line: %s", cmdline) + logging.debug("forwarding regexp: %s" % self.re_forwarding) match = self.re_forwarding.findall(cmdline) - logging.debug(match) + if log_sensitive: + logging.debug("[SENSITIVE] match: %s", match) if match: assert len(match) == 1 forward, in_port, target_host, out_port = match[0] - logging.debug("matches: %s", match) + if log_sensitive: + logging.debug("[SENSITIVE] matches: %s", match) else: raise ValueError("is not a ssh tunnel") @@ -184,7 +188,8 @@ class TunnelsParser: # FIXME this is an ugly hack i = 1 while i < len(cmd): - logging.debug("ici: %i %s", i, cmd[i]) + if log_sensitive: + logging.debug("[SENSITIVE] here: %i %s", i, cmd[i]) if cmd[i][0] == '-': if cmd[i][1] in '46AaCfGgKkMNnqsTtVvXxYy': # flag without argument @@ -214,12 +219,14 @@ class TunnelsParser: pass else: if process['name'] == 'ssh': - logging.debug(process) + if log_sensitive: + logging.debug("[SENSITIVE] process: %s ", process) try: in_port, via_host, target_host, out_port, forward = self.parse(cmd) except ValueError: continue - logging.debug("%s %s %s %s %s", in_port, via_host, target_host, out_port, forward) + if log_sensitive: + logging.debug("[SENSITIVE] parsed: %s %s %s %s %s", in_port, via_host, target_host, out_port, forward) # Check if this ssh tunnel is managed by autossh. parent = psutil.Process(process['ppid']) @@ -233,17 +240,20 @@ class TunnelsParser: self.tunnels[pid] = RawTunnel(pid, in_port, via_host, target_host, out_port, forward) for c in process['connections']: - logging.debug(c) + if log_sensitive: + logging.debug("[SENSITIVE] connection: %s", c) laddr, lport = c.laddr if c.raddr: raddr, rport = c.raddr else: raddr, rport = (None, None) connection = Connection(laddr, lport, raddr, rport, c.status, c.family) - logging.debug(connection) + if log_sensitive: + logging.debug("[SENSITIVE] connection: %s", connection) self.tunnels[pid].connections.append(connection) - logging.debug(self.tunnels) + if log_sensitive: + logging.debug("[SENSITIVE] %s", self.tunnels) def __repr__(self): reps = [self.header] @@ -263,7 +273,7 @@ class CursesMonitor: def __init__(self, scr): # hide cursor curses.curs_set(0) - + # curses screen self.scr = scr @@ -344,14 +354,14 @@ class CursesMonitor: def do_Q(self): """Quit""" - logging.debug("Waited: %s" % self.log_ticks) + logging.debug("Waited: %s", self.log_ticks) self.log_ticks = "" logging.debug("Key pushed: Q") return False def do_R(self): """Reload autossh tunnel""" - logging.debug("Waited: %s" % self.log_ticks) + logging.debug("Waited: %s", self.log_ticks) self.log_ticks = "" logging.debug("Key pushed: R") # if a pid is selected @@ -359,7 +369,8 @@ class CursesMonitor: # send the SIGUSR1 signal if type(self.tp.get_tunnel(self.cur_line)) == AutoTunnel: # autossh performs a reload of existing tunnels that it manages - logging.debug("SIGUSR1 on PID: %i" % self.cur_pid) + if log_sensitive: + logging.debug("[SENSITIVE] SIGUSR1 on PID: %i", self.cur_pid) os.kill(self.cur_pid, signal.SIGUSR1) else: logging.debug("Cannot reload a RAW tunnel") @@ -367,7 +378,7 @@ class CursesMonitor: def do_C(self): """Close tunnel""" - logging.debug("Waited: %s" % self.log_ticks) + logging.debug("Waited: %s", self.log_ticks) self.log_ticks = "" logging.debug("Key pushed: C") if self.cur_pid != -1: @@ -377,17 +388,21 @@ class CursesMonitor: tunnel = self.tp.get_tunnel(self.cur_line) if type(tunnel) == AutoTunnel: - logging.debug("SIGKILL on autossh PID: %i" % self.cur_pid) + if log_sensitive: + logging.debug("[SENSITIVE] SIGKILL on autossh PID: %i", self.cur_pid) try: os.kill(self.cur_pid, signal.SIGKILL) except OSError: - logging.error("No such process: %i" % self.cur_pid) + if log_sensitive: + logging.error("[SENSITIVE] No such process: %i", self.cur_pid) - logging.debug("SIGKILL on ssh PID: %i" % tunnel.ssh_pid) + if log_sensitive: + logging.debug("[SENSITIVE] SIGKILL on ssh PID: %i", tunnel.ssh_pid) try: os.kill(tunnel.ssh_pid, signal.SIGKILL) except OSError: - logging.error("No such process: %i" % tunnel.ssh_pid) + if log_sensitive: + logging.error("[SENSITIVE] No such process: %i", tunnel.ssh_pid) self.cur_line -= 1 self.cur_pid = -1 # FIXME update cur_pid or get rid of it everywhere @@ -395,7 +410,7 @@ class CursesMonitor: def do_N(self): """Show connections""" - logging.debug("Waited: %s" % self.log_ticks) + logging.debug("Waited: %s", self.log_ticks) self.log_ticks = "" logging.debug("Key pushed: N") self.show_connections = not self.show_connections @@ -403,7 +418,7 @@ class CursesMonitor: def do_258(self): """Move down""" - logging.debug("Waited: %s" % self.log_ticks) + logging.debug("Waited: %s", self.log_ticks) self.log_ticks = "" logging.debug("Key pushed: down") # if not the end of the list @@ -418,7 +433,7 @@ class CursesMonitor: def do_259(self): """Move up""" - logging.debug("Waited: %s" % self.log_ticks) + logging.debug("Waited: %s", self.log_ticks) self.log_ticks = "" logging.debug("Key pushed: up") if self.cur_line > -1: @@ -457,10 +472,11 @@ class CursesMonitor: state = "%s" % self.tp if state != self.last_state: - logging.debug("Waited: %s" % self.log_ticks) + logging.debug("Waited: %s", self.log_ticks) self.log_ticks = "" - logging.debug("----- Time of screen update: %s -----" % time.time()) - logging.debug("State of tunnels:\n%s" % self.tp) + logging.debug("----- Time of screen update: %s -----", time.time()) + if log_sensitive: + logging.debug("[SENSITIVE] State of tunnels:\n%s", self.tp) self.last_state = state else: self.log_ticks += "." @@ -479,12 +495,12 @@ class CursesMonitor: # Call the do_* handler. fch = "do_%s" % ch.capitalize() fkc = "do_%i" % kc - logging.debug("key func: %s / %s" % (fch, fkc)) + logging.debug("key func: %s / %s", fch, fkc) if fch in dir(self): notquit = eval("self."+fch+"()") elif fkc in dir(self): notquit = eval("self."+fkc+"()") - logging.debug("notquit = %s" % notquit) + logging.debug("notquit = %s", notquit) # update the display self.display() @@ -495,15 +511,15 @@ class CursesMonitor: # end of the loop def format(self): - """Prepare formating strings to pad with spaces up to the tolumn header width.""" + """Prepare formating strings to pad with spaces up to the column header width.""" reps = [self.tp.tunnels[t].repr_tunnel() for t in self.tp.tunnels] tuns = [t.split() for t in reps] tuns.append(self.header) cols = itertools.zip_longest(*tuns, fillvalue='') widths = [max(len(s) for s in col) for col in cols] - logging.debug(widths) + logging.debug("Columns widths: %s", widths) fmt = ['{{: <{}}}'.format(w) for w in widths] - logging.debug(fmt) + logging.debug("Columns formats: %s", fmt) return fmt def display(self): @@ -724,6 +740,10 @@ if __name__ == "__main__": help="Log to this file, default to standard output. \ If you use the curses interface, you may want to set this to actually see logs.") + parser.add_option("-s", "--log-sensitive", + action="store_true", default=False, + help="Do log sensitive informations (like hostnames, IPs and PIDs).") + parser.add_option('-f', '--config-file', default=None, metavar='FILE', help="Use this configuration file (default: '~/.tunnelmon.conf')") @@ -735,7 +755,7 @@ if __name__ == "__main__": logfile = asked_for.log_file logging.basicConfig(filename=logfile, level=LOG_LEVELS[asked_for.log_level]) logging.debug(logmsg) - logging.debug("Log in %s" % logfile) + logging.debug("Log in %s", logfile) else: if asked_for.curses: logging.warning("It's a bad idea to log to stdout while in the curses interface.") @@ -743,7 +763,11 @@ if __name__ == "__main__": logging.debug(logmsg) logging.debug("Log to stdout") - logging.debug("Asked for: %s", asked_for) + logging.debug("Asked for: %s" % asked_for) + + if asked_for.log_sensitive: + logging.debug("Asked for logging sensitive information.") + log_sensitive = True # unfortunately, asked_for class has no __len__ method in python 2.4.3 (bug?) # if len(asked_for) > 1: @@ -813,7 +837,8 @@ if __name__ == "__main__": tp = TunnelsParser() tp.update() # do not call update() but only get connections - logging.debug("UID: %i.", os.geteuid()) + if log_sensitive: + logging.debug("[SENSITIVE] UID: %i", os.geteuid()) # if os.geteuid() == 0: for t in tp.tunnels: for c in tp.tunnels[t].connections: From 2186d32e60550b288e8e1f549bb4f5a2ecf86474 Mon Sep 17 00:00:00 2001 From: nojhan Date: Thu, 20 Jul 2023 11:17:53 +0200 Subject: [PATCH 8/8] update README --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9ac277b..7fb04dd 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ tunnelmon -- Monitor and manage autoSSH tunnels `tunnelmon` [-h] -`tunnelmon` [-c] [-n] [-u] [-l LEVEL] [-g FILE] +`tunnelmon` [-c] [-n] [-u] [-l LEVEL] [-g FILE] [-s] ## DESCRIPTION -`tunnelmon` is an autossh tunnel monitor. It gives a user interface to monitor existing SSH tunnel, and tunnels managed with autossh. +`tunnelmon` is an autossh tunnel monitor. It gives a user interface to monitor existing SSH tunnel, and tunnels managed with autossh. It can print the current state of your tunnels or display them in an interactive text-based interface. @@ -55,6 +55,9 @@ Called without option,`tunnelmon` will print the current state of the autossh tu Log messages are written to the given FILE. Useful to debug the interactive interface. If not set, asking for the curses interface automatically set logging to the "tunnelmon.log" file. +* `-s`, `--log-sensitive`: + Allow sensitive information (hostnames, IPs, PIDs, etc.) into the logs. + ## INTERACTIVE INTERFACE @@ -64,7 +67,7 @@ Keyboard commands: * `R`: Reload the selected autossh instance (i.e. send a `SIGUSR1`, which is interpreted as a reload command by autossh). * `C`: Close the selected tunnel (i.e. send a `SIGTERM`). * `N`: Show the network connections related to each tunnel instances. -* `Q`: Quit tunnelmon. +* `Q`: Quit Tunnelmon. ## DISPLAY