From 463ac61739f69a7931fc03d386bbdd1490b370ef Mon Sep 17 00:00:00 2001 From: Cobinja Date: Mon, 4 Aug 2025 16:02:24 +0200 Subject: [PATCH 1/4] Basic IPv6 support Still missing: manual connection, QR-Code support needs to be changed to accomodate both IP versions (preferrably without breaking existing support --- src/auth.py | 23 +++- src/networkmonitor.py | 80 ++++++++----- src/remote.py | 27 +++-- src/remote_registration.py | 129 ++++++++++++--------- src/server.py | 226 ++++++++++++++++++++----------------- src/util.py | 63 ++++++++++- src/warp.proto | 2 + src/warp_pb2.py | 20 ++-- src/warpinator.py | 12 +- 9 files changed, 364 insertions(+), 218 deletions(-) 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..15b56ccf8 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() @@ -281,7 +307,8 @@ def RegisterService(self, reg:warp_pb2.ServiceRegistration, context): 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..e9c8a0148 100644 --- a/src/server.py +++ b/src/server.py @@ -8,6 +8,7 @@ import re import pkg_resources from concurrent import futures +import time from gi.repository import GObject, GLib @@ -30,7 +31,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 +72,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 +84,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 +123,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,116 +149,126 @@ 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) + with self.browser_mutex: + info = zeroconf.get_service_info(_type, name) - if info: - ident = name.partition(".%s" % SERVICE_TYPE)[0] + if info: + ident = name.partition(".%s" % SERVICE_TYPE)[0] - 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 - - 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): @@ -267,7 +289,7 @@ def register_with_host(self, host:str): 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), - timeout=5) + timeout=5, ipv6=self.ip_info.ip6_address) ip = m.group(2) auth_port = int(m.group(4)) self.handle_manual_service_registration(reg, ip, auth_port, True) @@ -284,7 +306,7 @@ def handle_manual_service_registration(self, reg, ip, auth_port, initiated_here= logging.debug("Host %s:%d was already connected" % (ip, 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(): + if self.remote_registrar.register(machine.ident, machine.hostname, machine.ip_info, machine.port, 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, ip, reg.port, auth_port)) self.idle_emit("manual-connect-result", initiated_here, False, "Authentication failed") return @@ -301,7 +323,7 @@ def handle_manual_service_registration(self, reg, ip, auth_port, initiated_here= ip_info = util.RemoteInterfaceInfo([]) ip_info.ip4_address = ip 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, 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)) self.idle_emit("manual-connect-result", initiated_here, False, "Authentication failed") @@ -336,7 +358,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 +387,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..f9bce402f 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 == other.ip6_address: + 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..5f6cd5821 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -338,7 +338,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 +375,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 +665,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 +1034,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) @@ -1420,9 +1420,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() From e89fc57cd6b3fa3fd6952523bc675cb2f0eba995 Mon Sep 17 00:00:00 2001 From: Cobinja Date: Fri, 8 Aug 2025 14:41:58 +0200 Subject: [PATCH 2/4] Add manual connection with IPv6 addresses This adds the ability to connect be connected from IPv6 addresses. This also fixes the way IP addresses are validated on input. Still currently broken: QR code connection --- src/remote_registration.py | 2 +- src/server.py | 55 ++++++++++++++++++++++++++------------ src/util.py | 2 +- src/warpinator.py | 12 +++++++-- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/remote_registration.py b/src/remote_registration.py index 15b56ccf8..a6b28bcef 100644 --- a/src/remote_registration.py +++ b/src/remote_registration.py @@ -301,7 +301,7 @@ 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(), diff --git a/src/server.py b/src/server.py index e9c8a0148..298603db3 100644 --- a/src/server.py +++ b/src/server.py @@ -9,6 +9,8 @@ import pkg_resources from concurrent import futures import time +import ipaddress +import urllib from gi.repository import GObject, GLib @@ -272,13 +274,18 @@ def add_service(self, zeroconf, _type, name): @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) @@ -288,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), - timeout=5, ipv6=self.ip_info.ip6_address) - ip = m.group(2) - auth_port = int(m.group(4)) - self.handle_manual_service_registration(reg, ip, auth_port, True) + auth_port=self.auth_port, ipv6=self.ip_info.ip6_address), + timeout=5) + 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 self.remote_registrar.register(machine.ident, machine.hostname, machine.ip_info, machine.port, 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, 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) @@ -321,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 self.remote_registrar.register(machine.ident, machine.hostname, machine.ip_info, machine.port, auth_port, machine.api_version) == util.CertProcessingResult.FAILURE 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 diff --git a/src/util.py b/src/util.py index f9bce402f..de1c6fe2a 100644 --- a/src/util.py +++ b/src/util.py @@ -223,7 +223,7 @@ def __eq__(self, other): if self.ip4_address is not None: return self.ip4_address == other.ip4_address - if self.ip6_address == other.ip6_address: + if self.ip6_address is not None: return self.ip6_address == other.ip6_address return False diff --git a/src/warpinator.py b/src/warpinator.py index 5f6cd5821..baec3ce46 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') @@ -1255,8 +1257,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): From 92df91c0ef5190e4e4695cfebddcaf7f10d7ffe7 Mon Sep 17 00:00:00 2001 From: Cobinja Date: Sat, 9 Aug 2025 20:32:24 +0200 Subject: [PATCH 3/4] Added IPv6 to QR code connection On devices with support for IPv4 (example: 192.168.1.10) and IPv6 (example: 2a02::1), the code will look like this: warpinator://192.168.1.10:42001/ipv6=2a02%3A%3A1 (%3A is ':') On devices that only do IPv6 but not IPv4, the IPv6 address will be used for the host part in the QR code url: warpinator://[2a02::1]:42001 (brackets indicate IPv6) --- src/warpinator.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/warpinator.py b/src/warpinator.py index baec3ce46..331cd7912 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -1213,7 +1213,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") @@ -1223,7 +1230,7 @@ def __init__(self, parent:WarpWindow): qr_image = Gtk.Image.new_from_surface(surface) qr_holder.add(qr_image) - ip_label.set_label("%s:%d" % (parent.current_ip, parent.current_auth_port)) + ip_label.set_label(url_data) self.set_focus(qr_holder) self.show_all() From 87721ad9ef07f56b79fff1252162416eb630b001 Mon Sep 17 00:00:00 2001 From: Cobinja Date: Thu, 14 Aug 2025 17:54:32 +0200 Subject: [PATCH 4/4] Split shown data for manual connection into two lines This splits the shown data for manual connection into two lines, one of which contains the data for IPv4 conenction, the other shows the one for IPv6. --- resources/manual-connect.ui | 23 +++++++++++++++++++++-- src/warpinator.py | 27 ++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 5 deletions(-) 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/warpinator.py b/src/warpinator.py index 331cd7912..13c16d718 100644 --- a/src/warpinator.py +++ b/src/warpinator.py @@ -1192,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) @@ -1229,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(url_data) + # 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