1
0

Merge pull request #9447 from f321x/node_ann_dns

Add gossip address field serialization, parsing and tests
This commit is contained in:
ThomasV
2025-01-17 10:52:14 +01:00
committed by GitHub
3 changed files with 100 additions and 6 deletions

View File

@@ -23,6 +23,7 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import ipaddress
import time
import random
import os
@@ -41,7 +42,7 @@ from electrum_ecc import ECPubkey
from .sql_db import SqlDB, sql
from . import constants, util
from .util import profiler, get_headers_dir, is_ip_address, json_normalize, UserFacingException
from .util import profiler, get_headers_dir, is_ip_address, json_normalize, UserFacingException, is_private_netaddress
from .logging import Logger
from .lntransport import LNPeerAddr
from .lnutil import (format_short_channel_id, ShortChannelID,
@@ -192,6 +193,48 @@ class NodeInfo(NamedTuple):
payload_dict = decode_msg(raw)[1]
return NodeInfo.from_msg(payload_dict)
@staticmethod
def to_addresses_field(hostname: str, port: int) -> bytes:
"""Encodes a hostname/port pair into a BOLT-7 'addresses' field."""
if (NodeInfo.invalid_announcement_hostname(hostname)
or port is None or port <= 0 or port > 65535):
return b''
port_bytes = port.to_bytes(2, 'big')
if is_ip_address(hostname): # ipv4 or ipv6
ip_addr = ipaddress.ip_address(hostname)
if ip_addr.version == 4:
return b'\x01' + ip_addr.packed + port_bytes
elif ip_addr.version == 6:
return b'\x02' + ip_addr.packed + port_bytes
elif hostname.endswith('.onion'): # Tor onion v3
onion_addr: bytes = base64.b32decode(hostname[:-6], casefold=True)
return b'\x04' + onion_addr + port_bytes
else:
try:
hostname_ascii: bytes = hostname.encode('ascii')
except UnicodeEncodeError:
# encoding single characters to punycode (according to spec) doesn't make sense
# as you can't differentiate them from regular ascii? encoding the whole string to punycode
# doesn't work either as the receiver would interpret it as regular ascii.
# hostname_ascii: bytes = hostname.encode('punycode')
return b''
if len(hostname_ascii) + 3 > 258: # + 1 byte for length and 2 for port
return b'' # too long
return b'\x05' + len(hostname_ascii).to_bytes(1, "big") + hostname_ascii + port_bytes
@staticmethod
def invalid_announcement_hostname(hostname: Optional[str]) -> bool:
"""Returns True if hostname unsuited for publishing in a NodeAnnouncement."""
if (hostname is None or hostname == ""
or is_private_netaddress(hostname)
or hostname.startswith("http://") # not catching 'http' due to onion addresses
or hostname.startswith("https://")):
return True
if hostname.endswith('.onion'):
if len(hostname) != 62: # not an onion v3 link (probably onion v2)
return True
return False
@staticmethod
def parse_addresses_field(addresses_field):
buf = addresses_field
@@ -216,15 +259,18 @@ class NodeInfo(NamedTuple):
if is_ip_address(ipv6_addr) and port != 0:
addresses.append((ipv6_addr, port))
elif atype == 3: # onion v2
host = base64.b32encode(read(10)) + b'.onion'
host = host.decode('ascii').lower()
port = int.from_bytes(read(2), 'big')
addresses.append((host, port))
read(12) # we skip onion v2 as it is deprecated
elif atype == 4: # onion v3
host = base64.b32encode(read(35)) + b'.onion'
host = host.decode('ascii').lower()
port = int.from_bytes(read(2), 'big')
addresses.append((host, port))
elif atype == 5: # dns hostname
len_hostname = int.from_bytes(read(1), 'big')
host = read(len_hostname).decode('ascii')
port = int.from_bytes(read(2), 'big')
if not NodeInfo.invalid_announcement_hostname(host) and port > 0:
addresses.append((host, port))
else:
# unknown address type
# we don't know how long it is -> have to escape

View File

@@ -1500,6 +1500,7 @@ class Peer(Logger, EventListener):
self.maybe_mark_open(chan)
def send_node_announcement(self, alias:str):
from .channel_db import NodeInfo
timestamp = int(time.time())
node_id = privkey_to_pubkey(self.privkey)
features = self.features.for_node_announcement()
@@ -1508,7 +1509,11 @@ class Peer(Logger, EventListener):
rgb_color = bytes.fromhex('000000')
alias = bytes(alias, 'utf8')
alias += bytes(32 - len(alias))
addresses = b''
addr = self.lnworker.config.LIGHTNING_LISTEN
hostname, port = addr.split(':')
if port is None: # use default port if not specified
port = 9735
addresses = NodeInfo.to_addresses_field(hostname, int(port))
raw_msg = encode_msg(
"node_announcement",
flen=flen,

View File

@@ -8,6 +8,7 @@ from electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNo
from electrum.lnonion import OnionRoutingFailure
from electrum.util import bfh
from electrum.lnutil import ShortChannelID, LnFeatures
from electrum.channel_db import NodeInfo
from electrum import constants
from . import ElectrumTestCase
@@ -370,3 +371,45 @@ class TestLNMsg(ElectrumTestCase):
OnionWireSerializer.decode_msg(orf2.to_bytes())
self.assertEqual(None, orf2.decode_data())
def test_address_parsing_and_serialization(self):
"""Tests the NodeInfo bolt7 node_announcement addresses field serialization and parsing"""
taf = NodeInfo.to_addresses_field
paf = NodeInfo.parse_addresses_field
# -- INVALID INPUTS --
invalid_inputs_parsing = (
b'', # empty input
b'\x06\x00', # address type 6 (\x06) is not specified
)
invalid_inputs_serialization = (
("::1", 9735), # local ipv6
("::", 9735), # local ipv6
("::1", 0), # local host, invalid port
("::1", 65536), # local host, invalid port
("127.0.0.1", 9735), # local ipv4
("localhost", 9735), # local host
("domain.com", 0), # domain, invalid port
("domain.com", 65536), # domain, invalid port
("domain.com", -1), # domain, invalid port
("expyuzz4wqqyqhjn.onion", 9735), # onion v2, not supported
("", 9735), # empty address
)
for invalid_input in invalid_inputs_parsing:
self.assertEqual(paf(invalid_input), [])
for host, port in invalid_inputs_serialization:
self.assertEqual(taf(host, port), b'')
# -- VALID INPUTS --
valid_inputs = (
("34.138.100.228", 9735), # ipv4
("2001:41d0:0001:b40d:0000:0000:0000:0001", 9735), # ipv6
("2gzyxa5ihm7nsggfxnu52rck2vv4rvmdlkiu3zzui5du4xyclen53wid.onion", 9735), # onion v3
("ecb.europa.eu", 8624), # domain
)
valid_inputs_with_defined_output = [
[["2001:41d0:1:b40d::1", 9735], [("2001:41d0:0001:b40d:0000:0000:0000:0001", 9735)]] # ipv6
]
for host, port in valid_inputs:
self.assertEqual(paf(taf(host, port)), [(host, port)])
for input_, output in valid_inputs_with_defined_output:
self.assertEqual(paf(taf(*input_)), output)