diff --git a/resources/manual-connect.ui b/resources/manual-connect.ui index 80efecb24..44ceb7cb7 100644 --- a/resources/manual-connect.ui +++ b/resources/manual-connect.ui @@ -192,7 +192,7 @@ - + True False Or connect to the address below @@ -204,7 +204,7 @@ - + True False 192.168.0.50:42001 @@ -222,6 +222,25 @@ 4 + + + True + False + [2a02::1]:42001 + + + + + + + + False + True + 5 + + False diff --git a/src/auth.py b/src/auth.py index fde8d402b..26672de7f 100644 --- a/src/auth.py +++ b/src/auth.py @@ -69,13 +69,13 @@ def get_server_creds(self): def get_cached_cert(self, hostname, ip_info): try: - return self.remote_certs["%s.%s" % (hostname, ip_info.ip4_address)] + return self.remote_certs["%s.%s" % (hostname, ip_info)] except KeyError: return None def process_remote_cert(self, hostname, ip_info, server_data): if server_data is None: - return False + return util.CertProcessingResult.FAILURE decoded = base64.decodebytes(server_data) hasher = hashlib.sha256() @@ -89,11 +89,20 @@ def process_remote_cert(self, hostname, ip_info, server_data): logging.debug("Decryption failed for remote '%s': %s" % (hostname, str(e))) cert = None + res = util.CertProcessingResult.FAILURE if cert: - self.remote_certs["%s.%s" % (hostname, ip_info.ip4_address)] = cert - return True - else: - return False + key = "%s.%s" % (hostname, ip_info) + val = self.remote_certs.get(key) + + if val is None: + res = util.CertProcessingResult.CERT_INSERTED + elif val == cert: + res = util.CertProcessingResult.CERT_UP_TO_DATE + return res + else: + res = util.CertProcessingResult.CERT_UPDATED + self.remote_certs[key] = cert + return res def get_encoded_local_cert(self): hasher = hashlib.sha256() @@ -133,6 +142,8 @@ def _make_key_cert_pair(self): if self.ip_info.ip4_address is not None: alt_names.append(x509.IPAddress(ipaddress.IPv4Address(self.ip_info.ip4_address))) + if self.ip_info.ip6_address is not None: + alt_names.append(x509.IPAddress(ipaddress.IPv6Address(self.ip_info.ip6_address))) builder = builder.add_extension(x509.SubjectAlternativeName(alt_names), critical=True) diff --git a/src/networkmonitor.py b/src/networkmonitor.py index c9894c165..0cdffaf6f 100644 --- a/src/networkmonitor.py +++ b/src/networkmonitor.py @@ -102,65 +102,89 @@ def get_valid_interface_infos(self): try: ip4 = iface[netifaces.AF_INET][0] + except KeyError: + ip4 = None + try: + ip6 = iface[netifaces.AF_INET6][0] + except KeyError: + ip6 = None - try: - ip6 = iface[netifaces.AF_INET6][0] - except KeyError: - ip6 = None - + if ip4 is not None or ip6 is not None: info = util.InterfaceInfo(ip4, ip6, iname) valid.append(info) - except KeyError: - continue return valid def get_default_interface_info(self): - ip = self.get_default_ip() + ip4 = self.get_default_ip4() + ip6 = self.get_default_ip6() fallback_info = None for info in self.get_valid_interface_infos(): if fallback_info is None: fallback_info = info try: - if ip == info.ip4["addr"]: + if ip4 == info.ip4["addr"]: + return info + except: + pass + try: + if ip6 == info.ip6["addr"]: return info except: pass return fallback_info - def get_default_ip(self): - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: - try: - s.connect(("8.8.8.8", 80)) - except OSError as e: - # print("Unable to retrieve IP address: %s" % str(e)) - return "0.0.0.0" + def get_default_ip(self, ip_version): + with socket.socket(ip_version, socket.SOCK_DGRAM) as s: + if ip_version == socket.AF_INET: + try: + s.connect(("8.8.8.8", 80)) + except OSError as e: + # print("Unable to retrieve IP address: %s" % str(e)) + return "0.0.0.0" + else: + try: + s.connect(("2001:4860:4860::8888", 80)) + except OSError as e: + # print("Unable to retrieve IP address: %s" % str(e)) + return "[::]" ans = s.getsockname()[0] return ans + def get_default_ip4(self): + return self.get_default_ip(socket.AF_INET) + + def get_default_ip6(self): + return self.get_default_ip(socket.AF_INET6) + def emit_state_changed(self): logging.debug("Network state changed: online = %s" % str(self.online)) self.emit("state-changed", self.online) # TODO: Do this with libnm def same_subnet(self, other_ip_info): - iface = ipaddress.IPv4Interface("%s/%s" % (self.current_ip_info.ip4_address, - self.current_ip_info.ip4["netmask"])) + if self.current_ip_info.ip4_address is not None and other_ip_info.ip4_address is not None: + iface = ipaddress.IPv4Interface("%s/%s" % (self.current_ip_info.ip4_address, + self.current_ip_info.ip4["netmask"])) - my_net = iface.network + my_net = iface.network - if my_net is None: - # We're more likely to have failed here than to have found something on a different subnet. - return True + if my_net is None: + # We're more likely to have failed here than to have found something on a different subnet. + return True - if my_net.netmask.exploded == "255.255.255.255": - logging.warning("Discovery: netmask is 255.255.255.255 - are you on a vpn?") - return False + if my_net.netmask.exploded == "255.255.255.255": + logging.warning("Discovery: netmask is 255.255.255.255 - are you on a vpn?") + return False - for addr in list(my_net.hosts()): - if other_ip_info.ip4_address == addr.exploded: - return True + for addr in list(my_net.hosts()): + if other_ip_info.ip4_address == addr.exploded: + return True + return False + if self.current_ip_info.ip6_address is not None and other_ip_info.ip6_address is not None: + return True # TODO: Verify that this is actually true + logging.debug("No IP address found: %s" % (self)) return False diff --git a/src/remote.py b/src/remote.py index b94a82acd..0ac1f0f7b 100644 --- a/src/remote.py +++ b/src/remote.py @@ -4,6 +4,7 @@ import gettext import threading import logging +import socket from gi.repository import GObject, GLib @@ -79,6 +80,8 @@ def __init__(self, ident, hostname, display_hostname, ip_info, port, local_ident self.has_zc_presence = False # This is currently unused. + self.last_register = 0 + def start_remote_thread(self): # func = lambda: return @@ -104,7 +107,7 @@ def remote_thread_v1(self): def run_secure_loop(): logging.debug("Remote: Starting a new connection loop for %s (%s:%d)" - % (self.display_hostname, self.ip_info.ip4_address, self.port)) + % (self.display_hostname, self.ip_info, self.port)) cert = auth.get_singleton().get_cached_cert(self.hostname, self.ip_info) creds = grpc.ssl_channel_credentials(cert) @@ -121,7 +124,7 @@ def run_secure_loop(): if not self.ping_timer.is_set(): logging.debug("Remote: Unable to establish secure connection with %s (%s:%d). Trying again in %ds" - % (self.display_hostname, self.ip_info.ip4_address, self.port, CHANNEL_RETRY_WAIT_TIME)) + % (self.display_hostname, self.ip_info, self.port, CHANNEL_RETRY_WAIT_TIME)) self.ping_timer.wait(CHANNEL_RETRY_WAIT_TIME) return True # run_secure_loop() @@ -134,13 +137,13 @@ def run_secure_loop(): if self.busy: logging.debug("Remote Ping: Skipping keepalive ping to %s (%s:%d) (busy)" - % (self.display_hostname, self.ip_info.ip4_address, self.port)) + % (self.display_hostname, self.ip_info, self.port)) self.busy = False else: try: # t = GLib.get_monotonic_time() logging.debug("Remote Ping: to %s (%s:%d)" - % (self.display_hostname, self.ip_info.ip4_address, self.port)) + % (self.display_hostname, self.ip_info, self.port)) self.stub.Ping(warp_pb2.LookupName(id=self.local_ident, readable_name=util.get_hostname()), timeout=5) @@ -150,7 +153,7 @@ def run_secure_loop(): self.set_remote_status(RemoteStatus.AWAITING_DUPLEX) if self.check_duplex_connection(): logging.debug("Remote: Connected to %s (%s:%d)" - % (self.display_hostname, self.ip_info.ip4_address, self.port)) + % (self.display_hostname, self.ip_info, self.port)) self.set_remote_status(RemoteStatus.ONLINE) @@ -161,12 +164,12 @@ def run_secure_loop(): duplex_fail_counter += 1 if duplex_fail_counter > DUPLEX_MAX_FAILURES: logging.debug("Remote: CheckDuplexConnection to %s (%s:%d) failed too many times" - % (self.display_hostname, self.ip_info.ip4_address, self.port)) + % (self.display_hostname, self.ip_info, self.port)) self.ping_timer.wait(CHANNEL_RETRY_WAIT_TIME) return True except grpc.RpcError as e: logging.debug("Remote: Ping failed, shutting down %s (%s:%d)" - % (self.display_hostname, self.ip_info.ip4_address, self.port)) + % (self.display_hostname, self.ip_info, self.port)) break self.ping_timer.wait(CONNECTED_PING_TIME if self.status == RemoteStatus.ONLINE else DUPLEX_WAIT_PING_TIME) @@ -185,7 +188,7 @@ def run_secure_loop(): continue except Exception as e: logging.critical("!! Major problem starting connection loop for %s (%s:%d): %s" - % (self.display_hostname, self.ip_info.ip4_address, self.port, e)) + % (self.display_hostname, self.ip_info, self.port, e)) self.set_remote_status(RemoteStatus.OFFLINE) self.run_thread_alive = False @@ -195,7 +198,9 @@ def remote_thread_v2(self): self.emit_machine_info_changed() # Let's make sure the button doesn't have junk in it if we fail to connect. - logging.debug("Remote: Attempting to connect to %s (%s) - api version 2" % (self.display_hostname, self.ip_info.ip4_address)) + remote_ip, _, ip_version = self.ip_info.get_usable_ip() + logging.debug("Remote: Attempting to connect to %s (%s) - api version 2" % (self.display_hostname, remote_ip)) + remote_ip = remote_ip if ip_version == socket.AF_INET else "[%s]" % (remote_ip,) self.set_remote_status(RemoteStatus.INIT_CONNECTING) @@ -212,7 +217,7 @@ def run_secure_loop(): ('grpc.http2.min_ping_interval_without_data_ms', 5000) ) - with grpc.secure_channel("%s:%d" % (self.ip_info.ip4_address, self.port), creds, options=opts) as channel: + with grpc.secure_channel("%s:%d" % (remote_ip, self.port), creds, options=opts) as channel: def channel_state_changed(state): if state != grpc.ChannelConnectivity.READY: @@ -335,7 +340,7 @@ def rpc_call(self, func, *args, **kargs): except Exception as e: # exception concurrent.futures.thread.BrokenThreadPool is not available in bionic/python3 < 3.7 logging.critical("!! RPC threadpool failure while submitting call to %s (%s:%d): %s" - % (self.display_hostname, self.ip_info.ip4_address, self.port, e)) + % (self.display_hostname, self.ip_info, self.port, e)) # Not added to thread pool def check_duplex_connection(self): diff --git a/src/remote_registration.py b/src/remote_registration.py index c75086e5c..a6b28bcef 100644 --- a/src/remote_registration.py +++ b/src/remote_registration.py @@ -55,9 +55,9 @@ def start_registration_servers(self): self.reg_server_v2.stop(grace=2).wait() self.reg_server_v2 = None - logging.debug("Starting v1 registration server (%s) with port %d" % (self.ip_info.ip4_address, self.port)) + logging.debug("Starting v1 registration server (%s) with port %d" % (self.ip_info, self.port)) self.reg_server_v1 = RegistrationServer_v1(self.ip_info, self.port) - logging.debug("Starting v2 registration server (%s) with auth port %d" % (self.ip_info.ip4_address, self.auth_port)) + logging.debug("Starting v2 registration server (%s) with auth port %d" % (self.ip_info, self.auth_port)) self.reg_server_v2 = RegistrationServer_v2(self.ip_info, self.auth_port) def shutdown_registration_servers(self): @@ -81,7 +81,7 @@ def register(self, ident, hostname, ip_info, port, auth_port, api_version): with self.reg_lock: self.active_registrations[ident] = details - ret = False + ret = None if api_version == "1": ret = register_v1(details) @@ -104,25 +104,25 @@ def register_v1(details): # or we tell the auth object to shutdown, in which case the request timer will cancel and return # here immediately (with None) - logging.debug("Registering with %s (%s:%d) - api version 1" % (details.hostname, details.ip_info.ip4_address, details.port)) + logging.debug("Registering with %s (%s:%d) - api version 1" % (details.hostname, details.ip_info, details.port)) success = retrieve_remote_cert(details) - if not success: + if success == util.CertProcessingResult.FAILURE: logging.debug("Unable to register with %s (%s:%d) - api version 1" - % (details.hostname, details.ip_info.ip4_address, details.port)) + % (details.hostname, details.ip_info, details.port)) return False return True def retrieve_remote_cert(details): - logging.debug("Auth: Starting a new RequestLoop for '%s' (%s:%d)" % (details.hostname, details.ip_info.ip4_address, details.port)) + logging.debug("Auth: Starting a new RequestLoop for '%s' (%s:%d)" % (details.hostname, details.ip_info, details.port)) details.request = Request(details.ip_info, details.port) data = details.request.request() if data is None or details.cancelled: - return False + return util.CertProcessingResult.FAILURE return auth.get_singleton().process_remote_cert(details.hostname, details.ip_info, @@ -137,22 +137,25 @@ def __init__(self, ip_info, port): self.port = port def request(self): - logging.debug("Auth: Requesting cert from remote (%s:%d)" % (self.ip_info.ip4_address, self.port)) + logging.debug("Auth: Requesting cert from remote (%s:%d)" % (self.ip_info, self.port)) + + remote_ip, _, ip_version = self.ip_info.get_usable_ip() try: - server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + ip = remote_ip if ip_version == socket.AF_INET else "[%s]" % (remote_ip,) + server_sock = socket.socket(ip_version, socket.SOCK_DGRAM) server_sock.settimeout(5.0) - server_sock.sendto(REQUEST, (self.ip_info.ip4_address, self.port)) + server_sock.sendto(REQUEST, (ip, self.port)) reply, addr = server_sock.recvfrom(2000) - if addr == (self.ip_info.ip4_address, self.port): + if addr == (remote_ip, self.port): return reply except socket.timeout: logging.debug("Auth: Cert request failed from remote (%s:%d) - (Is their udp port blocked?" - % (self.ip_info.ip4_address, self.port)) + % (self.ip_info, self.port)) except socket.error as e: - logging.critical("Something wrong with cert request (%s:%s): " % (self.ip_info.ip4_address, self.port, e)) + logging.critical("Something wrong with cert request (%s:%s): " % (remote_ip, self.port, e)) return None @@ -163,34 +166,43 @@ def __init__(self, ip_info, port): self.ip_info = ip_info self.port = port - self.thread = threading.Thread(target=self.serve_cert_thread) - self.thread.start() + self.thread4 = threading.Thread(target=self.serve_cert_thread, args=(socket.AF_INET,)) + self.thread6 = threading.Thread(target=self.serve_cert_thread, args=(socket.AF_INET6,)) + self.thread4.start() + self.thread6.start() - def serve_cert_thread(self): - try: - server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - # server_sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - server_sock.settimeout(1.0) - server_sock.bind((self.ip_info.ip4_address, self.port)) - except socket.error as e: - logging.critical("Could not create udp socket for cert requests: %s" % str(e)) - return + def serve_cert_thread(self, ip_version): + local_ip = None + if ip_version == socket.AF_INET: + local_ip = self.ip_info.ip4_address + elif ip_version == socket.AF_INET6: + local_ip = self.ip_info.ip6_address - while True: + if local_ip is not None: try: - data, address = server_sock.recvfrom(2000) - - if data == REQUEST: - cert_data = auth.get_singleton().get_encoded_local_cert() - server_sock.sendto(cert_data, address) - except socket.timeout as e: - if self.exit: - server_sock.close() - break + server_sock = socket.socket(ip_version, socket.SOCK_DGRAM) + server_sock.settimeout(1.0) + server_sock.bind((local_ip, self.port)) + except socket.error as e: + logging.critical("Could not create udp socket for cert requests: %s" % str(e)) + return + + while True: + try: + data, address = server_sock.recvfrom(2000) + + if data == REQUEST: + cert_data = auth.get_singleton().get_encoded_local_cert() + server_sock.sendto(cert_data, address) + except socket.timeout as e: + if self.exit: + server_sock.close() + break def stop(self): self.exit = True - self.thread.join() + self.thread4.join() + self.thread6.join() ####################### api v2 @@ -200,13 +212,12 @@ def register_v2(details): # This will block if the remote's warp udp port is closed, until either the port is unblocked # or we tell the auth object to shutdown, in which case the request timer will cancel and return # here immediately (with None) + logging.debug("Registering with %s (%s:%d) - api version 2" % (details.hostname, details.ip_info, details.auth_port)) - logging.debug("Registering with %s (%s:%d) - api version 2" % (details.hostname, details.ip_info.ip4_address, details.auth_port)) - - success = False + success = None remote_thread = threading.Thread(target=register_with_remote_thread, args=(details,), name="remote-auth-thread-%s" % id) - logging.debug("remote-registration-thread-%s-%s:%d-%s" % (details.hostname, details.ip_info.ip4_address, details.auth_port, details.ident)) + logging.debug("remote-registration-thread-%s-%s:%d-%s" % (details.hostname, details.ip_info, details.auth_port, details.ident)) remote_thread.start() remote_thread.join() @@ -215,30 +226,40 @@ def register_v2(details): details.ip_info, details.locked_cert) - if not success: + if success == util.CertProcessingResult.FAILURE: logging.debug("Unable to register with %s (%s:%d) - api version 2" - % (details.hostname, details.ip_info.ip4_address, details.auth_port)) - + % (details.hostname, details.ip_info, details.auth_port)) + elif success == util.CertProcessingResult.CERT_INSERTED: + logging.debug("Successfully registered with %s (%s:%d) - api version 2" + % (details.hostname, details.ip_info, details.auth_port)) + elif success == util.CertProcessingResult.CERT_UPDATED: + logging.debug("Successfully updated registration with %s (%s:%d) - api version 2" + % (details.hostname, details.ip_info, details.auth_port)) + elif success == util.CertProcessingResult.CERT_UPDATED: + logging.debug("Certificate already up to date, nothing to do for %s (%s:%d) - api version 2" + % (details.hostname, details.ip_info, details.auth_port)) return success def register_with_remote_thread(details): - logging.debug("Remote: Attempting to register %s (%s)" % (details.hostname, details.ip_info.ip4_address)) + logging.debug("Remote: Attempting to register %s (%s)" % (details.hostname, details.ip_info)) + + remote_ip, local_ip, ip_version = details.ip_info.get_usable_ip() + remote_ip = remote_ip if ip_version == socket.AF_INET else "[%s]" % (remote_ip,) - with grpc.insecure_channel("%s:%d" % (details.ip_info.ip4_address, details.auth_port)) as channel: + with grpc.insecure_channel("%s:%d" % (remote_ip, details.auth_port)) as channel: future = grpc.channel_ready_future(channel) try: - future.result(timeout=5) + # future.result(timeout=5) stub = warp_pb2_grpc.WarpRegistrationStub(channel) - ret = stub.RequestCertificate(warp_pb2.RegRequest(ip=details.ip_info.ip4_address, hostname=util.get_hostname()), + ret = stub.RequestCertificate(warp_pb2.RegRequest(ip=remote_ip, hostname=util.get_hostname()), timeout=5) - details.locked_cert = ret.locked_cert.encode("utf-8") except Exception as e: future.cancel() logging.critical("Problem with remote registration thread: %s (%s:%d) - api version 2: %s" - % (details.hostname, details.ip_info.ip4_address, details.auth_port, e)) + % (details.hostname, details.ip_info, details.auth_port, e)) class RegistrationServer_v2(): def __init__(self, ip_info, auth_port): @@ -256,13 +277,18 @@ def serve_cert_thread(self): self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=2)) warp_pb2_grpc.add_WarpRegistrationServicer_to_server(self, self.server) - self.server.add_insecure_port('%s:%d' % (self.ip_info.ip4_address, self.auth_port)) + if self.ip_info.ip4_address is not None: + self.server.add_insecure_port('%s:%d' % (self.ip_info.ip4_address, self.auth_port)) + if self.ip_info.ip6_address is not None: + self.server.add_insecure_port('[%s]:%d' % (self.ip_info.ip6_address, self.auth_port)) self.server.start() while not self.server_thread_keepalive.is_set(): self.server_thread_keepalive.wait(10) + logging.debug("Registration Server v2 stopping") self.server.stop(grace=2).wait() + logging.debug("Registration Server v2 stopped") def stop(self): self.server_thread_keepalive.set() @@ -275,13 +301,14 @@ def RequestCertificate(self, request, context): def RegisterService(self, reg:warp_pb2.ServiceRegistration, context): logging.debug("Received manual registration from " + reg.service_id) - self.service_registration_handler(reg, reg.ip, reg.auth_port) + self.service_registration_handler(reg) return warp_pb2.ServiceRegistration(service_id=prefs.get_connect_id(), ip=self.ip_info.ip4_address, port=prefs.get_port(), hostname=util.get_hostname(), api_version=int(config.RPC_API_VERSION), - auth_port=self.auth_port) + auth_port=self.auth_port, + ipv6=self.ip_info.ip6_address) diff --git a/src/server.py b/src/server.py index 829fac6f3..298603db3 100644 --- a/src/server.py +++ b/src/server.py @@ -8,6 +8,9 @@ import re import pkg_resources from concurrent import futures +import time +import ipaddress +import urllib from gi.repository import GObject, GLib @@ -30,7 +33,7 @@ from util import TransferDirection, OpStatus, RemoteStatus import zeroconf -from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser +from zeroconf import ServiceInfo, Zeroconf, ServiceBrowser, IPVersion _ = gettext.gettext @@ -71,9 +74,11 @@ def __init__(self, ip_info, port, auth_port): self.netmon = networkmonitor.get_network_monitor() self.server = None - self.browser = None + self.browser4 = None + self.browser6 = None self.zeroconf = None self.info = None + self.browser_mutex = threading.Lock() self.display_name = GLib.get_real_name() self.start() @@ -81,7 +86,12 @@ def __init__(self, ip_info, port, auth_port): def start_zeroconf(self): logging.info("Using zeroconf version %s %s" % (zeroconf.__version__, "(bundled)" if config.bundle_zeroconf else "")) - self.zeroconf = Zeroconf(interfaces=[self.ip_info.ip4_address]) + ip_addresses = [] + if self.ip_info.ip4_address is not None: + ip_addresses.append(self.ip_info.ip4_address) + if self.ip_info.ip6_address is not None: + ip_addresses.append(self.ip_info.ip6_address) + self.zeroconf = Zeroconf(interfaces=ip_addresses) self.service_ident = prefs.get_connect_id() self.service_name = "%s.%s" % (self.service_ident, SERVICE_TYPE) @@ -115,7 +125,11 @@ def start_zeroconf(self): 'type': 'real' }) self.zeroconf.register_service(self.info) - self.browser = ServiceBrowser(self.zeroconf, SERVICE_TYPE, self, addr=self.ip_info.ip4_address) + # ServiceBrowser can only do one IP version per instance + if self.ip_info.ip4_address is not None: + self.browser4 = ServiceBrowser(self.zeroconf, SERVICE_TYPE, self, addr=self.ip_info.ip4_address) + if self.ip_info.ip6_address is not None: + self.browser6 = ServiceBrowser(self.zeroconf, SERVICE_TYPE, self, addr=self.ip_info.ip6_address) return False @@ -137,126 +151,141 @@ def remove_service(self, zeroconf, _type, name): return logging.debug(">>> Discovery: service %s (%s:%d) has disappeared." - % (remote.display_hostname, remote.ip_info.ip4_address, remote.port)) + % (remote.display_hostname, remote.ip_info, remote.port)) remote.has_zc_presence = False # Zeroconf worker thread def add_service(self, zeroconf, _type, name): - info = zeroconf.get_service_info(_type, name) - - if info: - ident = name.partition(".%s" % SERVICE_TYPE)[0] + with self.browser_mutex: + info = zeroconf.get_service_info(_type, name) - try: - remote_hostname = info.properties[b"hostname"].decode() - except KeyError: - logging.critical(">>> Discovery: no hostname in service info properties. Is this an old version?") - return + if info: + ident = name.partition(".%s" % SERVICE_TYPE)[0] - remote_ip_info = util.RemoteInterfaceInfo(info.addresses) - - if remote_ip_info == self.ip_info: - return - - try: - # Check if this is a flush registration to reset the remote server's presence. - if info.properties[b"type"].decode() == "flush": - logging.debug(">>> Discovery: received flush service info (ignoring): %s (%s:%d)" - % (remote_hostname, remote_ip_info.ip4_address, info.port)) + try: + remote_hostname = info.properties[b"hostname"].decode() + except KeyError: + logging.critical(">>> Discovery: no hostname in service info properties. Is this an old version?") return - except KeyError: - logging.warning("No type in service info properties, assuming this is a real connect attempt") - - if ident == self.service_ident: - return - try: - api_version = info.properties[b"api-version"].decode() - auth_port = int(info.properties[b"auth-port"].decode()) - except KeyError: - api_version = "1" - auth_port = 0 - - # FIXME: I'm not sure why we still get discovered by other networks in some cases - - # The Zeroconf object has a specific ip it is set to, what more do I need to do? - if not self.netmon.same_subnet(remote_ip_info): - logging.debug(">>> Discovery: service is not on this subnet, ignoring: %s (%s)" % (remote_hostname, remote_ip_info.ip4_address)) - return + remote_ip_info = util.RemoteInterfaceInfo(info.addresses_by_version(IPVersion.All)) - try: - machine = self.remote_machines[ident] - machine.has_zc_presence = True - logging.info(">>> Discovery: existing remote: %s (%s:%d)" - % (machine.display_hostname, remote_ip_info.ip4_address, info.port)) - - # If the remote truly is the same one (our service info just dropped out - # momentarily), this will end up just retrieving the current cert again. - # If this was a real disconnect we didn't notice, we'll have the new cert - # which we'll need when our supposedly existing connection tries to continue - # pinging. It will fail out and restart the connection loop, and will need - # this updated one. - - # This blocks the zeroconf thread. - if not self.remote_registrar.register(ident, remote_hostname, remote_ip_info, info.port, auth_port, api_version) or self.server_thread_keepalive.is_set(): - logging.warning("Register failed, or the server was shutting down during registration, ignoring remote %s (%s:%d) auth port: %d" - % (remote_hostname, remote_ip_info.ip4_address, info.port, auth_port)) + if remote_ip_info == self.ip_info: return - if machine.status == RemoteStatus.ONLINE: - logging.debug(">>> Discovery: rejoining existing connect with %s (%s:%d)" - % (machine.display_hostname, remote_ip_info.ip4_address, info.port)) + try: + # Check if this is a flush registration to reset the remote server's presence. + if info.properties[b"type"].decode() == "flush": + logging.debug(">>> Discovery: received flush service info (ignoring): %s (%s:%d)" + % (remote_hostname, remote_ip_info, info.port)) + return + except KeyError: + logging.warning("No type in service info properties, assuming this is a real connect attempt") + + if ident == self.service_ident: return - # Update our connect info if it changed. - machine.hostname = remote_hostname - machine.ip_info = remote_ip_info - machine.port = info.port - machine.api_version = api_version - except KeyError: - display_hostname = self.ensure_unique_hostname(remote_hostname) - - logging.info(">>> Discovery: new remote: %s (%s:%d)" - % (display_hostname, remote_ip_info.ip4_address, info.port)) - - machine = remote.RemoteMachine(ident, - remote_hostname, - display_hostname, - remote_ip_info, - info.port, - self.service_ident, - api_version) - - # This blocks the zeroconf thread. Registration will timeout - if not self.remote_registrar.register(ident, remote_hostname, remote_ip_info, info.port, auth_port, api_version) or self.server_thread_keepalive.is_set(): - logging.debug("Register failed, or the server was shutting down during registration, ignoring remote %s (%s:%d) auth port: %d" - % (remote_hostname, remote_ip_info.ip4_address, info.port, auth_port)) + try: + api_version = info.properties[b"api-version"].decode() + auth_port = int(info.properties[b"auth-port"].decode()) + except KeyError: + api_version = "1" + auth_port = 0 + + # FIXME: I'm not sure why we still get discovered by other networks in some cases - + # The Zeroconf object has a specific ip it is set to, what more do I need to do? + if not self.netmon.same_subnet(remote_ip_info): + logging.debug(">>> Discovery: service is not on this subnet, ignoring: %s (%s)" % (remote_hostname, remote_ip_info)) return - self.remote_machines[ident] = machine - machine.connect("ops-changed", self.remote_ops_changed) - machine.connect("remote-status-changed", self.remote_status_changed) - self.idle_emit("remote-machine-added", machine) + cert_result = util.CertProcessingResult.FAILURE + try: + machine = self.remote_machines[ident] + # Known remote machine + machine.has_zc_presence = True + logging.info(">>> Discovery: existing remote: %s (%s:%d)" + % (machine.display_hostname, remote_ip_info, info.port)) + + # If the remote truly is the same one (our service info just dropped out + # momentarily), this will end up just retrieving the current cert again. + # If this was a real disconnect we didn't notice, we'll have the new cert + # which we'll need when our supposedly existing connection tries to continue + # pinging. It will fail out and restart the connection loop, and will need + # this updated one. + + # This blocks the zeroconf thread. + if not machine.status in (RemoteStatus.INIT_CONNECTING, RemoteStatus.AWAITING_DUPLEX): + now = time.time() + if now - machine.last_register > 15: # wait at least 15 seconds after initial discovery + cert_result = self.remote_registrar.register(ident, remote_hostname, remote_ip_info, info.port, auth_port, api_version) + if cert_result == util.CertProcessingResult.FAILURE or self.server_thread_keepalive.is_set(): + logging.warning("Register failed, or the server was shutting down during registration, ignoring remote %s (%s:%d) auth port: %d" + % (remote_hostname, remote_ip_info, info.port, auth_port)) + return + + if machine.status == RemoteStatus.ONLINE: + logging.debug(">>> Discovery: rejoining existing connect with %s (%s:%d)" + % (machine.display_hostname, remote_ip_info, info.port)) + return + + # Update our connect info if it changed. + machine.hostname = remote_hostname + machine.ip_info = remote_ip_info + machine.port = info.port + machine.api_version = api_version + except KeyError: + # New remote machine + display_hostname = self.ensure_unique_hostname(remote_hostname) + + logging.info(">>> Discovery: new remote: %s (%s:%d)" + % (display_hostname, remote_ip_info, info.port)) + + machine = remote.RemoteMachine(ident, + remote_hostname, + display_hostname, + remote_ip_info, + info.port, + self.service_ident, + api_version) + machine.last_register = time.time() + # This blocks the zeroconf thread. Registration will timeout + cert_result = self.remote_registrar.register(ident, remote_hostname, remote_ip_info, info.port, auth_port, api_version) + if cert_result == util.CertProcessingResult.FAILURE or self.server_thread_keepalive.is_set(): + logging.debug("Register failed, or the server was shutting down during registration, ignoring remote %s (%s:%d) auth port: %d" + % (remote_hostname, remote_ip_info, info.port, auth_port)) + return + + self.remote_machines[ident] = machine + machine.connect("ops-changed", self.remote_ops_changed) + machine.connect("remote-status-changed", self.remote_status_changed) + self.idle_emit("remote-machine-added", machine) - machine.has_zc_presence = True + machine.has_zc_presence = True - machine.shutdown() # This does nothing if run more than once. It's here to make sure - # the previous start thread is complete before starting a new one. - # This is needed in the corner case where the remote has gone offline, - # and returns before our Ping loop times out and closes the thread - # itself. + if cert_result in (util.CertProcessingResult.CERT_INSERTED, util.CertProcessingResult.CERT_UPDATED): + machine.shutdown() # This does nothing if run more than once. It's here to make sure + # the previous start thread is complete before starting a new one. + # This is needed in the corner case where the remote has gone offline, + # and returns before our Ping loop times out and closes the thread + # itself. - machine.start_remote_thread() + machine.start_remote_thread() @misc._async def register_with_host(self, host:str): - p = re.compile(r'(warpinator://)?(\d{1,3}(\.\d{1,3}){3}):(\d{1,6})/?$') - m = p.match(host) - if not m: + + try: + if not host.startswith("warpinator://"): + host = "warpinator://%s" % host + url = urllib.parse.urlparse(host) + ipaddress.ip_address(url.hostname) # validate IPv4/IPv6 address + except ValueError as e: logging.info("User tried to connect to invalid address %s" % host) self.idle_emit("manual-connect-result", True, False, "Invalid address") return - host = "%s:%s" % (m.group(2), m.group(4)) + + host = url.netloc logging.info("Registering with " + host) with grpc.insecure_channel(host) as channel: future = grpc.channel_ready_future(channel) @@ -266,30 +295,41 @@ def register_with_host(self, host:str): reg = stub.RegisterService(warp_pb2.ServiceRegistration(service_id=self.service_ident, ip=self.ip_info.ip4_address, port=self.port, hostname=util.get_hostname(), api_version=int(config.RPC_API_VERSION), - auth_port=self.auth_port), + auth_port=self.auth_port, ipv6=self.ip_info.ip6_address), timeout=5) - ip = m.group(2) - auth_port = int(m.group(4)) - self.handle_manual_service_registration(reg, ip, auth_port, True) + self.handle_manual_service_registration(reg, True) except Exception as e: future.cancel() logging.critical("Could not register with %s, err %s" % (host, e)) self.idle_emit("manual-connect-result", True, False, "Could not connect to remote") - def handle_manual_service_registration(self, reg, ip, auth_port, initiated_here=False): + def handle_manual_service_registration(self, reg, initiated_here=False): + ip4_addr = None + ip6_addr = None + try: + ip4_addr = ipaddress.ip_address(reg.ip) + except ValueError: + pass + try: + ip6_addr = ipaddress.ip_address(reg.ipv6) + except ValueError: + pass if reg.service_id in self.remote_machines.keys(): # Machine already known -> update machine = self.remote_machines[reg.service_id] if machine.status == RemoteStatus.ONLINE: - logging.debug("Host %s:%d was already connected" % (ip, auth_port)) + logging.debug("Host %s:%d was already connected" % (machine.ip_info, reg.auth_port)) self.idle_emit("manual-connect-result", initiated_here, True, "Already connected") return - if not self.remote_registrar.register(machine.ident, machine.hostname, machine.ip_info, machine.port, auth_port, machine.api_version) or self.server_thread_keepalive.is_set(): - logging.debug("Registration of static machine failed, ignoring remote %s (%s:%d) auth %d" % (reg.hostname, ip, reg.port, auth_port)) + if self.remote_registrar.register(machine.ident, machine.hostname, machine.ip_info, machine.port, reg.auth_port, machine.api_version) == util.CertProcessingResult.FAILURE or self.server_thread_keepalive.is_set(): + logging.debug("Registration of static machine failed, ignoring remote %s (%s:%d) auth %d" % (reg.hostname, machine.ip_info, reg.port, reg.auth_port)) self.idle_emit("manual-connect-result", initiated_here, False, "Authentication failed") return machine.hostname = reg.hostname - machine.ip_info.ip4_address = ip + if isinstance(ip4_addr, ipaddress.IPv4Address): + machine.ip_info.ip4_address = str(ip4_addr) + if isinstance(ip6_addr, ipaddress.IPv6Address): + machine.ip_info.ip6_address = str(ip6_addr) machine.port = reg.port machine.api_version = str(reg.api_version) @@ -299,11 +339,14 @@ def handle_manual_service_registration(self, reg, ip, auth_port, initiated_here= logging.debug("Adding new static machine (manual connection)") display_hostname = self.ensure_unique_hostname(reg.hostname) ip_info = util.RemoteInterfaceInfo([]) - ip_info.ip4_address = ip + if isinstance(ip4_addr, ipaddress.IPv4Address): + ip_info.ip4_address = str(ip4_addr) + if isinstance(ip6_addr, ipaddress.IPv6Address): + ip_info.ip6_address = str(ip6_addr) machine = remote.RemoteMachine(reg.service_id, reg.hostname, display_hostname, ip_info, reg.port, self.service_ident, str(reg.api_version)) - if not self.remote_registrar.register(machine.ident, machine.hostname, machine.ip_info, machine.port, auth_port, machine.api_version) or self.server_thread_keepalive.is_set(): + if self.remote_registrar.register(machine.ident, machine.hostname, machine.ip_info, machine.port, reg.auth_port, machine.api_version) == util.CertProcessingResult.FAILURE or self.server_thread_keepalive.is_set(): logging.debug("Registration of static machine failed, ignoring remote %s (%s:%d) auth %d" - % (machine.hostname, machine.ip_info.ip4_address, machine.port, auth_port)) + % (machine.hostname, machine.ip_info.ip4_address, machine.port, reg.auth_port)) self.idle_emit("manual-connect-result", initiated_here, False, "Authentication failed") return self.remote_machines[machine.ident] = machine @@ -336,7 +379,7 @@ def ensure_unique_hostname(self, hostname): def run(self): logging.info("Using grpc version %s %s" % (grpc.__version__, "(bundled)" if config.bundle_grpc else "")) logging.info("Using protobuf version %s %s" % (protobuf.__version__, "(bundled)" if config.bundle_grpc else "")) - logging.debug("Server: starting server on %s (%s)" % (self.ip_info.ip4_address, self.ip_info.iface)) + logging.debug("Server: starting server on %s (%s)" % (self.ip_info, self.ip_info.iface)) logging.info("Using api version %s" % config.RPC_API_VERSION) logging.info("Our uuid: %s" % prefs.get_connect_id()) @@ -365,9 +408,9 @@ def run(self): if self.ip_info.ip4_address: self.server.add_secure_port('%s:%d' % (self.ip_info.ip4_address, self.port), server_credentials) - # if self.ip_info.ip6_address: - # self.server.add_secure_port('%s:%d' % (self.ip_info.ip6_address, self.port), - # server_credentials) + if self.ip_info.ip6_address: + self.server.add_secure_port('[%s]:%d' % (self.ip_info.ip6_address, self.port), + server_credentials) self.server.start() self.server_thread_keepalive.clear() diff --git a/src/util.py b/src/util.py index c4eb8d68a..de1c6fe2a 100644 --- a/src/util.py +++ b/src/util.py @@ -16,6 +16,7 @@ from gi.repository import GLib, Gtk, Gdk, GObject, GdkPixbuf, Gio import prefs +from networkmonitor import get_network_monitor import config try: @@ -186,6 +187,11 @@ def shutdown(self, wait=True): STOP_TRANSFER_BY_RECEIVER \ REMOVE_TRANSFER') +CertProcessingResult = IntEnum('CertProcessingResult', 'CERT_INSERTED \ + CERT_UPDATED \ + CERT_UP_TO_DATE \ + FAILURE') + class ReceiveError(Exception): def __init__(self, message, fatal=True): self.fatal = fatal @@ -196,8 +202,13 @@ class InterfaceInfo(): def __init__(self, ip4, ip6, iface=None): self.iface = iface # netifaces AF_INET and AF_INET6 dicts - self.ip4 = ip4 - self.ip4_address = self.ip4["addr"] + + try: + self.ip4 = ip4 + self.ip4_address = self.ip4["addr"] + except: + self.ip6 = None + self.ip4_address = None try: self.ip6 = ip6 @@ -210,7 +221,14 @@ def __eq__(self, other): if other is None: return False - return self.ip4_address == other.ip4_address + if self.ip4_address is not None: + return self.ip4_address == other.ip4_address + if self.ip6_address is not None: + return self.ip6_address == other.ip6_address + return False + + def __str__(self): + return self.get_text() def as_binary_list(self): blist = [] @@ -228,6 +246,17 @@ def as_binary_list(self): return blist + def get_text(self, delimiter = ", "): + ips = [] + if self.ip4_address is not None: + ips.append(self.ip4_address) + if self.ip6_address is not None: + ips.append(self.ip6_address) + + if delimiter is None: + delimiter = ", " + return delimiter.join(ips) + class RemoteInterfaceInfo(): def __init__(self, blist, testing=False): if testing: @@ -238,11 +267,18 @@ def __init__(self, blist, testing=False): ip4 = None ip6 = None + self.ip4_address = None + self.ip6_address = None + for item in blist: try: ip4 = socket.inet_ntop(socket.AF_INET, item) except ValueError: + pass + try: ip6 = socket.inet_ntop(socket.AF_INET6, item) + except ValueError: + pass if ip4: self.ip4_address = ip4 @@ -253,7 +289,26 @@ def __eq__(self, other): if other is None: return False - return self.ip4_address == other.ip4_address + if self.ip4_address is not None: + return self.ip4_address == other.ip4_address + if self.ip6_address == other.ip6_address: + return self.ip6_address == other.ip6_address + return False + + def __str__(self): + return self.get_text() + + def get_text(self, delimiter = ", "): + remote_ip, _, _ = self.get_usable_ip() + return remote_ip + + def get_usable_ip(self): + local_ip_info = get_network_monitor().current_ip_info + if self.ip4_address is not None and local_ip_info.ip4_address is not None: + return (self.ip4_address, local_ip_info.ip4_address, socket.AF_INET) + if self.ip6_address is not None and local_ip_info.ip6_address is not None: + return (self.ip6_address, local_ip_info.ip6_address, socket.AF_INET6) + return None last_location = Gio.File.new_for_path(GLib.get_home_dir()) # A normal GtkFileChooserDialog only lets you pick folders OR files, not diff --git a/src/warp.proto b/src/warp.proto index 71c8558b6..880388777 100644 --- a/src/warp.proto +++ b/src/warp.proto @@ -97,6 +97,7 @@ service WarpRegistration { message RegRequest { string ip = 1; string hostname = 2; + string ipv6 = 3; } message RegResponse { @@ -110,5 +111,6 @@ message ServiceRegistration { string hostname = 4; uint32 api_version = 5; uint32 auth_port = 6; + string ipv6 = 7; } diff --git a/src/warp_pb2.py b/src/warp_pb2.py index a4d15cdcc..69f78764c 100644 --- a/src/warp_pb2.py +++ b/src/warp_pb2.py @@ -24,7 +24,7 @@ -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"*\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"}\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r2\xf2\x03\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nwarp.proto\"<\n\x11RemoteMachineInfo\x12\x14\n\x0c\x64isplay_name\x18\x01 \x01(\t\x12\x11\n\tuser_name\x18\x02 \x01(\t\"+\n\x13RemoteMachineAvatar\x12\x14\n\x0c\x61vatar_chunk\x18\x01 \x01(\x0c\"/\n\nLookupName\x12\n\n\x02id\x18\x01 \x01(\t\x12\x15\n\rreadable_name\x18\x02 \x01(\t\"\x1e\n\nHaveDuplex\x12\x10\n\x08response\x18\x02 \x01(\x08\"\x19\n\x08VoidType\x12\r\n\x05\x64ummy\x18\x01 \x01(\x05\"Z\n\x06OpInfo\x12\r\n\x05ident\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x15\n\rreadable_name\x18\x03 \x01(\t\x12\x17\n\x0fuse_compression\x18\x04 \x01(\x08\"0\n\x08StopInfo\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\r\n\x05\x65rror\x18\x02 \x01(\x08\"\xd0\x01\n\x11TransferOpRequest\x12\x15\n\x04info\x18\x01 \x01(\x0b\x32\x07.OpInfo\x12\x13\n\x0bsender_name\x18\x02 \x01(\t\x12\x15\n\rreceiver_name\x18\x03 \x01(\t\x12\x10\n\x08receiver\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\r\n\x05\x63ount\x18\x06 \x01(\x04\x12\x16\n\x0ename_if_single\x18\x07 \x01(\t\x12\x16\n\x0emime_if_single\x18\x08 \x01(\t\x12\x19\n\x11top_dir_basenames\x18\t \x03(\t\"\x88\x01\n\tFileChunk\x12\x15\n\rrelative_path\x18\x01 \x01(\t\x12\x11\n\tfile_type\x18\x02 \x01(\x05\x12\x16\n\x0esymlink_target\x18\x03 \x01(\t\x12\r\n\x05\x63hunk\x18\x04 \x01(\x0c\x12\x11\n\tfile_mode\x18\x05 \x01(\r\x12\x17\n\x04time\x18\x06 \x01(\x0b\x32\t.FileTime\"-\n\x08\x46ileTime\x12\r\n\x05mtime\x18\x01 \x01(\x04\x12\x12\n\nmtime_usec\x18\x02 \x01(\r\"8\n\nRegRequest\x12\n\n\x02ip\x18\x01 \x01(\t\x12\x10\n\x08hostname\x18\x02 \x01(\t\x12\x0c\n\x04ipv6\x18\x03 \x01(\t\"\"\n\x0bRegResponse\x12\x13\n\x0blocked_cert\x18\x01 \x01(\t\"\x8b\x01\n\x13ServiceRegistration\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\n\n\x02ip\x18\x02 \x01(\t\x12\x0c\n\x04port\x18\x03 \x01(\r\x12\x10\n\x08hostname\x18\x04 \x01(\t\x12\x13\n\x0b\x61pi_version\x18\x05 \x01(\r\x12\x11\n\tauth_port\x18\x06 \x01(\r\x12\x0c\n\x04ipv6\x18\x07 \x01(\t2\xf2\x03\n\x04Warp\x12\x33\n\x15\x43heckDuplexConnection\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12.\n\x10WaitingForDuplex\x12\x0b.LookupName\x1a\x0b.HaveDuplex\"\x00\x12\x39\n\x14GetRemoteMachineInfo\x12\x0b.LookupName\x1a\x12.RemoteMachineInfo\"\x00\x12?\n\x16GetRemoteMachineAvatar\x12\x0b.LookupName\x1a\x14.RemoteMachineAvatar\"\x00\x30\x01\x12;\n\x18ProcessTransferOpRequest\x12\x12.TransferOpRequest\x1a\t.VoidType\"\x00\x12\'\n\x0fPauseTransferOp\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12(\n\rStartTransfer\x12\x07.OpInfo\x1a\n.FileChunk\"\x00\x30\x01\x12/\n\x17\x43\x61ncelTransferOpRequest\x12\x07.OpInfo\x1a\t.VoidType\"\x00\x12&\n\x0cStopTransfer\x12\t.StopInfo\x1a\t.VoidType\"\x00\x12 \n\x04Ping\x12\x0b.LookupName\x1a\t.VoidType\"\x00\x32\x86\x01\n\x10WarpRegistration\x12\x31\n\x12RequestCertificate\x12\x0b.RegRequest\x1a\x0c.RegResponse\"\x00\x12?\n\x0fRegisterService\x12\x14.ServiceRegistration\x1a\x14.ServiceRegistration\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -52,13 +52,13 @@ _globals['_FILETIME']._serialized_start=721 _globals['_FILETIME']._serialized_end=766 _globals['_REGREQUEST']._serialized_start=768 - _globals['_REGREQUEST']._serialized_end=810 - _globals['_REGRESPONSE']._serialized_start=812 - _globals['_REGRESPONSE']._serialized_end=846 - _globals['_SERVICEREGISTRATION']._serialized_start=848 - _globals['_SERVICEREGISTRATION']._serialized_end=973 - _globals['_WARP']._serialized_start=976 - _globals['_WARP']._serialized_end=1474 - _globals['_WARPREGISTRATION']._serialized_start=1477 - _globals['_WARPREGISTRATION']._serialized_end=1611 + _globals['_REGREQUEST']._serialized_end=824 + _globals['_REGRESPONSE']._serialized_start=826 + _globals['_REGRESPONSE']._serialized_end=860 + _globals['_SERVICEREGISTRATION']._serialized_start=863 + _globals['_SERVICEREGISTRATION']._serialized_end=1002 + _globals['_WARP']._serialized_start=1005 + _globals['_WARP']._serialized_end=1503 + _globals['_WARPREGISTRATION']._serialized_start=1506 + _globals['_WARPREGISTRATION']._serialized_end=1640 # @@protoc_insertion_point(module_scope) diff --git a/src/warpinator.py b/src/warpinator.py index 1ac186619..13c16d718 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -11,6 +11,8 @@ import qrcode from io import BytesIO import re +import ipaddress +import urllib import gi gi.require_version('Gtk', '3.0') @@ -338,7 +340,7 @@ def remote_machine_status_changed(self, remote_machine): if remote_machine.status == RemoteStatus.INIT_CONNECTING: self.overview_user_connecting_spinner.show() self.overview_user_status_icon.hide() - self.ip_label.set_text(str(self.remote_machine.ip_info.ip4_address)) + self.ip_label.set_text(self.remote_machine.ip_info.get_text(" | ")) if have_info: self.button.set_tooltip_text(_("Connecting")) else: @@ -375,7 +377,7 @@ def _update_machine_info(self, remote_machine): else: self.overview_user_hostname.set_text(self.remote_machine.display_hostname) - self.ip_label.set_text(str(self.remote_machine.ip_info.ip4_address)) + self.ip_label.set_text(self.remote_machine.ip_info.get_text(" | ")) if self.remote_machine.avatar_surface: self.avatar_image.set_from_surface(self.remote_machine.avatar_surface) @@ -665,7 +667,7 @@ def search_entry_changed(self, entry, data=None): for button in self.user_list_box.get_children(): joined = " ".join([button.remote_machine.display_name, ("%s@%s" % (button.remote_machine.user_name, button.remote_machine.hostname)), - button.remote_machine.ip_info.ip4_address]) + button.remote_machine.ip_info.get_text(" | ")]) normalized_contents = GLib.utf8_normalize(joined, len(joined), GLib.NormalizeMode.DEFAULT).lower() if normalized_query in normalized_contents: @@ -1034,7 +1036,7 @@ def refresh_remote_machine_view(self): else: self.user_hostname_label.set_text(remote.display_hostname) - self.user_ip_label.set_text(str(remote.ip_info.ip4_address)) + self.user_ip_label.set_text(str(remote.ip_info.get_text(" | "))) if remote.avatar_surface is not None: self.user_avatar_image.set_from_surface(remote.avatar_surface) @@ -1190,8 +1192,10 @@ def __init__(self, parent:WarpWindow): self.entry = self.builder.get_object("ip_entry") self.connect_button = self.builder.get_object("connect_button") self.status_label = self.builder.get_object("status_label") - ip_label = self.builder.get_object("our_ip_label") + ip4_label = self.builder.get_object("local_ip4_label") + ip6_label = self.builder.get_object("local_ip6_label") qr_holder = self.builder.get_object("qr_holder") + url_description_label = self.builder.get_object("url_description_label") self.entry.connect("changed", self.validate_address) self.connect_button.connect("clicked", self.on_connecting) @@ -1211,7 +1215,14 @@ def __init__(self, parent:WarpWindow): border=2 ) - qr.add_data("warpinator://%s:%d" % (parent.current_ip, parent.current_auth_port)) + ip_info = networkmonitor.get_network_monitor().current_ip_info + host_ip = ip_info.ip4_address if ip_info.ip4_address is not None else "[%s]" % ip_info.ip6_address + url_data = "%s:%d" % (host_ip, parent.current_auth_port) + if ip_info.ip4_address is not None and ip_info.ip6_address is not None: + url_data = "%s?%s" %(url_data, urllib.parse.urlencode({"ipv6": ip_info.ip6_address})) + url = "warpinator://" + url_data + logging.debug("QR code data: %s" % url) + qr.add_data(url) img = qr.make_image() img.save(qrbytes, "BMP") @@ -1220,11 +1231,30 @@ def __init__(self, parent:WarpWindow): surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, self.get_scale_factor(), None) qr_image = Gtk.Image.new_from_surface(surface) qr_holder.add(qr_image) + qr_image.set_visible(True) - ip_label.set_label("%s:%d" % (parent.current_ip, parent.current_auth_port)) + # multiple_adresses = True + if ip_info.ip4_address is not None: + ip4_label.set_label("%s:%d" % (ip_info.ip4_address, parent.current_auth_port)) + ip4_label.set_visible(True) + multiple_adresses = True + else: + ip4_label.set_visible(False) + multiple_adresses = False + if ip_info.ip6_address is not None: + ip6_label.set_label("[%s]:%d" % (ip_info.ip6_address, parent.current_auth_port)) + ip6_label.set_visible(True) + multiple_adresses = multiple_adresses and True + else: + ip6_label.set_visible(False) + multiple_adresses = False + + if multiple_adresses: + url_description_label.set_label(_("Or connect to one of the addresses below:")) + else: + url_description_label.set_label(_("Or connect to the address below:")) self.set_focus(qr_holder) - self.show_all() def on_connecting(self, _btn): self.connecting = True @@ -1255,8 +1285,14 @@ def on_connection_result(self, result, msg): def validate_address(self, entry): address = entry.get_text() - m = self.ip_validator_re.match(address) - self.connect_button.set_sensitive(m is not None) + try: + if not address.startswith("warpinator://"): + address = "warpinator://%s" % address + url = urllib.parse.urlparse(address) + ipaddress.ip_address(url.hostname) # validate IPv4/IPv6 address + self.connect_button.set_sensitive(True) + except: + self.connect_button.set_sensitive(False) class WarpApplication(Gtk.Application): def __init__(self, testing=False): @@ -1420,9 +1456,9 @@ def new_server_continue(self): self.current_auth_port = prefs.get_auth_port() self.current_ip_info = self.netmon.get_current_ip_info() - logging.debug("New server requested for '%s' (%s)", self.current_ip_info.iface, self.current_ip_info.ip4_address) + logging.debug("New server requested for '%s' (%s)", self.current_ip_info.iface, self.current_ip_info) - self.window.update_local_user_info(self.current_ip_info.ip4_address, self.current_ip_info.iface, self.current_auth_port) + self.window.update_local_user_info(self.current_ip_info.get_text(" | "), self.current_ip_info.iface, self.current_auth_port) self.window.clear_remotes()