New upstream version 2.6.0
diff --git a/ChangeLog.md b/ChangeLog.md
index deafaf1..7c9835e 100644
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -3,6 +3,41 @@ ChangeLog
All notable changes to the project are documented in this file.
+[v2.6.0][] - 2026-06-27
+-----------------------
+
+> [!NOTE]
+> The headline of this release is that interfaces that arrive after
+> `smcrouted` has started, e.g., WireGuard, 6LoWPAN, late-binding
+> bridges and other tunnels, now activate automatically on Linux.
+
+### Changes
+
+- Add JSON output support `smcroutectl -j show [..]`, issue #198
+- Drop pre-v2.0 `smcroute` shell wrapper. Anyone still on `smcroute -a`
+ / `smcroute -j` / etc. should migrate to `smcroute.conf` or call
+ `smcroutectl(8)` directly, issue #199
+- Clearer diagnostic messages for unusable phyints (unknown name vs.
+ not multicast capable vs. VIF table exhausted). Promoted the "not
+ multicast capable" log line from `LOG_INFO` to `LOG_WARNING` so it
+ shows at the default log level. See discussion #201
+- `mroute` and `mgroup` directives whose inbound or outbound interface
+ does not yet exist are queued on a pending list instead of being
+ dropped with parse error. See list with `smcroutectl show pending`
+
+### Fixes
+
+- Fix #55: on Linux, `smcrouted` now subscribes to kernel netlink link
+ events (`RTNLGRP_LINK` + IPv4/IPv6 address groups) and activates any
+ pending routes/groups automatically when their inbound or outbound
+ interface arrives
+- Fix #71: VIFs and MIFs are now allocated lazily, only for interfaces
+ referenced by `smcroute.conf` or by `smcroutectl`. The old up-front
+ enumeration regularly exhausted the kernel's 32-slot table on hosts
+ with many unrelated interfaces. The `-N` option is now a no-op
+- Fix #143: repair kernel MFC on `IGMPMSG_WRONGVIF` / `MRT6MSG_WRONGMIF`
+ by remapping the `(S,G)` entry's inbound iif when a `(*,G)` rule
+ covers the upcall's actual iif
[v2.5.7][] - 2024-05-09
-----------------------
@@ -657,6 +692,7 @@ Initial public release by Carsten Schill.
[mrdisc]: https://github.com/troglobit/mrdisc
[RFC4286]: https://tools.ietf.org/html/rfc4286
[UNRELEASED]: https://github.com/troglobit/smcroute/compare/2.5.7...HEAD
+[v2.6.0]: https://github.com/troglobit/smcroute/compare/2.5.7...2.6.0
[v2.5.7]: https://github.com/troglobit/smcroute/compare/2.5.6...2.5.7
[v2.5.6]: https://github.com/troglobit/smcroute/compare/2.5.5...2.5.6
[v2.5.5]: https://github.com/troglobit/smcroute/compare/2.5.4...2.5.5
diff --git a/Makefile.am b/Makefile.am
index 7bdf164..565791d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,7 +2,6 @@
ACLOCAL_AMFLAGS = -I m4
SUBDIRS = man src
DISTCLEANFILES = *~ DEADJOE semantic.cache *.gdb *.elf core core.* *.d
-dist_sbin_SCRIPTS = smcroute
doc_DATA = README.md COPYING smcroute.conf
EXTRA_DIST = README.md ChangeLog.md autogen.sh smcroute.conf smcroute.default smcroute.init
diff --git a/Makefile.in b/Makefile.in
index f39c2ab..c776f04 100644
--- a/Makefile.in
+++ b/Makefile.in
@@ -14,7 +14,6 @@
@SET_MAKE@
-
VPATH = @srcdir@
am__is_gnu_make = { \
if test -z '$(MAKELEVEL)'; then \
@@ -97,43 +96,13 @@ am__aclocal_m4_deps = $(top_srcdir)/m4/misc.m4 \
am__configure_deps = $(am__aclocal_m4_deps) $(CONFIGURE_DEPENDENCIES) \
$(ACLOCAL_M4)
DIST_COMMON = $(srcdir)/Makefile.am $(top_srcdir)/configure \
- $(am__configure_deps) $(dist_sbin_SCRIPTS) $(am__DIST_COMMON)
+ $(am__configure_deps) $(am__DIST_COMMON)
am__CONFIG_DISTCLEAN_FILES = config.status config.cache config.log \
configure.lineno config.status.lineno
mkinstalldirs = $(install_sh) -d
CONFIG_HEADER = config.h
CONFIG_CLEAN_FILES = smcroute.service
CONFIG_CLEAN_VPATH_FILES =
-am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
-am__vpath_adj = case $$p in \
- $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
- *) f=$$p;; \
- esac;
-am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
-am__install_max = 40
-am__nobase_strip_setup = \
- srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
-am__nobase_strip = \
- for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
-am__nobase_list = $(am__nobase_strip_setup); \
- for p in $$list; do echo "$$p $$p"; done | \
- sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
- $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
- if (++n[$$2] == $(am__install_max)) \
- { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
- END { for (dir in files) print dir, files[dir] }'
-am__base_list = \
- sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
- sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
-am__uninstall_files_from_dir = { \
- test -z "$$files" \
- || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
- || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
- $(am__cd) "$$dir" && rm -f $$files; }; \
- }
-am__installdirs = "$(DESTDIR)$(sbindir)" "$(DESTDIR)$(docdir)" \
- "$(DESTDIR)$(systemddir)"
-SCRIPTS = $(dist_sbin_SCRIPTS)
AM_V_P = $(am__v_P_@AM_V@)
am__v_P_ = $(am__v_P_@AM_DEFAULT_V@)
am__v_P_0 = false
@@ -161,6 +130,34 @@ am__can_run_installinfo = \
n|no|NO) false;; \
*) (install-info --version) >/dev/null 2>&1;; \
esac
+am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`;
+am__vpath_adj = case $$p in \
+ $(srcdir)/*) f=`echo "$$p" | sed "s|^$$srcdirstrip/||"`;; \
+ *) f=$$p;; \
+ esac;
+am__strip_dir = f=`echo $$p | sed -e 's|^.*/||'`;
+am__install_max = 40
+am__nobase_strip_setup = \
+ srcdirstrip=`echo "$(srcdir)" | sed 's/[].[^$$\\*|]/\\\\&/g'`
+am__nobase_strip = \
+ for p in $$list; do echo "$$p"; done | sed -e "s|$$srcdirstrip/||"
+am__nobase_list = $(am__nobase_strip_setup); \
+ for p in $$list; do echo "$$p $$p"; done | \
+ sed "s| $$srcdirstrip/| |;"' / .*\//!s/ .*/ ./; s,\( .*\)/[^/]*$$,\1,' | \
+ $(AWK) 'BEGIN { files["."] = "" } { files[$$2] = files[$$2] " " $$1; \
+ if (++n[$$2] == $(am__install_max)) \
+ { print $$2, files[$$2]; n[$$2] = 0; files[$$2] = "" } } \
+ END { for (dir in files) print dir, files[dir] }'
+am__base_list = \
+ sed '$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;$$!N;s/\n/ /g' | \
+ sed '$$!N;$$!N;$$!N;$$!N;s/\n/ /g'
+am__uninstall_files_from_dir = { \
+ test -z "$$files" \
+ || { test ! -d "$$dir" && test ! -f "$$dir" && test ! -r "$$dir"; } \
+ || { echo " ( cd '$$dir' && rm -f" $$files ")"; \
+ $(am__cd) "$$dir" && rm -f $$files; }; \
+ }
+am__installdirs = "$(DESTDIR)$(docdir)" "$(DESTDIR)$(systemddir)"
DATA = $(doc_DATA) $(systemd_DATA)
RECURSIVE_CLEAN_TARGETS = mostlyclean-recursive clean-recursive \
distclean-recursive maintainer-clean-recursive
@@ -354,7 +351,6 @@ top_srcdir = @top_srcdir@
ACLOCAL_AMFLAGS = -I m4
SUBDIRS = man src $(am__append_1)
DISTCLEANFILES = *~ DEADJOE semantic.cache *.gdb *.elf core core.* *.d
-dist_sbin_SCRIPTS = smcroute
doc_DATA = README.md COPYING smcroute.conf
EXTRA_DIST = README.md ChangeLog.md autogen.sh smcroute.conf smcroute.default smcroute.init
@HAVE_SYSTEMD_TRUE@systemd_DATA = smcroute.service
@@ -415,41 +411,6 @@ distclean-hdr:
-rm -f config.h stamp-h1
smcroute.service: $(top_builddir)/config.status $(srcdir)/smcroute.service.in
cd $(top_builddir) && $(SHELL) ./config.status $@
-install-dist_sbinSCRIPTS: $(dist_sbin_SCRIPTS)
- @$(NORMAL_INSTALL)
- @list='$(dist_sbin_SCRIPTS)'; test -n "$(sbindir)" || list=; \
- if test -n "$$list"; then \
- echo " $(MKDIR_P) '$(DESTDIR)$(sbindir)'"; \
- $(MKDIR_P) "$(DESTDIR)$(sbindir)" || exit 1; \
- fi; \
- for p in $$list; do \
- if test -f "$$p"; then d=; else d="$(srcdir)/"; fi; \
- if test -f "$$d$$p"; then echo "$$d$$p"; echo "$$p"; else :; fi; \
- done | \
- sed -e 'p;s,.*/,,;n' \
- -e 'h;s|.*|.|' \
- -e 'p;x;s,.*/,,;$(transform)' | sed 'N;N;N;s,\n, ,g' | \
- $(AWK) 'BEGIN { files["."] = ""; dirs["."] = 1; } \
- { d=$$3; if (dirs[d] != 1) { print "d", d; dirs[d] = 1 } \
- if ($$2 == $$4) { files[d] = files[d] " " $$1; \
- if (++n[d] == $(am__install_max)) { \
- print "f", d, files[d]; n[d] = 0; files[d] = "" } } \
- else { print "f", d "/" $$4, $$1 } } \
- END { for (d in files) print "f", d, files[d] }' | \
- while read type dir files; do \
- if test "$$dir" = .; then dir=; else dir=/$$dir; fi; \
- test -z "$$files" || { \
- echo " $(INSTALL_SCRIPT) $$files '$(DESTDIR)$(sbindir)$$dir'"; \
- $(INSTALL_SCRIPT) $$files "$(DESTDIR)$(sbindir)$$dir" || exit $$?; \
- } \
- ; done
-
-uninstall-dist_sbinSCRIPTS:
- @$(NORMAL_UNINSTALL)
- @list='$(dist_sbin_SCRIPTS)'; test -n "$(sbindir)" || exit 0; \
- files=`for p in $$list; do echo "$$p"; done | \
- sed -e 's,.*/,,;$(transform)'`; \
- dir='$(DESTDIR)$(sbindir)'; $(am__uninstall_files_from_dir)
install-docDATA: $(doc_DATA)
@$(NORMAL_INSTALL)
@list='$(doc_DATA)'; test -n "$(docdir)" || list=; \
@@ -797,10 +758,10 @@ distcleancheck: distclean
exit 1; } >&2
check-am: all-am
check: check-recursive
-all-am: Makefile $(SCRIPTS) $(DATA) config.h
+all-am: Makefile $(DATA) config.h
installdirs: installdirs-recursive
installdirs-am:
- for dir in "$(DESTDIR)$(sbindir)" "$(DESTDIR)$(docdir)" "$(DESTDIR)$(systemddir)"; do \
+ for dir in "$(DESTDIR)$(docdir)" "$(DESTDIR)$(systemddir)"; do \
test -z "$$dir" || $(MKDIR_P) "$$dir"; \
done
install: install-recursive
@@ -861,7 +822,7 @@ install-dvi: install-dvi-recursive
install-dvi-am:
-install-exec-am: install-dist_sbinSCRIPTS
+install-exec-am:
install-html: install-html-recursive
@@ -901,8 +862,7 @@ ps: ps-recursive
ps-am:
-uninstall-am: uninstall-dist_sbinSCRIPTS uninstall-docDATA \
- uninstall-systemdDATA
+uninstall-am: uninstall-docDATA uninstall-systemdDATA
.MAKE: $(am__recursive_targets) all install-am install-strip
@@ -913,16 +873,15 @@ uninstall-am: uninstall-dist_sbinSCRIPTS uninstall-docDATA \
dist-zstd distcheck distclean distclean-generic distclean-hdr \
distclean-tags distcleancheck distdir distuninstallcheck dvi \
dvi-am html html-am info info-am install install-am \
- install-data install-data-am install-dist_sbinSCRIPTS \
- install-docDATA install-dvi install-dvi-am install-exec \
- install-exec-am install-html install-html-am install-info \
- install-info-am install-man install-pdf install-pdf-am \
- install-ps install-ps-am install-strip install-systemdDATA \
- installcheck installcheck-am installdirs installdirs-am \
- maintainer-clean maintainer-clean-generic mostlyclean \
- mostlyclean-generic pdf pdf-am ps ps-am tags tags-am uninstall \
- uninstall-am uninstall-dist_sbinSCRIPTS uninstall-docDATA \
- uninstall-systemdDATA
+ install-data install-data-am install-docDATA install-dvi \
+ install-dvi-am install-exec install-exec-am install-html \
+ install-html-am install-info install-info-am install-man \
+ install-pdf install-pdf-am install-ps install-ps-am \
+ install-strip install-systemdDATA installcheck installcheck-am \
+ installdirs installdirs-am maintainer-clean \
+ maintainer-clean-generic mostlyclean mostlyclean-generic pdf \
+ pdf-am ps ps-am tags tags-am uninstall uninstall-am \
+ uninstall-docDATA uninstall-systemdDATA
.PRECIOUS: Makefile
diff --git a/README.md b/README.md
index dd0341a..fdcdcd3 100644
--- a/README.md
+++ b/README.md
@@ -42,8 +42,8 @@ daemon runs. On Linux, multiple multicast routers can run simultaneously
using different multicast routing tables.
The full documentation of SMCRoute is available in the manual pages, see
-[smcrouted(8)][], [smcroutectl(8)][], and [smcroute.conf(5)][].
-
+[smcrouted(8)][], [smcroutectl(8)][], and [smcroute.conf(5)][]. Common
+error messages and their fixes are collected in [TROUBLESHOOTING.md][1].
Features
--------
@@ -168,23 +168,49 @@ flush connection tracking after installing a multicast route.
### Many Interfaces
- smcrouted -N
-
-With the `-N` command line option SMCRoute does *not* prepare all system
-interfaces for multicast routing. Very useful if your system has a lot
-of interfaces but only a select few are required for multicast routing.
-Use the following in `/etc/smcroute.conf` to enable interfaces:
+SMCRoute allocates kernel VIFs/MIFs on demand, only for interfaces that
+appear in `smcroute.conf` or that `smcroutectl` references at runtime.
+Hosts with many unrelated multicast-capable interfaces therefore stay
+well under the kernel's VIF/MIF limit (32 on Linux). In typical setups
+just list the interfaces you need in `/etc/smcroute.conf`:
phyint eth0 enable
phyint eth1 enable
phyint eth2 enable
-It is possible to use any interface that supports the `MULTICAST` flag.
+`mroute` and `mgroup` lines also count as references, so an explicit
+`phyint enable` is not required if the interface only appears there.
+It is possible to use any interface that supports the `MULTICAST` flag.
Note, however, that depending on the UNIX kernel in use, you may have to
have an interface address set, in the relevant address family, and the
interface may likely also have to be `UP`.
+The legacy `-N` flag — which used to suppress the now-removed upfront
+VIF enumeration — is still accepted for backwards compatibility but is
+effectively a no-op.
+
+
+### Late-arriving Interfaces
+
+If an `mroute` or `mgroup` directive in `smcroute.conf` names an
+interface that does not yet exist on the system, `smcrouted` keeps the
+directive on an internal pending list rather than dropping it. Inspect
+the list with:
+
+ smcroutectl show pending
+
+On Linux, `smcrouted` listens for kernel link events (netlink
+`RTNLGRP_LINK`) and activates pending entries automatically when their
+inbound interface arrives — no `SIGHUP` or `smcroutectl reload`
+required. This is the recommended fix for tunnels (WireGuard,
+6LoWPAN, ...) and other late-binding interfaces.
+
+On other systems the pending entries are activated on the next reload.
+Use `smcrouted -d SEC` to delay startup, or trigger an external reload
+from your network manager, until a BSD route-socket equivalent of the
+Linux listener is implemented.
+
### Multiple Routing Tables
@@ -392,6 +418,7 @@ author.
[smcrouted(8)]: https://man.troglobit.com/man8/smcrouted.8.html
[smcroutectl(8)]: https://man.troglobit.com/man8/smcroutectl.8.html
[smcroute.conf(5)]:https://man.troglobit.com/man5/smcroute.conf.5.html
+[1]: doc/TROUBLESHOOTING.md
[Finit]: https://github.com/troglobit/finit
[mrouted]: https://github.com/troglobit/mrouted
[pimd]: https://github.com/troglobit/pimd
diff --git a/aclocal.m4 b/aclocal.m4
index 174327b..b9921cc 100644
--- a/aclocal.m4
+++ b/aclocal.m4
@@ -20,7 +20,7 @@ You have another version of autoconf. It may work, but is not guaranteed to.
If you have problems, you may need to regenerate the build system entirely.
To do so, use the procedure documented by the package, typically 'autoreconf'.])])
-# pkg.m4 - Macros to locate and utilise pkg-config. -*- Autoconf -*-
+# pkg.m4 - Macros to locate and use pkg-config. -*- Autoconf -*-
# serial 12 (pkg-config-0.29.2)
dnl Copyright © 2004 Scott James Remnant <scott@netsplit.com>.
@@ -108,7 +108,7 @@ dnl Check to see whether a particular set of modules exists. Similar to
dnl PKG_CHECK_MODULES(), but does not set variables or print errors.
dnl
dnl Please remember that m4 expands AC_REQUIRE([PKG_PROG_PKG_CONFIG])
-dnl only at the first occurence in configure.ac, so if the first place
+dnl only at the first occurrence in configure.ac, so if the first place
dnl it's called might be skipped (such as if it is within an "if", you
dnl have to call PKG_CHECK_EXISTS manually
AC_DEFUN([PKG_CHECK_EXISTS],
@@ -177,14 +177,14 @@ if test $pkg_failed = yes; then
AC_MSG_RESULT([no])
_PKG_SHORT_ERRORS_SUPPORTED
if test $_pkg_short_errors_supported = yes; then
- $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1`
+ $1[]_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "$2" 2>&1`
else
- $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1`
+ $1[]_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "$2" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD
+ # Put the nasty error message in config.log where it belongs
+ echo "$$1[]_PKG_ERRORS" >&AS_MESSAGE_LOG_FD
- m4_default([$4], [AC_MSG_ERROR(
+ m4_default([$4], [AC_MSG_ERROR(
[Package requirements ($2) were not met:
$$1_PKG_ERRORS
@@ -196,7 +196,7 @@ _PKG_TEXT])[]dnl
])
elif test $pkg_failed = untried; then
AC_MSG_RESULT([no])
- m4_default([$4], [AC_MSG_FAILURE(
+ m4_default([$4], [AC_MSG_FAILURE(
[The pkg-config script could not be found or is too old. Make sure it
is in your PATH or set the PKG_CONFIG environment variable to the full
path to pkg-config.
@@ -206,10 +206,10 @@ _PKG_TEXT
To get pkg-config, see <http://pkg-config.freedesktop.org/>.])[]dnl
])
else
- $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS
- $1[]_LIBS=$pkg_cv_[]$1[]_LIBS
+ $1[]_CFLAGS=$pkg_cv_[]$1[]_CFLAGS
+ $1[]_LIBS=$pkg_cv_[]$1[]_LIBS
AC_MSG_RESULT([yes])
- $3
+ $3
fi[]dnl
])dnl PKG_CHECK_MODULES
@@ -296,6 +296,74 @@ AS_VAR_COPY([$1], [pkg_cv_][$1])
AS_VAR_IF([$1], [""], [$5], [$4])dnl
])dnl PKG_CHECK_VAR
+dnl PKG_WITH_MODULES(VARIABLE-PREFIX, MODULES,
+dnl [ACTION-IF-FOUND],[ACTION-IF-NOT-FOUND],
+dnl [DESCRIPTION], [DEFAULT])
+dnl ------------------------------------------
+dnl
+dnl Prepare a "--with-" configure option using the lowercase
+dnl [VARIABLE-PREFIX] name, merging the behaviour of AC_ARG_WITH and
+dnl PKG_CHECK_MODULES in a single macro.
+AC_DEFUN([PKG_WITH_MODULES],
+[
+m4_pushdef([with_arg], m4_tolower([$1]))
+
+m4_pushdef([description],
+ [m4_default([$5], [build with ]with_arg[ support])])
+
+m4_pushdef([def_arg], [m4_default([$6], [auto])])
+m4_pushdef([def_action_if_found], [AS_TR_SH([with_]with_arg)=yes])
+m4_pushdef([def_action_if_not_found], [AS_TR_SH([with_]with_arg)=no])
+
+m4_case(def_arg,
+ [yes],[m4_pushdef([with_without], [--without-]with_arg)],
+ [m4_pushdef([with_without],[--with-]with_arg)])
+
+AC_ARG_WITH(with_arg,
+ AS_HELP_STRING(with_without, description[ @<:@default=]def_arg[@:>@]),,
+ [AS_TR_SH([with_]with_arg)=def_arg])
+
+AS_CASE([$AS_TR_SH([with_]with_arg)],
+ [yes],[PKG_CHECK_MODULES([$1],[$2],$3,$4)],
+ [auto],[PKG_CHECK_MODULES([$1],[$2],
+ [m4_n([def_action_if_found]) $3],
+ [m4_n([def_action_if_not_found]) $4])])
+
+m4_popdef([with_arg])
+m4_popdef([description])
+m4_popdef([def_arg])
+
+])dnl PKG_WITH_MODULES
+
+dnl PKG_HAVE_WITH_MODULES(VARIABLE-PREFIX, MODULES,
+dnl [DESCRIPTION], [DEFAULT])
+dnl -----------------------------------------------
+dnl
+dnl Convenience macro to trigger AM_CONDITIONAL after PKG_WITH_MODULES
+dnl check._[VARIABLE-PREFIX] is exported as make variable.
+AC_DEFUN([PKG_HAVE_WITH_MODULES],
+[
+PKG_WITH_MODULES([$1],[$2],,,[$3],[$4])
+
+AM_CONDITIONAL([HAVE_][$1],
+ [test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"])
+])dnl PKG_HAVE_WITH_MODULES
+
+dnl PKG_HAVE_DEFINE_WITH_MODULES(VARIABLE-PREFIX, MODULES,
+dnl [DESCRIPTION], [DEFAULT])
+dnl ------------------------------------------------------
+dnl
+dnl Convenience macro to run AM_CONDITIONAL and AC_DEFINE after
+dnl PKG_WITH_MODULES check. HAVE_[VARIABLE-PREFIX] is exported as make
+dnl and preprocessor variable.
+AC_DEFUN([PKG_HAVE_DEFINE_WITH_MODULES],
+[
+PKG_HAVE_WITH_MODULES([$1],[$2],[$3],[$4])
+
+AS_IF([test "$AS_TR_SH([with_]m4_tolower([$1]))" = "yes"],
+ [AC_DEFINE([HAVE_][$1], 1, [Enable ]m4_tolower([$1])[ support])])
+])dnl PKG_HAVE_DEFINE_WITH_MODULES
+
# Copyright (C) 2002-2021 Free Software Foundation, Inc.
#
# This file is free software; the Free Software Foundation
diff --git a/configure b/configure
index cfb1ed0..fede694 100755
--- a/configure
+++ b/configure
@@ -1,6 +1,6 @@
#! /bin/sh
# Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.71 for SMCRoute 2.5.7.
+# Generated by GNU Autoconf 2.71 for SMCRoute 2.6.0.
#
# Report bugs to <https://github.com/troglobit/smcroute/issues>.
#
@@ -611,10 +611,10 @@ MAKEFLAGS=
# Identity of this package.
PACKAGE_NAME='SMCRoute'
PACKAGE_TARNAME='smcroute'
-PACKAGE_VERSION='2.5.7'
-PACKAGE_STRING='SMCRoute 2.5.7'
+PACKAGE_VERSION='2.6.0'
+PACKAGE_STRING='SMCRoute 2.6.0'
PACKAGE_BUGREPORT='https://github.com/troglobit/smcroute/issues'
-PACKAGE_URL='https://troglobit.com/smcroute.html'
+PACKAGE_URL='https://github.com/troglobit/smcroute/'
ac_unique_file="src/smcrouted.c"
# Factoring default headers for most tests.
@@ -674,17 +674,11 @@ libsystemd_CFLAGS
PKG_CONFIG_LIBDIR
PKG_CONFIG_PATH
PKG_CONFIG
+HAVE_NETLINK_FALSE
+HAVE_NETLINK_TRUE
USE_MRDISC_FALSE
USE_MRDISC_TRUE
LIBOBJS
-host_os
-host_vendor
-host_cpu
-host
-build_os
-build_vendor
-build_cpu
-build
EGREP
GREP
CPP
@@ -705,6 +699,14 @@ CPPFLAGS
LDFLAGS
CFLAGS
CC
+host_os
+host_vendor
+host_cpu
+host
+build_os
+build_vendor
+build_cpu
+build
AM_BACKSLASH
AM_DEFAULT_VERBOSITY
AM_DEFAULT_V
@@ -1348,7 +1350,7 @@ if test "$ac_init_help" = "long"; then
# Omit some internal or obsolete options to make the list less imposing.
# This message is too long to be a string in the A/UX 3.1 sh.
cat <<_ACEOF
-\`configure' configures SMCRoute 2.5.7 to adapt to many kinds of systems.
+\`configure' configures SMCRoute 2.6.0 to adapt to many kinds of systems.
Usage: $0 [OPTION]... [VAR=VALUE]...
@@ -1419,7 +1421,7 @@ fi
if test -n "$ac_init_help"; then
case $ac_init_help in
- short | recursive ) echo "Configuration of SMCRoute 2.5.7:";;
+ short | recursive ) echo "Configuration of SMCRoute 2.6.0:";;
esac
cat <<\_ACEOF
@@ -1466,7 +1468,7 @@ Use these variables to override the choices made by `configure' or to help
it to find libraries and programs with nonstandard names/locations.
Report bugs to <https://github.com/troglobit/smcroute/issues>.
-SMCRoute home page: <https://troglobit.com/smcroute.html>.
+SMCRoute home page: <https://github.com/troglobit/smcroute/>.
_ACEOF
ac_status=$?
fi
@@ -1530,7 +1532,7 @@ fi
test -n "$ac_init_help" && exit $ac_status
if $ac_init_version; then
cat <<\_ACEOF
-SMCRoute configure 2.5.7
+SMCRoute configure 2.6.0
generated by GNU Autoconf 2.71
Copyright (C) 2021 Free Software Foundation, Inc.
@@ -2003,7 +2005,7 @@ cat >config.log <<_ACEOF
This file contains any messages produced by compilers while
running configure, to aid debugging if configure makes a mistake.
-It was created by SMCRoute $as_me 2.5.7, which was
+It was created by SMCRoute $as_me 2.6.0, which was
generated by GNU Autoconf 2.71. Invocation command line was
$ $0$ac_configure_args_raw
@@ -2598,7 +2600,7 @@ as_fn_append ac_func_c_list " fork HAVE_FORK"
as_fn_append ac_func_c_list " vfork HAVE_VFORK"
# Auxiliary files required by this configure script.
-ac_aux_files="config.guess config.sub compile missing install-sh"
+ac_aux_files="compile config.guess config.sub missing install-sh"
# Locations in which to look for auxiliary files.
ac_aux_dir_candidates="${srcdir}/aux"
@@ -3278,7 +3280,7 @@ fi
# Define the identity of the package.
PACKAGE='smcroute'
- VERSION='2.5.7'
+ VERSION='2.6.0'
printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h
@@ -3429,6 +3431,81 @@ ac_config_headers="$ac_config_headers config.h"
ac_config_files="$ac_config_files Makefile man/Makefile src/Makefile test/Makefile smcroute.service"
+
+ # Make sure we can run config.sub.
+$SHELL "${ac_aux_dir}config.sub" sun4 >/dev/null 2>&1 ||
+ as_fn_error $? "cannot run $SHELL ${ac_aux_dir}config.sub" "$LINENO" 5
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking build system type" >&5
+printf %s "checking build system type... " >&6; }
+if test ${ac_cv_build+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ ac_build_alias=$build_alias
+test "x$ac_build_alias" = x &&
+ ac_build_alias=`$SHELL "${ac_aux_dir}config.guess"`
+test "x$ac_build_alias" = x &&
+ as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5
+ac_cv_build=`$SHELL "${ac_aux_dir}config.sub" $ac_build_alias` ||
+ as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $ac_build_alias failed" "$LINENO" 5
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5
+printf "%s\n" "$ac_cv_build" >&6; }
+case $ac_cv_build in
+*-*-*) ;;
+*) as_fn_error $? "invalid value of canonical build" "$LINENO" 5;;
+esac
+build=$ac_cv_build
+ac_save_IFS=$IFS; IFS='-'
+set x $ac_cv_build
+shift
+build_cpu=$1
+build_vendor=$2
+shift; shift
+# Remember, the first character of IFS is used to create $*,
+# except with old shells:
+build_os=$*
+IFS=$ac_save_IFS
+case $build_os in *\ *) build_os=`echo "$build_os" | sed 's/ /-/g'`;; esac
+
+
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking host system type" >&5
+printf %s "checking host system type... " >&6; }
+if test ${ac_cv_host+y}
+then :
+ printf %s "(cached) " >&6
+else $as_nop
+ if test "x$host_alias" = x; then
+ ac_cv_host=$ac_cv_build
+else
+ ac_cv_host=`$SHELL "${ac_aux_dir}config.sub" $host_alias` ||
+ as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $host_alias failed" "$LINENO" 5
+fi
+
+fi
+{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5
+printf "%s\n" "$ac_cv_host" >&6; }
+case $ac_cv_host in
+*-*-*) ;;
+*) as_fn_error $? "invalid value of canonical host" "$LINENO" 5;;
+esac
+host=$ac_cv_host
+ac_save_IFS=$IFS; IFS='-'
+set x $ac_cv_host
+shift
+host_cpu=$1
+host_vendor=$2
+shift; shift
+# Remember, the first character of IFS is used to create $*,
+# except with old shells:
+host_os=$*
+IFS=$ac_save_IFS
+case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac
+
+
+
# Older versions of autoconf (<2.58) do not have AC_CONFIG_MACRO_DIR()
# Misc. helper macros
@@ -5650,81 +5727,6 @@ printf "%s\n" "#define HAVE_WORKING_FORK 1" >>confdefs.h
fi
-
-
- # Make sure we can run config.sub.
-$SHELL "${ac_aux_dir}config.sub" sun4 >/dev/null 2>&1 ||
- as_fn_error $? "cannot run $SHELL ${ac_aux_dir}config.sub" "$LINENO" 5
-
-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking build system type" >&5
-printf %s "checking build system type... " >&6; }
-if test ${ac_cv_build+y}
-then :
- printf %s "(cached) " >&6
-else $as_nop
- ac_build_alias=$build_alias
-test "x$ac_build_alias" = x &&
- ac_build_alias=`$SHELL "${ac_aux_dir}config.guess"`
-test "x$ac_build_alias" = x &&
- as_fn_error $? "cannot guess build type; you must specify one" "$LINENO" 5
-ac_cv_build=`$SHELL "${ac_aux_dir}config.sub" $ac_build_alias` ||
- as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $ac_build_alias failed" "$LINENO" 5
-
-fi
-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_build" >&5
-printf "%s\n" "$ac_cv_build" >&6; }
-case $ac_cv_build in
-*-*-*) ;;
-*) as_fn_error $? "invalid value of canonical build" "$LINENO" 5;;
-esac
-build=$ac_cv_build
-ac_save_IFS=$IFS; IFS='-'
-set x $ac_cv_build
-shift
-build_cpu=$1
-build_vendor=$2
-shift; shift
-# Remember, the first character of IFS is used to create $*,
-# except with old shells:
-build_os=$*
-IFS=$ac_save_IFS
-case $build_os in *\ *) build_os=`echo "$build_os" | sed 's/ /-/g'`;; esac
-
-
-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking host system type" >&5
-printf %s "checking host system type... " >&6; }
-if test ${ac_cv_host+y}
-then :
- printf %s "(cached) " >&6
-else $as_nop
- if test "x$host_alias" = x; then
- ac_cv_host=$ac_cv_build
-else
- ac_cv_host=`$SHELL "${ac_aux_dir}config.sub" $host_alias` ||
- as_fn_error $? "$SHELL ${ac_aux_dir}config.sub $host_alias failed" "$LINENO" 5
-fi
-
-fi
-{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_cv_host" >&5
-printf "%s\n" "$ac_cv_host" >&6; }
-case $ac_cv_host in
-*-*-*) ;;
-*) as_fn_error $? "invalid value of canonical host" "$LINENO" 5;;
-esac
-host=$ac_cv_host
-ac_save_IFS=$IFS; IFS='-'
-set x $ac_cv_host
-shift
-host_cpu=$1
-host_vendor=$2
-shift; shift
-# Remember, the first character of IFS is used to create $*,
-# except with old shells:
-host_os=$*
-IFS=$ac_save_IFS
-case $host_os in *\ *) host_os=`echo "$host_os" | sed 's/ /-/g'`;; esac
-
-
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for working chown" >&5
printf %s "checking for working chown... " >&6; }
if test ${ac_cv_func_chown_works+y}
@@ -6512,6 +6514,21 @@ else
fi
+# Netlink listener for kernel link events. Linux only, BSD route-socket
+# port tracked separately. See src/ifevent.h.
+case "$host_os" in
+ linux*) have_netlink=yes ;;
+ *) have_netlink=no ;;
+esac
+ if test "x$have_netlink" = "xyes"; then
+ HAVE_NETLINK_TRUE=
+ HAVE_NETLINK_FALSE='#'
+else
+ HAVE_NETLINK_TRUE='#'
+ HAVE_NETLINK_FALSE=
+fi
+
+
# Required to check for libsystemd-dev
@@ -6727,24 +6744,24 @@ else
_pkg_short_errors_supported=no
fi
if test $_pkg_short_errors_supported = yes; then
- libsystemd_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libsystemd" 2>&1`
+ libsystemd_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "libsystemd" 2>&1`
else
- libsystemd_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libsystemd" 2>&1`
+ libsystemd_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "libsystemd" 2>&1`
fi
- # Put the nasty error message in config.log where it belongs
- echo "$libsystemd_PKG_ERRORS" >&5
+ # Put the nasty error message in config.log where it belongs
+ echo "$libsystemd_PKG_ERRORS" >&5
- true
+ true
elif test $pkg_failed = untried; then
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5
printf "%s\n" "no" >&6; }
- true
+ true
else
- libsystemd_CFLAGS=$pkg_cv_libsystemd_CFLAGS
- libsystemd_LIBS=$pkg_cv_libsystemd_LIBS
+ libsystemd_CFLAGS=$pkg_cv_libsystemd_CFLAGS
+ libsystemd_LIBS=$pkg_cv_libsystemd_LIBS
{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5
printf "%s\n" "yes" >&6; }
- with_libsystemd=yes
+ with_libsystemd=yes
fi
systemddir=$with_systemd
@@ -7710,6 +7727,10 @@ if test -z "${USE_MRDISC_TRUE}" && test -z "${USE_MRDISC_FALSE}"; then
as_fn_error $? "conditional \"USE_MRDISC\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
fi
+if test -z "${HAVE_NETLINK_TRUE}" && test -z "${HAVE_NETLINK_FALSE}"; then
+ as_fn_error $? "conditional \"HAVE_NETLINK\" was never defined.
+Usually this means the macro was only invoked conditionally." "$LINENO" 5
+fi
if test -z "${HAVE_SYSTEMD_TRUE}" && test -z "${HAVE_SYSTEMD_FALSE}"; then
as_fn_error $? "conditional \"HAVE_SYSTEMD\" was never defined.
Usually this means the macro was only invoked conditionally." "$LINENO" 5
@@ -8116,7 +8137,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1
# report actual input values of CONFIG_FILES etc. instead of their
# values after options handling.
ac_log="
-This file was extended by SMCRoute $as_me 2.5.7, which was
+This file was extended by SMCRoute $as_me 2.6.0, which was
generated by GNU Autoconf 2.71. Invocation command line was
CONFIG_FILES = $CONFIG_FILES
@@ -8177,7 +8198,7 @@ Configuration commands:
$config_commands
Report bugs to <https://github.com/troglobit/smcroute/issues>.
-SMCRoute home page: <https://troglobit.com/smcroute.html>."
+SMCRoute home page: <https://github.com/troglobit/smcroute/>."
_ACEOF
ac_cs_config=`printf "%s\n" "$ac_configure_args" | sed "$ac_safe_unquote"`
@@ -8185,7 +8206,7 @@ ac_cs_config_escaped=`printf "%s\n" "$ac_cs_config" | sed "s/^ //; s/'/'\\\\\\\\
cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
ac_cs_config='$ac_cs_config_escaped'
ac_cs_version="\\
-SMCRoute config.status 2.5.7
+SMCRoute config.status 2.6.0
configured by $0, generated by GNU Autoconf 2.71,
with options \\"\$ac_cs_config\\"
diff --git a/configure.ac b/configure.ac
index 105165d..d8c264b 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,8 +2,8 @@
# Process this file with autoconf to produce a configure script.
AC_PREREQ(2.61)
-AC_INIT([SMCRoute], [2.5.7], [https://github.com/troglobit/smcroute/issues],
- [smcroute], [https://troglobit.com/smcroute.html])
+AC_INIT([SMCRoute], [2.6.0], [https://github.com/troglobit/smcroute/issues],
+ [smcroute], [https://github.com/troglobit/smcroute/])
AC_CONFIG_AUX_DIR(aux)
AM_INIT_AUTOMAKE([1.11 foreign])
AM_SILENT_RULES([yes])
@@ -11,6 +11,7 @@ AM_SILENT_RULES([yes])
AC_CONFIG_SRCDIR([src/smcrouted.c])
AC_CONFIG_HEADERS([config.h])
AC_CONFIG_FILES([Makefile man/Makefile src/Makefile test/Makefile smcroute.service])
+AC_CANONICAL_HOST
# Older versions of autoconf (<2.58) do not have AC_CONFIG_MACRO_DIR()
m4_include([m4/misc.m4])
@@ -90,6 +91,14 @@ AS_IF([test "x$enable_mrdisc" = "xyes"],
enable_mrdisc=no)
AM_CONDITIONAL([USE_MRDISC], [test "x$enable_mrdisc" = "xyes"])
+# Netlink listener for kernel link events. Linux only, BSD route-socket
+# port tracked separately. See src/ifevent.h.
+case "$host_os" in
+ linux*) have_netlink=yes ;;
+ *) have_netlink=no ;;
+esac
+AM_CONDITIONAL([HAVE_NETLINK], [test "x$have_netlink" = "xyes"])
+
# Required to check for libsystemd-dev
PKG_PROG_PKG_CONFIG
diff --git a/lib/tempfile.c b/lib/tempfile.c
index dbe8bb6..7b77249 100644
--- a/lib/tempfile.c
+++ b/lib/tempfile.c
@@ -19,6 +19,7 @@
#include <fcntl.h> /* O_TMPFILE requires -D_GNU_SOURCE */
#include <stdio.h> /* fdopen() */
#include <sys/stat.h> /* umask() */
+#include <errno.h>
/**
* tempfile - A secure tmpfile() replacement
@@ -42,8 +43,13 @@ FILE *tempfile(void)
oldmask = umask(0077);
fd = open(_PATH_TMP, O_TMPFILE | O_RDWR | O_EXCL | O_CLOEXEC, S_IRUSR | S_IWUSR);
umask(oldmask);
- if (-1 == fd)
- return NULL;
+ if (fd == -1) {
+ /* Fall back to tmpfile() if O_TMPFILE is not supported */
+ if (errno == EOPNOTSUPP)
+ return tmpfile();
+
+ return NULL;
+ }
return fdopen(fd, "w+");
#else
diff --git a/man/smcroute.conf.5 b/man/smcroute.conf.5
index b3b2a54..78ba71f 100644
--- a/man/smcroute.conf.5
+++ b/man/smcroute.conf.5
@@ -1,5 +1,5 @@
.\" -*- nroff -*-
-.Dd August 15, 2021
+.Dd June 9, 2026
.Dt SMCROUTE.CONF 5
.Os
.Sh NAME
@@ -10,14 +10,12 @@ The file
.Nm
is, strictly speaking, not critical to the operation of
.Nm smcrouted .
-Some warnings may be logged, and provided the daemon is
-.Sy not
-started with
-.Fl N ,
-it enables VIFs for all interfaces it can find and then waits for
-commands from
-.Xr smcroutectl 8 .
-As you can tell from that caveat, any non-trivial setup requires an
+With no
+.Nm
+present, the daemon starts up and waits for commands from
+.Xr smcroutectl 8 ,
+allocating a VIF for each interface as it is first referenced. Any
+non-trivial setup, however, requires an
.Nm .
.Pp
On most systems the configuration file(s) are available in:
@@ -55,17 +53,25 @@ and run to the end of line. See the
below.
.Bl -tag -offset indent
.It Cm phyint Ar IFNAME Oo Cm enable | Cm disable Oc Oo Cm mrdisc Oc Oo Cm ttl-threshold Ar TTL Oc
-By default all interfaces on the system are enabled and possible to
-route between, provided they have the
-.Cm MULTICAST
-interface flag set. The
-.Cm phyint
-directive can be used to selectively enable, or disable, interfaces from
-being mapped to virtual interfaces (VIFs), which the multicast routing
-stack actually employs. VIFs are limited, most operating systems only
-have 32, it is recommended to disable all interfaces by default, with
-.Ql Cm smcrouted Fl N ,
-and enable them one by one using this directive.
+.Nm smcrouted
+maps interfaces to virtual interfaces (VIFs), which the multicast
+routing stack actually employs. VIFs are allocated on demand, the
+first time an interface is referenced from
+.Nm
+or
+.Xr smcroutectl 8 ,
+so this directive is only needed when you want to set per-interface
+options such as
+.Cm ttl-threshold
+or
+.Cm mrdisc ,
+or to keep the interface allocated even though no
+.Cm mroute
+or
+.Cm mgroup
+line references it. VIFs are a limited resource — most operating
+systems support 32 in total — so on hosts with many interfaces only
+list the ones you actually need.
.Pp
.Cm mrdisc
is an IPv4 specific feature flag to enable Multicast Router Discovery
@@ -155,6 +161,16 @@ kernel, not
To add a (*,G) route, either leave SOURCE out completely or set it to
0.0.0.0, and if you want to specify a range, set GROUP/LEN, e.g.
225.0.0.0/24.
+.Pp
+If
+.Cm IIF
+does not yet exist when the configuration is read, the directive is
+queued. On Linux it is activated automatically as soon as the
+interface arrives; on other systems on the next reload. Use
+.Ql smcroutectl show pending
+to inspect what is waiting. The same applies to
+.Cm mgroup
+directives.
.It Cm include Ar PATH
Include another
.Nm
diff --git a/man/smcroutectl.8 b/man/smcroutectl.8
index b2cc006..3b2323c 100644
--- a/man/smcroutectl.8
+++ b/man/smcroutectl.8
@@ -1,5 +1,5 @@
.\" -*- nroff -*-
-.Dd November 28, 2021
+.Dd June 9, 2026
.Dt SMCROUTECTL 8 SMM
.Os
.Sh NAME
@@ -140,16 +140,14 @@ can be any multicast capable network interface as listed by
.Ql Cm ifconfig
or
.Ql Cm ip link list ,
-including tunnel interfaces and loopback. Provided
+including tunnel interfaces and loopback.
.Nm smcrouted
-has "enumerated" them. See
-.Xr smcrouted 8 ,
-in particular the command line option
-.Fl N ,
-and the
+enumerates them on demand, so no
.Xr smcroute.conf 5
.Ql Cm phyint
-directive.
+directive is required up front; see
+.Xr smcrouted 8
+for details.
.Pp
To add a (*,G) route, either omit the
.Ar SOURCE
@@ -221,9 +219,18 @@ Note, any routes or groups added or removed with
will be lost. Only the configuration set in the file
.Pa smcroute.conf
is activated.
-.It Nm show [groups|routes]
-Show joined multicast groups or multicast routes, defaults to show
-routes. Can be combined with the
+.It Nm show [groups|interfaces|pending|routes]
+Show joined multicast groups, configured interfaces, pending routes
+and groups, or multicast routes — defaults to routes.
+.Cm pending
+lists
+.Cm mroute
+and
+.Cm mgroup
+directives whose inbound interface is not yet present on the system.
+On Linux they are activated automatically as soon as the interface
+arrives (kernel netlink link events). On other systems they are
+activated on the next configuration reload. Can be combined with the
.Fl d
option to get details for each multicast route.
.It Nm version
diff --git a/man/smcrouted.8 b/man/smcrouted.8
index f0ed448..8143977 100644
--- a/man/smcrouted.8
+++ b/man/smcrouted.8
@@ -1,5 +1,5 @@
.\" -*- nroff -*-
-.Dd November 28, 2021
+.Dd June 9, 2026
.Dt SMCROUTED 8 SMM
.Os
.Sh NAME
@@ -137,18 +137,16 @@ is built with mrdisc support (Linux, and IPv4, only). RFC4286.
.It Fl n
Run daemon in foreground, do not detach from controlling terminal
.It Fl N
-By default
-.Nm
-enables multicast routing on all available, and multicast capable,
-interfaces in the system. These interfaces are enumerated as VIFs,
-virtual interfaces, of which most UNIX systems have a very limited
-amount, usually 32. This daemon option inverts the behavior so no
-interfaces are enabled by default. Useful on systems with many
-interfaces, where multicast routing only makes use of a few.
-.Pp
-The config file setting
-.Ar phyint IFNAME enable
-is required to enable the required interfaces.
+Accepted for backwards compatibility but has no effect.
+.Nm
+now allocates virtual interfaces (VIFs) lazily, only for interfaces
+referenced by
+.Pa smcroute.conf
+or by an
+.Xr smcroutectl 8
+command. Hosts with many unrelated multicast capable interfaces
+therefore no longer risk exhausting the kernel's VIF table (usually
+32 slots) just by starting the daemon.
.It Fl p Ar USER Op :GROUP
Drop root privileges to USER:GROUP after start and retain CAP_NET_ADMIN
capabilities only. The :GROUP is optional. This option is only
@@ -225,35 +223,49 @@ When
.Nm
starts up it scans for available network interfaces that have the
.Cm MULTICAST
-flag set. Provided the
-.Fl N
-flag is not set, each interface is enumerated as a virtual interface
-(VIF) which is what the kernel's multicast routing stack uses. The
-enumeration process on some operating systems also require each
-interface to have an IP address, but Linux and FreeBSD systems only
-require the ifindex and the MULTICAST flag. If the interface does not
-yet exist when
-.Nm
-starts, the
-.Fl d Ar SEC
-flag can be used to delay startup. Otherwise
+flag set. A virtual interface (VIF) — what the kernel's multicast
+routing stack uses — is then allocated on demand, the first time an
+interface is named in
+.Pa smcroute.conf
+or in an
+.Xr smcroutectl 8
+command. The enumeration process on some operating systems also
+requires each interface to have an IP address, but Linux and FreeBSD
+systems only require the ifindex and the MULTICAST flag.
+.Pp
+If an
+.Cm mroute
+or
+.Cm mgroup
+directive references an interface that does not yet exist,
.Nm
-needs to be reloaded (e.g., using SIGHUP) when a new interface has been
-added to the system.
+keeps the directive on an internal pending list — visible with
+.Ql smcroutectl show pending —
+rather than dropping it with a parse error.
.Pp
-Since VIFs are a limited resource, most operating systems only support
-32 in total, the administrator may need to declare which interfaces to
-use for multicast routing using the
-.Pa /etc/smcroute.conf
-.Cm phyint
-directive. It is recommended to always start
+On Linux,
.Nm
-with the
-.Fl N
-flag, disabling VIF creation by default, and then selectively enable
-each of the interfaces you are going to route between. See
+subscribes to kernel link events via netlink
+.Pq Cm RTNLGRP_LINK
+and activates pending entries automatically as soon as their inbound
+interface arrives — no
+.Cm SIGHUP
+or
+.Ql smcroutectl reload
+required. On systems without the netlink listener the pending entries
+are activated on the next reload; the
+.Fl d Ar SEC
+flag may be used to delay startup if a more synchronous behaviour is
+preferred.
+.Pp
+VIFs are a limited resource — most operating systems only support 32
+in total — but because VIFs are now allocated on demand the limit is
+only reached if the configuration actually references that many
+interfaces. See
.Xr smcroute.conf 5
-for more information.
+for the
+.Cm phyint
+directive.
.Ss Multicast Scoping
Because multicast inherently is broadcast there is an obvious need to
limit. On a LAN this is usually managed automatically by bridges
diff --git a/smcroute b/smcroute
deleted file mode 100755
index bc3e744..0000000
--- a/smcroute
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/bin/sh
-# Compatibility wrapper for users with old startup scripts
-# Written by Joachim Wiberg, placed in the public domain
-
-OP=$1
-shift
-
-case "$OP" in
- -d)
- smcrouted $*
- ;;
- -h)
- echo "Usage: smcroute [OPTIONS] [ARGS]"
- echo
- echo " -d Start daemon"
- echo " -k Kill a running daemon"
- echo
- echo " -h This help text"
- echo " -v Show version"
- echo
- echo " -a ARGS Add a multicast route"
- echo " -r ARGS Remove a multicast route"
- echo
- echo " -j ARGS Join a multicast group"
- echo " -l ARGS Leave a multicast group"
- echo
- echo " <------------- INBOUND --------------> <----- OUTBOUND ------>"
- echo " -a <IFNAME> <SOURCE-IP> <MULTICAST-GROUP> <IFNAME> [<IFNAME> ...]"
- echo " -r <IFNAME> <SOURCE-IP> <MULTICAST-GROUP>"
- echo
- echo " -j <IFNAME> <MULTICAST-GROUP>"
- echo " -l <IFNAME> <MULTICAST-GROUP>"
- echo
- echo "NOTE: This is a compatibility wrapper script for SMCRoute. Intended for"
- echo " use with old style startup scripts. It is recommended to migrate"
- echo " to /etc/smcroute.conf, see the smcroute(8) man page for help."
- return 0
- ;;
- -k)
- smcroutectl kill
- ;;
- -v)
- smcroutectl version
- ;;
- -a)
- smcroutectl add $*
- ;;
- -r)
- smcroutectl remove $*
- ;;
- -j)
- smcroutectl join $*
- ;;
- *)
- echo "Unknown command or option to the SMCRoute compatiblity wrapper script."
- echo "See the smcroute(8) man page for help on available commands."
- return 1
- ;;
-esac
diff --git a/smcroute.conf b/smcroute.conf
index 45c1625..cce167a 100644
--- a/smcroute.conf
+++ b/smcroute.conf
@@ -26,8 +26,10 @@
# mroute from IIF [source ADDR[/LEN]] group GROUP[/LEN] to OIF [OIF ...]
# include /path/to/*.conf
-# This example assumes smcrouted was started with the `-N` flag.
-# Only enable interfaces required for inbound and outbound traffic.
+# VIFs are allocated on demand for interfaces named below; the daemon
+# no longer enumerates every multicast-capable interface up front.
+# Use `phyint` only when you want to override the default TTL threshold
+# or enable mrdisc; an mroute or mgroup line is enough by itself.
phyint eth0 enable ttl-threshold 11
phyint eth1 enable ttl-threshold 3
phyint eth2 enable ttl-threshold 5
@@ -56,6 +58,13 @@ mroute from eth0 group 225.3.2.1 to eth1 eth2
mgroup from eth0 group 225.0.0.0/24
mroute from eth0 group 225.0.0.0/24 to eth1 eth2
+# An mroute whose inbound or outbound interface does not yet exist is
+# kept on a pending list rather than dropped. Useful for tunnels that
+# come up after smcrouted starts (WireGuard, 6LoWPAN, etc.); the route
+# is activated when the interface arrives. Inspect the queue with
+# `smcroutectl show pending`.
+mroute from wg0 group 239.255.255.250 to eth0
+
# Include any snippet in /etc/smcroute.d/, but please remember that
# all phyint statements must be read first.
include /etc/smcroute.d/*.conf
diff --git a/src/Makefile.am b/src/Makefile.am
index ef742d6..04a571a 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,11 +1,12 @@
AUTOMAKE_OPTIONS = subdir-objects
sbin_PROGRAMS = smcrouted smcroutectl
-smcrouted_SOURCES = smcrouted.c conf.c conf.h mroute.c mroute.h iface.c \
- iface.h inet.c inet.h ipc.c ipc.h kern.c kern.h \
+smcrouted_SOURCES = smcrouted.c conf.c conf.h ifevent.h mroute.c mroute.h \
+ iface.c iface.h inet.c inet.h ipc.c ipc.h kern.c kern.h \
log.c log.h mcgroup.c mcgroup.h msg.c msg.h \
- notify.c notify.h pidfile.c queue.h script.c \
- script.h socket.c socket.h timer.c timer.h util.h
+ notify.c notify.h pending.c pending.h pidfile.c \
+ queue.h script.c script.h show.h socket.c socket.h \
+ timer.c timer.h util.h
smcrouted_CFLAGS = -W -Wall -Wextra -Wno-deprecated-declarations -std=gnu99
smcrouted_CPPFLAGS = -D_ATFILE_SOURCE -D_INCOMPLETE_XOPEN_C063
@@ -27,6 +28,12 @@ if USE_MRDISC
smcrouted_SOURCES += mrdisc.c mrdisc.h
endif
+if HAVE_NETLINK
+smcrouted_SOURCES += ifevent_nl.c
+else
+smcrouted_SOURCES += ifevent_stub.c
+endif
+
smcroutectl_SOURCES = smcroutectl.c msg.h util.h
smcroutectl_CFLAGS = -W -Wall -Wextra -std=gnu99
smcroutectl_CPPFLAGS = -DRUNSTATEDIR=\"@runstatedir@\"
diff --git a/src/Makefile.in b/src/Makefile.in
index fcc7767..b3c480e 100644
--- a/src/Makefile.in
+++ b/src/Makefile.in
@@ -95,6 +95,8 @@ sbin_PROGRAMS = smcrouted$(EXEEXT) smcroutectl$(EXEEXT)
@HAVE_LIBSYSTEMD_TRUE@am__append_4 = $(libsystemd_CFLAGS)
@HAVE_LIBSYSTEMD_TRUE@am__append_5 = $(libsystemd_LIBS)
@USE_MRDISC_TRUE@am__append_6 = mrdisc.c mrdisc.h
+@HAVE_NETLINK_TRUE@am__append_7 = ifevent_nl.c
+@HAVE_NETLINK_FALSE@am__append_8 = ifevent_stub.c
subdir = src
ACLOCAL_M4 = $(top_srcdir)/aclocal.m4
am__aclocal_m4_deps = $(top_srcdir)/m4/misc.m4 \
@@ -116,23 +118,28 @@ am__dirstamp = $(am__leading_dot)dirstamp
smcroutectl_DEPENDENCIES = $(am__DEPENDENCIES_1) $(LIBOBJS)
smcroutectl_LINK = $(CCLD) $(smcroutectl_CFLAGS) $(CFLAGS) \
$(AM_LDFLAGS) $(LDFLAGS) -o $@
-am__smcrouted_SOURCES_DIST = smcrouted.c conf.c conf.h mroute.c \
- mroute.h iface.c iface.h inet.c inet.h ipc.c ipc.h kern.c \
- kern.h log.c log.h mcgroup.c mcgroup.h msg.c msg.h notify.c \
- notify.h pidfile.c queue.h script.c script.h socket.c socket.h \
- timer.c timer.h util.h cap.c cap.h systemd.c mrdisc.c mrdisc.h
+am__smcrouted_SOURCES_DIST = smcrouted.c conf.c conf.h ifevent.h \
+ mroute.c mroute.h iface.c iface.h inet.c inet.h ipc.c ipc.h \
+ kern.c kern.h log.c log.h mcgroup.c mcgroup.h msg.c msg.h \
+ notify.c notify.h pending.c pending.h pidfile.c queue.h \
+ script.c script.h show.h socket.c socket.h timer.c timer.h \
+ util.h cap.c cap.h systemd.c mrdisc.c mrdisc.h ifevent_nl.c \
+ ifevent_stub.c
@USE_LIBCAP_TRUE@am__objects_1 = smcrouted-cap.$(OBJEXT)
@HAVE_LIBSYSTEMD_TRUE@am__objects_2 = smcrouted-systemd.$(OBJEXT)
@USE_MRDISC_TRUE@am__objects_3 = smcrouted-mrdisc.$(OBJEXT)
+@HAVE_NETLINK_TRUE@am__objects_4 = smcrouted-ifevent_nl.$(OBJEXT)
+@HAVE_NETLINK_FALSE@am__objects_5 = smcrouted-ifevent_stub.$(OBJEXT)
am_smcrouted_OBJECTS = smcrouted-smcrouted.$(OBJEXT) \
smcrouted-conf.$(OBJEXT) smcrouted-mroute.$(OBJEXT) \
smcrouted-iface.$(OBJEXT) smcrouted-inet.$(OBJEXT) \
smcrouted-ipc.$(OBJEXT) smcrouted-kern.$(OBJEXT) \
smcrouted-log.$(OBJEXT) smcrouted-mcgroup.$(OBJEXT) \
smcrouted-msg.$(OBJEXT) smcrouted-notify.$(OBJEXT) \
- smcrouted-pidfile.$(OBJEXT) smcrouted-script.$(OBJEXT) \
- smcrouted-socket.$(OBJEXT) smcrouted-timer.$(OBJEXT) \
- $(am__objects_1) $(am__objects_2) $(am__objects_3)
+ smcrouted-pending.$(OBJEXT) smcrouted-pidfile.$(OBJEXT) \
+ smcrouted-script.$(OBJEXT) smcrouted-socket.$(OBJEXT) \
+ smcrouted-timer.$(OBJEXT) $(am__objects_1) $(am__objects_2) \
+ $(am__objects_3) $(am__objects_4) $(am__objects_5)
smcrouted_OBJECTS = $(am_smcrouted_OBJECTS)
@HAVE_LIBSYSTEMD_TRUE@am__DEPENDENCIES_2 = $(am__DEPENDENCIES_1)
smcrouted_DEPENDENCIES = $(am__DEPENDENCIES_1) $(LIBOBJS) \
@@ -159,12 +166,16 @@ am__depfiles_remade = ../lib/$(DEPDIR)/malloc.Po \
../lib/$(DEPDIR)/tempfile.Po ../lib/$(DEPDIR)/utimensat.Po \
./$(DEPDIR)/smcroutectl-smcroutectl.Po \
./$(DEPDIR)/smcrouted-cap.Po ./$(DEPDIR)/smcrouted-conf.Po \
- ./$(DEPDIR)/smcrouted-iface.Po ./$(DEPDIR)/smcrouted-inet.Po \
- ./$(DEPDIR)/smcrouted-ipc.Po ./$(DEPDIR)/smcrouted-kern.Po \
- ./$(DEPDIR)/smcrouted-log.Po ./$(DEPDIR)/smcrouted-mcgroup.Po \
+ ./$(DEPDIR)/smcrouted-iface.Po \
+ ./$(DEPDIR)/smcrouted-ifevent_nl.Po \
+ ./$(DEPDIR)/smcrouted-ifevent_stub.Po \
+ ./$(DEPDIR)/smcrouted-inet.Po ./$(DEPDIR)/smcrouted-ipc.Po \
+ ./$(DEPDIR)/smcrouted-kern.Po ./$(DEPDIR)/smcrouted-log.Po \
+ ./$(DEPDIR)/smcrouted-mcgroup.Po \
./$(DEPDIR)/smcrouted-mrdisc.Po \
./$(DEPDIR)/smcrouted-mroute.Po ./$(DEPDIR)/smcrouted-msg.Po \
./$(DEPDIR)/smcrouted-notify.Po \
+ ./$(DEPDIR)/smcrouted-pending.Po \
./$(DEPDIR)/smcrouted-pidfile.Po \
./$(DEPDIR)/smcrouted-script.Po \
./$(DEPDIR)/smcrouted-smcrouted.Po \
@@ -330,11 +341,13 @@ top_build_prefix = @top_build_prefix@
top_builddir = @top_builddir@
top_srcdir = @top_srcdir@
AUTOMAKE_OPTIONS = subdir-objects
-smcrouted_SOURCES = smcrouted.c conf.c conf.h mroute.c mroute.h \
- iface.c iface.h inet.c inet.h ipc.c ipc.h kern.c kern.h log.c \
- log.h mcgroup.c mcgroup.h msg.c msg.h notify.c notify.h \
- pidfile.c queue.h script.c script.h socket.c socket.h timer.c \
- timer.h util.h $(am__append_1) $(am__append_3) $(am__append_6)
+smcrouted_SOURCES = smcrouted.c conf.c conf.h ifevent.h mroute.c \
+ mroute.h iface.c iface.h inet.c inet.h ipc.c ipc.h kern.c \
+ kern.h log.c log.h mcgroup.c mcgroup.h msg.c msg.h notify.c \
+ notify.h pending.c pending.h pidfile.c queue.h script.c \
+ script.h show.h socket.c socket.h timer.c timer.h util.h \
+ $(am__append_1) $(am__append_3) $(am__append_6) \
+ $(am__append_7) $(am__append_8)
smcrouted_CFLAGS = -W -Wall -Wextra -Wno-deprecated-declarations \
-std=gnu99 $(am__append_4)
smcrouted_CPPFLAGS = -D_ATFILE_SOURCE -D_INCOMPLETE_XOPEN_C063 \
@@ -452,6 +465,8 @@ distclean-compile:
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-cap.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-conf.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-iface.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-ifevent_nl.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-ifevent_stub.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-inet.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-ipc.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-kern.Po@am__quote@ # am--include-marker
@@ -461,6 +476,7 @@ distclean-compile:
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-mroute.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-msg.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-notify.Po@am__quote@ # am--include-marker
+@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-pending.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-pidfile.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-script.Po@am__quote@ # am--include-marker
@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/smcrouted-smcrouted.Po@am__quote@ # am--include-marker
@@ -658,6 +674,20 @@ smcrouted-notify.obj: notify.c
@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-notify.obj `if test -f 'notify.c'; then $(CYGPATH_W) 'notify.c'; else $(CYGPATH_W) '$(srcdir)/notify.c'; fi`
+smcrouted-pending.o: pending.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -MT smcrouted-pending.o -MD -MP -MF $(DEPDIR)/smcrouted-pending.Tpo -c -o smcrouted-pending.o `test -f 'pending.c' || echo '$(srcdir)/'`pending.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/smcrouted-pending.Tpo $(DEPDIR)/smcrouted-pending.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='pending.c' object='smcrouted-pending.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-pending.o `test -f 'pending.c' || echo '$(srcdir)/'`pending.c
+
+smcrouted-pending.obj: pending.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -MT smcrouted-pending.obj -MD -MP -MF $(DEPDIR)/smcrouted-pending.Tpo -c -o smcrouted-pending.obj `if test -f 'pending.c'; then $(CYGPATH_W) 'pending.c'; else $(CYGPATH_W) '$(srcdir)/pending.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/smcrouted-pending.Tpo $(DEPDIR)/smcrouted-pending.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='pending.c' object='smcrouted-pending.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-pending.obj `if test -f 'pending.c'; then $(CYGPATH_W) 'pending.c'; else $(CYGPATH_W) '$(srcdir)/pending.c'; fi`
+
smcrouted-pidfile.o: pidfile.c
@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -MT smcrouted-pidfile.o -MD -MP -MF $(DEPDIR)/smcrouted-pidfile.Tpo -c -o smcrouted-pidfile.o `test -f 'pidfile.c' || echo '$(srcdir)/'`pidfile.c
@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/smcrouted-pidfile.Tpo $(DEPDIR)/smcrouted-pidfile.Po
@@ -756,6 +786,34 @@ smcrouted-mrdisc.obj: mrdisc.c
@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-mrdisc.obj `if test -f 'mrdisc.c'; then $(CYGPATH_W) 'mrdisc.c'; else $(CYGPATH_W) '$(srcdir)/mrdisc.c'; fi`
+smcrouted-ifevent_nl.o: ifevent_nl.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -MT smcrouted-ifevent_nl.o -MD -MP -MF $(DEPDIR)/smcrouted-ifevent_nl.Tpo -c -o smcrouted-ifevent_nl.o `test -f 'ifevent_nl.c' || echo '$(srcdir)/'`ifevent_nl.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/smcrouted-ifevent_nl.Tpo $(DEPDIR)/smcrouted-ifevent_nl.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ifevent_nl.c' object='smcrouted-ifevent_nl.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-ifevent_nl.o `test -f 'ifevent_nl.c' || echo '$(srcdir)/'`ifevent_nl.c
+
+smcrouted-ifevent_nl.obj: ifevent_nl.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -MT smcrouted-ifevent_nl.obj -MD -MP -MF $(DEPDIR)/smcrouted-ifevent_nl.Tpo -c -o smcrouted-ifevent_nl.obj `if test -f 'ifevent_nl.c'; then $(CYGPATH_W) 'ifevent_nl.c'; else $(CYGPATH_W) '$(srcdir)/ifevent_nl.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/smcrouted-ifevent_nl.Tpo $(DEPDIR)/smcrouted-ifevent_nl.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ifevent_nl.c' object='smcrouted-ifevent_nl.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-ifevent_nl.obj `if test -f 'ifevent_nl.c'; then $(CYGPATH_W) 'ifevent_nl.c'; else $(CYGPATH_W) '$(srcdir)/ifevent_nl.c'; fi`
+
+smcrouted-ifevent_stub.o: ifevent_stub.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -MT smcrouted-ifevent_stub.o -MD -MP -MF $(DEPDIR)/smcrouted-ifevent_stub.Tpo -c -o smcrouted-ifevent_stub.o `test -f 'ifevent_stub.c' || echo '$(srcdir)/'`ifevent_stub.c
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/smcrouted-ifevent_stub.Tpo $(DEPDIR)/smcrouted-ifevent_stub.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ifevent_stub.c' object='smcrouted-ifevent_stub.o' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-ifevent_stub.o `test -f 'ifevent_stub.c' || echo '$(srcdir)/'`ifevent_stub.c
+
+smcrouted-ifevent_stub.obj: ifevent_stub.c
+@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -MT smcrouted-ifevent_stub.obj -MD -MP -MF $(DEPDIR)/smcrouted-ifevent_stub.Tpo -c -o smcrouted-ifevent_stub.obj `if test -f 'ifevent_stub.c'; then $(CYGPATH_W) 'ifevent_stub.c'; else $(CYGPATH_W) '$(srcdir)/ifevent_stub.c'; fi`
+@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/smcrouted-ifevent_stub.Tpo $(DEPDIR)/smcrouted-ifevent_stub.Po
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='ifevent_stub.c' object='smcrouted-ifevent_stub.obj' libtool=no @AMDEPBACKSLASH@
+@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@
+@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(smcrouted_CPPFLAGS) $(CPPFLAGS) $(smcrouted_CFLAGS) $(CFLAGS) -c -o smcrouted-ifevent_stub.obj `if test -f 'ifevent_stub.c'; then $(CYGPATH_W) 'ifevent_stub.c'; else $(CYGPATH_W) '$(srcdir)/ifevent_stub.c'; fi`
+
ID: $(am__tagged_files)
$(am__define_uniq_tagged_files); mkid -fID $$unique
tags: tags-am
@@ -897,6 +955,8 @@ distclean: distclean-am
-rm -f ./$(DEPDIR)/smcrouted-cap.Po
-rm -f ./$(DEPDIR)/smcrouted-conf.Po
-rm -f ./$(DEPDIR)/smcrouted-iface.Po
+ -rm -f ./$(DEPDIR)/smcrouted-ifevent_nl.Po
+ -rm -f ./$(DEPDIR)/smcrouted-ifevent_stub.Po
-rm -f ./$(DEPDIR)/smcrouted-inet.Po
-rm -f ./$(DEPDIR)/smcrouted-ipc.Po
-rm -f ./$(DEPDIR)/smcrouted-kern.Po
@@ -906,6 +966,7 @@ distclean: distclean-am
-rm -f ./$(DEPDIR)/smcrouted-mroute.Po
-rm -f ./$(DEPDIR)/smcrouted-msg.Po
-rm -f ./$(DEPDIR)/smcrouted-notify.Po
+ -rm -f ./$(DEPDIR)/smcrouted-pending.Po
-rm -f ./$(DEPDIR)/smcrouted-pidfile.Po
-rm -f ./$(DEPDIR)/smcrouted-script.Po
-rm -f ./$(DEPDIR)/smcrouted-smcrouted.Po
@@ -966,6 +1027,8 @@ maintainer-clean: maintainer-clean-am
-rm -f ./$(DEPDIR)/smcrouted-cap.Po
-rm -f ./$(DEPDIR)/smcrouted-conf.Po
-rm -f ./$(DEPDIR)/smcrouted-iface.Po
+ -rm -f ./$(DEPDIR)/smcrouted-ifevent_nl.Po
+ -rm -f ./$(DEPDIR)/smcrouted-ifevent_stub.Po
-rm -f ./$(DEPDIR)/smcrouted-inet.Po
-rm -f ./$(DEPDIR)/smcrouted-ipc.Po
-rm -f ./$(DEPDIR)/smcrouted-kern.Po
@@ -975,6 +1038,7 @@ maintainer-clean: maintainer-clean-am
-rm -f ./$(DEPDIR)/smcrouted-mroute.Po
-rm -f ./$(DEPDIR)/smcrouted-msg.Po
-rm -f ./$(DEPDIR)/smcrouted-notify.Po
+ -rm -f ./$(DEPDIR)/smcrouted-pending.Po
-rm -f ./$(DEPDIR)/smcrouted-pidfile.Po
-rm -f ./$(DEPDIR)/smcrouted-script.Po
-rm -f ./$(DEPDIR)/smcrouted-smcrouted.Po
diff --git a/src/cap.c b/src/cap.c
index b52e1ff..f5ad6ae 100644
--- a/src/cap.c
+++ b/src/cap.c
@@ -1,7 +1,7 @@
/* Daemon capability API
*
* Copyright (C) 2016 Markus Palonen <markus.palonen@gmail.com>
- * Copyright (C) 2016-2020 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2016-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/conf.c b/src/conf.c
index 5ae6f9d..d38a801 100644
--- a/src/conf.c
+++ b/src/conf.c
@@ -1,6 +1,6 @@
/* Simple .conf file parser for smcroute
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
@@ -23,11 +23,14 @@
#include <sysexits.h>
#include <unistd.h>
#include <arpa/inet.h>
+#include <net/if.h>
#include <netinet/in.h>
#include "log.h"
#include "conf.h"
#include "iface.h"
+#include "mroute.h"
+#include "pending.h"
#include "script.h"
#include "mcgroup.h"
#include "util.h"
@@ -153,17 +156,32 @@ int conf_mgroup(struct conf *conf, int cmd, char *iif, char *source, char *group
if (!src_len)
src_len = len_max;
- if (!conf_vrfy)
- rc += mcgroup_action(cmd, iif, &src, src_len, &grp, grp_len);
+ if (!conf_vrfy) {
+ if (cmd)
+ mroute_ensure_vif(iif);
+ rc = mcgroup_action(cmd, iif, &src, src_len, &grp, grp_len);
+ }
done:
return rc;
}
+/* Classify so the user does not need debug logging to see the cause. */
+#define WARN_UNUSABLE_PHYINT(role, ifname) do { \
+ struct iface *_if = iface_find_by_name(ifname); \
+ if (!_if) \
+ WARN("mroute: " role " %s is not a known phyint", ifname); \
+ else if ((_if->flags & IFF_MULTICAST) != IFF_MULTICAST) \
+ WARN("mroute: " role " %s is not multicast capable", ifname); \
+ else \
+ WARN("mroute: " role " %s has no VIF, VIF table exhausted?", ifname); \
+ } while (0)
+
int conf_mroute(struct conf *conf, int cmd, char *iif, char *source, char *group, char *oif[], int num)
{
struct ifmatch state_in, state_out;
struct mroute mroute = { 0 };
struct iface *iface_in, *iface;
+ int oif_unresolved = 0;
int len_max;
int family;
int rc = 0;
@@ -212,6 +230,12 @@ int conf_mroute(struct conf *conf, int cmd, char *iif, char *source, char *group
if (!mroute.src_len)
mroute.src_len = len_max;
+ if (!conf_vrfy && cmd) {
+ mroute_ensure_vif(iif);
+ for (int i = 0; i < num; i++)
+ mroute_ensure_vif(oif[i]);
+ }
+
iface_match_init(&state_in);
DEBUG("mroute: checking for input iface %s ...", iif);
while (iface_match_vif_by_name(iif, &state_in, &iface_in) != NO_VIF) {
@@ -240,8 +264,14 @@ int conf_mroute(struct conf *conf, int cmd, char *iif, char *source, char *group
/* Use configured TTL threshold for the output phyint */
mroute.ttl[vif] = iface->threshold;
}
- if (!state_out.match_count)
- WARN("mroute: outbound %s is not a known phyint, skipping", oif[i]);
+ if (!state_out.match_count) {
+ int defer = cmd && !conf_vrfy && !iface_exist(oif[i]);
+
+ if (defer)
+ oif_unresolved = 1;
+ else
+ WARN_UNUSABLE_PHYINT("outbound", oif[i]);
+ }
}
if (conf_vrfy)
@@ -251,18 +281,22 @@ int conf_mroute(struct conf *conf, int cmd, char *iif, char *source, char *group
smclog(LOG_DEBUG, "mroute: adding route from %s (%s/%u,%s/%u)", iface_in->ifname,
inet_addr2str(&mroute.source, src, sizeof(src)), mroute.src_len,
inet_addr2str(&mroute.group, grp, sizeof(grp)), mroute.len);
- rc += mroute_add_route(&mroute);
+ if (mroute_add_route(&mroute))
+ rc = -1;
} else {
smclog(LOG_DEBUG, "mroute: deleting route from %s (%s/%u,%s/%u)", iface_in->ifname,
inet_addr2str(&mroute.source, src, sizeof(src)), mroute.src_len,
inet_addr2str(&mroute.group, grp, sizeof(grp)), mroute.len);
- rc += mroute_del_route(&mroute);
+ if (mroute_del_route(&mroute))
+ rc = -1;
}
}
if (!state_in.match_count) {
- WARN("mroute: inbound %s is not a known phyint", iif);
- rc++;
+ WARN_UNUSABLE_PHYINT("inbound", iif);
+ rc = -1;
+ } else if (oif_unresolved) {
+ pending_add_mroute(iif, source, group, oif, num);
}
done:
@@ -407,11 +441,11 @@ next:
if (iif && !iface_exist(iif)) {
switch (op) {
case MGROUP:
- WARN("mgroup from %s matches no valid phyint, skipping ...", iif);
+ pending_add_mgroup(iif, source, group);
break;
case MROUTE:
- WARN("mroute from %s matches no valid phyint, skipping ...", iif);
+ pending_add_mroute(iif, source, group, oif, num);
break;
default:
@@ -435,15 +469,18 @@ next:
break;
case MGROUP:
- rc += conf_mgroup(conf, 1, iif, source, group);
+ if (conf_mgroup(conf, 1, iif, source, group))
+ rc = -1;
break;
case MROUTE:
- rc += conf_mroute(conf, 1, iif, source, group, oif, num);
+ if (conf_mroute(conf, 1, iif, source, group, oif, num))
+ rc = -1;
break;
case PHYINT:
- rc += conf_phyint(conf, enable, iif, mrdisc, threshold);
+ if (conf_phyint(conf, enable, iif, mrdisc, threshold))
+ rc = -1;
break;
case INCLUDE:
diff --git a/src/iface.c b/src/iface.c
index 903b974..b94cd89 100644
--- a/src/iface.c
+++ b/src/iface.c
@@ -4,7 +4,7 @@
* Copyright (C) 2006-2009 Julien BLACHE <jb@jblache.org>
* Copyright (C) 2009 Todd Hayton <todd.hayton@gmail.com>
* Copyright (C) 2009-2011 Micha Lenk <micha@debian.org>
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -78,9 +78,6 @@ void iface_update(void)
if (!iface->inaddr.s_addr && ifa->ifa_addr && ifa->ifa_addr->sa_family == AF_INET)
iface->inaddr = ((struct sockaddr_in *)ifa->ifa_addr)->sin_addr;
- if (do_vifs)
- iface->unused = 0;
-
continue;
}
@@ -426,14 +423,39 @@ mifi_t iface_match_mif_by_name(const char *ifname, struct ifmatch *state, struct
}
/* Return all currently known interfaces */
-int iface_show(int sd, int detail)
+int iface_show(int sd, enum show_mode mode)
{
struct iface *iface;
char *p = "PHYINT";
- char line[120];
+ char line[256];
int inw;
- (void)detail;
+ if (mode == SHOW_JSON) {
+ int first = 1;
+ const char *open = "{\"interfaces\":[";
+
+ ipc_send(sd, open, strlen(open));
+ iface = iface_iterator(1);
+ while (iface) {
+ char vif[12] = "null", mif[12] = "null";
+
+ if (iface->vif < 65535)
+ snprintf(vif, sizeof(vif), "%d", iface->vif);
+ if (iface->mif < 65535)
+ snprintf(mif, sizeof(mif), "%d", iface->mif);
+
+ snprintf(line, sizeof(line),
+ "%s{\"index\":%d,\"name\":\"%s\",\"vif\":%s,\"mif\":%s}",
+ first ? "" : ",", iface->ifindex,
+ iface->ifname, vif, mif);
+ if (ipc_send(sd, line, strlen(line)) < 0)
+ return -1;
+ first = 0;
+ iface = iface_iterator(0);
+ }
+ ipc_send(sd, "]}\n", 3);
+ return 0;
+ }
inw = iface_ifname_maxlen();
if (inw < (int)strlen(p))
diff --git a/src/iface.h b/src/iface.h
index 4019b6a..bcbe1b6 100644
--- a/src/iface.h
+++ b/src/iface.h
@@ -9,6 +9,7 @@
#include <netinet/in.h> /* struct in_addr */
#include "mroute.h" /* vifit_t + mifi_t */
+#include "show.h"
#ifndef ALL_MIFS
#define ALL_MIFS (vifi_t)-1
@@ -55,7 +56,7 @@ vifi_t iface_get_vif (int af_family, struct iface *iface);
vifi_t iface_match_vif_by_name (const char *ifname, struct ifmatch *state, struct iface **found);
mifi_t iface_match_mif_by_name (const char *ifname, struct ifmatch *state, struct iface **found);
-int iface_show (int sd, int detail);
+int iface_show (int sd, enum show_mode mode);
/*
* Check if interface exists, at all, on the system
diff --git a/src/ifevent.h b/src/ifevent.h
new file mode 100644
index 0000000..dfc8f40
--- /dev/null
+++ b/src/ifevent.h
@@ -0,0 +1,14 @@
+/* Kernel interface event listener.
+ *
+ * On platforms that support it (currently Linux netlink), ifevent_init()
+ * subscribes to interface arrival/removal/flag events and refreshes the
+ * iface list plus drains the pending list on each event. On other
+ * platforms it is a no-op stub.
+ */
+#ifndef SMCROUTE_IFEVENT_H_
+#define SMCROUTE_IFEVENT_H_
+
+void ifevent_init(void);
+void ifevent_exit(void);
+
+#endif /* SMCROUTE_IFEVENT_H_ */
diff --git a/src/ifevent_nl.c b/src/ifevent_nl.c
new file mode 100644
index 0000000..ec0d483
--- /dev/null
+++ b/src/ifevent_nl.c
@@ -0,0 +1,97 @@
+/* Linux netlink listener for interface arrival/removal events.
+ *
+ * Subscribes to RTNLGRP_LINK plus the IPv4/IPv6 address groups, refreshes
+ * the iface list on any event, and drains the pending list so routes/
+ * groups whose inbound interface just arrived install automatically.
+ *
+ * Copyright (C) 2026 Joachim Wiberg <troglobit@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+
+#include "ifevent.h"
+#include "iface.h"
+#include "log.h"
+#include "pending.h"
+#include "socket.h"
+
+static int nl_sd = -1;
+
+static void nl_cb(int sd, void *arg)
+{
+ char buf[8192];
+ ssize_t n;
+ int got = 0;
+
+ (void)arg;
+
+ /* Drain whatever the kernel has queued; we don't parse the
+ * payload — being woken up is the only signal we need. */
+ do {
+ n = recv(sd, buf, sizeof(buf), MSG_DONTWAIT);
+ if (n > 0)
+ got = 1;
+ } while (n > 0);
+
+ if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK)
+ smclog(LOG_WARNING, "netlink recv: %s", strerror(errno));
+
+ if (!got)
+ return;
+
+ iface_update();
+ pending_drain();
+}
+
+void ifevent_init(void)
+{
+ struct sockaddr_nl addr = { 0 };
+ int type = SOCK_RAW;
+
+#ifdef HAVE_SOCK_CLOEXEC
+ type |= SOCK_CLOEXEC;
+#endif
+ nl_sd = socket(AF_NETLINK, type, NETLINK_ROUTE);
+ if (nl_sd < 0) {
+ smclog(LOG_ERR, "Failed opening netlink socket: %s", strerror(errno));
+ return;
+ }
+
+ addr.nl_family = AF_NETLINK;
+ addr.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR;
+ if (bind(nl_sd, (struct sockaddr *)&addr, sizeof(addr))) {
+ smclog(LOG_ERR, "Failed binding netlink socket: %s", strerror(errno));
+ close(nl_sd);
+ nl_sd = -1;
+ return;
+ }
+
+ socket_register(nl_sd, nl_cb, NULL);
+}
+
+void ifevent_exit(void)
+{
+ if (nl_sd < 0)
+ return;
+ socket_close(nl_sd);
+ nl_sd = -1;
+}
+
+/**
+ * Local Variables:
+ * indent-tabs-mode: t
+ * c-file-style: "linux"
+ * End:
+ */
diff --git a/src/ifevent_stub.c b/src/ifevent_stub.c
new file mode 100644
index 0000000..65d875a
--- /dev/null
+++ b/src/ifevent_stub.c
@@ -0,0 +1,25 @@
+/* No-op fallback for platforms without a kernel link-event listener.
+ *
+ * Reload-driven activation (SIGHUP / `smcroutectl reload`) and the
+ * `-d SEC` startup delay remain the recommended workaround on these
+ * platforms. A BSD route-socket equivalent is tracked separately.
+ *
+ * Copyright (C) 2026 Joachim Wiberg <troglobit@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include "ifevent.h"
+
+void ifevent_init(void) { }
+void ifevent_exit(void) { }
+
+/**
+ * Local Variables:
+ * indent-tabs-mode: t
+ * c-file-style: "linux"
+ * End:
+ */
diff --git a/src/inet.c b/src/inet.c
index 7d0dd35..159ee72 100644
--- a/src/inet.c
+++ b/src/inet.c
@@ -1,6 +1,6 @@
/* Housekeeping IPv4/IPv6 wrapper functions
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -116,14 +116,14 @@ inet_addr_t inet_netaddr(inet_addr_t *addr, int len)
bits -= 32;
}
- s6.s6_addr32[pos] = htonl(ntohl(s6.s6_addr32[pos]) & (0xffffffffu << bits));
+ s6.s6_addr32[pos] = htonl(ntohl(s6.s6_addr32[pos]) & ((0xffffffffU << bits) & 0xffffffffU));
sin6->sin6_addr = s6;
} else
#endif
{
struct in_addr *ina = inet_addr_get(&net);
- ina->s_addr = htonl(ntohl(ina->s_addr) & (0xffffffffu << bits));
+ ina->s_addr = htonl(ntohl(ina->s_addr) & ((0xffffffffU << bits) & 0xffffffffU));
}
return net;
diff --git a/src/inet.h b/src/inet.h
index f3b92b8..051750d 100644
--- a/src/inet.h
+++ b/src/inet.h
@@ -1,6 +1,6 @@
/* Housekeeping IPv4/IPv6 wrapper functions
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/ipc.c b/src/ipc.c
index 784bd1f..81c4803 100644
--- a/src/ipc.c
+++ b/src/ipc.c
@@ -4,7 +4,7 @@
* Copyright (C) 2006-2009 Julien BLACHE <jb@jblache.org>
* Copyright (C) 2009 Todd Hayton <todd.hayton@gmail.com>
* Copyright (C) 2009-2011 Micha Lenk <micha@debian.org>
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -57,9 +57,8 @@ static void ipc_read(int sd)
preallocate ipc_msg only once in advance */
char msg_buf[sizeof(struct ipc_msg) + CMD_MAX_WORDS * sizeof(char *)];
char buf[MX_CMDPKT_SZ];
- const char* buf_ptr;
- ssize_t rx = 0, rx_curr;
int first_call = 1;
+ ssize_t pos = 0;
memset(buf, 0, sizeof(buf));
@@ -69,32 +68,40 @@ static void ipc_read(int sd)
* all data
*/
while (1) {
- rx_curr = ipc_receive(sd, buf + rx, sizeof(buf) - rx, first_call);
+ const char* ptr;
+ ssize_t rc;
+
+ rc = ipc_receive(sd, buf + pos, sizeof(buf) - pos - 1, first_call);
first_call = 0;
- if (rx_curr <= 0) {
+ if (rc <= 0) {
if (errno == EAGAIN) /* no more data from client */
return;
if (errno != ECONNRESET) /* Skip logging client disconnects */
smclog(LOG_WARNING, "Failed receiving IPC message from client: %s", strerror(errno));
return;
}
- rx += rx_curr;
+
+ pos += rc;
+ if (pos > (int)sizeof(buf) - 1) {
+ smclog(LOG_WARNING, "Too large IPC message, unsupported.");
+ return;
+ }
/* Make sure to always have at least one NUL, for strlen() */
- buf[rx] = 0;
+ buf[pos] = 0;
- buf_ptr = buf;
- while (rx > 0) {
+ ptr = buf;
+ while (pos > 0) {
struct ipc_msg* msg = (struct ipc_msg*)msg_buf;
/* extract one command at a time */
- if (ipc_parse(buf_ptr, rx, msg)) {
+ if (ipc_parse(ptr, pos, msg)) {
if (EAGAIN == errno) {
/*
* need more data from client? move last unused bytes (if any) to
* the begging of the buffer and lets try to receive more data
*/
- memmove(buf, buf_ptr, rx);
+ memmove(buf, ptr, pos);
break;
}
smclog(LOG_WARNING, "Failed to parse IPC message from client: %s", strerror(errno));
@@ -111,8 +118,8 @@ static void ipc_read(int sd)
}
/* shift to the next command if any and reduce remaining bytes in buffer */
- buf_ptr += msg->len;
- rx -= msg->len;
+ ptr += msg->len;
+ pos -= msg->len;
}
}
}
diff --git a/src/kern.c b/src/kern.c
index 8367220..48c29d3 100644
--- a/src/kern.c
+++ b/src/kern.c
@@ -1,6 +1,6 @@
/* Kernel API for join/leave multicast groups and add/del routes
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
diff --git a/src/kern.h b/src/kern.h
index c4fed97..0557fcb 100644
--- a/src/kern.h
+++ b/src/kern.h
@@ -1,6 +1,6 @@
/* Kernel API for join/leave multicast groups and add/del routes
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
diff --git a/src/log.c b/src/log.c
index 8401daa..f9aff4c 100644
--- a/src/log.c
+++ b/src/log.c
@@ -1,6 +1,6 @@
/* System logging API
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/mcgroup.c b/src/mcgroup.c
index b92f2d1..c33501f 100644
--- a/src/mcgroup.c
+++ b/src/mcgroup.c
@@ -4,7 +4,7 @@
* Copyright (C) 2006-2009 Julien BLACHE <jb@jblache.org>
* Copyright (C) 2009 Todd Hayton <todd.hayton@gmail.com>
* Copyright (C) 2009-2011 Micha Lenk <micha@debian.org>
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -468,14 +468,74 @@ static int show_mcgroup(int sd, struct mcgroup *entry)
return 0;
}
+static int show_mcgroup_json(int sd, struct mcgroup *entry, int first)
+{
+ int max_len = inet_max_len(&entry->group);
+ char src[INET_ADDRSTR_LEN] = "";
+ char grp[INET_ADDRSTR_LEN];
+ char src_field[INET_ADDRSTR_LEN + 16];
+ char grp_field[INET_ADDRSTR_LEN + 16];
+ char line[512];
+
+ if (is_anyaddr(&entry->source)) {
+ snprintf(src_field, sizeof(src_field), "null");
+ } else {
+ inet_addr2str(&entry->source, src, sizeof(src));
+ if (entry->src_len != max_len)
+ snprintf(src_field, sizeof(src_field), "\"%s/%u\"", src, entry->src_len);
+ else
+ snprintf(src_field, sizeof(src_field), "\"%s\"", src);
+ }
+
+ inet_addr2str(&entry->group, grp, sizeof(grp));
+ if (entry->len != max_len)
+ snprintf(grp_field, sizeof(grp_field), "\"%s/%u\"", grp, entry->len);
+ else
+ snprintf(grp_field, sizeof(grp_field), "\"%s\"", grp);
+
+ snprintf(line, sizeof(line),
+ "%s{\"source\":%s,\"group\":%s,\"iif\":\"%s\"}",
+ first ? "" : ",", src_field, grp_field, entry->ifname);
+ return ipc_send(sd, line, strlen(line));
+}
+
+/* Iterate from @head forward via TAILQ_NEXT; called with conf_list and
+ * kern_list which are different TAILQ_HEAD types but share the entry
+ * link layout. */
+static int show_mcgroup_list_json(int sd, struct mcgroup *head)
+{
+ struct mcgroup *entry;
+ int first = 1;
+
+ ipc_send(sd, "[", 1);
+ for (entry = head; entry; entry = TAILQ_NEXT(entry, link)) {
+ if (show_mcgroup_json(sd, entry, first) < 0)
+ return -1;
+ first = 0;
+ }
+ ipc_send(sd, "]", 1);
+ return 0;
+}
+
/* Write all joined IGMP/MLD groups to client socket */
-int mcgroup_show(int sd, int detail)
+int mcgroup_show(int sd, enum show_mode mode)
{
char *conf_str = "Group Memberships Table_\n";
char *kern_str = "Kernel Group Membership Table_\n";
struct mcgroup *entry;
char line[256];
+ if (mode == SHOW_JSON) {
+ ipc_send(sd, "{\"groups\":{\"conf\":", 18);
+ if (show_mcgroup_list_json(sd, TAILQ_FIRST(&conf_list)) < 0)
+ return -1;
+ ipc_send(sd, ",\"kernel\":", 10);
+ if (show_mcgroup_list_json(sd, TAILQ_FIRST(&kern_list)) < 0)
+ return -1;
+ ipc_send(sd, "}}\n", 3);
+ return 0;
+ }
+
if (TAILQ_EMPTY(&conf_list))
return 0;
@@ -488,7 +548,7 @@ int mcgroup_show(int sd, int detail)
return 1;
}
- if (!detail)
+ if (mode != SHOW_DETAIL)
return 0;
ipc_send(sd, kern_str, strlen(kern_str));
diff --git a/src/mcgroup.h b/src/mcgroup.h
index b8567fb..a40ae2c 100644
--- a/src/mcgroup.h
+++ b/src/mcgroup.h
@@ -4,6 +4,7 @@
#include "inet.h"
#include "queue.h"
+#include "show.h"
struct mcgroup {
TAILQ_ENTRY(mcgroup) link;
@@ -27,7 +28,7 @@ void mcgroup_exit (void);
int mcgroup_action (int cmd, const char *ifname, inet_addr_t *source, int src_len, inet_addr_t *group, int len);
-int mcgroup_show (int sd, int detail);
+int mcgroup_show (int sd, enum show_mode mode);
#endif /* SMCROUTE_MCGROUP_H_ */
diff --git a/src/mrdisc.c b/src/mrdisc.c
index 421a23b..073a9d8 100644
--- a/src/mrdisc.c
+++ b/src/mrdisc.c
@@ -1,6 +1,6 @@
/* Multicast Router Discovery Protocol, RFC4286 (IPv4 only)
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -86,8 +86,8 @@ static unsigned short in_cksum(unsigned short *addr, int len)
{
unsigned short answer = 0;
unsigned short *w = addr;
+ unsigned int sum = 0;
int nleft = len;
- int sum = 0;
/*
* Our algorithm is simple, using a 32 bit accumulator (sum), we add
@@ -108,7 +108,7 @@ static unsigned short in_cksum(unsigned short *addr, int len)
/* add back carry outs from top 16 bits to low 16 bits */
sum = (sum >> 16) + (sum & 0xffff); /* add hi 16 to low 16 */
sum += (sum >> 16); /* add carry */
- answer = ~sum; /* truncate to 16 bits */
+ answer = ~sum & 0xffff; /* truncate to 16 bits */
return answer;
}
diff --git a/src/mrdisc.h b/src/mrdisc.h
index 9341d7c..c157104 100644
--- a/src/mrdisc.h
+++ b/src/mrdisc.h
@@ -1,6 +1,6 @@
/* Multicast Router Discovery Protocol, RFC4286 (IPv4 only)
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/mroute.c b/src/mroute.c
index 9893692..429642b 100644
--- a/src/mroute.c
+++ b/src/mroute.c
@@ -4,7 +4,7 @@
* Copyright (C) 2006-2009 Julien BLACHE <jb@jblache.org>
* Copyright (C) 2009 Todd Hayton <todd.hayton@gmail.com>
* Copyright (C) 2009-2011 Micha Lenk <micha@debian.org>
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -58,6 +58,7 @@ static TAILQ_HEAD(kl, mroute) kern_list = TAILQ_HEAD_INITIALIZER(kern_list);
static int mroute4_add_vif (struct iface *iface);
static int mroute_dyn_add (struct mroute *route);
+static int mroute_resolve (struct mroute *route);
static int is_match (struct mroute *rule, struct mroute *cand);
static int is_exact_match (struct mroute *rule, struct mroute *cand);
static int mfc_install (struct mroute *route);
@@ -133,8 +134,14 @@ static void handle_nocache4(int sd, void *arg)
break;
case IGMPMSG_WRONGVIF:
- smclog(LOG_WARNING, "Multicast from %s, group %s, coming in on wrong VIF %u, iface %s",
- origin, group, mroute.inbound, iface->ifname);
+ if (mroute_resolve(&mroute) == 0) {
+ smclog(LOG_NOTICE, "Moved (%s,%s) inbound to %s after WRONGVIF",
+ origin, group, iface->ifname);
+ script_exec(&mroute);
+ } else {
+ smclog(LOG_INFO, "Multicast from %s, group %s on wrong VIF %u, iface %s",
+ origin, group, mroute.inbound, iface->ifname);
+ }
break;
case IGMPMSG_WHOLEPKT:
@@ -167,10 +174,8 @@ static void cache_flush(void *arg)
* Returns:
* POSIX OK(0) on success, non-zero on error with @errno set.
*/
-static int mroute4_enable(int do_vifs, int table_id)
+static int mroute4_enable(int table_id)
{
- struct iface *iface;
-
if (kern_mroute_init(table_id, handle_nocache4, NULL)) {
switch (errno) {
case ENOPROTOOPT:
@@ -185,6 +190,7 @@ static int mroute4_enable(int do_vifs, int table_id)
case EADDRINUSE:
smclog(LOG_ERR, "IPv4 multicast routing API already in use: %s",
strerror(errno));
+ smclog(LOG_ERR, "Is another multicast routing daemon (igmpproxy, mcpd, pimd) running?");
break;
case EOPNOTSUPP:
@@ -200,12 +206,6 @@ static int mroute4_enable(int do_vifs, int table_id)
return 1;
}
- /* Create virtual interfaces (VIFs) for all IFF_MULTICAST interfaces */
- if (do_vifs) {
- for (iface = iface_iterator(1); iface; iface = iface_iterator(0))
- mroute4_add_vif(iface);
- }
-
return 0;
}
@@ -261,7 +261,7 @@ static int mroute4_add_vif(struct iface *iface)
if (kern_vif_add(iface)) {
switch (errno) {
case ENOPROTOOPT:
- smclog(LOG_INFO, "Interface %s is not multicast capable, skipping VIF.",
+ smclog(LOG_WARNING, "Interface %s is not multicast capable, skipping VIF.",
iface->ifname);
return -1;
@@ -586,12 +586,14 @@ static int mfc_uninstall(struct mroute *route)
continue;
if (is_active(kern) || !removal) {
- rc += kern_mroute_add(kern);
+ if (kern_mroute_add(kern))
+ rc = -1;
continue;
}
cleanup:
- rc += kern_mroute_del(kern);
+ if (kern_mroute_del(kern))
+ rc = -1;
TAILQ_REMOVE(&kern_list, kern, link);
free(kern);
}
@@ -764,8 +766,14 @@ static void handle_nocache6(int sd, void *arg)
break;
case MRT6MSG_WRONGMIF:
- smclog(LOG_WARNING, "Multicast from %s, group %s, coming in on wrong MIF %u, iface %s",
- origin, group, mroute.inbound, iface->ifname);
+ if (mroute_resolve(&mroute) == 0) {
+ smclog(LOG_NOTICE, "Moved (%s,%s) inbound to %s after WRONGMIF",
+ origin, group, iface->ifname);
+ script_exec(&mroute);
+ } else {
+ smclog(LOG_INFO, "Multicast from %s, group %s on wrong MIF %u, iface %s",
+ origin, group, mroute.inbound, iface->ifname);
+ }
break;
case MRT6MSG_WHOLEPKT:
@@ -788,14 +796,11 @@ static void handle_nocache6(int sd, void *arg)
* Returns:
* POSIX OK(0) on success, non-zero on error with @errno set.
*/
-static int mroute6_enable(int do_vifs, int table_id)
+static int mroute6_enable(int table_id)
{
#ifndef HAVE_IPV6_MULTICAST_ROUTING
- (void)do_vifs;
(void)table_id;
#else
- struct iface *iface;
-
if (kern_mroute6_init(table_id, handle_nocache6, NULL)) {
switch (errno) {
case ENOPROTOOPT:
@@ -811,6 +816,7 @@ static int mroute6_enable(int do_vifs, int table_id)
case EADDRINUSE:
smclog(LOG_ERR, "IPv6 multicast routing API already in use: %s",
strerror(errno));
+ smclog(LOG_ERR, "Is another multicast routing daemon (mldproxy, pim6d) running?");
break;
case EOPNOTSUPP:
@@ -826,12 +832,6 @@ static int mroute6_enable(int do_vifs, int table_id)
return 1;
}
- /* Create virtual interfaces, IPv6 MIFs, for all IFF_MULTICAST interfaces */
- if (do_vifs) {
- for (iface = iface_iterator(1); iface; iface = iface_iterator(0))
- mroute6_add_mif(iface);
- }
-
return 0;
#endif /* HAVE_IPV6_MULTICAST_ROUTING */
@@ -881,7 +881,7 @@ static int mroute6_add_mif(struct iface *iface)
if (kern_mif_add(iface)) {
switch (errno) {
case ENOPROTOOPT:
- smclog(LOG_INFO, "Interface %s is not multicast capable, skipping MIF.",
+ smclog(LOG_WARNING, "Interface %s is not multicast capable, skipping MIF.",
iface->ifname);
return -1;
@@ -936,50 +936,46 @@ static int mroute6_del_mif(struct iface *iface)
}
#endif /* HAVE_IPV6_MULTICAST_ROUTING */
-/**
- * mroute_dyn_add - Add route to kernel if it matches a known (*,G) route.
- * @route: Pointer to candidate multicast route
- *
- * Returns:
- * POSIX OK(0) on success, non-zero on error with @errno set.
+/*
+ * Look up a configured (*,G) rule whose inbound matches @route's iif
+ * and install/replace the kernel MFC entry with the rule's ttl[] mask.
+ * Shared by both NOCACHE (via mroute_dyn_add) and WRONGVIF/WRONGMIF.
+ * Returns 0 on hit, -1 with errno=ENOENT on miss (kernel entry, if
+ * any, left alone).
*/
-static int mroute_dyn_add(struct mroute *route)
+static int mroute_resolve(struct mroute *route)
{
struct mroute *entry;
- int rc;
TAILQ_FOREACH(entry, &conf_list, link) {
- /* Find matching (*,G) ... and interface. */
if (is_ssm(entry) || !is_match(entry, route))
continue;
-
- /* Use configured template (*,G) outbound interfaces. */
memcpy(route->ttl, entry->ttl, NELEMS(route->ttl) * sizeof(route->ttl[0]));
- break;
- }
-
- if (!entry) {
- /*
- * No match, add entry without outbound interfaces
- * nevertheless to avoid continuous cache misses from
- * the kernel. Note that this still gets reported as an
- * error (ENOENT) below.
- */
- memset(route->ttl, 0, NELEMS(route->ttl) * sizeof(route->ttl[0]));
+ mfc_install(route);
+ return 0;
}
- rc = mfc_install(route);
+ errno = ENOENT;
+ return -1;
+}
- /* Signal to cache handler we've added a stop filter */
- if (!entry) {
- errno = ENOENT;
- return -1;
- }
+/*
+ * NOCACHE upcall handler. Resolve via mroute_resolve(); on miss
+ * install a zero-ttl stop filter so the kernel stops upcalling NOCACHE
+ * for this (S,G,IIF).
+ */
+static int mroute_dyn_add(struct mroute *route)
+{
+ if (mroute_resolve(route) == 0)
+ return 0;
- return rc;
+ memset(route->ttl, 0, NELEMS(route->ttl) * sizeof(route->ttl[0]));
+ mfc_install(route);
+ errno = ENOENT;
+ return -1;
}
-int mroute_init(int do_vifs, int table_id, int cache_tmo)
+int mroute_init(int table_id, int cache_tmo)
{
static int running = 0;
@@ -992,8 +988,8 @@ int mroute_init(int do_vifs, int table_id, int cache_tmo)
timer_add(cache_tmo, cache_flush, NULL);
}
- return mroute4_enable(do_vifs, table_id) ||
- mroute6_enable(do_vifs, table_id);
+ return mroute4_enable(table_id) ||
+ mroute6_enable(table_id);
}
void mroute_exit(void)
@@ -1015,9 +1011,11 @@ int mroute_add_vif(char *ifname, uint8_t mrdisc, uint8_t ttl)
iface->mrdisc = mrdisc;
iface->threshold = ttl;
iface->unused = 0;
- rc += mroute4_add_vif(iface);
+ if (mroute4_add_vif(iface))
+ rc = -1;
#ifdef HAVE_IPV6_MULTICAST_ROUTING
- rc += mroute6_add_mif(iface);
+ if (mroute6_add_mif(iface))
+ rc = -1;
#endif
}
@@ -1029,6 +1027,27 @@ int mroute_add_vif(char *ifname, uint8_t mrdisc, uint8_t ttl)
return rc;
}
+/* Like mroute_add_vif() but preserves any user-set mrdisc/ttl. */
+int mroute_ensure_vif(const char *ifname)
+{
+ struct ifmatch state;
+ struct iface *iface;
+ int rc = 0;
+
+ iface_match_init(&state);
+ while ((iface = iface_match_by_name(ifname, 1, &state))) {
+ iface->unused = 0;
+ if (mroute4_add_vif(iface))
+ rc = -1;
+#ifdef HAVE_IPV6_MULTICAST_ROUTING
+ if (mroute6_add_mif(iface))
+ rc = -1;
+#endif
+ }
+
+ return rc;
+}
+
/* Used by file parser to remove VIFs/MIFs after setup */
int mroute_del_vif(char *ifname)
{
@@ -1039,9 +1058,11 @@ int mroute_del_vif(char *ifname)
iface_match_init(&state);
while ((iface = iface_match_by_name(ifname, 1, &state))) {
smclog(LOG_DEBUG, "Removing multicast VIFs for %s", iface->ifname);
- rc += mroute4_del_vif(iface);
+ if (mroute4_del_vif(iface))
+ rc = -1;
#ifdef HAVE_IPV6_MULTICAST_ROUTING
- rc += mroute6_del_mif(iface);
+ if (mroute6_del_mif(iface))
+ rc = -1;
#endif
}
@@ -1073,7 +1094,7 @@ void mroute_reload_beg(void)
}
}
-void mroute_reload_end(int do_vifs)
+void mroute_reload_end(void)
{
struct mroute *entry, *tmp;
struct iface *iface;
@@ -1083,10 +1104,8 @@ void mroute_reload_end(int do_vifs)
char dummy[IFNAMSIZ];
first = 0;
- if (iface->unused || !if_indextoname(iface->ifindex, dummy)) {
+ if (iface->unused || !if_indextoname(iface->ifindex, dummy))
mroute_del_vif(iface->ifname);
- } else if (do_vifs)
- mroute_add_vif(iface->ifname, iface->mrdisc, iface->threshold);
}
TAILQ_FOREACH_SAFE(entry, &conf_list, link, tmp) {
@@ -1099,6 +1118,54 @@ void mroute_reload_end(int do_vifs)
mfc_install(entry);
}
+static int show_mroute_json(int sd, struct mroute *r, int first)
+{
+ char src[INET_ADDRSTR_LEN] = "", grp[INET_ADDRSTR_LEN];
+ char src_field[INET_ADDRSTR_LEN + 16];
+ char grp_field[INET_ADDRSTR_LEN + 16];
+ char line[512];
+ struct iface *iface;
+ int max_len, sent_oif = 0;
+
+ max_len = inet_max_len(&r->group);
+
+ if (is_anyaddr(&r->source)) {
+ snprintf(src_field, sizeof(src_field), "null");
+ } else {
+ inet_addr2str(&r->source, src, sizeof(src));
+ if (r->src_len != max_len)
+ snprintf(src_field, sizeof(src_field), "\"%s/%u\"", src, r->src_len);
+ else
+ snprintf(src_field, sizeof(src_field), "\"%s\"", src);
+ }
+
+ inet_addr2str(&r->group, grp, sizeof(grp));
+ if (r->len != max_len)
+ snprintf(grp_field, sizeof(grp_field), "\"%s/%u\"", grp, r->len);
+ else
+ snprintf(grp_field, sizeof(grp_field), "\"%s\"", grp);
+
+ iface = iface_find_by_inbound(r);
+ snprintf(line, sizeof(line),
+ "%s{\"source\":%s,\"group\":%s,\"iif\":\"%s\",\"oifs\":[",
+ first ? "" : ",", src_field, grp_field,
+ iface ? iface->ifname : "");
+ if (ipc_send(sd, line, strlen(line)) < 0)
+ return -1;
+
+ iface = iface_outbound_iterator(r, 1);
+ while (iface) {
+ snprintf(line, sizeof(line), "%s\"%s\"",
+ sent_oif ? "," : "", iface->ifname);
+ if (ipc_send(sd, line, strlen(line)) < 0)
+ return -1;
+ sent_oif = 1;
+ iface = iface_outbound_iterator(r, 0);
+ }
+ ipc_send(sd, "]}", 2);
+ return 0;
+}
+
static int show_mroute(int sd, struct mroute *r, int inw, int detail)
{
char src[INET_ADDRSTR_LEN] = "*";
@@ -1182,13 +1249,46 @@ static int has_any_asm(void)
}
/* Write all (*,G) routes to client socket */
-int mroute_show(int sd, int detail)
+int mroute_show(int sd, enum show_mode mode)
{
const char *r = "ROUTE (S,G)", *o = "OIFS", *i = "IIF";
+ int detail = mode == SHOW_DETAIL;
struct mroute *entry;
char line[256];
int inw;
+ if (mode == SHOW_JSON) {
+ int first;
+
+ ipc_send(sd, "{\"routes\":{\"asm\":[", 18);
+ first = 1;
+ TAILQ_FOREACH(entry, &conf_list, link) {
+ if (is_ssm(entry))
+ continue;
+ if (show_mroute_json(sd, entry, first) < 0)
+ return 1;
+ first = 0;
+ }
+ ipc_send(sd, "],\"ssm\":[", 9);
+ first = 1;
+ TAILQ_FOREACH(entry, &conf_list, link) {
+ if (!is_ssm(entry))
+ continue;
+ if (show_mroute_json(sd, entry, first) < 0)
+ return 1;
+ first = 0;
+ }
+ ipc_send(sd, "],\"kernel\":[", 12);
+ first = 1;
+ TAILQ_FOREACH(entry, &kern_list, link) {
+ if (show_mroute_json(sd, entry, first) < 0)
+ return 1;
+ first = 0;
+ }
+ ipc_send(sd, "]}}\n", 4);
+ return 0;
+ }
+
inw = iface_ifname_maxlen();
if (inw < (int)strlen(i))
inw = (int)strlen(i);
diff --git a/src/mroute.h b/src/mroute.h
index fd60539..9385f72 100644
--- a/src/mroute.h
+++ b/src/mroute.h
@@ -14,6 +14,7 @@
#endif
#include <netinet/ip.h>
#include "queue.h" /* Needed by netinet/ip_mroute.h on FreeBSD */
+#include "show.h"
#ifdef HAVE_LINUX_MROUTE_H
#define _LINUX_IN_H /* For Linux <= 2.6.25 */
@@ -104,10 +105,11 @@ struct mroute {
time_t last_use; /* timestamp of last forwarded packet */
};
-int mroute_init (int do_vifs, int table_id, int cache_tmo);
+int mroute_init (int table_id, int cache_tmo);
void mroute_exit (void);
int mroute_add_vif (char *ifname, uint8_t mrdisc, uint8_t threshold);
+int mroute_ensure_vif (const char *ifname);
int mroute_del_vif (char *ifname);
void mroute_expire (int max_idle);
@@ -116,8 +118,8 @@ int mroute_add_route (struct mroute *mroute);
int mroute_del_route (struct mroute *mroute);
void mroute_reload_beg (void);
-void mroute_reload_end (int do_vifs);
+void mroute_reload_end (void);
-int mroute_show (int sd, int detail);
+int mroute_show (int sd, enum show_mode mode);
#endif /* SMCROUTE_MROUTE_H_ */
diff --git a/src/msg.c b/src/msg.c
index 4bd1982..cdd0fd5 100644
--- a/src/msg.c
+++ b/src/msg.c
@@ -1,6 +1,6 @@
/* IPC command parser and builder for daemon and client
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -29,6 +29,7 @@
#include "conf.h"
#include "log.h"
#include "msg.h"
+#include "pending.h"
#include "iface.h"
#include "util.h"
#include "mroute.h"
@@ -119,24 +120,27 @@ static int do_mroute(struct ipc_msg *msg)
return conf_mroute(NULL, msg->cmd == 'a' ? 1 : 0, ifname, source, group, out, num);
}
-static int do_show(struct ipc_msg *msg, int sd, int detail)
+static int do_show(struct ipc_msg *msg, int sd, enum show_mode mode)
{
if (msg->count > 0) {
char cmd = msg->argv[0][0];
switch (cmd) {
case 'g':
- return mcgroup_show(sd, detail);
+ return mcgroup_show(sd, mode);
case 'i':
- return iface_show(sd, detail);
+ return iface_show(sd, mode);
+
+ case 'p':
+ return pending_show(sd, mode);
default:
break;
}
}
- return mroute_show(sd, detail);
+ return mroute_show(sd, mode);
}
/*
@@ -169,12 +173,16 @@ int msg_do(int sd, struct ipc_msg *msg)
running = 0;
break;
+ case 'J':
+ result = do_show(msg, sd, SHOW_JSON);
+ break;
+
case 'S':
- result = do_show(msg, sd, 1);
+ result = do_show(msg, sd, SHOW_DETAIL);
break;
case 's':
- result = do_show(msg, sd, 0);
+ result = do_show(msg, sd, SHOW_BRIEF);
break;
default:
diff --git a/src/notify.c b/src/notify.c
index e70e51f..c48926a 100644
--- a/src/notify.c
+++ b/src/notify.c
@@ -1,6 +1,6 @@
/* generic service monitor backend
*
- * Copyright (C) 2019-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2019-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/pending.c b/src/pending.c
new file mode 100644
index 0000000..3190e4a
--- /dev/null
+++ b/src/pending.c
@@ -0,0 +1,380 @@
+/* Pending mroute/mgroup directives whose inbound or outbound interface
+ * is not yet available on the system.
+ *
+ * Lifecycle:
+ *
+ * 1. conf.c parser encounters an mroute/mgroup whose iface is missing
+ * and calls pending_add_*(). The directive is held here with its
+ * original strings, deduplicated by (iif, source, group, oifs).
+ * `smcroutectl show pending` lists what's waiting.
+ *
+ * 2. On a fresh reload (SIGHUP / `smcroutectl reload`) pending_clear()
+ * empties the list at reload_beg() so conf_read() can rebuild it
+ * from the current file.
+ *
+ * 3. ifevent_nl.c (Linux netlink) calls pending_drain() on every
+ * kernel link/address event. For each entry whose every named
+ * iface is now present and `IFF_MULTICAST`-capable, drain replays
+ * the directive via conf_mroute()/conf_mgroup() (with conf=NULL,
+ * i.e. the IPC entry point) and drops the entry on success.
+ *
+ * This substrate is intentionally reusable for future dynamic-routing
+ * features — e.g., the IGMP/MLD proxying TODO in doc/TODO.md would
+ * enqueue routes here in response to incoming Membership Reports.
+ *
+ * Copyright (C) 2026 Joachim Wiberg <troglobit@gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+
+#include <errno.h>
+#include <net/if.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "queue.h"
+#include "conf.h"
+#include "iface.h"
+#include "ipc.h"
+#include "log.h"
+#include "mroute.h"
+#include "pending.h"
+
+enum pending_kind {
+ PENDING_MROUTE,
+ PENDING_MGROUP,
+};
+
+struct pending {
+ TAILQ_ENTRY(pending) link;
+
+ enum pending_kind kind;
+ char *iif;
+ char *source;
+ char *group;
+ char *oif[MAX_MC_VIFS];
+ int num;
+};
+
+static TAILQ_HEAD(, pending) pending_list = TAILQ_HEAD_INITIALIZER(pending_list);
+
+static const char *kind_name(enum pending_kind k)
+{
+ return k == PENDING_MROUTE ? "mroute" : "mgroup";
+}
+
+static void pending_free(struct pending *p)
+{
+ int i;
+
+ free(p->iif);
+ free(p->source);
+ free(p->group);
+ for (i = 0; i < p->num; i++)
+ free(p->oif[i]);
+ free(p);
+}
+
+/* strdup @src into *@dst. Returns 0 on success, -1 on OOM. */
+static int dup_field(char **dst, const char *src)
+{
+ if (!src) {
+ *dst = NULL;
+ return 0;
+ }
+ *dst = strdup(src);
+ return *dst ? 0 : -1;
+}
+
+/* Bounded snprintf that appends to a buffer, clamping on overflow. */
+static void append(char *buf, size_t size, size_t *off, const char *fmt, ...)
+{
+ va_list ap;
+ int n;
+
+ if (*off >= size)
+ return;
+ va_start(ap, fmt);
+ n = vsnprintf(buf + *off, size - *off, fmt, ap);
+ va_end(ap);
+ if (n <= 0)
+ return;
+ *off += (size_t)n;
+ if (*off >= size)
+ *off = size - 1;
+}
+
+static int streq(const char *a, const char *b)
+{
+ if (!a && !b)
+ return 1;
+ if (!a || !b)
+ return 0;
+ return !strcmp(a, b);
+}
+
+static int same_oifs(struct pending *p, char *const oif[], int num)
+{
+ int i;
+
+ if (p->num != num)
+ return 0;
+ for (i = 0; i < num; i++) {
+ if (!streq(p->oif[i], oif[i]))
+ return 0;
+ }
+ return 1;
+}
+
+static struct pending *find_mroute(const char *iif, const char *source,
+ const char *group, char *const oif[], int num)
+{
+ struct pending *p;
+
+ TAILQ_FOREACH(p, &pending_list, link) {
+ if (p->kind != PENDING_MROUTE)
+ continue;
+ if (streq(p->iif, iif) && streq(p->source, source) &&
+ streq(p->group, group) && same_oifs(p, oif, num))
+ return p;
+ }
+ return NULL;
+}
+
+static struct pending *find_mgroup(const char *iif, const char *source,
+ const char *group)
+{
+ struct pending *p;
+
+ TAILQ_FOREACH(p, &pending_list, link) {
+ if (p->kind != PENDING_MGROUP)
+ continue;
+ if (streq(p->iif, iif) && streq(p->source, source) &&
+ streq(p->group, group))
+ return p;
+ }
+ return NULL;
+}
+
+void pending_init(void)
+{
+ TAILQ_INIT(&pending_list);
+}
+
+void pending_clear(void)
+{
+ struct pending *p, *tmp;
+
+ TAILQ_FOREACH_SAFE(p, &pending_list, link, tmp) {
+ TAILQ_REMOVE(&pending_list, p, link);
+ pending_free(p);
+ }
+}
+
+void pending_exit(void)
+{
+ pending_clear();
+}
+
+int pending_add_mroute(const char *iif, const char *source, const char *group,
+ char *const oif[], int num)
+{
+ struct pending *p;
+ int i;
+
+ if (find_mroute(iif, source, group, oif, num))
+ return 0;
+
+ p = calloc(1, sizeof(*p));
+ if (!p)
+ goto oom;
+
+ p->kind = PENDING_MROUTE;
+ p->num = num;
+ if (dup_field(&p->iif, iif) || dup_field(&p->source, source) ||
+ dup_field(&p->group, group))
+ goto oom_free;
+ for (i = 0; i < num; i++) {
+ if (dup_field(&p->oif[i], oif[i]))
+ goto oom_free;
+ }
+
+ TAILQ_INSERT_TAIL(&pending_list, p, link);
+ smclog(LOG_NOTICE, "%s from %s group %s pending: interface not available",
+ kind_name(p->kind), iif, group);
+ return 0;
+
+oom_free:
+ pending_free(p);
+oom:
+ smclog(LOG_ERR, "Out of memory in %s()", __func__);
+ return -1;
+}
+
+int pending_add_mgroup(const char *iif, const char *source, const char *group)
+{
+ struct pending *p;
+
+ if (find_mgroup(iif, source, group))
+ return 0;
+
+ p = calloc(1, sizeof(*p));
+ if (!p)
+ goto oom;
+
+ p->kind = PENDING_MGROUP;
+ if (dup_field(&p->iif, iif) || dup_field(&p->source, source) ||
+ dup_field(&p->group, group))
+ goto oom_free;
+
+ TAILQ_INSERT_TAIL(&pending_list, p, link);
+ smclog(LOG_NOTICE, "%s from %s group %s pending: interface not available",
+ kind_name(p->kind), iif, group);
+ return 0;
+
+oom_free:
+ pending_free(p);
+oom:
+ smclog(LOG_ERR, "Out of memory in %s()", __func__);
+ return -1;
+}
+
+/* Wait for IFF_MULTICAST so a freshly-added iface mid-bringup does
+ * not trigger a transient "not multicast capable" warning on drain. */
+static int iface_ready(const char *name)
+{
+ struct ifmatch state;
+ struct iface *iface;
+
+ iface_match_init(&state);
+ while ((iface = iface_match_by_name(name, 1, &state))) {
+ if ((iface->flags & IFF_MULTICAST) == IFF_MULTICAST)
+ return 1;
+ }
+ return 0;
+}
+
+static int all_ifaces_ready(struct pending *p)
+{
+ int i;
+
+ if (!iface_ready(p->iif))
+ return 0;
+ for (i = 0; i < p->num; i++) {
+ if (!iface_ready(p->oif[i]))
+ return 0;
+ }
+ return 1;
+}
+
+void pending_drain(void)
+{
+ struct pending *p, *tmp;
+ int rc;
+
+ TAILQ_FOREACH_SAFE(p, &pending_list, link, tmp) {
+ if (!all_ifaces_ready(p))
+ continue;
+
+ if (p->kind == PENDING_MROUTE)
+ rc = conf_mroute(NULL, 1, p->iif, p->source, p->group,
+ p->oif, p->num);
+ else
+ rc = conf_mgroup(NULL, 1, p->iif, p->source, p->group);
+
+ if (rc)
+ continue;
+
+ smclog(LOG_NOTICE, "Activated pending %s from %s group %s",
+ kind_name(p->kind), p->iif, p->group);
+ TAILQ_REMOVE(&pending_list, p, link);
+ pending_free(p);
+ }
+}
+
+static int show_one(int sd, struct pending *p)
+{
+ char buf[512];
+ size_t len = 0;
+ int i;
+
+ append(buf, sizeof(buf), &len, "%-7s from %s", kind_name(p->kind), p->iif);
+ if (p->source)
+ append(buf, sizeof(buf), &len, " source %s", p->source);
+ append(buf, sizeof(buf), &len, " group %s", p->group);
+ if (p->num > 0)
+ append(buf, sizeof(buf), &len, " to");
+ for (i = 0; i < p->num; i++)
+ append(buf, sizeof(buf), &len, " %s", p->oif[i]);
+ append(buf, sizeof(buf), &len, "\n");
+
+ return ipc_send(sd, buf, len);
+}
+
+static int show_one_json(int sd, struct pending *p, int first)
+{
+ char buf[512];
+ size_t len = 0;
+ int i;
+
+ append(buf, sizeof(buf), &len,
+ "%s{\"kind\":\"%s\",\"iif\":\"%s\",\"source\":",
+ first ? "" : ",", kind_name(p->kind), p->iif);
+ if (p->source)
+ append(buf, sizeof(buf), &len, "\"%s\"", p->source);
+ else
+ append(buf, sizeof(buf), &len, "null");
+ append(buf, sizeof(buf), &len, ",\"group\":\"%s\",\"oifs\":[", p->group);
+ for (i = 0; i < p->num; i++)
+ append(buf, sizeof(buf), &len, "%s\"%s\"",
+ i == 0 ? "" : ",", p->oif[i]);
+ append(buf, sizeof(buf), &len, "]}");
+
+ return ipc_send(sd, buf, len);
+}
+
+int pending_show(int sd, enum show_mode mode)
+{
+ struct pending *p;
+ const char *header = "PENDING ROUTES AND GROUPS\n";
+
+ if (mode == SHOW_JSON) {
+ int first = 1;
+
+ ipc_send(sd, "{\"pending\":[", 12);
+ TAILQ_FOREACH(p, &pending_list, link) {
+ if (show_one_json(sd, p, first) < 0)
+ return -1;
+ first = 0;
+ }
+ ipc_send(sd, "]}\n", 3);
+ return 0;
+ }
+
+ if (TAILQ_EMPTY(&pending_list)) {
+ header = "No pending routes or groups\n";
+ return ipc_send(sd, header, strlen(header));
+ }
+
+ if (ipc_send(sd, header, strlen(header)) < 0)
+ return -1;
+
+ TAILQ_FOREACH(p, &pending_list, link) {
+ if (show_one(sd, p) < 0)
+ return -1;
+ }
+
+ return 0;
+}
+
+/**
+ * Local Variables:
+ * indent-tabs-mode: t
+ * c-file-style: "linux"
+ * End:
+ */
diff --git a/src/pending.h b/src/pending.h
new file mode 100644
index 0000000..477e336
--- /dev/null
+++ b/src/pending.h
@@ -0,0 +1,22 @@
+/* Pending routes/groups whose inbound interface is not yet available. */
+#ifndef SMCROUTE_PENDING_H_
+#define SMCROUTE_PENDING_H_
+
+#include "show.h"
+
+void pending_init (void);
+void pending_exit (void);
+
+int pending_add_mroute (const char *iif, const char *source, const char *group,
+ char *const oif[], int num);
+int pending_add_mgroup (const char *iif, const char *source, const char *group);
+
+/* Empty the list, e.g. before re-reading the config. */
+void pending_clear (void);
+
+/* Walk the list, retry each entry, drop the successful ones. */
+void pending_drain (void);
+
+int pending_show (int sd, enum show_mode mode);
+
+#endif /* SMCROUTE_PENDING_H_ */
diff --git a/src/script.c b/src/script.c
index d43474e..fced3ac 100644
--- a/src/script.c
+++ b/src/script.c
@@ -1,6 +1,6 @@
/* Run script when a (*,G) group is matched and installed in the kernel
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
diff --git a/src/show.h b/src/show.h
new file mode 100644
index 0000000..df6c600
--- /dev/null
+++ b/src/show.h
@@ -0,0 +1,11 @@
+/* Output mode for smcroutectl show subcommands. */
+#ifndef SMCROUTE_SHOW_H_
+#define SMCROUTE_SHOW_H_
+
+enum show_mode {
+ SHOW_BRIEF, /* default human-readable table */
+ SHOW_DETAIL, /* table with extra columns */
+ SHOW_JSON, /* JSON object, smcroutectl -j */
+};
+
+#endif /* SMCROUTE_SHOW_H_ */
diff --git a/src/smcroutectl.c b/src/smcroutectl.c
index 17ff7aa..7e83c14 100644
--- a/src/smcroutectl.c
+++ b/src/smcroutectl.c
@@ -1,6 +1,6 @@
/* Client for smcrouted, not needed if only using smcroute.conf
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,9 +42,10 @@ static const char version_info[] = PACKAGE_NAME " v" PACKAGE_VERSION;
static char *ident = PACKAGE;
static char *sock_file = NULL;
-static char *prognm = NULL;
+static const char *prognm = NULL;
static int heading = 1;
static int detail = 0;
+static int json = 0;
static int plain = 0;
static int help = 0;
@@ -59,6 +60,7 @@ struct arg {
} args[] = {
{ NULL, 0, 'b', NULL, "Batch mode, read commands from stdin", NULL, 0 },
{ NULL, 0, 'd', NULL, "Detailed output in show command", NULL, 0 },
+ { NULL, 0, 'j', NULL, "JSON output for show command", NULL, 0 },
{ NULL, 1, 'i', "NAME", "Identity of routing daemon instance, default: " PACKAGE, "foo", 0 },
{ NULL, 1, 'I', "NAME", NULL, NULL, 0 }, /* Alias, compat with older versions */
{ NULL, 0, 'p', NULL, "Use plain table headings, no ctrl chars", NULL, 0 },
@@ -317,7 +319,7 @@ static int ipc_command(uint16_t cmd, char *argv[], size_t count)
rewind(fp);
if (total > 1) {
- if (cmd == 'S' || cmd == 's') {
+ if (cmd == 'S' || cmd == 's' || cmd == 'J') {
while (fgets(buf, sizeof(buf), fp))
print(buf, 0);
} else {
@@ -373,6 +375,7 @@ static int usage(int code)
"\n"
" show interfaces Show configured multicast interfaces\n"
" show groups Show joined multicast groups\n"
+ " show pending Show routes/groups waiting for their interface\n"
" show routes Show (*,G) and (S,G) multicast routes, default\n"
"\n"
"Note:\n"
@@ -455,12 +458,14 @@ static int parse(int pos, int argc, char *argv[])
return status;
}
+ const char show_cmd = json ? 'J' : detail ? 'S' : 's';
+
if (!cmd)
- return ipc_command(detail ? 'S' : 's', NULL, 0);
+ return ipc_command(show_cmd, NULL, 0);
c = cmd->val;
- if (detail && cmd->has_detail)
- c -= 0x20;
+ if (cmd->has_detail)
+ c = show_cmd;
return ipc_command(c, &argv[pos], argc - pos);
}
@@ -490,15 +495,15 @@ static int batch(void)
return rc;
}
-static char *progname(const char *arg0)
+static const char *progname(const char *arg0)
{
- char *nm;
+ const char *nm;
nm = strrchr(arg0, '/');
if (nm)
nm++;
else
- nm = (char *)arg0;
+ nm = arg0;
return nm;
}
@@ -509,7 +514,7 @@ int main(int argc, char *argv[])
int c;
prognm = progname(argv[0]);
- while ((c = getopt(argc, argv, "bdhI:i:ptu:v")) != EOF) {
+ while ((c = getopt(argc, argv, "bdhI:i:jptu:v")) != EOF) {
switch (c) {
case 'b':
batch_mode = 1;
@@ -528,6 +533,10 @@ int main(int argc, char *argv[])
ident = optarg;
break;
+ case 'j':
+ json = 1;
+ break;
+
case 'p':
plain = 1;
break;
diff --git a/src/smcrouted.c b/src/smcrouted.c
index 19a9bdb..106c22d 100644
--- a/src/smcrouted.c
+++ b/src/smcrouted.c
@@ -1,6 +1,6 @@
/* Static multicast routing daemon
*
- * Copyright (C) 2011-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2011-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -42,8 +42,10 @@
#include "script.h"
#include "socket.h"
#include "mrdisc.h"
+#include "ifevent.h"
#include "mroute.h"
#include "mcgroup.h"
+#include "pending.h"
int background = 1;
int do_vifs = 1;
@@ -56,7 +58,7 @@ int table_id = 0;
char *script = NULL;
char *ident = PACKAGE;
-char *prognm = NULL;
+const char *prognm = NULL;
char *pid_file = NULL;
char *conf_file = NULL;
char *sock_file = NULL;
@@ -75,10 +77,12 @@ static const char version_info[] = PACKAGE_NAME " v" PACKAGE_VERSION;
static void clean(void)
{
timer_exit();
+ ifevent_exit();
mroute_exit();
mcgroup_exit();
ipc_exit();
iface_exit();
+ pending_exit();
smclog(LOG_NOTICE, "Exiting.");
}
@@ -88,11 +92,12 @@ void reload(void)
mcgroup_reload_beg();
mroute_reload_beg();
+ pending_clear();
iface_update();
conf_read(conf_file, do_vifs);
- mroute_reload_end(do_vifs);
+ mroute_reload_end();
mcgroup_reload_end();
/* Acknowledge client SIGHUP/reload */
@@ -194,8 +199,9 @@ static int start_server(void)
* Build list of multicast-capable physical interfaces
*/
iface_init();
+ pending_init();
- if (mroute_init(do_vifs, table_id, cache_tmo)) {
+ if (mroute_init(table_id, cache_tmo)) {
if (errno == EADDRINUSE)
busy++;
api--;
@@ -217,6 +223,7 @@ static int start_server(void)
signal_init();
mcgroup_init();
ipc_init(sock_file);
+ ifevent_init();
conf_read(conf_file, do_vifs);
@@ -335,15 +342,15 @@ static int usage(int code)
return code;
}
-static char *progname(const char *arg0)
+static const char *progname(const char *arg0)
{
- char *nm;
+ const char *nm;
nm = strrchr(arg0, '/');
if (nm)
nm++;
else
- nm = (char *)arg0;
+ nm = arg0;
return nm;
}
diff --git a/src/socket.c b/src/socket.c
index a5ed3a8..50e0a5f 100644
--- a/src/socket.c
+++ b/src/socket.c
@@ -1,6 +1,6 @@
/* Socket helper functions
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/socket.h b/src/socket.h
index cca27fa..015827b 100644
--- a/src/socket.h
+++ b/src/socket.h
@@ -1,6 +1,6 @@
/* Helper functions
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/timer.c b/src/timer.c
index f425daf..56e0d27 100644
--- a/src/timer.c
+++ b/src/timer.c
@@ -1,6 +1,6 @@
/* Timer helper functions
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/src/timer.h b/src/timer.h
index 8332797..630c2c8 100644
--- a/src/timer.h
+++ b/src/timer.h
@@ -1,6 +1,6 @@
/* Timer helper functions
*
- * Copyright (C) 2017-2021 Joachim Wiberg <troglobit@gmail.com>
+ * Copyright (C) 2017-2026 Joachim Wiberg <troglobit@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
diff --git a/test/Makefile.am b/test/Makefile.am
index a59aa0f..2157ed0 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -1,10 +1,10 @@
EXTRA_DIST = adv.sh basic.sh batch.sh bridge.sh dyn.sh expire.sh gre.sh ipv6.sh
-EXTRA_DIST += include.sh isolated.sh join.sh joinlen.sh lib.sh lost.sh
-EXTRA_DIST += multi.sh mem.sh mrcache.sh mrdisc.sh poison.sh
-EXTRA_DIST += reload.sh reload6.sh vlan.sh vrfy.sh
+EXTRA_DIST += include.sh isolated.sh join.sh joinlen.sh json.sh lib.sh
+EXTRA_DIST += lost.sh multi.sh mem.sh mrcache.sh mrdisc.sh pending.sh
+EXTRA_DIST += poison.sh reload.sh reload6.sh vlan.sh vrfy.sh wrongvif.sh
CLEANFILES = *~ *.trs *.log
TEST_EXTENSIONS = .sh
-TESTS_ENVIRONMENT = unshare -mrun
+TESTS_ENVIRONMENT = unshare -mrun --map-auto
TESTS = expire.sh
TESTS += adv.sh
@@ -18,13 +18,16 @@ TESTS += ipv6.sh
TESTS += isolated.sh
TESTS += join.sh
TESTS += joinlen.sh
+TESTS += json.sh
TESTS += lost.sh
TESTS += mem.sh
TESTS += mrcache.sh
TESTS += mrdisc.sh
TESTS += multi.sh
+TESTS += pending.sh
TESTS += poison.sh
TESTS += reload.sh
TESTS += reload6.sh
TESTS += vlan.sh
TESTS += vrfy.sh
+TESTS += wrongvif.sh
diff --git a/test/Makefile.in b/test/Makefile.in
index 7ab9f7a..df53268 100644
--- a/test/Makefile.in
+++ b/test/Makefile.in
@@ -436,15 +436,16 @@ top_builddir = @top_builddir@
top_srcdir = @top_srcdir@
EXTRA_DIST = adv.sh basic.sh batch.sh bridge.sh dyn.sh expire.sh \
gre.sh ipv6.sh include.sh isolated.sh join.sh joinlen.sh \
- lib.sh lost.sh multi.sh mem.sh mrcache.sh mrdisc.sh poison.sh \
- reload.sh reload6.sh vlan.sh vrfy.sh
+ json.sh lib.sh lost.sh multi.sh mem.sh mrcache.sh mrdisc.sh \
+ pending.sh poison.sh reload.sh reload6.sh vlan.sh vrfy.sh \
+ wrongvif.sh
CLEANFILES = *~ *.trs *.log
TEST_EXTENSIONS = .sh
-TESTS_ENVIRONMENT = unshare -mrun
+TESTS_ENVIRONMENT = unshare -mrun --map-auto
TESTS = expire.sh adv.sh basic.sh batch.sh bridge.sh dyn.sh gre.sh \
- include.sh ipv6.sh isolated.sh join.sh joinlen.sh lost.sh \
- mem.sh mrcache.sh mrdisc.sh multi.sh poison.sh reload.sh \
- reload6.sh vlan.sh vrfy.sh
+ include.sh ipv6.sh isolated.sh join.sh joinlen.sh json.sh \
+ lost.sh mem.sh mrcache.sh mrdisc.sh multi.sh pending.sh \
+ poison.sh reload.sh reload6.sh vlan.sh vrfy.sh wrongvif.sh
all: all-am
.SUFFIXES:
diff --git a/test/README.md b/test/README.md
index 5ef4b9b..1455ecd 100644
--- a/test/README.md
+++ b/test/README.md
@@ -11,13 +11,20 @@ in `$PATH`:
- `tshark` (because `tcpdump -w foo.pcap` doesn't work in an unshare)
- `valgrind`, for the memleak test
-> **Note:* one of the tests makes use of `iptables`, which may not work
-> in an `unshare(1)`, unless you have v1.8.7 or newer that supports the
+> [!IMPORTANT]
+> One of the tests makes use of `iptables`, which may not work in an
+> `unshare(1)`, unless you have v1.8.7 or newer that supports the
> `XTABLES_LOCKFILE` environment variable. As a workaround you can
> `chmod a+rw /var/run/xtables.lock`.
>
-> Also, the GRE test requires your system to have the `ip_gre.ko` kernel
-> module loaded. If not, the test will be skipped.
+> Also, as of Ubuntu 24.04 blocks user namespaces for unprivileged users
+> by default, meaning you need some `sysctl` magic to set up your system
+> see <https://github.com/YoYoGames/GameMaker-Bugs/issues/6015>:
+>
+> sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
+>
+> Finally, the GRE test require the `ip_gre.ko` kernel module to be
+> loaded. If not, the test will be skipped.
Running
@@ -40,9 +47,10 @@ Each unit test is standalone. To manually run select tests:
The tools `ping` and `tshark` are used to create and listen to multicast
streams "routed by" SMCRoute.
-> **Note:** these tests must be run in sequence, not in parallel,
-> because they use the same interface names *and*, most importantly,
-> we may run on a kernel w/o multicast policy routing support!
+> [!NOTE]
+> These tests must be run in sequence, not in parallel, because they use
+> the same interface names *and*, most importantly, we may run on a
+> kernel w/o multicast policy routing support!
[1]: https://github.com/libnet/nemesis
[2]: https://github.com/troglobit/smcroute/actions/workflows/build.yml
@@ -109,8 +117,9 @@ created on which routing takes place.
Both bridge ports, `a1` and `a2`, are untagged members of each VLAN.
-> **Note:** interface `a1` and `vlan1` are in the same VLAN (VID 1), and
-> interface `a2` and `vlan2` are in the same VLAN (VID 2).
+> [!NOTE]
+> interface `a1` and `vlan1` are in the same VLAN (VID 1), and
+> interface `a2` and `vlan2` are in the same VLAN (VID 2).
### Isolated Endpoints Bridged w/ VLANs
@@ -131,8 +140,9 @@ interfaces and using ping6 as emitter instead of requiring [nemesis][1].
Both bridge ports, `a1` and `a2`, are untagged members of each VLAN.
-> **Note:** interface `a1` and `vlan1` are in the same VLAN (VID 1), and
-> interface `a2` and `vlan2` are in the same VLAN (VID 2).
+> [!NOTE]
+> interface `a1` and `vlan1` are in the same VLAN (VID 1), and
+> interface `a2` and `vlan2` are in the same VLAN (VID 2).
### Isolated
diff --git a/test/json.sh b/test/json.sh
new file mode 100755
index 0000000..5716af4
--- /dev/null
+++ b/test/json.sh
@@ -0,0 +1,80 @@
+#!/bin/sh
+# Verifies `smcroutectl -j` emits valid JSON for every show subcommand,
+# and that the documented fields are present.
+#set -x
+
+# shellcheck source=/dev/null
+. "$(dirname "$0")/lib.sh"
+
+check_dep python3
+
+CTL="../src/smcroutectl -u /tmp/$NM/sock"
+
+# Validate JSON parses, optionally apply a path check via python (read
+# from $PYEXPR to avoid the shell mangling embedded quotes).
+check_json()
+{
+ label=$1
+ output=$2
+ PYEXPR=$3
+ export PYEXPR
+
+ echo "--- $label ---"
+ echo "$output"
+ echo "$output" | python3 -c 'import json, sys; json.loads(sys.stdin.read())' \
+ || FAIL "$label: output is not valid JSON"
+ if [ -n "$PYEXPR" ]; then
+ echo "$output" | python3 -c '
+import json, os, sys
+d = json.loads(sys.stdin.read())
+assert eval(os.environ["PYEXPR"]), "predicate failed"' \
+ || FAIL "$label: $PYEXPR"
+ fi
+}
+
+print "Creating world ..."
+ip link add a1 type dummy; ip link set a1 multicast on; ip link set a1 up
+ip addr add 10.0.0.1/24 dev a1
+ip link add a2 type dummy; ip link set a2 multicast on; ip link set a2 up
+ip addr add 20.0.0.1/24 dev a2
+
+print "Creating config ..."
+cat <<EOF > "/tmp/$NM/conf"
+phyint a1 enable
+phyint a2 enable
+
+mgroup from a1 group 225.1.1.1
+mroute from a1 group 225.3.3.3 to a2
+mroute from a1 source 10.0.0.5 group 225.4.4.4 to a2
+mroute from wg0 group 239.5.5.5 to a2
+EOF
+cat "/tmp/$NM/conf"
+
+print "Starting smcrouted ..."
+../src/smcrouted -f "/tmp/$NM/conf" -n -P "/tmp/$NM/pid" -u "/tmp/$NM/sock" &
+sleep 1
+
+print "Validating JSON output for each show subcommand ..."
+
+check_json "show interfaces" \
+ "$($CTL -j show interfaces)" \
+ "any(i['name'] == 'a1' and i['vif'] == 0 for i in d['interfaces'])"
+
+check_json "show groups" \
+ "$($CTL -j show groups)" \
+ "any(g['group'] == '225.1.1.1' and g['iif'] == 'a1' for g in d['groups']['conf'])"
+
+check_json "show routes" \
+ "$($CTL -j show routes)" \
+ "any(r['group'] == '225.3.3.3' and r['iif'] == 'a1' and r['oifs'] == ['a2'] for r in d['routes']['asm']) \
+ and any(r['group'] == '225.4.4.4' and r['source'] == '10.0.0.5' for r in d['routes']['ssm'])"
+
+check_json "show pending" \
+ "$($CTL -j show pending)" \
+ "any(p['iif'] == 'wg0' and p['group'] == '239.5.5.5' and p['oifs'] == ['a2'] for p in d['pending'])"
+
+check_json "show (default = routes)" \
+ "$($CTL -j show)" \
+ "'routes' in d and 'asm' in d['routes'] and 'ssm' in d['routes'] and 'kernel' in d['routes']"
+
+OK
diff --git a/test/lib.sh b/test/lib.sh
index 901446c..144588a 100755
--- a/test/lib.sh
+++ b/test/lib.sh
@@ -35,6 +35,7 @@ OK()
exit 0
}
+# shellcheck disable=SC2068
check_dep()
{
if [ -n "$2" ]; then
diff --git a/test/pending.sh b/test/pending.sh
new file mode 100755
index 0000000..84cc4dd
--- /dev/null
+++ b/test/pending.sh
@@ -0,0 +1,85 @@
+#!/bin/sh
+# Verifies that an mroute/mgroup referencing an interface that does not
+# yet exist is queued on the pending list and automatically activated
+# when the interface arrives, without SIGHUP or `smcroutectl reload`.
+# Covers both a missing inbound (a1) and a missing outbound (a3).
+#set -x
+
+# shellcheck source=/dev/null
+. "$(dirname "$0")/lib.sh"
+
+print "Creating world ..."
+# Only a2 exists at startup; a1 (inbound) and a3 (outbound) come later.
+ip link add a2 type dummy
+ip link set a2 multicast on
+ip link set a2 up
+ip addr add 20.0.0.1/24 dev a2
+ip -br a
+
+print "Creating config that references a1 and a3 (not yet present) ..."
+cat <<EOF > "/tmp/$NM/conf"
+mroute from a1 group 225.1.2.3 to a2
+mroute from a2 group 225.7.8.9 to a3
+mgroup from a1 group 225.4.5.6
+EOF
+cat "/tmp/$NM/conf"
+
+print "Starting smcrouted ..."
+../src/smcrouted -f "/tmp/$NM/conf" -n -P "/tmp/$NM/pid" -l debug -u "/tmp/$NM/sock" &
+sleep 1
+
+print "Verifying both mroutes and the mgroup are pending ..."
+../src/smcroutectl -pu "/tmp/$NM/sock" show pending | tee "/tmp/$NM/pending-before"
+grep -Eq "^mroute.*from a1.*group 225\.1\.2\.3.*to a2" "/tmp/$NM/pending-before" \
+ || FAIL "mroute with missing inbound (a1) not on pending list"
+grep -Eq "^mroute.*from a2.*group 225\.7\.8\.9.*to a3" "/tmp/$NM/pending-before" \
+ || FAIL "mroute with missing outbound (a3) not on pending list"
+grep -Eq "^mgroup.*from a1.*group 225\.4\.5\.6" "/tmp/$NM/pending-before" \
+ || FAIL "mgroup not on pending list"
+
+print "Creating the missing interfaces a1 and a3 ..."
+ip link add a1 type dummy
+ip link set a1 multicast on
+ip link set a1 up
+ip addr add 10.0.0.1/24 dev a1
+ip link add a3 type dummy
+ip link set a3 multicast on
+ip link set a3 up
+ip addr add 30.0.0.1/24 dev a3
+
+# Give the netlink listener a moment to deliver the events and drain.
+sleep 1
+
+print "Verifying pending list is now empty ..."
+../src/smcroutectl -pu "/tmp/$NM/sock" show pending | tee "/tmp/$NM/pending-after"
+grep -q "^No pending" "/tmp/$NM/pending-after" \
+ || FAIL "pending list should be empty after both interfaces arrived"
+
+print "Verifying both mroutes are installed in smcrouted ..."
+../src/smcroutectl -pu "/tmp/$NM/sock" show | tee "/tmp/$NM/show-after"
+grep -Eq "225\.1\.2\.3.*a1.*a2" "/tmp/$NM/show-after" \
+ || FAIL "(*,225.1.2.3) a1 -> a2 missing after auto-activation"
+grep -Eq "225\.7\.8\.9.*a2.*a3" "/tmp/$NM/show-after" \
+ || FAIL "(*,225.7.8.9) a2 -> a3 missing after auto-activation"
+
+print "Verifying a1 and a3 have VIFs allocated ..."
+../src/smcroutectl -pu "/tmp/$NM/sock" show interfaces | tee "/tmp/$NM/iface-after"
+grep -Eq "^[[:space:]]*[0-9]+[[:space:]]+a1[[:space:]]+[0-9]+" "/tmp/$NM/iface-after" \
+ || FAIL "a1 should have a VIF after auto-activation"
+grep -Eq "^[[:space:]]*[0-9]+[[:space:]]+a3[[:space:]]+[0-9]+" "/tmp/$NM/iface-after" \
+ || FAIL "a3 should have a VIF after auto-activation"
+
+print "Verifying multicast actually flows through the new route ..."
+collect a2 -c2 'dst 225.1.2.3'
+ping -c 3 -W 1 -I a1 -t 2 225.1.2.3 >/dev/null
+show_mroute
+
+print "Analyzing ..."
+lines=$(tshark -r "/tmp/$NM/pcap" 2>/dev/null | grep 225.1.2.3 | tee "/tmp/$NM/result" | wc -l)
+cat "/tmp/$NM/result"
+echo " => $lines for 225.1.2.3, expected >= 2"
+
+########################################################################### DONE
+# One frame lost to (*,G) -> (S,G) install on first packet.
+[ "$lines" -ge 2 ] && OK
+FAIL
diff --git a/test/wrongvif.sh b/test/wrongvif.sh
new file mode 100755
index 0000000..987b1ea
--- /dev/null
+++ b/test/wrongvif.sh
@@ -0,0 +1,138 @@
+#!/bin/sh
+# Verifies WRONGVIF remediation when two routers share a source IP on
+# the same shared bridge -- the colliding-source case from issue #143.
+#
+# br0 (root ns)
+# | |
+# netns: R1 | | netns: R2
+# .-------------. | | .-------------.
+# | smcrouted | | | | smcrouted |
+# .--->| eth1 eth0 |---br0 port br0 port---| eth0 eth1 |<---.
+# | '-------------' '-------------' |
+# | |
+# netns: fpga1 netns: fpga2
+# eth0 == 10.0.0.2 eth0 == 10.0.0.2
+# emits multicast emits multicast
+#
+# fpga1 and fpga2 share 10.0.0.2 as source; this is the colliding
+# source from gsmecher's #143 report. fpga1 emits first -- R1
+# forwards onto br0, R2 sees the (10.0.0.2, 225.1.2.3) flow on its
+# eth0 and installs a stop filter in its kernel MFC. Then fpga2
+# emits the same (S,G) into R2.eth1; the kernel finds the existing
+# entry, sees the packet arrived on the wrong VIF, and raises
+# IGMPMSG_WRONGVIF. smcrouted's WRONGVIF remediation looks up the
+# matching (*,G) rule with rule->inbound == real iif and reinstalls
+# the MFC with the correct inbound iface.
+#
+# Without remediation R2's flow stays blocked; with it 1+ packets
+# reach br0 from R2 (the WRONGVIF that triggers the repair is lost).
+#
+# Packets from R1 and R2 share SRCIP+GROUP, so the test distinguishes
+# them by TTL: fpga1 ttl=11 (decremented to 10 by R1 at br0), fpga2
+# ttl=21 (decremented to 20 by R2 at br0).
+#set -x
+
+# shellcheck source=/dev/null
+. "$(dirname "$0")/lib.sh"
+
+print "Checking dependencies ..."
+check_dep tshark
+
+print "Creating world ..."
+# Persistent netns mount files; nsenter --net=<file> opens them. We
+# can't use `topo multi` because we need eth1 as a veth into a
+# separate fpga netns rather than the dummy it provides.
+PID=$$
+for ns in R1 R2 fpga1 fpga2; do
+ touch "/tmp/$NM/$ns"
+ echo "/tmp/$NM/$ns" >> "/tmp/$NM/mounts"
+ unshare --net="/tmp/$NM/$ns" -- ip link set lo up
+done
+
+# br0 (root ns) connects R1.eth0 and R2.eth0 via veth pairs.
+ip link add br0 type bridge
+ip link set br0 up
+
+for r in R1 R2; do
+ nsenter --net="/tmp/$NM/$r" -- ip link add eth0 type veth peer "${r}br"
+ nsenter --net="/tmp/$NM/$r" -- ip link set "${r}br" netns $PID
+ nsenter --net="/tmp/$NM/$r" -- ip link set eth0 up
+ ip link set "${r}br" master br0
+ ip link set "${r}br" up
+done
+
+# fpgaN.eth0 <-> RN.eth1 direct veth pairs (create in fpga, move the
+# eth1 end into the router via the netns mount file path).
+for n in 1 2; do
+ nsenter --net="/tmp/$NM/fpga$n" -- ip link add eth0 type veth peer eth1
+ nsenter --net="/tmp/$NM/fpga$n" -- ip link set eth1 netns "/tmp/$NM/R$n"
+ nsenter --net="/tmp/$NM/fpga$n" -- ip link set eth0 up
+ nsenter --net="/tmp/$NM/R$n" -- ip link set eth1 up
+done
+
+# IP addressing. Both fpgas share 10.0.0.2 -- the colliding source.
+nsenter --net="/tmp/$NM/R1" -- ip addr add 192.168.0.10/24 dev eth0
+nsenter --net="/tmp/$NM/R1" -- ip addr add 10.0.0.1/24 dev eth1
+nsenter --net="/tmp/$NM/R2" -- ip addr add 192.168.0.20/24 dev eth0
+nsenter --net="/tmp/$NM/R2" -- ip addr add 10.0.0.1/24 dev eth1
+nsenter --net="/tmp/$NM/fpga1" -- ip addr add 10.0.0.2/24 dev eth0
+nsenter --net="/tmp/$NM/fpga2" -- ip addr add 10.0.0.2/24 dev eth0
+nsenter --net="/tmp/$NM/fpga1" -- ip route add 225.1.2.3 dev eth0
+nsenter --net="/tmp/$NM/fpga2" -- ip route add 225.1.2.3 dev eth0
+
+# Both routers see multicast from 10.0.0.2 arriving on eth0 (from the
+# OTHER router's forward); rp_filter strict-mode would drop it because
+# 10.0.0.0/24 is reachable via eth1 in our route table. accept_local
+# additionally lets us accept multicast whose src matches one of our
+# own local addresses on a non-local iface.
+for ns in R1 R2; do
+ for k in all default eth0 eth1; do
+ nsenter --net="/tmp/$NM/$ns" -- sysctl -wq net.ipv4.conf.$k.rp_filter=0
+ nsenter --net="/tmp/$NM/$ns" -- sysctl -wq net.ipv4.conf.$k.accept_local=1
+ done
+done
+
+print "Creating config ..."
+cat <<EOF >"/tmp/$NM/shared.conf"
+phyint eth0 enable
+phyint eth1 enable
+
+mroute from eth1 group 225.1.2.3 to eth0
+EOF
+cat "/tmp/$NM/shared.conf"
+
+print "Starting smcrouted instances ..."
+nsenter --net="/tmp/$NM/R1" -- ../src/smcrouted -f "/tmp/$NM/shared.conf" -n -N -i R1 -l debug -u "/tmp/$NM/R1.sock" &
+echo $! >> "/tmp/$NM/PIDs"
+nsenter --net="/tmp/$NM/R2" -- ../src/smcrouted -f "/tmp/$NM/shared.conf" -n -N -i R2 -l debug -u "/tmp/$NM/R2.sock" &
+echo $! >> "/tmp/$NM/PIDs"
+sleep 1
+
+collect br0 -c10 'dst 225.1.2.3'
+
+print "Phase 1: fpga1 emits first (ttl=11), R2 should install a stop filter ..."
+nsenter --net="/tmp/$NM/fpga1" -- ping -c 3 -W 1 -t 11 225.1.2.3 > /dev/null
+sleep 1
+
+print "R2 MFC after Phase 1 (expect iif: eth0, no oifs):"
+nsenter --net="/tmp/$NM/R2" -- ip mroute
+
+print "Phase 2: fpga2 emits (ttl=21), WRONGVIF must remap to eth1 ..."
+nsenter --net="/tmp/$NM/fpga2" -- ping -c 3 -W 1 -t 21 225.1.2.3 > /dev/null
+sleep 2
+
+print "R2 MFC after Phase 2 (expect iif: eth1, oifs: eth0):"
+nsenter --net="/tmp/$NM/R2" -- ip mroute
+
+print "Analyzing br0 pcap ..."
+lines1=$(tshark -r "/tmp/$NM/pcap" 2>/dev/null | grep 225.1.2.3 | grep 'ttl=10' | tee "/tmp/$NM/result" | wc -l)
+lines2=$(tshark -r "/tmp/$NM/pcap" 2>/dev/null | grep 225.1.2.3 | grep 'ttl=20' | tee -a "/tmp/$NM/result" | wc -l)
+cat "/tmp/$NM/result"
+echo " => $lines1 packets from R1 (ttl=10), expected >= 2"
+echo " => $lines2 packets from R2 (ttl=20), expected >= 1"
+
+# R1 always works (NOCACHE -> rule match). R2 only works after the
+# WRONGVIF remap installs the correct MFC entry; the WRONGVIF packet
+# that triggers the remap is itself lost.
+[ "$lines1" -ge 2 ] && [ "$lines2" -ge 1 ] && OK
+FAIL