TashRouter: An AppleTalk Router

The PowerBook still isn't seeing all zones, but a complete shutdown of both TashRouter and atalkd and then restart should clear that up. Its completely normal for routers to develop stale zone tables when you are starting up and shutting down routers one at a time.
Indeed, that was the case. Now that I could see the router actually working, I ended up assigning one zone name to the LtoudpPort instance and a different single zone name to the TapPort instance, so all the Mini vMac instances appear in the "Danger Zone" and the rest of the network is "EtherTalk Network". Of course I could have used the same zone name everywhere, but it's nice to see zones appear in the Chooser! :)

Code:
[atalkd.conf]
vioif0 -seed -phase 2 -net 2 -addr 2.22 -zone "EtherTalk Network"
tap0 -seed -phase 2 -net 3 -addr 3.33 -zone "EtherTalk Network"
#note: net range and zone for tap0 interface must exactly match tap0 configuration in TashRouter!

[TashRouter]
router = Router('router', ports=(
  LtoudpPort(seed_network=1, seed_zone_name=b'Danger Zone'),
  TapPort(tap_name='tap0', hw_addr=b'\xDE\xAD\xBE\xEF\xCA\xFE', seed_network_min=3, seed_network_max=3, seed_zone_names=[b'EtherTalk Network']),
))
#note: hw_addr must NOT match your actual tap0 interface address; use a fake MAC address like DE:AD:BE:EF:CA:FE here

Both the real and virtual Macs see two zones in this configuration, and can mount shared volumes from servers in either zone. This will make Mini vMac a lot more usable; previously, it was difficult to move files between its filesystem and a modern system.

More pictures!
 

Attachments

  • Zone for LToUDP network.png
    Zone for LToUDP network.png
    17.4 KB · Views: 29
  • Zone for main AppleTalk network.png
    Zone for main AppleTalk network.png
    17.1 KB · Views: 27
  • InterPoll on Mini vMac sees all devices.png
    InterPoll on Mini vMac sees all devices.png
    111.4 KB · Views: 27
Last edited:
Here is a cleaner diff for the BSD support. With this change applied, TashRouter is working fine on NetBSD, and should behave as it did before on Linux.
Code:
diff --git a/tashrouter/port/ethertalk/tap.py b/tashrouter/port/ethertalk/tap.py
index eb0fba6..86223b2 100755
--- a/tashrouter/port/ethertalk/tap.py
+++ b/tashrouter/port/ethertalk/tap.py
@@ -40,8 +40,13 @@ class TapPort(EtherTalkPort):
   __repr__ = short_str
 
   def start(self, router):
-    self._fp = os.open('/dev/net/tun', os.O_RDWR)
-    ioctl(self._fp, self.TUNSETIFF, struct.pack('16sH22x', self._tap_name.encode('ascii') or b'', self.IFF_TAP | self.IFF_NO_PI))
+    try:
+      # open and configure Linux tap device
+      self._fp = os.open('/dev/net/tun', os.O_RDWR)
+      ioctl(self._fp, self.TUNSETIFF, struct.pack('16sH22x', self._tap_name.encode('ascii') or b'', self.IFF_TAP | self.IFF_NO_PI))
+    except:
+      # open BSD tap device if no Linux tap device
+      self._fp = os.open('/dev/' + self._tap_name, os.O_RDWR)
     super().start(router)
     self._reader_thread = Thread(target=self._reader_run)
     self._reader_thread.start()
 
Last edited:
I've written a rough pcap implementation for ethertalk.

I've tested it under Debian and Windows.

Under Linux you'll need libpcap installed. Under Windows you'll need NPCAP installed. You'll also need pypcap or pcap-ct installed in python.

Diff:
diff --git a/tashrouter/port/ethertalk/pcap.py b/tashrouter/port/ethertalk/pcap.py
new file mode 100644
index 0000000..d25cdd8
--- /dev/null
+++ b/tashrouter/port/ethertalk/pcap.py
@@ -0,0 +1,91 @@
+'''Port driver for EtherTalk using libpcap.'''
+
+from queue import Queue
+import logging
+import time
+from threading import Thread, Event
+
+try:
+    import pcap
+except ImportError:
+    print("[!] Missing modules pcap, try running 'pip install --upgrade pcap-ct'")
+
+from . import EtherTalkPort
+
+class PCapPort(EtherTalkPort):
+  '''Port driver for EtherTalk using libpcap.'''
+  
+
+  def __init__(self, interface_name, hw_addr, **kwargs):
+    super().__init__(hw_addr, **kwargs)
+    self._reader_thread = None
+    self._reader_started_event = Event()
+    self._reader_stop_requested = False
+    self._reader_stopped_event = Event()
+    self._interface_name = interface_name
+    self._writer_thread = None
+    self._writer_started_event = Event()
+    self._writer_stop_flag = object()
+    self._writer_stopped_event = Event()
+    self._writer_queue = Queue()
+
+  def short_str(self):
+    return self._interface_name
+
+  __str__ = short_str
+  __repr__ = short_str
+
+  def start(self, router):
+  
+    self._sniffer = pcap.pcap(name=self._interface_name, promisc=True, immediate=True, timeout_ms=250)
+  
+    # Todo, add a packet filter to only capture EtherTalk packets?
+  
+    super().start(router)
+    self._reader_thread = Thread(target=self._reader_run)
+    self._reader_thread.start()
+    self._writer_thread = Thread(target=self._writer_run)
+    self._writer_thread.start()
+    self._reader_started_event.wait()
+    self._writer_started_event.wait()
+
+  def stop(self):
+    self._reader_stop_requested = True
+    self._writer_queue.put(self._writer_stop_flag)
+    self._reader_stopped_event.wait()
+    self._writer_stopped_event.wait()
+  
+    self._sniffer.close()
+  
+    super().stop()
+
+  def send_frame(self, frame_data):
+    self._writer_queue.put(frame_data)
+
+  def _reader_run(self):
+    self._reader_started_event.set()
+
+    while not self._reader_stop_requested:
+      # Todo: make sure we're not dropping packets here
+      self._sniffer.dispatch(1, self._pcap_callback)
+    self._reader_stopped_event.set()
+  
+
+  def _writer_run(self):
+    self._writer_started_event.set()
+    while True:
+      frame_data = self._writer_queue.get()
+      if frame_data is self._writer_stop_flag: break
+      try:
+        self._sniffer.sendpacket(frame_data)
+      except OSError:
+        logging.warning("Couldn't send packet")
+        logging.debug(self._sniffer.geterr())
+        pass
+    self._writer_stopped_event.set()
+
+  def _pcap_callback(self, ts, pkt):
+    self.inbound_frame(pkt)
+
+  def get_interface_names():
+    pcap.findalldevs()
\ No newline at end of file

You'll need a line in your router to enable it, eg under Windows I have:

Python:
router = Router('router', ports=(
  LtoudpPort(seed_network=1, seed_zone_name=b'LToUDP Network'),
  PCapPort(interface_name='\\Device\\NPF_{B7D4E073-2185-4912-BBE8-3948C6636D02}', hw_addr=b'\xDE\xAD\xBE\xEF\xCA\xFE', seed_network_min=3, seed_network_max=5, seed_zone_names=[b'EtherTalk Network']),
))

Under Windows, you can get the interface name from the attached EthList.exe. Under Linux just use your interface name, eg eth0 or ens123.

If anyone tests this please let me know how you go.
 

Attachments

Last edited:
I'm seeing fairly regular hangs with TashRouter. This is quite recent and I think this is probably happening due to the number of zones on #globaltalk getting a lot of interest over #MARCHintosh. ..or at least the stress of all those zones is causing this to hang undoubtedly due to an error in my config :)

Over the past week or so Tashrouter just stops routing and my Localtalk part of the network just disappears and so I need to restart it TashRouter. I'm assuming this is due to the number of zones as previously it has been running fine for six months or so. I've updated the RPi and rebooted it with no change.

Mar 21 11:51:17 wolfalice python3[14932]: Exception in thread Thread-10 (_reader_run):
Mar 21 11:51:17 wolfalice python3[14932]: Traceback (most recent call last):
Mar 21 11:51:17 wolfalice python3[14932]: File "/usr/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
Mar 21 11:51:17 wolfalice python3[14932]: self.run()
Mar 21 11:51:17 wolfalice python3[14932]: File "/usr/lib/python3.11/threading.py", line 975, in run
Mar 21 11:51:17 wolfalice python3[14932]: self._target(*self._args, **self._kwargs)
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/port/ethertalk/tap.py", line 69, in _reader_run
Mar 21 11:51:17 wolfalice python3[14932]: self.inbound_frame(os.read(self._fp, 65535))
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/port/ethertalk/__init__.py", line 263, in inbound_frame
Mar 21 11:51:17 wolfalice python3[14932]: self._router.inbound(datagram, self)
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/router/router.py", line 107, in inbound
Mar 21 11:51:17 wolfalice python3[14932]: self.route(datagram, originating=False)
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/router/router.py", line 152, in route
Mar 21 11:51:17 wolfalice python3[14932]: entry.port.broadcast(datagram)
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/port/localtalk/__init__.py", line 164, in broadcast
Mar 21 11:51:17 wolfalice python3[14932]: self.send_frame(bytes((0xFF, self.node, self.LLAP_APPLETALK_SHORT_HEADER)) + datagram.as_short_header_bytes())
Mar 21 11:51:17 wolfalice python3[14932]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/datagram.py", line 118, in as_short_header_bytes
Mar 21 11:51:17 wolfalice python3[14932]: raise ValueError('invalid hop count %d, short-header datagrams may not have non-zero hop count' % self.hop_count)
Mar 21 11:51:17 wolfalice python3[14932]: ValueError: invalid hop count 3, short-header datagrams may not have non-zero hop count

I also noticed that if I started the app ChatNET (on a Quadra connected via LocalTalk) then TashRouter immediately hangs with the same error message.

The Python script that starts TashRouter has:
router = Router('router', ports=(
LtoudpPort(seed_network=17767, seed_zone_name=b'scotgate'),
TashTalkPort(serial_port='/dev/ttyAMA0', seed_network=17768, seed_zone_name=b'scotgate', verify_checksums=False),
TapPort(tap_name='tap0', hw_addr=b'\x76\xb1\xb6\x25\xce\x46', seed_network_min=17769, seed_network_max=17769, seed_zone_names=[b'scotgate']),
))
My network has Apple Internet Rrouter running via QEMU then TashRouter/TashHat to connect a few Localtalk connected Macs and one printer. The same RPi that runs TashRouter also has NEtatalk 2.4 installed and running. The RPi 3 model B 1GB is running Raspbian Bookworm.
 
invalid hop count 3, short-header datagrams may not have non-zero hop count
Interesting. This doesn't point to a load issue, I don't think. This could still be an issue with TashRouter, but it might also be an issue with someone on the network creating an illegal packet and TashRouter gagging on it. TashRouter really should just discard the packet and keep on trucking. I'd have to look into this more to say for sure, but I'm extremely busy of late... if you want to paper over the issue, I'd suggest going into port/localtalk/__init__.py and putting a try-except around the self.send_frame call at line 164 that just swallows ValueErrors, comme ça:
Python:
try:
    self.send_frame(...)
except ValueError as e:
    print('oh no: %s' % str(e))
 
Interesting. This doesn't point to a load issue, I don't think. This could still be an issue with TashRouter, but it might also be an issue with someone on the network creating an illegal packet and TashRouter gagging on it. TashRouter really should just discard the packet and keep on trucking. I'd have to look into this more to say for sure, but I'm extremely busy of late... if you want to paper over the issue, I'd suggest going into port/localtalk/__init__.py and putting a try-except around the self.send_frame call at line 164 that just swallows ValueErrors, comme ça:
Python:
try:
    self.send_frame(...)
except ValueError as e:
    print('oh no: %s' % str(e))
Thanks for that extremely quick reply! I think the ink had barely dried on my post before you give me a solution :-) I will try as you suggest now and report back! Cheers
 
Over the past week or so Tashrouter just stops routing and my Localtalk part of the network just disappears and so I need to restart it TashRouter. I'm assuming this is due to the number of zones as previously it has been running fine for six months or so. I've updated the RPi and rebooted it with no change.

Not number of zones.

Looking at the stacktrace:

Code:
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/port/ethertalk/tap.py", line 69, in _reader_run
Mar 21 11:51:17 wolfalice python3[14932]: self.inbound_frame(os.read(self._fp, 65535))
Mar 21 11:51:17 wolfalice python3[14932]: File "/home/chris/src/tashrouter/tashrouter/port/ethertalk/__init__.py", line 263, in inbound_frame
Mar 21 11:51:17 wolfalice python3[14932]: self._router.inbound(datagram, self)
...
Mar 21 11:51:17 wolfalice python3[14932]:   File "/home/chris/src/tashrouter/tashrouter/port/localtalk/__init__.py", line 164, in broadcast
Mar 21 11:51:17 wolfalice python3[14932]:     self.send_frame(bytes((0xFF, self.node, self.LLAP_APPLETALK_SHORT_HEADER)) + datagram.as_short_header_bytes())
Mar 21 11:51:17 wolfalice python3[14932]:                                                                                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Mar 21 11:51:17 wolfalice python3[14932]:   File "/home/chris/src/tashrouter/tashrouter/datagram.py", line 118, in as_short_header_bytes
Mar 21 11:51:17 wolfalice python3[14932]:     raise ValueError('invalid hop count %d, short-header datagrams may not have non-zero hop count' % self.hop_count)
Mar 21 11:51:17 wolfalice python3[14932]: ValueError: invalid hop count 3, short-header datagrams may not have non-zero hop count

Someone's sending short-header packets over EtherTalk at all? For a packet routed from EtherTalk to LocalTalk I'd expect an LLAP layer 2 header followed by a long DDP header.

@Tashtari this feels like something that should be a 'log-shrug-and-carry-on' rather than a thrown exception
 
Thanks for that extremely quick reply! I think the ink had barely dried on my post before you give me a solution :) I will try as you suggest now and report back! Cheers
Yup. That does the trick. Previously just launching and clicking LOGIN on ChatNet would cause the issue. Now the network remains up. I assume that the sporadic network issue will do too since it was the same error. Again, many thanks!
 
Not number of zones.

Someone's sending short-header packets over EtherTalk at all? For a packet routed from EtherTalk to LocalTalk I'd expect an LLAP layer 2 header followed by a long DDP header.

@Tashtari this feels like something that should be a 'log-shrug-and-carry-on' rather than a thrown exception
Thanks. Where do you think this is coming from? I did notice that launching NetChat also immediately causes this error (although not with the workaround @Tashtari suggested. At a wild guess could somebody else be running this app and that's causing it. ..or could the cause be closer to home (i.e. my network!). Otherwise somebody else must have seen this as surely there are others running TashRouter on #GlobalTalk?
 
Sorry if terse, brain not behaving well today.

I'm not particularly inclined to debug globaltalk issues but I think this actually might be a TashRouter bug.

What I'm wondering is if this is a directed-broadcast issue. That is to say, some node in a distant network sends a packet to yournet.255, which is the "all nodes in yournet" broadcast address. That is routed through the routers on its path to you per the network ID, with the hop count increasing. It then arrives at TashRouter through EtherTalk (l.7 in the stacktrace above) and is passed through to the LocalTalk port. The LocalTalk port then assumes that it's a short-header packet, and barfs at the fact it has a hop count > 0. But it's not a short-header packet, it's a long-header packet that is directed to a broadcast address.

So TashRouter is actually treating the packet incorrectly. I think the deeper problem here is that it seems to need to work out whether a packet is a long- or short-header DDP packet by means of its source and destination, and that won't work in all legit circumstances; the header length of the packet needs to be a property of the packet, not inferred from where it has come from and where it is going.

Edit: in OmniTalk, for example, the header length of the packet is stored here https://github.com/cheesestraws/omnitalk/blob/main/omnitalk/main/mem/buffers.h#L36 and set on incoming LLAP frames here: https://github.com/cheesestraws/omnitalk/blob/main/omnitalk/main/lap/llap/llap.c#L37
 
Last edited:
This might have been a side effect of the fixes made for RTMP or the ImageWriter LocalTalk card. Watching actual traffic from AppleTalk routers, the only packets that must always use Short DDP packets are the RTMP broadcasts from the router itself. Everything else uses Long DDP packets on a network that has a router. This is due to older devices like the IIe Workstation Card and the original LaserWriter/LaserWriter Plus only being able to "see" RTMP broadcasts in short DDP packets.
 
Coming back to this thread late, but I can confirm that I also saw the same TashRouter crash that @fergycool reported. At the time, I saw the self.hop_count check was failing and figured it could safely be ignored rather than bringing down the router, so I just commented it out and everything has been running smoothly since then. The weird thing is that TashRouter was running fine for a month prior to this, without encountering the hop count issue, then the crashes started happening suddenly around the 21st. That suggests some new source of those packets was introduced which hadn't been running earlier.
 
I guess I've been called out :eek:o_O:cautious:🤔

So yes, I have tashrouter working in harmony with netatalk on the same raspberry pi.

The pi in question is a Pi2B running Raspbian Bullseye and equipped with a TashTalk Hat of Tashtari's design.

I experimented with the recommended macvtap interface but found it to be somewhat poorly documented.
In theory it's supposed to function like a bridge+tap, but I could not get that to work properly.

Instead, I just went with a classic tap + bridge setup.

I found the easiest way to set this up was to disable dhcpcd and use systemd networking to create the necessary interfaces on boot.

/etc/network/interfaces.d/br0
Code:
#loopback
auto lo
iface lo inet loopback

# Virtual interface
auto tap2
iface tap2 inet manual
   pre-up tunctl -t tap2 -u root
   up ip link set dev tap2 up
   down ip link set dev tap2 down

# Bridge interface
auto br0
iface br0 inet static
   address 10.40.0.216
   gateway 10.40.0.1
   dns-domain local.default.tel
   dns-nameservers 10.40.0.1 1.1.1.1 8.8.8.8
   bridge_ports eth0 tap2
   bridge_stp off
   bridge_maxwait 5

eth0 = the ethernet interface of the pi. I have predictable interface names disabled.
tap2 = the network tap. tashrouter will bind to this.
br0= a network bridge of tap2+eth0. Netatalk's atalkd daemon will be bound to this, which will allow netatalk to "hear" tashrouter.
this is also given a static IP address which will become the address of the pi.

Now a tashrouter script that binds to the tap interface must be created. This needs to be in the main tashrouter directory.

tap_router.py

Code:
import logging
import time

import tashrouter.netlog
from tashrouter.port.ethertalk.tap import TapPort
from tashrouter.port.localtalk.ltoudp import LtoudpPort
from tashrouter.port.localtalk.tashtalk import TashTalkPort
from tashrouter.router.router import Router


logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(levelname)s: %(message)s')
#tashrouter.netlog.set_log_str_func(logging.debug)  # comment this line for speed and reduced spam

router = Router('router', ports=(
  LtoudpPort(seed_network=1, seed_zone_name=b'LToUDP Network'),
  TashTalkPort(serial_port='/dev/ttyAMA0', seed_network=2, seed_zone_name=b'TashTalk Network'),
  TapPort(tap_name='tap2', hw_addr=b'\xDE\xAD\xBE\xEF\xCA\xFE', seed_network_min=3, seed_network_max=5, seed_zone_names=[b'EtherTalk Network']),
))
print('router away!')
router.start()

try:
  while True: time.sleep(1)
except KeyboardInterrupt:
  router.stop()

The important parts here are:
from tashrouter.port.ethertalk.tap import TapPort
this tells tashrouter to make the tap ethertalk module(?) available
TapPort(tap_name='tap2', hw_addr=b'\xDE\xAD\xBE\xEF\xCA\xFE', seed_network_min=3, seed_network_max=5, seed_zone_names=[b'EtherTalk Network']),
this tells tashrouter to bind to the specified tap interface (tap2), and give it the specified MAC address (\xDE\xAD\xBE\xEF\xCA\xFE).

Finally, Netatalk's atalkd.conf needed to be edited.
add a new interface to the top of the file, "br0" and comment out the interface already specified.
Code:
br0
#eth0 -phase 2 -net 0-65534 -addr 65280.238

The other attributes will be magically populated when atalkd runs.

Now things are ready to run. If netatalk is already running it needs to be shut down, and tashrouter needs to be run first.

I use systemctl to stop all the netatalk services. You can probably get away with just stopping the atalkd service though...

If you DO want to stop all the netatalk services, just substitute the particular service name in place of atalkd. there's 5 in total. atalkd, afpd, papd, a2boot, and timelord.

systemctl stop atalkd

Then tashrouter needs to be started (using the tap script above)

python3 ~/tashrouter/tap_router.py just note that this will NOT run in the background, use tmux or screen to run it in a seperate window for now

When tashrouter is running properly, start atalkd.

systemctl start atalkd

Then, check the status.

systemctl status atalkd

If everything worked, it should say something like this:

Code:
Jan 12 20:03:08 raspbx atalkd[543]: zip_getnetinfo for br0
Jan 12 20:03:08 raspbx atalkd[543]: zip gnireply from 3.125 (br0 812)
Jan 12 20:03:08 raspbx atalkd[543]: zip_packet configured br0 from 3.125
Jan 12 20:03:09 raspbx atalkd[543]: rtmp_packet gateway 3.125 up
Jan 12 20:17:41 raspbx atalkd[543]: ready 0/0/0
Jan 12 20:17:41 raspbx systemd[1]: Started Netatalk AppleTalk daemon.

Things should now be working, you should be able to see the netatalk share in the EtherTalk zone. Grab a few Macs and emulators and test if it works via ethertalk, localtalk, and LToUDP.

But you probably want things to start at boot and generally be a bit neater.

To make things tidy, make a systemd unit for tashrouter.

First, quit tashrouter, and shutdown atalkd.

nano /etc/systemd/system/tashrouter.service
yeah there's probably a better place to put this...

Code:
[Unit]
Description=TashRouter Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/bin/python3 /root/tashrouter/tap_router.py

[Install]
WantedBy=default.target

/root/tashrouter/tap_router.py = the location of your tap_router.py file in the tashrouter directory.

Save, reload systemd units.

systemctl daemon-reload

Then start the new unit.

systemctl start tashrouter

Check the status using systemctl status tashrouter and check if it's working. If it is, then systemctl enable tashrouter and it will start at boot.

This should be enough to have tashrouter start before netatalk, but if not... systemd overrides can be created to force netatalk to start after tashrouter. i haven't included them as i don't have them set up properly yet

Coming to this thread a bit late... I have Netatalk running in a Proxmox VM and it works well, I have two virtual network interfaces in the VM, one that connects to my NAS server (hidden bridge in proxmox only) and a 2nd NIC that binds to netatalk to send the Appletalk traffic over my "retro" VLAN tag.

All works great, but I wanted to impliment this so I could get LToUDP on the primary main VLAN that way I can get Snow and other emulators to see the appletalk traffic using the more modern standard.

Is there a way to get this to work without the hat and in a VM instead of a Pi? (I also need to figure out how to route LToUDP over a different lan/interface other than the appletalk one.)

This might be answered/solved already, but this thread is now 23 pages deep and thats hard to find.

Any thoughts?
 
All works great, but I wanted to impliment this so I could get LToUDP on the primary main VLAN that way I can get Snow and other emulators to see the appletalk traffic using the more modern standard.

Is there a way to get this to work without the hat and in a VM instead of a Pi? (I also need to figure out how to route LToUDP over a different lan/interface other than the appletalk one.)
hat isn't necessary for LToUDP, it's only needed for REAL localtalk :)

this is very doable, you just need to change the tashrouter config to be ethertalk and LToUDP only.
 
Back
Top