Add support for using an SSH tunnel.

This should be preffered approach when connecting to remarkable over
(W)LAN for security reasons.
This commit is contained in:
Tomaz Muraus 2021-03-14 14:02:38 +01:00
parent c26efcdead
commit ba98c0e28b
5 changed files with 126 additions and 16 deletions

View file

@ -113,6 +113,7 @@ Connection parameters are provided as a dictionary with the following keys (all
| `key` | Local path to key for ssh | not needed if password provided |
| `timeout` | Connection timeout in seconds | default: 1 |
| `host_key_policy` | `"ask"`, `"ignore_new"`, `"ignore_all"`, `"auto_add"` | default: `"ask"` (description below) |
| `tunnel` | True to connect to VNC server over a local SSH tunnel | default: `false` (description below) |
The `address` parameter can be either:
- a single string, in which case the address is used for connection
@ -121,6 +122,7 @@ The `address` parameter can be either:
To establish a connection with the tablet, you can use any of the following:
- Leave `auth_method`, `password` and `key` unspecified: this will ask for a password
- Specify `"auth_method": "key"` to use a SSH key. In case an SSH key hasn't already been associated with the tablet, you can provide its path with the `key` setting.
If key is password protected, you can specify key passphrase using `password` parameter.
- Provide a `password` in settings
If `auth_method` is `password` but no password is specified, then the tool will ask for the password on connection.
@ -141,7 +143,6 @@ The old `"insecure_auto_add_host": true` parameter is deprecated and equivalent
In case your `~/.ssh/known_hosts` file contains the relevant key associations, rMview should pick them up.
If you use the "Add/Update" feature when prompted by rMview (for example after a tablet update) then `~/.ssh/known_hosts` will be ignored from then on.
:warning: **Key format error:**
If you get an error when connect using a key, but the key seems ok when connecting manually with ssh, you probably need to convert the key to the PEM format (or re-generate it using the `-m PEM` option of `ssh-keygen`). See [here](https://github.com/paramiko/paramiko/issues/340#issuecomment-492448662) for details.
@ -158,6 +159,22 @@ cat ~/.ssh/known_hosts | grep 10.11.99.1 >> ~/.config/rmview_known_hosts
You should of course replace IP with your remarkable IP.
### Note on security and using an SSH tunnel
By default, this program will start VNC server on remarkable which listens on all the interfaces and doesn't expose
any authentication mechanism or uses encryption.
This program will then connect to the VNC server over the IP specified in the config.
Not using any authentication and exposing VNC server on all the network interfaces may be OK when connecting to the
remarkable over USB interface, but when you are connecting to remarkable over WLAN, you are strongly encouraged to
use built-in SSH tunnel functionality.
When SSH tunnel functionality is used, VNC server which is started on remarkable will only listen on localhost, this
program will create SSH tunnel to the remarkable and connect to the VNC server over the local SSH tunnel.
This means that the connection will be encrypted and existing SSH authentication will be used.
## To Do
- [ ] Settings dialog

View file

@ -0,0 +1,17 @@
{
"ssh": {
"timeout": 4,
"address": "192.168.160.100",
"username": "root",
"auth_method": "key",
"key": "/home/user/.ssh/id_rsa_remarkable",
"password": "ssh key passphrase",
"tunnel": true
},
"orientation": "auto",
"pen_size": 15,
"pen_color": "red",
"pen_trail": 200,
"background_color": "white",
"hide_pen_on_press": true
}

View file

@ -41,7 +41,7 @@ setup(
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
],
packages=['rmview'],
install_requires=['pyqt5', 'paramiko', 'twisted'],
install_requires=['pyqt5', 'paramiko', 'twisted', 'sshtunnel'],
entry_points={
'console_scripts':['rmview = rmview.rmview:rmViewMain']
},

View file

@ -287,7 +287,8 @@ class rMViewApp(QApplication):
self.openSettings(prompt=False)
return
self.fbworker = FrameBufferWorker(ssh, delay=self.config.get('fetch_frame_delay'))
self.fbworker = FrameBufferWorker(ssh, ssh_config=self.config.get('ssh', {}),
delay=self.config.get('fetch_frame_delay'))
self.fbworker.signals.onNewFrame.connect(self.onNewFrame)
self.fbworker.signals.onFatalError.connect(self.frameError)
self.threadpool.start(self.fbworker)

View file

@ -5,13 +5,14 @@ from PyQt5.QtCore import *
from .rmparams import *
import paramiko
import sshtunnel
import struct
import time
import sys
import os
import logging
import atexit
from twisted.internet.protocol import Protocol
from twisted.internet import protocol, reactor
@ -79,43 +80,117 @@ class FrameBufferWorker(QRunnable):
_stop = False
def __init__(self, ssh, delay=None, lz4_path=None, img_format=IMG_FORMAT):
def __init__(self, ssh, ssh_config, delay=None, lz4_path=None, img_format=IMG_FORMAT):
super(FrameBufferWorker, self).__init__()
self.ssh = ssh
self.ssh_config = ssh_config
self.img_format = img_format
self.use_ssh_tunnel = self.ssh_config.get("tunnel", False)
self.vncClient = None
self.sshTunnel = None
self.signals = FBWSignals()
def stop(self):
if self._stop:
# Already stopped
return
self._stop = True
log.info("Stopping framebuffer thread...")
reactor.callFromThread(reactor.stop)
try:
log.info("Stopping VNC server...")
self.ssh.exec_command("killall rM-vnc-server-standalone", timeout=3)
except Exception as e:
log.warning("VNC could not be stopped on the reMarkable.")
log.warning("Although this is not a big problem, it may consume some resources until you restart the tablet.")
log.warning("You can manually terminate it by running `ssh %s killall rM-vnc-server-standalone`.", self.ssh.hostname)
log.error(e)
if self.sshTunnel:
try:
log.info("Stopping SSH tunnel...")
self.sshTunnel.stop()
except Exception as e:
log.error(e)
log.info("Framebuffer thread stopped")
@pyqtSlot()
def run(self):
log.info("Starting VNC server")
# On start up we try to kill any previous "stray" running VNC server processes
try:
_,_,out = self.ssh.exec_command("$HOME/rM-vnc-server-standalone")
log.info(next(out))
self.ssh.exec_command("killall rM-vnc-server-standalone", timeout=3)
except Exception:
pass
# If using SSH tunnel, we ensure VNC server only listens on localhost
if self.use_ssh_tunnel:
server_run_cmd = "$HOME/rM-vnc-server-standalone -listen localhost"
else:
server_run_cmd = "$HOME/rM-vnc-server-standalone"
log.info("Starting VNC server (command=%s)" % (server_run_cmd))
try:
_,_,out = self.ssh.exec_command(server_run_cmd)
log.info("Command output: %s" % (next(out)))
except Exception as e:
self.signals.onFatalError.emit(e)
while self._stop == False:
log.info("Establishing connection to remote VNC server")
try:
self.vncClient = internet.TCPClient(self.ssh.hostname, 5900, RFBFactory(self.signals))
self.vncClient.startService()
reactor.run(installSignalHandlers=0)
except Exception as e:
log.error(e)
# Register atexit handler to ensure we always try to kill started server on exit
atexit.register(self.stop)
if self.use_ssh_tunnel:
tunnel = self._get_ssh_tunnel()
tunnel.start()
self.sshTunnel = tunnel
log.info("Setting up SSH tunnel %s:%s <-> %s:%s" % ("127.0.0.1", 5900, tunnel.local_bind_host,
tunnel.local_bind_port))
vnc_server_host = tunnel.local_bind_host
vnc_server_port = tunnel.local_bind_port
else:
vnc_server_host = self.ssh.hostname
vnc_server_port = 5900
while not self._stop:
log.info("Establishing connection to remote VNC server to %s:%s" % (vnc_server_host,
vnc_server_port))
try:
self.vncClient = internet.TCPClient(vnc_server_host, vnc_server_port, RFBFactory(self.signals))
self.vncClient.startService()
reactor.run(installSignalHandlers=0)
except Exception as e:
log.error("Failed to connect to the VNC server: %s" % (str(e)))
def _get_ssh_tunnel(self):
open_tunnel_kwargs = {
"ssh_username" : self.ssh_config.get("username", "root"),
}
if self.ssh_config.get("auth_method", "password") == "key":
open_tunnel_kwargs["ssh_pkey"] = self.ssh_config["key"]
if self.ssh_config.get("password", None):
open_tunnel_kwargs["ssh_private_key_password"] = self.ssh_config["password"]
else:
open_tunnel_kwargs["ssh_password"] = self.ssh_config["password"]
tunnel = sshtunnel.open_tunnel(
(self.ssh.hostname, 22),
remote_bind_address=("127.0.0.1", 5900),
# We don't specify port so library auto assigns random unused one in the high range
local_bind_address=('127.0.0.1',),
**open_tunnel_kwargs)
return tunnel
class PWSignals(QObject):