MultiCast Tree

by NLD in Circuits > Arduino

126 Views, 2 Favorites, 0 Comments

MultiCast Tree

multicast-tree-circuit.jpg

I find it one of the coolest parts of technology when we bring virtual data to reality, whether that is flipping a remote switch making coffee from your smartphone or just displaying what is going on the network ;)


With a single traffic indicator LED blinking on a router/switch will not reveal much about what is going on real-time on a network. One can have their wall full of Grafana, Zabbix, OPNsense dashboards displaying all the fancy flows, wouldn't it be just nice to display it in some other ways?


To promote Open Source the best system for the job is FreeBSD and for the display part Arduino will come for our help as always.


This Instructable is mostly a software hack, the hardware part is really simple and straightforward. As in many of my Instructables I'm using the Arduino as a 1 way display device, the data flows from PC->Arduino to keep it simple stupid.


If you complete the whole guide you will have all these data displayed one way or another:


  1. Upload/Download speeds with analog VU meters
  2. TCP/UDP/ICMP/other IP/non-IP/Broadcast/Multicast packets
  3. Display threat warnings coming from the local IDS (Suricata)
  4. Internal LAN host counting
  5. LED row VU meter for displaying the latency to specific host (extremely cool looking)
  6. Traffic stats aggregated from multiple network segments and visualized on a tree


What else should you monitor? I like to compare this question to when someone asks me what skill points should I put in on my Druid or Amazon in Diablo II. You can read all the guides you want on the Internet, but the best answer is: the skills you are using. What to monitor on your network? Monitor what's important for you. This build will serve as an easily customizable base hardware/software you can build on.


Here we arrived to the point why is this project called the MultiCast Tree? It is basically a joke as I never used multicast for anything after more than 20 years in IT. It mostly has significance with IPTV and other video streaming projects where one source data has to be broadcasted down on leaves of a switch tree structure with the least overhead to specific host groups. Can this project detect multicast? absolutely, does it use multicast? no :)


Those who spent countless hours looking at iptraf screens (yep a tool existed long before htop) familiar counters can come to mind:


| Other IP: 0 0 0 0 0 0 │
│ Non-IP: 0 0 0 0 0 0 │
│ Broadcast: 13 1177 13 1177 0 0


Broadcast is extremely useful, from ARP resolution to Samba and many automated Windows protocols use it and you don't exactly want the network to have too many of these packets, especially not broadcast storm. If you want to have some fun just plug an Ethernet cable in between 2 ports of a dumb switch and you will have your new Christmas tree right there.

Supplies

UNrVh.jpg
XbFlo.jpg
qFbHz.jpg
  1. Any generic X86/X64/ARM PC capable of running OPNsense with at least 2 Gbit/s network interfaces
  2. Arduino Mega
  3. 10+4 color leds (one 330 ohm resistor to each)
  4. Copper/glass tree
  5. 4 WS2812 (5V) led strips
  6. Additional external 5V@2A power supply recommended for the Arduino
  7. 2 VU meters from old Audio Mixer/Tape Deck (10k for testing then a fix resistor for final build)
  8. RGB LCD screen

Hardware/software Basics Arduino + OPNsense

multicast-tree.jpg

Ages ago I loved to have my own FreeBSD based IDS, which was usually a commodity X86 box with 3 network interfaces, 2 of them running without IPs in bridged mode and bridging all the Internet traffic between my main WAN router and the final cable modem connected to the net while having one inside the network for managing the box through ssh and alerts. The main advantage of this build was that an attacker who would get access to the (let's call it DMZ network, the one outside your NAT gateway) would not just not be able to get access to the IDS device but would not even know that it is there. Obviously one disadvantage of this build is if there is an attack going on all what this box sees is that it is originating from the router's WAN IP (as the internal LAN infrastructure is hidden from it). The advantage is that this is a literal drop-in setup for any networks.


While a setup like this would be pretty much doable with all the Linux and BSD distributions I picked OPNsense due to it being a super user friendly FreeBSD based network security and firewall appliance with a large community still under active development. Good hardware support (i386 still supported until version 20). It does not require any special hardware; you are fine with any recent dumpster-found PC with 2 Gbit NICs.


There are 2 ways to integrate the device into the network:


1, Bridging (completely transparent, no IPs involved on bridge interfaces, removable any time and cable can be connected in between the network equipment like it was not even there)


2, NATting (device does regular NAT, in this case its removed, reconfiguration is required)


NAT also has other "shortcomings" depending on the OS kernel, device memory etc. the connection table can be nuked with various things from regular Steam internet game search to pumping out millons of packets through the router with tools like Nmap, Zmap, Masscan which after the device might stays @ rekt city until it is rebooted :S


oom_kill_process: no need to kill process when out of memory and drop cache!


The setup of OPNsense is beyond this document for the bridge config please read: https://docs.opnsense.org/manual/how-tos/lan_bridge.html


As of the Arduino the connectivity diagram is super simple, the specific display parts will be explained later.


Arduino -> PC communication


We're going to stick with the usual one way communication and not get into using any binary protocols to KISS:


  1. Shell-friendly: Perfect match for shell scripting capabilities
  2. Debuggable: You can monitor with simple cat commands
  3. Robust: Line-based, easy to recover from errors
  4. Maintainable: Anyone can understand and modify it
  5. Good enough: 25 bytes @ 19200 baud = ~10ms transfer time


Most of the components are integrated into the C code, however I will publish the shell scripts I wrote earlier which can handle everything else from protocol level analysis with a bit more overhead.

This time - instead of the usual format, I write these articles separating hardware/software design- I will split it by difficulty to implement and leave it to the reader to do so, all other parameters you don't need can be easily removed from the code.

Counters: Number of Hosts Alive

iUTun.jpg

Difficulty: easy


There are a plethora of different ways to discover hosts online on the network ranging from regular ICMP ping to TCP ACK ping. I'm going to use the trickiest one which will not bother any firewalls, arping. When pinging an IP an ARP who-has query is sent.


The arping utility sends ARP requests to the specified host and displays the replies. The host may be specified by its hostname, its IP address, or its MAC address. ARP packets are usually replied to (on a LAN) so fast that the OS task scheduler can't keep up to get exact enough timing.


ARPing operates in LAYER2 that is why it will bypass all firewalls which also operates in LAYER3 like iptables/ipfw/npf/windoz firewall/cisco access lists etc. Is there a way to block this? Sure there is with firewalls which actually meant to operate in LAYER2 like ebtables you can block the scanner box mac address then it will detect that the host is down however blocking arp completely will render the workstation unusable.


Arping can only ping a single IP or broadcast address so we need another tool from our Swiss army knife which can ping IP range, arpscan.


Sadly this package is not included in OPNsense, the easiest way is if you have an up and running another FreeBSD 14 VM just fetch it and copy it over:


pkg fetch -o /tmp/libdnet -d arpscan-0.5_1
pkg fetch -o /tmp/libdnet -d libdnet-1.13_5


One trick pony:


arpscan -i ue0 192.168.1.0/24 | sort | uniq | sort | wc -l | tr -d ' '


One can ask why do we have to remove duplicates in this list ... of course because of Windows which can pull things like duplicates left and right when your laptop is in sleep mode. WiFi low energy TM, anyway arpscan is pretty good to determine if a host is dead or not.


The limitations: Arpscan cannot scan past through the broadcast domain (aka your LAN) if you want to check on hosts in other routed VLANs, subnets this scan is not sufficient for it.


Advantages: very fast, passes through firewalls like they were not there, will not create any firewall dropped packet log events


Other easy ICMP way (then process it).


nmap -sP 192.168.1.0/24 | grep 'Host is up' | wc -l | tr -d ' '


I did many tests between the two and the bottom line is, arpscan is just way faster than nmap with smaller footprint, take it or leave it choose which one you like; the end result will be a simple number for the hosts on the LAN.


This is an ACTIVE type of metric, meaning unlike the network flow through the bridge for this we generate network traffic (which might be a large amount depending on the hosts being scanned), doing so in every second in real-time would be an overkill, so it can be done from cron, let's say once every 10 minutes is enough, store the number in a text file and read it from there. A good practice when doing these file ops is that our display_updater script always validates (does the file exist, does it have the right value in a range ... if not then ignore it).

I did not implement this in the C code either, arpscan is called externally from cron if you look at the main C program that is only parsing a text file from tmp with the number of hosts.

Counters: Local TCP/UDP/ICMP Connection Counters

iSvpY.jpg
zabbix.jpg
router-items.jpg

Difficulty: easy


These we can easily extract from pf with:

pfctl -ss | grep -vi 127.0.0.1 | grep -vi <YOUR LAN RANGES>


Here we don't care about the LAN connection counts (e.g., SSH to this box, monitoring etc so grep them out)

This simple data is fed into our data transmission script and displayed on a 2x16 LCD screen.


Down: 1.374008 | Up: 0.433656 | Hosts: 138 | ICMP: 4 | TCP: 47898 | UDP: 58
Down: 0.022408 | Up: 0.341816 | Hosts: 164 | ICMP: 4 | TCP: 57948 | UDP: 40
Down: 0.014256 | Up: 0.332720 | Hosts: 135 | ICMP: 4 | TCP: 68886 | UDP: 44
Down: 0.125240 | Up: 0.343544 | Hosts: 127 | ICMP: 4 | TCP: 80472 | UDP: 50


Only issue with it: in case of too many connections it can get a bit sluggish (for this you can implement the same approach as for the host count, dump this once a minute from cron and only open the file). Having this many connections is not normal for my network so let's just stick with the "it works for me" analogy.


A beautiful visualization deserves a beautiful LCD screen, I decided to use the DFRobot Gravity I2C LCD1602. On some photos you can spot a popular cheap one with the default blue background, these are made blue because 90% of the customers don't care about using them in RGB mode, obviously when you have a screen which uses blue tinted glass then you can only get some mixture of blue no matter what you do.


With the DFRobot screen they claim that the back-light is adjustable through I2C as well however I did not have any success with this. There is a trio connector on the other side (with already resistors on them) which can be connected to the Arduino's digital pins. They use active LOW logic, if the pin is low the color is lit up.


To follow the KISS principle here as well the color change logic is implemented in the C code, then you can easily make any adjustments later on without reprogramming the micro.


For the LCD back-light colors I picked cyan as default because it looks just beautiful on this LCD. As the local RX/TX is already mapped on the VU meters I implemented to change the LCD backlight based on the TCP/UDP/ICMP connections.


Color Priority Order (Highest to Lowest):

RED (Color ID: 0)

• Condition: Total connections (TCP+UDP+ICMP) > 2000

• Priority: 1st (Highest) - Overrides all other colors

• Triggers immediately regardless of other conditions

BLUE (Color ID: 2)

• Condition: TCP connections > 1000

• Only if: Total connections ≤ 2000

• Priority: 2nd - Beats GREEN, YELLOW, MAGENTA, LIME, CYAN

GREEN (Color ID: 1)

• Condition: UDP connections > 1000

• Only if: Total connections ≤ 2000

• Priority: 3rd - Beats YELLOW, MAGENTA, LIME, CYAN

• Note: Loses to BLUE if both TCP > 1000 AND UDP > 1000

YELLOW (Color ID: 5)

• Condition: ICMP connections > 10

• Only if: Total connections ≤ 2000

• Priority: 4th - Beats MAGENTA, LIME, CYAN

• Note: Loses to BLUE and GREEN

MAGENTA (Color ID: 3)

• Condition: Broadcast consistently active for 10 consecutive checks

• Only if: Total connections ≤ 2000

• Priority: 5th - Beats LIME and CYAN

• Requirement: Broadcast active for all 10 recent checks

LIME (Color ID: 6)

• Condition: Multicast consistently active for 10 consecutive checks

• Only if: Total connections ≤ 2000

• Priority: 6th - Beats CYAN only

• Requirement: Multicast active for all 10 recent checks

CYAN (Color ID: 4)

• Condition: DEFAULT - None of the above conditions met

• Priority: Lowest (7th)

• Always active when no other conditions apply

Ping-O-VU

OllUr.jpg

Difficulty: moderate

Let's say you have a key server where you would like to monitor the ping real-time. I believe the coolest thing for this is one of those real square LED VU meters from decks and audio mixers. I did not have any in time so a led row is used as a quick replacement.

Since LEDs can eat up quite a lot of pins there are many ways to multiplex them but since we use the Arduino Mega with plenty of pins in this build let's waste them!

10 LEDs (the usual green, yellow, red combination), 10 digital pins with one 330 ohm resistor on each.

The LEDs are lit up based on the ping to the given server. This logic is better to be shifted to the OPNsense box again and only the number of LEDs to be passed each round as an unsigned integer to the micro.

ping -c 1 1.2.3.4

Easy right? Why is this moderate then?! Wrong. Ping response time and execution speed can vary slowing down our the main program. If one wants to implement this with a shell script, certainly can. The smart way to go in this case is to have it run like once per minute (lowest cron granularity) write the result let's say a single number 50 into a text file then read this from the main script which is sending the data continuously to the Arduino. At reading do a format is to check and a possibly retry attempt in the case the program would be just in the middle of writing the data out.

All of this is taken care of in my C code, thread safely. The server gets pinged from a thread, no matter how long the execution takes, the data will only be updated in the main program once the value is successfully obtained. It will not hold up the main program in any way. Even the corresponding network cable connected to the nic can be unplugged it will not make any difference.

Speed-O-Meter

tNVGI.jpg
asus.jpg

Difficulty: moderate


It's a really cool idea to use audio equipment, either VU meters or fader lights for showing your upload/download speeds. There are some speed-o-meters circulating online. Some rely on SNMP which is mostly not available on cheap SOHO routers. In the hardware design you already learned that for this project we will use a full FreeBSD bridge so the options to obtain this data are plenty, just some examples:


vnstat -i igb1 -l -m
ifstat -i igb
netstat -I igb1 1
sar -n DEV 1 1 | grep igb1 | tail -n 1 | gawk '{print "Down: "$5,"Up: "$6}'
netstat -q1 -w1 -I igb1 | tail -1 | awk '{ print $4","$7 }'

We're going to go with the last one utilizing the most basic netstat utility to pull the bytes flowing through the interface of our interest (this is bytes), do this every second and we get bytes/second. Now the VU meters or LEDs couldn't care much about this if you calibrate them they could work by the bits but let's convert this info to megabit/seconds right away (most commonly used in networking).


This is the one calculation I let the Arduino handle, for this to work you need to define your maximum Download and Upload speeds at the top of the main code eg: 100/100 mbit/s.


How does the Arduino do the job? With PWM (Pulse Width Modulation) because most analog VU meters are essentially galvanometers - they're current-driven devices that respond to the average voltage applied to them.


Analog Meter Movement: The needle is moved by a coil in a magnetic field

Coil Response: The coil can't respond to rapid voltage changes - it averages them out

PWM Principle: By rapidly switching between 0V and 5V, we create an "average" voltage

PWM Duty Cycle = Average Voltage
100% duty = 5V (full scale)
50% duty = 2.5V (half scale)
0% duty = 0V (no movement)


Why don't we use Analog Voltage Directly?

Arduino Limitation: Most Arduino pins can only output digital (0V or 5V)

PWM Pins: Special pins that can simulate analog using PWM

Precision: 256 possible values (0-255) gives smooth meter movement

// Map speed to PWM value (0-255)
int downloadPWM = mapSpeedToPWM(downloadSpeed, MAX_DOWNLOAD_SPEED);

// Send PWM signal to meter
analogWrite(downloadMeterPin, downloadPWM);


downloadSpeed = 50.0 Mbps (50% of your 100 Mbps max)

downloadPWM = 127 (50% of 255)

Arduino rapidly pulses pin: 50% time at 5V, 50% time at 0V

VU meter coil averages this to ~2.5V

Needle moves to 50% position


Calibrate full-scale deflection

Ensure the 5V PWM signal moves the needle to exactly 100%

If you had a DAC (Digital-to-Analog Converter), you could output true analog voltages, but PWM is:

  1. Built into Arduino
  2. Perfect for meter movements
  3. Simple and effective


So PWM is essentially faking an analog voltage in a way that perfectly suits analog meter movements!


The TLDR version: we will need one single resistor in series with VU meters and Arduino PWM pins, you dont need an Arduino to determine their value, get a lab power supply set it to 5V add a variable 10k resistor in series with the vu meter, adjust it until the meter goes out to the MAX position disconnect it from the circuit measure the resistance, this is what you will need. If you want to make it even more precise then power it from the Arduino's digital pins and do the same experiment because it never gives out exactly 5V regardless you power it from USB or bypass the regulator and use an external PSU.


Blinkies (Broadcast/Multicast/Other-IP/Non-IP)

iptraf.jpg

Difficulty: hard


Now when it comes down to separating multicast/broadcast from the actual traffic is when my device really shines. My system provides intelligent, protocol-aware visual feedback that ordinary network equipment cannot match, giving real insight into the network traffic patterns.


How is this done? BPF (Berkeley Packet Filter) is used to directly examine Ethernet frames and check the destination MAC addresses:


Broadcast


Broadcast is probably the most important to monitor as you never want too much of it especially not broadcast storm :)


// BROADCAST: FF:FF:FF:FF:FF:FF
if (eth->ether_dhost[0] == 0xFF &&
eth->ether_dhost[1] == 0xFF &&
eth->ether_dhost[2] == 0xFF &&
eth->ether_dhost[3] == 0xFF &&
eth->ether_dhost[4] == 0xFF &&
eth->ether_dhost[5] == 0xFF) {
broadcast_detected = 1;
}
// MULTICAST: First byte = 0x01
else if (eth->ether_dhost[0] == 0x01) {
multicast_detected = 1;
}


Multicast


Multicast sends data to a specific group of interested devices rather than all devices. Current popular use cases are service discovery protocols like mDNS (Apple/Chromecast), SSDP/UPnP (smart devices), and streaming media distribution in enterprise networks. OSPF also heavily uses multicast for efficient neighbor discovery and routing updates, particularly on broadcast networks (like Ethernet) by sending packets to special multicast addresses like 224.0.0.5 (AllSPFRouters) and 224.0.0.6 (AllDRouters) for IPv4, reducing unnecessary unicast traffic and broadcasts. While some OSPF packets (like DD, LSR, LSU) might use unicast on non-broadcast networks, multicast is key for dynamic neighbor finding and updates in standard broadcast environments.


Multicast operates at both layer 2 (Data Link) and layer 3 (Network), my detection is implemented in layer 2 so it is key to understand how do these multicast addresses get mapped to specific MACs.


Multicast IP addresses like 224.1.1.1 have a corresponding multicast hardware (MAC) address, which for this specific IP is 01:00:5E:01:01:01. This address is derived using a standard mapping process defined by the Internet Assigned Numbers Authority (IANA).


Mapping Process

The IPv4 to Ethernet MAC address mapping is a standardized procedure:

The first 24 bits of the MAC address are a fixed IANA-controlled prefix: 01:00:5E (in hexadecimal).

The 25th bit is fixed as zero.

The last 23 bits of the MAC address are directly copied from the last 23 bits of the multicast IP address.


A key point of this mapping is that only 23 of the 28 available multicast IP address bits are used in the MAC address.

This means that 32 different multicast IP addresses map to the same hardware address.


For example, the following IPv4 addresses all map to the same MAC address, 01:00:5E:01:01:01:


224.1.1.1

225.1.1.1

226.1.1.1

... up to ...

239.1.1.1


When a host receives a frame with the destination MAC address 01:00:5E:01:01:01, its network interface card (NIC) accepts the frame. The host's CPU then examines the Layer 3 IP header to determine which specific multicast group the packet belongs to (e.g., 224.1.1.1) and discards any packets for groups it has not joined.


MAC Address: OSPF uses 01:00:5E:00:00:05 (224.0.0.5) and 01:00:5E:00:00:06 (224.0.0.6)


The Problem:


All broadcast packets (FF:FF:FF:FF:FF:FF) are also technically multicast (LSB of first byte = 1)

Without the !is_broadcast check, broadcast packets would be counted TWICE:


+ As broadcast (correct)

+ As multicast (incorrect)


Broadcast: 0xFF = 1111 1111
nibble1 nibble2
(1111) (1111)
LSB of whole byte = 1

OSPF Multicast: 0x01 = 0000 0001
nibble1 nibble2
(0000) (0001)
LSB of whole byte = 1

Unicast: 0x00 = 0000 0000
nibble1 nibble2
(0000) (0000)
LSB of whole byte = 0


The detection logic:


1. Check: Is it broadcast? (all 0xFF) → If yes, broadcast_detected = 1

2. Check: Is it multicast AND NOT broadcast? → If yes, multicast_detected = 1


Fun fact here that you could reconfigure certain OSes or VMware to use these reserved MAC addresses with regular unicast IPs, modern OSes will not let you:

ifconfig br0 hw ether 01:00:5E:01:01:01
SIOCSIFHWADDR: Cannot assign requested address


ethernet0.addressType = "static"
ethernet0.address = "01:00:5E:01:01:01"
ethernet0.checkMACAddress = "false"


However if you do that most machines will even refuse to respond to your box for a regular ARP resolution request (aka you will never get their IP address unless you statically add them).

23:19:19.521033 01:00:5e:01:01:01 :> ff:ff:ff:ff:ff:ff, ethertype ARP (0x0806), length 60: Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.1 tell 192.168.1.2, length 46

"Hey 192.168.1.1, I'm at 01:00:5e:01:01:01!"

But 192.168.1.1 thinks: "That's a multicast MAC... I won't reply to that!"


Other-IP


These are IP protocol packets that aren't TCP/UDP/ICMP. IP has protocol numbers beyond just those three:

Common "Other IP" protocols you're seeing:

  1. 2 = IGMP (Internet Group Management Protocol) - multicast group membership
  2. 50 = ESP (Encapsulating Security Payload) - IPsec VPN traffic
  3. 51 = AH (Authentication Header) - IPsec VPN traffic
  4. 88 = EIGRP (Cisco routing protocol)
  5. 89 = OSPF (Open Shortest Path First) routing
  6. 103 = PIM (Protocol Independent Multicast)
  7. 115 = L2TP (Layer 2 Tunneling Protocol)
  8. 132 = SCTP (Stream Control Transmission Protocol)

Visualization idea: Each of these could trigger a different colored LED!

The full list is available here: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml#Robert_W_Scheifler


Non-IP


Now why would you want to monitor non-IP packets? Folks like me who still run weird stuff like Novell Netware servers with IPX this is actually very useful :D Will come handy for anomaly detection, the de-facto Wireshark filter for these packets is:


!(ip) && !(arp) && eth.dst[0] == 0x02


We're already examining every packet's Ethernet header. The EtherType check is just one more integer comparison per packet. No additional system calls or I/O operations needed.

It would add maybe 1-2 CPU instructions per packet - completely insignificant compared to everything else the code is already doing therefore it's added to the project.

In case you happen to have none in your network, it's easy to generate with Scapy (Python):

# Generate custom Ethernet frame (non-IP)
from scapy.all import *
ether_frame = Ether(dst="ff:ff:ff:ff:ff:ff", src="00:11:22:33:44:55")/Raw(load="test")
sendp(ether_frame, iface="eth0")

You will see this with tcpdump:

23:36:32.959761 00:11:22:33:44:55 (oui Unknown) > Broadcast, ethertype Loopback (0x9000), length 60: Loopback, skipCount 25972 (bogus) (invalid)


The non-IP LED will light up for:

✅ STP/RSTP frames (0x4242)

✅ LLDP packets (0x88cc)

✅ CDP packets (0x2000)

✅ LACP (0x8809)

✅ VLAN tags (0x8100)

✅ And any other proprietary layer 2 protocols


I leave that as an exercise for the reader to implement separation for these. From this point it's not hard at all (1 LED is fine for me for them :)


// Add to your BPF detection
uint16_t ether_type = ntohs(eth->ether_type);
if (ether_type == 0x4242) { // STP/RSTP
stp_detected = 1;
}
else if (ether_type == 0x88cc) { // LLDP
lldp_detected = 1;
}
else if (ether_type == 0x2000) { // CDP
cdp_detected = 1;
}


Why there is no real-time processing implemented


The individual status LEDs are the types of events which might be desirable to be real time (blink the LEDs as soon as the packets come in).


Here is why this was not implemented:


If there would be no delay in the main loop, the LCD would show garbage, the VU meters would barely twitch, and the whole system would act possessed.

  1. The LCD Command Traffic Jam HD44780 LCD screen is like an old-school worker - it needs about 40ms to process each command. When the code calls lcd.clear(), lcd.setCursor(), and lcd.print() in rapid fire without delays, it's like throwing a stack of paperwork at someone who's still processing the first page. The LCD controller gets overwhelmed and starts mixing up commands, which is why we see garbled text and missing characters.
  2. PWM Timer Interruption The VU meters use PWM (Pulse Width Modulation) which relies on precise hardware timers. When the code calls analogWrite() too frequently, it is essentially resetting the timer before it completes its cycle. Imagine trying to count to 100 but someone keeps shouting "start over!" every few seconds - that's what would happen to the PWM signals. The result? The VU meters barely move or flicker erratically instead of showing smooth level changes.
  3. Serial Buffer Starvation The serialEvent() function interrupts the main loop to read incoming data. Without the delay in the main loop it would run thousands of times per second, constantly checking if stringComplete is true. This hyperactive checking prevents the serial buffer from properly filling up with complete data packets.


If you want to over-perfect it with real-time LED blinkies there are a couple of ways to implement this:


1) Use a second Arduino on another USB port just put all the LEDs and LED strips on that and send commands for that separately real-time

2) Use another hardware serial on the Mega (it has 4!) and do the same (this will also block on heavy traffic since the MCU has to switch between the ports)

3) Implement a second type of DATA push e.g., call it LED,1,0,1 and add code that the Mega separates this from the normal data flow (you can send it only in case of change). This is kind of the poor man's choice as you won't need extra hardware but you do need to implement some kind of locking mechanism in the C code (if the LED updater is sending hold on with the regular DATA send) this you can consider as an "interrupt" basically, the more packets come in the more interrupted your LCD display will be ;) Not recommended.

MultiCast Tree

starting.jpg

Difficulty: hard


We have already exhausted the local upload/download bandwidth, TCP/UDP/ICMP flow, multicast/broadcast, non-IP, host count on the current network ... so what else is there to visualize.


Again there is just about a plethora of stuff what you can monitor on the network, this is a generic build what everybody can customize to fit their own needs.


Here is what I was thinking: my network is divided into 4 segments, all of these segments are connected to the Internet with Asus routers. You can start bashing on Asus right away for providing a small busy box crippled 2.6 kernel (yes 2.6 in 2026 because it works) based locked down device with a 50MB writable yaffs partition. Why on earth even bother with these? The answer is: WiFi. You can convert any X86 box into an OPNsense router; this is shown in this build too but the BSD WiFi support remains more than poor.


The main historical issue hasn't been the BSD kernels themselves, but the driver support for Wi-Fi chipsets. This stems from two factors:

  1. Linux-Centric Hardware Vendors: Many chipset manufacturers (like Intel, Qualcomm/Atheros, Broadcom) primarily develop and release drivers for Linux, as it has the largest market share in client devices and is often a requirement for hardware certification (e.g., Windows Subsystem for Linux). The BSD communities are then left to either create their own drivers from scratch or "port" the Linux drivers.
  2. Binary Blob Firmware: Even with an open-source driver, the low-level microcode that runs on the Wi-Fi chip itself (the firmware) is often a proprietary "blob." BSD projects are generally willing to distribute these blobs for users to download, but integrating them can be a legal and technical process.


Writing some tool to monitor these Asus routers was always on my list, making it universal (Linux 2.6+ kernel) with Zabbix integration and portability to other router platforms was a great achievement by itself.


The pros behind Asus: decent responsive engineering team, updates for fairly old 5+ year devices, vDSL integration (for those who may concern and live in areas with this being the only option), good performance and extreme reliability regardless of being a SOHO product. Next to the WRT54Gs I haven't had many other SOHO brands being able to reach over the 1 year continuous uptime mark. Asus certainly deserved to invest some work into it and it turns out cross-compiling for this platform is not difficult at all!



So what key metrics do we want to collect from the edge routers:


1) Flow through connections (TCP/UDP/ICMP)

2) Flow through rate (RX/TX on the WAN interface)

3) Since this is a WiFi router the number of connected WiFi clients can be interesting (the hosts on the network are already taken care of by other scripts) -> to be done


For these I developed a custom tool (find it on Gitlab) called FluxStream what is pulling the TCP/UDP/ICMP connections from /proc/net/nf_conntrack. A good trick here is to ignore any connections going directly to the router (SSH/Web admin) this can easily be in the hundreds as the web admin opens a lot of TCP connections. We don't need to count these.


Getting those counters was easy, which didn't make sense that the traffic counters like the wan interface did not operate properly.

route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
192.168.1.1 0.0.0.0 255.255.255.255 UH 0 0 0 nas10_0


Examining the loaded kernel modules reveals what's going on here


hw_nat 49775 0 - Live 0xc1d12000 (P)


hw_nat means Hardware NAT! This is the key! The traffic is being hardware-accelerated through the switch chip, bypassing normal Linux counters.


The hw_nat module is a vendor-specific, proprietary driver primarily associated with MediaTek (formerly Ralink) System on a Chip (SoC) devices, typically found in routers and access points. It is not part of the mainline, generic Linux kernel source code.


Hardware Network Address Translation (HW_NAT): The module is designed to offload Network Address Translation (NAT) and routing tasks from the main CPU to a dedicated hardware engine within the MediaTek SoC. This significantly improves network throughput, especially for gigabit internet connections, by allowing the hardware to handle data flow directly (often referred to as "flow offload"). Proprietary Nature: The source code for the original hw_nat module is generally not open-source and is provided by the chip vendor (MediaTek/Ralink) as part of a Software Development Kit (SDK).


Hardware Acceleration Bypass: The ASUS router uses hw_nat (hardware NAT) module that offloads traffic processing to the switch chip. This traffic bypasses the Linux kernel's normal network stack, so:


  1. /proc/net/dev counters showed minimal traffic
  2. /sys/class/net/nas10_0/statistics/ showed only CPU-handled traffic
  3. tc counters showed some but not all traffic
  4. Interface counters were essentially "lying" - showing 1-2% of real traffic


In this case the hardware gigabit switches traffic counters were used /proc/tc3162/gsw_stats to get the RX/TX data however I wanted this code to be generic so everybody can run it on whatever Linux v2.x/3.x/4.x/5.x/6.x generic routers therefore an option is added for this use the ASUS_HARDWARE_COUNTERS if you have an Asus router, GENERIC_ROUTER otherwise.

For other Zabbix fans here is how to monitor your router(s):

UserParameter=custom.routerstats.tcp[*],echo "" | nc -w 3 $1 9999 2>/dev/null | cut -d, -f1
UserParameter=custom.routerstats.udp[*],echo "" | nc -w 3 $1 9999 2>/dev/null | cut -d, -f2
UserParameter=custom.routerstats.icmp[*],echo "" | nc -w 3 $1 9999 2>/dev/null | cut -d, -f3
UserParameter=custom.routerstats.rx[*],echo "" | nc -w 3 $1 9999 2>/dev/null | cut -d, -f4
UserParameter=custom.routerstats.tx[*],echo "" | nc -w 3 $1 9999 2>/dev/null | cut -d, -f5


Even if you don't want to do this whole build just looked for a way to incorporate your Asus router into your monitoring infrastructure this Instructable already helped you. Zabbix itself is not supported on these routers directly and I would not attempt to build it either, just compile/run fluxstream and use another server/raspberry pi to poll the data from the router(s).


There are 2 more caveats for the Asus routers:


1) There is no SFTP (aka scp) so the easiest way to transfer your file to the routers is with regular ftp (if you look into my Makefile this is done for you automatically after the build just fill out the ftp credentials, make upload):

ftpget -u user -p pass <ftpserverip> /yaffs/fluxstream /up/fluxstream

2) There is no startup hook for custom binaries (at least for this brand I haven't find one yet), therefore after each reboot you need to start the server manually :(



So now we have all this data collected from the nodes, what is the most efficient way to transmit it to the multicast tree? Definitely not polling with TCP 3-way handshake where the BSD box would connect to each individual edge routers and ask them "please give me data" then gracefully close the connection. For this purpose UDP is perfect, the edge nodes will send the data to the tree, and we don't care about losing occasional packets since we get fresh data every second. With this method one edge node generates only ~8MB of traffic daily.


Per UDP packet:


Data: TCP,UDP,ICMP,RX_MBPS,TX_MBPS\n = ~20-25 bytes (e.g., "12,5,1,34.23,1.48\n" = 20 bytes)

UDP header: 8 bytes

IP header: 20 bytes

Ethernet header: 14 bytes

Total per packet: ~62-67 bytes

Per second: 62-67 bytes

Per minute: 65 × 60 = 3,900 bytes ≈ 3.8 KB

Per hour: 3.8 KB × 60 = 228 KB

Per day: 228 KB × 24 = 5,472 KB ≈ 5.3 MB per day

For 4 edge nodes: 5.3 MB × 4 = ~21.2 MB total daily traffic


Again here I tried very hard to incorporate MultiCast into this build but nope, does not make sense using it at all since there is 1 receiver 4 senders. If it would be the other way around when having multiple of these trees on different locations then it would make some sense but flooding a network with useless broadcast/multicast is just stupid.


Ok now we got the data how to display it on an LED strip? I use a convenient WS2812 LED control library I used in Ironforge and plethora of other projects (FastLED) to make life easier. The data what is coming in from the edge routers is:


seq 1> RX: 50 | TX: 50 | TCP: 512 | UDP: 200 | ICMP: 1
seq 2> RX: 10 | TX: 20 | TCP: 900 | UDP: 111 | ICMP: 1


You can see multiple problems here right away, the connection numbers can change in seconds and change a lot they will. So what are we going to do if the TCP connections jump from 512 to 900 light up 5 LEDs? No, some clever tricks need to be employed here to create the perfect visuals.


First of all let's decide on an animation: a slow snail like the Lighthouse effect in WLED will be fine. In this case whatever pixel is pushed into a FIFO it will be displayed on a LED, this little snail will have a 1 second heartbeat (every time it gets data from the OPNsense box), it will crawl its way down from top to bottom then all old data are discarded. So far I hope it's clear as we're going to complicate this further.


If you look at the previous example TCP connections increased from 512 to 900, UDP decreased from 200 to 111 and ICMP stayed stagnant, we have 1 pixel to display this what to do :|


We want to display the dominant traffic for this we need to calculate the delta change from the previous readout and the current one, in this case that is +388 TCP, -89 UDP, 0 ICMP. The winner will be where the delta is the largest, in case multiple protocols have the same delta, the code uses this order: ICMP → TCP → UDP as tie-breaker. All 4 LED strips have their own FIFO queues where the "actual packet" data is pushed in.


So far this is how it works:


  1. Each time new data arrives from OPNsense, the code calculates deltas
  2. It determines which protocol had the highest increase (positive delta)
  3. The appropriate color is added to the FIFO
  4. The entire strip X is updated to show the last 25 protocol events
  5. Old events shift to the right as new ones come in


LED colors:


  1. ICMP: Red (ping requests, network diagnostics)
  2. TCP: Blue (web browsing, file transfers, secure connections)
  3. UDP: Green (video streaming, VoIP, DNS queries)


More colors could be displayed on the local led strip (eg different color leds for multicast broadcast) but since we are only getting the 3 main protocols from the edge routers I decided just serialize them and not to bother.


Ok now we have 2 more key metrics we can use to manipulate the display: rx and tx flow through the routers. Originally my idea was here to make the LED strips run faster based on the speedup/slowdown of network traffic from the edge routers however this would require real-time data processing and we are on a 1 sec tick based system, so what other property could be used to display the traffic flow? The variable brightness of the LEDs!


Heavy traffic = brighter strip

Light traffic = dimmer strip


Simple? No actually if you would do that then it would be more of a Christmas tree instead of a Network Traffic Visualizer dimming back and forth every second.

For this the good old technique comes to our help what I use in a plethora of my Arduino projects: smoothing

In this case if there is a traffic increase, UP or DOWN does not matter, we average this over a 10 tick period and make a decision to adjust the brightness based on that.

The Test

multicast-tree

The test scripts will be uploaded to the git. Here are the tests performed:

  1. RX/TX test are done by iperf to another server in the DMZ on gbit
  2. TCP/UDP/ICMP simple nmap
  3. Broadcast, multicast, non-ip, packets generated by Scapy
  4. IPX with Scapy and directly from Novell 6.5 for fun

Closure and ToDo List

HpJjZ.jpg

Implement IDS functionality:


A good question to consider even before getting into this part: are network based IDS devices such as Snort still relevant to use with almost every TCP stream these days getting SSL encrypted therefore one might say that endpoint security products which can see into packets before that would happen and analyze it are more relevant, nevertheless having an extra layer of security can never hurt.


When it comes to open source IDSes there are 2 options: Suricata and Snort, I found Suricata more modern and ready to be available (at the time of writing Snort 3 is not available as binary package in FreeBSD yet).


Enumerating even more protocol info, wifi clients etc and sending it over via UDP.


Adding an mp3 player module and speaker plus additional 8 channel relay board based on network events.


The code will be added to the Gitlab, for now you can find it attached as a reminder here is how and what systems to build them on:

  1. Multicast-tree-arduino => Arduino IDE 2.x install the required libraries
  2. Multicast-tree => FreeBSD 14.1 64-bit (this has to correspond to your OPNSense environment)
  3. Fluxstream => Either use the Asus toolchain to crossbuild it for MIPS or you can use the GENERIC_ROUTER and build it to x86/64 for regular Linux machines


Sadly the case did not finish by the time of writing I might make an update when the whole circuitry is assembled ;)