computer made a thing
This commit is contained in:
commit
c7d2470f22
7 changed files with 651 additions and 0 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
45
.forgejo/workflows/container.yaml
Normal file
45
.forgejo/workflows/container.yaml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: Build container
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 12 * * *'
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
enable-email-notifications: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
|
||||||
|
REGISTRY_URL: git.sb20.xengi.de
|
||||||
|
REGISTRY_TAG: latest
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: buildkit
|
||||||
|
name: Build container
|
||||||
|
steps:
|
||||||
|
- name: Check out the repository
|
||||||
|
run: git clone ${{ env.FORGEJO_SERVER_URL }}/${{ env.FORGEJO_REPOSITORY }} .
|
||||||
|
- name: Setup buildkit
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
echo "{\"auths\":{\"${{ env.REGISTRY_URL }}\":{\"username\":\"${{ env.FORGEJO_REPOSITORY_OWNER }}\",\"password\":\"${{ secrets.REGISTRY_TOKEN }}\"}}}" > ~/.docker/config.json
|
||||||
|
- name: Build container
|
||||||
|
run: |
|
||||||
|
export CREATED=$(date -Iseconds)
|
||||||
|
sed -i "s/%%CREATED%%/$CREATED/" Dockerfile
|
||||||
|
sed -i "s/%%COMMIT%%/${{ env.FORGEJO_SHA }}/" Dockerfile
|
||||||
|
buildctl-daemonless.sh build \
|
||||||
|
--frontend dockerfile.v0 \
|
||||||
|
--local context=./ \
|
||||||
|
--local dockerfile=./ \
|
||||||
|
--output type=image,name=${{ env.REGISTRY_URL }}/${{ env.FORGEJO_REPOSITORY }}:${{ env.REGISTRY_TAG }},push=true
|
||||||
|
- name: Send notification
|
||||||
|
if: ${{ failure() }}
|
||||||
|
run: |
|
||||||
|
curl -s \
|
||||||
|
--form-string "token=${{ secrets.PUSHOVER_API_TOKEN }}" \
|
||||||
|
--form-string "user=${{ secrets.PUSHOVER_USER_KEY }}" \
|
||||||
|
--form-string "message=Building mdns-reflector container failed." \
|
||||||
|
https://api.pushover.net/1/messages.json
|
||||||
|
|
||||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
FROM docker.io/alpine:latest AS builder
|
||||||
|
|
||||||
|
RUN apk add --no-cache build-base linux-headers
|
||||||
|
|
||||||
|
COPY mdns-repeater.c /src/
|
||||||
|
RUN gcc -static -O2 -o /mdns-repeater /src/mdns-repeater.c -lpthread
|
||||||
|
|
||||||
|
FROM docker.io/alpine:latest as runtime
|
||||||
|
|
||||||
|
COPY --from=builder /mdns-repeater /usr/local/bin/
|
||||||
|
COPY --chmod=755 entrypoint.sh /
|
||||||
|
COPY --chmod=755 healthcheck.sh /usr/local/bin/
|
||||||
|
|
||||||
|
RUN apk add --no-cache libcap iproute2 \
|
||||||
|
&& adduser -D -s /sbin/nologin mdns \
|
||||||
|
&& setcap 'cap_net_raw,cap_net_bind_service=+ep' /usr/local/bin/mdns-repeater \
|
||||||
|
&& mkdir -p /tmp \
|
||||||
|
&& chown mdns:mdns /tmp
|
||||||
|
|
||||||
|
USER mdns
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=5s --retries=3 \
|
||||||
|
CMD /usr/local/bin/healthcheck.sh
|
||||||
|
|
||||||
|
EXPOSE 5353/udp
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
|
||||||
103
README.md
Normal file
103
README.md
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# mdns-reflector — IPv4 + IPv6 mDNS reflector for MikroTik RouterOS
|
||||||
|
|
||||||
|
A lightweight mDNS reflector (multicast DNS repeater) designed to run as a container on MikroTik RouterOS 7.x. It relays mDNS packets (port 5353, groups `224.0.0.251` and `ff02::fb`) between all specified interfaces — both IPv4 **and** IPv6 — filling the gap where RouterOS' built-in `/ip dns set mdns-repeat-ifaces=` only supports IPv4.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- Opens one IPv4 and one IPv6 UDP socket **per interface**, bound to that interface via `SO_BINDTODEVICE`
|
||||||
|
- Joins the mDNS multicast groups on each interface
|
||||||
|
- Spawns one reader thread per interface; packets received on any interface are forwarded to every other interface
|
||||||
|
- Writes a health heartbeat to `/tmp/mdns-health` every 10 seconds
|
||||||
|
- Runs as non-root user `mdns` with `cap_net_raw,cap_net_bind_service` capabilities
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
podman build -t mdns-reflector .
|
||||||
|
|
||||||
|
# Run (auto-detect non-loopback interfaces)
|
||||||
|
podman run --rm --network host mdns-reflector
|
||||||
|
|
||||||
|
# Run with explicit interfaces
|
||||||
|
podman run --rm --network host \
|
||||||
|
-e INTERFACES="ether1 bridge" \
|
||||||
|
mdns-reflector
|
||||||
|
```
|
||||||
|
|
||||||
|
## MikroTik RouterOS container setup
|
||||||
|
|
||||||
|
RB5009 running RouterOS 7.x:
|
||||||
|
|
||||||
|
```routeros
|
||||||
|
/container config set registry-url=https://git.sb20.xengi.de
|
||||||
|
|
||||||
|
/container add \
|
||||||
|
remote-image=git.sb20.xengi.de/xengi/mdns-reflector:latest \
|
||||||
|
interface=bridge \
|
||||||
|
root-dir=mdns-reflector \
|
||||||
|
envlist=mdns-env \
|
||||||
|
start-on-boot=yes
|
||||||
|
|
||||||
|
/container env add name=mdns-env key=INTERFACES value="bridge iot private"
|
||||||
|
|
||||||
|
/container start [find where image~"mdns-reflector"]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** MikroTik containers require `--network host`-like behavior, which you get by attaching the container to a single interface (usually your LAN bridge). The `INTERFACES` env var tells the reflector which interfaces *inside* the host to bridge mDNS across.
|
||||||
|
|
||||||
|
### Unprivileged container considerations
|
||||||
|
|
||||||
|
> This container already runs as non-root user `mdns`.
|
||||||
|
> On RouterOS, ensure the container has sufficient privileges:
|
||||||
|
>
|
||||||
|
> ```routeros
|
||||||
|
> /container set [find where image~"mdns-reflector"] \
|
||||||
|
> nesting=yes \
|
||||||
|
> hostname=mdns-reflector
|
||||||
|
> ```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|---------------|-----------------------|--------------------------------------------------|
|
||||||
|
| `INTERFACES` | all non-lo interfaces | Space-separated list of interface names to bridge |
|
||||||
|
|
||||||
|
## Health check
|
||||||
|
|
||||||
|
The container exposes a Docker `HEALTHCHECK` driven by `healthcheck.sh`. It verifies:
|
||||||
|
|
||||||
|
1. Health heartbeat file exists and is < 30 seconds old
|
||||||
|
2. `mdns-repeater` process is running
|
||||||
|
3. At least one bound UDP socket exists on port 5353
|
||||||
|
|
||||||
|
Then on RouterOS, pull the updated image:
|
||||||
|
|
||||||
|
```routeros
|
||||||
|
/container pull [find where image~"mdns-reflector"]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Multi-stage build (produces ~3 MB image)
|
||||||
|
podman build -t mdns-reflector .
|
||||||
|
|
||||||
|
# Or compile the C binary directly
|
||||||
|
gcc -static -O2 -o mdns-repeater mdns-repeater.c -lpthread
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---------------------|-----------------------------------------------|
|
||||||
|
| `mdns-repeater.c` | IPv4/IPv6 mDNS reflector |
|
||||||
|
| `Dockerfile` | Multi-stage Alpine build |
|
||||||
|
| `entrypoint.sh` | Interface detection & launcher |
|
||||||
|
| `healthcheck.sh` | Docker HEALTHCHECK probe |
|
||||||
|
| `.dockerignore` | Reduces build context size |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
24
entrypoint.sh
Executable file
24
entrypoint.sh
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# entrypoint.sh — mdns-reflector container entrypoint
|
||||||
|
#
|
||||||
|
# Expects interface names via env var INTERFACES (space-separated),
|
||||||
|
# or falls back to all non-loopback interfaces.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ -n "$INTERFACES" ]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
set -- $INTERFACES
|
||||||
|
else
|
||||||
|
# Auto-detect: all 'up' interfaces except loopback
|
||||||
|
set -- $(ip -o link show up | awk -F': ' '!/lo/{print $2}' | cut -d@ -f1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
echo "ERROR: No interfaces found. Set INTERFACES env var or attach interfaces."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[entrypoint] Starting mdns-repeater on: $*"
|
||||||
|
exec /usr/local/bin/mdns-repeater "$@"
|
||||||
|
|
||||||
56
healthcheck.sh
Executable file
56
healthcheck.sh
Executable file
|
|
@ -0,0 +1,56 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# healthcheck.sh — RouterOS container health check for mdns-reflector
|
||||||
|
#
|
||||||
|
# Returns 0 (healthy) if:
|
||||||
|
# 1. Health file exists and is younger than 30 seconds
|
||||||
|
# 2. mdns-repeater process is running
|
||||||
|
# 3. At least one interface socket is alive
|
||||||
|
#
|
||||||
|
# Returns 1 (unhealthy) otherwise.
|
||||||
|
|
||||||
|
HEALTH_FILE="/tmp/mdns-health"
|
||||||
|
|
||||||
|
# 1. Check health file exists
|
||||||
|
if [ ! -f "$HEALTH_FILE" ]; then
|
||||||
|
echo "unhealthy: no health file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Check health file freshness (must be < 30s old)
|
||||||
|
now=$(date +%s)
|
||||||
|
file_ts=$(awk -F= '/^ts=/{print $2}' "$HEALTH_FILE" 2>/dev/null)
|
||||||
|
if [ -z "$file_ts" ]; then
|
||||||
|
echo "unhealthy: no timestamp in health file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
age=$((now - file_ts))
|
||||||
|
if [ "$age" -gt 30 ]; then
|
||||||
|
echo "unhealthy: health file stale (${age}s old)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Check process is running
|
||||||
|
pid=$(pidof mdns-repeater 2>/dev/null)
|
||||||
|
if [ -z "$pid" ]; then
|
||||||
|
echo "unhealthy: mdns-repeater not running"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Check at least one interface has a socket (fd4 or fd6 open)
|
||||||
|
n_ifaces=$(awk -F= '/^n_ifaces=/ {print $2}' "$HEALTH_FILE" 2>/dev/null)
|
||||||
|
if [ -z "$n_ifaces" ] || [ "$n_ifaces" -eq 0 ]; then
|
||||||
|
echo "unhealthy: no interfaces configured"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Verify sockets are actually bound to port 5353
|
||||||
|
ss -lupn 2>/dev/null | grep -q ":5353"
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "unhealthy: no UDP socket on port 5353"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "healthy: ${n_ifaces} interfaces, age=${age}s"
|
||||||
|
exit 0
|
||||||
|
|
||||||
392
mdns-repeater.c
Normal file
392
mdns-repeater.c
Normal file
|
|
@ -0,0 +1,392 @@
|
||||||
|
/*
|
||||||
|
* mdns-repeater.c — Lightweight IPv4/IPv6 mDNS reflector with health check
|
||||||
|
*
|
||||||
|
* Listens on all specified interfaces for mDNS packets (port 5353,
|
||||||
|
* multicast groups 224.0.0.251 and ff02::fb) and repeats them
|
||||||
|
* to every other interface.
|
||||||
|
*
|
||||||
|
* Writes a heartbeat to /tmp/mdns-health for container health checks.
|
||||||
|
*
|
||||||
|
* Build:
|
||||||
|
* gcc -static -O2 -o mdns-repeater mdns-repeater.c -lpthread
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* mdns-repeater eth0 eth1 eth2 ...
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define _GNU_SOURCE
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <time.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/select.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <net/if.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
#define MDNS_PORT 5353
|
||||||
|
#define MDNS_GROUP_IP "224.0.0.251"
|
||||||
|
#define MDNS_GROUP_IP6 "ff02::fb"
|
||||||
|
#define PACKET_SZ 4096
|
||||||
|
#define MAX_IFACES 32
|
||||||
|
#define HEALTH_FILE "/tmp/mdns-health"
|
||||||
|
|
||||||
|
/* ── Per-interface state ────────────────────────────────────── */
|
||||||
|
typedef struct {
|
||||||
|
char name[IFNAMSIZ];
|
||||||
|
int index;
|
||||||
|
int fd4; /* IPv4 socket */
|
||||||
|
int fd6; /* IPv6 socket */
|
||||||
|
uint64_t pkts_in; /* health: packets received */
|
||||||
|
uint64_t pkts_out; /* health: packets forwarded */
|
||||||
|
} iface_t;
|
||||||
|
|
||||||
|
static iface_t ifaces[MAX_IFACES];
|
||||||
|
static int n_ifaces = 0;
|
||||||
|
static volatile int running = 1;
|
||||||
|
|
||||||
|
/* ── Signal handler ─────────────────────────────────────────── */
|
||||||
|
static void handle_signal(int sig) {
|
||||||
|
(void)sig;
|
||||||
|
running = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Write health heartbeat ─────────────────────────────────── */
|
||||||
|
static void write_health(void) {
|
||||||
|
FILE *f = fopen(HEALTH_FILE, "w");
|
||||||
|
if (!f) return;
|
||||||
|
|
||||||
|
time_t now = time(NULL);
|
||||||
|
uint64_t total_in = 0, total_out = 0;
|
||||||
|
|
||||||
|
fprintf(f, "ts=%ld\n", (long)now);
|
||||||
|
for (int i = 0; i < n_ifaces; i++) {
|
||||||
|
fprintf(f, "iface.%s.pkts_in=%lu\n",
|
||||||
|
ifaces[i].name, (unsigned long)ifaces[i].pkts_in);
|
||||||
|
fprintf(f, "iface.%s.pkts_out=%lu\n",
|
||||||
|
ifaces[i].name, (unsigned long)ifaces[i].pkts_out);
|
||||||
|
total_in += ifaces[i].pkts_in;
|
||||||
|
total_out += ifaces[i].pkts_out;
|
||||||
|
}
|
||||||
|
fprintf(f, "total.pkts_in=%lu\n", (unsigned long)total_in);
|
||||||
|
fprintf(f, "total.pkts_out=%lu\n", (unsigned long)total_out);
|
||||||
|
fprintf(f, "n_ifaces=%d\n", n_ifaces);
|
||||||
|
fprintf(f, "alive=1\n");
|
||||||
|
fclose(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Heartbeat thread: writes health file every 10 seconds ──── */
|
||||||
|
static void *heartbeat_thread(void *arg) {
|
||||||
|
(void)arg;
|
||||||
|
write_health(); /* initial write */
|
||||||
|
while (running) {
|
||||||
|
sleep(10);
|
||||||
|
write_health();
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Join IPv4 mDNS group on an fd ──────────────────────────── */
|
||||||
|
static int join_mcast4(int fd, int ifindex) {
|
||||||
|
struct ip_mreqn mreq = {0};
|
||||||
|
inet_pton(AF_INET, MDNS_GROUP_IP, &mreq.imr_multiaddr);
|
||||||
|
mreq.imr_ifindex = ifindex;
|
||||||
|
if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
|
||||||
|
fprintf(stderr, "IP_ADD_MEMBERSHIP(%s): %s\n", MDNS_GROUP_IP, strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Join IPv6 mDNS group on an fd ──────────────────────────── */
|
||||||
|
static int join_mcast6(int fd, int ifindex) {
|
||||||
|
struct ipv6_mreq mreq = {0};
|
||||||
|
inet_pton(AF_INET6, MDNS_GROUP_IP6, &mreq.ipv6mr_multiaddr);
|
||||||
|
mreq.ipv6mr_interface = ifindex;
|
||||||
|
if (setsockopt(fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) < 0) {
|
||||||
|
fprintf(stderr, "IPV6_JOIN_GROUP(%s): %s\n", MDNS_GROUP_IP6, strerror(errno));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Set common socket options for mDNS ─────────────────────── */
|
||||||
|
static int setup_sockopts(int fd, int arr_idx, int sys_ifindex, int af) {
|
||||||
|
int on = 1;
|
||||||
|
int off = 0;
|
||||||
|
|
||||||
|
/* Reuse address (allow multiple listeners) */
|
||||||
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
|
||||||
|
perror("SO_REUSEADDR");
|
||||||
|
#if defined(SO_REUSEPORT)
|
||||||
|
if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)) < 0)
|
||||||
|
perror("SO_REUSEPORT");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* Bind to interface so packets from wrong interface are dropped */
|
||||||
|
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, ifaces[arr_idx].name,
|
||||||
|
strlen(ifaces[arr_idx].name)) < 0)
|
||||||
|
perror("SO_BINDTODEVICE");
|
||||||
|
|
||||||
|
if (af == AF_INET) {
|
||||||
|
if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &off, sizeof(off)) < 0)
|
||||||
|
perror("IP_MULTICAST_LOOP");
|
||||||
|
if (setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &sys_ifindex, sizeof(sys_ifindex)) < 0)
|
||||||
|
perror("IP_MULTICAST_IF");
|
||||||
|
} else {
|
||||||
|
if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &off, sizeof(off)) < 0)
|
||||||
|
perror("IPV6_MULTICAST_LOOP");
|
||||||
|
if (setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &sys_ifindex, sizeof(sys_ifindex)) < 0)
|
||||||
|
perror("IPV6_MULTICAST_IF");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Create and bind an mDNS socket ─────────────────────────── */
|
||||||
|
static int create_mdns_socket(int af, int arr_idx) {
|
||||||
|
struct sockaddr_storage addr = {0};
|
||||||
|
socklen_t addrlen;
|
||||||
|
int sys_ifindex = ifaces[arr_idx].index;
|
||||||
|
int fd;
|
||||||
|
|
||||||
|
fd = socket(af, SOCK_DGRAM, 0);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("socket");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_sockopts(fd, arr_idx, sys_ifindex, af);
|
||||||
|
|
||||||
|
if (af == AF_INET) {
|
||||||
|
struct sockaddr_in *sa = (struct sockaddr_in *)&addr;
|
||||||
|
sa->sin_family = AF_INET;
|
||||||
|
sa->sin_port = htons(MDNS_PORT);
|
||||||
|
sa->sin_addr.s_addr = INADDR_ANY;
|
||||||
|
addrlen = sizeof(struct sockaddr_in);
|
||||||
|
} else {
|
||||||
|
struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&addr;
|
||||||
|
sa6->sin6_family = AF_INET6;
|
||||||
|
sa6->sin6_port = htons(MDNS_PORT);
|
||||||
|
sa6->sin6_addr = in6addr_any;
|
||||||
|
sa6->sin6_scope_id = sys_ifindex;
|
||||||
|
addrlen = sizeof(struct sockaddr_in6);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bind(fd, (struct sockaddr *)&addr, addrlen) < 0) {
|
||||||
|
fprintf(stderr, "bind(%s): %s\n", ifaces[arr_idx].name, strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (af == AF_INET) {
|
||||||
|
if (join_mcast4(fd, sys_ifindex) < 0) { close(fd); return -1; }
|
||||||
|
} else {
|
||||||
|
if (join_mcast6(fd, sys_ifindex) < 0) { close(fd); return -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Send a packet to all interfaces except the source ──────── */
|
||||||
|
static void repeat(const char *buf, int len, int src_idx, int af) {
|
||||||
|
struct msghdr msg = {0};
|
||||||
|
struct iovec iov[1];
|
||||||
|
char cmsg_buf[CMSG_SPACE(sizeof(struct in_pktinfo))];
|
||||||
|
|
||||||
|
iov[0].iov_base = (void *)buf;
|
||||||
|
iov[0].iov_len = len;
|
||||||
|
msg.msg_iov = iov;
|
||||||
|
msg.msg_iovlen = 1;
|
||||||
|
|
||||||
|
for (int i = 0; i < n_ifaces; i++) {
|
||||||
|
if (i == src_idx) continue;
|
||||||
|
|
||||||
|
int fd = (af == AF_INET) ? ifaces[i].fd4 : ifaces[i].fd6;
|
||||||
|
if (fd < 0) continue;
|
||||||
|
|
||||||
|
if (af == AF_INET) {
|
||||||
|
struct in_pktinfo pktinfo = {0};
|
||||||
|
pktinfo.ipi_ifindex = ifaces[i].index;
|
||||||
|
inet_pton(AF_INET, MDNS_GROUP_IP, &pktinfo.ipi_spec_dst);
|
||||||
|
|
||||||
|
msg.msg_control = cmsg_buf;
|
||||||
|
msg.msg_controllen = sizeof(cmsg_buf);
|
||||||
|
|
||||||
|
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
|
||||||
|
cmsg->cmsg_level = IPPROTO_IP;
|
||||||
|
cmsg->cmsg_type = IP_PKTINFO;
|
||||||
|
cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo));
|
||||||
|
memcpy(CMSG_DATA(cmsg), &pktinfo, sizeof(pktinfo));
|
||||||
|
msg.msg_controllen = cmsg->cmsg_len;
|
||||||
|
|
||||||
|
msg.msg_name = NULL;
|
||||||
|
msg.msg_namelen = 0;
|
||||||
|
} else {
|
||||||
|
struct sockaddr_in6 dst = {0};
|
||||||
|
dst.sin6_family = AF_INET6;
|
||||||
|
dst.sin6_port = htons(MDNS_PORT);
|
||||||
|
inet_pton(AF_INET6, MDNS_GROUP_IP6, &dst.sin6_addr);
|
||||||
|
dst.sin6_scope_id = ifaces[i].index;
|
||||||
|
|
||||||
|
msg.msg_name = &dst;
|
||||||
|
msg.msg_namelen = sizeof(dst);
|
||||||
|
msg.msg_control = NULL;
|
||||||
|
msg.msg_controllen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t sent = sendmsg(fd, &msg, 0);
|
||||||
|
if (sent < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||||
|
fprintf(stderr, "sendmsg(%s): %s\n", ifaces[i].name, strerror(errno));
|
||||||
|
} else {
|
||||||
|
ifaces[i].pkts_out++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Reader thread: read packets from one interface ─────────── */
|
||||||
|
static void *reader_thread(void *arg) {
|
||||||
|
int idx = (int)(intptr_t)arg;
|
||||||
|
iface_t *iface = &ifaces[idx];
|
||||||
|
char buf[PACKET_SZ];
|
||||||
|
int maxfd = (iface->fd4 > iface->fd6) ? iface->fd4 : iface->fd6;
|
||||||
|
|
||||||
|
while (running) {
|
||||||
|
fd_set rfds;
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
if (iface->fd4 >= 0) FD_SET(iface->fd4, &rfds);
|
||||||
|
if (iface->fd6 >= 0) FD_SET(iface->fd6, &rfds);
|
||||||
|
|
||||||
|
struct timeval tv = {1, 0};
|
||||||
|
int ret = select(maxfd + 1, &rfds, NULL, NULL, &tv);
|
||||||
|
if (ret < 0) {
|
||||||
|
if (errno == EINTR) continue;
|
||||||
|
perror("select");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ret == 0) continue;
|
||||||
|
|
||||||
|
if (iface->fd4 >= 0 && FD_ISSET(iface->fd4, &rfds)) {
|
||||||
|
int len = recv(iface->fd4, buf, sizeof(buf), 0);
|
||||||
|
if (len > 0) {
|
||||||
|
iface->pkts_in++;
|
||||||
|
repeat(buf, len, idx, AF_INET);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (iface->fd6 >= 0 && FD_ISSET(iface->fd6, &rfds)) {
|
||||||
|
int len = recv(iface->fd6, buf, sizeof(buf), 0);
|
||||||
|
if (len > 0) {
|
||||||
|
iface->pkts_in++;
|
||||||
|
repeat(buf, len, idx, AF_INET6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Print usage ────────────────────────────────────────────── */
|
||||||
|
static void usage(const char *prog) {
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage: %s <interface> [interface ...]\n"
|
||||||
|
"\n"
|
||||||
|
"Reflect mDNS packets (IPv4 + IPv6) between all listed interfaces.\n"
|
||||||
|
"Writes health status to " HEALTH_FILE " every 10s.\n"
|
||||||
|
"\n"
|
||||||
|
"Example:\n"
|
||||||
|
" %s eth0 eth1 br-lan\n",
|
||||||
|
prog, prog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Main ────────────────────────────────────────────────────── */
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc < 2) {
|
||||||
|
usage(argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argc - 1 > MAX_IFACES) {
|
||||||
|
fprintf(stderr, "Too many interfaces (max %d)\n", MAX_IFACES);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[*] mdns-repeater starting...\n");
|
||||||
|
|
||||||
|
/* Parse interfaces */
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
strncpy(ifaces[n_ifaces].name, argv[i], IFNAMSIZ - 1);
|
||||||
|
ifaces[n_ifaces].name[IFNAMSIZ - 1] = '\0';
|
||||||
|
|
||||||
|
ifaces[n_ifaces].index = if_nametoindex(argv[i]);
|
||||||
|
if (ifaces[n_ifaces].index == 0) {
|
||||||
|
fprintf(stderr, "Unknown interface: %s\n", argv[i]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ifaces[n_ifaces].fd4 = create_mdns_socket(AF_INET, n_ifaces);
|
||||||
|
if (ifaces[n_ifaces].fd4 < 0)
|
||||||
|
fprintf(stderr, "WARNING: Could not create IPv4 mDNS socket on %s\n", argv[i]);
|
||||||
|
|
||||||
|
ifaces[n_ifaces].fd6 = create_mdns_socket(AF_INET6, n_ifaces);
|
||||||
|
if (ifaces[n_ifaces].fd6 < 0)
|
||||||
|
fprintf(stderr, "WARNING: Could not create IPv6 mDNS socket on %s\n", argv[i]);
|
||||||
|
|
||||||
|
if (ifaces[n_ifaces].fd4 < 0 && ifaces[n_ifaces].fd6 < 0) {
|
||||||
|
fprintf(stderr, "FATAL: No sockets created for %s\n", argv[i]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ifaces[n_ifaces].pkts_in = 0;
|
||||||
|
ifaces[n_ifaces].pkts_out = 0;
|
||||||
|
|
||||||
|
printf("[+] %-16s (idx=%3d) fd4=%d fd6=%d\n",
|
||||||
|
ifaces[n_ifaces].name, ifaces[n_ifaces].index,
|
||||||
|
ifaces[n_ifaces].fd4, ifaces[n_ifaces].fd6);
|
||||||
|
n_ifaces++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Install signal handlers */
|
||||||
|
signal(SIGINT, handle_signal);
|
||||||
|
signal(SIGTERM, handle_signal);
|
||||||
|
signal(SIGQUIT, handle_signal);
|
||||||
|
|
||||||
|
/* Start heartbeat thread */
|
||||||
|
pthread_t hb_thread;
|
||||||
|
pthread_create(&hb_thread, NULL, heartbeat_thread, NULL);
|
||||||
|
|
||||||
|
/* Start one reader thread per interface */
|
||||||
|
pthread_t threads[MAX_IFACES];
|
||||||
|
for (int i = 0; i < n_ifaces; i++) {
|
||||||
|
if (pthread_create(&threads[i], NULL, reader_thread, (void*)(intptr_t)i) != 0) {
|
||||||
|
perror("pthread_create");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("[*] Reflecting mDNS between %d interfaces (IPv4 + IPv6)\n", n_ifaces);
|
||||||
|
|
||||||
|
/* Wait for all reader threads */
|
||||||
|
for (int i = 0; i < n_ifaces; i++) {
|
||||||
|
pthread_join(threads[i], NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stop heartbeat */
|
||||||
|
running = 0;
|
||||||
|
pthread_join(hb_thread, NULL);
|
||||||
|
|
||||||
|
/* Cleanup */
|
||||||
|
for (int i = 0; i < n_ifaces; i++) {
|
||||||
|
if (ifaces[i].fd4 >= 0) close(ifaces[i].fd4);
|
||||||
|
if (ifaces[i].fd6 >= 0) close(ifaces[i].fd6);
|
||||||
|
}
|
||||||
|
unlink(HEALTH_FILE);
|
||||||
|
|
||||||
|
printf("[*] Shutdown complete.\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in a new issue