diff --git a/backup/remote.md b/backup/remote.md index f8108dc..76d25d4 100644 --- a/backup/remote.md +++ b/backup/remote.md @@ -15,10 +15,12 @@ The remote backup system consists of two parts: - A server daemon that receives changes from the backup backend and communicates with a local backup backend to store them. The server side does not need to be running c-lightning, nor have it installed. -The backend URL format is `socket::`. For example `socket:127.0.0.1:1234`. To supply a IPv6 +### URL scheme + +The backend URL format is `socket::[?=[&...]]`. For example `socket:127.0.0.1:1234`. To supply a IPv6 address use the bracketed syntax `socket:[::1]:1234`. -To run the server against a local backend use `backup-cli server file://.../ 127.0.0.1:1234`. +The only currently accepted `` is `proxy`. This can be used to connect to the backup server through a proxy. See [Usage with Tor](#usage-with-tor). Usage ----- @@ -42,6 +44,9 @@ lightningd ... \ --important-plugin /path/to/plugins/backup/backup.py ``` +Usage with SSH +-------------- + The easiest way to connect the server and client if they are not running on the same host is with a ssh forward. For example, when connecting from another machine to the one running c-lightning use: @@ -55,6 +60,25 @@ Or when it is the other way around: ssh backupserver -L 8700:127.0.0.1:8700 ``` +Usage with Tor +-------------- + +To use the backup plugin with Tor the Python module PySocks needs to be installed (`pip install --user pysocks`). + +Assuming Tor's `SocksPort` is 9050, the following URL can be used to connect the backup plugin to a backup server over an onion service: + +``` +socket:axz53......onion:8700?proxy=socks5:127.0.0.1:9050 +``` + +On the server side, manually define an onion service in `torrc` that forwards incoming connections to the local port, e.g.: + +``` +HiddenServiceDir /var/lib/tor/lightning/ +HiddenServiceVersion 3 +HiddenServicePort 8700 127.0.0.1:8700 +``` + Goals ----- diff --git a/backup/socketbackend.py b/backup/socketbackend.py index 689fbe0..85b2ecf 100644 --- a/backup/socketbackend.py +++ b/backup/socketbackend.py @@ -6,34 +6,37 @@ from urllib.parse import urlparse, parse_qs from backend import Backend, Change from protocol import PacketType, recvall, PKT_CHANGE_TYPES, change_from_packet, packet_from_change, send_packet, recv_packet -SocketURLInfo = namedtuple('SocketURLInfo', ['host', 'port', 'addrtype']) +HostPortInfo = namedtuple('HostPortInfo', ['host', 'port', 'addrtype']) +SocketURLInfo = namedtuple('SocketURLInfo', ['target', 'proxytype', 'proxytarget']) +# Network address type. class AddrType: IPv4 = 0 IPv6 = 1 NAME = 2 -def parse_socket_url(destination: str) -> SocketURLInfo: - '''Parse a socket: URL to extract the information contained in it.''' - url = urlparse(destination) - if url.scheme != 'socket': - raise ValueError('Scheme for socket backend must be socket:...') +# Proxy type. Only SOCKS5 supported at the moment as this is sufficient for Tor. +class ProxyType: + DIRECT = 0 + SOCKS5 = 1 - if url.path.startswith('['): # bracketed IPv6 address - eidx = url.path.find(']') +def parse_host_port(path: str) -> HostPortInfo: + '''Parse a host:port pair.''' + if path.startswith('['): # bracketed IPv6 address + eidx = path.find(']') if eidx == -1: raise ValueError('Unterminated bracketed host address.') - host = url.path[1:eidx] + host = path[1:eidx] addrtype = AddrType.IPv6 eidx += 1 - if eidx >= len(url.path) or url.path[eidx] != ':': + if eidx >= len(path) or path[eidx] != ':': raise ValueError('Port number missing.') eidx += 1 else: - eidx = url.path.find(':') + eidx = path.find(':') if eidx == -1: raise ValueError('Port number missing.') - host = url.path[0:eidx] + host = path[0:eidx] if re.match('\d+\.\d+\.\d+\.\d+$', host): # matches IPv4 address format addrtype = AddrType.IPv4 else: @@ -41,17 +44,40 @@ def parse_socket_url(destination: str) -> SocketURLInfo: eidx += 1 try: - port = int(url.path[eidx:]) + port = int(path[eidx:]) except ValueError: raise ValueError('Invalid port number') + return HostPortInfo(host=host, port=port, addrtype=addrtype) + +def parse_socket_url(destination: str) -> SocketURLInfo: + '''Parse a socket: URL to extract the information contained in it.''' + url = urlparse(destination) + if url.scheme != 'socket': + raise ValueError('Scheme for socket backend must be socket:...') + + target = parse_host_port(url.path) + + proxytype = ProxyType.DIRECT + proxytarget = None # parse query parameters # reject unknown parameters (currently all of them) qs = parse_qs(url.query) - if len(qs): - raise ValueError('Invalid query string') + for (key, values) in qs.items(): + if key == 'proxy': # proxy=socks5:127.0.0.1:9050 + if len(values) != 1: + raise ValueError('Proxy can only have one value') - return SocketURLInfo(host=host, port=port, addrtype=addrtype) + (ptype, ptarget) = values[0].split(':', 1) + if ptype != 'socks5': + raise ValueError('Unknown proxy type ' + ptype) + + proxytype = ProxyType.SOCKS5 + proxytarget = parse_host_port(ptarget) + else: + raise ValueError('Unknown query string parameter ' + key) + + return SocketURLInfo(target=target, proxytype=proxytype, proxytarget=proxytarget) class SocketBackend(Backend): def __init__(self, destination: str, create: bool): @@ -59,13 +85,22 @@ class SocketBackend(Backend): self.prev_version = None self.destination = destination self.url = parse_socket_url(destination) - if self.url.addrtype == AddrType.IPv6: - self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - else: # TODO NAME is assumed to be IPv4 for now - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - logging.info('Initialized socket backend, connecting to {}:{} (addrtype {})...'.format( - self.url.host, self.url.port, self.url.addrtype)) - self.sock.connect((self.url.host, self.url.port)) + + if self.url.proxytype == ProxyType.DIRECT: + if self.url.target.addrtype == AddrType.IPv6: + self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + else: # TODO NAME is assumed to be IPv4 for now + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + else: + assert(self.url.proxytype == ProxyType.SOCKS5) + import socks + self.sock = socks.socksocket() + self.sock.set_proxy(socks.SOCKS5, self.url.proxytarget.host, self.url.proxytarget.port) + + logging.info('Initialized socket backend, connecting to {}:{} (addrtype {}, proxytype {}, proxytarget {})...'.format( + self.url.target.host, self.url.target.port, self.url.target.addrtype, + self.url.proxytype, self.url.proxytarget)) + self.sock.connect((self.url.target.host, self.url.target.port)) logging.info('Connected to {}'.format(destination)) def _send_packet(self, typ: int, payload: bytes) -> None: diff --git a/backup/test_backup.py b/backup/test_backup.py index a3afebc..180c96d 100644 --- a/backup/test_backup.py +++ b/backup/test_backup.py @@ -297,21 +297,40 @@ def test_parse_socket_url(): socketbackend.parse_socket_url('socket:127.0.0.1:12bla') # fail: unrecognized query string key socketbackend.parse_socket_url('socket:127.0.0.1:1234?dummy=value') + # fail: incomplete proxy spec + socketbackend.parse_socket_url('socket:127.0.0.1:1234?proxy=socks5') + socketbackend.parse_socket_url('socket:127.0.0.1:1234?proxy=socks5:') + socketbackend.parse_socket_url('socket:127.0.0.1:1234?proxy=socks5:127.0.0.1:') + # fail: unknown proxy scheme + socketbackend.parse_socket_url('socket:127.0.0.1:1234?proxy=socks6:127.0.0.1:9050') # IPv4 s = socketbackend.parse_socket_url('socket:127.0.0.1:1234') - assert(s.host == '127.0.0.1') - assert(s.port == 1234) - assert(s.addrtype == socketbackend.AddrType.IPv4) + assert(s.target.host == '127.0.0.1') + assert(s.target.port == 1234) + assert(s.target.addrtype == socketbackend.AddrType.IPv4) + assert(s.proxytype == socketbackend.ProxyType.DIRECT) # IPv6 s = socketbackend.parse_socket_url('socket:[::1]:1235') - assert(s.host == '::1') - assert(s.port == 1235) - assert(s.addrtype == socketbackend.AddrType.IPv6) + assert(s.target.host == '::1') + assert(s.target.port == 1235) + assert(s.target.addrtype == socketbackend.AddrType.IPv6) + assert(s.proxytype == socketbackend.ProxyType.DIRECT) # Hostname s = socketbackend.parse_socket_url('socket:backup.local:1236') - assert(s.host == 'backup.local') - assert(s.port == 1236) - assert(s.addrtype == socketbackend.AddrType.NAME) + assert(s.target.host == 'backup.local') + assert(s.target.port == 1236) + assert(s.target.addrtype == socketbackend.AddrType.NAME) + assert(s.proxytype == socketbackend.ProxyType.DIRECT) + + # Tor + s = socketbackend.parse_socket_url('socket:backupserver.onion:1234?proxy=socks5:127.0.0.1:9050') + assert(s.target.host == 'backupserver.onion') + assert(s.target.port == 1234) + assert(s.target.addrtype == socketbackend.AddrType.NAME) + assert(s.proxytype == socketbackend.ProxyType.SOCKS5) + assert(s.proxytarget.host == '127.0.0.1') + assert(s.proxytarget.port == 9050) + assert(s.proxytarget.addrtype == socketbackend.AddrType.IPv4)