2025-04-15 20:19:33 +02:00
# Reticulum License
2022-04-01 17:18:18 +02:00
#
2025-04-15 20:19:33 +02:00
# Copyright (c) 2016-2025 Mark Qvist
2022-04-01 17:18:18 +02:00
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
2025-04-15 20:19:33 +02:00
# - The Software shall not be used in any kind of system which includes amongst
# its functions the ability to purposefully do harm to human beings.
#
# - The Software shall not be used, directly or indirectly, in the creation of
# an artificial intelligence, machine learning or language model training
# dataset, including but not limited to any use that contributes to the
# training or development of such a model or algorithm.
#
# - The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
2022-04-01 17:18:18 +02:00
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
2024-11-27 17:45:05 +01:00
from RNS . Interfaces . Interface import Interface
2023-05-11 19:54:26 +02:00
from collections import deque
2021-12-08 20:46:53 +01:00
import socketserver
import threading
2023-02-28 09:47:09 -05:00
import re
2021-12-08 20:46:53 +01:00
import socket
import struct
import time
import sys
import RNS
class AutoInterface ( Interface ) :
2025-02-23 22:40:36 +01:00
HW_MTU = 1196
FIXED_MTU = True
2021-12-08 20:46:53 +01:00
DEFAULT_DISCOVERY_PORT = 29716
DEFAULT_DATA_PORT = 42671
DEFAULT_GROUP_ID = " reticulum " . encode ( " utf-8 " )
2024-11-20 20:14:02 +01:00
DEFAULT_IFAC_SIZE = 16
2021-12-08 20:46:53 +01:00
SCOPE_LINK = " 2 "
SCOPE_ADMIN = " 4 "
SCOPE_SITE = " 5 "
SCOPE_ORGANISATION = " 8 "
2021-12-09 16:07:36 +01:00
SCOPE_GLOBAL = " e "
2021-12-08 20:46:53 +01:00
2024-03-23 00:54:56 -03:00
MULTICAST_PERMANENT_ADDRESS_TYPE = " 0 "
MULTICAST_TEMPORARY_ADDRESS_TYPE = " 1 "
2025-11-01 17:29:53 +01:00
PEERING_TIMEOUT = 22.0
ANNOUNCE_INTERVAL = 1.6
PEER_JOB_INTERVAL = 4.0
MCAST_ECHO_TIMEOUT = 6.5
2021-12-08 20:46:53 +01:00
2023-03-07 16:43:10 +01:00
ALL_IGNORE_IFS = [ " lo0 " ]
2021-12-10 11:10:09 +01:00
DARWIN_IGNORE_IFS = [ " awdl0 " , " llw0 " , " lo0 " , " en5 " ]
2026-04-23 01:04:01 +02:00
ANDROID_IGNORE_IFS = [ " dummy0 " , " lo " , " tun0 " , " rmnet0 " , " rmnet1 " , " rmnet2 " , " rmnet3 " , " rmnet4 " , " rmnet5 " , " rmnet6 " , " rmnet7 " ]
2021-12-10 10:58:28 +01:00
2022-04-17 19:07:32 +02:00
BITRATE_GUESS = 10 * 1000 * 1000
2023-09-14 22:14:31 +02:00
MULTI_IF_DEQUE_LEN = 48
MULTI_IF_DEQUE_TTL = 0.75
2023-05-11 19:54:26 +02:00
2022-11-24 17:19:01 +01:00
def handler_factory ( self , callback ) :
def create_handler ( * args , * * keys ) :
return AutoInterfaceHandler ( callback , * args , * * keys )
return create_handler
2023-02-28 09:47:09 -05:00
def descope_linklocal ( self , link_local_addr ) :
# Drop scope specifier expressd as %ifname (macOS)
link_local_addr = link_local_addr . split ( " % " ) [ 0 ]
# Drop embedded scope specifier (NetBSD, OpenBSD)
link_local_addr = re . sub ( r " fe80:[0-9a-f]*:: " , " fe80:: " , link_local_addr )
return link_local_addr
2023-05-04 17:55:58 +02:00
def list_interfaces ( self ) :
ifs = self . netinfo . interfaces ( )
return ifs
def list_addresses ( self , ifname ) :
ifas = self . netinfo . ifaddresses ( ifname )
return ifas
2024-05-17 04:09:11 +12:00
def interface_name_to_index ( self , ifname ) :
# socket.if_nametoindex doesn't work with uuid interface names on windows, it wants the ethernet_0 style
# we will just get the index from netinfo instead as it seems to work
if RNS . vendor . platformutils . is_windows ( ) :
return self . netinfo . interface_names_to_indexes ( ) [ ifname ]
return socket . if_nametoindex ( ifname )
2024-11-20 20:14:02 +01:00
def __init__ ( self , owner , configuration ) :
2024-11-21 14:41:22 +01:00
c = Interface . get_config_obj ( configuration )
2024-11-20 20:14:02 +01:00
name = c [ " name " ]
group_id = c [ " group_id " ] if " group_id " in c else None
discovery_scope = c [ " discovery_scope " ] if " discovery_scope " in c else None
discovery_port = int ( c [ " discovery_port " ] ) if " discovery_port " in c else None
multicast_address_type = c [ " multicast_address_type " ] if " multicast_address_type " in c else None
data_port = int ( c [ " data_port " ] ) if " data_port " in c else None
allowed_interfaces = c . as_list ( " devices " ) if " devices " in c else None
ignored_interfaces = c . as_list ( " ignored_devices " ) if " ignored_devices " in c else None
2024-11-21 14:41:22 +01:00
configured_bitrate = c [ " configured_bitrate " ] if " configured_bitrate " in c else None
2024-11-20 20:14:02 +01:00
2025-04-11 12:38:46 +02:00
from RNS . Interfaces import netinfo
2023-09-30 19:11:10 +02:00
super ( ) . __init__ ( )
2025-04-11 12:22:58 +02:00
self . netinfo = netinfo
2021-12-08 20:46:53 +01:00
2025-02-23 22:40:36 +01:00
self . HW_MTU = AutoInterface . HW_MTU
2021-12-08 20:46:53 +01:00
self . IN = True
self . OUT = False
self . name = name
2025-03-09 18:39:23 +01:00
self . owner = owner
2021-12-08 20:46:53 +01:00
self . online = False
2025-07-13 13:14:18 +02:00
self . final_init_done = False
2021-12-08 20:46:53 +01:00
self . peers = { }
2021-12-09 16:07:36 +01:00
self . link_local_addresses = [ ]
self . adopted_interfaces = { }
2022-11-24 17:19:01 +01:00
self . interface_servers = { }
2022-02-22 14:43:14 +01:00
self . multicast_echoes = { }
2025-11-01 14:51:50 +01:00
self . initial_echoes = { }
2022-02-22 20:16:02 +01:00
self . timed_out_interfaces = { }
2025-02-24 01:57:39 +01:00
self . spawned_interfaces = { }
self . write_lock = threading . Lock ( )
2023-05-11 19:54:26 +02:00
self . mif_deque = deque ( maxlen = AutoInterface . MULTI_IF_DEQUE_LEN )
2023-09-14 22:14:31 +02:00
self . mif_deque_times = deque ( maxlen = AutoInterface . MULTI_IF_DEQUE_LEN )
2022-12-22 18:20:34 +01:00
self . carrier_changed = False
2021-12-08 20:46:53 +01:00
self . outbound_udp_socket = None
2025-12-19 14:16:03 +01:00
self . announce_rate_target = None
self . announce_interval = AutoInterface . ANNOUNCE_INTERVAL
self . peer_job_interval = AutoInterface . PEER_JOB_INTERVAL
self . peering_timeout = AutoInterface . PEERING_TIMEOUT
self . multicast_echo_timeout = AutoInterface . MCAST_ECHO_TIMEOUT
self . reverse_peering_interval = self . announce_interval * 3.25
2021-12-09 16:07:36 +01:00
2023-09-20 00:53:51 +02:00
# Increase peering timeout on Android, due to potential
# low-power modes implemented on many chipsets.
if RNS . vendor . platformutils . is_android ( ) :
2025-11-01 17:29:53 +01:00
self . peering_timeout * = 1.25
2023-09-20 00:53:51 +02:00
2021-12-09 16:07:36 +01:00
if allowed_interfaces == None :
self . allowed_interfaces = [ ]
else :
self . allowed_interfaces = allowed_interfaces
if ignored_interfaces == None :
self . ignored_interfaces = [ ]
else :
self . ignored_interfaces = ignored_interfaces
2021-12-08 20:46:53 +01:00
if group_id == None :
self . group_id = AutoInterface . DEFAULT_GROUP_ID
else :
self . group_id = group_id . encode ( " utf-8 " )
if discovery_port == None :
self . discovery_port = AutoInterface . DEFAULT_DISCOVERY_PORT
else :
self . discovery_port = discovery_port
2025-12-19 14:16:03 +01:00
self . unicast_discovery_port = self . discovery_port + 1
2024-03-23 00:54:56 -03:00
if multicast_address_type == None :
self . multicast_address_type = AutoInterface . MULTICAST_TEMPORARY_ADDRESS_TYPE
elif str ( multicast_address_type ) . lower ( ) == " temporary " :
self . multicast_address_type = AutoInterface . MULTICAST_TEMPORARY_ADDRESS_TYPE
elif str ( multicast_address_type ) . lower ( ) == " permanent " :
self . multicast_address_type = AutoInterface . MULTICAST_PERMANENT_ADDRESS_TYPE
2024-05-01 15:49:48 +02:00
else :
self . multicast_address_type = AutoInterface . MULTICAST_TEMPORARY_ADDRESS_TYPE
2024-03-23 00:54:56 -03:00
2021-12-08 20:46:53 +01:00
if data_port == None :
self . data_port = AutoInterface . DEFAULT_DATA_PORT
else :
self . data_port = data_port
if discovery_scope == None :
self . discovery_scope = AutoInterface . SCOPE_LINK
elif str ( discovery_scope ) . lower ( ) == " link " :
self . discovery_scope = AutoInterface . SCOPE_LINK
elif str ( discovery_scope ) . lower ( ) == " admin " :
self . discovery_scope = AutoInterface . SCOPE_ADMIN
elif str ( discovery_scope ) . lower ( ) == " site " :
self . discovery_scope = AutoInterface . SCOPE_SITE
elif str ( discovery_scope ) . lower ( ) == " organisation " :
self . discovery_scope = AutoInterface . SCOPE_ORGANISATION
elif str ( discovery_scope ) . lower ( ) == " global " :
self . discovery_scope = AutoInterface . SCOPE_GLOBAL
2021-12-09 16:07:36 +01:00
self . group_hash = RNS . Identity . full_hash ( self . group_id )
g = self . group_hash
#gt = "{:02x}".format(g[1]+(g[0]<<8))
gt = " 0 "
gt + = " : " + " {:02x} " . format ( g [ 3 ] + ( g [ 2 ] << 8 ) )
gt + = " : " + " {:02x} " . format ( g [ 5 ] + ( g [ 4 ] << 8 ) )
gt + = " : " + " {:02x} " . format ( g [ 7 ] + ( g [ 6 ] << 8 ) )
gt + = " : " + " {:02x} " . format ( g [ 9 ] + ( g [ 8 ] << 8 ) )
gt + = " : " + " {:02x} " . format ( g [ 11 ] + ( g [ 10 ] << 8 ) )
gt + = " : " + " {:02x} " . format ( g [ 13 ] + ( g [ 12 ] << 8 ) )
2024-03-23 00:54:56 -03:00
self . mcast_discovery_address = " ff " + self . multicast_address_type + self . discovery_scope + " : " + gt
2021-12-09 16:07:36 +01:00
2021-12-08 20:46:53 +01:00
suitable_interfaces = 0
2023-05-04 17:55:58 +02:00
for ifname in self . list_interfaces ( ) :
2024-05-17 23:54:48 +02:00
try :
if RNS . vendor . platformutils . is_darwin ( ) and ifname in AutoInterface . DARWIN_IGNORE_IFS and not ifname in self . allowed_interfaces :
RNS . log ( str ( self ) + " skipping Darwin AWDL or tethering interface " + str ( ifname ) , RNS . LOG_EXTREME )
elif RNS . vendor . platformutils . is_darwin ( ) and ifname == " lo0 " :
RNS . log ( str ( self ) + " skipping Darwin loopback interface " + str ( ifname ) , RNS . LOG_EXTREME )
elif RNS . vendor . platformutils . is_android ( ) and ifname in AutoInterface . ANDROID_IGNORE_IFS and not ifname in self . allowed_interfaces :
RNS . log ( str ( self ) + " skipping Android system interface " + str ( ifname ) , RNS . LOG_EXTREME )
elif ifname in self . ignored_interfaces :
RNS . log ( str ( self ) + " ignoring disallowed interface " + str ( ifname ) , RNS . LOG_EXTREME )
elif ifname in AutoInterface . ALL_IGNORE_IFS :
RNS . log ( str ( self ) + " skipping interface " + str ( ifname ) , RNS . LOG_EXTREME )
2021-12-08 20:46:53 +01:00
else :
2024-05-17 23:54:48 +02:00
if len ( self . allowed_interfaces ) > 0 and not ifname in self . allowed_interfaces :
RNS . log ( str ( self ) + " ignoring interface " + str ( ifname ) + " since it was not allowed " , RNS . LOG_EXTREME )
else :
addresses = self . list_addresses ( ifname )
if self . netinfo . AF_INET6 in addresses :
link_local_addr = None
for address in addresses [ self . netinfo . AF_INET6 ] :
if " addr " in address :
if address [ " addr " ] . startswith ( " fe80: " ) :
link_local_addr = self . descope_linklocal ( address [ " addr " ] )
self . link_local_addresses . append ( link_local_addr )
self . adopted_interfaces [ ifname ] = link_local_addr
self . multicast_echoes [ ifname ] = time . time ( )
nice_name = self . netinfo . interface_name_to_nice_name ( ifname )
if nice_name != None and nice_name != ifname :
RNS . log ( f " { self } Selecting link-local address { link_local_addr } for interface { nice_name } / { ifname } " , RNS . LOG_EXTREME )
else :
RNS . log ( f " { self } Selecting link-local address { link_local_addr } for interface { ifname } " , RNS . LOG_EXTREME )
if link_local_addr == None :
RNS . log ( str ( self ) + " No link-local IPv6 address configured for " + str ( ifname ) + " , skipping interface " , RNS . LOG_EXTREME )
2023-05-02 17:39:06 +02:00
else :
2025-12-19 14:16:03 +01:00
RNS . log ( str ( self ) + " Creating unicast discovery listener on " + str ( ifname ) + " with address " + str ( link_local_addr ) , RNS . LOG_EXTREME )
2024-05-17 23:54:48 +02:00
# Struct with interface index
if_struct = struct . pack ( " I " , self . interface_name_to_index ( ifname ) )
2025-12-19 14:16:03 +01:00
# Set up unicast discovery socket
unicast_discovery_socket = socket . socket ( socket . AF_INET6 , socket . SOCK_DGRAM )
unicast_discovery_socket . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
if hasattr ( socket , " SO_REUSEPORT " ) : unicast_discovery_socket . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEPORT , 1 )
# Bind unicast discovery socket
if RNS . vendor . platformutils . is_windows ( ) :
# Windows throws "[WinError 10049] The requested address is not valid in its context"
# when trying to use the multicast address as host, or when providing interface index
# passing an empty host appears to work, but probably not exactly how we want it to...
unicast_discovery_socket . bind ( ( ' ' , self . unicast_discovery_port ) )
else :
addr_info = socket . getaddrinfo ( link_local_addr + " % " + ifname , self . unicast_discovery_port , socket . AF_INET6 , socket . SOCK_DGRAM )
unicast_discovery_socket . bind ( addr_info [ 0 ] [ 4 ] )
mcast_addr = self . mcast_discovery_address
RNS . log ( str ( self ) + " Creating multicast discovery listener on " + str ( ifname ) + " with address " + str ( mcast_addr ) , RNS . LOG_EXTREME )
# Set up multicast discovery socket
2024-05-17 23:54:48 +02:00
discovery_socket = socket . socket ( socket . AF_INET6 , socket . SOCK_DGRAM )
discovery_socket . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
2025-12-19 14:16:03 +01:00
if hasattr ( socket , " SO_REUSEPORT " ) : discovery_socket . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEPORT , 1 )
2024-05-17 23:54:48 +02:00
discovery_socket . setsockopt ( socket . IPPROTO_IPV6 , socket . IPV6_MULTICAST_IF , if_struct )
# Join multicast group
mcast_group = socket . inet_pton ( socket . AF_INET6 , mcast_addr ) + if_struct
discovery_socket . setsockopt ( socket . IPPROTO_IPV6 , socket . IPV6_JOIN_GROUP , mcast_group )
2025-12-19 14:16:03 +01:00
# Bind multicast socket
2024-05-17 23:54:48 +02:00
if RNS . vendor . platformutils . is_windows ( ) :
2025-12-19 14:16:03 +01:00
# Windows throws "[WinError 10049] The requested address is not valid in its context"
2024-05-17 23:54:48 +02:00
# when trying to use the multicast address as host, or when providing interface index
# passing an empty host appears to work, but probably not exactly how we want it to...
discovery_socket . bind ( ( ' ' , self . discovery_port ) )
2023-05-02 17:39:06 +02:00
2024-05-17 04:09:11 +12:00
else :
2024-05-17 23:54:48 +02:00
if self . discovery_scope == AutoInterface . SCOPE_LINK :
addr_info = socket . getaddrinfo ( mcast_addr + " % " + ifname , self . discovery_port , socket . AF_INET6 , socket . SOCK_DGRAM )
else :
addr_info = socket . getaddrinfo ( mcast_addr , self . discovery_port , socket . AF_INET6 , socket . SOCK_DGRAM )
discovery_socket . bind ( addr_info [ 0 ] [ 4 ] )
2021-12-09 16:07:36 +01:00
2025-12-19 14:16:03 +01:00
# Set up thread for multicast discovery packets
2025-07-13 13:14:18 +02:00
def discovery_loop ( ) : self . discovery_handler ( discovery_socket , ifname )
2025-12-19 14:16:03 +01:00
thread = threading . Thread ( target = discovery_loop , daemon = True ) . start ( )
# Set up thread for unicast discovery packets
def unicast_discovery_loop ( ) : self . discovery_handler ( unicast_discovery_socket , ifname , announce = False )
thread = threading . Thread ( target = unicast_discovery_loop , daemon = True ) . start ( )
2021-12-09 16:07:36 +01:00
2024-05-17 23:54:48 +02:00
suitable_interfaces + = 1
except Exception as e :
nice_name = self . netinfo . interface_name_to_nice_name ( ifname )
if nice_name != None and nice_name != ifname :
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 )
else :
RNS . log ( f " Could not configure the system interface { ifname } for use with { self } , skipping it. The contained exception was: { e } " , RNS . LOG_ERROR )
2021-12-08 20:46:53 +01:00
if suitable_interfaces == 0 :
2021-12-11 15:41:34 +01:00
RNS . log ( str ( self ) + " could not autoconfigure. This interface currently provides no connectivity. " , RNS . LOG_WARNING )
2021-12-08 20:46:53 +01:00
else :
self . receives = True
2023-03-09 18:32:14 +01:00
if configured_bitrate != None :
self . bitrate = configured_bitrate
else :
self . bitrate = AutoInterface . BITRATE_GUESS
2025-03-09 18:39:23 +01:00
def final_init ( self ) :
peering_wait = self . announce_interval * 1.2
RNS . log ( str ( self ) + " discovering peers for " + str ( round ( peering_wait , 2 ) ) + " seconds... " , RNS . LOG_VERBOSE )
2021-12-11 15:41:34 +01:00
2025-03-09 18:39:23 +01:00
socketserver . UDPServer . address_family = socket . AF_INET6
2021-12-08 20:46:53 +01:00
2025-03-09 18:39:23 +01:00
for ifname in self . adopted_interfaces :
local_addr = self . adopted_interfaces [ ifname ] + " % " + str ( self . interface_name_to_index ( ifname ) )
addr_info = socket . getaddrinfo ( local_addr , self . data_port , socket . AF_INET6 , socket . SOCK_DGRAM )
address = addr_info [ 0 ] [ 4 ]
2021-12-10 10:35:25 +01:00
2025-03-09 18:39:23 +01:00
udp_server = socketserver . UDPServer ( address , self . handler_factory ( self . process_incoming ) )
self . interface_servers [ ifname ] = udp_server
thread = threading . Thread ( target = udp_server . serve_forever )
thread . daemon = True
thread . start ( )
2021-12-08 20:46:53 +01:00
2025-03-09 18:39:23 +01:00
job_thread = threading . Thread ( target = self . peer_jobs )
job_thread . daemon = True
job_thread . start ( )
2021-12-09 16:07:36 +01:00
2025-03-09 18:39:23 +01:00
time . sleep ( peering_wait )
2021-12-08 20:46:53 +01:00
2025-03-09 18:39:23 +01:00
self . online = True
2025-07-13 13:14:18 +02:00
self . final_init_done = True
2021-12-08 20:46:53 +01:00
2025-12-19 14:16:03 +01:00
def discovery_handler ( self , socket , ifname , announce = True ) :
def announce_loop ( ) : self . announce_handler ( ifname )
if announce :
thread = threading . Thread ( target = announce_loop )
thread . daemon = True
thread . start ( )
2021-12-09 16:07:36 +01:00
2021-12-08 20:46:53 +01:00
while True :
data , ipv6_src = socket . recvfrom ( 1024 )
2025-07-13 13:14:18 +02:00
if self . final_init_done :
peering_hash = data [ : RNS . Identity . HASHLENGTH / / 8 ]
expected_hash = RNS . Identity . full_hash ( self . group_id + ipv6_src [ 0 ] . encode ( " utf-8 " ) )
if peering_hash == expected_hash :
self . add_peer ( ipv6_src [ 0 ] , ifname )
else :
RNS . log ( str ( self ) + " received peering packet on " + str ( ifname ) + " from " + str ( ipv6_src [ 0 ] ) + " , but authentication hash was incorrect. " , RNS . LOG_DEBUG )
2021-12-08 20:46:53 +01:00
2021-12-09 16:07:36 +01:00
def peer_jobs ( self ) :
while True :
time . sleep ( self . peer_job_interval )
now = time . time ( )
timed_out_peers = [ ]
2022-02-22 14:43:14 +01:00
# Check for timed out peers
2021-12-09 16:07:36 +01:00
for peer_addr in self . peers :
peer = self . peers [ peer_addr ]
last_heard = peer [ 1 ]
if now > last_heard + self . peering_timeout :
timed_out_peers . append ( peer_addr )
2022-02-22 14:43:14 +01:00
# Remove any timed out peers
2021-12-09 16:07:36 +01:00
for peer_addr in timed_out_peers :
2021-12-10 14:48:30 +01:00
removed_peer = self . peers . pop ( peer_addr )
2025-02-24 01:57:39 +01:00
if peer_addr in self . spawned_interfaces :
spawned_interface = self . spawned_interfaces [ peer_addr ]
spawned_interface . detach ( )
spawned_interface . teardown ( )
2021-12-10 14:48:30 +01:00
RNS . log ( str ( self ) + " removed peer " + str ( peer_addr ) + " on " + str ( removed_peer [ 0 ] ) , RNS . LOG_DEBUG )
2022-02-22 20:16:02 +01:00
2025-12-19 14:16:03 +01:00
# Send reverse peering packets
for peer_addr in self . peers :
try :
peer = self . peers [ peer_addr ]
ifname = peer [ 0 ]
last_outbound = peer [ 2 ]
if now > last_outbound + self . reverse_peering_interval :
self . reverse_announce ( ifname , peer_addr )
peer [ 2 ] = time . time ( )
except Exception as e :
RNS . log ( f " Error while sending reverse peering packet to { peer_addr } : { e } " , RNS . LOG_ERROR )
2022-02-22 20:16:02 +01:00
for ifname in self . adopted_interfaces :
2022-10-19 11:57:09 +02:00
# Check that the link-local address has not changed
try :
2023-05-04 17:55:58 +02:00
addresses = self . list_addresses ( ifname )
if self . netinfo . AF_INET6 in addresses :
2022-10-19 11:57:09 +02:00
link_local_addr = None
2023-05-04 17:55:58 +02:00
for address in addresses [ self . netinfo . AF_INET6 ] :
2022-10-19 11:57:09 +02:00
if " addr " in address :
if address [ " addr " ] . startswith ( " fe80: " ) :
2023-02-28 09:47:09 -05:00
link_local_addr = self . descope_linklocal ( address [ " addr " ] )
2022-10-19 11:57:09 +02:00
if link_local_addr != self . adopted_interfaces [ ifname ] :
2022-12-22 17:46:46 +01:00
old_link_local_address = self . adopted_interfaces [ ifname ]
RNS . log ( " Replacing link-local address " + str ( old_link_local_address ) + " for " + str ( ifname ) + " with " + str ( link_local_addr ) , RNS . LOG_DEBUG )
2022-10-19 11:57:09 +02:00
self . adopted_interfaces [ ifname ] = link_local_addr
2022-12-22 17:46:46 +01:00
self . link_local_addresses . append ( link_local_addr )
if old_link_local_address in self . link_local_addresses :
self . link_local_addresses . remove ( old_link_local_address )
2022-10-19 11:57:09 +02:00
2022-11-24 17:19:01 +01:00
local_addr = link_local_addr + " % " + ifname
addr_info = socket . getaddrinfo ( local_addr , self . data_port , socket . AF_INET6 , socket . SOCK_DGRAM )
listen_address = addr_info [ 0 ] [ 4 ]
if ifname in self . interface_servers :
2022-12-22 17:46:46 +01:00
RNS . log ( " Shutting down previous UDP listener for " + str ( self ) + " " + str ( ifname ) , RNS . LOG_DEBUG )
2022-11-24 17:19:01 +01:00
previous_server = self . interface_servers [ ifname ]
2026-05-31 23:54:46 +02:00
def shutdown_server ( ) : previous_server . shutdown ( )
2022-11-24 17:19:01 +01:00
threading . Thread ( target = shutdown_server , daemon = True ) . start ( )
2022-12-22 17:46:46 +01:00
RNS . log ( " Starting new UDP listener for " + str ( self ) + " " + str ( ifname ) , RNS . LOG_DEBUG )
2022-11-24 17:19:01 +01:00
2026-05-31 23:54:46 +02:00
retry_delay = 1.25
listener_started = False
while not listener_started :
try :
time . sleep ( retry_delay )
udp_server = socketserver . UDPServer ( listen_address , self . handler_factory ( self . process_incoming ) )
self . interface_servers [ ifname ] = udp_server
listener_started = True
except Exception as e :
RNS . log ( f " Could not start new UDP listener for { self } on { listen_address } : { e } " , RNS . LOG_WARNING )
RNS . log ( f " Retrying in { retry_delay } seconds " , RNS . LOG_WARNING )
2022-11-24 17:19:01 +01:00
thread = threading . Thread ( target = udp_server . serve_forever )
thread . daemon = True
thread . start ( )
2023-12-06 00:06:45 +01:00
self . carrier_changed = True
2022-10-19 11:57:09 +02:00
except Exception as e :
RNS . log ( " Could not get device information while updating link-local addresses for " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
# Check multicast echo timeouts
2025-11-01 14:51:50 +01:00
last_multicast_echo = 0
multicast_echo_received = False
if ifname in self . multicast_echoes : last_multicast_echo = self . multicast_echoes [ ifname ]
if ifname in self . initial_echoes : multicast_echo_received = True
2022-02-22 20:16:02 +01:00
if now - last_multicast_echo > self . multicast_echo_timeout :
if ifname in self . timed_out_interfaces and self . timed_out_interfaces [ ifname ] == False :
2022-12-22 18:20:34 +01:00
self . carrier_changed = True
2022-02-22 20:16:02 +01:00
RNS . log ( " Multicast echo timeout for " + str ( ifname ) + " . Carrier lost. " , RNS . LOG_WARNING )
self . timed_out_interfaces [ ifname ] = True
else :
if ifname in self . timed_out_interfaces and self . timed_out_interfaces [ ifname ] == True :
2022-12-22 18:20:34 +01:00
self . carrier_changed = True
2022-02-22 20:16:02 +01:00
RNS . log ( str ( self ) + " Carrier recovered on " + str ( ifname ) , RNS . LOG_WARNING )
self . timed_out_interfaces [ ifname ] = False
2025-11-01 14:51:50 +01:00
if not multicast_echo_received :
RNS . log ( f " { self } No multicast echoes received on { ifname } . The networking hardware or a firewall may be blocking multicast traffic. " , RNS . LOG_ERROR )
# else:
# RNS.log(f"{self} Initial multicast echo on {ifname} received {RNS.prettytime(time.time()-self.initial_echoes[ifname])} ago.", RNS.LOG_DEBUG)
2021-12-09 16:07:36 +01:00
def announce_handler ( self , ifname ) :
while True :
self . peer_announce ( ifname )
time . sleep ( self . announce_interval )
2025-12-19 14:16:03 +01:00
def reverse_announce ( self , ifname , peer_addr ) :
try :
link_local_address = self . adopted_interfaces [ ifname ]
discovery_token = RNS . Identity . full_hash ( self . group_id + link_local_address . encode ( " utf-8 " ) )
announce_socket = socket . socket ( socket . AF_INET6 , socket . SOCK_DGRAM )
addr_info = socket . getaddrinfo ( f " { peer_addr } % { ifname } " , self . unicast_discovery_port , socket . AF_INET6 , socket . SOCK_DGRAM )
ifis = struct . pack ( " I " , self . interface_name_to_index ( ifname ) )
announce_socket . sendto ( discovery_token , addr_info [ 0 ] [ 4 ] )
announce_socket . close ( )
except Exception as e :
RNS . log ( f " Could not send reverse peering packet to { peer_addr } on { ifname } : { e } " , RNS . LOG_ERROR )
2021-12-09 16:07:36 +01:00
def peer_announce ( self , ifname ) :
2022-02-22 14:43:14 +01:00
try :
link_local_address = self . adopted_interfaces [ ifname ]
discovery_token = RNS . Identity . full_hash ( self . group_id + link_local_address . encode ( " utf-8 " ) )
announce_socket = socket . socket ( socket . AF_INET6 , socket . SOCK_DGRAM )
addr_info = socket . getaddrinfo ( self . mcast_discovery_address , self . discovery_port , socket . AF_INET6 , socket . SOCK_DGRAM )
2024-05-17 04:09:11 +12:00
ifis = struct . pack ( " I " , self . interface_name_to_index ( ifname ) )
2022-02-22 14:43:14 +01:00
announce_socket . setsockopt ( socket . IPPROTO_IPV6 , socket . IPV6_MULTICAST_IF , ifis )
announce_socket . sendto ( discovery_token , addr_info [ 0 ] [ 4 ] )
2022-06-09 08:48:55 +02:00
announce_socket . close ( )
2022-06-10 17:05:00 +02:00
2022-02-22 14:43:14 +01:00
except Exception as e :
2022-02-22 20:16:02 +01:00
if ( ifname in self . timed_out_interfaces and self . timed_out_interfaces [ ifname ] == False ) or not ifname in self . timed_out_interfaces :
RNS . log ( str ( self ) + " Detected possible carrier loss on " + str ( ifname ) + " : " + str ( e ) , RNS . LOG_WARNING )
else :
pass
2021-12-09 16:07:36 +01:00
2025-02-24 01:57:39 +01:00
@property
def peer_count ( self ) :
return len ( self . spawned_interfaces )
2021-12-09 16:07:36 +01:00
def add_peer ( self , addr , ifname ) :
2022-02-22 14:43:14 +01:00
if addr in self . link_local_addresses :
ifname = None
for interface_name in self . adopted_interfaces :
if self . adopted_interfaces [ interface_name ] == addr :
ifname = interface_name
if ifname != None :
self . multicast_echoes [ ifname ] = time . time ( )
2025-11-01 14:51:50 +01:00
if not ifname in self . initial_echoes : self . initial_echoes [ ifname ] = time . time ( )
2022-02-22 14:43:14 +01:00
else :
2022-02-25 20:29:47 +01:00
RNS . log ( str ( self ) + " received multicast echo on unexpected interface " + str ( ifname ) , RNS . LOG_WARNING )
2022-02-22 14:43:14 +01:00
else :
2021-12-09 16:07:36 +01:00
if not addr in self . peers :
2025-12-19 14:16:03 +01:00
self . peers [ addr ] = [ ifname , time . time ( ) , time . time ( ) ]
2025-02-24 01:57:39 +01:00
spawned_interface = AutoInterfacePeer ( self , addr , ifname )
spawned_interface . OUT = self . OUT
spawned_interface . IN = self . IN
2026-04-12 18:39:06 +02:00
spawned_interface . ingress_control = self . ingress_control
spawned_interface . ic_max_held_announces = self . ic_max_held_announces
spawned_interface . ic_burst_hold = self . ic_burst_hold
spawned_interface . ic_burst_freq = self . ic_burst_freq
spawned_interface . ic_burst_freq_new = self . ic_burst_freq_new
spawned_interface . ic_new_time = self . ic_new_time
spawned_interface . ic_burst_penalty = self . ic_burst_penalty
spawned_interface . ic_held_release_interval = self . ic_held_release_interval
2026-05-09 02:27:31 +02:00
spawned_interface . egress_control = self . egress_control
spawned_interface . ec_pr_freq = self . ec_pr_freq
spawned_interface . ic_pr_burst_freq_new = self . ic_pr_burst_freq_new
spawned_interface . ic_pr_burst_freq = self . ic_pr_burst_freq
2026-04-12 18:39:06 +02:00
2025-02-24 01:57:39 +01:00
spawned_interface . parent_interface = self
spawned_interface . bitrate = self . bitrate
spawned_interface . ifac_size = self . ifac_size
spawned_interface . ifac_netname = self . ifac_netname
spawned_interface . ifac_netkey = self . ifac_netkey
if spawned_interface . ifac_netname != None or spawned_interface . ifac_netkey != None :
ifac_origin = b " "
if spawned_interface . ifac_netname != None :
ifac_origin + = RNS . Identity . full_hash ( spawned_interface . ifac_netname . encode ( " utf-8 " ) )
if spawned_interface . ifac_netkey != None :
ifac_origin + = RNS . Identity . full_hash ( spawned_interface . ifac_netkey . encode ( " utf-8 " ) )
ifac_origin_hash = RNS . Identity . full_hash ( ifac_origin )
spawned_interface . ifac_key = RNS . Cryptography . hkdf (
length = 64 ,
derive_from = ifac_origin_hash ,
salt = RNS . Reticulum . IFAC_SALT ,
context = None
)
spawned_interface . ifac_identity = RNS . Identity . from_bytes ( spawned_interface . ifac_key )
spawned_interface . ifac_signature = spawned_interface . ifac_identity . sign ( RNS . Identity . full_hash ( spawned_interface . ifac_key ) )
spawned_interface . announce_rate_target = self . announce_rate_target
spawned_interface . announce_rate_grace = self . announce_rate_grace
spawned_interface . announce_rate_penalty = self . announce_rate_penalty
spawned_interface . mode = self . mode
2026-07-23 12:37:59 +02:00
spawned_interface . gravity = self . gravity
2025-02-24 01:57:39 +01:00
spawned_interface . HW_MTU = self . HW_MTU
spawned_interface . online = True
2026-05-15 17:08:22 +02:00
RNS . Transport . add_interface ( spawned_interface )
2025-02-24 01:57:39 +01:00
if addr in self . spawned_interfaces :
self . spawned_interfaces [ addr ] . detach ( )
self . spawned_interfaces [ addr ] . teardown ( )
2025-12-02 21:18:02 +01:00
if addr in self . spawned_interfaces : self . spawned_interfaces . pop ( addr )
2025-02-24 01:57:39 +01:00
self . spawned_interfaces [ addr ] = spawned_interface
2021-12-09 16:07:36 +01:00
RNS . log ( str ( self ) + " added peer " + str ( addr ) + " on " + str ( ifname ) , RNS . LOG_DEBUG )
else :
self . refresh_peer ( addr )
def refresh_peer ( self , addr ) :
2025-12-19 14:16:03 +01:00
try : self . peers [ addr ] [ 1 ] = time . time ( )
except Exception as e : RNS . log ( f " An error occurred while refreshing peer { addr } on { self } : { e } " , RNS . LOG_ERROR )
2021-12-08 20:46:53 +01:00
2025-02-24 01:57:39 +01:00
def process_incoming ( self , data , addr = None ) :
if self . online and addr in self . spawned_interfaces :
self . spawned_interfaces [ addr ] . process_incoming ( data , addr )
2025-12-20 14:00:42 +01:00
def process_outgoing ( self , data ) : pass
2025-02-24 01:57:39 +01:00
2025-12-02 21:24:43 +01:00
def detach ( self ) : self . online = False
2025-02-24 01:57:39 +01:00
2025-12-02 21:24:43 +01:00
def __str__ ( self ) : return f " AutoInterface[ { self . name } ] "
2025-02-24 01:57:39 +01:00
class AutoInterfacePeer ( Interface ) :
def __init__ ( self , owner , addr , ifname ) :
super ( ) . __init__ ( )
self . owner = owner
self . parent_interface = owner
self . addr = addr
self . ifname = ifname
self . peer_addr = None
self . addr_info = None
self . HW_MTU = self . owner . HW_MTU
self . FIXED_MTU = self . owner . FIXED_MTU
def __str__ ( self ) :
return f " AutoInterfacePeer[ { self . ifname } / { self . addr } ] "
def process_incoming ( self , data , addr = None ) :
if self . online and self . owner . online :
2025-01-16 14:09:18 +01:00
data_hash = RNS . Identity . full_hash ( data )
deque_hit = False
2025-02-24 01:57:39 +01:00
if data_hash in self . owner . mif_deque :
for te in self . owner . mif_deque_times :
2025-01-16 14:09:18 +01:00
if te [ 0 ] == data_hash and time . time ( ) < te [ 1 ] + AutoInterface . MULTI_IF_DEQUE_TTL :
deque_hit = True
break
if not deque_hit :
2025-02-24 01:57:39 +01:00
self . owner . refresh_peer ( self . addr )
self . owner . mif_deque . append ( data_hash )
self . owner . mif_deque_times . append ( [ data_hash , time . time ( ) ] )
2025-01-16 14:09:18 +01:00
self . rxb + = len ( data )
2025-02-24 01:57:39 +01:00
self . owner . rxb + = len ( data )
self . owner . owner . inbound ( data , self )
2021-12-08 20:46:53 +01:00
2025-02-24 01:57:39 +01:00
def process_outgoing ( self , data ) :
2025-01-16 14:09:18 +01:00
if self . online :
2025-02-24 01:57:39 +01:00
with self . owner . write_lock :
2021-12-08 20:46:53 +01:00
try :
2025-02-24 01:57:39 +01:00
if self . owner . outbound_udp_socket == None : self . owner . outbound_udp_socket = socket . socket ( socket . AF_INET6 , socket . SOCK_DGRAM )
if self . peer_addr == None : self . peer_addr = str ( self . addr ) + " % " + str ( self . owner . interface_name_to_index ( self . ifname ) )
2025-04-07 18:48:12 +02:00
if self . addr_info == None : self . addr_info = socket . getaddrinfo ( self . peer_addr , self . owner . data_port , socket . AF_INET6 , socket . SOCK_DGRAM )
self . owner . outbound_udp_socket . sendto ( data , self . addr_info [ 0 ] [ 4 ] )
2025-02-24 01:57:39 +01:00
self . txb + = len ( data )
self . owner . txb + = len ( data )
2021-12-08 20:46:53 +01:00
except Exception as e :
RNS . log ( " Could not transmit on " + str ( self ) + " . The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2025-02-24 01:57:39 +01:00
def detach ( self ) :
self . online = False
self . detached = True
def teardown ( self ) :
if not self . detached :
2025-12-02 21:18:02 +01:00
RNS . log ( f " The interface { self } experienced an unrecoverable error and is being torn down. " , RNS . LOG_ERROR )
if RNS . Reticulum . panic_on_interface_error : RNS . panic ( )
2025-02-24 01:57:39 +01:00
2025-12-02 21:18:02 +01:00
else : RNS . log ( f " The interface { self } is being torn down. " , RNS . LOG_VERBOSE )
2025-02-24 01:57:39 +01:00
self . online = False
self . OUT = False
self . IN = False
if self . addr in self . owner . spawned_interfaces :
try : self . owner . spawned_interfaces . pop ( self . addr )
except Exception as e :
RNS . log ( f " Could not remove { self } from parent interface on detach. The contained exception was: { e } " , RNS . LOG_ERROR )
2026-05-15 17:08:22 +02:00
RNS . Transport . remove_interface ( self )
2021-12-08 20:46:53 +01:00
class AutoInterfaceHandler ( socketserver . BaseRequestHandler ) :
def __init__ ( self , callback , * args , * * keys ) :
self . callback = callback
socketserver . BaseRequestHandler . __init__ ( self , * args , * * keys )
def handle ( self ) :
data = self . request [ 0 ]
2025-02-24 01:57:39 +01:00
addr = self . client_address [ 0 ]
self . callback ( data , addr )