Skip to content

Commit 70cc5eb

Browse files
authored
Ipv6 support (#231)
* Basic IPv6 support Still missing: manual connection, QR-Code support needs to be changed to accomodate both IP versions (preferrably without breaking existing support * 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 * 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) * 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.
1 parent 4f8217a commit 70cc5eb

File tree

10 files changed

+462
-240
lines changed

10 files changed

+462
-240
lines changed

resources/manual-connect.ui

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@
192192
</packing>
193193
</child>
194194
<child>
195-
<object class="GtkLabel">
195+
<object class="GtkLabel" id="url_description_label">
196196
<property name="visible">True</property>
197197
<property name="can-focus">False</property>
198198
<property name="label" translatable="yes" comments="The user's ip address and port are displayed here">Or connect to the address below</property>
@@ -204,7 +204,7 @@
204204
</packing>
205205
</child>
206206
<child>
207-
<object class="GtkLabel" id="our_ip_label">
207+
<object class="GtkLabel" id="local_ip4_label">
208208
<property name="visible">True</property>
209209
<property name="can-focus">False</property>
210210
<property name="label">192.168.0.50:42001</property>
@@ -222,6 +222,25 @@
222222
<property name="position">4</property>
223223
</packing>
224224
</child>
225+
<child>
226+
<object class="GtkLabel" id="local_ip6_label">
227+
<property name="visible">True</property>
228+
<property name="can-focus">False</property>
229+
<property name="label">[2a02::1]:42001</property>
230+
<attributes>
231+
<attribute name="weight" value="bold"/>
232+
<attribute name="scale" value="1.8"/>
233+
</attributes>
234+
<style>
235+
<class name="monospace"/>
236+
</style>
237+
</object>
238+
<packing>
239+
<property name="expand">False</property>
240+
<property name="fill">True</property>
241+
<property name="position">5</property>
242+
</packing>
243+
</child>
225244
</object>
226245
<packing>
227246
<property name="expand">False</property>

src/auth.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ def get_server_creds(self):
6969

7070
def get_cached_cert(self, hostname, ip_info):
7171
try:
72-
return self.remote_certs["%s.%s" % (hostname, ip_info.ip4_address)]
72+
return self.remote_certs["%s.%s" % (hostname, ip_info)]
7373
except KeyError:
7474
return None
7575

7676
def process_remote_cert(self, hostname, ip_info, server_data):
7777
if server_data is None:
78-
return False
78+
return util.CertProcessingResult.FAILURE
7979
decoded = base64.decodebytes(server_data)
8080

8181
hasher = hashlib.sha256()
@@ -89,11 +89,20 @@ def process_remote_cert(self, hostname, ip_info, server_data):
8989
logging.debug("Decryption failed for remote '%s': %s" % (hostname, str(e)))
9090
cert = None
9191

92+
res = util.CertProcessingResult.FAILURE
9293
if cert:
93-
self.remote_certs["%s.%s" % (hostname, ip_info.ip4_address)] = cert
94-
return True
95-
else:
96-
return False
94+
key = "%s.%s" % (hostname, ip_info)
95+
val = self.remote_certs.get(key)
96+
97+
if val is None:
98+
res = util.CertProcessingResult.CERT_INSERTED
99+
elif val == cert:
100+
res = util.CertProcessingResult.CERT_UP_TO_DATE
101+
return res
102+
else:
103+
res = util.CertProcessingResult.CERT_UPDATED
104+
self.remote_certs[key] = cert
105+
return res
97106

98107
def get_encoded_local_cert(self):
99108
hasher = hashlib.sha256()
@@ -133,6 +142,8 @@ def _make_key_cert_pair(self):
133142

134143
if self.ip_info.ip4_address is not None:
135144
alt_names.append(x509.IPAddress(ipaddress.IPv4Address(self.ip_info.ip4_address)))
145+
if self.ip_info.ip6_address is not None:
146+
alt_names.append(x509.IPAddress(ipaddress.IPv6Address(self.ip_info.ip6_address)))
136147

137148
builder = builder.add_extension(x509.SubjectAlternativeName(alt_names), critical=True)
138149

src/networkmonitor.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -102,65 +102,89 @@ def get_valid_interface_infos(self):
102102

103103
try:
104104
ip4 = iface[netifaces.AF_INET][0]
105+
except KeyError:
106+
ip4 = None
107+
try:
108+
ip6 = iface[netifaces.AF_INET6][0]
109+
except KeyError:
110+
ip6 = None
105111

106-
try:
107-
ip6 = iface[netifaces.AF_INET6][0]
108-
except KeyError:
109-
ip6 = None
110-
112+
if ip4 is not None or ip6 is not None:
111113
info = util.InterfaceInfo(ip4, ip6, iname)
112114
valid.append(info)
113-
except KeyError:
114-
continue
115115

116116
return valid
117117

118118
def get_default_interface_info(self):
119-
ip = self.get_default_ip()
119+
ip4 = self.get_default_ip4()
120+
ip6 = self.get_default_ip6()
120121
fallback_info = None
121122

122123
for info in self.get_valid_interface_infos():
123124
if fallback_info is None:
124125
fallback_info = info
125126
try:
126-
if ip == info.ip4["addr"]:
127+
if ip4 == info.ip4["addr"]:
128+
return info
129+
except:
130+
pass
131+
try:
132+
if ip6 == info.ip6["addr"]:
127133
return info
128134
except:
129135
pass
130136

131137
return fallback_info
132138

133-
def get_default_ip(self):
134-
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
135-
try:
136-
s.connect(("8.8.8.8", 80))
137-
except OSError as e:
138-
# print("Unable to retrieve IP address: %s" % str(e))
139-
return "0.0.0.0"
139+
def get_default_ip(self, ip_version):
140+
with socket.socket(ip_version, socket.SOCK_DGRAM) as s:
141+
if ip_version == socket.AF_INET:
142+
try:
143+
s.connect(("8.8.8.8", 80))
144+
except OSError as e:
145+
# print("Unable to retrieve IP address: %s" % str(e))
146+
return "0.0.0.0"
147+
else:
148+
try:
149+
s.connect(("2001:4860:4860::8888", 80))
150+
except OSError as e:
151+
# print("Unable to retrieve IP address: %s" % str(e))
152+
return "[::]"
140153
ans = s.getsockname()[0]
141154
return ans
142155

156+
def get_default_ip4(self):
157+
return self.get_default_ip(socket.AF_INET)
158+
159+
def get_default_ip6(self):
160+
return self.get_default_ip(socket.AF_INET6)
161+
143162
def emit_state_changed(self):
144163
logging.debug("Network state changed: online = %s" % str(self.online))
145164
self.emit("state-changed", self.online)
146165

147166
# TODO: Do this with libnm
148167
def same_subnet(self, other_ip_info):
149-
iface = ipaddress.IPv4Interface("%s/%s" % (self.current_ip_info.ip4_address,
150-
self.current_ip_info.ip4["netmask"]))
168+
if self.current_ip_info.ip4_address is not None and other_ip_info.ip4_address is not None:
169+
iface = ipaddress.IPv4Interface("%s/%s" % (self.current_ip_info.ip4_address,
170+
self.current_ip_info.ip4["netmask"]))
151171

152-
my_net = iface.network
172+
my_net = iface.network
153173

154-
if my_net is None:
155-
# We're more likely to have failed here than to have found something on a different subnet.
156-
return True
174+
if my_net is None:
175+
# We're more likely to have failed here than to have found something on a different subnet.
176+
return True
157177

158-
if my_net.netmask.exploded == "255.255.255.255":
159-
logging.warning("Discovery: netmask is 255.255.255.255 - are you on a vpn?")
160-
return False
178+
if my_net.netmask.exploded == "255.255.255.255":
179+
logging.warning("Discovery: netmask is 255.255.255.255 - are you on a vpn?")
180+
return False
161181

162-
for addr in list(my_net.hosts()):
163-
if other_ip_info.ip4_address == addr.exploded:
164-
return True
182+
for addr in list(my_net.hosts()):
183+
if other_ip_info.ip4_address == addr.exploded:
184+
return True
165185

186+
return False
187+
if self.current_ip_info.ip6_address is not None and other_ip_info.ip6_address is not None:
188+
return True # TODO: Verify that this is actually true
189+
logging.debug("No IP address found: %s" % (self))
166190
return False

src/remote.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import gettext
55
import threading
66
import logging
7+
import socket
78

89
from gi.repository import GObject, GLib
910

@@ -79,6 +80,8 @@ def __init__(self, ident, hostname, display_hostname, ip_info, port, local_ident
7980

8081
self.has_zc_presence = False # This is currently unused.
8182

83+
self.last_register = 0
84+
8285
def start_remote_thread(self):
8386
# func = lambda: return
8487

@@ -104,7 +107,7 @@ def remote_thread_v1(self):
104107

105108
def run_secure_loop():
106109
logging.debug("Remote: Starting a new connection loop for %s (%s:%d)"
107-
% (self.display_hostname, self.ip_info.ip4_address, self.port))
110+
% (self.display_hostname, self.ip_info, self.port))
108111

109112
cert = auth.get_singleton().get_cached_cert(self.hostname, self.ip_info)
110113
creds = grpc.ssl_channel_credentials(cert)
@@ -121,7 +124,7 @@ def run_secure_loop():
121124

122125
if not self.ping_timer.is_set():
123126
logging.debug("Remote: Unable to establish secure connection with %s (%s:%d). Trying again in %ds"
124-
% (self.display_hostname, self.ip_info.ip4_address, self.port, CHANNEL_RETRY_WAIT_TIME))
127+
% (self.display_hostname, self.ip_info, self.port, CHANNEL_RETRY_WAIT_TIME))
125128
self.ping_timer.wait(CHANNEL_RETRY_WAIT_TIME)
126129
return True # run_secure_loop()
127130

@@ -134,13 +137,13 @@ def run_secure_loop():
134137

135138
if self.busy:
136139
logging.debug("Remote Ping: Skipping keepalive ping to %s (%s:%d) (busy)"
137-
% (self.display_hostname, self.ip_info.ip4_address, self.port))
140+
% (self.display_hostname, self.ip_info, self.port))
138141
self.busy = False
139142
else:
140143
try:
141144
# t = GLib.get_monotonic_time()
142145
logging.debug("Remote Ping: to %s (%s:%d)"
143-
% (self.display_hostname, self.ip_info.ip4_address, self.port))
146+
% (self.display_hostname, self.ip_info, self.port))
144147
self.stub.Ping(warp_pb2.LookupName(id=self.local_ident,
145148
readable_name=util.get_hostname()),
146149
timeout=5)
@@ -150,7 +153,7 @@ def run_secure_loop():
150153
self.set_remote_status(RemoteStatus.AWAITING_DUPLEX)
151154
if self.check_duplex_connection():
152155
logging.debug("Remote: Connected to %s (%s:%d)"
153-
% (self.display_hostname, self.ip_info.ip4_address, self.port))
156+
% (self.display_hostname, self.ip_info, self.port))
154157

155158
self.set_remote_status(RemoteStatus.ONLINE)
156159

@@ -161,12 +164,12 @@ def run_secure_loop():
161164
duplex_fail_counter += 1
162165
if duplex_fail_counter > DUPLEX_MAX_FAILURES:
163166
logging.debug("Remote: CheckDuplexConnection to %s (%s:%d) failed too many times"
164-
% (self.display_hostname, self.ip_info.ip4_address, self.port))
167+
% (self.display_hostname, self.ip_info, self.port))
165168
self.ping_timer.wait(CHANNEL_RETRY_WAIT_TIME)
166169
return True
167170
except grpc.RpcError as e:
168171
logging.debug("Remote: Ping failed, shutting down %s (%s:%d)"
169-
% (self.display_hostname, self.ip_info.ip4_address, self.port))
172+
% (self.display_hostname, self.ip_info, self.port))
170173
break
171174

172175
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():
185188
continue
186189
except Exception as e:
187190
logging.critical("!! Major problem starting connection loop for %s (%s:%d): %s"
188-
% (self.display_hostname, self.ip_info.ip4_address, self.port, e))
191+
% (self.display_hostname, self.ip_info, self.port, e))
189192

190193
self.set_remote_status(RemoteStatus.OFFLINE)
191194
self.run_thread_alive = False
@@ -195,7 +198,9 @@ def remote_thread_v2(self):
195198

196199
self.emit_machine_info_changed() # Let's make sure the button doesn't have junk in it if we fail to connect.
197200

198-
logging.debug("Remote: Attempting to connect to %s (%s) - api version 2" % (self.display_hostname, self.ip_info.ip4_address))
201+
remote_ip, _, ip_version = self.ip_info.get_usable_ip()
202+
logging.debug("Remote: Attempting to connect to %s (%s) - api version 2" % (self.display_hostname, remote_ip))
203+
remote_ip = remote_ip if ip_version == socket.AF_INET else "[%s]" % (remote_ip,)
199204

200205
self.set_remote_status(RemoteStatus.INIT_CONNECTING)
201206

@@ -212,7 +217,7 @@ def run_secure_loop():
212217
('grpc.http2.min_ping_interval_without_data_ms', 5000)
213218
)
214219

215-
with grpc.secure_channel("%s:%d" % (self.ip_info.ip4_address, self.port), creds, options=opts) as channel:
220+
with grpc.secure_channel("%s:%d" % (remote_ip, self.port), creds, options=opts) as channel:
216221

217222
def channel_state_changed(state):
218223
if state != grpc.ChannelConnectivity.READY:
@@ -335,7 +340,7 @@ def rpc_call(self, func, *args, **kargs):
335340
except Exception as e:
336341
# exception concurrent.futures.thread.BrokenThreadPool is not available in bionic/python3 < 3.7
337342
logging.critical("!! RPC threadpool failure while submitting call to %s (%s:%d): %s"
338-
% (self.display_hostname, self.ip_info.ip4_address, self.port, e))
343+
% (self.display_hostname, self.ip_info, self.port, e))
339344

340345
# Not added to thread pool
341346
def check_duplex_connection(self):

0 commit comments

Comments
 (0)