diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 3a531d5..9a2f2cb 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -8,8 +8,9 @@ from threading import Thread, RLock import curses import re import socket +import time -from .messages import Packet, DataTLV +from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType class Hazelnut: @@ -72,8 +73,13 @@ class Squirrel(Hazelnut): #dictionnaries of neighbours self.potentialhazelnuts = dict() self.activehazelnuts = dict() #of the form [hazelnut, time of last - #hello, time of last long hello, is symetric] + #hello, time of last long hello] + self.nbNS = 0 + self.minNS = 3 #minimal number of symetric neighbours a squirrel needs + #to have. + self.add_system_message(f"Listening on {self.address}:{self.port}") + self.add_system_message(f"I am {self.id}") def new_hazel(self, address: IPv6Address, port: int) -> Hazelnut: """ @@ -89,7 +95,7 @@ class Squirrel(Hazelnut): return (hazel.address, hazel.port) in self.potentialhazelnuts def remove_from_potential(self, address: IPv6Address, port: int)-> None: - self.potentialhazelnuts.pop((address, port), None) + self.potentialhazelnuts.pop((str(address), port), None) def find_hazelnut(self, address: str, port: int) -> Hazelnut: """ @@ -105,16 +111,22 @@ class Squirrel(Hazelnut): """ Send a formatted packet to a client. """ + self.refresh_lock.acquire() if len(pkt) > 1024: # The packet is too large to be sent by the protocol. We split the packet in subpackets. return sum(self.send_packet(client, subpkt) for subpkt in pkt.split(1024)) - return self.send_raw_data(client, pkt.marshal()) + res = self.send_raw_data(client, pkt.marshal()) + self.refresh_lock.release() + return res def send_raw_data(self, client: Hazelnut, data: bytes) -> int: """ Send a raw packet to a client. """ - return self.socket.sendto(data, (str(client.address), client.port)) + self.refresh_lock.acquire() + res = self.socket.sendto(data, (str(client.address), client.port)) + self.refresh_lock.release() + return res def receive_packet(self) -> Tuple[Packet, Hazelnut]: """ @@ -225,7 +237,7 @@ class Squirrel(Hazelnut): pkt = Packet.construct(DataTLV.construct(msg, self)) for hazelnut in list(self.activehazelnuts.values()): - self.send_packet(hazelnut, pkt) + self.send_packet(hazelnut[0], pkt) def handle_mouse_click(self, y: int, x: int, attr: int) -> None: """ @@ -488,8 +500,71 @@ class Squirrel(Hazelnut): curses.LINES - 2, curses.COLS - 2) self.refresh_lock.release() - - + + def potential_to_contact(self) -> list: + """ + Returns a list of hazelnuts the squirrel should contact if it does + not have enough symmetric neighbours. + """ + self.refresh_lock.acquire() + + res = [] + lp = len(self.potentialhazelnuts) + val = list(self.potentialhazelnuts.values()) + + for i in range(min(lp, max(0,self.minNS-self.nbNS))) : + a = randint(0, lp-1) + res.append(val[a]) + + self.refresh_lock.release() + return res + + def send_hello(self) -> None: + """ + Sends a long HelloTLV to all active neighbours. + """ + self.refresh_lock.acquire() + + for hazelnut in self.activehazelnuts.values() : + htlv = HelloTLV().construct(16, self, hazelnut[0]) + pkt = Packet().construct(htlv) + self.send_packet(hazelnut[0], pkt) + + self.refresh_lock.release() + + def verify_activity(self) -> None: + """ + All neighbours that have not sent a HelloTLV in the last 2 + minutes are considered not active. + """ + self.refresh_lock.acquire() + + val = list(self.activehazelnuts.values()) #create a copy because the dict size will change + + for hazelnut in val : + if time.time()-hazelnut[1]>10: #2*60: + gatlv = GoAwayTLV().construct(GoAwayType.TIMEOUT, "you did not talk to me") + pkt = Packet().construct(gatlv) + self.send_packet(hazelnut[0], pkt) + self.activehazelnuts.pop((str(hazelnut[0].address), hazelnut[0].port)) + self.potentialhazelnuts[(str(hazelnut[0].address), hazelnut[0].port)] = hazelnut[0] + + self.refresh_lock.release() + + def send_neighbours(self) -> None: + """ + Update the number of symmetric neighbours and + send all neighbours NeighbourTLV + """ + self.refresh_lock.acquire() + + nbNS = 0 + #for hazelnut in self.activehazelnuts.values() : + # if time.time()-hazelnut[2]<2*60: #could send the same to all neighbour, but it means that neighbour A could receive a message with itself in it -> if the others do not pay attention, trouble + + + self.refresh_lock.release() + class Worm(Thread): """ The worm is the hazel listener. @@ -513,6 +588,43 @@ class Worm(Thread): self.squirrel.refresh_history() self.squirrel.refresh_input() +class HazelManager(Thread): + """ + A process to cleanly manage the squirrel's neighbours + """ + def __init__(self, squirrel: Squirrel, *args, **kwargs): + super().__init__(*args, **kwargs) + self.squirrel = squirrel + self.last_potential = 0 + self.last_check = 0 + self.last_neighbour = 0 + + htlv = HelloTLV().construct(8, self.squirrel) + pkt = Packet().construct(htlv) + self.hellopkt = pkt + + def run(self) -> None: + while True: + #First part of neighbour management: ensure the squirrel has enough + #symmetric neighbours. + if time.time()-self.last_potential > 5: + to_contact = self.squirrel.potential_to_contact() + + for hazel in to_contact : + self.squirrel.send_packet(hazel, self.hellopkt) + self.last_potential = time.time() + + #Second part: send long HelloTLVs to neighbours every 30 seconds + if time.time()-self.last_check > 5: #30 : + self.squirrel.send_hello() + self.last_check = time.time() + + #Third part: get rid of inactive neighbours + self.squirrel.verify_activity() + + #Fourth part: verify symmetric neighbours and send NeighbourTLV every minute + + class Message: """ diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 5a68bec..d6e6312 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -162,32 +162,33 @@ class HelloTLV(TLV): if not squirrel.is_active(sender) : sender.id = self.source_id #The sender we are given misses an id else : - timeHL = squirrel.activehazelnuts[(sender.address, sender.port)] - if self.is_long and dest_id == squirrel.id : + timeHL = squirrel.activehazelnuts[(str(sender.address), sender.port)] + if self.is_long and self.dest_id == squirrel.id : timeHL = time.time() - #elif source_id != sender.id : - #That neighbour is lying about its ID squirrel.remove_from_potential(sender.address, sender.port) - squirrel.activehazelnuts[(sender.address, sender.port)] = [sender, timeH,\ - timeHL, True] - squirrel.add_system_message("Aaaawwww, someone spoke to me and said me Hello smiling_face_with_" - + (":3_hearts:" if self.is_long else "smiling_eyes:")) + squirrel.activehazelnuts[(str(sender.address), sender.port)] = [sender, timeH,\ + timeHL] + squirrel.nbNS += 1 + squirrel.add_system_message(f"Aaaawwww, {self.source_id} spoke to me and said me Hello " + + ("long" if self.is_long else "short")) @property def is_long(self) -> bool: return self.length == 16 @staticmethod - def construct(length: str, squirrel: Any) -> "HelloTLV": + def construct(length: int, squirrel: Any, destination: Any = None) -> "HelloTLV": tlv = HelloTLV() tlv.type = 2 tlv.source_id = squirrel.id if squirrel else 0 - if length == "short": + if (destination is None) or destination.id == -1 or length == 8: tlv.length = 8 + tlv.dest_id = None #if the destination id is not known, or + #if the destination was not precised, send a short hello else : tlv.length = 16 - tlv.dest_id = None + tlv.dest_id = destination.id return tlv @@ -322,14 +323,14 @@ class AckTLV(TLV): tlv.nonce = nonce return tlv - -class GoAwayTLV(TLV): - class GoAwayType(Enum): +class GoAwayType(Enum): UNKNOWN = 0 EXIT = 1 TIMEOUT = 2 PROTOCOL_VIOLATION = 3 +class GoAwayTLV(TLV): + type: int = 6 length: int code: GoAwayType @@ -350,15 +351,15 @@ class GoAwayTLV(TLV): def handle(self, squirrel: Any, sender: Any) -> None: if squirrel.is_active(sender) : squirrel.activehazelnuts.pop((sender.addess, sender.port)) - squirrel.potentialhazelnuts[(sender.address, sender.port)] = sender - squirrel.add_system_message("Some told me that he went away : "+message) + squirrel.potentialhazelnuts[(str(sender.address), sender.port)] = sender + squirrel.add_system_message("Some told me that he went away : "+self.message) @staticmethod - def construct(GAtype: GoAwayType, message: str) -> "Pad1TLV": + def construct(GAtype: GoAwayType, message: str) -> "GoAwayTLV": tlv = GoAwayTLV() tlv.type = 6 tlv.code = GAtype - tlv.message = message.encode("UTF-8") + tlv.message = message tlv.length = 1 + len(tlv.message) return tlv diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index c1f900b..2a94668 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -2,10 +2,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later import curses +import time from argparse import ArgumentParser from typing import Any -from .hazel import Hazelnut, Squirrel, Worm +from .hazel import Hazelnut, Squirrel, Worm, HazelManager +from .messages import Packet, HelloTLV from .term_manager import TermManager @@ -74,7 +76,14 @@ class Squinnondation: if instance.args.client_address and instance.args.client_port: hazelnut = Hazelnut(address=instance.args.client_address, port=instance.args.client_port) - squirrel.activehazelnuts[(instance.args.client_address, instance.args.client_port)] = hazelnut + htlv = HelloTLV().construct(8, squirrel) + pkt = Packet().construct(htlv) + squirrel.send_packet(hazelnut, pkt) + +## if squirrel.port != 8082: +## hazelnut = Hazelnut(address='::1', port=8082) +## squirrel.potentialhazelnuts['::1',8082] = hazelnut Worm(squirrel).start() + HazelManager(squirrel).start() squirrel.wait_for_key()