A lightweight, standards-compliant Python daemon that acts as an IGMPv2 Querier for networks with IGMP snooping but no active querier.
Many "Smart Managed" switches (like TP-Link Easy Smart TL-SG108E/TL-SG116E, Netgear Plus, or similar consumer-grade managed switches) support IGMP Snooping to prevent multicast flooding. However, they often lack an active IGMP Querier.
Without a Querier, the switch snoops the initial "Join" request from a client, opens the port, but then closes it a few minutes later because no "General Queries" are sent to refresh the membership. This causes multicast streams to drop out after ~260 seconds (the typical IGMP snooping timeout).
This daemon was specifically developed to support ka9q-radio (Phil Karn's software-defined radio package). KA9Q-radio uses IP multicast extensively to distribute audio and IQ data streams across a local network:
- radiod transmits SDR audio/data on multicast groups
- Clients (like
monitor,control, or custom applications) join these multicast groups to receive streams - On networks with IGMP snooping but no querier, clients receive data for ~4 minutes, then the stream "drops out" as the switch stops forwarding multicast traffic
This querier daemon keeps the multicast group memberships alive, ensuring uninterrupted reception of ka9q-radio streams.
You likely need this daemon if:
- Consumer-grade managed switch with IGMP snooping enabled (TP-Link Easy Smart, Netgear Plus, etc.)
- No enterprise router acting as IGMP querier on the network
- Multicast applications like ka9q-radio, IPTV, or other streaming services
- Symptoms: Streams work initially but drop after 2-5 minutes
This daemon solves that problem by periodically sending IGMP General Queries, keeping multicast group memberships alive on your network.
IGMP (Internet Group Management Protocol) is used by hosts to report their multicast group memberships to neighboring routers and switches. When a host wants to receive a multicast stream, it sends an IGMP Join (Membership Report) message. Switches with IGMP Snooping enabled listen to these messages to learn which ports need multicast traffic.
┌─────────────┐ IGMP Join ┌─────────────┐ Multicast ┌─────────────┐
│ Client │ ─────────────────► │ Switch │ ◄──────────────── │ Source │
│ (receiver) │ │ (snooping) │ │ (radiod) │
└─────────────┘ └─────────────┘ └─────────────┘
▲ │
│ Multicast Data │
└──────────────────────────────────┘
An IGMP Querier periodically sends General Query messages to the all-hosts multicast address (224.0.0.1). These queries prompt hosts to re-report their group memberships. Without periodic queries:
- The switch's IGMP snooping table entries expire (typically after 260 seconds)
- The switch stops forwarding multicast traffic to ports that previously requested it
- Multicast streams appear to "drop out" even though the source is still transmitting
┌─────────────┐ ┌─────────────┐
│ Querier │ ── IGMP Query ───► │ Switch │ ──► All Hosts (224.0.0.1)
│ (this app) │ (periodic) │ │
└─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐
│ │ Client │ ── IGMP Report ──►
│ │ │ (refreshes membership)
│ └─────────────┘
│
└── Keeps snooping table entries alive!
When multiple queriers exist on a network, they elect a single Master Querier using the following rules:
- All queriers listen for IGMP Query messages from other queriers
- When a querier receives a query from a lower IP address, it backs off and becomes passive
- The querier with the lowest IP address wins and becomes the Master
- If the Master stops sending queries (timeout), other queriers will take over
This daemon implements this election logic, so you can safely run it on multiple machines for redundancy.
The daemon sends IGMPv2 General Query packets with:
- Type:
0x11(Membership Query) - Max Response Time: 100 (10 seconds)
- Group Address:
0.0.0.0(General Query - all groups) - Checksum: Dynamically calculated per RFC 1071
- Router Alert Option: RFC 2113 IP option for proper routing
Per RFC 2236, the daemon sends a burst of 3 rapid queries at startup (5 seconds apart) to quickly establish multicast group state, then settles into the normal query interval.
Per RFC 3376, query intervals include random jitter (up to 25%) to prevent synchronization issues when multiple queriers or hosts respond simultaneously.
- RFC 2236 Compliant: Implements election logic (lowest IP wins) and startup query burst
- RFC 2113 Router Alert: Includes IP Router Alert option for proper IGMP routing
- RFC 3376 Jitter: Randomized query intervals prevent network synchronization
- IGMPv1/v2/v3 Compatible: Detects and responds to all IGMP query versions for election
- Robust & Recoverable: Automatic socket recovery, IP change detection, and listener health monitoring
- Graceful Shutdown: Proper signal handling (SIGTERM/SIGINT) with statistics on exit
- Security Hardened: Systemd service runs with minimal privileges (CAP_NET_RAW only)
- Configurable: CLI options for query interval, timeout, and verbose logging
- Thread-Safe: Proper synchronization for multi-threaded operation
- Python 3.6+ (uses f-strings and type hints)
- Linux with raw socket support
- CAP_NET_RAW capability or root privileges (required for raw IGMP sockets)
No external Python packages are required—only the standard library.
The querier should be deployed on a host that:
- Is always on - The querier must run continuously to keep multicast alive
- Has a stable, low IP address - Lower IPs win elections (e.g.,
.1or.2) - Is on the same Layer 2 network as multicast sources and receivers
| Location | Pros | Cons |
|---|---|---|
| Dedicated server/Pi | Always on, stable IP | Extra hardware |
| NAS/Home server | Usually always on | May have higher IP |
| Same host as radiod | Convenient, ensures querier runs when needed | Single point of failure |
| Router (if Linux-based) | Central, always on, often lowest IP | May require custom setup |
┌─────────────────────────────────────────┐
│ Home Network (LAN) │
│ │
┌─────────────┐ │ ┌─────────────┐ ┌─────────────┐ │
│ Router │─────┼──│ Switch │────│ radiod │ │
│ 192.168.1.1 │ │ │ (snooping) │ │ 192.168.1.10│ │
└─────────────┘ │ └──────┬──────┘ └─────────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ ┌──┴──┐ ┌──┴──┐ │
│ │Pi/ │ │Client│ │
│ │NAS │ │ PC │ │
│ │.5 │ │ .20 │ │
│ └─────┘ └──────┘ │
│ ▲ │
│ │ │
│ └── Run querier here (always on, │
│ low IP, same L2 segment) │
└─────────────────────────────────────────┘
For redundancy, run the querier on multiple hosts. The election mechanism ensures only one is active:
# Host A (192.168.1.5) - Will be master (lower IP)
sudo systemctl enable --now igmp-querier
# Host B (192.168.1.10) - Backup (higher IP, will take over if A fails)
sudo systemctl enable --now igmp-querier- Python 3.6+ installed
- Linux with systemd
- Root access (for installation and raw sockets)
# Clone the repository
git clone https://github.com/mijahauan/igmp-querier.git
cd igmp-querier
# Run the install script
sudo ./install.shThe install script will:
- Detect available network interfaces
- Let you select which interface to use
- Install the script and systemd service
- Enable and start the service automatically
If you prefer to install manually:
# Copy the script to a system location
sudo cp igmp_querier.py /usr/local/bin/
sudo chmod +x /usr/local/bin/igmp_querier.py
# Edit the service file to specify your interface
nano igmp-querier.service
# Change 'enp1s0' to your interface name on the ExecStart line
# Install and enable the systemd service
sudo cp igmp-querier.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now igmp-querier# Check service status
sudo systemctl status igmp-querier
# Watch live logs
journalctl -u igmp-querier -fYou should see output like:
Starting IGMP Querier on enp1s0 (192.168.1.10)
Query interval: 60s, Competitor timeout: 255s
[Startup] Sending 3 rapid queries...
[Listener] Election listener started.
# Using the uninstall script
sudo ./uninstall.sh
# Or manually:
sudo systemctl disable --now igmp-querier
sudo rm /etc/systemd/system/igmp-querier.service
sudo rm /usr/local/bin/igmp_querier.py
sudo systemctl daemon-reloadusage: igmp_querier.py [-h] -i INTERFACE [-q QUERY_INTERVAL] [-t TIMEOUT] [-v]
Simple IGMP Querier Daemon
options:
-h, --help show this help message and exit
-i INTERFACE, --interface INTERFACE
Network interface to bind to (e.g., eth0, enp1s0)
-q QUERY_INTERVAL, --query-interval QUERY_INTERVAL
Seconds between IGMP queries (default: 60)
-t TIMEOUT, --timeout TIMEOUT
Seconds before assuming competitor querier is gone (default: 255)
-v, --verbose Enable verbose (debug) logging
# Basic usage (requires root or CAP_NET_RAW)
sudo python3 /usr/local/bin/igmp_querier.py -i eth0
# Custom query interval (30 seconds) and timeout (120 seconds)
sudo python3 /usr/local/bin/igmp_querier.py -i enp1s0 -q 30 -t 120
# Verbose mode for debugging (shows each query sent)
sudo python3 /usr/local/bin/igmp_querier.py -i eth0 -vFor high availability, you can run this daemon on multiple machines on the same LAN. They will automatically elect a Master Querier (lowest IP wins). If the Master fails, another will take over within the timeout period.
| Parameter | Default | Description |
|---|---|---|
--query-interval |
60s | How often to send IGMP queries. RFC default is 125s, but 60s is safer for home networks. |
--timeout |
255s | How long to wait before assuming a competing querier is gone. Should be ~2x the query interval plus buffer. |
To customize the service (e.g., change the interface or add options), edit the service file:
sudo systemctl edit igmp-querier --fullOr create an override:
sudo systemctl edit igmp-querierAdd:
[Service]
ExecStart=
ExecStart=/usr/bin/python3 /usr/local/bin/igmp_querier.py --interface br0 --query-interval 30 --verboseThe systemd service file is configured with comprehensive security hardening:
| Setting | Purpose |
|---|---|
User=nobody |
Runs as unprivileged user |
CapabilityBoundingSet=CAP_NET_RAW |
Only allows raw socket capability |
AmbientCapabilities=CAP_NET_RAW |
Grants raw socket capability to non-root user |
NoNewPrivileges=true |
Prevents privilege escalation |
ProtectSystem=strict |
Read-only access to /usr, /boot, /efi |
ProtectHome=true |
No access to /home, /root, /run/user |
PrivateTmp=true |
Isolated /tmp directory |
ProtectKernelTunables=true |
No access to /proc/sys, /sys |
MemoryDenyWriteExecute=true |
Prevents code injection attacks |
The daemon requires raw socket access. Either run as root or grant the capability:
# Option 1: Run as root
sudo python3 igmp_querier.py -i eth0
# Option 2: Grant capability to Python (not recommended for security)
sudo setcap cap_net_raw+ep /usr/bin/python3
# Option 3: Use the systemd service (recommended)
sudo systemctl start igmp-querierVerify the interface name:
ip link show-
Check the querier is running:
sudo systemctl status igmp-querier
-
Verify queries are being sent (requires tcpdump):
sudo tcpdump -i eth0 igmp
You should see periodic "igmp query" packets.
-
Check your switch settings:
- Ensure IGMP Snooping is enabled
- Some switches have a "Querier" setting that may conflict—disable it if this daemon is running
-
Check for competing queriers: Run with
-vflag and look for "LOST to lower IP" messages.
| RFC | Description | Implementation |
|---|---|---|
| RFC 1112 | Host Extensions for IP Multicasting | Basic multicast support |
| RFC 1071 | Computing the Internet Checksum | IGMP checksum calculation |
| RFC 2113 | IP Router Alert Option | Included in outgoing packets |
| RFC 2236 | IGMPv2 | Query format, election logic, startup burst |
| RFC 3376 | IGMPv3 | Query detection for election, jitter |
To verify the querier is working, capture IGMP traffic:
# Watch all IGMP traffic
sudo tcpdump -i eth0 igmp -vv
# Expected output (query from querier):
# IP (tos 0xc0, ttl 1, id 0, offset 0, flags [DF], proto IGMP (2), length 32, options (RA))
# 192.168.1.5 > 224.0.0.1: igmp query v2
# Expected output (report from client):
# IP (tos 0xc0, ttl 1, id 0, offset 0, flags [DF], proto IGMP (2), length 32, options (RA))
# 192.168.1.20 > 239.1.2.3: igmp v2 report 239.1.2.3For TP-Link Easy Smart switches (TL-SG108E, TL-SG116E, etc.):
- Enable IGMP Snooping:
Switching→IGMP Snooping→ Enable - Disable built-in Querier: If the switch has a querier option, disable it (this daemon is more reliable)
- Set appropriate timeout: Default 260s is fine; ensure query interval is less than half this value
-
Verify querier is running:
sudo systemctl status igmp-querier journalctl -u igmp-querier -f
-
Check for IGMP traffic:
sudo tcpdump -i eth0 igmp
-
Verify multicast group membership on client:
netstat -g # or ip maddr show -
Check switch snooping table: (if accessible via web UI)
- Look for multicast group entries
- Verify ports are correctly associated
-
Test with ka9q-radio:
# On client, monitor a stream monitor -v # Stream should remain stable beyond 260 seconds
MIT License
Contributions are welcome! Please ensure any changes:
- Maintain RFC compliance
- Include appropriate error handling
- Update documentation as needed
- Test on actual hardware with IGMP snooping
- Phil Karn (KA9Q) for ka9q-radio and the motivation for this project
- The IETF for the IGMP RFCs that define the protocol