diff --git a/.gitignore b/.gitignore index da8f802..1a0bd77 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,7 @@ crash-* leak-* timeout-* oom-* + +# Test helper +test/solicit +test/.deps/ diff --git a/ChangeLog.md b/ChangeLog.md index 1520d1b..7f4316b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,10 +3,60 @@ Change Log All relevant, user visible, changes are documented in this file. -[UNRELEASED][] --------------- +[v3.1][] - 2026-10-21 +--------------------- + +Faster convergence and Multicast Router Discovery. `mcd` queries the +LAN as soon as a bridge port starts forwarding, can advertise itself +to snooping switches that learn router ports from MRD, and follows an +interface's own `query-interval` in all its timers. Many fixes for +robustness, found by fuzzing and static analysis in the Nix flake +contributed by Dave Seddon. + +> **Note:** the router timeout and the group membership interval now +> follow an interface's own `query-interval`. With `query-interval 4` +> on a bridge, `mcd` takes over from a silent querier after 13 sec and +> drops a group without members after 18 sec, not after 255 and 260 +> sec. An explicit `router-timeout` still applies to all interfaces. + +### Changes + - General query when a bridge port starts forwarding, RFC 4541 §2.1.1. + Link up, STP settling and a port added to the bridge all count, for + the bridge itself and VLAN interfaces on top of it. Hosts behind + the port are back in the MDB within the response interval instead + of at the next periodic query. On by default, see `topology-query` + in mcd.conf(5) + - Multicast Router Discovery, RFC 4286, IGMP and MLD. New `iface` + keyword `mrd on` advertises `mcd` as a multicast router to the + snooping switches on the LAN, answers their solicitations, and sends + a termination on the way down. Off by default. New global keyword + `mrd-interval` sets the advertisement interval, 4 to 180 sec + - `mctl show routers` lists the multicast routers heard through MRD + on each interface, with their advertised intervals. `mctl show + interfaces` has a new MRD column + - `mctl show interfaces json` lists each interface's router timeout + as `router-timeout`. `mctl show` labels the global value as the + default + - New `mcd -t` option to check a configuration file and exit, like + `nginx -t`. Errors are reported as `FILE:LINE: message`, and it + runs as a normal user + - Shell completion for `mctl`, bash and zsh. The commands come from + the running daemon, via the new `mctl -C` option. Installed with + `make install`, see `--with-bash-completion-dir` and + `--with-zsh-completion-dir` + - Nix flake with a development shell, native and cross builds, OCI + images, static analysis, fuzzing, and the test suite in microVMs, + see `nix/README.md`. By Dave Seddon ### Fixes + - Fix router timeout (other querier present interval) ignoring the + configured `robustness`, query and response intervals. It was always + 255 sec unless `router-timeout` was set, it now follows each + interface's own `query-interval` + - Fix group membership interval ignoring the interface's own + `query-interval` + - Fix `-l LEVEL` having no effect in the foreground, `mcd -n` printed + every message, debug included, whatever the level - Fix `mctl show router-ports` and `show igmp json` missing the router ports of bridges with a hyphen in their name, e.g. `br-0` - Fix shell command injection through bridge names when listing router @@ -15,9 +65,6 @@ All relevant, user visible, changes are documented in this file. the client - Fix `mcd` dying of `SIGPIPE` when an IPC client hangs up before reading its reply - - Fix default router timeout (other querier present interval) ignoring - the configured `robustness`, query and response intervals, it was - always 255 sec unless `router-timeout` was set - Fix `mcd` exiting 0 when it cannot fork to daemonize - Fix `mctl` crashing on a daemon command of more than 32 words - Fix timers stopping if an expiry landed exactly on a whole second @@ -295,7 +342,8 @@ Initial public release. Limited IGMPv1/v2/v3 querier with hard-coded query interval, etc. Put interfaces in a .conf file, whitespace separated to enable querier. -[UNRELEASED]: https://github.com/kernelkit/mcd/compare/v3.0...HEAD +[UNRELEASED]: https://github.com/kernelkit/mcd/compare/v3.1...HEAD +[v3.1]: https://github.com/kernelkit/mcd/compare/v3.0...v3.1 [v3.0]: https://github.com/kernelkit/mcd/compare/v2.4...v3.0 [v2.4]: https://github.com/kernelkit/mcd/compare/v2.3...v2.4 [v2.3]: https://github.com/kernelkit/mcd/compare/v2.2...v2.3 diff --git a/Makefile.am b/Makefile.am index 01c8af8..cf01822 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,6 @@ doc_DATA = README.md ChangeLog.md LICENSE mcd.conf EXTRA_DIST = README.md ChangeLog.md LICENSE mcd.conf doc/overview.svg doc/querier-timeline.svg doc/why.svg +EXTRA_DIST += completions/bash/mctl completions/zsh/_mctl DISTCLEANFILES = *~ DEADJOE semantic.cache *.gdb *.elf core core.* *.d SUBDIRS = man src @@ -7,6 +8,14 @@ if ENABLE_TEST SUBDIRS += test endif +if BASHCOMP +bashcomp_DATA = completions/bash/mctl +endif + +if ZSHCOMP +zshcomp_DATA = completions/zsh/_mctl +endif + ## Check if tagged in git release-hook: @if [ "x`git tag -l v$(PACKAGE_VERSION)|grep $(PACKAGE_VERSION)`" = "x" ]; then \ diff --git a/README.md b/README.md index 80f7d5a..7844b54 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,11 @@ Features `robustness` queries spaced `query-interval / 4` apart, per RFC 3376 §8.6 and §8.7. A host that missed the first query gets another one within half a minute instead of waiting a full query interval. +- Topology query. When a bridge port starts forwarding, after link + up, STP settling or being added to the bridge, `mcd` sends a general + query at once, per RFC 4541 §2.1.1. Hosts behind the port are back + in the MDB within the response interval instead of at the next + periodic query, up to two minutes later with the defaults. - Version fallback. Starts at IGMPv3 and MLDv2, drops to the older versions when such hosts appear, and returns when they go quiet. - Proxy mode, a backup querier that sends from `0.0.0.0` or `::` and can @@ -79,6 +84,9 @@ Features - Raw VLAN mode. Run a querier directly on a VLAN of a filtering bridge, no `vlanN` interface required. - Static groups, joined in the kernel so they never time out. +- Multicast Router Discovery, RFC 4286. With `mrd on`, `mcd` advertises + itself to the snooping switches on the LAN, for the ones that learn + their router ports from MRD rather than from queries. - `mctl` shows interface and querier status, group memberships, router ports per bridge, and the bridge MDB in a readable form. JSON output for scripts, a watch mode for humans. @@ -202,24 +210,25 @@ arguments for an overview: $ mctl Multicast Overview Query Interval (default): 125 sec - Router Timeout : 255 + Router Timeout (default): 255 sec Fast Leave Ports : Router Ports : eth3 Flood Ports : - Interface VID Proto Querier State Interval Timeout Ver - vlan1 IGMP 192.168.1.1 Up 125 None 3 - vlan1 MLD fe80::7a4c:9bff:fe12:3456 Up 125 None 2 - vlan2 IGMP 192.168.2.1 Up 125 None 3 - vlan2 MLD fe80::7a4c:9bff:fe12:3456 Up 125 None 2 - br0 42 IGMP 0.0.0.0 Up 125 None 3 - br0 42 MLD :: Up 125 None 2 + Interface VID Proto Querier State Interval Timeout MRD Ver + vlan1 IGMP 192.168.1.1 Up 125 None on 3 + vlan1 MLD fe80::7a4c:9bff:fe12:3456 Up 125 None on 2 + vlan2 IGMP 192.168.2.1 Up 125 None on 3 + vlan2 MLD fe80::7a4c:9bff:fe12:3456 Up 125 None on 2 + br0 42 IGMP 0.0.0.0 Up 125 None off 3 + br0 42 MLD :: Up 125 None off 2 Bridge VID Multicast Group Ports br0 2 239.1.1.1 eth3 The parts are also available on their own, as `show interfaces`, -`show groups`, `show router-ports` and `show mdb`. Add `json` to any +`show groups`, `show router-ports` and `show mdb`. `show routers` +lists the multicast routers heard through MRD. Add `json` to any `show` command for machine readable output, and `-m` to keep it updating like `watch(1)`. All commands and options are in [mctl(8)][]. @@ -243,10 +252,11 @@ it in the foreground with more logging: sudo mcd -n -l info `SIGHUP` reloads the configuration and probes the interfaces again. -To run several instances, give each its own identity with `-i NAME`, -which also names its configuration file, PID file and IPC socket, and -point `mctl -i NAME` at the one you want. The options, signals and -files are in [mcd(8)][]. +To check a configuration file before loading it, run `mcd -t -f FILE` +as any user. To run several instances, give each its own identity +with `-i NAME`, which also names its configuration file, PID file and +IPC socket, and point `mctl -i NAME` at the one you want. The +options, signals and files are in [mcd(8)][]. Building @@ -267,6 +277,7 @@ Options worth knowing: |------------------------|------------------------------------------------------------| | `--with-group=NAME` | Group owner of `/run/mcd.sock`, default `root`. Use e.g. `wheel` to let admins run `mctl` without `sudo` | | `--enable-test` | Build the test suite, see [Testing](#testing) | +| `--with-bash-completion-dir=DIR`, `--with-zsh-completion-dir=DIR` | Where to install the `mctl` completion scripts, default under `DATADIR`, `no` to skip | | `--prefix=`, `--sysconfdir=` | The usual, defaults are `/usr/local` and `/etc` | Configure prints a summary at the end, check it before running `make`. diff --git a/completions/bash/mctl b/completions/bash/mctl new file mode 100644 index 0000000..156882d --- /dev/null +++ b/completions/bash/mctl @@ -0,0 +1,36 @@ +# bash completion for mctl(8) +# +# The commands come from the running mcd, via mctl -C, so there is +# nothing to complete when it is down, apart from the options. + +_mctl() +{ + local cur=${COMP_WORDS[COMP_CWORD]} + local prev=${COMP_WORDS[COMP_CWORD-1]} + local word + + case $prev in + -u|--ipc) + COMPREPLY=($(compgen -f -- "$cur")) + return + ;; + -i|--ident) + return + ;; + esac + + if [[ $cur == -* ]]; then + COMPREPLY=($(compgen -W '-h --help -i --ident -m --monitor -p --plain + -t --no-heading -u --ipc -v --version' -- "$cur")) + return + fi + + # All words so far, options included, so -i NAME and -u FILE reach + # the same daemon the command will. The last word is the partial. + COMPREPLY=() + while IFS=$'\t' read -r word _; do + COMPREPLY+=("$word") + done < <(mctl -C "${COMP_WORDS[@]:1:COMP_CWORD}" 2>/dev/null) +} + +complete -F _mctl mctl diff --git a/completions/zsh/_mctl b/completions/zsh/_mctl new file mode 100644 index 0000000..1b96de8 --- /dev/null +++ b/completions/zsh/_mctl @@ -0,0 +1,34 @@ +#compdef mctl +# +# zsh completion for mctl(8). The commands come from the running +# mcd, via mctl -C, with their descriptions. + +_mctl() { + # The line as typed, _arguments rewrites $words for the command + local -a typed=("${words[@]}") cands + local cur=$CURRENT line + + _arguments -s \ + '(-h --help)'{-h,--help}'[Show usage and the available commands]' \ + '(-i --ident)'{-i,--ident}'[Talk to the mcd started with this identity]:name' \ + '(-m --monitor)'{-m,--monitor}'[Run the command every two seconds]' \ + '(-p --plain)'{-p,--plain}'[Plain table headings]' \ + '(-t --no-heading)'{-t,--no-heading}'[Skip table headings]' \ + '(-u --ipc)'{-u,--ipc}'[IPC socket]:file:_files' \ + '(-v --version)'{-v,--version}'[Show version and exit]' \ + '*::command:->command' && return + + case $state in + command) + # All words so far, options included, so -i NAME and -u FILE + # reach the same daemon the command will. worddesc + # becomes word:desc for _describe + for line in ${(f)"$(mctl -C "${typed[@]:1:cur-1}" 2>/dev/null)"}; do + cands+=("${line/$'\t'/:}") + done + _describe -t commands 'mctl command' cands + ;; + esac +} + +_mctl "$@" diff --git a/configure.ac b/configure.ac index 3913f3c..7ee53cd 100644 --- a/configure.ac +++ b/configure.ac @@ -1,5 +1,5 @@ AC_PREREQ([2.69]) -AC_INIT([mcd], [3.0], [https://github.com/kernelkit/mcd/issues]) +AC_INIT([mcd], [3.1], [https://github.com/kernelkit/mcd/issues]) AC_CONFIG_AUX_DIR(.aux) AM_INIT_AUTOMAKE([1.11 foreign]) AM_SILENT_RULES([yes]) @@ -65,6 +65,19 @@ AC_ARG_WITH(group, AS_HELP_STRING([--with-group=NAME], [Group for /run/mcd.sock (mctl), default: root]), [group=$withval], [group=root]) +AC_ARG_WITH(bash-completion-dir, + AS_HELP_STRING([--with-bash-completion-dir=DIR], [Install mctl bash completion in DIR, default: DATADIR/bash-completion/completions, no to skip]), + [bashcompdir=$withval], [bashcompdir=yes]) + +AC_ARG_WITH(zsh-completion-dir, + AS_HELP_STRING([--with-zsh-completion-dir=DIR], [Install mctl zsh completion in DIR, default: DATADIR/zsh/site-functions, no to skip]), + [zshcompdir=$withval], [zshcompdir=yes]) + +AS_IF([test "x$bashcompdir" = "xyes"], [bashcompdir='$(datadir)/bash-completion/completions']) +AS_IF([test "x$zshcompdir" = "xyes"], [zshcompdir='$(datadir)/zsh/site-functions']) +AC_SUBST(bashcompdir) +AC_SUBST(zshcompdir) + # Create config.h from selected features and fallback defautls AS_IF([test "x$with_group" != "xno"], [ AS_IF([test "x$group" = "xyes"], [ @@ -74,6 +87,8 @@ AC_DEFINE_UNQUOTED(DEFGROUP, "$group", [For /run/finit/socket]) AM_CONDITIONAL(BSD, [test "x$ac_cv_header_net_if_dl_h" = "xyes"]) AM_CONDITIONAL(LINUX, [test "x$ac_cv_header_linux_netlink_h" = "xyes"]) AM_CONDITIONAL(ENABLE_TEST, [test "x$enable_test" != "xno"]) +AM_CONDITIONAL(BASHCOMP, [test "x$bashcompdir" != "xno"]) +AM_CONDITIONAL(ZSHCOMP, [test "x$zshcompdir" != "xno"]) # Workaround for as-of-yet unreleased runstatedir support, planned for # autoconf 2.70, which some major distros have backported. @@ -95,6 +110,8 @@ cat <ifi_querier->al_addr, s1, sizeof(s1)); now = time(NULL); - timeout = router_timeout - (now - ifi->ifi_querier->al_ctime); + timeout = iface_router_timeout(ifi) - (now - ifi->ifi_querier->al_ctime); dumpster(s1, mac, sizeof(mac), port, sizeof(port)); if (detail) - fprintf(fp, "%4d %-15s %-17s %-16s %4d sec %d sec\n", vid, s1, mac, port, igmp_query_interval, timeout); + fprintf(fp, "%4d %-15s %-17s %-16s %4d sec %d sec\n", vid, s1, mac, port, iface_query_interval(ifi), timeout); else fprintf(fp, "%4d %-15s %-16s %d sec\n", vid, s1, port, timeout); } diff --git a/src/cfparse.y b/src/cfparse.y index 05e2931..e97f6cb 100644 --- a/src/cfparse.y +++ b/src/cfparse.y @@ -56,10 +56,10 @@ static void ifset(uint32_t set, uint32_t clr) %token GLOBAL_QUERY_INTERVAL GLOBAL_QUERY_LAST_MEMBER_INTERVAL GLOBAL_QUERY_RESPONSE_INTERVAL %token QUERY_INTERVAL -%token IGMP_ROBUSTNESS ROUTER_TIMEOUT ROUTER_ALERT INCLUDE +%token IGMP_ROBUSTNESS ROUTER_TIMEOUT ROUTER_ALERT TOPOLOGY_QUERY MRD_INTERVAL INCLUDE %token NO PHYINT %token DISABLE ENABLE IGMPV1 IGMPV2 IGMPV3 STATIC_GROUP PROXY_QUERIER -%token MLDV1 MLDV2 IGMP MLD +%token MLDV1 MLDV2 IGMP MLD MRD %token VLAN %token BOOLEAN %token NUMBER @@ -117,6 +117,20 @@ stmt : error { router_alert = $2; } + | NO TOPOLOGY_QUERY + { + topology_query = 0; + } + | TOPOLOGY_QUERY BOOLEAN + { + topology_query = $2; + } + | MRD_INTERVAL NUMBER + { + if ($2 < 4 || $2 > 180) + fatal("Invalid MRD advertisement interval [4,180]: %d", $2); + mrd_interval = $2; + } | ROUTER_TIMEOUT NUMBER { if ($2 < 1 || $2 > 1024) @@ -163,6 +177,7 @@ ifmod : DISABLE { ifset(IFIF_DISABLED, 0); } | MLDV2 { if (twin) twin->ifi_compat = GEN_V3; } | IGMP BOOLEAN { off4 = !$2; } | MLD BOOLEAN { off6 = !$2; } + | MRD BOOLEAN { ifset($2 ? IFIF_MRD : 0, $2 ? 0 : IFIF_MRD); } | STATIC_GROUP GROUP { struct listaddr *a; @@ -295,6 +310,8 @@ static struct keyword { { "disable", DISABLE, 0 }, { "enable", ENABLE, 0 }, { "router-alert", ROUTER_ALERT, 0 }, + { "topology-query", TOPOLOGY_QUERY, 0 }, + { "mrd-interval", MRD_INTERVAL, 0 }, { "igmpv1", IGMPV1, 0 }, { "igmpv2", IGMPV2, 0 }, { "igmpv3", IGMPV3, 0 }, @@ -302,6 +319,7 @@ static struct keyword { { "mldv2", MLDV2, 0 }, { "igmp", IGMP, 0 }, { "mld", MLD, 0 }, + { "mrd", MRD, 0 }, { "static-group", STATIC_GROUP, 0 }, { "proxy-queries", PROXY_QUERIER, 0}, /* compat */ { "proxy-mode", PROXY_QUERIER, 0}, @@ -375,22 +393,23 @@ static int yylex(void) return STRING; } -void config_init(void) +int config_init(void) { TAILQ_INIT(&scrap.ifi_static); TAILQ_INIT(&scrap.ifi_groups); lineno = 0; - dbg("Parsing file %s ...", config_file); - if (config_parse(config_file) && errno != ENOENT) { + if (config_parse(config_file)) { + int rc = errno; + err("cannot open %s", config_file); - exit(EX_DATAERR); + if (rc != ENOENT) + exit(EX_DATAERR); + return -1; /* may be created later, wait for SIGHUP */ } - /* Unless set, follows the intervals and robustness just read */ - if (!router_timeout) - router_timeout = IGMP_OTHER_QUERIER_PRESENT_INTERVAL; + return 0; } int config_parse(const char *file) diff --git a/src/config.c b/src/config.c index 4f08ab0..4e9269f 100644 --- a/src/config.c +++ b/src/config.c @@ -122,6 +122,7 @@ void config_iface_update(struct ifi *ifi) { if (getmac(ifi->ifi_name, ifi->ifi_hwaddr, sizeof(ifi->ifi_hwaddr))) warn("failed finding hw address for iface %s", ifi->ifi_name); + ifi->ifi_lower = netlink_link_lower(ifi->ifi_index); } /* @@ -172,14 +173,12 @@ struct ifi *config_iface_add(char *ifname) * May not exist yet, prepare for netlink event later */ ifindex = if_nametoindex(ifname); - if (ifindex) + if (ifindex) { ifi->ifi_index = ifindex; - else + config_iface_update(ifi); + } else ifi->ifi_flags |= IFIF_DOWN; - if (getmac(ifname, ifi->ifi_hwaddr, sizeof(ifi->ifi_hwaddr))) - warn("failed finding hw address for iface %s", ifname); - TAILQ_INSERT_TAIL(&ifaces, ifi, ifi_link); iface_clone(ifi, AF_INET6, ifi); diff --git a/src/defs.h b/src/defs.h index ba7379e..17c440c 100644 --- a/src/defs.h +++ b/src/defs.h @@ -60,6 +60,8 @@ TAILQ_HEAD(ifaces, ifi); * External declarations for global variables and functions. */ extern int router_alert; +extern int topology_query; +extern int mrd_interval; extern uint32_t router_timeout; extern uint32_t allhosts_group; extern uint32_t allrtrs_group; @@ -69,7 +71,6 @@ extern uint32_t igmp_query_interval; extern uint32_t igmp_last_member_interval; extern uint32_t igmp_robustness; -extern int loglevel; extern int use_syslog; extern int running; extern int haveterminal; @@ -101,6 +102,9 @@ extern int did_final_init; #ifndef INADDR_ALLRPTS_GROUP #define INADDR_ALLRPTS_GROUP ((in_addr_t)0xe0000016) /* 224.0.0.22, IGMPv3 */ +#ifndef INADDR_ALLSNOOPERS_GROUP +#define INADDR_ALLSNOOPERS_GROUP 0xe000006aU /* 224.0.0.106, RFC 4286 */ +#endif #endif #ifndef INADDR_MAX_LOCAL_GROUP @@ -131,10 +135,13 @@ extern void iface_add(int, int); extern void iface_del(int, int); extern void iface_check_election(struct ifi *); extern void iface_check(int, unsigned int); +extern void iface_topology_change(int, int); extern void iface_check_state(void); extern void iface_exit(void); extern const char *iface_proto(const struct ifi *); extern int iface_pv(const struct ifi *, int); +extern uint32_t iface_query_interval(const struct ifi *); +extern uint32_t iface_router_timeout(const struct ifi *); extern void accept_group_report(struct ifi *, const inet_addr_t *, const inet_addr_t *, int); extern void accept_group_record(struct ifi *, const inet_addr_t *, const inet_addr_t *, int, int); extern void accept_v3_report(struct ifi *, const inet_addr_t *, const uint8_t *, size_t); @@ -144,9 +151,10 @@ extern void accept_membership_query(struct ifi *, const inet_addr_t *, const in /* netlink.c */ extern void netlink_init(void); extern void netlink_exit(void); +extern int netlink_link_lower(int); /* config.c */ -extern void config_init(void); +extern int config_init(void); extern void config_set_ifflag(uint32_t); extern struct ifi *config_iface_iter(int); extern struct ifi *config_iface_add(char *); diff --git a/src/iface.c b/src/iface.c index 384be42..02a5e78 100644 --- a/src/iface.c +++ b/src/iface.c @@ -1,6 +1,7 @@ /* SPDX-License-Identifier: ISC */ #include "defs.h" +#include "mrd.h" #include "inet.h" /* @@ -30,6 +31,7 @@ static void startup_query_cb (int timeout, void *arg); static void startup_query_timer(struct ifi *ifi); static void router_timeout_cb (int timeout, void *arg); +static void querier_start (struct ifi *ifi); static void delete_group_cb (int timeout, void *arg); static int delete_group_timer (struct ifi *ifi, struct listaddr *g, int tmo); @@ -81,6 +83,7 @@ void iface_lists_init(struct ifi *ifi) TAILQ_INIT(&ifi->ifi_static); TAILQ_INIT(&ifi->ifi_groups); TAILQ_INIT(&ifi->ifi_addrs); + TAILQ_INIT(&ifi->ifi_mrd_routers); } void iface_zero(struct ifi *ifi) @@ -96,6 +99,9 @@ void iface_zero(struct ifi *ifi) ifi->ifi_query_interval = igmp_query_interval; ifi->ifi_timerid = 0; ifi->ifi_startup_timerid = 0; + ifi->ifi_mrd_timerid = 0; + ifi->ifi_mrd_init = 0; + ifi->ifi_mrd_pending = 0; ifi->ifi_igmpv1_warn = 0; } @@ -118,6 +124,31 @@ int iface_pv(const struct ifi *ifi, int pv) return ifi->ifi_family == AF_INET6 ? pv - 1 : pv; } +/* The interface's query interval, or the global one, NULL for global */ +uint32_t iface_query_interval(const struct ifi *ifi) +{ + return ifi && ifi->ifi_query_interval ? ifi->ifi_query_interval : igmp_query_interval; +} + +/* + * Other querier present interval, RFC 3376 section 8.5 and RFC 3810 + * section 9.5, from the interface's own query interval. The global + * default when ifi is NULL, router-timeout overrides both. + */ +uint32_t iface_router_timeout(const struct ifi *ifi) +{ + if (router_timeout) + return router_timeout; + + return igmp_robustness * iface_query_interval(ifi) + igmp_response_interval / 2; +} + +/* Group membership interval, RFC 3376 section 8.4, RFC 3810 section 9.4 */ +static uint32_t iface_group_interval(const struct ifi *ifi) +{ + return igmp_robustness * iface_query_interval(ifi) + igmp_response_interval; +} + static void querier_del(struct ifi *ifi) { if (!ifi->ifi_querier) @@ -211,8 +242,7 @@ void iface_check_election(struct ifi *ifi) ifi->ifi_flags |= IFIF_IGMP_QUERIER; dbg("Assuming %squerier duties on interface %s", is_igmp_proxy(ifi) ? "proxy " : "", ifi->ifi_name); - send_query(ifi, igmp_response_interval, NULL); - startup_query_timer(ifi); /* RFC3376 §8.6, §8.7 startup query burst */ + querier_start(ifi); } /* @@ -381,11 +411,7 @@ static void join_static_groups(struct ifi *ifi) static void start_iface(struct ifi *ifi) { - uint32_t qi = ifi->ifi_query_interval; - - /* Check interface specific settings */ - if (!qi) - qi = igmp_query_interval; + uint32_t qi = iface_query_interval(ifi); sock_iface_init(ifi); join_static_groups(ifi); @@ -403,6 +429,8 @@ static void start_iface(struct ifi *ifi) if (!(ifi->ifi_flags & IFIF_DISABLED)) iface_check_election(ifi); + mrd_start(ifi); + info("Interface %s now in service", ifi->ifi_name); } @@ -410,13 +438,17 @@ static void stop_iface(struct ifi *ifi) { struct listaddr *a, *tmp; + mrd_stop(ifi); + /* - * Stop query timers (periodic and startup burst) + * Stop query timers (periodic, startup burst and topology hold-off) */ if (ifi->ifi_startup_timerid > 0) { pev_timer_del(ifi->ifi_startup_timerid); ifi->ifi_startup_timerid = 0; } + ifi->ifi_topo_timerid = pev_timer_del(ifi->ifi_topo_timerid); + ifi->ifi_topo_pending = 0; if (ifi->ifi_timerid > 0) pev_timer_del(ifi->ifi_timerid); ifi->ifi_timerid = 0; @@ -424,6 +456,7 @@ static void stop_iface(struct ifi *ifi) TAILQ_FOREACH_SAFE(a, &ifi->ifi_groups, al_link, tmp) group_del(ifi, a); querier_del(ifi); + mrd_flush(ifi); dbg("Releasing querier duties on interface %s", ifi->ifi_name); ifi->ifi_flags &= ~IFIF_IGMP_QUERIER; @@ -461,6 +494,7 @@ static void query_groups(int period, void *arg) void accept_membership_query(struct ifi *ifi, const inet_addr_t *src, const inet_addr_t *group, int intv, int tmo, int ver) { + uint32_t oqpi = iface_router_timeout(ifi); int notnew = 1; inet_addr_fmt(src, s1, sizeof(s1)); @@ -509,7 +543,7 @@ void accept_membership_query(struct ifi *ifi, const inet_addr_t *src, const inet again: dbg("New querier %s (was %s) on %s, timeout %d", s1, ifi->ifi_querier ? inet_addr_fmt(&ifi->ifi_querier->al_addr, s2, sizeof(s2)) : "me", - ifi->ifi_name, router_timeout); + ifi->ifi_name, oqpi); if (!ifi->ifi_querier) { ifi->ifi_querier = calloc(1, sizeof(struct listaddr)); @@ -518,7 +552,7 @@ void accept_membership_query(struct ifi *ifi, const inet_addr_t *src, const inet exit(EX_OSERR); } - ifi->ifi_querier->al_timerid = pev_timer_add(router_timeout * 1000000, 0, router_timeout_cb, ifi); + ifi->ifi_querier->al_timerid = pev_timer_add(oqpi * 1000000, 0, router_timeout_cb, ifi); ifi->ifi_flags &= ~IFIF_IGMP_QUERIER; } @@ -544,8 +578,8 @@ void accept_membership_query(struct ifi *ifi, const inet_addr_t *src, const inet * Reset the timer since we've received a query. */ if (notnew && ifi->ifi_querier && !inet_addr_cmp(src, &ifi->ifi_querier->al_addr)) { - dbg("Resetting query timeout %d sec", router_timeout); - pev_timer_set(ifi->ifi_querier->al_timerid, router_timeout * 1000000); + dbg("Resetting query timeout %d sec", oqpi); + pev_timer_set(ifi->ifi_querier->al_timerid, oqpi * 1000000); time(&ifi->ifi_querier->al_ctime); } @@ -628,7 +662,7 @@ void accept_group_report(struct ifi *ifi, const inet_addr_t *src, const inet_add if (g->al_timerid > 0) g->al_timerid = pev_timer_del(g->al_timerid); - g->al_timerid = delete_group_timer(ifi, g, IGMP_GROUP_MEMBERSHIP_INTERVAL); + g->al_timerid = delete_group_timer(ifi, g, iface_group_interval(ifi)); /* * Reset timer for switching version back every time an older @@ -655,7 +689,7 @@ void accept_group_report(struct ifi *ifi, const inet_addr_t *src, const inet_add /** set a timer for expiration **/ g->al_reporter = *src; - g->al_timerid = delete_group_timer(ifi, g, IGMP_GROUP_MEMBERSHIP_INTERVAL); + g->al_timerid = delete_group_timer(ifi, g, iface_group_interval(ifi)); /* * Set timer for swithing version back if an older version @@ -886,7 +920,7 @@ static void startup_query_cb(int timeout, void *arg) */ static void startup_query_timer(struct ifi *ifi) { - uint32_t qi = ifi->ifi_query_interval ? ifi->ifi_query_interval : igmp_query_interval; + uint32_t qi = iface_query_interval(ifi); uint32_t sqi = qi / 4 > 0 ? qi / 4 : 1; cbk_t *cbk; @@ -904,6 +938,78 @@ static void startup_query_timer(struct ifi *ifi) pev_timer_set_cb_del(ifi->ifi_startup_timerid, free); } +/* + * Become querier: first general query and the RFC 3376 §8.6, §8.7 + * startup query burst. + */ +static void querier_start(struct ifi *ifi) +{ + send_query(ifi, igmp_response_interval, NULL); + startup_query_timer(ifi); +} + +static void topo_holdoff_cb(int timeout, void *arg); + +/* + * Topology query, then one second of hold-off so a flapping port + * gets one query per second. + */ +static void topo_query(struct ifi *ifi) +{ + ifi->ifi_topo_pending = 0; + querier_start(ifi); + + if (ifi->ifi_topo_timerid > 0) + pev_timer_set(ifi->ifi_topo_timerid, 1000000); + else + ifi->ifi_topo_timerid = pev_timer_add(1000000, 0, topo_holdoff_cb, ifi); +} + +static void topo_holdoff_cb(int timeout, void *arg) +{ + struct ifi *ifi = (struct ifi *)arg; + + if (ifi->ifi_topo_pending && (ifi->ifi_flags & IFIF_IGMP_QUERIER)) + topo_query(ifi); + else + ifi->ifi_topo_timerid = pev_timer_del(ifi->ifi_topo_timerid); +} + +/* + * A port of the bridge started forwarding: link up, STP settled or + * port added. Hosts behind it are not in the MDB until they answer + * a query, so send one now rather than at the next query interval, + * RFC 4541 section 2.1.1. Our interface is the bridge itself or a + * vlan interface on top of it. + */ +void iface_topology_change(int bridge, int port) +{ + struct ifi *ifi; + + if (!topology_query) + return; + + TAILQ_FOREACH(ifi, &ifaces, ifi_link) { + char ifname[IFNAMSIZ]; + + if (ifi->ifi_index != bridge && ifi->ifi_lower != bridge) + continue; + if (ifi->ifi_flags & (IFIF_DOWN | IFIF_DISABLED)) + continue; + if (!(ifi->ifi_flags & IFIF_IGMP_QUERIER)) + continue; + + if (ifi->ifi_topo_timerid > 0) { + ifi->ifi_topo_pending = 1; + continue; + } + + info("Port %s forwarding, sending query on %s", + if_indextoname(port, ifname) ?: "?", ifi->ifi_name); + topo_query(ifi); + } +} + /* * When an active querier times out we assume the role here. */ @@ -915,8 +1021,7 @@ static void router_timeout_cb(int timeout, void *arg) querier_del(ifi); ifi->ifi_flags |= IFIF_IGMP_QUERIER; - send_query(ifi, igmp_response_interval, NULL); - startup_query_timer(ifi); /* RFC3376 §8.6, §8.7 startup query burst */ + querier_start(ifi); } /* @@ -939,7 +1044,7 @@ static void group_version_cb(int timeout, void *arg) inet_addr_fmt(&cbk->g->al_addr, s1, sizeof(s1)), ifi->ifi_name); if (cbk->g->al_pv < 3) - pev_timer_set(cbk->g->al_pv_timerid, IGMP_GROUP_MEMBERSHIP_INTERVAL * 1000000); + pev_timer_set(cbk->g->al_pv_timerid, iface_group_interval(ifi) * 1000000); else cbk->g->al_pv_timerid = pev_timer_del(cbk->g->al_pv_timerid); } @@ -951,7 +1056,7 @@ static int group_version_timer(struct ifi *ifi, struct listaddr *g) { int tid; - tid = pev_timer_add(IGMP_GROUP_MEMBERSHIP_INTERVAL * 1000000, 0, group_version_cb, cbk_new(ifi, g)); + tid = pev_timer_add(iface_group_interval(ifi) * 1000000, 0, group_version_cb, cbk_new(ifi, g)); pev_timer_set_cb_del(tid, free); return tid; diff --git a/src/iface.h b/src/iface.h index 6f3ac7a..59526cb 100644 --- a/src/iface.h +++ b/src/iface.h @@ -17,11 +17,13 @@ struct ifi { TAILQ_HEAD(,listaddr) ifi_static; /* list of static groups (phyints) */ TAILQ_HEAD(,listaddr) ifi_groups; /* list of local groups (phyints) */ TAILQ_HEAD(,phaddr) ifi_addrs; /* Secondary addresses */ + TAILQ_HEAD(,mrd_router) ifi_mrd_routers; /* MRD routers heard */ struct sock *ifi_sock; /* Packet socket, shared per family */ uint32_t ifi_flags; /* IFIF_ flags defined below */ int ifi_vlan; /* Raw VLAN ID for send and accept */ char ifi_name[IFNAMSIZ]; /* interface name */ int ifi_index; /* Primarily for Linux systems */ + int ifi_lower; /* Bridge below a vlan interface */ int ifi_family; /* AF_INET or AF_INET6, one querier each */ int ifi_compat; /* Protocol generation, GEN_V1 .. GEN_V3 */ inet_addr_t ifi_inaddr; /* Current address of this interface */ @@ -29,6 +31,11 @@ struct ifi { struct listaddr *ifi_querier; /* IGMP querier (one or none) */ int ifi_timerid; /* IGMP query timer */ int ifi_startup_timerid;/* startup query burst timer */ + int ifi_topo_timerid; /* hold-off after a topology query */ + int ifi_topo_pending; /* port change during the hold-off */ + int ifi_mrd_timerid; /* next MRD advertisement */ + int ifi_mrd_init; /* initial advertisements sent */ + int ifi_mrd_pending; /* advertisement already on its way */ int ifi_igmpv1_warn; /* To rate-limit IGMPv1 warnings */ uint8_t ifi_hwaddr[6]; /* MAC address of this interface */ }; @@ -37,6 +44,7 @@ struct ifi { #define IFIF_DISABLED 0x000200 /* administratively disabled */ #define IFIF_IGMP_QUERIER 0x000400 /* I am the subnet's querier */ #define IFIF_PROXY_QUERIER 0x002000 /* Act as proxy querier */ +#define IFIF_MRD 0x004000 /* Multicast Router Discovery */ /* * Protocol generations shared by IGMP and MLD, per interface compat @@ -67,6 +75,16 @@ struct listaddr { #define NBRF_STATIC_GROUP 0x4000 /* Static group entry */ +/* A multicast router heard on the LAN, RFC 4286 */ +struct mrd_router { + TAILQ_ENTRY(mrd_router) link; + inet_addr_t addr; + time_t expires; /* NeighborDeadInterval from last */ + int interval; /* its advertisement interval, sec */ + int qqi; /* its query interval, sec */ + int qrv; /* its robustness variable */ +}; + #endif /* MCD_IFACE_H_ */ /** diff --git a/src/igmp.c b/src/igmp.c index efe4bb9..3252bdd 100644 --- a/src/igmp.c +++ b/src/igmp.c @@ -6,11 +6,14 @@ #include "defs.h" #include "igmp.h" #include "inet.h" +#include "mrd.h" /* * Exported variables. */ int router_alert; /* IP option Router Alert */ +int topology_query; /* Query when a port comes up */ +int mrd_interval; /* MRD advertisement interval */ uint32_t router_timeout; /* Other querier present intv. */ uint32_t igmp_query_interval; /* Default: 125 sec */ uint32_t igmp_response_interval; /* Default: 10 sec */ @@ -30,8 +33,10 @@ void igmp_init(void) igmp_response_interval = IGMP_QUERY_RESPONSE_INTERVAL; igmp_last_member_interval = IGMP_LAST_MEMBER_INTERVAL_DEFAULT; igmp_robustness = IGMP_ROBUSTNESS_DEFAULT; - router_timeout = 0; /* unset, see config_init() */ + router_timeout = 0; /* unset, see iface_router_timeout() */ router_alert = 1; + topology_query = 1; + mrd_interval = MRD_ADV_INTERVAL; } static const char *igmp_packet_kind(uint8_t type) @@ -42,6 +47,9 @@ static const char *igmp_packet_kind(uint8_t type) case IGMP_V2_MEMBERSHIP_REPORT: return "v2 member report "; case IGMP_V3_MEMBERSHIP_REPORT: return "v3 member report "; case IGMP_V2_LEAVE_GROUP: return "leave message "; + case IGMP_MRD_ADVERT: return "mrd advertisement "; + case IGMP_MRD_ADVERT + 1: return "mrd solicitation "; + case IGMP_MRD_ADVERT + 2: return "mrd termination "; default: return NULL; } } @@ -52,6 +60,7 @@ static const char *igmp_packet_kind(uint8_t type) void accept_igmp(struct ifi *ifi, int vid, uint8_t *buf, size_t len) { int ipdatalen, iphdrlen, igmpdatalen, timeout, interval = 0, ver = GEN_V3; + int msg; inet_addr_t src, group; const char *kind; struct igmp *igmp; @@ -75,20 +84,34 @@ void accept_igmp(struct ifi *ifi, int vid, uint8_t *buf, size_t len) return; } - igmp = (struct igmp *)(buf + iphdrlen); - igmpdatalen = ipdatalen - IGMP_MINLEN; - if (igmpdatalen < 0) { - info("received IP data field too short (%u bytes) for IGMP, from %s", ipdatalen, s1); - return; - } - + igmp = (struct igmp *)(buf + iphdrlen); kind = igmp_packet_kind(igmp->igmp_type); if (!kind) return; + if (ipdatalen < 4 || inet_cksum((uint16_t *)igmp, ipdatalen)) { + info("received IGMP packet with bad checksum from %s", s1); + return; + } + dbg("RECV %s from %-15s ifi %-2d.%d to %s", kind, s1, ifi->ifi_index, vid, inet_fmt(ip->ip_dst.s_addr, s2, sizeof(s2))); + msg = mrd_msg(ifi, igmp->igmp_type); + if (msg >= 0) { + inet_addr_t dst; + + inet_addr_set4(&dst, ip->ip_dst.s_addr); + mrd_accept(ifi, msg, &src, &dst, igmp, ipdatalen); + return; + } + + igmpdatalen = ipdatalen - IGMP_MINLEN; + if (igmpdatalen < 0) { + info("received IP data field too short (%u bytes) for IGMP, from %s", ipdatalen, s1); + return; + } + inet_addr_set4(&group, igmp->igmp_group.s_addr); switch (igmp->igmp_type) { @@ -128,7 +151,7 @@ void accept_igmp(struct ifi *ifi, int vid, uint8_t *buf, size_t len) } } -static size_t build_ipv4(uint8_t *buf, uint32_t src, uint32_t dst, short unsigned int datalen) +size_t build_ipv4(uint8_t *buf, uint32_t src, uint32_t dst, short unsigned int datalen) { struct ip *ip = (struct ip *)(buf); size_t len = IP_HEADER_RAOPT_LEN; diff --git a/src/igmp.h b/src/igmp.h index a5a0735..f0ceefe 100644 --- a/src/igmp.h +++ b/src/igmp.h @@ -14,15 +14,6 @@ #define IGMP_ROBUSTNESS_DEFAULT 2 #define IGMP_QUERY_INTERVAL_DEFAULT 125 #define IGMP_QUERY_RESPONSE_INTERVAL 10 -#define IGMP_GROUP_MEMBERSHIP_INTERVAL (igmp_robustness * \ - igmp_query_interval + \ - igmp_response_interval) -#define IGMP_OTHER_QUERIER_PRESENT_INTERVAL (igmp_robustness * \ - igmp_query_interval + \ - igmp_response_interval / 2) - /* Round to the nearest TIMER_INTERVAL */ -#define IGMP_STARTUP_QUERY_INTERVAL (((igmp_query_interval / 4) \ - / TIMER_INTERVAL) * TIMER_INTERVAL) #define IGMP_STARTUP_QUERY_COUNT igmp_robustness #define IGMP_LAST_MEMBER_INTERVAL_DEFAULT 1 #define IGMP_LAST_MEMBER_QUERY_COUNT igmp_robustness @@ -207,6 +198,8 @@ static inline uint16_t igmp_code_time(uint8_t code) return (mant | 0x10) << (exp + 3); } +extern size_t build_ipv4(uint8_t *, uint32_t, uint32_t, short unsigned int); + #endif /* MCD_IGMP_H_ */ /** diff --git a/src/ipc.c b/src/ipc.c index 50ed4a6..7d90ef7 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -49,6 +49,7 @@ enum { IPC_STATUS, IPC_MDB, IPC_ROUTER_PORTS, + IPC_MRD_ROUTERS, }; struct ipcmd { @@ -63,6 +64,7 @@ struct ipcmd { { IPC_IGMP_GRP, "show groups", "[json]", "Show IGMP/MLD group memberships" }, { IPC_IGMP_IFACE, "show interfaces", "[json]", "Show IGMP/MLD interface status" }, { IPC_ROUTER_PORTS, "show router-ports", "[json]", "Show multicast router ports per bridge" }, + { IPC_MRD_ROUTERS, "show routers", "[json]", "Show multicast routers heard, RFC 4286" }, { IPC_STATUS, "show status", "[json]", "Show daemon status (default)" }, { IPC_IGMP, "show igmp", "[json]", "Show interfaces and group memberships" }, { IPC_MDB, "show mdb", NULL, "Show multicast forwarding database" }, @@ -308,8 +310,10 @@ static int show_status(FILE *fp) jprinti(fp, "query-response-interval", igmp_response_interval, &first); jprinti(fp, "query-last-member-interval", igmp_last_member_interval, &first); jprinti(fp, "robustness", igmp_robustness, &first); - jprinti(fp, "router-timeout", router_timeout, &first); + jprinti(fp, "router-timeout", iface_router_timeout(NULL), &first); jprintb(fp, "router-alert", router_alert, &first); + jprintb(fp, "topology-query", topology_query, &first); + jprinti(fp, "mrd-interval", mrd_interval, &first); if (!nested) { prefix -= 2; @@ -324,9 +328,12 @@ static int show_status(FILE *fp) fprintf(fp, "Last Member Interval : %d\n", igmp_last_member_interval); fprintf(fp, "Robustness Value : %d\n", igmp_robustness); } - fprintf(fp, "Router Timeout : %d\n", router_timeout); - if (detail) + fprintf(fp, "Router Timeout (default): %d sec\n", iface_router_timeout(NULL)); + if (detail) { fprintf(fp, "Router Alert : %s\n", ENABLED(router_alert)); + fprintf(fp, "Topology Query : %s\n", ENABLED(topology_query)); + fprintf(fp, "MRD Interval : %d\n", mrd_interval); + } } return 0; @@ -341,11 +348,12 @@ static int show_igmp_iface(FILE *fp, int family) fprintf(fp, "[\n"); prefix += 2; } else - fprintf(fp, "%-15s%4s %-5s %-27s %8s %8s %7s %3s=\n", "Interface", "VID", - "Proto", "Querier", "State", "Interval", "Timeout", "Ver"); + fprintf(fp, "%-15s%4s %-5s %-27s %8s %8s %7s %3s %3s=\n", "Interface", "VID", + "Proto", "Querier", "State", "Interval", "Timeout", "MRD", "Ver"); for (ifi = config_iface_iter(1); ifi; ifi = config_iface_iter(0)) { int interval = ifi->ifi_query_interval, rt_tmo = -1; + int oqpi = iface_router_timeout(ifi); char timeout[10]; int version; @@ -361,7 +369,7 @@ static int show_igmp_iface(FILE *fp, int family) inet_addr_fmt(&ifi->ifi_querier->al_addr, s1, sizeof(s1)); interval = ifi->ifi_querier->al_interval; t = time(NULL) - ifi->ifi_querier->al_ctime; - rt_tmo = (int)((time_t)router_timeout - t); + rt_tmo = (int)((time_t)oqpi - t); snprintf(timeout, sizeof(timeout), "%d", rt_tmo); } @@ -387,6 +395,8 @@ static int show_igmp_iface(FILE *fp, int family) if (rt_tmo > -1) jprinti(fp, "timeout", rt_tmo, &once); jprinti(fp, "interval", interval, &once); + jprinti(fp, "router-timeout", oqpi, &once); + jprintb(fp, "mrd", ifi->ifi_flags & IFIF_MRD, &once); jprinti(fp, "version", version, &once); prefix -= 2; @@ -397,8 +407,9 @@ static int show_igmp_iface(FILE *fp, int family) if (ifi->ifi_vlan) snprintf(vlan, sizeof(vlan), "%4d", ifi->ifi_vlan); - fprintf(fp, "%-15s%4s %-5s %-27s %8s %8d %7s %3d\n", ifi->ifi_name, - vlan, iface_proto(ifi), s1, ifstate(ifi), interval, timeout, version); + fprintf(fp, "%-15s%4s %-5s %-27s %8s %8d %7s %3s %3d\n", ifi->ifi_name, + vlan, iface_proto(ifi), s1, ifstate(ifi), interval, timeout, + ifi->ifi_flags & IFIF_MRD ? "on" : "off", version); } } @@ -415,6 +426,64 @@ static int show_igmp_ifaces(FILE *fp) return show_igmp_iface(fp, AF_UNSPEC); } +static int show_mrd_routers(FILE *fp) +{ + time_t now = time(NULL); + struct ifi *ifi; + int first = 1; + + if (json) { + fprintf(fp, "[\n"); + prefix += 2; + } else + fprintf(fp, "%-15s%4s %-5s %-27s %8s %5s %3s %7s=\n", "Interface", "VID", + "Proto", "Router", "Interval", "Query", "QRV", "Expires"); + + for (ifi = config_iface_iter(1); ifi; ifi = config_iface_iter(0)) { + struct mrd_router *r; + + TAILQ_FOREACH(r, &ifi->ifi_mrd_routers, link) { + int expires = (int)(r->expires - now); + + if (expires <= 0) + continue; + inet_addr_fmt(&r->addr, s1, sizeof(s1)); + if (json) { + int once = 1; + + fprintf(fp, "%s%*s{\n", first ? "" : ",\n", prefix, ""); + prefix += 2; + jprint(fp, "interface", ifi->ifi_name, &once); + jprint(fp, "family", ifi->ifi_family == AF_INET6 ? "ipv6" : "ipv4", &once); + if (ifi->ifi_vlan) + jprinti(fp, "vid", ifi->ifi_vlan, &once); + jprint(fp, "router", s1, &once); + jprinti(fp, "interval", r->interval, &once); + jprinti(fp, "query-interval", r->qqi, &once); + jprinti(fp, "robustness", r->qrv, &once); + jprinti(fp, "expires", expires, &once); + prefix -= 2; + fprintf(fp, "\n%*s}", prefix, ""); + } else { + char vlan[5] = { 0 }; + + if (ifi->ifi_vlan) + snprintf(vlan, sizeof(vlan), "%4d", ifi->ifi_vlan); + fprintf(fp, "%-15s%4s %-5s %-27s %8d %5d %3d %7d\n", ifi->ifi_name, + vlan, iface_proto(ifi), s1, r->interval, r->qqi, r->qrv, expires); + } + first = 0; + } + } + + if (json) { + prefix -= 2; + fprintf(fp, "\n%*s]", prefix, ""); + } + + return 0; +} + static int show_igmp(FILE *fp) { int rc = 0; @@ -662,6 +731,10 @@ static void ipc_handle(int sd, void *arg) ipc_show(client, show_router_ports, cmd, sizeof(cmd)); break; + case IPC_MRD_ROUTERS: + ipc_show(client, show_mrd_routers, cmd, sizeof(cmd)); + break; + case IPC_COMPAT: ipc_show(client, show_bridge_compat, cmd, sizeof(cmd)); break; diff --git a/src/log.c b/src/log.c index c4825dd..804a8f7 100644 --- a/src/log.c +++ b/src/log.c @@ -102,44 +102,20 @@ void prinl(int syserr, const char *fmt, ...) } /* - * Log errors and other messages to the system log daemon and to stderr, - * according to the severity of the message and the current debug level. - * For errors of severity LOG_ERR or worse, terminate the program. + * Log to the system log daemon, the level is checked by logit(). + * Messages to stderr go through prinl(). */ void logger(int severity, int syserr, const char *format, ...) { va_list ap; static char fmt[211] = "warning - "; char *msg; - struct timeval now; - time_t now_sec; - struct tm *thyme; va_start(ap, format); vsnprintf(&fmt[10], sizeof(fmt) - 10, format, ap); va_end(ap); msg = (severity == LOG_WARNING) ? fmt : &fmt[10]; - if (!logging) { - if (severity > loglevel) - return; - - /* Only OK use-case for unsafe gettimeofday(), logging. */ - gettimeofday(&now, NULL); - now_sec = now.tv_sec; - thyme = localtime(&now_sec); -// if (!debug) - fprintf(stderr, "%s: ", log_name); - fprintf(stderr, "%02d:%02d:%02d.%03ld %s", thyme->tm_hour, - thyme->tm_min, thyme->tm_sec, now.tv_usec / 1000, msg); - if (syserr == 0) - fprintf(stderr, "\n"); - else - fprintf(stderr, ": %s\n", strerror(syserr)); - - return; - } - if (syserr != 0) syslog(severity, "%s: %s", msg, strerror(syserr)); else diff --git a/src/log.h b/src/log.h index dbe33e4..5291e09 100644 --- a/src/log.h +++ b/src/log.h @@ -7,6 +7,8 @@ #include #define logit(pri, eno, fmt, arg...) do { \ + if (pri > loglevel) \ + break; \ if (logging) \ logger(pri, eno, fmt, ##arg); \ else \ @@ -22,6 +24,7 @@ #define dbg(fmt, arg...) logit(LOG_DEBUG, 0, fmt, ##arg) extern int logging; +extern int loglevel; extern void log_init (char *, int); extern int log_str2lvl (char *); diff --git a/src/main.c b/src/main.c index 623d8b9..0e36266 100644 --- a/src/main.c +++ b/src/main.c @@ -67,7 +67,7 @@ static int usage(int code) else snprintf(ipcbuf, sizeof(ipcbuf), _PATH_MCD_SOCK, ident); - printf("Usage: %s [-himnpsv] [-f FILE] [-i NAME] [-l LEVEL] [-p FILE] [-u FILE]\n" + printf("Usage: %s [-hnstv] [-f FILE] [-i NAME] [-l LEVEL] [-p FILE] [-u FILE]\n" "\n" " -f, --config=FILE Default filename derived from -i: %s\n" " -h, --help Show this help text\n" @@ -76,6 +76,7 @@ static int usage(int code) " -n, --foreground Run in foreground, do not detach from controlling terminal\n" " -p, --pidfile=FILE Default pidfile, derived from -i: %s\n" " -s, --syslog Log to syslog, default unless running in --foreground\n" + " -t, --check-config Check the configuration file and exit\n" " -u, --ipc=FILE UNIX domain socket, default from -i: %s\n" " -v, --version Show program version\n", prognm, config_file, nm, pidfn, sockfn); @@ -107,17 +108,19 @@ int main(int argc, char *argv[]) { "foreground", 0, 0, 'n' }, { "pidfile", 1, 0, 'p' }, { "syslog", 0, 0, 's' }, + { "check-config", 0, 0, 't' }, { "ipc", 1, 0, 'u' }, { "version", 0, 0, 'v' }, { NULL, 0, 0, 0 } }; int foreground = 0; + int check = 0; int ch; prognm = ident = progname(argv[0]); sock_file = getenv("MCD_SOCK"); - while ((ch = getopt_long(argc, argv, "f:hi:l:np:su:v", long_options, NULL)) != EOF) { + while ((ch = getopt_long(argc, argv, "f:hi:l:np:stu:v", long_options, NULL)) != EOF) { switch (ch) { case 'f': config_file = strdup(optarg); @@ -156,6 +159,10 @@ int main(int argc, char *argv[]) use_syslog++; break; + case 't': /* --check-config */ + check = 1; + break; + case 'u': sock_file = strdup(optarg); break; @@ -169,6 +176,17 @@ int main(int argc, char *argv[]) } } + log_init(ident, check ? 0 : use_syslog); + compose_paths(); + + if (check) { + if (config_init()) + return EX_NOINPUT; + + prinl(0, "config file %s is ok", config_file); /* whatever -l says */ + return 0; + } + if (geteuid() != 0) { errx("must be root"); exit(1); @@ -210,13 +228,8 @@ int main(int argc, char *argv[]) } else setlinebuf(stderr); - /* - * Setup logging - */ - log_init(ident, use_syslog); - dbg("%s starting", versionstring); - - compose_paths(); + dbg("%s starting, config %s", versionstring, config_file); + srand(time(NULL) ^ getpid()); pev_init(); sock_init(); diff --git a/src/mctl.c b/src/mctl.c index 2a90d12..e56167d 100644 --- a/src/mctl.c +++ b/src/mctl.c @@ -41,6 +41,10 @@ #define MIN(a,b) (((a) <= (b))? (a) : (b)) #endif +#ifndef NELEMS +#define NELEMS(a) (sizeof((a)) / sizeof((a)[0])) +#endif + #ifndef strlcat extern size_t strlcat(char *, const char *, size_t); #endif @@ -488,8 +492,10 @@ static int ipc_fetch(void) *desc++ = 0; opt = strchr(cmd, '['); - if (opt) + if (opt) { *opt++ = 0; + opt[strcspn(opt, "]")] = 0; + } add_cmd(cmd, opt, desc); } @@ -573,6 +579,7 @@ static int usage(int rc) if (c->opt) { strlcat(buf, " [", sizeof(buf)); strlcat(buf, c->opt, sizeof(buf)); + strlcat(buf, "]", sizeof(buf)); } printf(" %-25s %s\n", buf, c->desc); @@ -587,6 +594,61 @@ static int version(void) return 0; } +/* + * Shell completion, mctl -C WORD...: the words typed so far, the last + * one partial. Prints the candidates for it, one per line, with the + * command's description after a tab when the word completes it. + */ +static int complete(int argc, char *argv[]) +{ + const char *seen[MAXARGS * 4]; + int n = argc ? argc - 1 : 0; /* position of the partial word */ + const char *cur = argc ? argv[n] : ""; + size_t len = strlen(cur); + int nseen = 0; + struct cmd *c; + + if (ipc_fetch()) + return 1; + + TAILQ_FOREACH(c, &cmds, link) { + const char *word; + int i; + + for (i = 0; i < n && i < c->argc; i++) { + if (!string_match(argv[i], c->argv[i])) + break; + } + if (i < n) + continue; + + if (n < c->argc) + word = c->argv[n]; + else if (n == c->argc && c->opt) + word = c->opt; + else + continue; + + if (strncasecmp(cur, word, len)) + continue; + + for (i = 0; i < nseen; i++) { + if (!strcmp(seen[i], word)) + break; + } + if (i < nseen || nseen >= (int)NELEMS(seen)) + continue; + seen[nseen++] = word; + + if (n + 1 == c->argc && c->desc) + printf("%s\t%s\n", word, c->desc); + else + puts(word); + } + + return 0; +} + static int cmd(int argc, char *argv[]) { char buf[768]; @@ -624,6 +686,7 @@ static int cmd(int argc, char *argv[]) int main(int argc, char *argv[]) { struct option long_options[] = { + { "complete", 0, NULL, 'C' }, { "debug", 0, NULL, 'd' }, { "help", 0, NULL, 'h' }, { "ident", 1, NULL, 'i' }, @@ -635,12 +698,17 @@ int main(int argc, char *argv[]) { NULL, 0, NULL, 0 } }; int monitor = 0; + int compl = 0; int c, rc; sock_file = getenv("MCD_SOCK"); - while ((c = getopt_long(argc, argv, "dh?i:mptu:v", long_options, NULL)) != EOF) { + while ((c = getopt_long(argc, argv, "Cdh?i:mptu:v", long_options, NULL)) != EOF) { switch(c) { + case 'C': /* --complete, for the shell completion scripts */ + compl = 1; + break; + case 'd': debug = 1; break; @@ -680,6 +748,9 @@ int main(int argc, char *argv[]) } } + if (compl) + return complete(argc - optind, &argv[optind]); + do { if (monitor) { time_t now; diff --git a/src/mld.c b/src/mld.c index 54581c2..d55f5c1 100644 --- a/src/mld.c +++ b/src/mld.c @@ -6,6 +6,7 @@ #include "defs.h" #include "inet.h" #include "mld.h" +#include "mrd.h" static const struct in6_addr mld_allnodes = { { { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 } } }; @@ -51,8 +52,8 @@ static unsigned int mld_mrc_time(uint16_t code) * One's complement sums are associative, so the two parts are summed * separately and folded together. */ -static uint16_t icmp6_cksum(const struct in6_addr *src, const struct in6_addr *dst, - const void *msg, size_t len) +uint16_t icmp6_cksum(const struct in6_addr *src, const struct in6_addr *dst, + const void *msg, size_t len) { struct { struct in6_addr src; @@ -83,6 +84,9 @@ static const char *mld_packet_kind(uint8_t type) case MLD_LISTENER_REPORT: return "v1 listener report"; case MLD_LISTENER_REDUCTION: return "listener done "; case MLDV2_LISTENER_REPORT: return "v2 listener report"; + case ICMP6_MRD_ADVERT: return "mrd advertisement "; + case ICMP6_MRD_ADVERT + 1: return "mrd solicitation "; + case ICMP6_MRD_ADVERT + 2: return "mrd termination "; default: return NULL; } } @@ -99,6 +103,7 @@ void accept_mld(struct ifi *ifi, int vid, uint8_t *buf, size_t len) const char *kind; size_t off, plen; uint8_t nxt; + int msg; if (len < sizeof(*ip6)) { info("received packet too short (%zu bytes) for IPv6 header", len); @@ -149,6 +154,15 @@ void accept_mld(struct ifi *ifi, int vid, uint8_t *buf, size_t len) dbg("RECV %s from %s ifi %-2d.%d to %s", kind, s1, ifi->ifi_index, vid, inet_ntop(AF_INET6, &ip6->ip6_dst, s2, sizeof(s2))); + msg = mrd_msg(ifi, mld->mld_type); + if (msg >= 0) { + inet_addr_t dst; + + inet_addr_set6(&dst, &ip6->ip6_dst); + mrd_accept(ifi, msg, &src, &dst, mld, plen); + return; + } + if (mld->mld_type == MLDV2_LISTENER_REPORT) { accept_v3_report(ifi, &src, (uint8_t *)mld, plen); return; @@ -183,6 +197,32 @@ void accept_mld(struct ifi *ifi, int vid, uint8_t *buf, size_t len) } } +/* + * IPv6 header and the hop-by-hop header with the Router Alert option, + * RFC 2711, for an ICMPv6 message of datalen bytes. + */ +size_t build_ipv6(uint8_t *buf, const struct in6_addr *src, const struct in6_addr *dst, size_t datalen) +{ + struct ip6_hdr *ip6 = (struct ip6_hdr *)buf; + struct mld_hbh *hbh = (struct mld_hbh *)(ip6 + 1); + + memset(hbh, 0, sizeof(*hbh)); + hbh->hdr.ip6h_nxt = IPPROTO_ICMPV6; + hbh->ra.ip6or_type = IP6OPT_ROUTER_ALERT; + hbh->ra.ip6or_len = sizeof(hbh->ra.ip6or_value); + hbh->pad[0] = IP6OPT_PADN; + + memset(ip6, 0, sizeof(*ip6)); + ip6->ip6_flow = htonl(6U << 28); + ip6->ip6_plen = htons(sizeof(*hbh) + datalen); + ip6->ip6_nxt = IPPROTO_HOPOPTS; + ip6->ip6_hlim = 1; + ip6->ip6_src = *src; + ip6->ip6_dst = *dst; + + return sizeof(*ip6) + sizeof(*hbh); +} + /* * Send a General Query, or a Group-Specific one when group is set, as * MLDv1 or MLDv2 depending on the interface compatibility mode. @@ -193,24 +233,15 @@ void send_mld_query(const struct ifi *ifi, int qri, const inet_addr_t *group) const struct in6_addr *src = inet_addr_get6(&ifi->ifi_inaddr); uint8_t dmac[ETH_ALEN] = { 0x33, 0x33 }; struct mldv2_query *query; - struct ip6_hdr *ip6; - struct mld_hbh *hbh; size_t len, qlen; memcpy(&dmac[2], &dst->s6_addr[12], 4); - len = build_ether(send_buf, ifi, dmac, ETH_P_IPV6); + qlen = ifi->ifi_compat == GEN_V2 ? sizeof(struct mld_hdr) : sizeof(*query); - ip6 = (struct ip6_hdr *)(send_buf + len); - hbh = (struct mld_hbh *)(ip6 + 1); - query = (struct mldv2_query *)(hbh + 1); + len = build_ether(send_buf, ifi, dmac, ETH_P_IPV6); + len += build_ipv6(send_buf + len, src, dst, qlen); + query = (struct mldv2_query *)(send_buf + len); - memset(hbh, 0, sizeof(*hbh)); - hbh->hdr.ip6h_nxt = IPPROTO_ICMPV6; - hbh->ra.ip6or_type = IP6OPT_ROUTER_ALERT; - hbh->ra.ip6or_len = sizeof(hbh->ra.ip6or_value); - hbh->pad[0] = IP6OPT_PADN; - - qlen = ifi->ifi_compat == GEN_V2 ? sizeof(struct mld_hdr) : sizeof(*query); memset(query, 0, qlen); query->type = MLD_LISTENER_QUERY; if (group) @@ -224,15 +255,7 @@ void send_mld_query(const struct ifi *ifi, int qri, const inet_addr_t *group) query->qqic = igmp_floating_point(ifi->ifi_query_interval); } query->csum = icmp6_cksum(src, dst, query, qlen); - - memset(ip6, 0, sizeof(*ip6)); - ip6->ip6_flow = htonl(6U << 28); - ip6->ip6_plen = htons(sizeof(*hbh) + qlen); - ip6->ip6_nxt = IPPROTO_HOPOPTS; - ip6->ip6_hlim = 1; - ip6->ip6_src = *src; - ip6->ip6_dst = *dst; - len += sizeof(*ip6) + sizeof(*hbh) + qlen; + len += qlen; inet_ntop(AF_INET6, src, s1, sizeof(s1)); inet_ntop(AF_INET6, dst, s2, sizeof(s2)); diff --git a/src/mld.h b/src/mld.h index 6126990..9673567 100644 --- a/src/mld.h +++ b/src/mld.h @@ -20,6 +20,8 @@ struct mldv2_query { struct in6_addr srcs[0]; }; +extern uint16_t icmp6_cksum(const struct in6_addr *, const struct in6_addr *, const void *, size_t); +extern size_t build_ipv6(uint8_t *, const struct in6_addr *, const struct in6_addr *, size_t); extern void accept_mld(struct ifi *, int, uint8_t *, size_t); extern void send_mld_query(const struct ifi *, int, const inet_addr_t *); diff --git a/src/mrd.c b/src/mrd.c new file mode 100644 index 0000000..ef8b832 --- /dev/null +++ b/src/mrd.c @@ -0,0 +1,270 @@ +/* SPDX-License-Identifier: ISC */ + +#include +#include +#include + +#include "defs.h" +#include "igmp.h" +#include "inet.h" +#include "mld.h" +#include "mrd.h" + +/* + * Advertisement. A solicitation or termination is the first 4 bytes + * but sent at this size, the Linux bridge drops IGMP shorter than its + * 8 byte header. + */ +struct mrd_msg { + uint8_t type; + uint8_t interval; /* AdvertisementInterval, sec */ + uint16_t csum; + uint16_t qqi; /* Querier's Query Interval, sec */ + uint16_t qrv; /* Robustness Variable, 0 for IGMPv1 */ +}; + +static const char *mrd_msg_name[] = { "advertisement", "solicitation", "termination" }; + +static const struct in6_addr mrd6_allsnoopers = { { { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x6a } } }; +static const struct in6_addr mrd6_allrouters = { { { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x02 } } }; + +/* base plus a random part below spread, microseconds */ +static int jitter(int base, int spread) +{ + return base + rand() % spread; +} + + +/* The first MRD type of the family, the messages follow in order */ +static int mrd_base(const struct ifi *ifi) +{ + return ifi->ifi_family == AF_INET6 ? ICMP6_MRD_ADVERT : IGMP_MRD_ADVERT; +} + +/* MRD_ADVERT, MRD_SOLICIT or MRD_TERM, -1 for any other type */ +int mrd_msg(const struct ifi *ifi, int type) +{ + int msg = type - mrd_base(ifi); + + return msg >= MRD_ADVERT && msg <= MRD_TERM ? msg : -1; +} + +static void send_mrd(const struct ifi *ifi, int msg) +{ + uint8_t dmac[ETH_ALEN] = { 0x33, 0x33 }; + struct mrd_msg body = { 0 }, *m; + inet_addr_t dst; + size_t len; + + /* Proxy mode, no address to advertise */ + if (inet_addr_is_any(&ifi->ifi_inaddr)) + return; + + body.type = mrd_base(ifi) + msg; + if (msg == MRD_ADVERT) { + body.interval = mrd_interval; + body.qqi = htons(ifi->ifi_query_interval); + body.qrv = htons(ifi->ifi_compat == GEN_V1 ? 0 : igmp_robustness); + } + + if (ifi->ifi_family == AF_INET6) { + const struct in6_addr *src = inet_addr_get6(&ifi->ifi_inaddr); + + inet_addr_set6(&dst, &mrd6_allsnoopers); + memcpy(&dmac[2], &mrd6_allsnoopers.s6_addr[12], 4); + len = build_ether(send_buf, ifi, dmac, ETH_P_IPV6); + len += build_ipv6(send_buf + len, src, &mrd6_allsnoopers, sizeof(body)); + m = memcpy(send_buf + len, &body, sizeof(body)); + m->csum = icmp6_cksum(src, &mrd6_allsnoopers, m, sizeof(body)); + } else { + uint32_t src = inet_addr_get4(&ifi->ifi_inaddr); + uint32_t grp = htonl(INADDR_ALLSNOOPERS_GROUP); + + inet_addr_set4(&dst, grp); + ETHER_MAP_IP_MULTICAST(&grp, dmac); + len = build_ether(send_buf, ifi, dmac, ETH_P_IP); + len += build_ipv4(send_buf + len, src, grp, sizeof(body)); + m = memcpy(send_buf + len, &body, sizeof(body)); + m->csum = inet_cksum((uint16_t *)m, sizeof(body)); + } + len += sizeof(body); + + inet_addr_fmt(&ifi->ifi_inaddr, s1, sizeof(s1)); + inet_addr_fmt(&dst, s2, sizeof(s2)); + + if (sock_send(ifi, len) < 0) { + warn("sendto %s from %s on %s", s2, s1, ifi->ifi_name); + return; + } + + dbg("SENT MRD %s from %s to %s on %s", mrd_msg_name[msg], s1, s2, ifi->ifi_name); +} + +/* + * Advertise, then the next one: the initial ones within + * MaxInitialAdvertisementInterval, after that every + * AdvertisementInterval with jitter, RFC 4286 section 3.4. + */ +static void mrd_advert_cb(int timeout, void *arg) +{ + struct ifi *ifi = (struct ifi *)arg; + int next; + + ifi->ifi_mrd_pending = 0; + send_mrd(ifi, MRD_ADVERT); + + if (++ifi->ifi_mrd_init < MRD_MAX_INITIAL_ADVERTS) + next = jitter(1, MRD_MAX_INITIAL_INTERVAL * 1000000); + else + next = jitter(mrd_interval * 1000000 - MRD_ADV_JITTER, 2 * MRD_ADV_JITTER); + + pev_timer_set(ifi->ifi_mrd_timerid, next); +} + +void mrd_start(struct ifi *ifi) +{ + if (!(ifi->ifi_flags & IFIF_MRD)) + return; + + if (ifi->ifi_mrd_timerid > 0) + pev_timer_del(ifi->ifi_mrd_timerid); + + ifi->ifi_mrd_init = 0; + ifi->ifi_mrd_pending = 1; + ifi->ifi_mrd_timerid = pev_timer_add(jitter(1, MRD_MAX_INITIAL_INTERVAL * 1000000), 0, + mrd_advert_cb, ifi); +} + +/* Called before the interface socket closes, so the termination gets out */ +void mrd_stop(struct ifi *ifi) +{ + if (ifi->ifi_mrd_timerid > 0) { + ifi->ifi_mrd_timerid = pev_timer_del(ifi->ifi_mrd_timerid); + send_mrd(ifi, MRD_TERM); + } + ifi->ifi_mrd_pending = 0; +} + +static void router_del(struct ifi *ifi, struct mrd_router *r) +{ + TAILQ_REMOVE(&ifi->ifi_mrd_routers, r, link); + free(r); +} + +/* Forget the routers heard, the interface stops */ +void mrd_flush(struct ifi *ifi) +{ + struct mrd_router *r; + + while ((r = TAILQ_FIRST(&ifi->ifi_mrd_routers))) + router_del(ifi, r); +} + +/* + * Add or refresh a router from its advertisement. One pass drops the + * ones not heard from in time, finds this one and counts the rest. + * It is dead after 3 * (interval + 2.5 % jitter), RFC 4286 section 3.5. + */ +static void router_seen(struct ifi *ifi, const inet_addr_t *src, const void *buf, size_t len) +{ + const struct mrd_msg *m = buf; + struct mrd_router *r, *tmp, *found = NULL; + time_t now = time(NULL); + int n = 0, dead; + + if (len < sizeof(*m) || !m->interval) + return; + + TAILQ_FOREACH_SAFE(r, &ifi->ifi_mrd_routers, link, tmp) { + if (r->expires <= now) { + dbg("MRD router %s on %s timed out", inet_addr_fmt(&r->addr, s2, sizeof(s2)), ifi->ifi_name); + router_del(ifi, r); + } else if (!inet_addr_cmp(&r->addr, src)) + found = r; + else + n++; + } + + r = found; + if (!r) { + if (n >= MRD_MAX_ROUTERS) + return; + + r = calloc(1, sizeof(*r)); + if (!r) + return; + + r->addr = *src; + TAILQ_INSERT_TAIL(&ifi->ifi_mrd_routers, r, link); + info("MRD router %s on %s", s1, ifi->ifi_name); + } + + dead = 3 * m->interval; + r->interval = m->interval; + r->qqi = ntohs(m->qqi); + r->qrv = ntohs(m->qrv); + r->expires = now + dead + (dead + 39) / 40; +} + +static void router_gone(struct ifi *ifi, const inet_addr_t *src) +{ + struct mrd_router *r; + + TAILQ_FOREACH(r, &ifi->ifi_mrd_routers, link) { + if (inet_addr_cmp(&r->addr, src)) + continue; + + info("MRD router %s on %s terminated", s1, ifi->ifi_name); + router_del(ifi, r); + return; + } +} + +/* + * Checksum and length are already validated, s1 holds the source. A + * solicitation is answered within MaxResponseDelay, unless an + * advertisement is already on its way, RFC 4286 section 3.4. + */ +void mrd_accept(struct ifi *ifi, int msg, const inet_addr_t *src, const inet_addr_t *dst, + const void *buf, size_t len) +{ + inet_addr_t expect; + + if (ifi->ifi_family == AF_INET6) { + if (!inet_addr_linklocal(src)) + return; + inet_addr_set6(&expect, msg == MRD_SOLICIT ? &mrd6_allrouters : &mrd6_allsnoopers); + } else + inet_addr_set4(&expect, msg == MRD_SOLICIT ? allrtrs_group : htonl(INADDR_ALLSNOOPERS_GROUP)); + + if (inet_addr_cmp(dst, &expect)) + return; + + switch (msg) { + case MRD_ADVERT: + if (!inet_addr_cmp(src, &ifi->ifi_inaddr)) + return; /* our own */ + router_seen(ifi, src, buf, len); + break; + + case MRD_TERM: + router_gone(ifi, src); + break; + + case MRD_SOLICIT: + if (ifi->ifi_mrd_timerid <= 0 || ifi->ifi_mrd_pending) + return; + + dbg("MRD solicitation from %s on %s, advertising", s1, ifi->ifi_name); + ifi->ifi_mrd_pending = 1; + pev_timer_set(ifi->ifi_mrd_timerid, jitter(1, MRD_MAX_RESPONSE_DELAY * 1000000)); + break; + } +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "cc-mode" + * End: + */ diff --git a/src/mrd.h b/src/mrd.h new file mode 100644 index 0000000..6edb8c1 --- /dev/null +++ b/src/mrd.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: ISC */ + +#ifndef MCD_MRD_H_ +#define MCD_MRD_H_ + +/* Multicast Router Discovery, RFC 4286 */ +#define IGMP_MRD_ADVERT 0x30 +#define ICMP6_MRD_ADVERT 151 + +enum { MRD_ADVERT, MRD_SOLICIT, MRD_TERM }; + +#define MRD_ADV_INTERVAL 20 /* AdvertisementInterval, sec */ +#define MRD_ADV_JITTER (mrd_interval * 25000) /* 2.5 %, us */ +#define MRD_MAX_INITIAL_INTERVAL 2 /* MaxInitialAdvertisementInterval */ +#define MRD_MAX_INITIAL_ADVERTS 3 /* MaxInitialAdvertisements */ +#define MRD_MAX_RESPONSE_DELAY 2 /* Reply to a solicitation within */ +#define MRD_MAX_ROUTERS 32 /* Routers kept per interface */ + +extern int mrd_msg(const struct ifi *, int); +extern void mrd_start(struct ifi *); +extern void mrd_stop(struct ifi *); +extern void mrd_flush(struct ifi *); +extern void mrd_accept(struct ifi *, int, const inet_addr_t *, const inet_addr_t *, + const void *, size_t); + +#endif /* MCD_MRD_H_ */ diff --git a/src/netlink.c b/src/netlink.c index 7d79e75..e3f3472 100644 --- a/src/netlink.c +++ b/src/netlink.c @@ -1,15 +1,132 @@ /* SPDX-License-Identifier: ISC */ -#include -#include +#include "defs.h" + #include #include +#include #include -#include "defs.h" static int id; static int sd; +/* Bridge ports seen, to tell a change of state from a repeat */ +struct port { + TAILQ_ENTRY(port) link; + int ifindex; + int state; +}; + +static TAILQ_HEAD(, port) ports = TAILQ_HEAD_INITIALIZER(ports); + +struct link { + int lower; /* IFLA_LINK, the interface below a vlan */ + int master; /* IFLA_MASTER, the bridge of a port */ + int state; /* IFLA_BRPORT_STATE, in AF_BRIDGE messages */ +}; + +static void link_attrs(struct nlmsghdr *nlh, struct link *l) +{ + struct ifinfomsg *ifi = NLMSG_DATA(nlh); + struct rtattr *rta = IFLA_RTA(ifi); + int len = IFLA_PAYLOAD(nlh); + + for (; RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) { + switch (rta->rta_type & NLA_TYPE_MASK) { + case IFLA_LINK: + l->lower = *(int *)RTA_DATA(rta); + break; + + case IFLA_MASTER: + l->master = *(int *)RTA_DATA(rta); + break; + + case IFLA_PROTINFO: + if (ifi->ifi_family == AF_BRIDGE) { + struct rtattr *sub = RTA_DATA(rta); + int sublen = RTA_PAYLOAD(rta); + + for (; RTA_OK(sub, sublen); sub = RTA_NEXT(sub, sublen)) { + if (sub->rta_type == IFLA_BRPORT_STATE) + l->state = *(uint8_t *)RTA_DATA(sub); + } + } + break; + } + } +} + +static struct port *port_find(int ifindex) +{ + struct port *p; + + TAILQ_FOREACH(p, &ports, link) { + if (p->ifindex == ifindex) + return p; + } + + return NULL; +} + +static void port_del(int ifindex) +{ + struct port *p = port_find(ifindex); + + if (p) { + TAILQ_REMOVE(&ports, p, link); + free(p); + } +} + +/* + * Remember the state of a bridge port, report when it starts + * forwarding: link up, STP settled, or port added to the bridge. + */ +static void port_state(int ifindex, int master, int state) +{ + struct port *p = port_find(ifindex); + + if (!p) { + p = calloc(1, sizeof(*p)); + if (!p) + return; + + p->ifindex = ifindex; + p->state = -1; + TAILQ_INSERT_TAIL(&ports, p, link); + } + + if (state == p->state) + return; + p->state = state; + + if (state == BR_STATE_FORWARDING) + iface_topology_change(master, ifindex); +} + +static void link_msg(struct nlmsghdr *nlh) +{ + struct ifinfomsg *ifi = NLMSG_DATA(nlh); + struct link l = { .state = -1 }; + + if (nlh->nlmsg_type == RTM_DELLINK) { + port_del(ifi->ifi_index); + iface_del(ifi->ifi_index, ifi->ifi_flags); + return; + } + + if (ifi->ifi_family == AF_BRIDGE) { + link_attrs(nlh, &l); + if (l.master && l.state >= 0) + port_state(ifi->ifi_index, l.master, l.state); + return; + } + + if (!config_find_ifindex(ifi->ifi_index)) + iface_add(ifi->ifi_index, ifi->ifi_flags); + else + iface_check(ifi->ifi_index, ifi->ifi_flags); +} static void netlink_read(int sd, void *arg) { @@ -51,23 +168,89 @@ static void netlink_read(int sd, void *arg) } } - if (nlh->nlmsg_type == RTM_NEWLINK || nlh->nlmsg_type == RTM_DELLINK) { - struct ifinfomsg *ifi = (struct ifinfomsg *)NLMSG_DATA(nlh); - - if (nlh->nlmsg_type == RTM_NEWLINK) { - if (!config_find_ifindex(ifi->ifi_index)) - iface_add(ifi->ifi_index, ifi->ifi_flags); - else - iface_check(ifi->ifi_index, ifi->ifi_flags); - } else - iface_del(ifi->ifi_index, ifi->ifi_flags); - } + if (nlh->nlmsg_type == RTM_NEWLINK || nlh->nlmsg_type == RTM_DELLINK) + link_msg(nlh); nlh = NLMSG_NEXT(nlh, len); } } } +/* + * One RTM_GETLINK request on a socket of its own, cb for each reply. + * A dump (NLM_F_DUMP) runs to NLMSG_DONE, a lookup is one message. + */ +static int link_request(int family, int ifindex, int flags, void (*cb)(struct nlmsghdr *, void *), void *arg) +{ + struct { + struct nlmsghdr nlh; + struct ifinfomsg ifi; + } req = { 0 }; + char buf[16384]; + int done = 0; + int rsd; + + rsd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, NETLINK_ROUTE); + if (rsd == -1) + return -1; + + req.nlh.nlmsg_len = NLMSG_LENGTH(sizeof(req.ifi)); + req.nlh.nlmsg_type = RTM_GETLINK; + req.nlh.nlmsg_flags = NLM_F_REQUEST | flags; + req.ifi.ifi_family = family; + req.ifi.ifi_index = ifindex; + + if (send(rsd, &req, req.nlh.nlmsg_len, 0) == -1) { + close(rsd); + return -1; + } + + while (!done) { + ssize_t len = recv(rsd, buf, sizeof(buf), 0); + struct nlmsghdr *nlh = (struct nlmsghdr *)buf; + + if (len <= 0) + break; + + for (; NLMSG_OK(nlh, len); nlh = NLMSG_NEXT(nlh, len)) { + if (nlh->nlmsg_type == NLMSG_DONE || nlh->nlmsg_type == NLMSG_ERROR) { + done = 1; + break; + } + + cb(nlh, arg); + if (!(flags & NLM_F_DUMP)) + done = 1; + } + } + + close(rsd); + return 0; +} + +static void lower_cb(struct nlmsghdr *nlh, void *arg) +{ + struct link l = { 0 }; + + link_attrs(nlh, &l); + *(int *)arg = l.lower; +} + +/* The interface below a vlan interface, or 0 */ +int netlink_link_lower(int ifindex) +{ + int lower = 0; + + link_request(AF_UNSPEC, ifindex, 0, lower_cb, &lower); + + return lower; +} + +static void port_cb(struct nlmsghdr *nlh, void *arg) +{ + link_msg(nlh); +} + void netlink_init(void) { struct sockaddr_nl addr = { 0 }; @@ -91,10 +274,24 @@ void netlink_init(void) err("failed registering NETLINK handler"); exit(EX_OSERR); } + + /* + * Current state of all bridge ports, so only a change is one. + * Runs before iface_init(), no interface is querier yet, so + * this sends nothing. + */ + link_request(AF_BRIDGE, 0, NLM_F_DUMP, port_cb, NULL); } void netlink_exit(void) { + struct port *p, *tmp; + + TAILQ_FOREACH_SAFE(p, &ports, link, tmp) { + TAILQ_REMOVE(&ports, p, link); + free(p); + } + pev_sock_del(id); close(sd); } diff --git a/test/Makefile.am b/test/Makefile.am index afad882..e81d453 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1,15 +1,21 @@ -EXTRA_DIST = lib.sh basic.sh sleepy.sh two.sh ipc.sh ipc-errors.sh ipc-disconnect.sh router-timeout.sh late.sh router-ports.sh static-group.sh mld.sh bridge-names.sh mctl-help.sh +EXTRA_DIST = lib.sh querier-takeover.sh group-timeout.sh config-check.sh completion.sh topology.sh mrd.sh mrd-bridge.sh mrd-routers.sh basic.sh sleepy.sh two.sh ipc.sh ipc-errors.sh ipc-disconnect.sh router-timeout.sh late.sh router-ports.sh static-group.sh mld.sh bridge-names.sh mctl-help.sh CLEANFILES = *~ *.trs *.log TEST_EXTENSIONS = .sh TESTS_ENVIRONMENT = unshare -mrun --map-auto -TESTS = sleepy.sh +check_PROGRAMS = solicit +solicit_SOURCES = solicit.c + +TESTS = config-check.sh +TESTS += sleepy.sh TESTS += basic.sh TESTS += ipc.sh TESTS += ipc-errors.sh TESTS += ipc-disconnect.sh TESTS += router-timeout.sh +TESTS += querier-takeover.sh +TESTS += group-timeout.sh TESTS += late.sh TESTS += two.sh TESTS += router-ports.sh @@ -17,3 +23,8 @@ TESTS += static-group.sh TESTS += mld.sh TESTS += bridge-names.sh TESTS += mctl-help.sh +TESTS += completion.sh +TESTS += topology.sh +TESTS += mrd.sh +TESTS += mrd-bridge.sh +TESTS += mrd-routers.sh diff --git a/test/completion.sh b/test/completion.sh new file mode 100755 index 0000000..ab4f9ab --- /dev/null +++ b/test/completion.sh @@ -0,0 +1,104 @@ +#!/bin/sh +# Verifies shell completion for mctl: mctl -C WORD... prints the +# candidates for the last, partial, word from the running mcd's +# command list, and the bash completion function builds on it + +# shellcheck source=/dev/null +. "$(dirname "$0")/lib.sh" + +print "Check deps ..." +check_dep socat +check_dep bash + +print "Creating world ..." +addif eth0 192.168.0.1/24 + +print "Creating config ..." +cat < "/tmp/$NM/config" +iface eth0 enable igmpv3 +EOF +cat "/tmp/$NM/config" + +print "Starting mcd ..." +SOCK=/tmp/$NM/sock +../src/mcd -f "/tmp/$NM/config" -p "/tmp/$NM/pid" -l debug -n -u "$SOCK" & +echo $! >> "/tmp/$NM/PIDs" +tenacious 10 test -S "$SOCK" + +PATH=$(pwd)/../src:$PATH +export PATH + +# Candidates as one sorted line +line() +{ + # shellcheck disable=SC2046 + echo $(cut -f1 | LC_ALL=C sort) +} + +# The bash function, driven without a terminal +bashcomp() +{ + bash -c ' + . ../completions/bash/mctl + COMP_WORDS=("$@"); COMP_CWORD=$(($# - 1)) + _mctl + printf "%s\n" "${COMPREPLY[@]}"' bash "$@" +} + +fail=0 +check() +{ + if [ "$2" = "$3" ]; then + echo "ok: $1" + else + echo "FAIL: $1: got '$3', expected '$2'" + fail=1 + fi +} + +# One row per line: description | words so far, the last one partial, +# '' for an empty one | expected candidates, sorted +print "Checking mctl -C ..." +while IFS='|' read -r desc words expect; do + eval "set -- $words" + check "$desc" "$expect" "$(mctl -u "$SOCK" -C "$@" 2>&1 | line)" +done <