From a0038107deb4e2fa47aadcf21c0bfcb9c46e0cc3 Mon Sep 17 00:00:00 2001
From: Kevin Hester
Date: Thu, 24 Dec 2020 11:19:47 +0800
Subject: [PATCH] 1.1.26
---
docs/meshtastic/index.html | 45 ++-
docs/meshtastic/mesh_pb2.html | 232 +++++++++----
docs/meshtastic/portnums_pb2.html | 10 +-
docs/meshtastic/tunnel.html | 553 ++++++++++++++++++++++++++++++
setup.py | 2 +-
5 files changed, 750 insertions(+), 92 deletions(-)
create mode 100644 docs/meshtastic/tunnel.html
diff --git a/docs/meshtastic/index.html b/docs/meshtastic/index.html
index 8f6c1b2..362ceca 100644
--- a/docs/meshtastic/index.html
+++ b/docs/meshtastic/index.html
@@ -34,7 +34,7 @@ the device.
Includes always up-to-date location and username information for each
node in the mesh.
This is a read-only datastructure.
-
myNodeInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
+
myInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
Published PubSub topics
We use a publish-subscribe model to communicate asynchronous events.
@@ -97,7 +97,7 @@ properties of SerialInterface:
the device.
- nodes - The database of received nodes. Includes always up-to-date location and username information for each
node in the mesh. This is a read-only datastructure.
-- myNodeInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
+- myInfo - Contains read-only information about the local radio device (software version, hardware version, etc)
# Published PubSub topics
@@ -322,7 +322,7 @@ class MeshInterface:
self._sendToRadio(t)
logging.debug("Wrote config")
- def getMyNode(self):
+ def getMyUser(self):
if self.myInfo is None:
return None
myId = self.myInfo.my_node_num
@@ -333,13 +333,13 @@ class MeshInterface:
return None
def getLongName(self):
- user = self.getMyNode()
+ user = self.getMyUser()
if user is not None:
return user.get('longName', None)
return None
def getShortName(self):
- user = self.getMyNode()
+ user = self.getMyUser()
if user is not None:
return user.get('shortName', None)
return None
@@ -600,7 +600,7 @@ class MeshInterface:
n = self._getOrCreateByNum(asDict["from"])
n["user"] = u
# We now have a node ID, make sure it is uptodate in that table
- self.nodes[u["id"]] = u
+ self.nodes[u["id"]] = n
logging.debug(f"Publishing topic {topic}")
pub.sendMessage(topic, packet=asDict, interface=self)
@@ -817,7 +817,9 @@ class SerialInterface(StreamInterface):
# control and will always drive RTS either high or low (rather than letting the CP102 leave
# it as an open-collector floating pin). Since it is going to drive it anyways we want to make
# sure it is driven low, so that the TBEAM won't reset
- self.stream.rts = False
+ # Linux does this properly, so don't apply this hack (because it makes the reset button not work)
+ if platform.system() != 'Linux':
+ self.stream.rts = False
self.stream.open()
StreamInterface.__init__(
@@ -896,6 +898,10 @@ class TCPInterface(StreamInterface):
@@ -1169,7 +1175,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d
self._sendToRadio(t)
logging.debug("Wrote config")
- def getMyNode(self):
+ def getMyUser(self):
if self.myInfo is None:
return None
myId = self.myInfo.my_node_num
@@ -1180,13 +1186,13 @@ noProto – If True, don't try to run our protocol on the link - just be a d
return None
def getLongName(self):
- user = self.getMyNode()
+ user = self.getMyUser()
if user is not None:
return user.get('longName', None)
return None
def getShortName(self):
- user = self.getMyNode()
+ user = self.getMyUser()
if user is not None:
return user.get('shortName', None)
return None
@@ -1447,7 +1453,7 @@ noProto – If True, don't try to run our protocol on the link - just be a d
n = self._getOrCreateByNum(asDict["from"])
n["user"] = u
# We now have a node ID, make sure it is uptodate in that table
- self.nodes[u["id"]] = u
+ self.nodes[u["id"]] = n
logging.debug(f"Publishing topic {topic}")
pub.sendMessage(topic, packet=asDict, interface=self)
@@ -1488,14 +1494,14 @@ def channelURL(self):
Expand source code
def getLongName(self):
- user = self.getMyNode()
+ user = self.getMyUser()
if user is not None:
return user.get('longName', None)
return None
def getMyUser(self):
if self.myInfo is None:
return None
myId = self.myInfo.my_node_num
@@ -1524,7 +1530,7 @@ def channelURL(self):
Expand source code
def getShortName(self):
- user = self.getMyNode()
+ user = self.getMyUser()
if user is not None:
return user.get('shortName', None)
return None
@@ -1836,7 +1842,9 @@ debugOut {stream} – If a stream is provided, any debug serial output from
# control and will always drive RTS either high or low (rather than letting the CP102 leave
# it as an open-collector floating pin). Since it is going to drive it anyways we want to make
# sure it is driven low, so that the TBEAM won't reset
- self.stream.rts = False
+ # Linux does this properly, so don't apply this hack (because it makes the reset button not work)
+ if platform.system() != 'Linux':
+ self.stream.rts = False
self.stream.open()
StreamInterface.__init__(
@@ -2195,6 +2203,7 @@ hostname {string} – Hostname/IP address of the device to connect to
def getter(self):
+ field_value = self._fields.get(field)
+ if field_value is None:
+ # Construct a new object to represent this field.
+ field_value = field._default_constructor(self)
+
+ # Atomically check if another thread has preempted us and, if not, swap
+ # in the new object we just created. If someone has preempted us, we
+ # take that object and discard ours.
+ # WARNING: We are relying on setdefault() being atomic. This is true
+ # in CPython but we haven't investigated others. This warning appears
+ # in several other locations in this file.
+ field_value = self._fields.setdefault(field, field_value)
+ return field_value
def getter(self):
+ field_value = self._fields.get(field)
+ if field_value is None:
+ # Construct a new object to represent this field.
+ field_value = field._default_constructor(self)
+
+ # Atomically check if another thread has preempted us and, if not, swap
+ # in the new object we just created. If someone has preempted us, we
+ # take that object and discard ours.
+ # WARNING: We are relying on setdefault() being atomic. This is true
+ # in CPython but we haven't investigated others. This warning appears
+ # in several other locations in this file.
+ field_value = self._fields.setdefault(field, field_value)
+ return field_value
+
+
Methods
@@ -4941,6 +5014,10 @@ shown below.
Class variables
+
var CHANNEL_INDEX_FIELD_NUMBER
+
+
+
var DECODED_FIELD_NUMBER
@@ -5019,6 +5096,19 @@ shown below.
Instance variables
+
var channel_index
+
+
Getter for channel_index.
+
+
+Expand source code
+
+
def getter(self):
+ # TODO(protobuf-team): This may be broken since there may not be
+ # default_value. Combine with has_default_value somehow.
+ return self._fields.get(field, default_value)
+
+
var decoded
Getter for decoded.
@@ -10868,6 +10958,7 @@ and propagates this to our listener iff this was a state change.
# 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
+# sudo bin/run.sh --port /dev/ttyUSB0 --setch-shortfast
+# sudo bin/run.sh --port /dev/ttyUSB0 --tunnel --debug
+# ssh -Y root@192.168.10.151 (or dietpi), default password p
+# ncat -e /bin/cat -k -u -l 1235
+# ncat -u 10.115.64.152 1235
+# ping -c 1 -W 20 10.115.64.152
+
+# FIXME: use a more optimal MTU
+
+from . import portnums_pb2
+from pubsub import pub
+from pytap2 import TapDevice
+import logging
+import threading
+
+# A new non standard log level that is lower level than DEBUG
+LOG_TRACE = 5
+
+# fixme - find a way to move onTunnelReceive inside of the class
+tunnelInstance = None
+
+"""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"""
+ tunnelInstance.onReceive(packet)
+
+class Tunnel:
+ """A TUN based IP tunnel over meshtastic"""
+
+ def __init__(self, iface, subnet=None, netmask="255.255.0.0"):
+ """
+ Constructor
+
+ iface is the already open MeshInterface instance
+ subnet is used to construct our network number (normally 10.115.x.x)
+ """
+
+ if subnet is None:
+ subnet = "10.115"
+
+ self.iface = iface
+ self.subnetPrefix = subnet
+
+ global tunnelInstance
+ tunnelInstance = self
+
+ logging.info("Starting IP to mesh tunnel (you must be root for this *pre-alpha* feature to work). Mesh members:")
+
+ pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP")
+ myAddr = self._nodeNumToIp(self.iface.myInfo.my_node_num)
+
+ for node in self.iface.nodes.values():
+ nodeId = node["user"]["id"]
+ ip = self._nodeNumToIp(node["num"])
+ logging.info(f"Node { nodeId } has IP address { ip }")
+
+ logging.debug("creating TUN device")
+ # FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data
+ self.tun = TapDevice(name="mesh", mtu=200)
+ self.tun.up()
+ self.tun.ifconfig(address=myAddr,netmask=netmask)
+ logging.debug(f"starting TUN reader, our IP address is {myAddr}")
+ self._rxThread = threading.Thread(target=self.__tunReader, args=(), daemon=True)
+ self._rxThread.start()
+
+ def onReceive(self, packet):
+ p = packet["decoded"]["data"]["payload"]
+ if packet["from"] == self.iface.myInfo.my_node_num:
+ logging.debug("Ignoring message we sent")
+ else:
+ logging.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
+ # we don't really need to check for filtering here (sender should have checked), but this provides
+ # useful debug printing on types of packets received
+ if not self._shouldFilterPacket(p):
+ self.tun.write(p)
+
+ def _shouldFilterPacket(self, p):
+ """Given a packet, decode it and return true if it should be ignored"""
+ 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.log(LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
+ elif protocol == 0x01: # ICMP
+ icmpType = p[20]
+ icmpCode = p[21]
+ checksum = p[22:24]
+ logging.debug(f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}")
+ # 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)
+ if destport in udpBlacklist:
+ ignore = True
+ logging.log(LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
+ else:
+ logging.debug(f"forwarding udp srcport={srcport}, destport={destport}")
+ elif protocol == 0x06: # TCP
+ srcport = readnet_u16(p, subheader)
+ destport = readnet_u16(p, subheader + 2)
+ if destport in tcpBlacklist:
+ ignore = True
+ logging.log(LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
+ else:
+ logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
+ else:
+ logging.warning(f"forwarding unexpected protocol 0x{protocol:02x}, src={ipstr(srcaddr)}, dest={ipstr(destAddr)}")
+
+ return ignore
+
+ def __tunReader(self):
+ tap = self.tun
+ logging.debug("TUN reader running")
+ while True:
+ p = tap.read()
+ #logging.debug(f"IP packet received on TUN interface, type={type(p)}")
+ destAddr = p[16:20]
+
+ if not self._shouldFilterPacket(p):
+ self.sendPacket(destAddr, p)
+
+ def _ipToNodeId(self, ipAddr):
+ # We only consider the last 16 bits of the nodenum for IP address matching
+ ipBits = ipAddr[2] * 256 + ipAddr[3]
+
+ if ipBits == 0xffff:
+ return "^all"
+
+ for node in self.iface.nodes.values():
+ nodeNum = node["num"] & 0xffff
+ # logging.debug(f"Considering nodenum 0x{nodeNum:x} for ipBits 0x{ipBits:x}")
+ if (nodeNum) == ipBits:
+ return node["user"]["id"]
+ return None
+
+ def _nodeNumToIp(self, nodeNum):
+ return f"{self.subnetPrefix}.{(nodeNum >> 8) & 0xff}.{nodeNum & 0xff}"
+
+ def sendPacket(self, destAddr, p):
+ """Forward the provided IP packet into the mesh"""
+ nodeId = self._ipToNodeId(destAddr)
+ if nodeId is not None:
+ logging.debug(f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}")
+ self.iface.sendData(p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck = False)
+ else:
+ logging.warning(f"Dropping packet because no node found for destIP={ipstr(destAddr)}")
+
+ def close(self):
+ self.tun.close()
+
+
+
+
+
+
Global variables
+
+
var tcpBlacklist
+
+
A list of protocols we ignore
+
+
var tunnelInstance
+
+
A list of chatty UDP services we should never accidentally
+forward to our slow network
+
+
var udpBlacklist
+
+
A list of TCP services to block
+
+
+
+
+
Functions
+
+
+def hexstr(barray)
+
+
+
Print a string of hex digits
+
+
+Expand source code
+
+
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
+
+
+Expand source code
+
+
def ipstr(barray):
+ """Print a string of ip digits"""
+ return ".".join('{}'.format(x) for x in barray)
+
+
+
+def onTunnelReceive(packet, interface)
+
+
+
Callback for received tunneled messages from mesh
+
FIXME figure out how to do closures with methods in python
+
+
+Expand source code
+
+
def onTunnelReceive(packet, interface):
+ """Callback for received tunneled messages from mesh
+
+ FIXME figure out how to do closures with methods in python"""
+ tunnelInstance.onReceive(packet)
iface is the already open MeshInterface instance
+subnet is used to construct our network number (normally 10.115.x.x)
+
+
+Expand source code
+
+
class Tunnel:
+ """A TUN based IP tunnel over meshtastic"""
+
+ def __init__(self, iface, subnet=None, netmask="255.255.0.0"):
+ """
+ Constructor
+
+ iface is the already open MeshInterface instance
+ subnet is used to construct our network number (normally 10.115.x.x)
+ """
+
+ if subnet is None:
+ subnet = "10.115"
+
+ self.iface = iface
+ self.subnetPrefix = subnet
+
+ global tunnelInstance
+ tunnelInstance = self
+
+ logging.info("Starting IP to mesh tunnel (you must be root for this *pre-alpha* feature to work). Mesh members:")
+
+ pub.subscribe(onTunnelReceive, "meshtastic.receive.data.IP_TUNNEL_APP")
+ myAddr = self._nodeNumToIp(self.iface.myInfo.my_node_num)
+
+ for node in self.iface.nodes.values():
+ nodeId = node["user"]["id"]
+ ip = self._nodeNumToIp(node["num"])
+ logging.info(f"Node { nodeId } has IP address { ip }")
+
+ logging.debug("creating TUN device")
+ # FIXME - figure out real max MTU, it should be 240 - the overhead bytes for SubPacket and Data
+ self.tun = TapDevice(name="mesh", mtu=200)
+ self.tun.up()
+ self.tun.ifconfig(address=myAddr,netmask=netmask)
+ logging.debug(f"starting TUN reader, our IP address is {myAddr}")
+ self._rxThread = threading.Thread(target=self.__tunReader, args=(), daemon=True)
+ self._rxThread.start()
+
+ def onReceive(self, packet):
+ p = packet["decoded"]["data"]["payload"]
+ if packet["from"] == self.iface.myInfo.my_node_num:
+ logging.debug("Ignoring message we sent")
+ else:
+ logging.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
+ # we don't really need to check for filtering here (sender should have checked), but this provides
+ # useful debug printing on types of packets received
+ if not self._shouldFilterPacket(p):
+ self.tun.write(p)
+
+ def _shouldFilterPacket(self, p):
+ """Given a packet, decode it and return true if it should be ignored"""
+ 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.log(LOG_TRACE, f"Ignoring blacklisted protocol 0x{protocol:02x}")
+ elif protocol == 0x01: # ICMP
+ icmpType = p[20]
+ icmpCode = p[21]
+ checksum = p[22:24]
+ logging.debug(f"forwarding ICMP message src={ipstr(srcaddr)}, dest={ipstr(destAddr)}, type={icmpType}, code={icmpCode}, checksum={checksum}")
+ # 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)
+ if destport in udpBlacklist:
+ ignore = True
+ logging.log(LOG_TRACE, f"ignoring blacklisted UDP port {destport}")
+ else:
+ logging.debug(f"forwarding udp srcport={srcport}, destport={destport}")
+ elif protocol == 0x06: # TCP
+ srcport = readnet_u16(p, subheader)
+ destport = readnet_u16(p, subheader + 2)
+ if destport in tcpBlacklist:
+ ignore = True
+ logging.log(LOG_TRACE, f"ignoring blacklisted TCP port {destport}")
+ else:
+ logging.debug(f"forwarding tcp srcport={srcport}, destport={destport}")
+ else:
+ logging.warning(f"forwarding unexpected protocol 0x{protocol:02x}, src={ipstr(srcaddr)}, dest={ipstr(destAddr)}")
+
+ return ignore
+
+ def __tunReader(self):
+ tap = self.tun
+ logging.debug("TUN reader running")
+ while True:
+ p = tap.read()
+ #logging.debug(f"IP packet received on TUN interface, type={type(p)}")
+ destAddr = p[16:20]
+
+ if not self._shouldFilterPacket(p):
+ self.sendPacket(destAddr, p)
+
+ def _ipToNodeId(self, ipAddr):
+ # We only consider the last 16 bits of the nodenum for IP address matching
+ ipBits = ipAddr[2] * 256 + ipAddr[3]
+
+ if ipBits == 0xffff:
+ return "^all"
+
+ for node in self.iface.nodes.values():
+ nodeNum = node["num"] & 0xffff
+ # logging.debug(f"Considering nodenum 0x{nodeNum:x} for ipBits 0x{ipBits:x}")
+ if (nodeNum) == ipBits:
+ return node["user"]["id"]
+ return None
+
+ def _nodeNumToIp(self, nodeNum):
+ return f"{self.subnetPrefix}.{(nodeNum >> 8) & 0xff}.{nodeNum & 0xff}"
+
+ def sendPacket(self, destAddr, p):
+ """Forward the provided IP packet into the mesh"""
+ nodeId = self._ipToNodeId(destAddr)
+ if nodeId is not None:
+ logging.debug(f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}")
+ self.iface.sendData(p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck = False)
+ else:
+ logging.warning(f"Dropping packet because no node found for destIP={ipstr(destAddr)}")
+
+ def close(self):
+ self.tun.close()
+
+
Methods
+
+
+def close(self)
+
+
+
+
+
+Expand source code
+
+
def close(self):
+ self.tun.close()
+
+
+
+def onReceive(self, packet)
+
+
+
+
+
+Expand source code
+
+
def onReceive(self, packet):
+ p = packet["decoded"]["data"]["payload"]
+ if packet["from"] == self.iface.myInfo.my_node_num:
+ logging.debug("Ignoring message we sent")
+ else:
+ logging.debug(f"Received mesh tunnel message type={type(p)} len={len(p)}")
+ # we don't really need to check for filtering here (sender should have checked), but this provides
+ # useful debug printing on types of packets received
+ if not self._shouldFilterPacket(p):
+ self.tun.write(p)
+
+
+
+def sendPacket(self, destAddr, p)
+
+
+
Forward the provided IP packet into the mesh
+
+
+Expand source code
+
+
def sendPacket(self, destAddr, p):
+ """Forward the provided IP packet into the mesh"""
+ nodeId = self._ipToNodeId(destAddr)
+ if nodeId is not None:
+ logging.debug(f"Forwarding packet bytelen={len(p)} dest={ipstr(destAddr)}, destNode={nodeId}")
+ self.iface.sendData(p, nodeId, portnums_pb2.IP_TUNNEL_APP, wantAck = False)
+ else:
+ logging.warning(f"Dropping packet because no node found for destIP={ipstr(destAddr)}")
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/setup.py b/setup.py
index b832b85..c5b6d85 100644
--- a/setup.py
+++ b/setup.py
@@ -12,7 +12,7 @@ with open("README.md", "r") as fh:
# This call to setup() does all the work
setup(
name="meshtastic",
- version="1.1.25",
+ version="1.1.26",
description="Python API & client shell for talking to Meshtastic devices",
long_description=long_description,
long_description_content_type="text/markdown",