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
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()