├── .gitignore ├── GNUmakefile ├── LICENSE ├── LICENSE.LGPLv3 ├── NEWS.md ├── README.md ├── etc ├── NetworkManager │ └── dispatcher.d │ │ ├── 08-ipv6-prefix │ │ ├── 09-ddns │ │ ├── 90-transmission │ │ ├── 95-radvd-gen │ │ ├── 96-interface-action │ │ └── pre-down.d │ │ ├── 08-ipv6-prefix │ │ └── 96-interface-action ├── nmutils │ ├── ddns-functions │ ├── dispatcher_action │ ├── general-functions │ └── ipv6_utils.sh └── systemd │ └── system │ ├── ddns-onboot@.service │ └── ddns-onboot@.timer ├── examples ├── complex │ ├── ddns-eth0-from-eth0.conf │ ├── ddns-eth0.conf │ ├── ddns-eth1-from-eth0.conf │ ├── ddns-eth1.conf │ ├── general.conf │ ├── ifa50-mylogger-eth1.conf │ ├── ipv6-prefix-eth0.conf │ ├── ipv6-prefix-eth1-from-eth0.conf │ ├── ipv6-prefix-eth2-from-eth0.conf │ ├── radvd-gen.conf │ └── radvd.conf.templ └── simple │ ├── ifa99-rsyslog-eth0.conf │ ├── ipv6-prefix-eth0.conf │ └── radvd.conf.templ ├── meson.build ├── meson_options.txt ├── nmutils.spec ├── selinux ├── GNUmakefile ├── nmutils.fc └── nmutils.te └── test ├── Makefile ├── bin ├── dhclient-mock ├── dhcpcd-mock ├── dig-mock ├── dummy-mock ├── ip-mock ├── nmcli-mock ├── nsupdate-mock ├── radvd-trigger ├── rdisc6-mock ├── resolvconf-mock ├── resolvectl-mock ├── systemctl-mock └── systemd-run-mock ├── conf ├── 1-ip-mock-addrs ├── 1-radvd.conf.templ ├── 2-ip-mock-addrs ├── 2-radvd.conf.templ ├── 3-ip-mock-addrs ├── 3-radvd.conf.templ ├── 4-ip-mock-addrs ├── 4-radvd.conf ├── 4-radvd.conf.templ ├── common.conf ├── ddns-br1-from-wan0.conf ├── ddns-br1-from-wan1.conf ├── ddns-eth0-from-eth0.conf ├── ddns-eth0.conf ├── ddns-eth1.conf ├── dig-mock-names ├── general.conf ├── ip-mock-addrs ├── ip-mock-monitor ├── ip-mock-routes ├── ipv6-prefix-br0-from-wan0.conf ├── ipv6-prefix-br1-from-wan0.conf ├── ipv6-prefix-eth3-from-wan0.conf ├── ipv6-prefix-wan1.conf ├── ipv6-prefix-wan2.conf ├── nm-ddns-br1-from-xxx.conf ├── nm-ddns-eth0.conf ├── nmcli-mock-values ├── nmg_xtest ├── radvd-gen.conf ├── shtest_setup ├── test-ddns.conf ├── testweb-ddns.conf └── xtest_setup ├── ddns-test ├── expected ├── 1-radvd.conf ├── 2-radvd.conf ├── 3-radvd.conf └── 4-radvd.conf ├── general-test ├── ipv6-prefix-addr-test ├── ipv6-prefix-dhclient-test ├── ipv6-prefix-dhcpcd-test ├── ipv6-prefix-nm-test ├── nm-ddns-test └── radvd-test /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*.sw? 3 | *.bak 4 | *.tgz 5 | *.tar.gz 6 | *.rpm 7 | /test/run/ 8 | /test/results/ 9 | /selinux/nmutils.if 10 | /selinux/nmutils.pp* 11 | /selinux/tmp/ 12 | /build/ 13 | -------------------------------------------------------------------------------- /GNUmakefile: -------------------------------------------------------------------------------- 1 | # 2 | # GNU Makefile to: 3 | # - create 'dist' source tarball 4 | # - install/uninstall nmutils (requires meson/ninja) 5 | # - create patched 'tarball' for direct install 6 | # - create SRPM (requires rpmbuild) 7 | # - build RPM (requires mock) 8 | # 9 | # 'make help' for options 10 | # 11 | 12 | prefix := /usr 13 | 14 | SED := sed 15 | TAR := tar 16 | MESON := meson 17 | NINJA := ninja 18 | RPMBUILD := rpmbuild 19 | MOCK := mock 20 | 21 | TMPDIR := ./tmp 22 | 23 | V := 0 24 | VB_0 := @ 25 | VB := $(VB_$(V)) 26 | 27 | # official version in meson.build 28 | VERSION := $(shell $(SED) -E -n "/^.*[[:space:],]*version[[:space:]]*:[[:space:]]*'([^']+)'.*$$/{s//\\1/p;q;}" meson.build || :) 29 | ifeq ($(VERSION),) 30 | $(error Unable to extract version from meson.build) 31 | endif 32 | 33 | PACKAGE := nmutils 34 | DISTDIR := $(PACKAGE) 35 | DISTNAME := $(PACKAGE)-$(VERSION) 36 | TARBALL := $(DISTNAME).tgz 37 | DIST := $(DISTNAME).tar.gz 38 | 39 | .SUFFIXES: 40 | 41 | .PHONY: Makefile all 42 | all: build 43 | 44 | .PHONY: help 45 | help: 46 | @echo "Usage: make [ =... ]" 47 | @echo " may be:" 48 | @echo " help - show this help" 49 | @echo " all (default) - configure (uses MESON_FLAGS)" 50 | @echo " install - install files (DESTDIR= honored)" 51 | @echo " uninstall - uninstall files (DESTDIR honored)" 52 | @echo " tarball - create patched $(TARBALL) (uses MESON_FLAGS) " 53 | @echo " dist - create $(DIST) with source files" 54 | @echo " srpm - create SRPM for building" 55 | @echo " rpm - build rpm using mock" 56 | @echo " check - run all tests" 57 | @echo 58 | @echo " may be (with defaults):" 59 | @echo " prefix=$(prefix)" 60 | @echo " MESON_FLAGS=" 61 | @echo " -Dpkg=false - =true for packaged install" 62 | @echo " -Dnmlibdir=/usr/lib - NetworkManager system libdir" 63 | @echo " -Drunstatedir=/run - runtime state dir" 64 | @echo " -Dselinuxtype=auto - SELinux type (auto-detected)" 65 | @echo " -Dunitdir=auto - systemd unit dir (auto-detected)" 66 | 67 | # order-only test for commands 68 | .SUFFIXES: .cmd 69 | %.cmd: 70 | $(VB)for cmd in $*; do \ 71 | type >/dev/null 2>/dev/null "$$cmd" || \ 72 | { echo "$$cmd not found (please install first)"; exit 1; } \ 73 | done 74 | 75 | build/build.ninja: | $(MESON).cmd $(NINJA).cmd 76 | @echo $(MESON) setup --prefix="$(prefix)" $(MESON_FLAGS) build 77 | $(VB)$(MESON) setup --prefix="$(prefix)" $(MESON_FLAGS) build || { \ 78 | rm -rf build; false; } 79 | 80 | .PHONY: build 81 | build: build/build.ninja 82 | $(MESON) compile -C build 83 | 84 | .PHONY: install 85 | install: build 86 | DESTDIR="$(DESTDIR)" $(MESON) install -C build 87 | 88 | .PHONY: uninstall 89 | uninstall: build 90 | cd build && DESTDIR="$(DESTDIR)" $(NINJA) uninstall 91 | $(VB)if command >/dev/null -v semodule; then \ 92 | echo semodule -r nmutils && semodule -r nmutils; \ 93 | else :; fi 94 | 95 | .PHONY: dist 96 | dist: clean 97 | $(VB)if [ -n "$$($(TAR) --version | grep GNU)" ]; then \ 98 | $(TAR) czf "$(DIST)" --exclude "$(DIST)" --transform "s/^[.]/$(DISTDIR)/S" ./* ./.gitignore; \ 99 | else \ 100 | $(TAR) czf "$(DIST)" --exclude "$(DIST)" -s "/^[.]/$(DISTDIR)/S" ./* ./.gitignore; \ 101 | fi 102 | @echo "Source tar created: $(DIST)" 103 | 104 | .PHONY: srpm 105 | srpm: DISTDIR=$(DISTNAME) 106 | srpm: dist | $(RPMBUILD).cmd 107 | $(VB)mkdir "$(TMPDIR)" && mv "$(DIST)" "$(TMPDIR)" 108 | $(VB)$(SED) -E -e 's/(Version:[[:space:]]+).*$$/\1$(VERSION)/'\ 109 | -e 's/(Name:[[:space:]]+).*$$/\1$(PACKAGE)/' \ 110 | nmutils.spec > "$(TMPDIR)/$(PACKAGE).spec" 111 | $(RPMBUILD) -D "_topdir $(TMPDIR)" -D "_sourcedir $(TMPDIR)"\ 112 | -D "_rpmdir $(TMPDIR)" -D "_specdir $(TMPDIR)"\ 113 | -D "_builddir $(TMPDIR)" -D "_srcrpmdir ." -D "srpm 1" \ 114 | -bs "$(TMPDIR)/$(PACKAGE).spec" 115 | $(VB)rm -rf "$(TMPDIR)" 116 | 117 | .PHONY: rpm 118 | rpm: srpm | $(MOCK).cmd 119 | $(MOCK) $(MOCK_FLAGS) $(PACKAGE)-$(VERSION)-*.src.rpm 120 | 121 | .PHONY: tarball 122 | tarball: DESTDIR=root 123 | tarball: clean install 124 | $(VB)$(TAR) czf $(TARBALL) -C build/root . 125 | @echo "Install tar created: $(TARBALL)" 126 | $(VB)rm -rf build/root 127 | 128 | .PHONY: check 129 | check: 130 | $(MAKE) -C test 131 | 132 | .PHONY: clean 133 | clean: 134 | -make -C selinux clean V=$(V) 135 | make -C test clean V=$(V) 136 | $(VB)rm -f *.tar.gz *.tgz *.src.rpm 137 | $(VB)rm -rf build "$(TMPDIR)" 138 | -------------------------------------------------------------------------------- /LICENSE.LGPLv3: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | Overview of changes in nmutils-20250418 2 | ======================================= 3 | 4 | - Added config command to 08-ipv6-prefix and 09-ddns with tests 5 | - general-functions 1.8.0 adds nmg::print_env, nmg::unset_env, 6 | nmg::get_config and nmg::load_comment 7 | - ddns-functions 1.6.0 adds nmddns_get_config and nmddns_get_globals 8 | - Added tests for new functions 9 | - `08-ipv6-prefix` now has config command 10 | - `09-ddns` now has help and config command 11 | 12 | Overview of changes in nmutils-20241216 13 | ======================================= 14 | 15 | - SELinux patch for dhclient pid file 16 | - Correct make dist 17 | 18 | Overview of changes in nmutils-20241215 19 | ======================================= 20 | 21 | ***BREAKING CHANGE:*** 22 | - package installs now look for config in /etc/nmutils by default 23 | (/etc/nmutils/conf still used for 'make install' installs) 24 | 25 | Other significant changes: 26 | - Rewrite of `08-ipv6-prefix` adding: 27 | - `dhcpcd` dhcp client support 28 | - adds RFC6603 prefix exclude support 29 | - supports RA DNS assignments 30 | - ipv6.method=ignore full ipv6 management 31 | - required for dhcpcd client support 32 | - `rdisc6` may be needed for default maintenance 33 | - `resolvectl` or `resolvconf` needed for DNS handling 34 | - many config changes may now be made with `nmcli device reapply` 35 | - dhcp client monitoring to recover from lockups/bugs in client 36 | - DNS server routes added 37 | - Source-based prefix routes supporting multiple WANs without 38 | firewall rules 39 | - Unreachable routes for unassigned prefix subnets 40 | - Configurable route metrics (DNS routes or SADR routes) 41 | - Gateway monitoring and active restoration 42 | - Subnet deprecation when gateway lost (after short timeout), WAN 43 | down, or via external trigger (see "deprecate" command) 44 | - DAD detection of generated addresses, and address retry 45 | - fallback dhclient-script support if requested 46 | - monitor and dhcp client daemons managed with systemctl 47 | - `08-ipv6-prefix` may now be run directly, commands include 48 | - `deprecate` for creating/releasing multiple independent 49 | prefix deprecation "locks" 50 | - `status` to display current WAN/LAN stats 51 | - `help` to display full documentation 52 | - extensive tests for all core features 53 | - Addition of `96-interface-action` to complement `dispatcher_action` 54 | - systemctl action independent of state-file creation/removal 55 | - is-enable may be ignored (great for startup after iface up) 56 | - actions enabled by config file presence (no creating dispatchers) 57 | with simpler config and additional features. 58 | - `meson` patch and install (supporting relocation). Package 59 | installation can be easily masked by non-package install for 60 | temporary overrides of source/config for testing/evaluation. 61 | - Makefile supporing: 62 | - `make help` to explain targets 63 | - `make install` and `make uninstall` 64 | - `make dist` for source tar 65 | - `make tarball` for patched install tar 66 | - `make srpm` and `make rpm` for package creation 67 | - Test suite expanded to include: 68 | - `nmcli-mock` supporting state modification (updates) 69 | - `dummy-mock` generalized to support most "fake binaries" via symlinks 70 | - `ip-mock` added support for link, route and monitor. Much better 71 | ipv6 prefix support 72 | - Many tests for added functionlity (now over 1400!) 73 | - new functions 74 | - `dhcpcd` client 75 | - ipv6.method=ignore 76 | - general-functions updated with many new functions 77 | - general hashing 78 | - ip monitor support for watching address/route changes 79 | - ipv6 prefix compression 80 | - ip route functions 81 | - many new tests 82 | - re-licensed LGPLv3 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Network Manager Utility Scripts 3 | ================================ 4 | [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) 5 | [![License: LGPL v3](https://img.shields.io/badge/License-LGPL_v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) 6 | 7 | A collection of BASH based utility scripts and support functions for 8 | use as [NetworkManager](https://networkmanager.dev/) dispatchers. 9 | 10 | - [Included Dispatch Scripts](#included-dispatch-scripts) 11 | - [IPv6 Prefix Delegation](#ipv6-prefix-delegation) 12 | - [Setup](#setup) 13 | - [Installation](#installation) 14 | - [Configuration](#configuration) 15 | - [Examples](#examples) 16 | - [Troubleshooting](#troubleshooting) 17 | - [Support Functions](#support-functions) 18 | - [Usage](#usage) 19 | - [Documentation](#documentation) 20 | - [SELinux](#selinux) 21 | - [Test Suite](#test-suite) 22 | - [Support](#support) 23 | - [License](#license) 24 | 25 | Included Dispatch Scripts 26 | ------------------------- 27 | 28 | - `08-ipv6-prefix` - **IPv6 Prefix Delegation**
29 | The dispatch script will spawn a DHCP client to request an IPv6 30 | prefix (as well as WAN address), and then assign sub-prefixes from 31 | the delegated prefix to the LAN interfaces. 32 | Simple configuration allows fully automatic behavior, 33 | but can be configured to fully control sub-prefix and address 34 | creation by interface. Also supports Dynamic DNS based on prefix 35 | assignment similar to the interface state DNS script below. 36 | 37 | - `09-ddns` - **Dynamic DNS**
38 | The dispatch script will use `nsupdate` to set or clear DNS entries 39 | based on interface state. Supports address assignment to A or AAAA 40 | records (with fallback values), and assignment of other records 41 | (such as CNAME or TXT) individually configurable for each interface. 42 | 43 | - `95-radvd-gen` - **radvd.conf Generation**
44 | The dispatch script will create `/etc/radvd.conf` based on the 45 | prefixes assigned to interfaces defined in the template 46 | `/etc/NetworkManager/radvd.conf.templ` 47 | 48 | - `96-interface-action` - **Interface action to Service action**
49 | Performs systemctl actions on designated services based on interface 50 | state changes (up, down, pre-down, dhcp-change etc). 51 | 52 | IPv6 Prefix Delegation 53 | ---------------------- 54 | 55 | The prefix delegation feature supports multiple address/prefix delegations 56 | from multiple upstream DHCP servers to multiple LAN interfaces. 57 | Features include 58 | 59 | - address, prefix and route lifetimes 60 | - DDNS update hooks 61 | - both `dhclient` or `dhcpcd` DHCP client support 62 | - flexible route metrics 63 | - DHCP client monitoring with restart on stalls/failures 64 | - DHCP client command options or config file overrides 65 | 66 | WAN (dhcp server) interface supports 67 | 68 | - duplicate address detection (with DHCP declines) 69 | - default route monitoring and maintenance using `rdisc6` or linux kernel 70 | - static address, DNS or domain search assignments 71 | - DNS server routes to source WAN (if needed) 72 | - Legacy dhclient-script as option 73 | 74 | LAN (delegated sub-prefixes) interfaces support 75 | 76 | - per-LAN subnet prefix-size and "subnet #" pre-allocation 77 | - radvd hooks 78 | - prefix delegation size hints 79 | - high-metric unreachable route for unassigned delegated prefixes 80 | - [RFC6603](https://datatracker.ietf.org/doc/html/rfc6603) DHCP Prefix 81 | Exclude support (requires `dhcpcd`) 82 | - [RFC8678](https://datatracker.ietf.org/doc/html/rfc8678) IPV6 83 | Multihoming Support: 84 | - source-based default routes created/maintained for each delegated 85 | prefix to it source WAN supporting multiple upstream WANs without 86 | policy-routing 87 | - NMG_RADVD_TRIGGER assigned to `95-radvd-gen` supports fast router 88 | deprecation when all routable WANs are offline 89 | - delegated subnets quickly deprecated using radvd when: 90 | - WAN down 91 | - WAN default route lost 92 | - any external trigger (via script command, see "help") 93 | 94 | There are 2 NetworkManager ipv6 connection methods supported 95 | 96 | 1. **ipv6.method: ignore** 97 | - supports `dhcpcd` or `dhclient` 98 | - uses `resolvectl` or `resolvconf` for DNS management 99 | - WAN addresses/routes have lifetimes 100 | - when using `dhcpcd` (which requires `rdisc6`): 101 | - supports DNS via Router Advertisements 102 | - supports prefix-exclude option 103 | 2. **ipv6.method: link-local** 104 | - addresses and DNS managed with NetworkManager, but require 105 | "device reapply" (done automatically) 106 | - requires `rdisc6` for default route management (unless never-default=yes) 107 | - not compatible with `dhcpcd` (or prefix-exclude) 108 | - WAN addresses/routes lack lifetimes (addresses cannot be deprecated) 109 | 110 | Setup 111 | ----- 112 | 113 | ### Installation 114 | 115 | Automated install requires GNU `make` and `meson`. There are two 116 | independent modes of installation: ***packaged*** or ***development***. Both 117 | modes can be installed simultaneously, but ***development*** installs 118 | completely mask ***packaged***; both have independent config directories. 119 | Type `make help` for a list of supported "targets" 120 | 121 | #### Prerequisites 122 | 123 | - [NetworkManager](https://networkmanager.dev/) 124 | - [bash](http://www.gnu.org/software/bash/) 125 | - required command-line tools: `pgrep` `ip` 126 | - optional: `logger` `radvd` `dig` `nsupdate` `flock` 127 | - required for `08-ipv6-prefix`: `nmcli` `systemd` `dhclient` or `dhcpcd` 128 | - optional for `08-ipv6-prefix`: `rdisc6` `resolvectl` `resolvconf` 129 | 130 | #### Development Installation 131 | 132 | ``` 133 | $ make 134 | $ sudo make install 135 | ``` 136 | - Installs to `/etc/nmutils` and `/etc/NetworkManager/dispatcher.d` 137 | - Configuration in `/etc/nmutils/conf` 138 | - Removal: `$ sudo make uninstall` 139 | 140 | #### Packaged Installation 141 | 142 | ``` 143 | $ make MESON_FLAGS='-Dpkg=true' 144 | $ sudo make install 145 | ``` 146 | - Installs to `/usr/share/nmutils` and `/usr/lib/NetworkManager/dispatcher.d` 147 | - Configuration in `/etc/nmutils` 148 | - Removal: `$ sudo make uninstall` 149 | 150 | Alternatively, if `rpmbuild` and `mock` are installed 151 | ``` 152 | $ make rpm 153 | ``` 154 | will create a source rpm (`make srpm`), then use mock to build rpms 155 | that can be installed on any rpm-based system. 156 | 157 | ### Configuration 158 | 159 | Configuration files should be placed in `/etc/nmutils/conf` 160 | (development) or `/etc/nmutils` (packaged) - henceforth referred to 161 | as "NMCONF" 162 | 163 | #### IPv6 Prefix Delegation Configuration 164 | 165 | All configuration for prefix delegation is documented in the file 166 | `etc/NetworkManager/dispatcher.d/08-ipv6-prefix`. Basic prefix 167 | delegation is enabled as simply setting ***ipv6.method*** to 168 | "link-local" or "ignore", and creating a configuration file for 169 | the WAN interface the prefix will be queried on 170 | 171 | `NMCONF/ipv6-prefix-.conf` 172 | ``` 173 | WAN_LAN_INTFS=" ..." 174 | ``` 175 | 176 | Where `` and `` should be replaced by the interface names (eth0 177 | etc). 178 | 179 | Optional per-LAN configuration can be set in the optional files, eg 180 | 181 | `NMCONF/ipv6-prefix--from-.conf` 182 | ``` 183 | # trigger the radvd.conf generation script 184 | NMG_RADVD_TRIGGER="/etc/NetworkManager/dispatcher.d/95-radvd-gen" 185 | ``` 186 | 187 | There are many additional configuraion options. The script may 188 | be run directly supporting a few additional features 189 | ``` 190 | # display full documentation, explaining all config options 191 | $ ./08-ipv6-prefix help 192 | 193 | # prints configuration of all interfaces (or s if given) 194 | # "-a" includes unset/empty configuration with defaults 195 | $ ./08-ipv6-prefix config [ -a ] [ ... ] 196 | 197 | # prints runtime status of all interfaces (or if given) 198 | $ ./08-ipv6-prefix status [ ] 199 | 200 | # manually deprecate with tag (w/o removes dep.) 201 | $ ./08-ipv6-prefix deprecate [ ] 202 | ``` 203 | 204 | More sample configurations can be found in the `examples` directory. 205 | 206 | #### Dynamic DNS Configuration 207 | 208 | DNS configuration is documented in `etc/nmutils/ddns-functions`. 209 | The `etc/NetworkManager/dispatcher.d/09-ddns` dispatcher script 210 | enables the DDNS features, eg 211 | 212 | `NMCONF/ddns-.conf` 213 | ``` 214 | DDNS_ZONE=example.net. 215 | DDNS_RREC_A_NAME=wan.example.net. 216 | ``` 217 | 218 | which would set the `A` record for wan.example.net to the public 219 | IPv4 addresses on `` when it's up, and remove the record when the 220 | interface is down. 221 | 222 | Again, there are many more configuration options in the documentation, 223 | including fallback addresses, setting TXT, CNAME, AAAA values, 224 | assignment of IPv6 prefix addresses assigned by `08-ipv6-prefix`, and 225 | configuration for locks and `nsupdate` keys. Configuration for 226 | DDNS addresses assigned by `08-ipv6-prefix` are placed in 227 | 228 | `NMCONF/ddns--from-.conf` - for WAN addresses 229 | 230 | `NMCONF/ddns--from-.conf` - for LAN delegated addresses from WAN 231 | 232 | Optionally, the systemd service files `ddns-onboot@.service` and 233 | `ddns-onboot@.timer` can be installed, and enabled with 234 | ``` 235 | $ systemctl daemon-reload 236 | $ systemctl enable ddns-onboot@.timer 237 | ``` 238 | to perform late boot DDNS setup that may not have been possible during 239 | system boot. 240 | 241 | There are many additional configuraion options. The script may 242 | be run directly supporting a few additional features 243 | ``` 244 | # display full documentation, explaining all config options 245 | $ ./09-ddns help 246 | 247 | # prints configuration of all interfaces (or s if given) 248 | # "-a" includes unset/empty configuration with defaults 249 | $ ./09-ddns config [ -a ] [ ... ] 250 | ``` 251 | 252 | ### Examples 253 | 254 | There are extensive configuration examples in the source under 255 | `examples/simple` and `examples/complex`. In addition, refer 256 | to the [Documentation](#documentation) to see complete descriptions of all 257 | the configuration options (there are **lots**). 258 | 259 | ### Troubleshooting 260 | 261 | By default, the scripts log minimal informational messages to syslog 262 | under the daemon facility (info and error). To track down problems, 263 | verbose debug messages may be enabled by adding the file 264 | `NMCONF/general.conf` containing 265 | ``` 266 | nmg_show_debug=1 267 | ``` 268 | 269 | Debug messages are logged as daemon.debug, so your syslog daemon 270 | should be configured to direct those messages someplace useful. 271 | Logging may be sent to stderr by setting `nmg_log_stderr=1` to 272 | ease tracing any problems. 273 | 274 | In addition, the `test` directory in the source includes "mock" 275 | programs that can be used to simulate many programs such as 276 | `nmcli`, `ip`, `dig` and `dhclient` and others (see the documentation 277 | in those scripts for details). NOTE: configuration in 278 | the `test` directory is located in `test/conf` 279 | 280 | Support Functions 281 | ----------------- 282 | 283 | The included support scripts `general-functions` and `ddns-functions` 284 | offer an extensive and well tested set of BASH functions which can be 285 | useful in creating additional dispatch scripts. They include: 286 | 287 | - Logging functions 288 | - Configuration (optionally required) file parsing 289 | - File creation/removal with error reporting 290 | - Command/daemon execution with output capture 291 | - Hex-decimal number conversion 292 | - IPv4/IPv6 address query and assignment with error handling 293 | - IPv6 network and host prefix creation and address creation 294 | - Full configuration of all paths, prefixes and configuration 295 | locations 296 | - Extensive debugging hooks to ease development with scripts, 297 | including stderr logging redirect and dry-run command execution. 298 | - nsupdate-based Dynamic DNS (optionally asynchronous) and serialized 299 | with locking 300 | 301 | ### Usage 302 | 303 | To use the support functions in your own dispatch scripts, just 304 | include them; for example add the following to the start of 305 | your script (set default $NMUTILS based on install location) 306 | 307 | ``` 308 | NMUTILS="${NMUTILS:-/etc/nmutils}" 309 | NMG="${NMG:-$NMUTILS/general-functions}" 310 | [ -f "$NMG" -a -r "$NMG" ] && . "$NMG" || { 311 | echo >&2 "Unable to load $NMG" && exit 2; } 312 | ``` 313 | 314 | Functions can be fully customized, either by setting file-local 315 | defaults before including them, or creating a global configuration 316 | file to set new defaults for all scripts (default: 317 | `NMCONF/general.conf`). All customization variables and 318 | their defaults are documented in the scripts. 319 | 320 | Documentation 321 | ------------- 322 | 323 | Each script fully documents all the functions it provides at the 324 | beginning of the script. To see the list of supported functions and 325 | configuration settings, please refer to the following files: 326 | 327 | - General utility functions: 328 | `etc/nmutils/general-functions` 329 | 330 | - Dynamic DNS utility functions: 331 | `etc/nmutils/ddns-functions` 332 | 333 | The following utility scripts only document configuration, as they 334 | are are just executed by NetworkManager: 335 | 336 | - IPv6 Prefix Delegation trigger script: 337 | `etc/NetworkManager/dispatcher.d/08-ipv6-prefix` 338 | 339 | - Dynamic DNS trigger script: 340 | `etc/NetworkManager/dispatcher.d/09-ddns` 341 | 342 | - radvd.conf generation script: 343 | `etc/NetworkManager/dispatcher.d/95-radvd-gen` 344 | 345 | - general action to service trigger script: 346 | `etc/NetworkManager/dispatcher.d/96-interface-action` 347 | 348 | - Transmission config generation script: 349 | `etc/NetworkManager/dispatcher.d/90-transmission` 350 | 351 | SELinux 352 | ------- 353 | 354 | A source module for SELinux is provided in the selinux directory. 355 | The module provides rules allowing `08-ipv6-prefix` to manage the 356 | dhcp client, manage radvd and perform DDNS functions. 357 | `sudo make -C selinux install` will build and install the module 358 | (`selinux-policy-devel` is required) 359 | 360 | Test Suite 361 | ---------- 362 | 363 | A complete test suite with over 1400 tests for all documented 364 | functions, and most of the utility scripts can be found in the 365 | `test` directory. To run all tests (with detailed reporting), 366 | simply run `make check`. 367 | 368 | Support 369 | ------- 370 | 371 | nmutils is hosted at 372 | [github.com/sshambar/nmutils](https://github.com/sshambar/nmutils). 373 | Please feel free to comment or send pull requests if you find 374 | bugs or want to contribute new features. 375 | 376 | I can be reached via email at: 377 | "Scott Shambarger" `` 378 | 379 | License 380 | ------- 381 | 382 | nmutils is licensed under the GPLv3 and LGPLv3 (for library scripts) 383 | -------------------------------------------------------------------------------- /etc/NetworkManager/dispatcher.d/09-ddns: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- 3 | # vim:set ft=sh et sw=2 ts=2: 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | # 6 | # Copyright (C) 2014-2025 Scott Shambarger 7 | # 8 | # 09-ddns v1.5.0 - NetworkManager dispatch for ipv4 Dynamic DNS updates 9 | # Author: Scott Shambarger 10 | # 11 | # Instructions for use: 12 | # 13 | # Put this script in /etc/NetworkManager/dispatcher.d (or wherever 14 | # your distro has these files) 15 | # 16 | # The settings are discussed in NMUTILS/ddns-functions. 17 | # 18 | # NOTE: By default, A and AAAA records use the global addresses 19 | # on an interface (see DDNS_RREC__PRIVATE to also consider 20 | # private addresses), so only set DDNS_RREC_A_VALUE or 21 | # DDNS_RREC_AAAA_VALUE if you wish to overrides those values 22 | # with static ones. 23 | # 24 | # Requires: 25 | # 26 | # NMUTILS/ddns-functions - dynamic DNS functions 27 | # NMUTILS/general-functions - shared functions 28 | # 29 | # External features: 30 | # 31 | # This script may be used directly for a few key features. The following 32 | # parameters may be passed to the script to perform these features. 33 | # 34 | # help - show these instructions and config variables docs in a pager 35 | # 36 | # config [ -a ] [ ... ] - show configuration (of ...) 37 | # If "-a" supplied, unset/empty variables with defaults are displayed. 38 | # 39 | # Config location: 40 | # 41 | # NMCONF/ddns-.conf (see $NMUTILS/ddns-functions for settings) 42 | # 43 | # State file: 44 | # 45 | # DDNS_STATE_DIR/ddns--.state 46 | # 47 | # shellcheck disable=SC1090,SC2153 48 | 49 | ########## SCRIPT START 50 | 51 | # for logging 52 | NMG_TAG=${NMG_TAG:-nmddns} 53 | [[ -z ${NM_DISPATCHER_ACTION-} && -z ${NMDDNSH_ACTION-} ]] && { 54 | # run from shell, log to stderr 55 | # shellcheck disable=SC2034 56 | [[ ${LOGNAME-} ]] && nmg_log_stderr=1 57 | [[ $1 =~ ^help|-h|config$ ]] && _NMG_IGNORE_PROGS=1 58 | } 59 | 60 | # set NMUTILS early, and allow environment to override 61 | NMUTILS=${NMUTILS:-/etc/nmutils} 62 | 63 | # shellcheck disable=SC2034 64 | NMDDNS_REQUIRED="1.6.0" 65 | 66 | # load ddns-functions 67 | NMDDNS=${NMDDNS:-${NMUTILS}/ddns-functions} 68 | { [[ -r ${NMDDNS} ]] && . "${NMDDNS}"; } || { 69 | echo 1>&2 "Unable to load ${NMDDNS} (set \$NMUTILS to it's dir)" && exit 2; } 70 | 71 | [[ ${NMDDNS_VERSION} ]] || { 72 | nmg_err "${0##*/} requires NMDDNS v${NMDDNS_REQUIRED}+"; exit 2; } 73 | 74 | ddns_nm_action() { 75 | # 76 | local interface=$1 action=$2 77 | local config=${NMDDNS_CONFIG_PAT/@MATCH@/${interface}} 78 | 79 | nmddns_read_config "${config}" || return 0 80 | 81 | # run from NM 82 | nmg_debug "interface: ${interface} action: ${action}" 83 | 84 | [[ -e ${DDNS_STATE_DIR} ]] || { 85 | [[ ${NM_DISPATCHER_ACTION} ]] || { 86 | nmg_err "STATE_DIR ${DDNS_STATE_DIR} missing; run from\ 87 | NetworkManager as dispatcher to create with correct permissions!" 88 | return 1 89 | } 90 | nmg_cmd mkdir -p "${DDNS_STATE_DIR}" || return 91 | } 92 | 93 | # use current ips on interface 94 | local addr="!${interface}" 95 | local state_pat=${NMDDNS_STATE_PAT/@MATCH@/${interface}} 96 | 97 | case ${action} in 98 | up|down) 99 | [[ ${action} == up ]] || addr='' 100 | nmddns_spawn_update_all "${action}" "${config}" "${addr}" "${addr}" \ 101 | "${state_pat}" 102 | ;; 103 | dhcp4-change) 104 | nmddns_spawn_update "${config}" "A" "${addr}" "${state_pat}" 105 | ;; 106 | dhcp6-change) 107 | nmddns_spawn_update "${config}" "AAAA" "${addr}" "${state_pat}" 108 | ;; 109 | esac 110 | 111 | return 0 112 | } 113 | 114 | ddns_helper_action() { 115 | local rc=0 116 | 117 | nmg_debug "ACTION: ${NMDDNSH_ACTION}" 118 | 119 | nmddns_required_config "${NMDDNSH_CONFIG-}" 120 | 121 | case ${NMDDNSH_ACTION} in 122 | update) 123 | nmddns_update "${NMDDNSH_RREC-}" "${NMDDNSH_VALUE-}" \ 124 | "${NMDDNSH_STATE-}" || rc=$? 125 | ;; 126 | up|down) 127 | nmddns_update_all "${NMDDNSH_ACTION}" "${NMDDNSH_ADDR4-}" \ 128 | "${NMDDNSH_ADDR6-}" "${NMDDNSH_STATE-}" || rc=$? 129 | ;; 130 | *) 131 | nmg_err "${0##*/} Unknown helper action ${NMDDNSH_ACTION}" 132 | rc=1 133 | ;; 134 | esac 135 | 136 | # ignore server unreachable 137 | (( rc == 25 )) && rc=0 138 | 139 | return ${rc} 140 | } 141 | 142 | function ddns_interface_rrec() { 143 | # 144 | local match=$1 rrec=$2 value='' rc=0 145 | 146 | # get state file pattern (used for per-config overrides) 147 | local state=${NMDDNS_STATE_PAT/@MATCH@-@RREC@/${match}-${rrec}} 148 | 149 | # get interface from match 150 | local intf=${match%-from-*} 151 | 152 | if [[ -e ${state} ]] && nmg::read value "" "${state}"; then 153 | 154 | # strip newline... 155 | value=${value%%$'\n'*} 156 | 157 | if [[ ${value} ]]; then 158 | # if ip-addresses, verify on interface 159 | if [[ ${rrec} =~ ^(A|AAAA)$ && ! ${value} =~ ^[!].+$ ]]; then 160 | # get values that are valid on , remove addrs if down 161 | local avail=() avals=() newvals=() tval 162 | 163 | # cleanup supplied values 164 | nmg::lowercase value "${value}" 165 | nmg::array avals "," "${value}" 166 | 167 | if [[ ${rrec} == A ]]; then 168 | nmddns::get_A_addrs avail "${intf}" \ 169 | "${DDNS_RREC_A_PRIVATE-}" || : 170 | else 171 | nmddns::get_AAAA_addrs avail "${intf}" \ 172 | "${DDNS_RREC_AAAA_PRIVATE-}" || : 173 | fi 174 | 175 | # find intersection with available addrs 176 | for value in ${avals[@]+"${avals[@]}"}; do 177 | # strip any /prefix 178 | value=${value%%/*} 179 | for tval in ${avail[@]+"${avail[@]}"}; do 180 | [[ ${tval} == "${value}" ]] && { newvals+=("${value}"); break; } 181 | done 182 | done 183 | nmg::array_join value "," "${newvals[@]-}" 184 | fi 185 | fi 186 | fi 187 | 188 | nmddns_update "${rrec}" "${value}" || rc=$? 189 | 190 | # ignore server unreachable 191 | (( rc == 25 )) && rc=0 192 | 193 | return ${rc} 194 | } 195 | 196 | ddns_rep() { Report+="$1"$'\n'; } 197 | 198 | ddns_config_intf() { # [ ] 199 | local file=$1 match=$2 intf=${3-} out 200 | match=${match%-from-*} 201 | [[ ${match} ]] || return 0 202 | shift 2 203 | [[ ${intf} && ${intf} != "${match}" ]] && return 0 204 | [[ ${match} != "${Intf}" ]] && { 205 | Intf=${match}; ddns_rep $'\n'"Interface ${Intf}" 206 | } 207 | if nmddns_read_config "${file}"; then 208 | # shellcheck disable=SC2059 209 | ddns_rep " DDNS config \"${file}\"" 210 | nmddns_get_config out "${Fmt} %s=%s\n" 211 | Report+="${out}" 212 | else 213 | out="not found" 214 | [[ -e ${file} ]] && out="invalid" 215 | ddns_rep " DDNS config \"${file}\" ($out)" 216 | fi 217 | nmddns_reset_config 218 | } 219 | 220 | ddns_config() { 221 | # [ -a ] [ ... ] 222 | local Report='' Fmt='' Intf='' out intf 223 | # shellcheck disable=SC2034 224 | nmg_show_debug='' 225 | 226 | [[ ${1-} == "-a" ]] && { shift; Fmt="%-"; } 227 | Report+="Global config \"${NMCONF}/general.conf\""$'\n' 228 | nmg::get_config "out" "${Fmt} %s=%s\n" 229 | Report+="${out}" 230 | nmddns_get_globals "out" "${Fmt} %s=%s\n" 231 | Report+="${out}" 232 | 233 | if [[ ${1-} ]]; then 234 | for intf in "$@"; do 235 | nmg::foreach_filematch "${NMDDNS_CONFIG_PAT}" "@MATCH@" \ 236 | ddns_config_intf "${intf}" 237 | [[ ${intf} == "${Intf}" ]] || 238 | ddns_rep $'\n'"Interface ${intf} not configured" 239 | done 240 | else 241 | nmg::foreach_filematch "${NMDDNS_CONFIG_PAT}" "@MATCH@" \ 242 | ddns_config_intf 243 | fi 244 | 245 | printf "%s" "${Report}" 246 | } 247 | 248 | ddns_help() { 249 | local out dout 250 | nmg::load_comment "${BASH_SOURCE[0]}" out 09-ddns "^# State file" || exit 251 | nmg::load_comment "${NMDDNS}" dout "^# Global Over" "NOTE:\\ exec" || exit 252 | out+=${dout/, see nmddns_read_config() above/} 253 | [[ ${PAGER} ]] || { 254 | for PAGER in less more cat; do 255 | command >/dev/null -v "${PAGER}" && break 256 | done 257 | } 258 | [[ ${PAGER} != cat ]] && { printf %s "${out}" | "${PAGER}"; return; } 259 | printf %s "${out}" 260 | } 261 | 262 | ddns_direct_cb() { 263 | # [ ] 264 | local config=$1 match=$2 intf=${3-} name rrec 265 | 266 | [[ ${match} ]] || return 0 267 | 268 | # if we have an interface, make sure we filter for it 269 | [[ ${intf} && ${intf} != "${match%-from-*}" ]] && return 0 270 | 271 | # load DDNS config 272 | nmddns_read_config "${config}" || return 0 273 | 274 | for name in "${!DDNS_RREC_@}"; do 275 | [[ ${name} =~ _NAME$ ]] || continue 276 | name=${name#DDNS_RREC_} 277 | rrec=${name%_NAME} 278 | [[ ${rrec} ]] || continue 279 | ddns_interface_rrec "${match}" "${rrec}" 280 | done 281 | 282 | return 0 283 | } 284 | 285 | ddns_command() { 286 | # [ | ] [ ] 287 | case $1 in 288 | config) 289 | shift 290 | ddns_config "$@" 291 | ;; 292 | help|-h) 293 | ddns_help 294 | ;; 295 | *) 296 | [[ ${1-} == direct ]] && shift 297 | nmg_debug "DIRECT: ${1-}" 298 | 299 | nmg::foreach_filematch "${NMDDNS_CONFIG_PAT}" "@MATCH@" \ 300 | ddns_direct_cb "$@" 301 | ;; 302 | esac 303 | } 304 | 305 | if [[ ${1-} && ${NM_DISPATCHER_ACTION-} ]]; then 306 | 307 | case ${NM_DISPATCHER_ACTION} in 308 | up|down|dhcp4-change|dhcp6-change) 309 | ddns_nm_action "$1" "${NM_DISPATCHER_ACTION}" || exit 310 | ;; 311 | esac 312 | elif [[ ${NMDDNSH_ACTION-} ]]; then 313 | 314 | # called from ddns-functions 315 | ddns_helper_action || exit 316 | 317 | elif [[ -z ${NM_DISPATCHER_ACTION-} ]]; then 318 | 319 | # called from boot script/command-line 320 | ddns_command "$@" 321 | fi 322 | : # for loading in tests 323 | -------------------------------------------------------------------------------- /etc/NetworkManager/dispatcher.d/90-transmission: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- 3 | # vim:set ft=sh et sw=2 ts=2: 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | # 6 | # Copyright (C) 2015-2022 Scott Shambarger 7 | # 8 | # 90-transmission v1.1.2 - Transmission dispatcher script 9 | # Author: Scott Shambarger 10 | # 11 | # This script sets transmissions bind address to the public IP 12 | # address of the desired interface (required until transmission supports 13 | # binding to devices). The script will update transmission's 14 | # configuration and then start transmission on interface up, 15 | # and stop transmission on interface down. 16 | # 17 | # Instructions for use: 18 | # 19 | # Put this script in /etc/NetworkManager/dispatcher.d (or wherever 20 | # your distro has these files) 21 | # 22 | # Requires: 23 | # 24 | # NMUTILS/general-functions - shared functions 25 | # 26 | # Global overrides (put in NMCONF/general.conf) 27 | # 28 | # TR_CONFIG (default: NMCONF/transmission-${interface}.conf}) - existance 29 | # of this file identifies the interface transmission should bind to. 30 | # 31 | # Config settings (put in TR_CONFIG) 32 | # 33 | # TR_STATE (default: /run/transmission-configured) - state file 34 | # created when transmission settings have been configured 35 | # 36 | # TR_HOME (default: transmission user home directory) 37 | # default transmission user's home dir (used for default TR_SETTINGS) 38 | # 39 | # TR_SETTINGS (default: "$TR_HOME/.config/transmission-daemon/settings.json") 40 | # default location of transmission settings file 41 | # 42 | # TR_UNIT (default: transmission-daemon) - default transmission 43 | # systemd service name 44 | # 45 | # TR_IGNORE_V4 (default: empty) - non-empty, ipv4 is ignored 46 | # 47 | # TR_PRIVATE_V4 (default: empty) - non-empty, allow private ipv4 addrs 48 | # 49 | # TR_IGNORE_V6 (default: empty) - non-empty, ipv6 is ignored 50 | # 51 | # TR_PRIVATE_V6 (default: empty) - non-empty, allow private ipv6 addrs 52 | # 53 | # State file: 54 | # 55 | # /run/transmission-configured 56 | # 57 | # shellcheck disable=SC1090 58 | 59 | interface=$1 60 | action=$2 61 | 62 | # for logging 63 | # shellcheck disable=SC2034 64 | NMG_TAG="trans-cfg" 65 | 66 | # set NMUTILS early, and allow environment to override 67 | NMUTILS=${NMUTILS:-/etc/nmutils} 68 | 69 | ########## State location 70 | 71 | TR_STATE="/run/transmission-configured" 72 | 73 | ########## Default paths 74 | 75 | # Default transmission user's home dir 76 | TR_HOME=~transmission 77 | # Default transmission settings file 78 | TR_SETTINGS="${TR_HOME}/.config/transmission-daemon/settings.json" 79 | # Default transmission service name 80 | TR_UNIT="transmission-daemon" 81 | # default: no private ip 82 | TR_PRIVATE_V4='' 83 | TR_PRIVATE_V6='' 84 | # default: don't ignore addrs 85 | TR_IGNORE_V4='' 86 | TR_IGNORE_V6='' 87 | 88 | ########## SCRIPT START 89 | 90 | # anything for us to do? 91 | [[ ${interface} && ${action} ]] || exit 0 92 | 93 | # load general-functions 94 | NMG=${NMG:-${NMUTILS}/general-functions} 95 | { [[ -r ${NMG} ]] && . "${NMG}"; } || { 96 | echo 1>&2 "Unable to load $NMG" && exit 2; } 97 | 98 | ########## Config location default (if unset) 99 | TR_CONFIG=${TR_CONFIG:-${NMCONF}/transmission-${interface}.conf} 100 | 101 | # see if we're configured for this interface 102 | nmg_read_config "${TR_CONFIG}" || exit 0 103 | 104 | # if no settings yet, bail 105 | [[ -w ${TR_SETTINGS} ]] || exit 0 106 | 107 | function get_addr4() { # 108 | 109 | local idx vname addr priv_ok='' 110 | 111 | [[ ${IP4_NUM_ADDRESSES:-0} == 0 ]] && return 0 112 | [[ ${TR_IGNORE_V4} ]] && return 0 113 | [[ ${TR_PRIVATE_V4} ]] && priv_ok=1 114 | 115 | vname=$1 116 | [[ ${!vname} ]] && return 0 117 | 118 | # choose first public address 119 | for (( idx=0; idx < IP4_NUM_ADDRESSES; idx++ )); do 120 | vname="IP4_ADDRESS_${idx}" 121 | addr=${!vname%%/*} 122 | # check addr (remove netmask and gateway) 123 | nmg_check_ip4_addr "${addr}" "${priv_ok}" || continue 124 | 125 | if nmg::query_ips "" "nolog" 4 "${interface}" "^${addr}" \ 126 | "scope global tentative"; then 127 | nmg_debug "Address ${addr} still tentative" 128 | elif nmg::query_ips "" "nolog" 4 "${interface}" "^${addr}" \ 129 | "scope global"; then 130 | nmg_debug "Selecting ${addr}" 131 | printf -v "$1" "%s" "${addr}" 132 | return 0 133 | fi 134 | done 135 | 136 | # still waiting... 137 | return 1 138 | } 139 | 140 | function get_addr6() { # 141 | 142 | local idx vname addr priv_ok='' 143 | 144 | [[ ${IP6_NUM_ADDRESSES:-0} == 0 ]] && return 0 145 | [[ ${TR_IGNORE_V6} ]] && return 0 146 | [[ ${TR_PRIVATE_V6} ]] && priv_ok=1 147 | 148 | # if addr selected... skip loop 149 | vname=$1 150 | [[ ${!vname} ]] && return 0 151 | 152 | # choose first allowed address 153 | for (( idx=0; idx < IP6_NUM_ADDRESSES; idx++ )); do 154 | vname="IP6_ADDRESS_${idx}" 155 | addr=${!vname%%/*} 156 | # check addr (remove netmask and gateway) 157 | nmg_check_ip6_addr "${addr}" "${priv_ok}" || continue 158 | 159 | if nmg::query_ips "" "nolog" 6 "${interface}" "^${addr}" \ 160 | "scope global tentative"; then 161 | nmg_debug "Address ${addr} still tentative" 162 | elif nmg::query_ips "" "nolog" 6 "${interface}" "^${addr}" \ 163 | "scope global"; then 164 | nmg_debug "Selecting ${addr}" 165 | printf -v "$1" "%s" "${addr}" 166 | return 0 167 | fi 168 | done 169 | 170 | # still waiting... 171 | return 1 172 | } 173 | 174 | function tr_stop() { 175 | 176 | # stop daemon (to write settings) 177 | /bin/systemctl stop "${TR_UNIT}" 178 | nmg_remove "${TR_STATE}" 179 | } 180 | 181 | function tr_start() { 182 | 183 | local addr4='' addr6='' change='' 184 | 185 | /bin/systemctl 2>/dev/null -q is-enabled "${TR_UNIT}" || return 0 186 | 187 | # loop over all addresses until one passes DAD, or 2.5sec timeout 188 | for (( cnt=0; cnt<=5; cnt++ )); do 189 | # get addresses 190 | [[ $cnt -ne 0 ]] && sleep 0.5 191 | get_addr4 "addr4" && get_addr6 "addr6" && break 192 | done 193 | 194 | [[ ${addr4} || ${addr6} ]] || return 0 195 | 196 | # check if any changes 197 | if [[ ${addr4} ]]; then 198 | grep -q "\"bind-address-ipv4\":[[:space:]]*\"${addr4}\"" "${TR_SETTINGS}" || change=1 199 | fi 200 | if [[ ${addr6} ]]; then 201 | grep -q "\"bind-address-ipv6\":[[:space:]]*\"${addr6}\"" "${TR_SETTINGS}" || change=1 202 | fi 203 | 204 | if [[ ${change} ]]; then 205 | 206 | nmg_debug "Updating settings and restarting daemon" 207 | 208 | # stop transmission so it doesn't overwrite setting changes 209 | tr_stop 210 | 211 | # edit file in place 212 | if [[ ${addr4} ]]; then 213 | sed -i "s|\(.*\"bind-address-ipv4\":\).*|\1 \"${addr4}\",|" "${TR_SETTINGS}" || return 0 214 | fi 215 | if [[ ${addr6} ]]; then 216 | sed -i "s|\(.*\"bind-address-ipv6\":\).*|\1 \"${addr6}\",|" "${TR_SETTINGS}" || return 0 217 | if grep -q "\"rpc-bind-address\":[[:space:]]*\"0.0.0.0\"" "${TR_SETTINGS}"; then 218 | # add rpc-bind-address=:: if 0.0.0.0 to enable ipv6 219 | sed -i "s|\(.*\"rpc-bind-address\":\).*|\1 \"::\",|" "${TR_SETTINGS}" || return 0 220 | fi 221 | fi 222 | fi 223 | 224 | nmg_write "${TR_STATE}" 225 | /bin/systemctl start "${TR_UNIT}" 226 | } 227 | 228 | nmg_debug "interface: ${interface} action: ${action}" 229 | 230 | case "${action}" in 231 | up|dhcp4-change|dhcp6-change) tr_start ;; 232 | down) tr_stop ;; 233 | esac 234 | -------------------------------------------------------------------------------- /etc/NetworkManager/dispatcher.d/96-interface-action: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- 3 | # vim:set ft=sh et sw=2 ts=2: 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | # 6 | # Copyright (C) 2024 Scott Shambarger 7 | # 8 | # 96-interface-action v1.0.0 - service restart on interface change 9 | # Author: Scott Shambarger 10 | # 11 | # Instructions for use: 12 | # 13 | # Put this script in /etc/NetworkManager/dispatcher.d (or wherever 14 | # your distro has these files). 15 | # 16 | # Create a configuration file NMCONF/ifa##--.conf 17 | # where ## is a 2-digit ordering number, is the target 18 | # systemd service, and is the interface to act upon. 19 | # 20 | # The following may be used below for : 21 | # 22 | # UP - on interface up 23 | # DOWN - on interface down 24 | # PRE_DOWN - before interface down (blocks interface down) 25 | # CHANGE - any DHCP IPv4 address change 26 | # CHANGE6 - any DHCP IPv6 address change 27 | # 28 | # Required: 29 | # 30 | # NMUTILS/general-functions - shared functions (see docs in file) 31 | # systemctl - used to manage 32 | # 33 | # Config options are: 34 | # 35 | # IGNORE_ENABLED - Any value causes any RESTART_* or "on" commands to 36 | # ignore whether service is enabled. 37 | # 38 | # CMD_= - contains the systemctl unit-command for 39 | # applied in response to interface . Additionally, the following 40 | # aliases may be used for : 41 | # 42 | # on - reload-or-restart 43 | # off - stop 44 | # noop - 45 | # 46 | # STATE_FILE= - file created containing value of RESTART_ 47 | # before service command, and removed if STOP_ has value 48 | # (may be used for conditional multi-interface service starts on boot). 49 | # 50 | # RESTART_= - contents of STATE_FILE created on . 51 | # Additionally, if CMD_ is unset/empty, the default service 52 | # command "on" is applied (use "noop" for no action) 53 | # 54 | # STOP_= - Any value causes STATE_FILE to be 55 | # removed on . Additionally, if CMD_ is unset/empty, 56 | # the default service command "off" is applied ("noop" for no action). 57 | # 58 | # shellcheck disable=SC1090,SC2317 59 | interface=${1-} 60 | action=${2-} 61 | 62 | # set NMUTILS early, and allow environment to override 63 | NMUTILS=${NMUTILS:-/etc/nmutils} 64 | SYSTEMCTL=${SYSTEMCTL:-systemctl} 65 | 66 | ########## SCRIPT START 67 | 68 | NMG_TAG=${NMG_TAG-nm-ifa} 69 | NMG_REQUIRED="1.7.0" 70 | 71 | # load general-functions 72 | NMG=${NMG:-${NMUTILS}/general-functions} 73 | { [[ -r ${NMG} ]] && . "${NMG}"; } || { 74 | echo 1>&2 "Unable to load $NMG" && exit 2; } 75 | 76 | [[ ${NMG_VERSION} ]] || { 77 | nmg_err "${0##*/} requires NMG ${NMG_REQUIRED}"; exit 2; } 78 | 79 | # IFA_CONFIG_PAT must include @ORDER@, @SERVICE@ and @INTERFACE@ 80 | IFA_CONFIG_PAT="${IFA_CONFIG_PAT:-${NMCONF}/ifa@ORDER@-@SERVICE@-@INTERFACE@.conf}" 81 | 82 | svc_action() { 83 | # 84 | local command=$1 restart=$2 stop=$3 type 85 | 86 | # create any restart file 87 | [[ ${STATE_FILE-} ]] && { 88 | if [[ ${restart} ]]; then 89 | nmg_write "${STATE_FILE}" "${restart}" 90 | elif [[ ${stop} ]]; then 91 | [[ -f ${STATE_FILE} ]] && nmg_remove "${STATE_FILE}" 92 | fi 93 | } 94 | 95 | [[ ${command} ]] || { 96 | # get default command 97 | if [[ ${restart} ]]; then 98 | command=on 99 | elif [[ ${stop} ]]; then 100 | command=off 101 | fi 102 | } 103 | 104 | # map aliases 105 | case ${command} in 106 | ''|noop) return 0 ;; 107 | on) command=reload-or-restart ;; 108 | off) command=stop ;; 109 | esac 110 | 111 | case ${command} in 112 | start|reload|restart|try-restart|reload-or-restart|try-reload-or-restart) 113 | if [[ ${IGNORE_ENABLED-} ]]; then 114 | nmg::saferun type "nolog" "${SYSTEMCTL}" show --property Type \ 115 | "${SVC_UNIT}" || : 116 | [[ ${type#Type=} ]] || return 0 117 | else 118 | nmg::saferun "" "nolog" "${SYSTEMCTL}" -q is-enabled "${SVC_UNIT}" || 119 | return 0 120 | fi 121 | ;; 122 | stop) 123 | nmg::saferun "" "nolog" "${SYSTEMCTL}" -q is-active "${SVC_UNIT}" || 124 | return 0 125 | ;; 126 | esac 127 | 128 | nmg_info "Interface ${interface} ${action}: ${command} ${SVC_UNIT}" 129 | 130 | nmg_cmd "${SYSTEMCTL}" "${command}" "${SVC_UNIT}" || : 131 | } 132 | 133 | read_ifa_config() { 134 | # 135 | local file=$1 136 | 137 | unset CMD_UP CMD_DOWN CMD_PRE_DOWN CMD_CHANGE CMD_CHANGE6 138 | unset RESTART_UP RESTART_DOWN RESTART_PRE_DOWN RESTART_CHANGE RESTART_CHANGE6 139 | unset STOP_UP STOP_DOWN STOP_PRE_DOWN STOP_CHANGE STOP_CHANGE6 140 | unset STATE_FILE IGNORE_ENABLED 141 | 142 | nmg_read_config "${file}" 143 | } 144 | 145 | handle_config() { 146 | read_ifa_config "${SVC_CONFIG}" || return 0 147 | 148 | nmg_need_progs_env SYSTEMCTL || return 0 149 | 150 | case "${action}" in 151 | up) 152 | svc_action "${CMD_UP-}" "${RESTART_UP-}" "${STOP_UP-}" 153 | ;; 154 | down) 155 | svc_action "${CMD_DOWN-}" "${RESTART_DOWN-}" "${STOP_DOWN-}" 156 | ;; 157 | pre-down) 158 | svc_action "${CMD_PRE_DOWN-}" "${RESTART_PRE_DOWN-}" "${STOP_PRE_DOWN-}" 159 | ;; 160 | dhcp4-change) 161 | svc_action "${CMD_CHANGE-}" "${RESTART_CHANGE-}" "${STOP_CHANGE-}" 162 | ;; 163 | dhcp6-change) 164 | svc_action "${CMD_CHANGE6-}" "${RESTART_CHANGE6-}" "${STOP_CHANGE6-}" 165 | ;; 166 | esac 167 | } 168 | 169 | handle_ifd() { 170 | SVC_CONFIG="${NMCONF}/${IFD_CONFIG}" 171 | SVC_UNIT=${IFD_UNIT} 172 | handle_config 173 | } 174 | 175 | handle_match() { 176 | # ##- 177 | local SVC_CONFIG=$1 service=$2 178 | 179 | # check dispatcher name format for ##-ifd-service 180 | [[ ${service} =~ ^[0-9][0-9]-(.+)$ ]] || { 181 | nmg_err "Invalid interface-action config name: ${SVC_CONFIG}" && return 0 182 | } 183 | # used in svc_action 184 | local SVC_UNIT=${service#*-} 185 | 186 | handle_config 187 | } 188 | 189 | handle_action() { 190 | nmg::foreach_filematch "${IFA_CONFIG_PAT//@INTERFACE@/${interface}}" \ 191 | "@ORDER@-@SERVICE@" handle_match "${interface}" 192 | } 193 | 194 | if [[ ${IFD_CONFIG} && ${IFD_UNIT} ]]; then 195 | # dispatcher_action sourced us 196 | handle_ifd 197 | elif [[ ${interface} && ${action} ]]; then 198 | handle_action 199 | fi 200 | 201 | exit 0 202 | -------------------------------------------------------------------------------- /etc/NetworkManager/dispatcher.d/pre-down.d/08-ipv6-prefix: -------------------------------------------------------------------------------- 1 | ../08-ipv6-prefix -------------------------------------------------------------------------------- /etc/NetworkManager/dispatcher.d/pre-down.d/96-interface-action: -------------------------------------------------------------------------------- 1 | ../96-interface-action -------------------------------------------------------------------------------- /etc/nmutils/ddns-functions: -------------------------------------------------------------------------------- 1 | # -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- 2 | # vim:set ft=sh et sw=2 ts=2: 3 | # SPDX-License-Identifier: LGPL-3.0-or-later 4 | # 5 | # Copyright (C) 2014-2025 Scott Shambarger 6 | # 7 | # NMDDNS - Dynamic DNS functions scripts can include and use 8 | # Author: Scott Shambarger 9 | # 10 | # Instructions for use: 11 | # 12 | # Setup a few constants in your NetworkManager dispatcher script, and 13 | # include this file, here's an example: 14 | # 15 | # # optional, for logging 16 | # NMG_TAG="ddns" 17 | # 18 | # # set NMUTILS early, and allow environment to override 19 | # NMUTILS=${NMUTILS:-/etc/nmutils} 20 | # 21 | # # optional min-version required 22 | # NMDDNS_REQUIRED="1.3.7" 23 | # 24 | # NMDDNS=${NMDDNS:-${NMUTILS}/ddns-functions} 25 | # { [[ -r ${NMDDNS} ]] && . "${NMDDNS}"; } || NMDDNS='' 26 | # 27 | # Use of NM* variables above is optional (NMDDNS here indicates 28 | # nmddns_* functions were loaded), but the above allows easy 29 | # overrides from the environment (and easy testing). You may also 30 | # want to customize some settings in NMCONF/general.conf (see 31 | # "Global overrides" below) 32 | # 33 | # Requires: 34 | # 35 | # NMUTILS/general-functions - shared functions 36 | # nsupdate - for DNS updates 37 | # dig - for DNS queries 38 | # 39 | # Supported, but optional: 40 | # 41 | # 09-ddns - asynchronous DDNS helper- place "HELPER" in 42 | # /etc/NetworkManager/dispatcher.d (or set NMDDNS_DHELPER 43 | # in NMG's general.conf) 44 | # flock - used for locking 45 | # 46 | # Dynamic DNS Functions: 47 | # 48 | # nmddns_read_config 49 | # 50 | # Reset config setting (including for all resource records) and 51 | # read . Return 1 if not found, 2 if DDNS_ZONE not set. 52 | # 53 | # nmddns_required_config 54 | # 55 | # Same as nmddns_read_config but exit 0 if not found/required element 56 | # missing, or exit with error if other error. 57 | # 58 | # nmddns_reset_config 59 | # 60 | # Unset all config environment 61 | # 62 | # nmddns_get_config [ ] 63 | # 64 | # Sets with DDNS config using nmg::print_env 65 | # 66 | # nmddns_cleanup 67 | # 68 | # Unset all loaded environment 69 | # 70 | # nmddns_get_globals [ ] 71 | # 72 | # Sets with global DDNS config using nmg::print_env 73 | # 74 | # nmddns::get_A_addrs [ ] 75 | # 76 | # Sets to all global ipv4 addresses on that 77 | # pass (see nmg_check_ip4_addr). Fails if 78 | # nmg::query_ips does. 79 | # 80 | # nmddns::get_AAAA_addrs [ ] 81 | # 82 | # As get_A_addrs above, but for ipv6 addresses that also pass DAD. 83 | # 84 | # nmddns_update [ [ ] ] 85 | # 86 | # Set DNS resource record to , or if empty, 87 | # set to the fallback value if not empty (see below), or remove 88 | # the record (DDNS_* values should be set). If supplied, 89 | # is expected to be in RDATA format appropriate for . 90 | # If provided, must contain the string "@RREC@" and 91 | # is used to save and override value assigned later by 09-ddns. 92 | # Returns any errors from nsupdate. 93 | # 94 | # SPECIAL CASE: for A/AAAA records, values containing "," are handled 95 | # as multiple (comma-separated) address records (see _LISTSEP below for 96 | # multiple records of other types). 97 | # 98 | # SPECIAL CASE: for A/AAAA records, values of "!" will 99 | # use nmddns::get__addrs to query current ips on . 100 | # 101 | # nmddns_spawn_update [ [ ] ] 102 | # 103 | # Checks if defined in , and if so spawns HELPER 104 | # to call nmddns_update asynchronously. If HELPER not found, calls 105 | # nmddns_update directly with reduced timeouts. 106 | # 107 | # nmddns_update_all <"up" | "down"> [ [ ] ] 108 | # 109 | # Updates for all configured DDNS names (DDNS_* values should be set), 110 | # using for A and for AAAA (if respective 111 | # DDNS_RREC_*_VALUEs don't override). Returns any errors from nsupdate. 112 | # See nmddns_update for special A/AAAA values and use of . 113 | # 114 | # nmddns_spawn_update_all <"up" | "down"> 115 | # [ [ ] ] 116 | # 117 | # Spawn HELPER to call nmddns_update_all asynchronously, or read 118 | # and call it directly if HELPER not found. 119 | # 120 | # Configuration Settings (set before including this file) 121 | # 122 | # Any general-functions configuration, eg. NMUTILS/NMCONF 123 | # 124 | # NMDDNS_REQUIRED (optional) - minimum required NMDDNS_VERSION 125 | # 126 | # Global Overrides (put in NMCONF/general.conf) 127 | # 128 | # "@MATCH@" can be , or -from- 129 | # 130 | # NMDDNS_CONFIG_PAT (default: "NMCONF/ddns-@MATCH@.conf") 131 | # Value must contain a single "@MATCH@" 132 | # See "Config Settings" below for file contents. 133 | # 134 | # DDNS_STATE_DIR (default: "$RUNDIR", or "/run/nmutils" if unset) 135 | # 136 | # NMDDNS_STATE_PAT (default: "DDNS_STATE_DIR/ddns-@MATCH@-@RREC@.state") 137 | # Value must contain a single "@MATCH@-@RREC@" and is used by 138 | # 09-ddns for per-@RREC@ overrides for @MATCH@ config. 139 | # (use with nmddns_update) 140 | # 141 | # DDNS_GLOBAL_LOCKFILE (optional) - flock lockfile used to prevent 142 | # races between query and set of records. Overrides use of config 143 | # file with this file for all locking (useful to serialize all DNS 144 | # updates). 145 | # 146 | # DDNS_GLOBAL_FLOCK_TIMEOUT (default: 15) - flock timeout in seconds 147 | # 148 | # DDNS_GLOBAL_DIG_TIMEOUT (default: 3) - DNS query (dig) timeout 149 | # 150 | # DDNS_GLOBAL_DIG_RETRIES (default: 2) - DNS query (dig) retries 151 | # 152 | # DDNS_GLOBAL_DIG_OPTIONS (optional) - options for dig, to have 153 | # it use TCP ("+tcp") or a keyfile 154 | # (eg "-k /etc/Kexample.net.+157+12345.private") 155 | # 156 | # DDNS_GLOBAL_NSUPDATE_TIMEOUT (default: 10) - nsupdate timeout in seconds 157 | # 158 | # DDNS_GLOBAL_NSUPDATE_OPTIONS (optional) - options for nsupdate, to have 159 | # it use TCP ("-v") or a keyfile 160 | # (eg "-k /etc/Kexample.net.+157+12345.private") 161 | # 162 | # Config Settings (put in , see nmddns_read_config() above): 163 | # 164 | # DDNS_ZONE (required) - zone to update 165 | # 166 | # DDNS_RREC__NAME (one per ) - name to update for record 167 | # ( is A, AAAA, CNAME, TXT etc). This is generally a 168 | # domain or host name, but can be any valid DNS name. 169 | # 170 | # DDNS_RREC__VALUE (optional) - use this value when interface is up. 171 | # If empty, then DDNS_RREC__FALLBACK is used. If set to "*" 172 | # (an asterisk), then FALLBACK is ignored, and the is removed 173 | # when the interface is up. 174 | # 175 | # DDNS_RREC__FALLBACK (optional) - value to use if a record would 176 | # otherwise be removed (empty value); useful to set a value when 177 | # interface is down, or an global address is not yet available 178 | # on an interface. 179 | # 180 | # DDNS_RREC__PRIVATE (optional) - for A/AAAA only, allow private 181 | # interface addresses to be used (otherwise would use FALLBACK) 182 | # 183 | # DDNS_RREC__LISTSEP (optional) - for non-A/AAAA records, 184 | # handles and *_VALUEs as multiple records separated by 185 | # this variable's value. 186 | # 187 | # DDNS_DIG_TIMEOUT (optional) - override global DNS query (dig) timeout 188 | # 189 | # DDNS_DIG_RETRIES (optional) - override global DNS query (dig) retries 190 | # 191 | # DDNS_DIG_OPTIONS (optional) - override global dig options 192 | # 193 | # DDNS_NSUPDATE_TIMEOUT (optional) - override global nsupdate timeout 194 | # 195 | # DDNS_NSUPDATE_OPTIONS (optional) - override global nsupdate options 196 | # 197 | # DDNS_SERVER (default: 127.0.0.1) - dns server to update 198 | # 199 | # DDNS_TTL (default: 600) - ttl of entry 200 | # 201 | # DDNS_FLOCK_TIMEOUT (optional) - override global flock timeout 202 | # 203 | # DDNS_LOCKFILE (optional) - flock lockfile used to prevent races between 204 | # query and update. Defaults to config file itself. Set to empty 205 | # in a config file to disable locking for just that file. 206 | # 207 | # NOTE: executable paths (see below) may be overriden if needed. 208 | # 209 | # Globals 210 | # 211 | # NMDDNS_VERSION - current file version 212 | # 213 | # shellcheck shell=bash disable=SC1090 214 | 215 | [[ ${NMDDNS_VERSION-} ]] || declare -r NMDDNS_VERSION="1.6.0" 216 | 217 | # set default paths if missing 218 | NMUTILS=${NMUTILS:-/etc/nmutils} 219 | 220 | ########## Global Defaults (customize in $NMCONF/general.conf) 221 | 222 | DDNS_STATE_DIR=${RUNDIR:-/run/nmutils} 223 | 224 | ########## Support Programs 225 | 226 | NMDDNS_DHELPER=${NMDDNS_DHELPER:-/etc/NetworkManager/dispatcher.d/09-ddns} 227 | NMDDNS_DIG=${NMDDNS_DIG:-dig} 228 | NMDDNS_NSUPDATE=${NMDDNS_NSUPDATE:-nsupdate} 229 | 230 | # set NMDDNS_FLOCK to empty to disable all locking 231 | NMDDNS_FLOCK=${NMDDNS_FLOCK:-flock} 232 | 233 | ########## SCRIPT START 234 | 235 | # load general-functions 236 | NMG=${NMG:-${NMUTILS}/general-functions} 237 | # save any existing NMG_REQUIRED (checked later) 238 | NMDDNS_NMG_REQ=${NMG_REQUIRED-} 239 | NMG_REQUIRED="1.8.0" 240 | { [[ -r ${NMG} ]] && . "${NMG}"; } || { 241 | echo 1>&2 "Unable to load ${NMG}"; NMG=''; } 242 | 243 | ########## Config Locations (defaults for HELPER) 244 | 245 | NMDDNS_CONFIG_PAT=${NMDDNS_CONFIG_PAT:-${NMCONF}/ddns-@MATCH@.conf} 246 | NMDDNS_STATE_PAT=${NMDDNS_STATE_PAT:-${DDNS_STATE_DIR}/ddns-@MATCH@-@RREC@.state} 247 | 248 | # private 249 | nmddns::_loaded() { 250 | 251 | # test if general-functions loaded.. 252 | [[ ${NMG} ]] || { 253 | # exit, as any fallback will fail to load NMG anyway 254 | exit 2 255 | } 256 | 257 | if [[ ${NMDDNS_NMG_REQ} ]]; then 258 | NMG_REQUIRED=${NMDDNS_NMG_REQ} 259 | if ! nmg::require_version "${NMG_VERSION}" "${NMG_REQUIRED}"; then 260 | nmg_err "${BASH_SOURCE[0]}: NMG_VERSION=${NMG_VERSION} < NMG_REQUIRED=${NMG_REQUIRED}" 261 | return 1 262 | fi 263 | fi 264 | if [[ ${NMDDNS_REQUIRED-} ]] && 265 | ! nmg::require_version "${NMDDNS_VERSION}" "${NMDDNS_REQUIRED}"; then 266 | nmg_err "${BASH_SOURCE[0]}: NMDDNS_VERSION=${NMDDNS_VERSION} < NMDDNS_REQUIRED=${NMDDNS_REQUIRED}" 267 | return 1 268 | fi 269 | 270 | # test required programs 271 | nmg_need_progs_env NMDDNS_DIG NMDDNS_NSUPDATE || return 272 | } 273 | 274 | # internal 275 | nmddns::_short_timeouts() { 276 | # fast timeouts with no background HELPER is available 277 | DDNS_DIG_TIMEOUT=1 278 | DDNS_DIG_RETRIES=0 279 | DDNS_NSUPDATE_TIMEOUT=2 280 | DDNS_FLOCK_TIMEOUT=1 281 | } 282 | 283 | # internal 284 | nmddns::_update() { 285 | # 286 | local ddns_name=$1 ddns_rrec=$2 ddns_value=$3 cur_value name sep 287 | 288 | DDNS_SERVER=${DDNS_SERVER:-127.0.0.1} 289 | 290 | # check if server already has correct entry 291 | nmg_debug "Looking up current ${ddns_rrec} on server ${DDNS_SERVER}" 292 | 293 | local qtime=${DDNS_DIG_TIMEOUT:-${DDNS_GLOBAL_DIG_TIMEOUT:-3}} 294 | local qretry=${DDNS_DIG_RETRIES:-${DDNS_GLOBAL_DIG_RETRIES:-2}} 295 | local qopts=${DDNS_DIG_OPTIONS:-${DDNS_GLOBAL_DIG_OPTIONS-}} 296 | # shellcheck disable=SC2086 297 | if ! nmg::run cur_value "" "${NMDDNS_DIG}" "@${DDNS_SERVER}" \ 298 | +short +retry="${qretry}" +time="${qtime}" ${qopts} \ 299 | "${ddns_rrec}" "${ddns_name}"; then 300 | if [[ ${ddns_value} ]]; then 301 | nmg_err "Update ${ddns_name} ${ddns_rrec} to ${ddns_value} failed" 302 | else 303 | nmg_err "Removal of ${ddns_name} ${ddns_rrec} failed" 304 | fi 305 | return 25 306 | fi 307 | 308 | name="DDNS_RREC_${ddns_rrec}_LISTSEP"; sep=${!name:-} 309 | 310 | [[ ${ddns_rrec} =~ ^(A|AAAA)$ ]] && sep="," 311 | 312 | if [[ ${sep} ]]; then 313 | # shellcheck disable=SC2034 314 | local acur=() anew=() 315 | nmg::array acur $'\n' "${cur_value}" 316 | nmg::array anew "${sep}" "${ddns_value}" 317 | if nmg::array_match_values acur anew; then 318 | nmg_debug "${ddns_name} ${ddns_rrec} entry current: ${ddns_value}" 319 | return 320 | fi 321 | elif [[ ${cur_value} == "${ddns_value}" ]]; then 322 | nmg_debug "${ddns_name} ${ddns_rrec} entry current: ${ddns_value}" 323 | return 324 | fi 325 | 326 | nmg_debug "Old ${ddns_name} ${ddns_rrec} value: ${cur_value}" 327 | local ddns_cmd='' items=() 328 | if [[ ${ddns_value} ]]; then 329 | nmg_info "Setting ${ddns_name} ${ddns_rrec} to ${ddns_value}" 330 | if [[ ${sep} ]]; then 331 | nmg::array items "${sep}" "${ddns_value}" 332 | for ddns_value in "${items[@]}"; do 333 | ddns_cmd+="update add ${ddns_name} ${DDNS_TTL:-600} ${ddns_rrec} ${ddns_value}"$'\n' 334 | done 335 | else 336 | ddns_cmd="update add ${ddns_name} ${DDNS_TTL:-600} ${ddns_rrec} ${ddns_value}"$'\n' 337 | fi 338 | else 339 | nmg_info "Removing ${ddns_name} ${ddns_rrec}" 340 | fi 341 | 342 | # update the entry (15 sec timeout) 343 | local timeout=${DDNS_NSUPDATE_TIMEOUT:-${DDNS_GLOBAL_NSUPDATE_TIMEOUT:-10}} 344 | local options=${DDNS_NSUPDATE_OPTIONS:-${DDNS_GLOBAL_NSUPDATE_OPTIONS-}} 345 | # shellcheck disable=SC2086 346 | nmg_cmd "${NMDDNS_NSUPDATE}" -t "${timeout}" ${options} <<- EOF 347 | server ${DDNS_SERVER} 348 | zone ${DDNS_ZONE} 349 | update delete ${ddns_name} ${ddns_rrec} 350 | ${ddns_cmd}send 351 | EOF 352 | local rc=$? 353 | [[ ${rc} != 0 ]] && nmg_err "DNS update to server ${DDNS_SERVER} failed for ${ddns_name} ${ddns_rrec}" 354 | 355 | return ${rc} 356 | } 357 | 358 | _nmddns_conf_env=( 359 | DDNS_ZONE DDNS_SERVER DDNS_TTL 360 | 'DDNS_FLOCK_TIMEOUT="'"${DDNS_GLOBAL_FLOCK_TIMEOUT:-15}"'"' 361 | 'DDNS_DIG_TIMEOUT="'"${DDNS_GLOBAL_DIG_TIMEOUT:-3}"'"' 362 | 'DDNS_DIG_RETRIES="'"${DDNS_GLOBAL_DIG_RETRIES:-2}"'"' 363 | 'DDNS_DIG_OPTIONS="'"${DDNS_GLOBAL_DIG_OPTIONS-}"'"' 364 | 'DDNS_NSUPDATE_TIMEOUT="'"${DDNS_GLOBAL_NSUPDATE_TIMEOUT:-10}"'"' 365 | 'DDNS_NSUPDATE_OPTIONS="'"${DDNS_GLOBAL_NSUPDATE_OPTIONS-}"'"' 366 | 'DDNS_LOCKFILE="'"${DDNS_GLOBAL_LOCKFILE-}"'"' 367 | ) 368 | 369 | nmddns_reset_config() { 370 | nmg::unset_env "${!DDNS_RREC_@}" "${_nmddns_conf_env[@]}" 371 | } 372 | 373 | nmddns_read_config() { 374 | # 375 | local config=${1-} 376 | 377 | # clear config 378 | nmddns_reset_config 379 | 380 | # lockfile defaults to config-file (as it will exist if used) 381 | DDNS_LOCKFILE=${DDNS_GLOBAL_LOCKFILE-${config}} 382 | 383 | # read config if any 384 | nmg_read_config "${config}" || return 385 | 386 | # check required elements 387 | [[ ${DDNS_ZONE-} ]] || return 2 388 | } 389 | 390 | _nmddns_gconf_env=( 391 | DDNS_GLOBAL_LOCKFILE 'DDNS_GLOBAL_FLOCK_TIMEOUT="15"' 392 | 'DDNS_GLOBAL_DIG_TIMEOUT="3"' 'DDNS_GLOBAL_DIG_RETRIES="2"' 393 | DDNS_GLOBAL_DIG_OPTIONS 'DDNS_GLOBAL_NSUPDATE_TIMEOUT="10"' 394 | DDNS_GLOBAL_NSUPDATE_OPTIONS 395 | ) 396 | 397 | nmddns_cleanup() { 398 | nmddns_reset_config 399 | nmg::unset_env "${_nmddns_gconf_env[@]}" 400 | } 401 | 402 | nmddns_get_config() { # [ ] 403 | [[ ${1-} ]] || { nmg_err "nmddns_get_config: missing "; return 3; } 404 | nmg::print_env "$1" "${2-}" "${_nmddns_conf_env[@]}" "${!DDNS_RREC_@}" 405 | } 406 | 407 | nmddns_get_globals() { # [ ] 408 | [[ ${1-} ]] || { nmg_err "nmddns_get_globals: missing "; return 3; } 409 | nmg::print_env "$1" "${2-}" "${_nmddns_gconf_env[@]}" 410 | } 411 | 412 | nmddns_required_config() { 413 | # 414 | local rc=0 415 | nmddns_read_config "${1-}" || rc=$? 416 | 417 | # 1 means no file, just exit 0 418 | (( rc == 1 )) && exit 0 419 | # any other error, exit with it 420 | (( rc != 0 )) && exit ${rc} 421 | # no errors, continue... 422 | return 0 423 | } 424 | 425 | # Sets _rlist arrays to all active addresses on 426 | # If not empty, allow private addresses 427 | nmddns::_query_addrs() { # fails if empty or query_ips fails 428 | # 429 | local ver=$1 intf=$2 priv=$3 addr addrp alist=() 430 | 431 | [[ ${intf} ]] || return 432 | 433 | # query addresses with properties 434 | nmg::query_ips alist "nolog" "${ver}p" "${intf}" || return 435 | 436 | for addrp in ${alist[@]+"${alist[@]}"}; do 437 | # strip / 438 | addr=${addrp%%/*} 439 | if [[ ${ver} == 4 ]]; then 440 | nmg_check_ip4_addr "${addr}" "${priv}" || continue 441 | else 442 | nmg_check_ip6_addr "${addr}" "${priv}" || continue 443 | [[ ${addrp} =~ (^| )dadfailed($| ) ]] && continue 444 | [[ ${addrp} =~ (^| )tentative($| ) ]] && { 445 | nmg::wait_dad6 "${intf}" "${addrp%% *}" || continue 446 | } 447 | fi 448 | _rlist+=("${addr}") 449 | done 450 | 451 | return 0 452 | } 453 | 454 | # sets with all valid dns ip4 address on 455 | nmddns::get_A_addrs() { # returns err if query_ips fails 456 | # [ ] 457 | local _rlist=() _rc=0 458 | nmddns::_query_addrs 4 "${2-}" "${3-}" || _rc=$? 459 | [[ ${1-} ]] && nmg::array_copy "$1" "_rlist" 460 | return ${_rc} 461 | } 462 | 463 | # sets with all valid dns ip4 address on 464 | nmddns::get_AAAA_addrs() { # returns err if query_ips fails 465 | # [ ] 466 | local _rlist=() _rc=0 467 | nmddns::_query_addrs 6 "${2-}" "${3-}" || _rc=$? 468 | [[ ${1-} ]] && nmg::array_copy "$1" "_rlist" 469 | return ${_rc} 470 | } 471 | 472 | nmddns_update() { 473 | # [ []] 474 | local rrec=${1-} value=${2-} state_pat=${3-} name ddns_name state 475 | local new_value priv_ok timeout avalue=() anew=() addr 476 | 477 | [[ ${rrec} ]] || { 478 | nmg_err "nmddns_update: missing "; return 1; } 479 | 480 | # are we configured to update this ? 481 | name="DDNS_RREC_${rrec}_NAME"; ddns_name=${!name-} 482 | [[ ${ddns_name} ]] || return 0 483 | 484 | # check config 485 | [[ ${DDNS_ZONE-} ]] || { 486 | nmg_err "Missing required DDNS_ZONE config"; return 5; } 487 | 488 | [[ ${state_pat} && ! ${state_pat} =~ @RREC@ ]] && { 489 | nmg_err "nmddns_update: must contain '@RREC@'" 490 | state_pat='' 491 | } 492 | 493 | state=${state_pat/@RREC@/${rrec}} 494 | 495 | if [[ ${value} ]]; then 496 | 497 | # set state if requested 498 | [[ ${state} ]] && { nmg_write "${state}" "${value}"$'\n' || :; } 499 | 500 | if [[ ${rrec} =~ ^(A|AAAA)$ ]]; then 501 | 502 | # check for ! value 503 | if [[ ${value} =~ ^[!].+$ ]]; then 504 | # if interface down, remove addrs 505 | if [[ ${rrec} == A ]]; then 506 | nmddns::get_A_addrs anew "${value#!}" \ 507 | "${DDNS_RREC_A_PRIVATE-}" || : 508 | else 509 | nmddns::get_AAAA_addrs anew "${value#!}" \ 510 | "${DDNS_RREC_AAAA_PRIVATE-}"|| : 511 | fi 512 | else 513 | nmg::lowercase value "${value}" 514 | nmg::array anew "," "${value}" 515 | fi 516 | # strip any "/" 517 | for addr in ${anew[@]+"${anew[@]}"}; do avalue+=("${addr%%/*}"); done 518 | nmg::array_join value "," "${avalue[@]-}" 519 | fi 520 | 521 | # use DDNS_RREC_*_VALUE, or $value if not set/empty 522 | name="DDNS_RREC_${rrec}_VALUE"; new_value=${!name:-${value}} 523 | 524 | if [[ ${rrec} =~ ^(A|AAAA)$ ]]; then 525 | # SPECIAL CASE: if A or AAAA (without _VALUE override), make 526 | # sure new value is valid 527 | nmg::lowercase new_value "${new_value}" 528 | nmg::array anew "," "${new_value}" 529 | # if list new_value == value (no override), check values are valid 530 | if nmg::array_match_values avalue anew; then 531 | name="DDNS_RREC_${rrec}_PRIVATE"; priv_ok=${!name-} 532 | anew=() 533 | for addr in ${avalue[@]+"${avalue[@]}"}; do 534 | if [[ ${rrec} == A ]]; then 535 | nmg_check_ip4_addr "${addr}" "${priv_ok}" && anew+=("${addr}") 536 | else 537 | nmg_check_ip6_addr "${addr}" "${priv_ok}" && anew+=("${addr}") 538 | fi 539 | done 540 | # update new_value with only checked values 541 | nmg::array_join new_value "," "${anew[@]-}" 542 | fi 543 | fi 544 | 545 | # use fallback (if any) if empty 546 | [[ ${new_value} ]] || { 547 | name="DDNS_RREC_${rrec}_FALLBACK"; new_value=${!name-}; } 548 | 549 | # new_value of * means remove entry on set (ignoring fallback) 550 | [[ ${new_value} == "*" ]] && value='' || value=${new_value} 551 | else 552 | 553 | # remove state if requested 554 | [[ ${state} ]] && nmg_remove "${state}" 555 | 556 | # clearing value, use fallback (if any) 557 | name="DDNS_RREC_${rrec}_FALLBACK"; value=${!name-} 558 | fi 559 | 560 | if [[ -f ${DDNS_LOCKFILE-} ]] && 561 | command &>/dev/null -v "${NMDDNS_FLOCK}"; then 562 | timeout=${DDNS_FLOCK_TIMEOUT:-${DDNS_GLOBAL_FLOCK_TIMEOUT:-15}} 563 | ("${NMDDNS_FLOCK}" -w "${timeout}" 9 || { 564 | nmg_err "Timeout getting DDNS lock for ${rrec}"; exit 1; } 565 | nmddns::_update "${ddns_name}" "${rrec}" "${value}" 566 | ) 9<"${DDNS_LOCKFILE}" || return 567 | else 568 | # no locking 569 | [[ ${DDNS_LOCKFILE-} ]] && nmg_info "Locking not available for DDNS" 570 | nmddns::_update "${ddns_name}" "${rrec}" "${value}" || return 571 | fi 572 | return 0 573 | } 574 | 575 | nmddns::_daemon_helper() { # return 1 if helper unavail or daemonize fails 576 | # [ ] 577 | [[ ${NMDDNS_DHELPER-} ]] || return 1 578 | 579 | # export helper action 580 | local -x NMDDNSH_ACTION=$1 NMDDNSH_CONFIG=$2 NMDDNSH_STATE=${3-} 581 | 582 | nmg_daemon "${NMDDNS_DHELPER}" || return 583 | } 584 | 585 | nmddns_spawn_update() { 586 | # [ []] 587 | local config=${1-} rrec=${2-} name ddns_name 588 | 589 | [[ ${config} ]] || { 590 | nmg_err "nmddns_spawn_update: requires a value" 591 | return 1 592 | } 593 | 594 | [[ ${rrec} ]] || { 595 | nmg_err "nmddns_spawn_update: must be a RREC name" 596 | return 1 597 | } 598 | 599 | # check if config exists 600 | nmddns_read_config "${config}" || return 0 601 | 602 | # check if name configured 603 | name="DDNS_RREC_${rrec}_NAME"; ddns_name=${!name-} 604 | [[ ${ddns_name} ]] || return 0 605 | 606 | # we have a valid config, spawn the HELPER 607 | local -x NMDDNSH_RREC=${rrec} NMDDNSH_VALUE=${3-} 608 | 609 | nmddns::_daemon_helper update "${config}" "${4-}" && return 0 610 | 611 | # make timeouts much faster, or NetworkManager will kill us 612 | nmddns::_short_timeouts 613 | 614 | # HELPER not found, or can't be started, update directly. 615 | shift 1 616 | nmddns_update "$@" 617 | } 618 | 619 | nmddns_update_all() { 620 | # <"up"|"down"> [ [ ] ] 621 | local action=$1 addr4=${2-} addr6=${3-} state_pat=${4-} 622 | local name rrec value rc=0 623 | 624 | for name in "${!DDNS_RREC_@}"; do 625 | 626 | # DDNS_RREC__NAME is required 627 | [[ ${name} =~ _NAME$ ]] || continue 628 | 629 | # parse rrec 630 | name=${name#DDNS_RREC_} 631 | rrec=${name%_NAME} 632 | [[ ${rrec} ]] || continue 633 | 634 | case ${action} in 635 | up) 636 | # special case values for A and AAAA 637 | case ${rrec} in 638 | A) value=${addr4} ;; 639 | AAAA) value=${addr6} ;; 640 | *) name="DDNS_RREC_${rrec}_VALUE"; value=${!name-} ;; 641 | esac 642 | ;; 643 | down) value='' ;; 644 | *) 645 | nmg_err "nmddns_update_all: must be 'up' or 'down'" 646 | return 1 647 | ;; 648 | esac 649 | 650 | # perform update 651 | nmddns_update "${rrec}" "${value}" "${state_pat}" || rc=$? 652 | done 653 | 654 | return ${rc} 655 | } 656 | 657 | nmddns_spawn_update_all() { 658 | # <"up"|"down"> [ [ ] ] 659 | local action=${1-} config=${2-} 660 | 661 | [[ ${action} =~ ^(up|down)$ ]] || { 662 | nmg_err "nmddns_spawn_update_all: must be 'up' of 'down'" 663 | return 1 664 | } 665 | 666 | [[ ${config} ]] || { 667 | nmg_err "nmddns_spawn_update_all: requires a value" 668 | return 1 669 | } 670 | 671 | # check if config exists 672 | nmddns_read_config "${config}" || return 0 673 | 674 | # check if any names configured 675 | local name name_found='' 676 | for name in "${!DDNS_RREC_@}"; do 677 | # DDNS_RREC__NAME is required 678 | [[ ${name} =~ _NAME$ ]] && name_found=1 && break 679 | done 680 | [[ ${name_found} ]] || return 0 681 | 682 | # we have a valid config, spawn the HELPER 683 | local -x NMDDNSH_ADDR4=${3-} NMDDNSH_ADDR6=${4-} 684 | 685 | nmddns::_daemon_helper "${action}" "${config}" "${5-}" && return 0 686 | 687 | # make timeouts much faster, or NetworkManager will kill us 688 | nmddns::_short_timeouts 689 | 690 | # HELPER not found, or can't be started, update directly. 691 | shift 2 692 | nmddns_update_all "${action}" "$@" 693 | } 694 | 695 | # last, to fail load if any missing components 696 | nmddns::_loaded 697 | -------------------------------------------------------------------------------- /etc/nmutils/dispatcher_action: -------------------------------------------------------------------------------- 1 | # -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- 2 | # vim:set ft=sh et sw=2 ts=2: 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | # 5 | # Copyright (C) 2015-2024 Scott Shambarger 6 | # 7 | # dispatcher_action v1.4.0 - service restart on interface change 8 | # Author: Scott Shambarger 9 | # 10 | # This file supports the same functions as 96-interface-action, but allows 11 | # re-ordering the action relative to other dispatchers. However, it requires 12 | # an extra config step. 13 | # 14 | # The extra step is creating a file named 15 | # 16 | # /etc/NetworkManager/dispatcher.d/##-ifd- 17 | # 18 | # (or wherever your distro has these files) where <##> is a 2-digit 19 | # number, and is a systemd service name. The file should be 20 | # executable and contain the following: 21 | # 22 | # --- start 23 | # #!/bin/bash 24 | # . /etc/nmutils/dispatcher_action 25 | # --- end 26 | # 27 | # The configuration settings are documented in 96-interface-action, but 28 | # those settings are should instead be placed in: 29 | # 30 | # /etc/nmutils/conf/ifd--.conf 31 | # 32 | # NOTE: If any PRE_DOWN actions are used, the ##-ifd- script 33 | # should be symlinked to the pre-down.d directory. 34 | # 35 | # shellcheck shell=bash disable=SC1090 36 | interface=${1-} 37 | action=${2-} 38 | 39 | ########## SCRIPT START 40 | 41 | # anything for us to do? 42 | [[ ${interface} && ${action} ]] || exit 0 43 | 44 | # check dispatcher name format for ##-ifd-service 45 | base_name=${0##*/} 46 | [[ ${base_name} =~ ^[0-9][0-9]-ifd-([^/]+)$ ]] || { 47 | echo >&2 "Invalid command name: ${base_name}" && exit 3 48 | } 49 | IFD_UNIT=${BASH_REMATCH[1]} 50 | 51 | # shellcheck disable=SC2034 52 | IFD_CONFIG="ifd-${IFD_UNIT}-${interface}.conf" 53 | 54 | NMG_TAG=${NMG_TAG-nm-ifd} 55 | IFA_FILE="/etc/NetworkManager/dispatcher.d/96-interface-action" 56 | { [[ -r "${IFA_FILE}" ]] && . "${IFA_FILE}"; } || { 57 | echo >&2 "Unable to load ${IFA_FILE}" && exit 2 58 | } 59 | 60 | exit 0 61 | -------------------------------------------------------------------------------- /etc/nmutils/ipv6_utils.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # 3 | # Calculate IPv6 address segments entirely in bash 4 | # 5 | # Based loosely on wg-ip (https://github.com/chmduquesne/wg-ip) 6 | # 7 | # IPv6 definitions from https://en.wikipedia.org/wiki/IPv6_address 8 | # 9 | # Frank Crawford - - 31-Jul-2021 10 | # 11 | # Functions: 12 | # expand_ipv6 $ip 13 | # - expand out IPv6 ($ip) address with all digits 14 | # compress_ipv6 $ip 15 | # - returns compressed IPv6 address ($ip) under the form recommended by RFC5952 16 | # ipv6_prefix $ip $subnet 17 | # - extract the IPv6 routing prefix from $ip with subnet length $subnet 18 | # ipv6_subnetid $ip $subnet $fmt 19 | # - extract the local subnet ID from unicast address ($ip) with optional $fmt 20 | # ipv6_interface $ip 21 | # - IPv6 host or interface part of address ($ip) 22 | # ipv6_split_mask $ip/$mask 23 | # - returns 2 values $ip and $mask 24 | # is_ipv6 $ip 25 | # - tests if address ($ip) is a valid IPv6 in either the expanded form 26 | # or the compressed one 27 | # ipv6_type $ip 28 | # - return IPv6 address ($ip) category 29 | 30 | # helper to convert hex to dec (portable version) 31 | hex2dec() { 32 | [ "$1" != "" ] && printf "%d" "$(( 0x$1 ))" 33 | } 34 | 35 | # convert ipv6 to lowercase 36 | # inspired by https://stackoverflow.com/a/51573758/14179001 37 | lowercase_ipv6() { # - echoes result 38 | local lcs="abcdef" ucs="ABCDEF" 39 | local result="${1-}" uchar uoffset 40 | 41 | while [[ "$result" =~ ([A-F]) ]]; do 42 | uchar="${BASH_REMATCH[1]}" 43 | uoffset="${ucs%%${uchar}*}" 44 | result="${result//${uchar}/${lcs:${#uoffset}:1}}" 45 | done 46 | 47 | echo -n "$result" 48 | } 49 | 50 | # expand an IPv6 address 51 | expand_ipv6() { 52 | local ip=$(lowercase_ipv6 ${1:-::1}) 53 | 54 | # prepend 0 if we start with : 55 | [[ "$ip" =~ ^: ]] && ip="0${ip}" 56 | 57 | # expand :: 58 | if [[ "$ip" =~ :: ]]; then 59 | local colons=${ip//[^:]/} 60 | local missing=':::::::::' 61 | missing=${missing/$colons/} 62 | local expanded=${missing//:/:0} 63 | ip=${ip/::/$expanded} 64 | fi 65 | 66 | local blocks=${ip//[^0-9a-f]/ } 67 | set $blocks 68 | 69 | printf "%04x:%04x:%04x:%04x:%04x:%04x:%04x:%04x\n" \ 70 | $(hex2dec $1) \ 71 | $(hex2dec $2) \ 72 | $(hex2dec $3) \ 73 | $(hex2dec $4) \ 74 | $(hex2dec $5) \ 75 | $(hex2dec $6) \ 76 | $(hex2dec $7) \ 77 | $(hex2dec $8) 78 | } 79 | 80 | # returns a compressed IPv6 address under the form recommended by RFC5952 81 | compress_ipv6() { 82 | local ip=$(expand_ipv6 $1) 83 | 84 | local blocks=${ip//[^0-9a-f]/ } 85 | set $blocks 86 | 87 | # compress leading zeros 88 | ip=$(printf "%x:%x:%x:%x:%x:%x:%x:%x\n" \ 89 | $(hex2dec $1) \ 90 | $(hex2dec $2) \ 91 | $(hex2dec $3) \ 92 | $(hex2dec $4) \ 93 | $(hex2dec $5) \ 94 | $(hex2dec $6) \ 95 | $(hex2dec $7) \ 96 | $(hex2dec $8) 97 | ) 98 | 99 | # prepend : for easier matching 100 | ip=:$ip 101 | 102 | # :: must compress the longest chain 103 | local pattern 104 | for pattern in :0:0:0:0:0:0:0:0 \ 105 | :0:0:0:0:0:0:0 \ 106 | :0:0:0:0:0:0 \ 107 | :0:0:0:0:0 \ 108 | :0:0:0:0 \ 109 | :0:0:0 \ 110 | :0:0; do 111 | if [[ "$ip" =~ $pattern ]]; then 112 | ip=${ip/$pattern/::} 113 | # if the substitution occured before the end, we have ::: 114 | ip=${ip/:::/::} 115 | break # only one substitution 116 | fi 117 | done 118 | 119 | # remove prepending : if necessary 120 | [[ "$ip" =~ ^:[^:] ]] && ip=${ip/#:/} 121 | 122 | echo -n $ip 123 | } 124 | 125 | # extract the IPv6 routing prefix 126 | ipv6_prefix() { 127 | local prefix=$(expand_ipv6 $1) 128 | local subnet=${2:-64} 129 | 130 | local nibble='' 131 | 132 | (( $subnet > 64 )) && subnet=64 133 | 134 | if (( $subnet % 16 )); then 135 | nibble=$(printf "%04x" "$(( 0x${prefix:($subnet/16)*5:4} & ~((1<<(16-$subnet%16))-1) ))") 136 | fi 137 | 138 | compress_ipv6 "${prefix:0:($subnet/16)*5}${nibble}::" 139 | } 140 | 141 | # extract the local subnet ID 142 | ipv6_subnetid() { 143 | local ip=$(expand_ipv6 $1) 144 | local subnet=${2:-64} 145 | local fmt="${3:-%x}" 146 | 147 | local len=$(( 64-$subnet )) 148 | 149 | if (( $len < 0 || $len > 16 )); then 150 | # Not really valid for non-route entries 151 | echo -n '-' 152 | else 153 | ip=${ip//:/} 154 | printf "$fmt" "$(( 0x${ip:14:2} & ((1<<$len)-1) ))" 155 | fi 156 | } 157 | 158 | # IPv6 host or interface part of address 159 | ipv6_interface() { 160 | local ip=$(expand_ipv6 $1) 161 | 162 | compress_ipv6 "::${ip:20}" 163 | } 164 | 165 | # split into two parts, the address and the mask 166 | ipv6_split_mask() { 167 | local ip=${1:-::/0} 168 | 169 | set ${ip/\// } 170 | 171 | echo -n $1 ${2:-128} 172 | } 173 | 174 | # a valid IPv6 in either the expanded form or the compressed one 175 | is_ipv6() { 176 | local orig=$(lowercase_ipv6 $1) 177 | local expanded="$(expand_ipv6 $orig)" 178 | [ "$orig" = "$expanded" ] && return 0 179 | local compressed="$(compress_ipv6 $expanded)" 180 | [ "$orig" = "$compressed" ] && return 0 181 | return 1 182 | } 183 | 184 | # return IPv6 address category 185 | ipv6_type() { 186 | local ip=$(lowercase_ipv6 $1) 187 | 188 | if [[ $(ipv6_prefix $ip 10) == 'fe80::' ]]; then 189 | echo -n 'Link-local' 190 | elif [[ $(ipv6_prefix $ip 10) == 'fec0::' ]]; then 191 | echo -n 'Site-local (deprecated)' 192 | elif [[ $(ipv6_prefix $ip 16) == '2002::' ]]; then 193 | echo -n '6to4' 194 | elif [[ $(ipv6_prefix $ip 16) == '3ffe::' ]]; then 195 | echo -n '6bone (returned)' 196 | elif [[ $(ipv6_prefix $ip 32) == '2001::' ]]; then 197 | echo -n 'Teredo tunneling' 198 | else 199 | case $(ipv6_prefix $ip 8) in 200 | ::) echo 'Special' ;; 201 | fc00::) echo -n 'Global ULA' ;; 202 | fd00::) echo -n 'Local ULA' ;; 203 | ff00::) echo -n 'Multicast' ;; 204 | f?00::) echo -n 'Invalid' ;; 205 | *) echo -n 'Unicast' ;; 206 | esac 207 | fi 208 | } 209 | -------------------------------------------------------------------------------- /etc/systemd/system/ddns-onboot@.service: -------------------------------------------------------------------------------- 1 | # 2 | # ddns-onboot.service 3 | # 4 | # systemd service to trigger ddns-onboot 5 | # 6 | [Unit] 7 | Description=Set addresses in Bind from interface %I 8 | After=nss-lookup.target 9 | 10 | [Service] 11 | Type=oneshot 12 | ExecStart=/etc/NetworkManager/dispatcher.d/09-ddns direct %I 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /etc/systemd/system/ddns-onboot@.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Activate DDNS after boot 3 | 4 | [Timer] 5 | OnActiveSec=45 6 | RemainAfterElapse=no 7 | 8 | [Install] 9 | WantedBy=named.service 10 | 11 | -------------------------------------------------------------------------------- /examples/complex/ddns-eth0-from-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Dynamic DNS configuration for 08-ipv6-prefix 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/08-ipv6-prefix and 5 | # /etc/nmutils/ddns-functions for docs 6 | # 7 | # FILENAME: ddns-eth0-from-eth0.conf 8 | # PATTERN: ddns--from-.conf 9 | # 10 | # So below will affect addresses assigned by the DHCP client running 11 | # on eth0 (WAN) to eth0 (INTF) 12 | # 13 | 14 | # zone to update (required) 15 | DDNS_ZONE=example.net. 16 | 17 | # resources 18 | # for prefix, only AAAA will be assigned 19 | DDNS_RREC_AAAA_NAME=www.example.net. 20 | 21 | # A values can be set when prefix assigned by using DDNS_RREC_A_VALUE 22 | DDNS_RREC_A_NAME=www.example.net. 23 | DDNS_RREC_A_VALUE=4.3.2.1 24 | 25 | DDNS_RREC_CNAME_NAME=mail.example.net. 26 | DDNS_RREC_CNAME_VALUE=www.example.net. 27 | DDNS_RREC_CNAME_FALLBACK=alt.example.net. 28 | 29 | # change default nsupdate options 30 | DDNS_NSUPDATE_OPTIONS="-v -k /etc/Kpublic.example.net.+157+{random}.private" 31 | DDNS_SERVER=2.3.4.5 32 | DDNS_TTL=300 33 | 34 | -------------------------------------------------------------------------------- /examples/complex/ddns-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for 09-ddns 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/09-ddns and /etc/nmutils/ddns-functions 5 | # for docs 6 | # 7 | # FILENAME: ddns-eth0.conf 8 | # PATTERN: ddns-.conf 9 | # 10 | # So below will affect addresses assigned to eth0 (INTF) 11 | # 12 | 13 | # zone to update (required) 14 | DDNS_ZONE=example.net. 15 | 16 | # resources 17 | DDNS_RREC_A_NAME=www.example.net. 18 | # if DDNS_RREC_A_VALUE (or AAAA) set, it's value is used instead 19 | # of address on interface 20 | #DDNS_RREC_A_VALUE=10.10.10.10 21 | # if DDNS_RREC__FALLBACK empty (default) removed when down 22 | #DDNS_RREC_A_FALLBACK= 23 | 24 | DDNS_RREC_AAAA_NAME=www.example.net. 25 | 26 | DDNS_RREC_CNAME_NAME=mail.example.net. 27 | DDNS_RREC_CNAME_VALUE=www.example.net. 28 | DDNS_RREC_CNAME_FALLBACK=alt.example.net. 29 | 30 | # change default nsupdate options 31 | DDNS_NSUPDATE_OPTIONS="-v -k /etc/Kpublic.example.net.+157+{random}.private" 32 | DDNS_SERVER=2.3.4.5 33 | DDNS_TTL=300 34 | 35 | # DDNS server is slow 36 | DDNS_DIG_TIMEOUT=15 37 | DDNS_NSUPDATE_TIMEOUT=15 38 | DDNS_FLOCK_TIMEOUT=30 39 | # use our own lockfile 40 | DDNS_LOCKFILE="/etc/nmutils/conf/ddns-eth0.conf" -------------------------------------------------------------------------------- /examples/complex/ddns-eth1-from-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Dynamic DNS configuration for 08-ipv6-prefix 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/08-ipv6-prefix and 5 | # /etc/nmutils/ddns-functions for docs 6 | # 7 | # FILENAME: ddns-eth1-from-eth0.conf 8 | # PATTERN: ddns--from-.conf 9 | # 10 | # So below will affect addresses assigned by the DHCP client running 11 | # on eth0 (WAN) to eth1 (INTF) via sub-prefix assignment 12 | # 13 | 14 | # zone to update (required) 15 | DDNS_ZONE=example.net. 16 | 17 | # resources 18 | DDNS_RREC_AAAA_NAME=int.example.net. 19 | 20 | DDNS_RREC_CNAME_NAME=ldap.example.net. 21 | DDNS_RREC_CNAME_VALUE=int.example.net. 22 | 23 | # change default nsupdate options 24 | DDNS_NSUPDATE_OPTIONS="-v -k /etc/Kinternal.example.net.+157+{random}.private" 25 | DDNS_SERVER=192.168.1.1 26 | DDNS_TTL=1200 27 | 28 | -------------------------------------------------------------------------------- /examples/complex/ddns-eth1.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for 09-ddns 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/09-ddns and /etc/nmutils/ddns-functions 5 | # for docs 6 | # 7 | # FILENAME: ddns-eth1.conf 8 | # PATTERN: ddns-.conf 9 | # 10 | # So below will affect addresses assigned to eth1 (INTF) 11 | # 12 | 13 | # zone to update (required) 14 | DDNS_ZONE=example.net. 15 | 16 | # resources 17 | DDNS_RREC_A_NAME=router.example.net. 18 | DDNS_RREC_A_FALLBACK=192.168.1.1 19 | DDNS_RREC_A_PRIVATE=1 20 | 21 | DDNS_RREC_AAAA_NAME=router.example.net. 22 | DDNS_RREC_AAAA_PRIVATE=1 23 | 24 | DDNS_RREC_CNAME_NAME=mail.example.net. 25 | DDNS_RREC_CNAME_VALUE=router.example.net. 26 | DDNS_RREC_CNAME_FALLBACK=internal.example.net. 27 | 28 | DDNS_RREC_TXT_NAME=router.example.net. 29 | DDNS_RREC_TXT_VALUE="The router is up" 30 | 31 | # change default nsupdate options 32 | DDNS_NSUPDATE_OPTIONS="-v -k /etc/Kinternal.example.net.+157+{random}.private" 33 | DDNS_SERVER=192.168.1.1 34 | DDNS_TTL=1200 35 | -------------------------------------------------------------------------------- /examples/complex/general.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Optional overrides of global values 3 | # 4 | # See /etc/nmutils/general-functions for docs 5 | # 6 | 7 | # uncomment for command line debugging 8 | nmg_log_stderr=1 9 | nmg_show_debug=1 10 | 11 | # uncomment to disable commands that change things 12 | #nmg_dryrun=0 13 | 14 | # for systems without /run 15 | RUNDIR="/tmp/nmutils" 16 | 17 | # use single lockfile for everything... (default is ddns-*.conf file) 18 | DDNS_GLOBAL_LOCKFILE="/etc/named.conf" 19 | 20 | # longer timeouts for slow name servers 21 | DDNS_GLOBAL_DIG_TIMEOUT=15 22 | DDNS_GLOBAL_NSUPDATE_TIMEOUT=15 23 | DDNS_GLOBAL_NSUPDATE_OPTIONS="-v -k /etc/Kexample.net.+157+43833.private" 24 | -------------------------------------------------------------------------------- /examples/complex/ifa50-mylogger-eth1.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Config for 96-interface-action 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/96-interface-action for docs 5 | # 6 | # FILENAME: ifa50-mylogger-eth1.conf 7 | # PATTERN: ifa##--.conf 8 | # 9 | # So config below affects the `mylogger` service based on on `eth1` interface 10 | # changes, ordered at "50" vs other interface-action configs 11 | # (ie. after ifa{49-}-xxx, before ifa{51+}-xxx) 12 | # 13 | # ignore service is-enabled flag - eg. service may be disabled at boot, 14 | # but started when interface is up 15 | IGNORE_ENABLED=1 16 | # service is started/restarted on interface up 17 | CMD_UP=restart 18 | # service is reloaded on dhcp4-change 19 | CMD_CHANGE=reload 20 | # ... also on dhcp6-change 21 | CMD_CHANGE6=reload 22 | # service is stopped on interface before interface is brought down 23 | CMD_PRE_DOWN=stop 24 | 25 | # name of state file (may be used in combination with other flags 26 | # in service conditions) 27 | STATE_FILE=/run/mylogger-flag-eth1 28 | # contents on $RESTART_UP written to $STATE_FILE on interface up 29 | RESTART_UP="eth1 up at $(date)" 30 | # $STATE_FILE removed on interface down 31 | STOP_DOWN=1 32 | 33 | -------------------------------------------------------------------------------- /examples/complex/ipv6-prefix-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # WAN configuration for 08-ipv6-prefix 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/08-ipv6-prefix for docs 5 | # 6 | # FILENAME: ipv6-prefix-eth0.conf 7 | # PATTERN: ipv6-prefix-.conf 8 | # 9 | # So below will affect configuration for eth0 (WAN) 10 | # 11 | # Add space-separated list of LAN interfaces to assign prefixes to them 12 | WAN_LAN_INTFS="eth1 eth2" 13 | 14 | # prefix-len (works on dhclient and dhcpcd) 15 | WAN_PREFIXLEN_HINT=60 16 | 17 | # uncomment to force use of dhclient 18 | #DHCPCD='' 19 | WAN_DHCLIENT_ARGS=(-H router.example.net) 20 | 21 | # debug for dhcpcd 22 | WAN_DHCPCD_ARGS=(-d) 23 | # add global config for dhcpcd 24 | #WAN_DHCPCD_PRECONFIG=/etc/nmutils/conf/dhcpcd-pre-eth0.conf 25 | # add vlan1 interface config for dhcpcd 26 | #WAN_DHCPCD_POSTCONFIG=/etc/nmutils/conf/dhcpcd-post-eth0.conf 27 | 28 | # override 2m timeout to restart dhcp client 29 | DHCP_REBIND_TIMEOUT=30 30 | 31 | # set WAN_REQUIRE_IP4=any if even a private address is enough to trigger 32 | # prefix delegation (unset means no ipv4 address is needed to start dhclient) 33 | WAN_REQUIRE_IP4=1 34 | 35 | # Static ips added when interface up 36 | WAN_STATIC_IP6="2001:db8:55::1/64, 2001:db8:100::1/64" 37 | # Static DNS entries for interface (can't be assigned in NM until 38 | # interface has an address) 39 | WAN_STATIC_DNS6="2001:db8:aaaa::1, 2001:db8:bbbb::1" 40 | # Static DNS search list 41 | WAN_STATIC_DNS6_SEARCH='home.lan, office.lan' 42 | 43 | # config for source-based default routes 44 | WAN_SADR_METRIC=100 45 | #WAN_SADR_DISABLE=1 46 | 47 | # use the 95-radvd-gen to update radvd.conf when LAN delegations change 48 | NMG_RADVD_TRIGGER="/etc/NetworkManager/dispatcher.d/95-radvd-gen" 49 | -------------------------------------------------------------------------------- /examples/complex/ipv6-prefix-eth1-from-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Optional LAN configuration for 08-ipv6-prefix 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/08-ipv6-prefix for docs 5 | # 6 | # FILENAME: ipv6-prefix-eth1-from-eth0.conf 7 | # PATTERN: ipv6-prefix--from-.conf 8 | # 9 | # So below will affect eth1 (LAN) sub-prefix allocated from delegation 10 | # given to eth0 (WAN) 11 | # 12 | # eg: with 2001:db8:3311:aa00::/56 delegation on eth0, config would assign 13 | # 2001:db8:3311:aa00::4000/62 14 | # made from 15 | # 2001:db8:3311:aa - 56-bit delegation from DHCP 16 | LAN_PREFIX_LEN=62 17 | # LAN_PREFIX_LEN /62 18 | # LAN_SITE=auto 00:: - 6-bit (LAN_PREFIX_LEN - 56) auto-allocated site 19 | LAN_NODE=4000 20 | # LAN_NODE ::4000 21 | -------------------------------------------------------------------------------- /examples/complex/ipv6-prefix-eth2-from-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Optional LAN configuration for 08-ipv6-prefix 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/08-ipv6-prefix for docs 5 | # 6 | # FILENAME: ipv6-prefix-eth2-from-eth0.conf 7 | # PATTERN: ipv6-prefix--from-.conf 8 | # 9 | # So below will affect eth2 (LAN) sub-prefix allocated from delegation 10 | # given to eth0 (WAN) 11 | # 12 | # eg: with 2001:db8:3311:aa00::/56 delegation on eth0, config would assign 13 | # 2001:db8:3311:aad0::1/60 14 | # made from 15 | # 2001:db8:3311:aa - 56-bit delegation from DHCP 16 | LAN_PREFIX_LEN=60 17 | # LAN_PREFIX_LEN /60 18 | LAN_SITE=cd 19 | # LAN_SITE d0:: - lowest 4-bits (LAN_PREFIX_LEN - 56) of "cd" 20 | LAN_NODE=1 21 | # LAN_NODE ::1 22 | -------------------------------------------------------------------------------- /examples/complex/radvd-gen.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for 95-radvd-gen 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/95-radvd-gen for docs 5 | # 6 | # Unlike other configurations in this directory, place this file in 7 | # /etc/NetworkManager 8 | # 9 | 10 | # only regen radvd.conf from template if differences are 20% (default 10%) 11 | PERDIFF=20 12 | 13 | # override conf group 14 | RADVD_GROUP=wheel 15 | 16 | # set SELinux context 17 | RESTORECON_EXE="/usr/sbin/restorecon" 18 | 19 | # default for @ROUTER_LIFETIME@ if no dynamic prefixes 20 | ROUTER_DEFAULT_LIFETIME=900 21 | # override min/max defaults for @ROUTER_LIFETIME@ 22 | ROUTER_MIN_LIFETIME=900 23 | ROUTER_MAX_LIFETIME=3600 24 | 25 | # disable router test (always succeed) 26 | DEFROUTE_TEST= 27 | -------------------------------------------------------------------------------- /examples/complex/radvd.conf.templ: -------------------------------------------------------------------------------- 1 | # 2 | # radvd template used by 95-radvd-gen 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/95-radvd-gen for docs 5 | # 6 | # Unlike other configurations in this directory, place this file in 7 | # /etc/NetworkManager 8 | # 9 | # @PREFIX@ will be replaced with any ipv6 prefixes on interface eth1 10 | # 11 | interface eth0 12 | { 13 | AdvSendAdvert on; 14 | MinRtrAdvInterval 30; 15 | # static prefix 16 | prefix fda5:0000:2bc7:203d::/64 { 17 | # no addr-gen for this prefix 18 | AdvAutonomous off; 19 | }; 20 | # delegated prefixes with fixed lifetimes 21 | @PREFIX@ { 22 | AdvAutonomous on; 23 | DecrementLifetimes on; 24 | }; 25 | # @ROUTER_LIFETIME@ replaced with from max preferred life of 26 | # any addresses found for @PREFIX@ above 27 | AdvDefaultLifetime @ROUTER_LIFETIME@; 28 | }; 29 | interface eth1 30 | { 31 | AdvSendAdvert on; 32 | MinRtrAdvInterval 30; 33 | # prefix, no options 34 | prefix fdac:3741:50f8::/48; 35 | # override lifetimes 36 | @PREFIX@ { 37 | AdvValidLifetime 14400; 38 | AdvPreferredLifetime 86400; 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /examples/simple/ifa99-rsyslog-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for 96-interface-action 3 | # 4 | # Named: ifa99-rsyslog-eth0.conf 5 | # 6 | # Pattern: ifa##--.conf 7 | # 8 | # See /etc/NetworkManager/dispatcher.d/96-interface-action for docs 9 | # 10 | 11 | # Config with restart `rsyslog` whenever the `eth0` interface is brought up, 12 | # ordered 99 (after other 96-interface-action configs) 13 | # 14 | # Useful when a UDP listener doesn't have netlink support 15 | # 16 | CMD_UP=on 17 | -------------------------------------------------------------------------------- /examples/simple/ipv6-prefix-eth0.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Configuration for 08-ipv6-prefix 3 | # 4 | # Named: ipv6-prefix-eth0.conf 5 | # 6 | # Pattern: ipv6-prefix-.conf 7 | # 8 | # Simplest setup to assign an ipv6 sub-prefix (/64 by default) to a LAN 9 | # interface (`eth1` below) from the prefix delegated to WAN 10 | # (`eth0` in the file name) 11 | # 12 | # See /etc/NetworkManager/dispatcher.d/08-ipv6-prefix for docs 13 | # 14 | 15 | # WAN_LAN_INTFS contains a space-separated list of LAN interfaces 16 | # to receive sub-prefixes 17 | WAN_LAN_INTFS="eth1" 18 | -------------------------------------------------------------------------------- /examples/simple/radvd.conf.templ: -------------------------------------------------------------------------------- 1 | # 2 | # radvd template used by 95-radvd-gen 3 | # 4 | # See /etc/NetworkManager/dispatcher.d/95-radvd-gen for docs 5 | # 6 | # Unlike other configurations in this directory, place this file in 7 | # /etc/NetworkManager 8 | # 9 | # @PREFIX@ will be replaced with any ipv6 prefixes on interface eth1 10 | # 11 | interface eth1 12 | { 13 | @PREFIX@ { 14 | DecrementLifetimes on; 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: LGPL-3.0-or-later 2 | 3 | #meson setup --prefix=/usr -Dpkg=true build 4 | #meson install --destdir=destdir -C build 5 | 6 | project('nmutils', version: '20250418', license: 'GPL-3.0-or-later', 7 | default_options: [ 'sysconfdir=/etc' ], 8 | meson_version: '>=0.62.0') # test() verbose 9 | 10 | # get options 11 | nm_prefix = get_option('prefix') 12 | nm_name = meson.project_name() 13 | nm_sysconfdir = nm_prefix / get_option('sysconfdir') 14 | nm_unitdir = get_option('unitdir') 15 | nm_rundir = get_option('runstatedir') 16 | nm_selinuxtype = get_option('selinuxtype') 17 | nm_datadir = nm_prefix / get_option('datadir') 18 | nm_nmlibdir = get_option('nmlibdir') 19 | nm_ispkg = get_option('pkg') 20 | 21 | # get auto SELinux from config 22 | seconfig = '/etc/selinux/config' 23 | semake = '/usr/share/selinux/devel/Makefile' 24 | 25 | sed_cmd = find_program('sed') 26 | fs = import('fs') 27 | 28 | if nm_nmlibdir == '' 29 | if fs.is_dir(nm_prefix / 'lib/NetworkManager/dispatcher.d') 30 | nm_nmlibdir = nm_prefix / 'lib' 31 | else 32 | nm_nmlibdir = '/usr/lib' 33 | endif 34 | endif 35 | 36 | if nm_ispkg 37 | nm_pkgconfdir = nm_sysconfdir / nm_name 38 | else 39 | nm_pkgconfdir = nm_sysconfdir / nm_name / 'conf' 40 | endif 41 | 42 | # systemd units may be disabled with -Dunitdir=no 43 | install_units = (nm_unitdir != '') 44 | if install_units and nm_unitdir == 'auto' 45 | if nm_ispkg 46 | systemd_dep = dependency('systemd', method: 'pkg-config', required: false) 47 | if systemd_dep.found() 48 | nm_unitdir = systemd_dep.get_variable(pkgconfig: 'systemdsystemunitdir') 49 | elif fs.is_dir('/usr/lib/systemd/system') 50 | nm_unitdir = '/usr/lib/systemd/system' 51 | endif 52 | elif fs.is_dir('/etc/systemd/system') 53 | nm_unitdir = '/etc/systemd/system' 54 | endif 55 | install_units = (nm_unitdir != 'auto') 56 | endif 57 | 58 | install_selinux = (nm_selinuxtype != '') 59 | if install_selinux and nm_selinuxtype == 'auto' 60 | nm_selinuxtype = '' 61 | if fs.is_file(seconfig) and fs.is_file(semake) 62 | seconfig_type = run_command(sed_cmd, '-E', '-n', 63 | '/^SELINUXTYPE=(.*)$/{s//\\1/p;q;}', seconfig, 64 | check: true) 65 | nm_selinuxtype = seconfig_type.stdout().strip() 66 | endif 67 | install_selinux = (nm_selinuxtype != '') 68 | endif 69 | 70 | if install_selinux 71 | if nm_ispkg 72 | sepkgdir = nm_datadir / 'selinux/packages' / nm_selinuxtype 73 | else 74 | sepkgdir = '/etc/selinux' / nm_selinuxtype / 'packages' 75 | endif 76 | assert(fs.is_file(semake), 77 | 'selinuxtype set, but selinux-policy-devel package not installed') 78 | endif 79 | 80 | # packages have different locations for scripts 81 | if nm_ispkg 82 | nm_pkglibdir = nm_nmlibdir / 'NetworkManager/dispatcher.d' 83 | nm_pkgdatadir = nm_datadir / nm_name 84 | else 85 | nm_pkglibdir = nm_sysconfdir / 'NetworkManager/dispatcher.d' 86 | nm_pkgdatadir = nm_sysconfdir / nm_name 87 | endif 88 | 89 | install_sets = [ 90 | [ 91 | 'etc/nmutils', 92 | nm_pkgdatadir, 93 | 'rw-r--r--', 94 | [ 95 | 'general-functions', 96 | 'ddns-functions', 97 | 'ipv6_utils.sh', 98 | 'dispatcher_action', 99 | ] 100 | ], 101 | [ 102 | 'etc/NetworkManager/dispatcher.d', 103 | nm_pkglibdir, 104 | 'rwxr-xr-x', 105 | [ 106 | '08-ipv6-prefix', 107 | '09-ddns', 108 | '90-transmission', 109 | '95-radvd-gen', 110 | '96-interface-action', 111 | ] 112 | ], 113 | ] 114 | 115 | if install_units 116 | install_sets += [ 117 | [ 118 | 'etc/systemd/system', 119 | nm_unitdir, 120 | 'rw-r--r--', 121 | [ 122 | 'ddns-onboot@.service', 123 | 'ddns-onboot@.timer', 124 | ], 125 | ], 126 | ] 127 | endif 128 | 129 | patch_cmd = [ 130 | sed_cmd, '-e', 's|/etc/nmutils|' + nm_pkgdatadir + '|g', 131 | '-e', 's|' + nm_pkgdatadir + '/conf|' + nm_pkgconfdir + '|g', 132 | '-e', 's|/etc/NetworkManager|' + nm_sysconfdir + '/NetworkManager|g', 133 | '-e', 's|' + nm_sysconfdir + '/NetworkManager/dispatcher.d|' + 134 | nm_pkglibdir + '|g', 135 | '-e', 's|' + nm_pkglibdir + '/##-ifd|' + nm_sysconfdir + 136 | '/NetworkManager/dispatcher.d/##-ifd' + '|g', 137 | '-e', 's|/run/|' + nm_rundir + '/|g', 138 | '@INPUT@', 139 | ] 140 | 141 | sepatch_cmd = [ 142 | sed_cmd, 143 | '-e', 's|^/usr/lib/|' + nm_nmlibdir + '/|g', 144 | '-e', 's|^/etc/|' + nm_sysconfdir + '/|g', 145 | '@INPUT@', 146 | ] 147 | 148 | selinux_files = [ 149 | 'nmutils.fc', 150 | 'nmutils.te', 151 | ] 152 | # 153 | # INSTALL 154 | # 155 | foreach p: install_sets 156 | foreach tgt: p[3] 157 | custom_target(command: patch_cmd, capture: true, input: p[0] / tgt, 158 | output: tgt, install: true, install_dir: p[1], 159 | install_mode: p[2], install_tag: 'base') 160 | endforeach 161 | endforeach 162 | 163 | install_emptydir(nm_pkgconfdir, install_tag: 'base') 164 | predown_symlinks = [ 165 | '08-ipv6-prefix', 166 | '96-interface-action', 167 | ] 168 | foreach p: predown_symlinks 169 | install_symlink(p, install_dir: nm_pkglibdir / 'pre-down.d', 170 | install_tag: 'base', pointing_to: '..' / p) 171 | endforeach 172 | 173 | if install_selinux 174 | 175 | sedeps = [] 176 | foreach p: selinux_files 177 | sedeps += custom_target(command: sepatch_cmd, capture: true, 178 | input: 'selinux' / p, output: p) 179 | endforeach 180 | 181 | custom_target(command: [ find_program('make'), '-f', 182 | meson.current_source_dir() / 'selinux/GNUmakefile', 183 | 'VPATH=.', 'nmutils.pp.bz2' ], 184 | depends: sedeps, output: 'nmutils.pp.bz2', install: true, 185 | install_dir: sepkgdir, install_tag: 'selinux') 186 | meson.add_install_script(find_program('semodule'), '-s', nm_selinuxtype, 187 | '-i', sepkgdir / 'nmutils.pp.bz2', 188 | install_tag: 'selinux', skip_if_destdir: true) 189 | endif 190 | 191 | # 192 | # TESTS 193 | # 194 | testdir = meson.current_source_dir() / 'test' 195 | builddir = meson.current_build_dir() 196 | 197 | test_files = [ 198 | 'general-test', 199 | 'ddns-test', 200 | 'nm-ddns-test', 201 | 'ipv6-prefix-addr-test', 202 | 'ipv6-prefix-nm-test', 203 | 'ipv6-prefix-dhclient-test', 204 | 'ipv6-prefix-dhcpcd-test', 205 | ] 206 | 207 | test_env = [ 208 | 'TEST_NMUTILS=' + builddir, 209 | 'TEST_NMDIR=' + builddir, 210 | 'TEST_OUT=' + builddir / 'results', 211 | 'TEST_RUNDIR=' + builddir / 'run/nmutils', 212 | ] 213 | 214 | foreach p: test_files 215 | test(p, find_program(p, dirs: testdir), args: [ 'strict', 'verbose' ], 216 | env: test_env, is_parallel: false, verbose: true, workdir: testdir) 217 | endforeach 218 | 219 | radvd_tests = [ 220 | 'radvd-test-1', 221 | 'radvd-test-2', 222 | 'radvd-test-3', 223 | 'radvd-test-4', 224 | ] 225 | 226 | make_cmd = find_program('make') 227 | foreach p: radvd_tests 228 | test(p, make_cmd, args: [ p ], env: test_env, is_parallel: false, 229 | workdir: testdir) 230 | endforeach 231 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('unitdir', type: 'string', value: 'auto', 2 | description: 'Directory for systemd service files, or "" to disable') 3 | option('runstatedir', type: 'string', value: '/run', 4 | description: 'Directory for transient runtime state') 5 | option('selinuxtype', type: 'string', value: 'auto', 6 | description: 'SELinux policy type (eg. targeted,mls...), or "" to disable') 7 | option('nmlibdir', type: 'string', value: '/usr/lib', 8 | description: 'NetworkManager system libdir') 9 | option('pkg', type: 'boolean', value: false, 10 | description: 'Patch paths for packaged install') 11 | -------------------------------------------------------------------------------- /nmutils.spec: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-3.0-or-later 2 | # 3 | # Copyright (C) 2022-24 Scott Shambarger, Kenneth Porter 4 | # 5 | %bcond_without selinux 6 | %bcond_with test 7 | 8 | %global selinuxtype targeted 9 | 10 | %if 0%{?srpm} 11 | %undefine dist 12 | %endif 13 | 14 | Name: nmutils 15 | Version: devel 16 | Release: 1%{?dist} 17 | Summary: Network Manager Utility Scripts 18 | BuildArch: noarch 19 | 20 | License: GPL-3.0-or-later AND LGPL-3.0-or-later 21 | URL: https://github.com/sshambar/nmutils 22 | Source0: https://github.com/sshambar/nmutils/archive/%{version}/%{name}-%{version}.tar.gz 23 | Requires: NetworkManager 24 | Requires: iproute 25 | Requires: procps-ng 26 | Requires: systemd >= 236 27 | Recommends: bind-utils 28 | Recommends: util-linux-core 29 | Recommends: ndisc6 30 | Recommends: dhcp-client 31 | Suggests: dhcpcd 32 | Suggests: radvd 33 | BuildRequires: make 34 | BuildRequires: meson 35 | %if %{with selinux} 36 | Requires: (%{name}-selinux if selinux-policy-%{selinuxtype}) 37 | %endif 38 | BuildRequires: systemd-rpm-macros 39 | %{?systemd_ordering} 40 | 41 | %description 42 | A collection of BASH based utility scripts and support functions for 43 | use with Gnome's NetworkManager dispatcher. 44 | 45 | %if %{with selinux} 46 | %package selinux 47 | Summary: Selinux policy module 48 | BuildArch: noarch 49 | License: GPL-3.0-or-later 50 | BuildRequires: make 51 | BuildRequires: bzip2 52 | BuildRequires: selinux-policy-devel 53 | %{?selinux_requires} 54 | Requires: selinux-policy-%{selinuxtype} 55 | Requires(post): selinux-policy-%{selinuxtype} 56 | 57 | %description selinux 58 | Install nmutils-selinux to ensure your system contains the SELinux policy 59 | required for dhcp clients to run 08-ipv6-prefix, manage radvd and 60 | perform DDNS operations. 61 | %endif 62 | 63 | %prep 64 | %autosetup 65 | 66 | %build 67 | %meson \ 68 | -Dselinuxtype=%{?with_selinux:%{selinuxtype}} \ 69 | -Dunitdir=%{_unitdir} \ 70 | -Dnmlibdir=/usr/lib \ 71 | -Drunstatedir=%{_runstatedir} \ 72 | -Dpkg=true 73 | 74 | %meson_build 75 | 76 | %check 77 | %if %{with test} 78 | %meson_test 79 | %endif 80 | 81 | %install 82 | %meson_install \ 83 | --tags base 84 | 85 | %if %{with selinux} 86 | %meson_install \ 87 | --tags selinux 88 | %endif 89 | 90 | %preun 91 | if [[ $1 -eq 0 ]] && command -v systemctl >/dev/null; then 92 | # Package removal, not upgrade 93 | # disable service/timers (systemd macros don't handled templated units well) 94 | if systemctl is-enabled ddns-onboot@.service ddns-onboot@.timer >/dev/null; then 95 | systemctl --no-reload disable ddns-onboot@ 96 | fi 97 | fi 98 | 99 | %files 100 | %license LICENSE 101 | %license LICENSE.LGPLv3 102 | %doc README.md examples 103 | %{_prefix}/lib/NetworkManager/dispatcher.d/* 104 | %{_datadir}/nmutils 105 | %config %{_sysconfdir}/nmutils 106 | %{_unitdir}/* 107 | 108 | %if %{with selinux} 109 | 110 | %pre selinux 111 | %selinux_relabel_pre -s %{selinuxtype} 112 | 113 | %post selinux 114 | %selinux_modules_install -s %{selinuxtype} %{_datadir}/selinux/packages/%{selinuxtype}/nmutils.pp.bz2 || : 115 | 116 | %postun selinux 117 | if [ $1 -eq 0 ]; then 118 | # Package removal, not upgrade 119 | %selinux_modules_uninstall -s %{selinuxtype} nmutils || : 120 | fi 121 | 122 | %posttrans selinux 123 | %selinux_relabel_post -s %{selinuxtype} || : 124 | 125 | %files selinux 126 | %license LICENSE 127 | %attr(0644,root,root) %{_datadir}/selinux/packages/%{selinuxtype}/nmutils.pp.bz2 128 | %ghost %verify(not md5 size mode mtime) %{_sharedstatedir}/selinux/%{selinuxtype}/active/modules/200/nmutils 129 | %endif 130 | 131 | %changelog 132 | * Fri Apr 18 2025 Scott Shambarger 20250418-1 133 | - Added config command to 08-ipv6-prefix and 09-ddns with supporting 134 | functions 135 | 136 | * Mon Dec 16 2024 Scott Shambarger 20241216-1 137 | - Release 20241216 138 | 139 | * Thu Nov 28 2024 Scott Shambarger 20241126-1 140 | - Updated for meson build 141 | - Config for package now in /etc/nmutils 142 | 143 | * Tue Jun 21 2022 Scott Shambarger 20220621-1 144 | - Moved script libraries to datadir, handle instanced systemd files 145 | - Added SELinux subpackage 146 | 147 | * Mon May 16 2022 Kenneth Porter 20220516-1 148 | - Initial spec file 149 | -------------------------------------------------------------------------------- /selinux/GNUmakefile: -------------------------------------------------------------------------------- 1 | 2 | .SUFFIXES: 3 | .SUFFIXES: .pp .bz2 4 | 5 | V := 0 6 | VB_0 := @ 7 | VB := $(VB_$(V)) 8 | SELINUX_MAKE := /usr/share/selinux/devel/Makefile 9 | 10 | all: nmutils.pp.bz2 11 | 12 | nmutils.pp.bz2: selinux-devel nmutils.pp 13 | $(VB)rm -f "$@" 14 | bzip2 -k -9 nmutils.pp 15 | 16 | .PHONY: selinux-devel 17 | selinux-devel: 18 | $(VB)[ -f "$(SELINUX_MAKE)" ] || { \ 19 | echo "Install selinux-policy-devel before compiling policy"; \ 20 | false; } 21 | 22 | local_clean: 23 | $(VB)rm -f nmutils.pp.bz2 nmutils.if 24 | 25 | # clean defined in include, add dep 26 | clean: local_clean 27 | 28 | -include $(SELINUX_MAKE) 29 | -------------------------------------------------------------------------------- /selinux/nmutils.fc: -------------------------------------------------------------------------------- 1 | /run/nmutils/dhclient.* -- gen_context(system_u:object_r:dhcpc_var_run_t,s0) 2 | /run/nmutils/.* -- gen_context(system_u:object_r:initrc_var_run_t,s0) 3 | /etc/NetworkManager/dispatcher\.d/[0-9][0-9]-ifd-.* -- gen_context(system_u:object_r:nmutils_exec_t,s0) 4 | /etc/NetworkManager/dispatcher\.d/08-ipv6-prefix -- gen_context(system_u:object_r:nmutils_exec_t,s0) 5 | /etc/NetworkManager/dispatcher\.d/09-ddns -- gen_context(system_u:object_r:nmutils_exec_t,s0) 6 | /etc/NetworkManager/dispatcher\.d/90-transmission -- gen_context(system_u:object_r:nmutils_exec_t,s0) 7 | /etc/NetworkManager/dispatcher\.d/95-radvd-gen -- gen_context(system_u:object_r:nmutils_exec_t,s0) 8 | /etc/NetworkManager/dispatcher\.d/96-interfac(e)-action -- gen_context(system_u:object_r:nmutils_exec_t,s0) 9 | /usr/lib/NetworkManager/dispatcher\.d/08-ipv6-prefix -- gen_context(system_u:object_r:nmutils_exec_t,s0) 10 | /usr/lib/NetworkManager/dispatcher\.d/09-ddns -- gen_context(system_u:object_r:nmutils_exec_t,s0) 11 | /usr/lib/NetworkManager/dispatcher\.d/90-transmission -- gen_context(system_u:object_r:nmutils_exec_t,s0) 12 | /usr/lib/NetworkManager/dispatcher\.d/95-radvd-gen -- gen_context(system_u:object_r:nmutils_exec_t,s0) 13 | /usr/lib/NetworkManager/dispatcher\.d/96-interfac(e)-action -- gen_context(system_u:object_r:nmutils_exec_t,s0) 14 | -------------------------------------------------------------------------------- /selinux/nmutils.te: -------------------------------------------------------------------------------- 1 | policy_module(nmutils, 0.2.0) 2 | 3 | # 4 | # Policy labels nmutils files in dispatcher.d as nmutils_exec_t (see .fc file) 5 | # and then defines domain transition so scripts are run in the initrc_t domain 6 | # 7 | require { 8 | type dhcpc_t; 9 | type NetworkManager_etc_t; 10 | type NetworkManager_initrc_exec_t; 11 | } 12 | 13 | # define entry point, useable by NetworkManager and init scripts 14 | type nmutils_exec_t; 15 | init_script_file(nmutils_exec_t) 16 | 17 | # Required for dhclient to execute 08-ipv6-prefix 18 | search_dirs_pattern(dhcpc_t, NetworkManager_etc_t, NetworkManager_initrc_exec_t); 19 | domtrans_pattern(dhcpc_t, nmutils_exec_t, initrc_t) 20 | 21 | # required for newer NetworkManager 22 | optional { 23 | require { 24 | type NetworkManager_dispatcher_t; 25 | type NetworkManager_dispatcher_script_t; 26 | } 27 | domtrans_pattern(NetworkManager_dispatcher_t, nmutils_exec_t, initrc_t) 28 | search_dirs_pattern(dhcpc_t, NetworkManager_etc_t, NetworkManager_dispatcher_script_t); 29 | } 30 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Tests for nmutils 3 | # 4 | # make help for instructions 5 | # 6 | .EXPORT_ALL_VARIABLES: 7 | .SUFFIXES: 8 | 9 | V := 0 10 | VB_0 := @ 11 | VB := $(VB_$(V)) 12 | 13 | # command progress to stderr 14 | #VERBOSE := 1 15 | # logs of debug output to stderr 16 | #DEBUG := 2 17 | # test args 18 | TEST_ARGS := strict verbose 19 | 20 | TEST_ROOT := . 21 | TEST_BIN := $(TEST_ROOT)/bin 22 | TEST_SHELL := bash 23 | 24 | # variables used in configuration files (exported) 25 | 26 | # directory with source files 27 | SRC_ROOT := .. 28 | # test configuration files 29 | TEST_CONF := $(TEST_ROOT)/conf 30 | # common test configuration 31 | TEST_COMMON := $(TEST_CONF)/common.conf 32 | # directory for generated results 33 | TEST_OUT := $(TEST_ROOT)/results 34 | # directory for expected results 35 | TEST_EXPECT := $(TEST_ROOT)/expected 36 | 37 | # default addrs for ip-mock 38 | IP_MOCK_ADDRS := $(TEST_CONF)/ip-mock-addrs 39 | 40 | .PHONY: Makefile all 41 | all: general ddns prefix all-radvd 42 | 43 | .PHONY: help 44 | help: 45 | @echo "make targets:" 46 | @echo " all - run all tests" 47 | @echo " general - run general-funcions tests" 48 | @echo " ddns - run ddns-funcions tests" 49 | @echo " prefix - run ipv6 prefix tests" 50 | @echo " all-radvd - run all radvd tests" 51 | @echo " radvd-test-# - run radvd test # (1, 2, etc)" 52 | @echo " bless-radvd-test - replace a test results (prompts for #)" 53 | 54 | .PHONY: general 55 | general: 56 | $(TEST_SHELL) $(TEST_ROOT)/general-test $(TEST_ARGS) 57 | 58 | .PHONY: ddns 59 | ddns: 60 | $(TEST_SHELL) $(TEST_ROOT)/ddns-test $(TEST_ARGS) 61 | $(TEST_SHELL) $(TEST_ROOT)/nm-ddns-test $(TEST_ARGS) 62 | 63 | .PHONY: prefix 64 | prefix: 65 | $(TEST_SHELL) $(TEST_ROOT)/ipv6-prefix-addr-test $(TEST_ARGS) 66 | $(TEST_SHELL) $(TEST_ROOT)/ipv6-prefix-nm-test $(TEST_ARGS) 67 | $(TEST_SHELL) $(TEST_ROOT)/ipv6-prefix-dhclient-test $(TEST_ARGS) 68 | $(TEST_SHELL) $(TEST_ROOT)/ipv6-prefix-dhcpcd-test $(TEST_ARGS) 69 | 70 | .PHONY: all-radvd 71 | all-radvd: radvd-test-1 radvd-test-2 radvd-test-3 radvd-test-4 72 | 73 | $(TEST_OUT): 74 | $(VB)mkdir "$(TEST_OUT)" 75 | 76 | # 77 | # 95-radvd-gen tests 78 | # 79 | # For each test #: 80 | # 81 | # Config is CONF/radvd-gen.conf - which in turn sets: 82 | # SRC = CONF/#-radvd.conf.templ 83 | # DST = OUT/#-radvd.conf 84 | # IP_MOCK_ADDRS = CONF/#-ip-mock-addrs (if present) 85 | # 86 | # The output is then compared with EXPECT/#-radvd.conf 87 | # 88 | RADVDGEN_CONF := $(TEST_CONF)/radvd-gen.conf 89 | 90 | $(TEST_OUT)/%-radvd.conf: $(TEST_EXPECT)/%-radvd.conf 91 | @echo "Running radvd-gen test $(TEST_NUM)" 92 | $(VB)[ -f "$(TEST_CONF)/$(TEST_NUM)-radvd.conf" ] && \ 93 | cp "$(TEST_CONF)/$(TEST_NUM)-radvd.conf" "$@" || : 94 | $(VB)VERBOSE=$(VERBOSE) $(TEST_SHELL) $(TEST_ROOT)/radvd-test 95 | $(VB)diff >/dev/null "$<" "$@" || { \ 96 | echo "FAIL: $< $@ differ:"; diff "$<" "$@"; } 97 | 98 | radvd-test-%: $(TEST_EXPECT)/%-radvd.conf $(TEST_OUT) 99 | -$(VB)rm -f "$(TEST_OUT)/$*-radvd.conf" 100 | $(VB)$(MAKE) "$(TEST_OUT)/$*-radvd.conf" TEST_NUM="$*" 101 | 102 | .PHONY: bless-radvd-test 103 | bless-radvd-test: 104 | @echo "WARNING: this will overwrite the expected for this test!" 105 | $(VB)read -p "Test # to replace (empty to quit): " TEST_NUM && \ 106 | [ -n "$$TEST_NUM" ] && export TEST_NUM && \ 107 | $(TEST_SHELL) $(TEST_ROOT)/radvd-test && \ 108 | cp -f "$(TEST_OUT)/$${TEST_NUM}-radvd.conf" $(TEST_EXPECT) 109 | 110 | .PHONY: clean 111 | clean: 112 | $(VB)rm -rf $(TEST_OUT) ./run 113 | -------------------------------------------------------------------------------- /test/bin/dhclient-mock: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # -*- mode: sh; sh-basic-offset: 2; indent-tabs-mode: nil; -*- 3 | # vim:set ft=sh et sw=2 ts=2: 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | # 6 | # dhclient-mock for test scripts 7 | # 8 | # Set the following in your ipv6-prefix-wan-${interface}.conf file to test: 9 | # 10 | #if [ "$reason" = "BOUND6" ]; then 11 | # old_ip6_prefix= 12 | # new_ip6_prefix= 13 | # new_max_life=100 14 | #elif [ "$reason" = "STOP6" ]; then 15 | # old_ip6_prefix= 16 | #fi 17 | 18 | unset IFS 19 | 20 | err() { 21 | printf >&2 "%s\n" "$*" 22 | } 23 | 24 | fail() { 25 | local -i rc=0 26 | [[ $1 != 0 ]] && { printf 2>/dev/null -v rc "%d" "$1" || rc=1; } 27 | shift 28 | [[ $1 ]] && { if [[ $rc == 0 ]]; then echo "$@"; else err "$@"; fi; } 29 | exit "$rc" 30 | } 31 | 32 | [[ ${DHCLIENT_MOCK_FAIL-} || ${DHCLIENT_MOCK_OUTPUT-} ]] && { 33 | args='' arg=''; for arg in "$@"; do args+="${args:+ }'${arg}'"; done 34 | fail "${DHCLIENT_MOCK_FAIL:-0}" \ 35 | "${DHCLIENT_MOCK_OUTPUT+${DHCLIENT_MOCK_OUTPUT/@ARGS@/${args}}}" 36 | } 37 | 38 | SCRIPT='' PID='' CMD='' 39 | 40 | parse_args() { 41 | local arg nextarg='' 42 | 43 | for arg in "$@"; do 44 | 45 | [[ ${nextarg} ]] && { 46 | case ${nextarg} in 47 | script) SCRIPT=${arg} ;; 48 | pid) PID=${arg} ;; 49 | esac 50 | nextarg='' 51 | continue 52 | } 53 | 54 | case ${arg} in 55 | -sf) nextarg=script ;; 56 | -pf) nextarg=pid ;; 57 | *) CMD=${arg}; break ;; 58 | esac 59 | done 60 | return 0 61 | } 62 | 63 | usage() { 64 | err "Usage: ${0##*/} [ -sf