mirror of
https://github.com/osmarks/random-stuff
synced 2024-12-28 02:50:33 +00:00
221 lines
7.1 KiB
Python
221 lines
7.1 KiB
Python
|
#!/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
|
||
|
"""
|