/ RNS / Interfaces / AutoInterface.py
AutoInterface.py
  1  # Reticulum License
  2  #
  3  # Copyright (c) 2016-2025 Mark Qvist
  4  #
  5  # Permission is hereby granted, free of charge, to any person obtaining a copy
  6  # of this software and associated documentation files (the "Software"), to deal
  7  # in the Software without restriction, including without limitation the rights
  8  # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9  # copies of the Software, and to permit persons to whom the Software is
 10  # furnished to do so, subject to the following conditions:
 11  #
 12  # - The Software shall not be used in any kind of system which includes amongst
 13  #   its functions the ability to purposefully do harm to human beings.
 14  #
 15  # - The Software shall not be used, directly or indirectly, in the creation of
 16  #   an artificial intelligence, machine learning or language model training
 17  #   dataset, including but not limited to any use that contributes to the
 18  #   training or development of such a model or algorithm.
 19  #
 20  # - The above copyright notice and this permission notice shall be included in
 21  #   all copies or substantial portions of the Software.
 22  #
 23  # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 24  # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 25  # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 26  # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 27  # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 28  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 29  # SOFTWARE.
 30  
 31  from RNS.Interfaces.Interface import Interface
 32  from collections import deque
 33  import socketserver
 34  import threading
 35  import re
 36  import socket
 37  import struct
 38  import time
 39  import sys
 40  import RNS
 41  
 42  
 43  class AutoInterface(Interface):
 44      HW_MTU = 1196
 45      FIXED_MTU = True
 46  
 47      DEFAULT_DISCOVERY_PORT = 29716
 48      DEFAULT_DATA_PORT      = 42671
 49      DEFAULT_GROUP_ID       = "reticulum".encode("utf-8")
 50      DEFAULT_IFAC_SIZE      = 16
 51  
 52      SCOPE_LINK         = "2"
 53      SCOPE_ADMIN        = "4"
 54      SCOPE_SITE         = "5"
 55      SCOPE_ORGANISATION = "8"
 56      SCOPE_GLOBAL       = "e"
 57  
 58      MULTICAST_PERMANENT_ADDRESS_TYPE = "0"
 59      MULTICAST_TEMPORARY_ADDRESS_TYPE = "1"
 60  
 61      PEERING_TIMEOUT    = 22.0
 62      ANNOUNCE_INTERVAL  =  1.6
 63      PEER_JOB_INTERVAL  =  4.0
 64      MCAST_ECHO_TIMEOUT =  6.5
 65  
 66      ALL_IGNORE_IFS     = ["lo0"]
 67      DARWIN_IGNORE_IFS  = ["awdl0", "llw0", "lo0", "en5"]
 68      ANDROID_IGNORE_IFS = ["dummy0", "lo", "tun0"]
 69  
 70      BITRATE_GUESS      = 10*1000*1000
 71  
 72      MULTI_IF_DEQUE_LEN = 48
 73      MULTI_IF_DEQUE_TTL = 0.75
 74  
 75      def handler_factory(self, callback):
 76          def create_handler(*args, **keys):
 77              return AutoInterfaceHandler(callback, *args, **keys)
 78          return create_handler
 79  
 80      def descope_linklocal(self, link_local_addr):
 81          # Drop scope specifier expressd as %ifname (macOS)
 82          link_local_addr = link_local_addr.split("%")[0]
 83          # Drop embedded scope specifier (NetBSD, OpenBSD)
 84          link_local_addr = re.sub(r"fe80:[0-9a-f]*::","fe80::", link_local_addr)
 85          return link_local_addr
 86  
 87      def list_interfaces(self):
 88          ifs = self.netinfo.interfaces()
 89          return ifs
 90  
 91      def list_addresses(self, ifname):
 92          ifas = self.netinfo.ifaddresses(ifname)
 93          return ifas
 94  
 95      def interface_name_to_index(self, ifname):
 96          # socket.if_nametoindex doesn't work with uuid interface names on windows, it wants the ethernet_0 style
 97          # we will just get the index from netinfo instead as it seems to work
 98          if RNS.vendor.platformutils.is_windows():
 99              return self.netinfo.interface_names_to_indexes()[ifname]
100  
101          return socket.if_nametoindex(ifname)
102  
103      def __init__(self, owner, configuration):
104          c                      = Interface.get_config_obj(configuration)
105          name                   = c["name"]
106          group_id               = c["group_id"] if "group_id" in c else None
107          discovery_scope        = c["discovery_scope"] if "discovery_scope" in c else None
108          discovery_port         = int(c["discovery_port"]) if "discovery_port" in c else None
109          multicast_address_type = c["multicast_address_type"] if "multicast_address_type" in c else None
110          data_port              = int(c["data_port"]) if "data_port" in c else None
111          allowed_interfaces     = c.as_list("devices") if "devices" in c else None
112          ignored_interfaces     = c.as_list("ignored_devices") if "ignored_devices" in c else None
113          configured_bitrate     = c["configured_bitrate"] if "configured_bitrate" in c else None
114  
115          from RNS.Interfaces import netinfo
116          super().__init__()
117          self.netinfo = netinfo
118  
119          self.HW_MTU = AutoInterface.HW_MTU
120          self.IN  = True
121          self.OUT = False
122          self.name = name
123          self.owner = owner
124          self.online = False
125          self.final_init_done = False
126          self.peers = {}
127          self.link_local_addresses = []
128          self.adopted_interfaces = {}
129          self.interface_servers = {}
130          self.multicast_echoes = {}
131          self.initial_echoes = {}
132          self.timed_out_interfaces = {}
133          self.spawned_interfaces = {}
134          self.write_lock = threading.Lock()
135          self.mif_deque = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
136          self.mif_deque_times = deque(maxlen=AutoInterface.MULTI_IF_DEQUE_LEN)
137          self.carrier_changed = False
138  
139          self.outbound_udp_socket = None
140  
141          self.announce_rate_target     = None
142          self.announce_interval        = AutoInterface.ANNOUNCE_INTERVAL
143          self.peer_job_interval        = AutoInterface.PEER_JOB_INTERVAL
144          self.peering_timeout          = AutoInterface.PEERING_TIMEOUT
145          self.multicast_echo_timeout   = AutoInterface.MCAST_ECHO_TIMEOUT
146          self.reverse_peering_interval = self.announce_interval*3.25
147  
148          # Increase peering timeout on Android, due to potential
149          # low-power modes implemented on many chipsets.
150          if RNS.vendor.platformutils.is_android():
151              self.peering_timeout *= 1.25
152  
153          if allowed_interfaces == None:
154              self.allowed_interfaces = []
155          else:
156              self.allowed_interfaces = allowed_interfaces
157  
158          if ignored_interfaces == None:
159              self.ignored_interfaces = []
160          else:
161              self.ignored_interfaces = ignored_interfaces
162  
163          if group_id == None:
164              self.group_id = AutoInterface.DEFAULT_GROUP_ID
165          else:
166              self.group_id = group_id.encode("utf-8")
167  
168          if discovery_port == None:
169              self.discovery_port = AutoInterface.DEFAULT_DISCOVERY_PORT
170          else:
171              self.discovery_port = discovery_port
172  
173          self.unicast_discovery_port = self.discovery_port+1
174  
175          if multicast_address_type == None:
176              self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
177          elif str(multicast_address_type).lower() == "temporary":
178              self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
179          elif str(multicast_address_type).lower() == "permanent":
180              self.multicast_address_type = AutoInterface.MULTICAST_PERMANENT_ADDRESS_TYPE
181          else:
182              self.multicast_address_type = AutoInterface.MULTICAST_TEMPORARY_ADDRESS_TYPE
183  
184          if data_port == None:
185              self.data_port = AutoInterface.DEFAULT_DATA_PORT
186          else:
187              self.data_port = data_port
188  
189          if discovery_scope == None:
190              self.discovery_scope = AutoInterface.SCOPE_LINK
191          elif str(discovery_scope).lower() == "link":
192              self.discovery_scope = AutoInterface.SCOPE_LINK
193          elif str(discovery_scope).lower() == "admin":
194              self.discovery_scope = AutoInterface.SCOPE_ADMIN
195          elif str(discovery_scope).lower() == "site":
196              self.discovery_scope = AutoInterface.SCOPE_SITE
197          elif str(discovery_scope).lower() == "organisation":
198              self.discovery_scope = AutoInterface.SCOPE_ORGANISATION
199          elif str(discovery_scope).lower() == "global":
200              self.discovery_scope = AutoInterface.SCOPE_GLOBAL
201  
202          self.group_hash = RNS.Identity.full_hash(self.group_id)
203          g = self.group_hash
204          #gt  = "{:02x}".format(g[1]+(g[0]<<8))
205          gt  = "0"
206          gt += ":"+"{:02x}".format(g[3]+(g[2]<<8))
207          gt += ":"+"{:02x}".format(g[5]+(g[4]<<8))
208          gt += ":"+"{:02x}".format(g[7]+(g[6]<<8))
209          gt += ":"+"{:02x}".format(g[9]+(g[8]<<8))
210          gt += ":"+"{:02x}".format(g[11]+(g[10]<<8))
211          gt += ":"+"{:02x}".format(g[13]+(g[12]<<8))
212          self.mcast_discovery_address = "ff"+self.multicast_address_type+self.discovery_scope+":"+gt
213  
214          suitable_interfaces = 0
215          for ifname in self.list_interfaces():
216              try:
217                  if RNS.vendor.platformutils.is_darwin() and ifname in AutoInterface.DARWIN_IGNORE_IFS and not ifname in self.allowed_interfaces:
218                      RNS.log(str(self)+" skipping Darwin AWDL or tethering interface "+str(ifname), RNS.LOG_EXTREME)
219                  elif RNS.vendor.platformutils.is_darwin() and ifname == "lo0":
220                      RNS.log(str(self)+" skipping Darwin loopback interface "+str(ifname), RNS.LOG_EXTREME)
221                  elif RNS.vendor.platformutils.is_android() and ifname in AutoInterface.ANDROID_IGNORE_IFS and not ifname in self.allowed_interfaces:
222                      RNS.log(str(self)+" skipping Android system interface "+str(ifname), RNS.LOG_EXTREME)
223                  elif ifname in self.ignored_interfaces:
224                      RNS.log(str(self)+" ignoring disallowed interface "+str(ifname), RNS.LOG_EXTREME)
225                  elif ifname in AutoInterface.ALL_IGNORE_IFS:
226                      RNS.log(str(self)+" skipping interface "+str(ifname), RNS.LOG_EXTREME)
227                  else:
228                      if len(self.allowed_interfaces) > 0 and not ifname in self.allowed_interfaces:
229                          RNS.log(str(self)+" ignoring interface "+str(ifname)+" since it was not allowed", RNS.LOG_EXTREME)
230                      else:
231                          addresses = self.list_addresses(ifname)
232                          if self.netinfo.AF_INET6 in addresses:
233                              link_local_addr = None
234                              for address in addresses[self.netinfo.AF_INET6]:
235                                  if "addr" in address:
236                                      if address["addr"].startswith("fe80:"):
237                                          link_local_addr = self.descope_linklocal(address["addr"])
238                                          self.link_local_addresses.append(link_local_addr)
239                                          self.adopted_interfaces[ifname] = link_local_addr
240                                          self.multicast_echoes[ifname] = time.time()
241                                          nice_name = self.netinfo.interface_name_to_nice_name(ifname)
242                                          if nice_name != None and nice_name != ifname:
243                                              RNS.log(f"{self} Selecting link-local address {link_local_addr} for interface {nice_name} / {ifname}", RNS.LOG_EXTREME)
244                                          else:
245                                              RNS.log(f"{self} Selecting link-local address {link_local_addr} for interface {ifname}", RNS.LOG_EXTREME)
246  
247                              if link_local_addr == None:
248                                  RNS.log(str(self)+" No link-local IPv6 address configured for "+str(ifname)+", skipping interface", RNS.LOG_EXTREME)
249                              else:
250                                  RNS.log(str(self)+" Creating unicast discovery listener on "+str(ifname)+" with address "+str(link_local_addr), RNS.LOG_EXTREME)
251  
252                                  # Struct with interface index
253                                  if_struct = struct.pack("I", self.interface_name_to_index(ifname))
254  
255                                  # Set up unicast discovery socket
256                                  unicast_discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
257                                  unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
258                                  if hasattr(socket, "SO_REUSEPORT"): unicast_discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
259  
260                                  # Bind unicast discovery socket
261                                  if RNS.vendor.platformutils.is_windows():
262                                      # Windows throws "[WinError 10049] The requested address is not valid in its context"
263                                      # when trying to use the multicast address as host, or when providing interface index
264                                      # passing an empty host appears to work, but probably not exactly how we want it to...
265                                      unicast_discovery_socket.bind(('', self.unicast_discovery_port))
266  
267                                  else:
268                                      addr_info = socket.getaddrinfo(link_local_addr+"%"+ifname, self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
269                                      unicast_discovery_socket.bind(addr_info[0][4])
270  
271                                  mcast_addr = self.mcast_discovery_address
272                                  RNS.log(str(self)+" Creating multicast discovery listener on "+str(ifname)+" with address "+str(mcast_addr), RNS.LOG_EXTREME)
273  
274                                  # Set up multicast discovery socket
275                                  discovery_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
276                                  discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
277                                  if hasattr(socket, "SO_REUSEPORT"): discovery_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
278                                  discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, if_struct)
279  
280                                  # Join multicast group
281                                  mcast_group = socket.inet_pton(socket.AF_INET6, mcast_addr) + if_struct
282                                  discovery_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mcast_group)
283  
284                                  # Bind multicast socket
285                                  if RNS.vendor.platformutils.is_windows():
286                                      # Windows throws "[WinError 10049] The requested address is not valid in its context"
287                                      # when trying to use the multicast address as host, or when providing interface index
288                                      # passing an empty host appears to work, but probably not exactly how we want it to...
289                                      discovery_socket.bind(('', self.discovery_port))
290  
291                                  else:
292                                      if self.discovery_scope == AutoInterface.SCOPE_LINK:
293                                          addr_info = socket.getaddrinfo(mcast_addr+"%"+ifname, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
294                                      else:
295                                          addr_info = socket.getaddrinfo(mcast_addr, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
296  
297                                      discovery_socket.bind(addr_info[0][4])
298  
299                                  # Set up thread for multicast discovery packets
300                                  def discovery_loop(): self.discovery_handler(discovery_socket, ifname)
301                                  thread = threading.Thread(target=discovery_loop, daemon=True).start()
302                                  
303                                  # Set up thread for unicast discovery packets
304                                  def unicast_discovery_loop(): self.discovery_handler(unicast_discovery_socket, ifname, announce=False)
305                                  thread = threading.Thread(target=unicast_discovery_loop, daemon=True).start()
306  
307                                  suitable_interfaces += 1
308  
309              except Exception as e:
310                  nice_name = self.netinfo.interface_name_to_nice_name(ifname)
311                  if nice_name != None and nice_name != ifname:
312                      RNS.log(f"Could not configure the system interface {nice_name} / {ifname} for use with {self}, skipping it. The contained exception was: {e}", RNS.LOG_ERROR)
313                  else:
314                      RNS.log(f"Could not configure the system interface {ifname} for use with {self}, skipping it. The contained exception was: {e}", RNS.LOG_ERROR)
315  
316          if suitable_interfaces == 0:
317              RNS.log(str(self)+" could not autoconfigure. This interface currently provides no connectivity.", RNS.LOG_WARNING)
318          else:
319              self.receives = True
320  
321              if configured_bitrate != None:
322                  self.bitrate = configured_bitrate
323              else:
324                  self.bitrate = AutoInterface.BITRATE_GUESS
325  
326      def final_init(self):
327          peering_wait = self.announce_interval*1.2
328          RNS.log(str(self)+" discovering peers for "+str(round(peering_wait, 2))+" seconds...", RNS.LOG_VERBOSE)
329  
330          socketserver.UDPServer.address_family = socket.AF_INET6
331  
332          for ifname in self.adopted_interfaces:
333              local_addr = self.adopted_interfaces[ifname]+"%"+str(self.interface_name_to_index(ifname))
334              addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
335              address = addr_info[0][4]
336  
337              udp_server = socketserver.UDPServer(address, self.handler_factory(self.process_incoming))
338              self.interface_servers[ifname] = udp_server
339              
340              thread = threading.Thread(target=udp_server.serve_forever)
341              thread.daemon = True
342              thread.start()
343  
344          job_thread = threading.Thread(target=self.peer_jobs)
345          job_thread.daemon = True
346          job_thread.start()
347  
348          time.sleep(peering_wait)
349  
350          self.online = True
351          self.final_init_done = True
352  
353      def discovery_handler(self, socket, ifname, announce=True):
354          def announce_loop(): self.announce_handler(ifname)
355          
356          if announce:
357              thread = threading.Thread(target=announce_loop)
358              thread.daemon = True
359              thread.start()
360          
361          while True:
362              data, ipv6_src = socket.recvfrom(1024)
363              if self.final_init_done:
364                  peering_hash = data[:RNS.Identity.HASHLENGTH//8]
365                  expected_hash = RNS.Identity.full_hash(self.group_id+ipv6_src[0].encode("utf-8"))
366                  if peering_hash == expected_hash:
367                      self.add_peer(ipv6_src[0], ifname)
368                  else:
369                      RNS.log(str(self)+" received peering packet on "+str(ifname)+" from "+str(ipv6_src[0])+", but authentication hash was incorrect.", RNS.LOG_DEBUG)
370  
371      def peer_jobs(self):
372          while True:
373              time.sleep(self.peer_job_interval)
374              now = time.time()
375              timed_out_peers = []
376  
377              # Check for timed out peers
378              for peer_addr in self.peers:
379                  peer = self.peers[peer_addr]
380                  last_heard = peer[1]
381                  if now > last_heard+self.peering_timeout:
382                      timed_out_peers.append(peer_addr)
383  
384              # Remove any timed out peers
385              for peer_addr in timed_out_peers:
386                  removed_peer = self.peers.pop(peer_addr)
387                  if peer_addr in self.spawned_interfaces:
388                      spawned_interface = self.spawned_interfaces[peer_addr]
389                      spawned_interface.detach()
390                      spawned_interface.teardown()
391                  RNS.log(str(self)+" removed peer "+str(peer_addr)+" on "+str(removed_peer[0]), RNS.LOG_DEBUG)
392  
393              # Send reverse peering packets
394              for peer_addr in self.peers:
395                  try:
396                      peer = self.peers[peer_addr]
397                      ifname = peer[0]
398                      last_outbound = peer[2]
399                      if now > last_outbound+self.reverse_peering_interval:
400                          self.reverse_announce(ifname, peer_addr)
401                          peer[2] = time.time()
402                  except Exception as e:
403                      RNS.log(f"Error while sending reverse peering packet to {peer_addr}: {e}", RNS.LOG_ERROR)
404  
405              for ifname in self.adopted_interfaces:
406                  # Check that the link-local address has not changed
407                  try:
408                      addresses = self.list_addresses(ifname)
409                      if self.netinfo.AF_INET6 in addresses:
410                          link_local_addr = None
411                          for address in addresses[self.netinfo.AF_INET6]:
412                              if "addr" in address:
413                                  if address["addr"].startswith("fe80:"):
414                                      link_local_addr = self.descope_linklocal(address["addr"])
415                                      if link_local_addr != self.adopted_interfaces[ifname]:
416                                          old_link_local_address = self.adopted_interfaces[ifname]
417                                          RNS.log("Replacing link-local address "+str(old_link_local_address)+" for "+str(ifname)+" with "+str(link_local_addr), RNS.LOG_DEBUG)
418                                          self.adopted_interfaces[ifname] = link_local_addr
419                                          self.link_local_addresses.append(link_local_addr)
420  
421                                          if old_link_local_address in self.link_local_addresses:
422                                              self.link_local_addresses.remove(old_link_local_address)
423  
424                                          local_addr = link_local_addr+"%"+ifname
425                                          addr_info = socket.getaddrinfo(local_addr, self.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
426                                          listen_address = addr_info[0][4]
427  
428                                          if ifname in self.interface_servers:
429                                              RNS.log("Shutting down previous UDP listener for "+str(self)+" "+str(ifname), RNS.LOG_DEBUG)
430                                              previous_server = self.interface_servers[ifname]
431                                              def shutdown_server():
432                                                  previous_server.shutdown()
433                                              threading.Thread(target=shutdown_server, daemon=True).start()
434  
435                                          RNS.log("Starting new UDP listener for "+str(self)+" "+str(ifname), RNS.LOG_DEBUG)
436  
437                                          udp_server = socketserver.UDPServer(listen_address, self.handler_factory(self.process_incoming))
438                                          self.interface_servers[ifname] = udp_server
439  
440                                          thread = threading.Thread(target=udp_server.serve_forever)
441                                          thread.daemon = True
442                                          thread.start()
443  
444                                          self.carrier_changed = True
445  
446                  except Exception as e:
447                      RNS.log("Could not get device information while updating link-local addresses for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
448  
449                  # Check multicast echo timeouts
450                  last_multicast_echo     = 0
451                  multicast_echo_received = False
452                  if ifname in self.multicast_echoes: last_multicast_echo     = self.multicast_echoes[ifname]
453                  if ifname in self.initial_echoes:   multicast_echo_received = True
454  
455                  if now - last_multicast_echo > self.multicast_echo_timeout:
456                      if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False:
457                          self.carrier_changed = True
458                          RNS.log("Multicast echo timeout for "+str(ifname)+". Carrier lost.", RNS.LOG_WARNING)
459                      self.timed_out_interfaces[ifname] = True
460                  else:
461                      if ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == True:
462                          self.carrier_changed = True
463                          RNS.log(str(self)+" Carrier recovered on "+str(ifname), RNS.LOG_WARNING)
464                      self.timed_out_interfaces[ifname] = False
465  
466                  if not multicast_echo_received:
467                      RNS.log(f"{self} No multicast echoes received on {ifname}. The networking hardware or a firewall may be blocking multicast traffic.", RNS.LOG_ERROR)
468                  # else:
469                  #     RNS.log(f"{self} Initial multicast echo on {ifname} received {RNS.prettytime(time.time()-self.initial_echoes[ifname])} ago.", RNS.LOG_DEBUG)
470                  
471  
472      def announce_handler(self, ifname):
473          while True:
474              self.peer_announce(ifname)
475              time.sleep(self.announce_interval)
476              
477      def reverse_announce(self, ifname, peer_addr):
478          try:
479              link_local_address = self.adopted_interfaces[ifname]
480              discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
481              announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
482              addr_info = socket.getaddrinfo(f"{peer_addr}%{ifname}", self.unicast_discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
483  
484              ifis = struct.pack("I", self.interface_name_to_index(ifname))
485              announce_socket.sendto(discovery_token, addr_info[0][4])
486              announce_socket.close()
487              
488          except Exception as e:
489              RNS.log(f"Could not send reverse peering packet to {peer_addr} on {ifname}: {e}", RNS.LOG_ERROR)
490  
491      def peer_announce(self, ifname):
492          try:
493              link_local_address = self.adopted_interfaces[ifname]
494              discovery_token = RNS.Identity.full_hash(self.group_id+link_local_address.encode("utf-8"))
495              announce_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
496              addr_info = socket.getaddrinfo(self.mcast_discovery_address, self.discovery_port, socket.AF_INET6, socket.SOCK_DGRAM)
497  
498              ifis = struct.pack("I", self.interface_name_to_index(ifname))
499              announce_socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifis)
500              announce_socket.sendto(discovery_token, addr_info[0][4])
501              announce_socket.close()
502              
503          except Exception as e:
504              if (ifname in self.timed_out_interfaces and self.timed_out_interfaces[ifname] == False) or not ifname in self.timed_out_interfaces:
505                  RNS.log(str(self)+" Detected possible carrier loss on "+str(ifname)+": "+str(e), RNS.LOG_WARNING)
506              else:
507                  pass
508  
509      @property
510      def peer_count(self):
511          return len(self.spawned_interfaces)
512  
513      def add_peer(self, addr, ifname):
514          if addr in self.link_local_addresses:
515              ifname = None
516              for interface_name in self.adopted_interfaces:
517                  if self.adopted_interfaces[interface_name] == addr:
518                      ifname = interface_name
519  
520              if ifname != None:
521                  self.multicast_echoes[ifname] = time.time()
522                  if not ifname in self.initial_echoes: self.initial_echoes[ifname] = time.time()
523              else:
524                  RNS.log(str(self)+" received multicast echo on unexpected interface "+str(ifname), RNS.LOG_WARNING)
525  
526          else:
527              if not addr in self.peers:
528                  self.peers[addr] = [ifname, time.time(), time.time()]
529  
530                  spawned_interface = AutoInterfacePeer(self, addr, ifname)
531                  spawned_interface.OUT = self.OUT
532                  spawned_interface.IN  = self.IN
533                  spawned_interface.parent_interface = self
534                  spawned_interface.bitrate = self.bitrate
535                  
536                  spawned_interface.ifac_size = self.ifac_size
537                  spawned_interface.ifac_netname = self.ifac_netname
538                  spawned_interface.ifac_netkey = self.ifac_netkey
539                  if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None:
540                      ifac_origin = b""
541                      if spawned_interface.ifac_netname != None:
542                          ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8"))
543                      if spawned_interface.ifac_netkey != None:
544                          ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8"))
545  
546                      ifac_origin_hash = RNS.Identity.full_hash(ifac_origin)
547                      spawned_interface.ifac_key = RNS.Cryptography.hkdf(
548                          length=64,
549                          derive_from=ifac_origin_hash,
550                          salt=RNS.Reticulum.IFAC_SALT,
551                          context=None
552                      )
553                      spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key)
554                      spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key))
555  
556                  spawned_interface.announce_rate_target = self.announce_rate_target
557                  spawned_interface.announce_rate_grace = self.announce_rate_grace
558                  spawned_interface.announce_rate_penalty = self.announce_rate_penalty
559                  spawned_interface.mode = self.mode
560                  spawned_interface.HW_MTU = self.HW_MTU
561                  spawned_interface.online = True
562                  RNS.Transport.interfaces.append(spawned_interface)
563                  if addr in self.spawned_interfaces:
564                      self.spawned_interfaces[addr].detach()
565                      self.spawned_interfaces[addr].teardown()
566                      if addr in self.spawned_interfaces: self.spawned_interfaces.pop(addr)
567                  self.spawned_interfaces[addr] = spawned_interface
568  
569                  RNS.log(str(self)+" added peer "+str(addr)+" on "+str(ifname), RNS.LOG_DEBUG)
570              else:
571                  self.refresh_peer(addr)
572  
573      def refresh_peer(self, addr):
574          try: self.peers[addr][1] = time.time()
575          except Exception as e: RNS.log(f"An error occurred while refreshing peer {addr} on {self}: {e}", RNS.LOG_ERROR)
576  
577      def process_incoming(self, data, addr=None):
578          if self.online and addr in self.spawned_interfaces:
579              self.spawned_interfaces[addr].process_incoming(data, addr)
580  
581      def process_outgoing(self, data): pass
582  
583      def detach(self): self.online = False
584  
585      def __str__(self): return f"AutoInterface[{self.name}]"
586  
587  class AutoInterfacePeer(Interface):
588  
589      def __init__(self, owner, addr, ifname):
590          super().__init__()
591          self.owner = owner
592          self.parent_interface = owner
593          self.addr = addr
594          self.ifname = ifname
595          self.peer_addr = None
596          self.addr_info = None
597          self.HW_MTU = self.owner.HW_MTU
598          self.FIXED_MTU = self.owner.FIXED_MTU
599  
600      def __str__(self):
601          return f"AutoInterfacePeer[{self.ifname}/{self.addr}]"
602  
603      def process_incoming(self, data, addr=None):
604          if self.online and self.owner.online:
605              data_hash = RNS.Identity.full_hash(data)
606              deque_hit = False
607              if data_hash in self.owner.mif_deque:
608                  for te in self.owner.mif_deque_times:
609                      if te[0] == data_hash and time.time() < te[1]+AutoInterface.MULTI_IF_DEQUE_TTL:
610                          deque_hit = True
611                          break
612  
613              if not deque_hit:
614                  self.owner.refresh_peer(self.addr)
615                  self.owner.mif_deque.append(data_hash)
616                  self.owner.mif_deque_times.append([data_hash, time.time()])
617                  self.rxb += len(data)
618                  self.owner.rxb += len(data)
619                  self.owner.owner.inbound(data, self)
620  
621      def process_outgoing(self, data):
622          if self.online:
623              with self.owner.write_lock:
624                  try:
625                      if self.owner.outbound_udp_socket == None: self.owner.outbound_udp_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
626                      if self.peer_addr == None: self.peer_addr = str(self.addr)+"%"+str(self.owner.interface_name_to_index(self.ifname))
627                      if self.addr_info == None: self.addr_info = socket.getaddrinfo(self.peer_addr, self.owner.data_port, socket.AF_INET6, socket.SOCK_DGRAM)
628                      self.owner.outbound_udp_socket.sendto(data, self.addr_info[0][4])
629                      self.txb += len(data)
630                      self.owner.txb += len(data)
631                  except Exception as e:
632                      RNS.log("Could not transmit on "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
633  
634      def detach(self):
635          self.online = False
636          self.detached = True
637          
638      def teardown(self):
639          if not self.detached:
640              RNS.log(f"The interface {self} experienced an unrecoverable error and is being torn down.", RNS.LOG_ERROR)
641              if RNS.Reticulum.panic_on_interface_error: RNS.panic()
642  
643          else: RNS.log(f"The interface {self} is being torn down.", RNS.LOG_VERBOSE)
644  
645          self.online = False
646          self.OUT = False
647          self.IN = False
648  
649          if self.addr in self.owner.spawned_interfaces:
650              try: self.owner.spawned_interfaces.pop(self.addr)
651              except Exception as e:
652                  RNS.log(f"Could not remove {self} from parent interface on detach. The contained exception was: {e}", RNS.LOG_ERROR)
653  
654          if self in RNS.Transport.interfaces: RNS.Transport.interfaces.remove(self)
655  
656  class AutoInterfaceHandler(socketserver.BaseRequestHandler):
657      def __init__(self, callback, *args, **keys):
658          self.callback = callback
659          socketserver.BaseRequestHandler.__init__(self, *args, **keys)
660  
661      def handle(self):
662          data = self.request[0]
663          addr = self.client_address[0]
664          self.callback(data, addr)