From 78602eabd69aebfa061cf201a4d44555fa9a36e7 Mon Sep 17 00:00:00 2001 From: osmarks Date: Mon, 14 Jun 2021 22:27:54 +0100 Subject: [PATCH] reorganize LAN chat stuff --- bcc.py => lan-chat/bcc.py | 0 lan-chat/bccplus.py | 221 ++++++++++++++++++++++++++++++++++++++ mcc.py => lan-chat/mcc.py | 0 3 files changed, 221 insertions(+) rename bcc.py => lan-chat/bcc.py (100%) create mode 100644 lan-chat/bccplus.py rename mcc.py => lan-chat/mcc.py (100%) diff --git a/bcc.py b/lan-chat/bcc.py similarity index 100% rename from bcc.py rename to lan-chat/bcc.py diff --git a/lan-chat/bccplus.py b/lan-chat/bccplus.py new file mode 100644 index 0000000..fd543b6 --- /dev/null +++ b/lan-chat/bccplus.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 + +import socket +import threading +import time +import sys +import subprocess +import os +import collections +import getpass +import re +import random +import ipaddress +import itertools +import struct + +PORT = 44718 +IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6") if "IPPROTO_IPV6" in dir(socket) else 41 # workaround for weird Windows/old Python quirk + +maddr = ("ff15::aeae", 44718) + +def configure_multicast(maddr, ifn): + mip, port = maddr + haddr = socket.getaddrinfo("::", port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1] + maddr = socket.getaddrinfo(mip, port, socket.AF_INET6, socket.SOCK_DGRAM)[0][-1] + + sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if hasattr(socket, "SO_REUSEPORT"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_LOOP, 1) + sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 5) + + ifn = struct.pack("I", ifn) + sock.setsockopt(IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, ifn) + + group = socket.inet_pton(socket.AF_INET6, mip) + ifn + sock.setsockopt(IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, group) + + sock.bind(haddr) + + return sock, maddr + +def chunks(l, n): + n = max(1, n) + return [l[i:i+n] for i in range(0, len(l), n)] + +def recv(q, iface): + s, _ = configure_multicast(maddr, iface) + + while True: + data, peer = s.recvfrom(2048) + peer = peer[0].split("%")[0], peer[1] + q.put((data, peer)) + +def normalize_ip(ip): + return str(ipaddress.ip_address(ip)) + +def shorthex(x): return "{:04x}".format(x) +def encode_packet(ty, nick, content): + return struct.pack("!BH16s", ty, local_id, nick.encode("utf-8")) + content.encode("utf-8") +def decode_packet(pkt): + ty, local_id, nick = struct.unpack("!BH16s", pkt[:19]) + content = pkt[19:] + return ty, local_id, nick.rstrip(b"\0").decode("utf-8"), content.decode("utf-8") + +MTY_PING = 0 +MTY_MESSAGE = 1 + +if __name__ == "__main__": + mynick = getpass.getuser() + "@" + socket.gethostname() + peers = {} + local_id = random.randint(0, 0xFFFF) + + # dark horrors, TODO refactor into more consistent interface (no pun intended) + if sys.platform.startswith("win32"): + out = subprocess.check_output(["ipconfig"]) + match = re.search(b"\n +Link-local IPv6 Address[ .]: ([a-f0-9:])%([0-9]+)", out) + own_ips = {match.group(1)} + iface = int(match.group(2)) + for match in re.findall(b"\n +IPv6 Address[ .]: ([a-f0-9:])", out): + own_ips.add(match.group(1)) + else: + try: + raise PermissionError + addrs = collections.defaultdict(set) + for line in open("/proc/net/if_inet6").readlines(): + addr, ifnum, _, _, _, ifname = line.split() + ifnum = int(ifnum, 16) + addr = normalize_ip(":".join(chunks(addr, 4))) + addrs[ifnum].add(addr) + if ("wlan" in ifname or ifname.startswith("en") or ifname.startswith("eth")) and addr.startswith("fe80"): + iface = ifnum + if not iface: raise SystemExit("No suitable interface found, suffer") + own_ips = addrs[iface] + except PermissionError: + out = subprocess.check_output(["ip", "addr", "show"]).decode("ascii") + addrs = collections.defaultdict(set) + for line in out.split("\n"): + match = re.match("([0-9]+): ([A-Za-z0-9-_]+):", line) + if match: + num, ifname = int(match.group(1)), match.group(2) + current_if = num + if "wlan" in ifname or ifname.startswith("en") or ifname.startswith("eth"): + iface = num + match = re.match(" +inet6 ([a-z0-9:]+)/", line) + if match: + addrs[current_if].add(match.group(1)) + own_ips = addrs[iface] + + print("IP:", own_ips, "Iface:", ifname or iface, "LocID:", shorthex(local_id)) + + proc = None + if sys.platform.startswith("win32"): + from multiprocessing import Process, Queue + packet_queue = Queue() + proc = Process(target=recv, args=(packet_queue, iface)) + proc.start() + else: + import queue + packet_queue = queue.Queue() + thread = threading.Thread(target=recv, args=(packet_queue, iface)).start() + + def queuereader(): + while True: + data, (remote_addr, _) = packet_queue.get() + try: + ty, remote_local_id, nick, content = decode_packet(data) + if remote_addr in own_ips and remote_local_id == local_id: continue + peer_id = remote_addr + "/" + shorthex(remote_local_id) + try: + peer = peers[peer_id] + peer["ping_countdown"] = 5 + if nick != peer["nick"]: + print("! %s (%s) is now %s" % (peer["nick"], peer_id, nick)) + peer["nick"] = nick + except KeyError: + print("! %s (%s) now exists" % (nick, peer_id)) + peers[peer_id] = { "nick": nick, "ping_countdown": 5 } + if ty == MTY_MESSAGE: + print(nick + ": " + content) + except Exception as e: + print("Parse error", e) + + s, dest = configure_multicast(maddr, iface) + + def pinger(): + while True: + s.sendto(encode_packet(MTY_PING, mynick, ""), dest) + for id, peer in list(peers.items()): + peer["ping_countdown"] -= 1 + if peer["ping_countdown"] <= 0: + del peers[id] + print("! %s (%s) no longer exists" % (peer["nick"], id)) + time.sleep(1) + + threading.Thread(target=queuereader).start() + threading.Thread(target=pinger).start() + + try: + while True: + msg = input("> ") + if msg.startswith("/nick "): + newnick = msg[6:] + if len(newnick.encode("utf-8")) > 16: + print("! Max nick length is 16 bytes") + else: + print("! You are now", newnick) + mynick = newnick + elif msg.startswith("/peer"): + p = ["%s (%s)" % (i, p["nick"]) for i, p in peers.items()] + print("! Peers:", " ".join(p)) + else: + s.sendto(encode_packet(MTY_MESSAGE, mynick, msg), dest) + time.sleep(0.05) + except KeyboardInterrupt: + if proc: proc.terminate() + os._exit(0) + +# in case of things +""" +AF_INET AddressFamily.AF_INET +AF_INET6 AddressFamily.AF_INET6 +IPPROTO_IPV6 41 +IPV6_CHECKSUM 7 +IPV6_DONTFRAG 62 +IPV6_DSTOPTS 59 +IPV6_HOPLIMIT 52 +IPV6_HOPOPTS 54 +IPV6_JOIN_GROUP 20 +IPV6_LEAVE_GROUP 21 +IPV6_MULTICAST_HOPS 18 +IPV6_MULTICAST_IF 17 +IPV6_MULTICAST_LOOP 19 +IPV6_NEXTHOP 9 +IPV6_PATHMTU 61 +IPV6_PKTINFO 50 +IPV6_RECVDSTOPTS 58 +IPV6_RECVHOPLIMIT 51 +IPV6_RECVHOPOPTS 53 +IPV6_RECVPATHMTU 60 +IPV6_RECVPKTINFO 49 +IPV6_RECVRTHDR 56 +IPV6_RECVTCLASS 66 +IPV6_RTHDR 57 +IPV6_RTHDRDSTOPTS 55 +IPV6_RTHDR_TYPE_0 0 +IPV6_TCLASS 67 +IPV6_UNICAST_HOPS 16 +IPV6_V6ONLY 26 +SOL_ALG 279 +SOL_HCI 0 +SOL_IP 0 +SOL_RDS 276 +SOL_SOCKET 1 +SOL_TCP 6 +SOL_TIPC 271 +SOL_UDP 17 +""" \ No newline at end of file diff --git a/mcc.py b/lan-chat/mcc.py similarity index 100% rename from mcc.py rename to lan-chat/mcc.py