From 5c9b63d9dbd1e5484e1c388463dfdaa41189e895 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 17:05:03 +0100 Subject: [PATCH 01/19] Restructurate active hazels --- squinnondation/hazel.py | 48 +++++++++++++++++--------------- squinnondation/messages.py | 7 +++-- squinnondation/squinnondation.py | 5 +--- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 7b173f0..e258d90 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -21,6 +21,9 @@ class Hazelnut: def __init__(self, nickname: str = None, address: str = "localhost", port: int = 2500): self.nickname = nickname self.id = -1 + self.last_hello_time = 0 + self.last_long_hello_time = 0 + self.symmetric = False try: # Resolve DNS as an IPv6 @@ -75,11 +78,14 @@ 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 symmetric] + self.activehazelnuts = dict() self.nbNS = 0 self.minNS = 3 # minimal number of symmetric neighbours a squirrel needs to have. + self.worm = Worm(self) + self.hazel_manager = HazelManager(self) + self.inondator = Inondator(self) + self.add_system_message(f"Listening on {self.address}:{self.port}") self.add_system_message(f"I am {self.id}") @@ -105,7 +111,7 @@ class Squirrel(Hazelnut): creates a new hazelnut. """ if (address, port) in self.activehazelnuts: - return self.activehazelnuts[(address, port)][0] + return self.activehazelnuts[(address, port)] hazelnut = Hazelnut(address=address, port=port) return hazelnut @@ -148,10 +154,6 @@ class Squirrel(Hazelnut): """ Start asynchronous threads. """ - self.worm = Worm(self) - self.hazel_manager = HazelManager(self) - self.inondator = Inondator(self) - # Kill subthreads when exitting the program self.worm.setDaemon(True) self.hazel_manager.setDaemon(True) @@ -260,7 +262,7 @@ class Squirrel(Hazelnut): pkt = Packet.construct(DataTLV.construct(msg, self)) for hazelnut in list(self.activehazelnuts.values()): - self.send_packet(hazelnut[0], pkt) + self.send_packet(hazelnut, pkt) def handle_mouse_click(self, y: int, x: int, attr: int) -> None: """ @@ -346,9 +348,9 @@ class Squirrel(Hazelnut): res = dict() hazels = list(self.activehazelnuts.items()) for key, hazel in hazels: - if hazel[3]: # Only if the neighbour is symmetric + if hazel.symmetric: next_send = uniform(1, 2) - res[key] = [hazel[0], time.time() + next_send, 0] + res[key] = [hazel, time.time() + next_send, 0] return res def remove_from_inundation(self, hazel: Hazelnut, sender_id: int, nonce: int) -> None: @@ -626,9 +628,9 @@ class Squirrel(Hazelnut): self.refresh_lock.acquire() for hazelnut in self.activehazelnuts.values(): - htlv = HelloTLV().construct(16, self, hazelnut[0]) + htlv = HelloTLV().construct(16, self, hazelnut) pkt = Packet().construct(htlv) - self.send_packet(hazelnut[0], pkt) + self.send_packet(hazelnut, pkt) self.refresh_lock.release() @@ -642,12 +644,12 @@ class Squirrel(Hazelnut): val = list(self.activehazelnuts.values()) # create a copy because the dict size will change for hazelnut in val: - if time.time() - hazelnut[1] > 2 * 60: + if time.time() - hazelnut.last_hello_time > 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((hazelnut[0].address, hazelnut[0].port)) - self.potentialhazelnuts[(hazelnut[0].address, hazelnut[0].port)] = hazelnut[0] + self.send_packet(hazelnut, pkt) + self.activehazelnuts.pop((hazelnut.address, hazelnut.port)) + self.potentialhazelnuts[(hazelnut.address, hazelnut.port)] = hazelnut self.refresh_lock.release() @@ -662,16 +664,16 @@ class Squirrel(Hazelnut): # 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 for key, hazelnut in self.activehazelnuts.items(): - if time.time() - hazelnut[2] <= 2 * 60: + if time.time() - hazelnut.last_long_hello_time <= 2 * 60: nb_ns += 1 - self.activehazelnuts[key][3] = True - ntlv = NeighbourTLV().construct(hazelnut[0].address, hazelnut[0].port) + self.activehazelnuts[key].symmetric = True + ntlv = NeighbourTLV().construct(hazelnut.address, hazelnut.port) pkt = Packet().construct(ntlv) for destination in self.activehazelnuts.values(): - if destination[0].id != hazelnut[0].id: - self.send_packet(destination[0], pkt) + if destination.id != hazelnut.id: + self.send_packet(destination, pkt) else: - self.activehazelnuts[key][3] = False + self.activehazelnuts[key].symmetric = False self.nbNS = nb_ns self.refresh_lock.release() @@ -690,7 +692,7 @@ class Squirrel(Hazelnut): gatlv = GoAwayTLV().construct(GoAwayType.EXIT, "I am leaving! Good bye!") pkt = Packet.construct(gatlv) for hazelnut in self.activehazelnuts.values(): - self.send_packet(hazelnut[0], pkt) + self.send_packet(hazelnut, pkt) exit(0) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index d6157a4..3ab31d3 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -163,7 +163,7 @@ class HelloTLV(TLV): sender.id = self.source_id # The sender we are given misses an id time_hl = time.time() else: - time_hl = squirrel.activehazelnuts[(sender.address, sender.port)][2] + time_hl = sender.last_long_hello_time if self.is_long and self.dest_id == squirrel.id: time_hl = time.time() @@ -171,7 +171,10 @@ class HelloTLV(TLV): squirrel.remove_from_potential(sender.address, sender.port) # Add entry to/actualize the active hazelnuts dictionnary - squirrel.activehazelnuts[(sender.address, sender.port)] = [sender, time_h, time_hl, True] + sender.last_hello_time = time_h + sender.last_long_hello_time = time_hl + sender.symmetric = True + squirrel.activehazelnuts[(sender.address, sender.port)] = sender squirrel.nbNS += 1 # squirrel.add_system_message(f"Aaaawwww, {self.source_id} spoke to me and said Hello " # + ("long" if self.is_long else "short")) diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index 22c9999..be8f09d 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -78,13 +78,10 @@ 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.potentialhazelnuts[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 - squirrel.start_threads() squirrel.wait_for_key() From 18d17379146a39cec7ef2005a033506b0434c9ee Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 17:11:34 +0100 Subject: [PATCH 02/19] Send a long hello as a response of a HelloTLV --- squinnondation/messages.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 3ab31d3..661e6d2 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -176,8 +176,10 @@ class HelloTLV(TLV): sender.symmetric = True squirrel.activehazelnuts[(sender.address, sender.port)] = sender squirrel.nbNS += 1 - # squirrel.add_system_message(f"Aaaawwww, {self.source_id} spoke to me and said Hello " - # + ("long" if self.is_long else "short")) + squirrel.add_system_message(f"{self.source_id} sent me a Hello " + ("long" if self.is_long else "short")) + + if not self.is_long: + squirrel.send_packet(sender, Packet.construct(HelloTLV.construct(16, squirrel, sender))) @property def is_long(self) -> bool: From 311e1539f9316f933b8de1acd9d6c57f1ac7c1e5 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 18:12:57 +0100 Subject: [PATCH 03/19] Store a single hazelnut table --- squinnondation/hazel.py | 39 ++++++++++++++++++++------------------ squinnondation/messages.py | 16 ++++++++-------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index e258d90..a09f1c0 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -24,6 +24,8 @@ class Hazelnut: self.last_hello_time = 0 self.last_long_hello_time = 0 self.symmetric = False + self.active = False + self.potential = False try: # Resolve DNS as an IPv6 @@ -77,8 +79,7 @@ class Squirrel(Hazelnut): curses.init_pair(i + 1, i, curses.COLOR_BLACK) # dictionnaries of neighbours - self.potentialhazelnuts = dict() - self.activehazelnuts = dict() + self.hazelnuts = dict() self.nbNS = 0 self.minNS = 3 # minimal number of symmetric neighbours a squirrel needs to have. @@ -96,22 +97,21 @@ class Squirrel(Hazelnut): hazelnut = Hazelnut(address=address, port=port) return hazelnut - def is_active(self, hazel: Hazelnut) -> bool: - return (hazel.address, hazel.port) in self.activehazelnuts + @property + def activehazelnuts(self) -> dict: + return {(address, port): hazelnut for (address, port), hazelnut in self.hazelnuts.items() if hazelnut.active} - def is_potential(self, hazel: Hazelnut) -> bool: - return (hazel.address, hazel.port) in self.potentialhazelnuts - - def remove_from_potential(self, address: str, port: int) -> None: - self.potentialhazelnuts.pop((address, port), None) + @property + def potentialhazelnuts(self) -> dict: + return {(address, port): hazelnut for (address, port), hazelnut in self.hazelnuts.items() if hazelnut.potential} def find_hazelnut(self, address: str, port: int) -> Hazelnut: """ Translate an address into a hazelnut. If this hazelnut does not exist, creates a new hazelnut. """ - if (address, port) in self.activehazelnuts: - return self.activehazelnuts[(address, port)] + if (address, port) in self.hazelnuts: + return self.hazelnuts[(address, port)] hazelnut = Hazelnut(address=address, port=port) return hazelnut @@ -402,9 +402,8 @@ class Squirrel(Hazelnut): if self.recent_messages[key][2][key2][2] >= 5: # the neighbour is not reactive enough gatlv = GoAwayTLV().construct(GoAwayType.TIMEOUT, f"{self.id} No acknowledge") pkt = Packet().construct(gatlv) - self.send_packet(self.recent_messages[key][2][key2][0], pkt) - self.activehazelnuts.pop(key2) - self.potentialhazelnuts[key2] = self.recent_messages[key][2][key2][0] + hazelnut = self.recent_messages[key][2][key2][0] + self.send_packet(hazelnut, pkt) self.recent_messages[key][2].pop(key2) self.refresh_lock.release() @@ -648,11 +647,15 @@ class Squirrel(Hazelnut): gatlv = GoAwayTLV().construct(GoAwayType.TIMEOUT, "you did not talk to me") pkt = Packet().construct(gatlv) self.send_packet(hazelnut, pkt) - self.activehazelnuts.pop((hazelnut.address, hazelnut.port)) - self.potentialhazelnuts[(hazelnut.address, hazelnut.port)] = hazelnut + hazelnut.active = False + hazelnut.potential = True + self.update_hazelnut_table(hazelnut) self.refresh_lock.release() + def update_hazelnut_table(self, hazelnut: Hazelnut) -> None: + self.hazelnuts[(hazelnut.address, hazelnut.port)] = hazelnut + def send_neighbours(self) -> None: """ Update the number of symmetric neighbours and @@ -666,7 +669,7 @@ class Squirrel(Hazelnut): for key, hazelnut in self.activehazelnuts.items(): if time.time() - hazelnut.last_long_hello_time <= 2 * 60: nb_ns += 1 - self.activehazelnuts[key].symmetric = True + self.hazelnuts[key].symmetric = True ntlv = NeighbourTLV().construct(hazelnut.address, hazelnut.port) pkt = Packet().construct(ntlv) for destination in self.activehazelnuts.values(): @@ -748,7 +751,7 @@ class HazelManager(Thread): # Second part: send long HelloTLVs to neighbours every 30 seconds if time.time() - self.last_check > 30: - self.squirrel.add_system_message(f"I have {len(list(self.squirrel.activehazelnuts.values()))} friends") + self.squirrel.add_system_message(f"I have {len(list(self.squirrel.activehazelnuts))} friends") self.squirrel.send_hello() self.last_check = time.time() diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 661e6d2..531f3c9 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -159,7 +159,7 @@ class HelloTLV(TLV): def handle(self, squirrel: Any, sender: Any) -> None: time_h = time.time() - if not squirrel.is_active(sender): + if not sender.active: sender.id = self.source_id # The sender we are given misses an id time_hl = time.time() else: @@ -167,14 +167,13 @@ class HelloTLV(TLV): if self.is_long and self.dest_id == squirrel.id: time_hl = time.time() - # Make sure the sender is not in the potential hazelnuts - squirrel.remove_from_potential(sender.address, sender.port) - # Add entry to/actualize the active hazelnuts dictionnary sender.last_hello_time = time_h sender.last_long_hello_time = time_hl sender.symmetric = True - squirrel.activehazelnuts[(sender.address, sender.port)] = sender + sender.potential = False + sender.active = True + squirrel.update_hazelnut_table(sender) squirrel.nbNS += 1 squirrel.add_system_message(f"{self.source_id} sent me a Hello " + ("long" if self.is_long else "short")) @@ -374,9 +373,10 @@ class GoAwayTLV(TLV): self.message.encode("UTF-8")[:self.length - 1] def handle(self, squirrel: Any, sender: Any) -> None: - if squirrel.is_active(sender): - squirrel.activehazelnuts.pop((sender.address, sender.port)) - squirrel.potentialhazelnuts[(sender.address, sender.port)] = sender + if sender.active: + sender.active = False + sender.potential = True + squirrel.update_hazelnut_table(sender) squirrel.add_system_message("Some told me that he went away : " + self.message) @staticmethod From 95a2501e49ac4721826071d60234ca42faeec5ca Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 18:22:34 +0100 Subject: [PATCH 04/19] Update the full hazelnuts table when adding a new potential neighbour --- squinnondation/messages.py | 5 +++-- squinnondation/squinnondation.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 531f3c9..faaa7ad 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -229,8 +229,9 @@ class NeighbourTLV(TLV): return if not (str(self.ip_address), self.port) in squirrel.activehazelnuts \ and not (str(self.ip_address), self.port) in squirrel.potentialhazelnuts: - squirrel.potentialhazelnuts[(str(self.ip_address), self.port)] = \ - squirrel.new_hazel(str(self.ip_address), self.port) + hazelnut = squirrel.new_hazel(str(self.ip_address), self.port) + hazelnut.potential = True + squirrel.update_hazelnut_table(hazelnut) # squirrel.add_system_message(f"New potential friend {self.ip_address}:{self.port}!") @staticmethod diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index be8f09d..2b93e9b 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -78,7 +78,8 @@ 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.potentialhazelnuts[instance.args.client_address, instance.args.client_port] = hazelnut + hazelnut.potential = True + squirrel.update_hazelnut_table(hazelnut) htlv = HelloTLV().construct(8, squirrel) pkt = Packet().construct(htlv) squirrel.send_packet(hazelnut, pkt) From 5ffe0d21c365a1feccedbe526e8fb7863d81d6de Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 18:25:55 +0100 Subject: [PATCH 05/19] A hazelut is either active or potential --- squinnondation/hazel.py | 10 ++++++++-- squinnondation/messages.py | 2 -- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index a09f1c0..06f4ad8 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -25,7 +25,6 @@ class Hazelnut: self.last_long_hello_time = 0 self.symmetric = False self.active = False - self.potential = False try: # Resolve DNS as an IPv6 @@ -39,6 +38,14 @@ class Hazelnut: self.address = address # IPv6Address(address) self.port = port + @property + def potential(self) -> bool: + return not self.active + + @potential.setter + def potential(self, value: bool) -> None: + self.active = not value + class Squirrel(Hazelnut): """ @@ -648,7 +655,6 @@ class Squirrel(Hazelnut): pkt = Packet().construct(gatlv) self.send_packet(hazelnut, pkt) hazelnut.active = False - hazelnut.potential = True self.update_hazelnut_table(hazelnut) self.refresh_lock.release() diff --git a/squinnondation/messages.py b/squinnondation/messages.py index faaa7ad..2b94af8 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -171,7 +171,6 @@ class HelloTLV(TLV): sender.last_hello_time = time_h sender.last_long_hello_time = time_hl sender.symmetric = True - sender.potential = False sender.active = True squirrel.update_hazelnut_table(sender) squirrel.nbNS += 1 @@ -376,7 +375,6 @@ class GoAwayTLV(TLV): def handle(self, squirrel: Any, sender: Any) -> None: if sender.active: sender.active = False - sender.potential = True squirrel.update_hazelnut_table(sender) squirrel.add_system_message("Some told me that he went away : " + self.message) From 915dc3ec241afa31eeb7d2359206213b787fb7ce Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 19:13:07 +0100 Subject: [PATCH 06/19] Clients can have multiple addresses --- squinnondation/hazel.py | 27 +++++++++++++++++++-------- squinnondation/messages.py | 2 +- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 06f4ad8..dd74177 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -35,8 +35,8 @@ class Hazelnut: # See https://fr.wikipedia.org/wiki/Adresse_IPv6_mappant_IPv4 address = "::ffff:" + socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] - self.address = address # IPv6Address(address) - self.port = port + self.addresses = list() + self.addresses.append((address, port)) @property def potential(self) -> bool: @@ -46,6 +46,10 @@ class Hazelnut: def potential(self, value: bool) -> None: self.active = not value + @property + def main_address(self) -> Tuple[str, int]: + return self.addresses[0] + class Squirrel(Hazelnut): """ @@ -61,7 +65,7 @@ class Squirrel(Hazelnut): # Create UDP socket self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) # Bind the socket - self.socket.bind((self.address, self.port)) + self.socket.bind(self.main_address) self.squinnondation = instance @@ -94,7 +98,7 @@ class Squirrel(Hazelnut): self.hazel_manager = HazelManager(self) self.inondator = Inondator(self) - self.add_system_message(f"Listening on {self.address}:{self.port}") + self.add_system_message(f"Listening on {self.main_address[0]}:{self.main_address[1]}") self.add_system_message(f"I am {self.id}") def new_hazel(self, address: str, port: int) -> Hazelnut: @@ -139,7 +143,7 @@ class Squirrel(Hazelnut): Send a raw packet to a client. """ self.refresh_lock.acquire() - res = self.socket.sendto(data, (client.address, client.port)) + res = self.socket.sendto(data, client.main_address) self.refresh_lock.release() return res @@ -367,7 +371,8 @@ class Squirrel(Hazelnut): self.refresh_lock.acquire() if (sender_id, nonce) in self.recent_messages: # If a peer is late in its acknowledgement, the absence of the previous if causes an error. - self.recent_messages[(sender_id, nonce)][2].pop((hazel.address, hazel.port), None) + for addr in hazel.addresses: + self.recent_messages[(sender_id, nonce)][2].pop(addr, None) if not self.recent_messages[(sender_id, nonce)][2]: # If dictionnary is empty, remove the message self.recent_messages.pop((sender_id, nonce), None) @@ -660,7 +665,13 @@ class Squirrel(Hazelnut): self.refresh_lock.release() def update_hazelnut_table(self, hazelnut: Hazelnut) -> None: - self.hazelnuts[(hazelnut.address, hazelnut.port)] = hazelnut + for addr in hazelnut.addresses: + if addr in self.hazelnuts: + old_hazel = self.hazelnuts[addr] + for other_addr in old_hazel.addresses: + if other_addr not in hazelnut.addresses: + hazelnut.addresses.append(other_addr) + self.hazelnuts[addr] = hazelnut def send_neighbours(self) -> None: """ @@ -676,7 +687,7 @@ class Squirrel(Hazelnut): if time.time() - hazelnut.last_long_hello_time <= 2 * 60: nb_ns += 1 self.hazelnuts[key].symmetric = True - ntlv = NeighbourTLV().construct(hazelnut.address, hazelnut.port) + ntlv = NeighbourTLV().construct(*hazelnut.main_address) pkt = Packet().construct(ntlv) for destination in self.activehazelnuts.values(): if destination.id != hazelnut.id: diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 2b94af8..e718e34 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -222,7 +222,7 @@ class NeighbourTLV(TLV): self.port.to_bytes(2, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: - if squirrel.address == str(self.ip_address) and squirrel.port == self.port: + if (self.ip_address, self.port) in squirrel.addresses: # This case should never happen (and in our protocol it is not possible), # but we include this test as a security measure. return From f85b0883673db8f40d648a183031a70e965e5fb7 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 19:17:33 +0100 Subject: [PATCH 07/19] Don't need to use a lock when manipulating packets --- squinnondation/hazel.py | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index dd74177..68f2d0e 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -130,22 +130,17 @@ 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)) 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. """ - self.refresh_lock.acquire() - res = self.socket.sendto(data, client.main_address) - self.refresh_lock.release() - return res + return self.socket.sendto(data, client.main_address) def receive_packet(self) -> Tuple[Packet, Hazelnut]: """ @@ -368,7 +363,6 @@ class Squirrel(Hazelnut): """ Remove the sender from the list of neighbours to be inundated """ - self.refresh_lock.acquire() if (sender_id, nonce) in self.recent_messages: # If a peer is late in its acknowledgement, the absence of the previous if causes an error. for addr in hazel.addresses: @@ -376,26 +370,19 @@ class Squirrel(Hazelnut): if not self.recent_messages[(sender_id, nonce)][2]: # If dictionnary is empty, remove the message self.recent_messages.pop((sender_id, nonce), None) - self.refresh_lock.release() def clean_inundation(self) -> None: """ Remove messages which are overdue (older than 2 minutes) from the inundation dictionnary. """ - self.refresh_lock.acquire() - for key in self.recent_messages: if time.time() - self.recent_messages[key][1] > 120: self.recent_messages.pop(key) - self.refresh_lock.release() - def main_inundation(self) -> None: """ The main inundation function. """ - self.refresh_lock.acquire() - for key in self.recent_messages: k = list(self.recent_messages[key][2].keys()) for key2 in k: @@ -418,8 +405,6 @@ class Squirrel(Hazelnut): self.send_packet(hazelnut, pkt) self.recent_messages[key][2].pop(key2) - self.refresh_lock.release() - def add_system_message(self, msg: str) -> None: """ Add a new system log message. @@ -619,8 +604,6 @@ class Squirrel(Hazelnut): 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()) @@ -628,30 +611,22 @@ class Squirrel(Hazelnut): 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) pkt = Packet().construct(htlv) self.send_packet(hazelnut, 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: @@ -662,8 +637,6 @@ class Squirrel(Hazelnut): hazelnut.active = False self.update_hazelnut_table(hazelnut) - self.refresh_lock.release() - def update_hazelnut_table(self, hazelnut: Hazelnut) -> None: for addr in hazelnut.addresses: if addr in self.hazelnuts: @@ -678,8 +651,6 @@ class Squirrel(Hazelnut): Update the number of symmetric neighbours and send all neighbours NeighbourTLV """ - self.refresh_lock.acquire() - nb_ns = 0 # 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 @@ -696,14 +667,10 @@ class Squirrel(Hazelnut): self.activehazelnuts[key].symmetric = False self.nbNS = nb_ns - self.refresh_lock.release() - def leave(self) -> None: """ The program is exited. We send a GoAway to our neighbours, then close the program. """ - self.refresh_lock.acquire() - # Last inundation self.main_inundation() self.clean_inundation() From 56020390b2fd77bcfc9831372f4a2aea5169a38e Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 19:19:43 +0100 Subject: [PATCH 08/19] activehazelnuts -> active_hazelnuts --- squinnondation/hazel.py | 28 ++++++++++++++-------------- squinnondation/messages.py | 4 ++-- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 68f2d0e..c276daa 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -109,11 +109,11 @@ class Squirrel(Hazelnut): return hazelnut @property - def activehazelnuts(self) -> dict: + def active_hazelnuts(self) -> dict: return {(address, port): hazelnut for (address, port), hazelnut in self.hazelnuts.items() if hazelnut.active} @property - def potentialhazelnuts(self) -> dict: + def potential_hazelnuts(self) -> dict: return {(address, port): hazelnut for (address, port), hazelnut in self.hazelnuts.items() if hazelnut.potential} def find_hazelnut(self, address: str, port: int) -> Hazelnut: @@ -267,7 +267,7 @@ class Squirrel(Hazelnut): self.add_message(msg) pkt = Packet.construct(DataTLV.construct(msg, self)) - for hazelnut in list(self.activehazelnuts.values()): + for hazelnut in list(self.active_hazelnuts.values()): self.send_packet(hazelnut, pkt) def handle_mouse_click(self, y: int, x: int, attr: int) -> None: @@ -349,10 +349,10 @@ class Squirrel(Hazelnut): def make_inundation_dict(self) -> dict: """ - Takes the activehazels dictionnary and returns a list of [hazel, date+random, 0] + Takes the active hazels dictionnary and returns a list of [hazel, date+random, 0] """ res = dict() - hazels = list(self.activehazelnuts.items()) + hazels = list(self.active_hazelnuts.items()) for key, hazel in hazels: if hazel.symmetric: next_send = uniform(1, 2) @@ -605,8 +605,8 @@ class Squirrel(Hazelnut): not have enough symmetric neighbours. """ res = [] - lp = len(self.potentialhazelnuts) - val = list(self.potentialhazelnuts.values()) + lp = len(self.potential_hazelnuts) + val = list(self.potential_hazelnuts.values()) for i in range(min(lp, max(0, self.minNS - self.nbNS))): a = randint(0, lp - 1) @@ -617,7 +617,7 @@ class Squirrel(Hazelnut): """ Sends a long HelloTLV to all active neighbours. """ - for hazelnut in self.activehazelnuts.values(): + for hazelnut in self.active_hazelnuts.values(): htlv = HelloTLV().construct(16, self, hazelnut) pkt = Packet().construct(htlv) self.send_packet(hazelnut, pkt) @@ -627,7 +627,7 @@ class Squirrel(Hazelnut): All neighbours that have not sent a HelloTLV in the last 2 minutes are considered not active. """ - val = list(self.activehazelnuts.values()) # create a copy because the dict size will change + val = list(self.active_hazelnuts.values()) # create a copy because the dict size will change for hazelnut in val: if time.time() - hazelnut.last_hello_time > 2 * 60: @@ -654,17 +654,17 @@ class Squirrel(Hazelnut): nb_ns = 0 # 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 - for key, hazelnut in self.activehazelnuts.items(): + for key, hazelnut in self.active_hazelnuts.items(): if time.time() - hazelnut.last_long_hello_time <= 2 * 60: nb_ns += 1 self.hazelnuts[key].symmetric = True ntlv = NeighbourTLV().construct(*hazelnut.main_address) pkt = Packet().construct(ntlv) - for destination in self.activehazelnuts.values(): + for destination in self.active_hazelnuts.values(): if destination.id != hazelnut.id: self.send_packet(destination, pkt) else: - self.activehazelnuts[key].symmetric = False + self.active_hazelnuts[key].symmetric = False self.nbNS = nb_ns def leave(self) -> None: @@ -678,7 +678,7 @@ class Squirrel(Hazelnut): # Broadcast a GoAway gatlv = GoAwayTLV().construct(GoAwayType.EXIT, "I am leaving! Good bye!") pkt = Packet.construct(gatlv) - for hazelnut in self.activehazelnuts.values(): + for hazelnut in self.active_hazelnuts.values(): self.send_packet(hazelnut, pkt) exit(0) @@ -735,7 +735,7 @@ class HazelManager(Thread): # Second part: send long HelloTLVs to neighbours every 30 seconds if time.time() - self.last_check > 30: - self.squirrel.add_system_message(f"I have {len(list(self.squirrel.activehazelnuts))} friends") + self.squirrel.add_system_message(f"I have {len(list(self.squirrel.active_hazelnuts))} friends") self.squirrel.send_hello() self.last_check = time.time() diff --git a/squinnondation/messages.py b/squinnondation/messages.py index e718e34..35c85f7 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -226,8 +226,8 @@ class NeighbourTLV(TLV): # This case should never happen (and in our protocol it is not possible), # but we include this test as a security measure. return - if not (str(self.ip_address), self.port) in squirrel.activehazelnuts \ - and not (str(self.ip_address), self.port) in squirrel.potentialhazelnuts: + if not (str(self.ip_address), self.port) in squirrel.active_hazelnuts \ + and not (str(self.ip_address), self.port) in squirrel.potential_hazelnuts: hazelnut = squirrel.new_hazel(str(self.ip_address), self.port) hazelnut.potential = True squirrel.update_hazelnut_table(hazelnut) From 2d670a56a1b1702c59ec00db094d61a51d0b8204 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 20:05:19 +0100 Subject: [PATCH 09/19] Merge clients that have the same id if it speaks on multiple addresses --- squinnondation/hazel.py | 19 +++++++++++++++++++ squinnondation/messages.py | 7 +++++++ squinnondation/squinnondation.py | 2 -- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index c276daa..4af8e31 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -50,6 +50,12 @@ class Hazelnut: def main_address(self) -> Tuple[str, int]: return self.addresses[0] + def __repr__(self): + return f"{self.id or ''}: {self.nickname or ''}, {self.addresses}" + + def __str__(self): + return repr(self) + class Squirrel(Hazelnut): """ @@ -122,8 +128,11 @@ class Squirrel(Hazelnut): creates a new hazelnut. """ if (address, port) in self.hazelnuts: + self.add_system_message(str(self.hazelnuts)) return self.hazelnuts[(address, port)] hazelnut = Hazelnut(address=address, port=port) + self.hazelnuts[(address, port)] = hazelnut + self.add_system_message(str(self.hazelnuts)) return hazelnut def send_packet(self, client: Hazelnut, pkt: Packet) -> int: @@ -640,12 +649,22 @@ class Squirrel(Hazelnut): def update_hazelnut_table(self, hazelnut: Hazelnut) -> None: for addr in hazelnut.addresses: if addr in self.hazelnuts: + # Merge with the previous hazel old_hazel = self.hazelnuts[addr] for other_addr in old_hazel.addresses: if other_addr not in hazelnut.addresses: hazelnut.addresses.append(other_addr) + self.hazelnuts[other_addr] = hazelnut self.hazelnuts[addr] = hazelnut + for other_hazel in list(self.hazelnuts.values()): + if other_hazel.id == hazelnut.id > 0 and other_hazel != hazelnut: + # The hazelnut with the same id is known as a different address. We merge everything + for addr in other_hazel.addresses: + self.hazelnuts[addr] = hazelnut + if addr not in hazelnut.addresses: + hazelnut.addresses.append(addr) + def send_neighbours(self) -> None: """ Update the number of symmetric neighbours and diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 35c85f7..f938c66 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -167,6 +167,13 @@ class HelloTLV(TLV): if self.is_long and self.dest_id == squirrel.id: time_hl = time.time() + if sender.id != self.source_id: + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + f"You were known as the ID {sender.id}, but you declared that you have the ID {self.source_id}."))) + squirrel.add_system_message(f"A client known as the id {sender.id} declared that it uses " + f"the id {self.source_id}.") + sender.id = self.source_id + # Add entry to/actualize the active hazelnuts dictionnary sender.last_hello_time = time_h sender.last_long_hello_time = time_hl diff --git a/squinnondation/squinnondation.py b/squinnondation/squinnondation.py index 2b93e9b..f17da92 100644 --- a/squinnondation/squinnondation.py +++ b/squinnondation/squinnondation.py @@ -78,8 +78,6 @@ class Squinnondation: if instance.args.client_address and instance.args.client_port: hazelnut = Hazelnut(address=instance.args.client_address, port=instance.args.client_port) - hazelnut.potential = True - squirrel.update_hazelnut_table(hazelnut) htlv = HelloTLV().construct(8, squirrel) pkt = Packet().construct(htlv) squirrel.send_packet(hazelnut, pkt) From c7c8765fad6fc309f717c7c92e2a93617bd67979 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 20:21:13 +0100 Subject: [PATCH 10/19] Fix the warnings when a hazel use a different id/nickname that was previously known --- squinnondation/hazel.py | 12 ++++++++++-- squinnondation/messages.py | 32 +++++++++++++++++--------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 4af8e31..5bd436d 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -128,13 +128,21 @@ class Squirrel(Hazelnut): creates a new hazelnut. """ if (address, port) in self.hazelnuts: - self.add_system_message(str(self.hazelnuts)) return self.hazelnuts[(address, port)] hazelnut = Hazelnut(address=address, port=port) self.hazelnuts[(address, port)] = hazelnut - self.add_system_message(str(self.hazelnuts)) return hazelnut + def find_hazelnut_by_id(self, hazel_id: int) -> Hazelnut: + """ + Retrieve the hazelnut that is known by its id. Return None if it is unknown. + The given identifier must be positive. + """ + if hazel_id > 0: + for hazelnut in self.hazelnuts.values(): + if hazelnut.id == hazel_id: + return hazelnut + def send_packet(self, client: Hazelnut, pkt: Packet) -> int: """ Send a formatted packet to a client. diff --git a/squinnondation/messages.py b/squinnondation/messages.py index f938c66..7ce7d1a 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -12,7 +12,6 @@ import time class TLV: """ The Tag-Length-Value contains the different type of data that can be sent. - TODO: add subclasses for each type of TLV """ type: int length: int @@ -159,6 +158,14 @@ class HelloTLV(TLV): def handle(self, squirrel: Any, sender: Any) -> None: time_h = time.time() + + if sender.id > 0 and sender.id != self.source_id: + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + f"You were known as the ID {sender.id}, but you declared that you have the ID {self.source_id}."))) + squirrel.add_system_message(f"A client known as the id {sender.id} declared that it uses " + f"the id {self.source_id}.") + sender.id = self.source_id + if not sender.active: sender.id = self.source_id # The sender we are given misses an id time_hl = time.time() @@ -167,13 +174,6 @@ class HelloTLV(TLV): if self.is_long and self.dest_id == squirrel.id: time_hl = time.time() - if sender.id != self.source_id: - squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( - f"You were known as the ID {sender.id}, but you declared that you have the ID {self.source_id}."))) - squirrel.add_system_message(f"A client known as the id {sender.id} declared that it uses " - f"the id {self.source_id}.") - sender.id = self.source_id - # Add entry to/actualize the active hazelnuts dictionnary sender.last_hello_time = time_h sender.last_long_hello_time = time_hl @@ -296,13 +296,15 @@ class DataTLV(TLV): "Unable to retrieve your username. Please use the syntax 'nickname: message'"))) else: nickname = nickname_match.group(1) - if sender.nickname is None: - sender.nickname = nickname - elif sender.nickname != nickname: - squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( - "It seems that you used two different nicknames. " - f"Known nickname: {sender.nickname}, found: {nickname}"))) - sender.nickname = nickname + author = squirrel.find_hazelnut_by_id(self.sender_id) + if author: + if author.nickname is None: + author.nickname = nickname + elif author.nickname != nickname: + squirrel.send_packet(author, Packet.construct(WarningTLV.construct( + "It seems that you used two different nicknames. " + f"Known nickname: {author.nickname}, found: {nickname}"))) + author.nickname = nickname @staticmethod def construct(message: str, squirrel: Any) -> "DataTLV": From f062ee7b1313ce2ed2f24cc1180a5c43239138f2 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 20:26:22 +0100 Subject: [PATCH 11/19] Use hazelnut sets instead of dictionaries to avoid duplicate hazels --- squinnondation/hazel.py | 32 ++++++++++++++++---------------- squinnondation/messages.py | 3 +-- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 5bd436d..24fd6d2 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -115,12 +115,12 @@ class Squirrel(Hazelnut): return hazelnut @property - def active_hazelnuts(self) -> dict: - return {(address, port): hazelnut for (address, port), hazelnut in self.hazelnuts.items() if hazelnut.active} + def active_hazelnuts(self) -> set: + return set(hazelnut for hazelnut in self.hazelnuts.values() if hazelnut.active) @property - def potential_hazelnuts(self) -> dict: - return {(address, port): hazelnut for (address, port), hazelnut in self.hazelnuts.items() if hazelnut.potential} + def potential_hazelnuts(self) -> set: + return set(hazelnut for hazelnut in self.hazelnuts.values() if hazelnut.potential) def find_hazelnut(self, address: str, port: int) -> Hazelnut: """ @@ -284,7 +284,7 @@ class Squirrel(Hazelnut): self.add_message(msg) pkt = Packet.construct(DataTLV.construct(msg, self)) - for hazelnut in list(self.active_hazelnuts.values()): + for hazelnut in self.active_hazelnuts: self.send_packet(hazelnut, pkt) def handle_mouse_click(self, y: int, x: int, attr: int) -> None: @@ -369,7 +369,7 @@ class Squirrel(Hazelnut): Takes the active hazels dictionnary and returns a list of [hazel, date+random, 0] """ res = dict() - hazels = list(self.active_hazelnuts.items()) + hazels = self.active_hazelnuts for key, hazel in hazels: if hazel.symmetric: next_send = uniform(1, 2) @@ -622,8 +622,8 @@ class Squirrel(Hazelnut): not have enough symmetric neighbours. """ res = [] - lp = len(self.potential_hazelnuts) - val = list(self.potential_hazelnuts.values()) + val = list(self.potential_hazelnuts) + lp = len(val) for i in range(min(lp, max(0, self.minNS - self.nbNS))): a = randint(0, lp - 1) @@ -634,7 +634,7 @@ class Squirrel(Hazelnut): """ Sends a long HelloTLV to all active neighbours. """ - for hazelnut in self.active_hazelnuts.values(): + for hazelnut in self.active_hazelnuts: htlv = HelloTLV().construct(16, self, hazelnut) pkt = Packet().construct(htlv) self.send_packet(hazelnut, pkt) @@ -644,7 +644,7 @@ class Squirrel(Hazelnut): All neighbours that have not sent a HelloTLV in the last 2 minutes are considered not active. """ - val = list(self.active_hazelnuts.values()) # create a copy because the dict size will change + val = list(self.active_hazelnuts) # create a copy because the dict size will change for hazelnut in val: if time.time() - hazelnut.last_hello_time > 2 * 60: @@ -681,17 +681,17 @@ class Squirrel(Hazelnut): nb_ns = 0 # 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 - for key, hazelnut in self.active_hazelnuts.items(): + for hazelnut in self.active_hazelnuts: if time.time() - hazelnut.last_long_hello_time <= 2 * 60: nb_ns += 1 - self.hazelnuts[key].symmetric = True + hazelnut.symmetric = True ntlv = NeighbourTLV().construct(*hazelnut.main_address) pkt = Packet().construct(ntlv) - for destination in self.active_hazelnuts.values(): + for destination in self.active_hazelnuts: if destination.id != hazelnut.id: self.send_packet(destination, pkt) else: - self.active_hazelnuts[key].symmetric = False + hazelnut.symmetric = False self.nbNS = nb_ns def leave(self) -> None: @@ -705,7 +705,7 @@ class Squirrel(Hazelnut): # Broadcast a GoAway gatlv = GoAwayTLV().construct(GoAwayType.EXIT, "I am leaving! Good bye!") pkt = Packet.construct(gatlv) - for hazelnut in self.active_hazelnuts.values(): + for hazelnut in self.active_hazelnuts: self.send_packet(hazelnut, pkt) exit(0) @@ -762,7 +762,7 @@ class HazelManager(Thread): # Second part: send long HelloTLVs to neighbours every 30 seconds if time.time() - self.last_check > 30: - self.squirrel.add_system_message(f"I have {len(list(self.squirrel.active_hazelnuts))} friends") + self.squirrel.add_system_message(f"I have {len(self.squirrel.active_hazelnuts)} friends") self.squirrel.send_hello() self.last_check = time.time() diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 7ce7d1a..5145560 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -233,8 +233,7 @@ class NeighbourTLV(TLV): # This case should never happen (and in our protocol it is not possible), # but we include this test as a security measure. return - if not (str(self.ip_address), self.port) in squirrel.active_hazelnuts \ - and not (str(self.ip_address), self.port) in squirrel.potential_hazelnuts: + if not (str(self.ip_address), self.port) in squirrel.hazelnuts: hazelnut = squirrel.new_hazel(str(self.ip_address), self.port) hazelnut.potential = True squirrel.update_hazelnut_table(hazelnut) From 0a4ebd0c921b0a2f4e01d8921b42d128488d8836 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 20:38:19 +0100 Subject: [PATCH 12/19] An unknown client can only send HelloTLV and GoAwayTLV for security --- squinnondation/hazel.py | 6 +++--- squinnondation/messages.py | 40 +++++++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 24fd6d2..ed79fd6 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -51,7 +51,7 @@ class Hazelnut: return self.addresses[0] def __repr__(self): - return f"{self.id or ''}: {self.nickname or ''}, {self.addresses}" + return self.nickname or str(self.id) or str(self.main_address) def __str__(self): return repr(self) @@ -370,10 +370,10 @@ class Squirrel(Hazelnut): """ res = dict() hazels = self.active_hazelnuts - for key, hazel in hazels: + for hazel in hazels: if hazel.symmetric: next_send = uniform(1, 2) - res[key] = [hazel, time.time() + next_send, 0] + res[hazel.main_address] = [hazel, time.time() + next_send, 0] return res def remove_from_inundation(self, hazel: Hazelnut, sender_id: int, nonce: int) -> None: diff --git a/squinnondation/messages.py b/squinnondation/messages.py index 5145560..f8948c9 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -73,7 +73,12 @@ class Pad1TLV(TLV): return self.type.to_bytes(1, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: - # TODO Add some easter eggs + if not sender.active or not sender.id: + # It doesn't say hello, we don't listen to it + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + "You are not my neighbour, I don't listen to your Pad1TLV. Please say me Hello before."))) + return + squirrel.add_system_message("I received a Pad1TLV, how disapointing.") def __len__(self) -> int: @@ -118,7 +123,12 @@ class PadNTLV(TLV): + self.mbz[:self.length] def handle(self, squirrel: Any, sender: Any) -> None: - # TODO Add some easter eggs + if not sender.active or not sender.id: + # It doesn't say hello, we don't listen to it + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + "You are not my neighbour, I don't listen to your PadNTLV. Please say me Hello before."))) + return + squirrel.add_system_message(f"I received {self.length} zeros.") @staticmethod @@ -229,6 +239,12 @@ class NeighbourTLV(TLV): self.port.to_bytes(2, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: + if not sender.active or not sender.id: + # It doesn't say hello, we don't listen to it + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + "You are not my neighbour, I don't listen to your NeighbourTLV. Please say me Hello before."))) + return + if (self.ip_address, self.port) in squirrel.addresses: # This case should never happen (and in our protocol it is not possible), # but we include this test as a security measure. @@ -279,6 +295,12 @@ class DataTLV(TLV): """ A message has been sent. We log it. """ + if not sender.active or not sender.id: + # It doesn't say hello, we don't listen to it + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + "You are not my neighbour, I don't listen to your DataTLV. Please say me Hello before."))) + return + msg = self.data.decode('UTF-8') # Acknowledge the packet @@ -342,7 +364,13 @@ class AckTLV(TLV): """ When an AckTLV is received, we know that we do not have to inundate that neighbour anymore. """ - squirrel.add_system_message("I received an AckTLV") + if not sender.active or not sender.id: + # It doesn't say hello, we don't listen to it + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + "You are not my neighbour, I don't listen to your AckTLV. Please say me Hello before."))) + return + + squirrel.add_system_message(f"I received an AckTLV from {sender}") squirrel.remove_from_inundation(sender, self.sender_id, self.nonce) @staticmethod @@ -381,6 +409,12 @@ class GoAwayTLV(TLV): self.message.encode("UTF-8")[:self.length - 1] def handle(self, squirrel: Any, sender: Any) -> None: + if not sender.active or not sender.id: + # It doesn't say hello, we don't listen to it + squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( + "You are not my neighbour, I don't listen to your GoAwayTLV. Please say me Hello before."))) + return + if sender.active: sender.active = False squirrel.update_hazelnut_table(sender) From 5bb4748056823d8cba1076dc85a149bc369dd49c Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 20:54:14 +0100 Subject: [PATCH 13/19] Warn users when they send poop messages and ban them if they are too many --- squinnondation/hazel.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index ed79fd6..2a4a5ab 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -11,7 +11,7 @@ import re import socket import time -from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType, NeighbourTLV +from .messages import Packet, DataTLV, HelloTLV, GoAwayTLV, GoAwayType, NeighbourTLV, WarningTLV class Hazelnut: @@ -25,6 +25,7 @@ class Hazelnut: self.last_long_hello_time = 0 self.symmetric = False self.active = False + self.errors = 0 try: # Resolve DNS as an IPv6 @@ -48,8 +49,19 @@ class Hazelnut: @property def main_address(self) -> Tuple[str, int]: + """ + A client can have multiple addresses. + We contact it only on one of them. + """ return self.addresses[0] + @property + def banned(self) -> bool: + """ + If a client send more than 5 invalid packets, we don't trust it anymore. + """ + return self.errors >= 5 + def __repr__(self): return self.nickname or str(self.id) or str(self.main_address) @@ -165,7 +177,22 @@ class Squirrel(Hazelnut): Warning: the process is blocking, it should be ran inside a dedicated thread. """ data, addr = self.receive_raw_data() - return Packet.unmarshal(data), self.find_hazelnut(addr[0], addr[1]) + hazelnut = self.find_hazelnut(addr[0], addr[1]) + if hazelnut.banned: + # The client already sent errored packets + self.send_packet(hazelnut, Packet.construct(WarningTLV.construct( + "You got banned since you sent too much errored packets."))) + raise ValueError("Client is banned.") + try: + pkt = Packet.unmarshal(data) + except ValueError as error: + # The packet contains an error. We memorize it and warn the other user. + hazelnut.errors += 1 + self.send_packet(hazelnut, Packet.construct(WarningTLV.construct( + f"An error occured while reading your packet: {error}"))) + raise error + else: + return pkt, hazelnut def receive_raw_data(self) -> Tuple[bytes, Any]: """ From dc9f836932e8c5f21cb02f1a52bb2085a9d262c8 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 21:04:08 +0100 Subject: [PATCH 14/19] When merging clients, merge also error count --- squinnondation/hazel.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 2a4a5ab..4a1da53 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -36,8 +36,8 @@ class Hazelnut: # See https://fr.wikipedia.org/wiki/Adresse_IPv6_mappant_IPv4 address = "::ffff:" + socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] - self.addresses = list() - self.addresses.append((address, port)) + self.addresses = set() + self.addresses.add((address, port)) @property def potential(self) -> bool: @@ -53,7 +53,7 @@ class Hazelnut: A client can have multiple addresses. We contact it only on one of them. """ - return self.addresses[0] + return list(self.addresses)[0] @property def banned(self) -> bool: @@ -68,6 +68,19 @@ class Hazelnut: def __str__(self): return repr(self) + def merge(self, other: "Hazelnut") -> "Hazelnut": + """ + Merge the hazelnut data with one other. + The symmetric and active properties are kept from the original client. + """ + self.errors = max(self.errors, other.errors) + self.last_hello_time = max(self.last_hello_time, other.last_hello_time) + self.last_long_hello_time = max(self.last_hello_time, other.last_long_hello_time) + self.addresses.update(self.addresses) + self.addresses.update(other.addresses) + self.id = self.id if self.id > 0 else other.id + return self + class Squirrel(Hazelnut): """ @@ -686,19 +699,13 @@ class Squirrel(Hazelnut): if addr in self.hazelnuts: # Merge with the previous hazel old_hazel = self.hazelnuts[addr] - for other_addr in old_hazel.addresses: - if other_addr not in hazelnut.addresses: - hazelnut.addresses.append(other_addr) - self.hazelnuts[other_addr] = hazelnut + hazelnut.merge(old_hazel) self.hazelnuts[addr] = hazelnut for other_hazel in list(self.hazelnuts.values()): if other_hazel.id == hazelnut.id > 0 and other_hazel != hazelnut: # The hazelnut with the same id is known as a different address. We merge everything - for addr in other_hazel.addresses: - self.hazelnuts[addr] = hazelnut - if addr not in hazelnut.addresses: - hazelnut.addresses.append(addr) + hazelnut.merge(other_hazel) def send_neighbours(self) -> None: """ From edb738bffb555b20dc558ab0c8ddfcda7d5ad2f6 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 21:08:52 +0100 Subject: [PATCH 15/19] Don't log banned users too much --- squinnondation/hazel.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 4a1da53..958fa2d 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -193,9 +193,7 @@ class Squirrel(Hazelnut): hazelnut = self.find_hazelnut(addr[0], addr[1]) if hazelnut.banned: # The client already sent errored packets - self.send_packet(hazelnut, Packet.construct(WarningTLV.construct( - "You got banned since you sent too much errored packets."))) - raise ValueError("Client is banned.") + return Packet.construct(), hazelnut try: pkt = Packet.unmarshal(data) except ValueError as error: @@ -203,6 +201,10 @@ class Squirrel(Hazelnut): hazelnut.errors += 1 self.send_packet(hazelnut, Packet.construct(WarningTLV.construct( f"An error occured while reading your packet: {error}"))) + if hazelnut.banned: + self.send_packet(hazelnut, Packet.construct(WarningTLV.construct( + "You got banned since you sent too much errored packets."))) + raise ValueError("Client is banned since there were too many errors.", error) raise error else: return pkt, hazelnut @@ -761,7 +763,13 @@ class Worm(Thread): pkt, hazelnut = self.squirrel.receive_packet() except ValueError as error: self.squirrel.add_system_message("An error occurred while receiving a packet: {}".format(error)) + self.squirrel.refresh_history() + self.squirrel.refresh_input() else: + if hazelnut.banned: + # Ignore banned hazelnuts + continue + for tlv in pkt.body: tlv.handle(self.squirrel, hazelnut) self.squirrel.refresh_history() From 1db72e71dc13dca266ae1dff6965e4e3279e78aa Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 21:17:44 +0100 Subject: [PATCH 16/19] Banned clients are not potential clients --- squinnondation/hazel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 958fa2d..0c5ff2f 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -41,7 +41,7 @@ class Hazelnut: @property def potential(self) -> bool: - return not self.active + return not self.active and not self.banned @potential.setter def potential(self, value: bool) -> None: From eb0098552ab61a6a81b922ed6318cee23c4c9193 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 21:50:16 +0100 Subject: [PATCH 17/19] Banned clients are not potential clients --- squinnondation/messages.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index f8948c9..fad4f57 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -73,7 +73,7 @@ class Pad1TLV(TLV): return self.type.to_bytes(1, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: - if not sender.active or not sender.id: + if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( "You are not my neighbour, I don't listen to your Pad1TLV. Please say me Hello before."))) @@ -123,7 +123,7 @@ class PadNTLV(TLV): + self.mbz[:self.length] def handle(self, squirrel: Any, sender: Any) -> None: - if not sender.active or not sender.id: + if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( "You are not my neighbour, I don't listen to your PadNTLV. Please say me Hello before."))) @@ -239,7 +239,7 @@ class NeighbourTLV(TLV): self.port.to_bytes(2, sys.byteorder) def handle(self, squirrel: Any, sender: Any) -> None: - if not sender.active or not sender.id: + if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( "You are not my neighbour, I don't listen to your NeighbourTLV. Please say me Hello before."))) @@ -295,7 +295,7 @@ class DataTLV(TLV): """ A message has been sent. We log it. """ - if not sender.active or not sender.id: + if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( "You are not my neighbour, I don't listen to your DataTLV. Please say me Hello before."))) @@ -364,7 +364,7 @@ class AckTLV(TLV): """ When an AckTLV is received, we know that we do not have to inundate that neighbour anymore. """ - if not sender.active or not sender.id: + if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( "You are not my neighbour, I don't listen to your AckTLV. Please say me Hello before."))) @@ -409,7 +409,7 @@ class GoAwayTLV(TLV): self.message.encode("UTF-8")[:self.length - 1] def handle(self, squirrel: Any, sender: Any) -> None: - if not sender.active or not sender.id: + if not sender.active or not sender.symmetric or not sender.id: # It doesn't say hello, we don't listen to it squirrel.send_packet(sender, Packet.construct(WarningTLV.construct( "You are not my neighbour, I don't listen to your GoAwayTLV. Please say me Hello before."))) From 084f512d345bb3649d18676da2200ec8297d7ad0 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 22:01:49 +0100 Subject: [PATCH 18/19] Check that packet length is good --- squinnondation/messages.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/squinnondation/messages.py b/squinnondation/messages.py index fad4f57..bf00e23 100644 --- a/squinnondation/messages.py +++ b/squinnondation/messages.py @@ -492,14 +492,21 @@ class Packet: pkt.magic = data[0] pkt.version = data[1] pkt.body_length = socket.ntohs(int.from_bytes(data[2:4], sys.byteorder)) + if len(data) != 4 + pkt.body_length: + raise ValueError(f"Invalid packet length: " + f"declared body length is {pkt.body_length} while {len(data) - 4} bytes are avalaible") pkt.body = [] read_bytes = 0 while read_bytes < min(len(data) - 4, pkt.body_length): tlv_type = data[4 + read_bytes] if not (0 <= tlv_type < len(TLV.tlv_classes())): raise ValueError(f"TLV type is not supported: {tlv_type}") + tlv_length = data[4 + read_bytes + 1] if tlv_type > 0 else -1 + if 2 + tlv_length > pkt.body_length - read_bytes: + raise ValueError(f"TLV length is too long: requesting {tlv_length} bytes, " + f"remaining {pkt.body_length - read_bytes}") tlv = TLV.tlv_classes()[tlv_type]() - tlv.unmarshal(data[4 + read_bytes:4 + read_bytes + pkt.body_length]) + tlv.unmarshal(data[4 + read_bytes:4 + read_bytes + 2 + tlv_length]) pkt.body.append(tlv) read_bytes += len(tlv) From acd9b157b9425cf63e87ac377aad38d071b0f3f3 Mon Sep 17 00:00:00 2001 From: Yohann D'ANELLO Date: Tue, 5 Jan 2021 22:36:09 +0100 Subject: [PATCH 19/19] Add docstring for the update table function --- squinnondation/hazel.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/squinnondation/hazel.py b/squinnondation/hazel.py index 0c5ff2f..59f03d2 100644 --- a/squinnondation/hazel.py +++ b/squinnondation/hazel.py @@ -697,6 +697,10 @@ class Squirrel(Hazelnut): self.update_hazelnut_table(hazelnut) def update_hazelnut_table(self, hazelnut: Hazelnut) -> None: + """ + We insert the hazelnut into our table of clients. + If there is a collision with the address / the ID, then we merge clients into a unique one. + """ for addr in hazelnut.addresses: if addr in self.hazelnuts: # Merge with the previous hazel