diff --git a/.vscode/launch.json b/.vscode/launch.json index cdc4bdf..f6e6e22 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,6 +20,14 @@ "justMyCode": true, "args": ["--info"] }, + { + "name": "meshtastic tunnel", + "type": "python", + "request": "launch", + "module": "meshtastic", + "justMyCode": true, + "args": ["--tunnel", "--debug"] + }, { "name": "meshtastic debug", "type": "python", diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 135cd94..2e61290 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1,7 +1,7 @@ #!python3 import argparse -from . import SerialInterface, TCPInterface, BLEInterface, test, remote_hardware +from . import SerialInterface, TCPInterface, BLEInterface, test, remote_hardware, tunnel import logging import sys from pubsub import pub @@ -274,6 +274,11 @@ def onConnected(interface): print(f"Channel URL {interface.channelURL}") url = pyqrcode.create(interface.channelURL) print(url.terminal()) + + if args.tunnel: + closeNow = False # Even if others said we could close, stay open if the user asked for a tunnel + tunnel.Tunnel(interface) + except Exception as ex: print(ex) @@ -394,6 +399,9 @@ def main(): parser.add_argument('--unset-router', dest='router', action='store_false', help="Turns off router mode") + parser.add_argument('--tunnel', + action='store_true', help="Create a TUN tunnel device for forwarding IP packets over the mesh") + parser.set_defaults(router=None) parser.add_argument('--version', action='version', version=f"{pkg_resources.require('meshtastic')[0].version}") @@ -411,7 +419,7 @@ def main(): if args.info or args.set or args.seturl or args.setowner or args.setlat or args.setlon or \ args.settime or \ args.setch_longslow or args.setch_shortfast or args.setstr or args.setchan or args.sendtext or \ - args.router != None or args.qr: + args.tunnel or args.router != None or args.qr: args.seriallog = "none" # assume no debug output in this case else: args.seriallog = "stdout" # default to stdout diff --git a/meshtastic/tunnel.py b/meshtastic/tunnel.py new file mode 100644 index 0000000..ee063cf --- /dev/null +++ b/meshtastic/tunnel.py @@ -0,0 +1,124 @@ +# code for IP tunnel over a mesh +# Note python-pytuntap was too buggy +# using pip3 install pytap2 +# make sure to "sudo setcap cap_net_admin+eip /usr/bin/python3.8" so python can access tun device without being root +# sudo ip tuntap del mode tun tun0 + +# FIXME: set MTU correctly +# select local ip address based on nodeid +# print known node ids as IP addresses +# change dev name to mesh + +from . import portnums_pb2 +from pubsub import pub +from pytap2 import TapDevice +import logging +import threading + +"""A list of chatty UDP services we should never accidentally +forward to our slow network""" +udpBlacklist = { + 1900, # SSDP + 5353, # multicast DNS +} + +"""A list of TCP services to block""" +tcpBlacklist = {} + +"""A list of protocols we ignore""" +protocolBlacklist = { + 0x02, # IGMP + 0x80, # Service-Specific Connection-Oriented Protocol in a Multilink and Connectionless Environment +} + +def hexstr(barray): + """Print a string of hex digits""" + return ":".join('{:02x}'.format(x) for x in barray) + +def ipstr(barray): + """Print a string of ip digits""" + return ".".join('{}'.format(x) for x in barray) + +def readnet_u16(p, offset): + """Read big endian u16 (network byte order)""" + return p[offset] * 256 + p[offset + 1] + + + +def onTunnelReceive(packet, interface): + """Callback for received tunneled messages from mesh + + FIXME figure out how to do closures with methods in python""" + p = packet["decoded"]["data"]["payload"] + logging.debug(f"Received tunnel message") + + +class Tunnel: + """A TUN based IP tunnel over meshtastic""" + + def __init__(self, iface): + """ + Constructor + + iface is the already open MeshInterface instance + """ + self.iface = iface + + logging.info("Starting IP to mesh tunnel (you must be root for this pre-alpha feature to work)") + + pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP") + + logging.debug("creating TUN device") + self.tun = TapDevice(mtu=200) + # tun.create() + self.tun.up() + self.tun.ifconfig(address="10.115.1.2",netmask="255.255.0.0") + logging.debug("starting TUN reader") + self._rxThread = threading.Thread(target=self.__tunReader, args=(), daemon=True) + self._rxThread.start() + + def __tunReader(self): + tap = self.tun + logging.debug("TUN reader running") + while True: + p = tap.read() + + protocol = p[8 + 1] + srcaddr = p[12:16] + destaddr = p[16:20] + subheader = 20 + ignore = False # Assume we will be forwarding the packet + if protocol in protocolBlacklist: + ignore = True + logging.debug(f"Ignoring blacklisted protocol 0x{protocol:02x}") + elif protocol == 0x01: # ICMP + logging.debug("forwarding ICMP message") + # reply to pings (swap src and dest but keep rest of packet unchanged) + #pingback = p[:12]+p[16:20]+p[12:16]+p[20:] + #tap.write(pingback) + elif protocol == 0x11: # UDP + srcport = readnet_u16(p, subheader) + destport = readnet_u16(p, subheader + 2) + logging.debug(f"udp srcport={srcport}, destport={destport}") + if destport in udpBlacklist: + ignore = True + logging.debug(f"ignoring blacklisted UDP port {destport}") + elif protocol == 0x06: # TCP + srcport = readnet_u16(p, subheader) + destport = readnet_u16(p, subheader + 2) + logging.debug(f"tcp srcport={srcport}, destport={destport}") + if destport in tcpBlacklist: + ignore = True + logging.debug(f"ignoring blacklisted TCP port {destport}") + else: + logging.warning(f"unexpected protocol 0x{protocol:02x}, src={ipstr(srcaddr)}, dest={ipstr(destaddr)}") + + if not ignore: + logging.debug(f"Forwarding packet bytelen={len(p)} src={ipstr(srcaddr)}, dest={ipstr(destaddr)}") + + def close(self): + self.tun.close() + + + + diff --git a/tests/tuntest.py b/tests/tuntest.py index 761cee5..c8cd301 100644 --- a/tests/tuntest.py +++ b/tests/tuntest.py @@ -1,6 +1,7 @@ # delete me eventually # Note python-pytuntap was too buggy # using pip3 install pytap2 +# make sure to "sudo setcap cap_net_admin+eip /usr/bin/python3.8" so python can access tun device without being root # sudo ip tuntap del mode tun tun0 # FIXME: set MTU correctly @@ -28,8 +29,13 @@ protocolBlacklist = { } def hexstr(barray): + """Print a string of hex digits""" return ":".join('{:02x}'.format(x) for x in barray) +def ipstr(barray): + """Print a string of ip digits""" + return ".".join('{}'.format(x) for x in barray) + def readnet_u16(p, offset): """Read big endian u16 (network byte order)""" return p[offset] * 256 + p[offset + 1] @@ -66,10 +72,10 @@ def readtest(tap): ignore = True logging.debug(f"ignoring blacklisted TCP port {destport}") else: - logging.warning(f"unexpected protocol 0x{protocol:02x}, srcadddr {hexstr(srcaddr)}") + logging.warning(f"unexpected protocol 0x{protocol:02x}, src={ipstr(srcaddr)}, dest={ipstr(destaddr)}") if not ignore: - logging.debug(f"Forwarding packet bytes={hexstr(p)}") + logging.debug(f"Forwarding packet bytelen={len(p)} src={ipstr(srcaddr)}, dest={ipstr(destaddr)}")