backup: Add support for IPv6 addresses in socket backend

Support the bracketed `socket:[::]:1234` syntax for IPv6 addresses.

Also, add factor out the socket URI parsing to a function and add
tests for it. As a forward compatibility measure, reject query strings
(`?a=b`) because I intend to use these for parameters such as SOCKS5
proxy (for Tor) in a future patch.
This commit is contained in:
Wladimir J. van der Laan
2021-02-04 18:10:37 +01:00
committed by Christian Decker
parent ca75d7e132
commit cdfcd5a2fe
3 changed files with 96 additions and 9 deletions

View File

@@ -15,7 +15,8 @@ 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 - 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. to store them. The server side does not need to be running c-lightning, nor have it installed.
The backend URL format is `socket:<host>:<port>`. For example `socket:127.0.0.1:1234`. The backend URL format is `socket:<host>:<port>`. 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`. To run the server against a local backend use `backup-cli server file://.../ 127.0.0.1:1234`.

View File

@@ -1,21 +1,71 @@
import json from collections import namedtuple
import logging, socket, struct import json, logging, socket, re, struct
from typing import Tuple, Iterator from typing import Tuple, Iterator
from urllib.parse import urlparse from urllib.parse import urlparse, parse_qs
from backend import Backend, Change from backend import Backend, Change
from protocol import PacketType, recvall, PKT_CHANGE_TYPES, change_from_packet, packet_from_change, send_packet, recv_packet from protocol import PacketType, recvall, PKT_CHANGE_TYPES, change_from_packet, packet_from_change, send_packet, recv_packet
SocketURLInfo = namedtuple('SocketURLInfo', ['host', 'port', 'addrtype'])
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:...')
if url.path.startswith('['): # bracketed IPv6 address
eidx = url.path.find(']')
if eidx == -1:
raise ValueError('Unterminated bracketed host address.')
host = url.path[1:eidx]
addrtype = AddrType.IPv6
eidx += 1
if eidx >= len(url.path) or url.path[eidx] != ':':
raise ValueError('Port number missing.')
eidx += 1
else:
eidx = url.path.find(':')
if eidx == -1:
raise ValueError('Port number missing.')
host = url.path[0:eidx]
if re.match('\d+\.\d+\.\d+\.\d+$', host): # matches IPv4 address format
addrtype = AddrType.IPv4
else:
addrtype = AddrType.NAME
eidx += 1
try:
port = int(url.path[eidx:])
except ValueError:
raise ValueError('Invalid port number')
# parse query parameters
# reject unknown parameters (currently all of them)
qs = parse_qs(url.query)
if len(qs):
raise ValueError('Invalid query string')
return SocketURLInfo(host=host, port=port, addrtype=addrtype)
class SocketBackend(Backend): class SocketBackend(Backend):
def __init__(self, destination: str, create: bool): def __init__(self, destination: str, create: bool):
self.version = None self.version = None
self.prev_version = None self.prev_version = None
self.destination = destination self.destination = destination
self.url = urlparse(self.destination) self.url = parse_socket_url(destination)
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if self.url.addrtype == AddrType.IPv6:
(host, port) = self.url.path.split(':') self.sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
logging.info('Initialized socket backend') else: # TODO NAME is assumed to be IPv4 for now
self.sock.connect((host, int(port))) 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))
logging.info('Connected to {}'.format(destination)) logging.info('Connected to {}'.format(destination))
def _send_packet(self, typ: int, payload: bytes) -> None: def _send_packet(self, typ: int, payload: bytes) -> None:

View File

@@ -1,5 +1,6 @@
from backend import Backend from backend import Backend
from filebackend import FileBackend from filebackend import FileBackend
import socketbackend
from flaky import flaky from flaky import flaky
from pyln.testing.fixtures import * # noqa: F401,F403 from pyln.testing.fixtures import * # noqa: F401,F403
from pyln.testing.utils import sync_blockheight from pyln.testing.utils import sync_blockheight
@@ -279,3 +280,38 @@ def test_compact(bitcoind, directory, node_factory):
l1.rpc.backup_compact() l1.rpc.backup_compact()
tmp = tempfile.TemporaryDirectory() tmp = tempfile.TemporaryDirectory()
subprocess.check_call([cli_path, "restore", bdest, tmp.name]) subprocess.check_call([cli_path, "restore", bdest, tmp.name])
def test_parse_socket_url():
with pytest.raises(ValueError):
# fail: invalid url scheme
socketbackend.parse_socket_url('none')
# fail: no port number
socketbackend.parse_socket_url('socket:127.0.0.1')
socketbackend.parse_socket_url('socket:127.0.0.1:')
# fail: unbracketed IPv6
socketbackend.parse_socket_url('socket:::1:1234')
# fail: no port number IPv6
socketbackend.parse_socket_url('socket:[::1]')
socketbackend.parse_socket_url('socket:[::1]:')
# fail: invalid port number
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')
# 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)
# IPv6
s = socketbackend.parse_socket_url('socket:[::1]:1235')
assert(s.host == '::1')
assert(s.port == 1235)
assert(s.addrtype == socketbackend.AddrType.IPv6)
# 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)