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
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import os
|
import os
|
||||||
@@ -41,7 +42,7 @@ from electrum_ecc import ECPubkey
|
|||||||
|
|
||||||
from .sql_db import SqlDB, sql
|
from .sql_db import SqlDB, sql
|
||||||
from . import constants, util
|
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 .logging import Logger
|
||||||
from .lntransport import LNPeerAddr
|
from .lntransport import LNPeerAddr
|
||||||
from .lnutil import (format_short_channel_id, ShortChannelID,
|
from .lnutil import (format_short_channel_id, ShortChannelID,
|
||||||
@@ -192,6 +193,48 @@ class NodeInfo(NamedTuple):
|
|||||||
payload_dict = decode_msg(raw)[1]
|
payload_dict = decode_msg(raw)[1]
|
||||||
return NodeInfo.from_msg(payload_dict)
|
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
|
@staticmethod
|
||||||
def parse_addresses_field(addresses_field):
|
def parse_addresses_field(addresses_field):
|
||||||
buf = addresses_field
|
buf = addresses_field
|
||||||
@@ -216,15 +259,18 @@ class NodeInfo(NamedTuple):
|
|||||||
if is_ip_address(ipv6_addr) and port != 0:
|
if is_ip_address(ipv6_addr) and port != 0:
|
||||||
addresses.append((ipv6_addr, port))
|
addresses.append((ipv6_addr, port))
|
||||||
elif atype == 3: # onion v2
|
elif atype == 3: # onion v2
|
||||||
host = base64.b32encode(read(10)) + b'.onion'
|
read(12) # we skip onion v2 as it is deprecated
|
||||||
host = host.decode('ascii').lower()
|
|
||||||
port = int.from_bytes(read(2), 'big')
|
|
||||||
addresses.append((host, port))
|
|
||||||
elif atype == 4: # onion v3
|
elif atype == 4: # onion v3
|
||||||
host = base64.b32encode(read(35)) + b'.onion'
|
host = base64.b32encode(read(35)) + b'.onion'
|
||||||
host = host.decode('ascii').lower()
|
host = host.decode('ascii').lower()
|
||||||
port = int.from_bytes(read(2), 'big')
|
port = int.from_bytes(read(2), 'big')
|
||||||
addresses.append((host, port))
|
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:
|
else:
|
||||||
# unknown address type
|
# unknown address type
|
||||||
# we don't know how long it is -> have to escape
|
# we don't know how long it is -> have to escape
|
||||||
|
|||||||
@@ -1500,6 +1500,7 @@ class Peer(Logger, EventListener):
|
|||||||
self.maybe_mark_open(chan)
|
self.maybe_mark_open(chan)
|
||||||
|
|
||||||
def send_node_announcement(self, alias:str):
|
def send_node_announcement(self, alias:str):
|
||||||
|
from .channel_db import NodeInfo
|
||||||
timestamp = int(time.time())
|
timestamp = int(time.time())
|
||||||
node_id = privkey_to_pubkey(self.privkey)
|
node_id = privkey_to_pubkey(self.privkey)
|
||||||
features = self.features.for_node_announcement()
|
features = self.features.for_node_announcement()
|
||||||
@@ -1508,7 +1509,11 @@ class Peer(Logger, EventListener):
|
|||||||
rgb_color = bytes.fromhex('000000')
|
rgb_color = bytes.fromhex('000000')
|
||||||
alias = bytes(alias, 'utf8')
|
alias = bytes(alias, 'utf8')
|
||||||
alias += bytes(32 - len(alias))
|
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(
|
raw_msg = encode_msg(
|
||||||
"node_announcement",
|
"node_announcement",
|
||||||
flen=flen,
|
flen=flen,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from electrum.lnmsg import (read_bigsize_int, write_bigsize_int, FieldEncodingNo
|
|||||||
from electrum.lnonion import OnionRoutingFailure
|
from electrum.lnonion import OnionRoutingFailure
|
||||||
from electrum.util import bfh
|
from electrum.util import bfh
|
||||||
from electrum.lnutil import ShortChannelID, LnFeatures
|
from electrum.lnutil import ShortChannelID, LnFeatures
|
||||||
|
from electrum.channel_db import NodeInfo
|
||||||
from electrum import constants
|
from electrum import constants
|
||||||
|
|
||||||
from . import ElectrumTestCase
|
from . import ElectrumTestCase
|
||||||
@@ -370,3 +371,45 @@ class TestLNMsg(ElectrumTestCase):
|
|||||||
OnionWireSerializer.decode_msg(orf2.to_bytes())
|
OnionWireSerializer.decode_msg(orf2.to_bytes())
|
||||||
self.assertEqual(None, orf2.decode_data())
|
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