UDP server discovery. Ref #545.

This commit is contained in:
grossmj 2016-06-15 17:37:43 -06:00
parent aa5988788d
commit 56051b1142
3 changed files with 53 additions and 1723 deletions

View File

@ -88,7 +88,6 @@ def parse_arguments(argv):
parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__) parser.add_argument("-v", "--version", help="show the version", action="version", version=__version__)
parser.add_argument("--host", help="run on the given host/IP address") parser.add_argument("--host", help="run on the given host/IP address")
parser.add_argument("--port", help="run on the given port", type=int) parser.add_argument("--port", help="run on the given port", type=int)
parser.add_argument("--service_interface", help="interface from which to extract the IP address in order to advertise the server via mDNS")
parser.add_argument("--ssl", action="store_true", help="run in SSL mode") parser.add_argument("--ssl", action="store_true", help="run in SSL mode")
parser.add_argument("--controller", action="store_true", help="start as a GNS3 controller") parser.add_argument("--controller", action="store_true", help="start as a GNS3 controller")
parser.add_argument("--config", help="Configuration file") parser.add_argument("--config", help="Configuration file")
@ -112,7 +111,6 @@ def parse_arguments(argv):
defaults = { defaults = {
"host": config.get("host", "0.0.0.0"), "host": config.get("host", "0.0.0.0"),
"port": config.get("port", 3080), "port": config.get("port", 3080),
"service_interface": config.get("service_interface", "eth0"),
"ssl": config.getboolean("ssl", False), "ssl": config.getboolean("ssl", False),
"certfile": config.get("certfile", ""), "certfile": config.get("certfile", ""),
"certkey": config.get("certkey", ""), "certkey": config.get("certkey", ""),
@ -138,7 +136,6 @@ def set_config(args):
server_config["allow_remote_console"] = str(args.allow) server_config["allow_remote_console"] = str(args.allow)
server_config["host"] = args.host server_config["host"] = args.host
server_config["port"] = str(args.port) server_config["port"] = str(args.port)
server_config["service_interface"] = str(args.service_interface)
server_config["ssl"] = str(args.ssl) server_config["ssl"] = str(args.ssl)
server_config["certfile"] = args.certfile server_config["certfile"] = args.certfile
server_config["certkey"] = args.certkey server_config["certkey"] = args.certkey
@ -229,9 +226,8 @@ def run():
CrashReport.instance() CrashReport.instance()
host = server_config["host"] host = server_config["host"]
port = int(server_config["port"]) port = int(server_config["port"])
service_interface = server_config["service_interface"]
server = WebServer.instance(host, port, service_interface) server = WebServer.instance(host, port)
try: try:
server.run() server.run()
except OSError as e: except OSError as e:

File diff suppressed because it is too large Load Diff

View File

@ -23,8 +23,10 @@ import os
import sys import sys
import signal import signal
import socket import socket
import json
import ipaddress import ipaddress
import asyncio import asyncio
import select
import aiohttp import aiohttp
import aiohttp_cors import aiohttp_cors
import functools import functools
@ -33,12 +35,11 @@ import atexit
from .route import Route from .route import Route
from .request_handler import RequestHandler from .request_handler import RequestHandler
from ..utils.zeroconf import ServiceInfo, Zeroconf
from ..utils.interfaces import interfaces
from ..config import Config from ..config import Config
from ..compute import MODULES from ..compute import MODULES
from ..compute.port_manager import PortManager from ..compute.port_manager import PortManager
from ..controller import Controller from ..controller import Controller
from ..version import __version__
# do not delete this import # do not delete this import
@ -50,18 +51,18 @@ log = logging.getLogger(__name__)
class WebServer: class WebServer:
def __init__(self, host, port, service_interface): def __init__(self, host, port):
self._host = host self._host = host
self._port = port self._port = port
self._service_interface = service_interface
self._loop = None self._loop = None
self._handler = None self._handler = None
self._start_time = time.time() self._start_time = time.time()
self._port_manager = PortManager(host) self._port_manager = PortManager(host)
self._running = False
@staticmethod @staticmethod
def instance(host=None, port=None, service_interface=None): def instance(host=None, port=None):
""" """
Singleton to return only one instance of Server. Singleton to return only one instance of Server.
@ -71,8 +72,7 @@ class WebServer:
if not hasattr(WebServer, "_instance") or WebServer._instance is None: if not hasattr(WebServer, "_instance") or WebServer._instance is None:
assert host is not None assert host is not None
assert port is not None assert port is not None
assert service_interface is not None WebServer._instance = WebServer(host, port)
WebServer._instance = WebServer(host, port, service_interface)
return WebServer._instance return WebServer._instance
@asyncio.coroutine @asyncio.coroutine
@ -177,37 +177,52 @@ class WebServer:
atexit.register(close_asyncio_loop) atexit.register(close_asyncio_loop)
def _start_zeroconf(self): def _udp_server_discovery(self):
""" """
Starts the zero configuration networking service. UDP multicast and broadcast server discovery (Linux only)
""" """
service_ip = self._host import ctypes
service_port = self._port uint32_t = ctypes.c_uint32
in_addr_t = uint32_t
valid_ip = True class in_addr(ctypes.Structure):
try: _fields_ = [('s_addr', in_addr_t)]
# test if this is a valid IP address
ipaddress.ip_address(valid_ip)
except ValueError:
valid_ip = False
if service_ip == "0.0.0.0" or service_ip == "::": class in_pktinfo(ctypes.Structure):
valid_ip = False _fields_ = [('ipi_ifindex', ctypes.c_int),
('ipi_spec_dst', in_addr),
('ipi_addr', in_addr)]
if valid_ip is False: IP_PKTINFO = 8
# look for the service interface to extract its IP address sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
local_interfaces = [interface for interface in interfaces() if interface["name"] == self._service_interface] membership = socket.inet_aton("239.42.42.1") + socket.inet_aton("0.0.0.0")
if not local_interfaces: sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, membership)
log.error("Could not find service interface {}".format(self._service_interface)) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
else: sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
service_ip = local_interfaces[0]["ip_address"] sock.setsockopt(socket.SOL_IP, IP_PKTINFO, 1)
sock.bind(("0.0.0.0", self._port))
log.info("UDP server discovery started on {}:{}".format("0.0.0.0", self._port))
# Advertise the server with DNS multicast while self._running:
info = ServiceInfo("_http._tcp.local.", "GNS3VM._http._tcp.local.", socket.inet_aton(service_ip), service_port, 0, 0, properties={}) ready_to_read, _, _ = select.select([sock], [], [], 1.0)
zeroconf = Zeroconf(interfaces=[self._host]) if ready_to_read:
zeroconf.register_service(info) data, ancdata, _, address = sock.recvmsg(255, socket.CMSG_LEN(255))
return zeroconf, info cmsg_level, cmsg_type, cmsg_data = ancdata[0]
if cmsg_level == socket.SOL_IP and cmsg_type == IP_PKTINFO:
pktinfo = in_pktinfo.from_buffer_copy(cmsg_data)
request_address = ipaddress.IPv4Address(memoryview(pktinfo.ipi_addr).tobytes())
log.debug("UDP server discovery request received on {} using {}".format(socket.if_indextoname(pktinfo.ipi_ifindex),
request_address))
local_address = ipaddress.IPv4Address(memoryview(pktinfo.ipi_spec_dst).tobytes())
server_info = {"version": __version__,
"ip": str(local_address),
"port": self._port}
data = json.dumps(server_info)
sock.sendto(data.encode(), address)
log.debug("Sent server info to {}: {}".format(local_address, data))
time.sleep(1) # this is to prevent too many request to slow down the server
log.debug("UDP discovery stopped")
def run(self): def run(self):
""" """
@ -267,13 +282,17 @@ class WebServer:
self._handler = app.make_handler(handler=RequestHandler) self._handler = app.make_handler(handler=RequestHandler)
server = self._run_application(self._handler, ssl_context) server = self._run_application(self._handler, ssl_context)
self._loop.run_until_complete(server) self._loop.run_until_complete(server)
self._running = True
self._signal_handling() self._signal_handling()
self._exit_handling() self._exit_handling()
if server_config.getboolean("shell"): if server_config.getboolean("shell"):
asyncio.async(self.start_shell()) asyncio.async(self.start_shell())
zeroconf, info = self._start_zeroconf() if sys.platform.startswith("linux"):
# UDP discovery is only supported on
self._loop.run_in_executor(None, self._udp_server_discovery)
try: try:
self._loop.run_forever() self._loop.run_forever()
except TypeError as e: except TypeError as e:
@ -282,8 +301,7 @@ class WebServer:
# TypeError: async() takes 1 positional argument but 3 were given # TypeError: async() takes 1 positional argument but 3 were given
log.warning("TypeError exception in the loop {}".format(e)) log.warning("TypeError exception in the loop {}".format(e))
finally: finally:
zeroconf.unregister_service(info) self._running = False
zeroconf.close()
if self._handler and self._loop.is_running(): if self._handler and self._loop.is_running():
self._loop.run_until_complete(self._handler.finish_connections()) self._loop.run_until_complete(self._handler.finish_connections())
server.close() server.close()