Merge pull request #9447 from f321x/node_ann_dns
Add gossip address field serialization, parsing and tests
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user