Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IPv6 support to StatusServer and related classes. #119

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
57 changes: 53 additions & 4 deletions hub/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ipaddress
import logging
import logging.handlers
import socket
import typing
import collections
from bisect import insort_right
Expand Down Expand Up @@ -153,6 +154,38 @@ def protocol_version(client_req, min_tuple, max_tuple):
return result, client_min


async def resolve_host(url: str, port: int, proto: str,
family: int = socket.AF_INET, all_results: bool = False) \
-> typing.Union[str, typing.List[str]]:
if proto not in ['udp', 'tcp']:
raise Exception("invalid protocol")
try:
if ipaddress.ip_address(url):
return [url] if all_results else url
except ValueError:
pass
loop = asyncio.get_running_loop()
records = await loop.getaddrinfo(
url, port,
proto=socket.IPPROTO_TCP if proto == 'tcp' else socket.IPPROTO_UDP,
type=socket.SOCK_STREAM if proto == 'tcp' else socket.SOCK_DGRAM,
family=family,
)
def addr_not_ipv4_mapped(rec):
_, _, _, _, sockaddr = rec
ipaddr = ipaddress.ip_address(sockaddr[0])
return ipaddr.version != 6 or not ipaddr.ipv4_mapped
records = filter(addr_not_ipv4_mapped, records)
results = [sockaddr[0] for fam, type, prot, canonname, sockaddr in records]
if not results and not all_results:
raise socket.gaierror(
socket.EAI_ADDRFAMILY,
'The specified network host does not have any network '
'addresses in the requested address family'
)
return results if all_results else results[0]


class LRUCacheWithMetrics:
__slots__ = [
'capacity',
Expand Down Expand Up @@ -577,10 +610,10 @@ def __len__(self):
def is_valid_public_ipv4(address, allow_localhost: bool = False, allow_lan: bool = False):
try:
parsed_ip = ipaddress.ip_address(address)
if parsed_ip.is_loopback and allow_localhost:
return True
if allow_lan and parsed_ip.is_private:
return True
if parsed_ip.is_loopback:
return allow_localhost
if parsed_ip.is_private:
return allow_lan
if any((parsed_ip.version != 4, parsed_ip.is_unspecified, parsed_ip.is_link_local, parsed_ip.is_loopback,
parsed_ip.is_multicast, parsed_ip.is_reserved, parsed_ip.is_private)):
return False
Expand All @@ -590,6 +623,22 @@ def is_valid_public_ipv4(address, allow_localhost: bool = False, allow_lan: bool
except (ipaddress.AddressValueError, ValueError):
return False

def is_valid_public_ipv6(address, allow_localhost: bool = False, allow_lan: bool = False):
try:
parsed_ip = ipaddress.ip_address(address)
if parsed_ip.is_loopback:
return allow_localhost
if parsed_ip.is_private:
return allow_lan
return not any((parsed_ip.version != 6, parsed_ip.is_unspecified,
parsed_ip.is_link_local, parsed_ip.is_loopback,
parsed_ip.is_multicast, parsed_ip.is_reserved,
parsed_ip.is_private, parsed_ip.ipv4_mapped))
except (ipaddress.AddressValueError, ValueError):
return False

def is_valid_public_ip(address, **kwargs):
return is_valid_public_ipv6(address, **kwargs) or is_valid_public_ipv4(address, **kwargs)

def sha256(x):
"""Simple wrapper of hashlib sha256."""
Expand Down
4 changes: 0 additions & 4 deletions hub/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,6 @@ def cs_host(self):
result = [part.strip() for part in host.split(',')]
if len(result) == 1:
result = result[0]
if result == 'localhost':
# 'localhost' resolves to ::1 (ipv6) on many systems, which fails on default setup of
# docker, using 127.0.0.1 instead forces ipv4
result = '127.0.0.1'
return result

def sane_max_sessions(self):
Expand Down
19 changes: 15 additions & 4 deletions hub/herald/service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import errno
import time
import typing
import asyncio
Expand Down Expand Up @@ -170,10 +171,20 @@ async def failover_elastic_services(self, synchronized: asyncio.Event):

async def start_status_server(self):
if self.env.udp_port and int(self.env.udp_port):
await self.status_server.start(
0, bytes.fromhex(self.env.coin.GENESIS_HASH)[::-1], self.env.country,
self.env.host, self.env.udp_port, self.env.allow_lan_udp
)
hosts = self.env.cs_host()
started = False
while not started:
try:
await self.status_server.start(
0, bytes.fromhex(self.env.coin.GENESIS_HASH)[::-1], self.env.country,
hosts, self.env.udp_port, self.env.allow_lan_udp
)
started = True
except OSError as e:
if e.errno is errno.EADDRINUSE:
await asyncio.sleep(3)
continue
raise

def _iter_start_tasks(self):
yield self.start_status_server()
Expand Down
3 changes: 2 additions & 1 deletion hub/herald/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,8 @@ async def _start_server(self, kind, *args, **kw_args):
f'{host}:{port:d} : {e!r}')
raise
else:
self.logger.info(f'{kind} server listening on {host}:{port:d}')
for s in self.servers[kind].sockets:
self.logger.info(f'{kind} server listening on {s.getsockname()[:2]}')

async def _start_external_servers(self):
"""Start listening on TCP and SSL ports, but only if the respective
Expand Down
Loading