From eee5e90df442586b7f891df4fc0c67d273534737 Mon Sep 17 00:00:00 2001 From: Michael Schmoock Date: Thu, 16 Mar 2023 17:29:26 +0100 Subject: [PATCH] clearnet: adds clearnet plugin This can be used to enfroce clearnet connections on all peers or a given `peer_id`. --- README.md | 2 + clearnet/README.md | 10 ++++ clearnet/clearnet.py | 114 ++++++++++++++++++++++++++++++++++++++ clearnet/requirements.txt | 1 + clearnet/test_clearnet.py | 22 ++++++++ 5 files changed, 149 insertions(+) create mode 100644 clearnet/README.md create mode 100755 clearnet/clearnet.py create mode 100644 clearnet/requirements.txt create mode 100644 clearnet/test_clearnet.py diff --git a/README.md b/README.md index 104b5eb..02798ba 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Community curated plugins for Core-Lightning. | [circular][circular] | A smart rebalancing plugin for Core Lightning routing nodes | | [csvexportpays][csvexportpays] | A plugin that exports all payments to a CSV file | | [currencyrate][currencyrate] | A plugin to convert other currencies to BTC using web requests | +| [clearnet][clearnet] | A plugin that can be used to enforce clearnet connections when possible | | [donations][donations] | A simple donations page to accept donations from the web | | [drain][drain] | Draining, filling and balancing channels with automatic chunks. | | [event-websocket][event-websocket] | Exposes notifications over a Websocket | @@ -242,3 +243,4 @@ un-archive it. [blip12]: https://github.com/lightning/blips/blob/42cec1d0f66eb68c840443abb609a5a9acb34f8e/blip-0012.md [circular]: https://github.com/giovannizotta/circular [python-teos]: https://github.com/talaia-labs/python-teos +[clearnet]: https://github.com/lightningd/plugins/tree/master/clearnet diff --git a/clearnet/README.md b/clearnet/README.md new file mode 100644 index 0000000..f3f83bc --- /dev/null +++ b/clearnet/README.md @@ -0,0 +1,10 @@ +# clearnet enforcer plugin +This plugin aims to prefer usage over clearnet connections. +It does so by disconnecing TOR connections when there are known and usable +clearnet addresses. + +# Options + +# Methods +## clearnet-enforce [peer_id] +Tries to enforce clearnet on all peer or on a given peer_id diff --git a/clearnet/clearnet.py b/clearnet/clearnet.py new file mode 100755 index 0000000..85cada7 --- /dev/null +++ b/clearnet/clearnet.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +import socket +from contextlib import closing +from pyln.client import Plugin, RpcError + +plugin = Plugin() + + +def get_address_type(addrstr: str): + """ I know this can be more sophisticated, but works """ + if ".onion:" in addrstr: + return 'tor' + if addrstr[0].isdigit(): + return 'ipv4' + if addrstr.startswith("["): + return 'ipv6' + return 'dns' + + +# taken from: +# https://stackoverflow.com/questions/19196105/how-to-check-if-a-network-port-is-open +def check_socket(host: str, port: int, timeout: float = None): + """ Checks if a socket can be opened to a host """ + if host.count('.') == 3: + proto = socket.AF_INET + if host.count(':') > 1: + proto = socket.AF_INET6 + with closing(socket.socket(proto, socket.SOCK_STREAM)) as sock: + if timeout is not None: + sock.settimeout(timeout) # seconds (float) + if sock.connect_ex((host, port)) == 0: + return True + else: + return False + + +def clearnet_pid(peer: dict, messages: list): + peer_id = peer['id'] + if not peer['connected']: + messages += [f"Peer is not conencted: {peer_id}"] + return False + if get_address_type(peer['netaddr'][0]) != 'tor': + messages += [f"Already connected via clearnet: {peer_id}"] + return True + + # lets check what gossip knows about this peer + nodes = plugin.rpc.listnodes(peer_id)['nodes'] + if len(nodes) == 0: + messages += [f"Error: No gossip for: {peer_id}"] + return + addrs = [a for a in nodes[0]['addresses'] if not a['type'].startswith("tor")] + if len(addrs) == 0: + messages += [f"Error: No clearnet addresses known for: {peer_id}"] + return + + # now check addrs for open ports + for addr in addrs: + if addr['type'] == 'dns': + messages += [f"TODO: DNS lookups for: {addr['address']}"] + continue + if check_socket(addr['address'], addr['port'], 2.0): + # disconnect + result = plugin.rpc.disconnect(peer_id, True) + if len(result) != 0: + messages += [f"Error: Can't disconnect: {peer_id} {result}"] + continue + + # try clearnet connection + try: + result = plugin.rpc.connect(peer_id, addr['address'], addr['port']) + newtype = result['address']['type'] + if not newtype.startswith('tor'): + messages += [f"Established clearnet connection for: {peer_id} with {newtype}"] + return True + except RpcError: # we got an connection error, try reconnect + messages += [f"Error: Connection failed for: {peer_id} with {addr['type']}"] + try: + result = plugin.rpc.connect(peer_id) # without address + newtype = result['address']['type'] + if not newtype.startswith('tor'): + messages += [f"Established clearnet connection for: {peer_id} with {newtype}"] + return True + except RpcError: # we got a reconnection error + messages += [f"Error: Reconnection failed for: {peer_id}"] + continue + messages += [f"Reconnected: {peer_id} with {newtype}"] + continue + return False + + +@plugin.method("clearnet") +def clearnet(plugin: Plugin, peer_id: str = None): + """ Enforce a clearnet connection on all peers or a given `peer_id`.""" + if peer_id is None: + peers = plugin.rpc.listpeers(peer_id)['peers'] + else: + if not isinstance(peer_id, str) or len(peer_id) != 66: + return f"Error: Invalid peer_id: {peer_id}" + peers = plugin.rpc.listpeers(peer_id)['peers'] + if len(peers) == 0: + return f"Error: peer not found: {peer_id}" + + messages = [] + for peer in peers: + clearnet_pid(peer, messages) + return messages + + +@plugin.init() +def init(options: dict, configuration: dict, plugin: Plugin, **kwargs): + plugin.log(f"clearnet enforcer plugin initialized") + + +plugin.run() diff --git a/clearnet/requirements.txt b/clearnet/requirements.txt new file mode 100644 index 0000000..7ebb30e --- /dev/null +++ b/clearnet/requirements.txt @@ -0,0 +1 @@ +pyln-client>=0.12 diff --git a/clearnet/test_clearnet.py b/clearnet/test_clearnet.py new file mode 100644 index 0000000..9c74688 --- /dev/null +++ b/clearnet/test_clearnet.py @@ -0,0 +1,22 @@ +import os +from pyln.testing.fixtures import * # noqa: F401,F403 + +plugin_path = os.path.join(os.path.dirname(__file__), "clearnet.py") + + +def test_clearnet_starts(node_factory): + l1 = node_factory.get_node() + # Test dynamically + l1.rpc.plugin_start(plugin_path) + l1.rpc.plugin_stop(plugin_path) + l1.rpc.plugin_start(plugin_path) + l1.stop() + # Then statically + l1.daemon.opts["plugin"] = plugin_path + l1.start() + + +def test_clearnet_runs(node_factory): + pluginopt = {'plugin': plugin_path} + l1, l2 = node_factory.line_graph(2, opts=pluginopt) + l1.rpc.clearnet()