dotfiles

Alpine Linux dotfiles

git clone git://git.lin.moe/dotfiles.git

  1#!/bin/sh
  2
  3# -*- mode: sh; sh-indentation: 2; sh-basic-offset: 2; indent-tabs-mode: nil; fill-column: 100; coding: utf-8-unix; -*-
  4#
  5# SPDX-License-Identifier: GPL-2.0
  6#
  7# Copyright (C) 2015-2020 Jason A. Donenfeld <Jason at zx2c4.com>. All Rights Reserved.
  8# Austere posix/embedded variant derived by Rowan Thorpe <rowan at rowanthorpe.com>, 2021.
  9#
 10# original patch : https://lists.zx2c4.com/pipermail/wireguard/2021-June/006924.html
 11#
 12# Sanity checked with:
 13#  + shellcheck --check-sourced --external-sources --enable=all --shell=sh posix.sh
 14#  + shfmt -d -ln posix -i 2 -ci posix.sh
 15#
 16# TODO:
 17#  * Create local-vars drop-in functionality (without exploding complexity) to
 18#    ensure recursion doesn't shadow vars.
 19
 20set -e
 21
 22## setup needed before function-definitions
 23
 24trap - EXIT
 25trap 'exit 1' HUP INT QUIT TERM
 26
 27# primitive exit-trap stack to keep things manageable
 28exit_trap() {
 29	case "${1}" in
 30		push) EXIT_TRAP="${2}${EXIT_TRAP:+${NL}${EXIT_TRAP}}" ;;
 31		pop) EXIT_TRAP="$(printf '%s\n' "${EXIT_TRAP}" | tail -n +2)" ;;
 32		*) exit 1 ;;
 33	esac
 34	#shellcheck disable=SC2064
 35	trap "${EXIT_TRAP:--}" EXIT
 36}
 37
 38# embedded systems without char-classes in "tr" need monkeypatching
 39if ! [ "$(printf 'aBcD' | tr '[:upper:]' '[:lower:]')" = 'abcd' ]; then
 40	REAL_TR="$(command -v tr 2>/dev/null)"
 41	tr() {
 42		args=''
 43		while [ "${#}" -ne 0 ]; do
 44			case "${1}" in
 45				'[:upper:]')
 46					args="${args:+${args} }$(entity_save '[A-Z]')"
 47					;;
 48				'[:lower:]')
 49					args="${args:+${args} }$(entity_save '[a-z]')"
 50					;;
 51				*)
 52					args="${args:+${args} }$(entity_save "${1}")"
 53					;;
 54			esac
 55			shift
 56		done
 57		eval "${REAL_TR} ${args}"
 58		unset args
 59	}
 60fi
 61
 62# POSIX shells _may_ not have "type -p" so we need this drop-in
 63#shellcheck disable=SC2039
 64if [ -n "$(type -p cat 2>/dev/null || :)" ]; then
 65	type_p() {
 66		type -p "${@}"
 67	}
 68else
 69	type_p() {
 70		ret=0
 71		for arg; do
 72			found=0
 73			for path in $(printf %s "${PATH-}" | tr ':' ' '); do
 74				if [ -x "${path}/${arg}" ]; then
 75					found=1
 76					break
 77				fi
 78			done
 79			if [ "${found}" -eq 1 ]; then
 80				printf '%s/%s' "${path}" "${arg}"
 81			else
 82				ret=1
 83			fi
 84		done
 85		unset arg found path
 86		if [ "${ret}" -eq 0 ]; then
 87			unset ret
 88			return 0
 89		else
 90			unset ret
 91			return 1
 92		fi
 93	}
 94fi
 95
 96# embedded systems without "stat" need this drop-in
 97if command -v stat >/dev/null 2>&1; then
 98	stat_octal() {
 99		stat -c '%04a' "${@}"
100	}
101else
102	stat_octal() {
103		#shellcheck disable=SC2012 disable=SC2034
104		ls -l "${@}" |
105			sed -ne '
106        s/^[-dsbclp]\([-r]\)\([-w]\)\([-xsStT]\)\([-r]\)\([-w]\)\([-xsStT]\)\([-r]\)\([-w]\)\([-xsStT]\).*$/\1 \2 \3 \4 \5 \6 \7 \8 \9/g
107        t P
108        b
109        : P
110        p
111      ' |
112			while read -r ur uw ux gr gw gx or ow ox; do
113				out=''
114				spc_sum=0
115				for ctg in u g o; do
116					sum=0
117					for perm in r w x; do
118						var="${ctg}${perm}"
119						eval "val=\"\${${var}}\""
120						#shellcheck disable=SC2154
121						case "${val}" in
122							r) exp=2 ;;
123							w) exp=1 ;;
124							s | t | x) exp=0 ;;
125							- | S | T) exp=-1 ;;
126							*) exit 1 ;;
127						esac
128						case "${val}" in
129							- | w | r | x)
130								spc_exp=-1
131								;;
132							S | s)
133								case "${var}" in
134									u*) spc_exp=2 ;;
135									g*) spc_exp=1 ;;
136									*) exit 1 ;;
137								esac
138								;;
139							T | t)
140								case "${var}" in
141									o*) spc_exp=0 ;;
142									*) exit 1 ;;
143								esac
144								;;
145							*)
146								exit 1
147								;;
148						esac
149						[ "${exp}" -lt 0 ] ||
150							sum=$((sum + $((1 << exp))))
151						[ "${spc_exp}" -lt 0 ] ||
152							spc_sum=$((spc_sum + $((1 << spc_exp))))
153					done
154					out="${out}$(printf %o "${sum}")"
155				done
156				printf '%o%s\n' "${spc_sum}" "${out}"
157			done
158		unset ur uw ux gr gw gx or ow ox ctg spc_sum out perm sum var val exp spc_exp
159	}
160fi
161
162##
163
164e_body_save() { sed -e "s/'/'\\\\''/g"; }
165
166e_head_save() { sed -e "1s/^/'/"; }
167
168e_tail_save() { sed -e "\$s/\$/'/"; }
169
170e_save() { e_body_save | e_head_save | e_tail_save; }
171
172a_e_wrap() { sed -e '$s/$/ \\/'; }
173
174a_wrap() { sed -e '$s/$/\n /'; }
175
176entity_save() { printf '%s\n' "${1}" | e_save; }
177
178array_save() {
179	for i; do
180		entity_save "${i}" | a_e_wrap
181	done |
182		a_wrap
183	unset i
184}
185
186array_append() {
187	orig_name="${1}"
188	shift
189	new=$(array_save "${@}")
190	eval "
191    eval \"set -- \${${orig_name}}\"
192    set -- \"\${@}\" ${new}
193    ${orig_name}=\$(array_save \"\${@}\")
194  "
195	unset orig_name new
196}
197
198get_mtu() {
199	output="${1}"
200	existing_mtu="${2}"
201	shift 2
202	mtu_match=''
203	dev_match=''
204	mtu_match="$(printf %s "${output}" | sed -ne 's:^.*\<mtu \([0-9]\+\)\>.*$:\1:; t P; b; : P; p; q')"
205	if [ -z "${mtu_match}" ]; then
206		dev_match="$(printf %s "${output}" | sed -ne 's:^.*\<dev \([^]\+\)\>.*$:\1:; t P; b; : P; p; q')"
207		[ -z "${dev_match}" ] ||
208			mtu_match="$(ip link show dev "${dev_match}" | sed -ne 's:^.*\<mtu \([0-9]\+\)\>.*$:\1:; t P; b; : P; p; q')"
209	fi
210	if [ -n "${mtu_match}" ] &&
211		   [ "${mtu_match}" -gt "${existing_mtu}" ]; then
212		printf %s "${mtu_match}"
213	else
214		printf %s "${existing_mtu}"
215	fi
216	unset output existing_mtu mtu_match dev_match
217}
218
219##
220
221cmd() {
222	printf '[#] %s\n' "${*}" >&2
223	"${@}"
224}
225
226die() {
227	printf '%s: %s\n' "${PROGRAM}" "${*}" >&2
228	exit 1
229}
230
231parse_options() {
232	interface_section=0
233	line=''
234	key=''
235	value=''
236	stripped=''
237	v=''
238	header_line=0
239	CONFIG_FILE="${1}"
240	#shellcheck disable=SC2003
241	! expr match "${CONFIG_FILE}" '[a-zA-Z0-9_=+.-]\{1,15\}$' >/dev/null ||
242		CONFIG_FILE="${CONFIG_FILE_BASE}/${CONFIG_FILE}.conf"
243	[ -e "${CONFIG_FILE}" ] ||
244		die "\`${CONFIG_FILE}' does not exist"
245	#shellcheck disable=SC2003
246	expr match "${CONFIG_FILE}" '\(.*/\)\?\([a-zA-Z0-9_=+.-]\{1,15\}\)\.conf$' >/dev/null ||
247		die 'The config file must be a valid interface name, followed by .conf'
248	CONFIG_FILE="$(readlink -f "${CONFIG_FILE}")"
249	if {
250		stat_octal "${CONFIG_FILE}" || :
251		stat_octal "$(printf %s "${CONFIG_FILE}" | sed -e 's:/[^/]*$::')" || :
252	} 2>/dev/null | grep -vq '0$'; then
253		printf 'Warning: `%s'\'' is world accessible\n' "${CONFIG_FILE}" >&2
254	fi
255	INTERFACE="$(printf %s "${CONFIG_FILE}" | sed -e 's:^\(.*/\)\?\([^/.]\+\)\.conf$:\2:')"
256	while read -r line || [ -n "${line}" ]; do
257		stripped="$(printf %s "${line}" | sed -e 's:#.*$::; /^[[:blank:]]*$/d')"
258		key="$(printf %s "${stripped}" | sed -e 's#^[[:blank:]]*\([^=[:blank:]]\+\)[[:blank:]]*=.*$#\1#')"
259		case "${key}" in
260			'['*)
261				if [ "${key}" = '[Interface]' ]; then
262					interface_section=1
263				else
264					interface_section=0
265				fi
266				header_line=1
267				;;
268			*)
269				header_line=0
270				;;
271		esac
272		if [ "${header_line}" -eq 0 ] && [ "${interface_section}" -eq 1 ]; then
273			value="$(
274        printf %s "${stripped}" |
275          sed -e 's#^[^=]\+=[[:blank:]]*\([^[:blank:]]\(.*[^[:blank:]]\)\?\)\?[[:blank:]]*$#\1#'
276      )"
277			case "$(printf %s "${key}" | tr '[:upper:]' '[:lower:]')" in
278				address)
279					#shellcheck disable=SC2046
280					array_append ADDRESSES $(printf %s "${value}" | tr ',' ' ')
281					continue
282					;;
283				mtu)
284					MTU="${value}"
285					continue
286					;;
287				dns)
288					for v in $(printf %s "${value}" | tr ',' ' '); do
289						#shellcheck disable=SC2003
290						if expr match "${v}" '[0-9.]\+$' >/dev/null || expr match "${v}" '.*:.*$' >/dev/null; then
291							array_append DNS "${v}"
292						else
293							array_append DNS_SEARCH "${v}"
294						fi
295					done
296					continue
297					;;
298				table)
299					TABLE="${value}"
300					continue
301					;;
302				preup)
303					array_append PRE_UP "${value}"
304					continue
305					;;
306				predown)
307					array_append PRE_DOWN "${value}"
308					continue
309					;;
310				postup)
311					array_append POST_UP "${value}"
312					continue
313					;;
314				postdown)
315					array_append POST_DOWN "${value}"
316					continue
317					;;
318				saveconfig)
319					read_bool SAVE_CONFIG "${value}"
320					continue
321					;;
322				*)
323					:
324					;;
325			esac
326		fi
327		WG_CONFIG="${WG_CONFIG:+${WG_CONFIG}${NL}}${line}"
328	done <"${CONFIG_FILE}"
329	unset interface_section line key value stripped v header_line
330}
331
332read_bool() {
333	case "${2}" in
334		true) eval "${1}=1" ;;
335		false) eval "${1}=0" ;;
336		*) die "\`${2}' is neither true nor false" ;;
337	esac
338}
339
340#shellcheck disable=SC2120
341auto_su() {
342	: ${SUDO:="doas"}
343	if [ "${UID}" -ne 0 ]; then
344		eval "set -- ${ARGS}"
345		exec $SUDO "/bin/sh" -- "${SELF}" "${@}"
346	fi
347}
348
349add_if() {
350	ret=0
351	if ! cmd ip link add "${INTERFACE}" type wireguard; then
352		ret=${?}
353		! [ -e /sys/module/wireguard ] && command -v "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" >/dev/null ||
354				exit "${ret}"
355		printf '[!] Missing WireGuard kernel module. Falling back to slow userspace implementation.\n' >&2
356		cmd "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" "${INTERFACE}"
357	fi
358	unset ret
359}
360
361del_if() {
362	table=''
363	[ "${HAVE_SET_DNS-0}" -eq 0 ] || unset_dns
364	[ "${HAVE_SET_FIREWALL-0}" -eq 0 ] || remove_firewall
365	#shellcheck disable=SC2003
366	if [ -z "${TABLE}" ] ||
367		   [ "x${TABLE}" = 'xauto' ] &&
368			   get_fwmark table &&
369			   expr match "$(wg show "${INTERFACE}" allowed-ips)" '.*/0\(.*\|'"${NL}"'.*\)\?$' >/dev/null; then
370		for proto in -4 -6; do
371			while :; do
372				case "$(ip "${proto}" rule show 2>/dev/null)" in
373					*"lookup ${table}"*)
374						cmd ip "${proto}" rule delete table "${table}"
375						;;
376					*)
377						break
378						;;
379				esac
380			done
381			while :; do
382				case "$(ip "${proto}" rule show 2>/dev/null)" in
383					*"from all lookup main suppress_prefixlength 0"*)
384						cmd ip "${proto}" rule delete table main suppress_prefixlength 0
385						;;
386					*)
387						break
388						;;
389				esac
390			done
391		done
392		unset proto
393	fi
394	cmd ip link delete dev "${INTERFACE}"
395unset table
396}
397
398add_addr() {
399	case "${1}" in
400		*:*) proto=-6 ;;
401		*) proto=-4 ;;
402	esac
403	cmd ip "${proto}" address add "${1}" dev "${INTERFACE}"
404	unset proto
405}
406
407set_mtu_up() {
408	mtu=0
409	endpoint=''
410	v6_addr=''
411	if [ -n "${MTU}" ]; then
412		cmd ip link set mtu "${MTU}" up dev "${INTERFACE}"
413	else
414		wg show "${INTERFACE}" endpoints | {
415			while read -r _ endpoint; do
416				v6_addr="$(
417          printf %s "${endpoint}" |
418            sed -ne '
419              s%^\[\([a-z0-9:.]\+\)\]:[0-9]\+$%\1%
420              t P
421              s%^\([a-z0-9:.]\+\):[0-9]\+$%\1%
422              t P
423              b
424              : P
425              p
426            '
427        )"
428				[ -z "${v6_addr}" ] ||
429					mtu="$(get_mtu "$(ip route get "${v6_addr}" || :)" "${mtu}")"
430			done
431			[ "${mtu}" -gt 0 ] ||
432				mtu="$(get_mtu "$(ip route show default || :)" "${mtu}")"
433			[ "${mtu}" -gt 0 ] || mtu=1500
434			cmd ip link set mtu $((mtu - 80)) up dev "${INTERFACE}"
435		}
436	fi
437	unset mtu endpoint v6_addr
438}
439
440resolvconf_iface_prefix() {
441	if ! [ -f /etc/resolvconf/interface-order ]; then
442		iface=''
443		while read -r iface; do
444			#shellcheck disable=SC2003
445			expr match "${iface}" '\([A-Za-z0-9-]\+\)\*$' >/dev/null ||
446				continue
447			printf '%s\n' "${iface}" |
448				sed -e 's/\*\?$/./'
449			break
450		done </etc/resolvconf/interface-order
451		unset iface
452	fi
453}
454
455#shellcheck disable=SC2120
456set_dns() {
457	eval "set -- ${DNS}"
458	if [ ${#} -gt 0 ]; then
459		{
460			printf 'nameserver %s\n' "${@}"
461			eval "set -- ${DNS_SEARCH}"
462			[ ${#} -eq 0 ] ||
463				printf 'search %s\n' "${*}"
464		} | cmd resolvconf -a "$(resolvconf_iface_prefix)${INTERFACE}" -m 0 -x
465		HAVE_SET_DNS=1
466	fi
467}
468
469unset_dns() {
470	eval "set -- ${DNS}"
471	[ ${#} -eq 0 ] ||
472		cmd resolvconf -d "$(resolvconf_iface_prefix)${INTERFACE}" -f
473}
474
475add_route() {
476	case "${1}" in
477		*:*) proto=-6 ;;
478		*) proto=-4 ;;
479	esac
480	if [ "${TABLE}" != off ]; then
481		case "${TABLE}:${1}" in
482			auto:*)
483				cmd ip "${proto}" route add "${1}" dev "${INTERFACE}" table "${TABLE}"
484				;;
485			*:*/0)
486				add_default "${1}"
487				;;
488			*)
489				[ -n "$(ip "${proto}" route show dev "${INTERFACE}" match "${1}" 2>/dev/null)" ] ||
490					cmd ip "${proto}" route add "${1}" dev "${INTERFACE}"
491				;;
492		esac
493	fi
494	unset proto
495}
496
497get_fwmark() {
498	fwmark="$(wg show "${INTERFACE}" fwmark)" &&
499		[ -n "${fwmark}" ] &&
500		[ "x${fwmark}" != 'xoff' ] ||
501			return 1
502	eval "${1}=${fwmark}"
503	unset fwmark
504}
505
506remove_firewall() {
507	if type_p nft >/dev/null; then
508		table=''
509		nftcmd=''
510		nft list tables 2>/dev/null | {
511			while read -r table; do
512				case "${table}" in
513					*" wg-quick-${INTERFACE}")
514						nftcmd="${nftcmd:+${nftcmd}${NL}}delete ${table}"
515						;;
516					*)
517						:
518						;;
519				esac
520			done
521			if [ -n "${nftcmd}" ]; then
522				printf '%s\n' "${nftcmd}" |
523					cmd nft -f
524			fi
525		}
526		unset table nftcmd
527	fi
528	if type_p iptables >/dev/null; then
529		iptables=''
530		for iptables in iptables ip6tables; do
531			"${iptables}-save" 2>/dev/null | {
532				restore=''
533				found=0
534				line=''
535				while read -r line; do
536					case "${line}" in
537						\** | COMMIT | '-A '*'-m comment --comment "wg-quick(8) rule for '"${INTERFACE}"'"'*)
538							case "${line}" in
539								-A*)
540									found=1
541									;;
542								*)
543									:
544									;;
545							esac
546							restore="${restore:+${restore}${NL}}-D${line#-A}"
547							;;
548						*)
549							:
550							;;
551					esac
552				done
553				[ "${found}" -ne 1 ] ||
554					printf '%s\n' "${restore}" |
555						cmd "${iptables}-restore" -n
556				unset restore found line
557			}
558		done
559		unset iptables
560	fi
561}
562
563add_default() {
564	table=''
565	line=''
566	proto=''
567	iptables=''
568	pf=''
569	marker=''
570	restore=''
571	nftable=''
572	nftcmd=''
573	if ! get_fwmark table; then
574		table=51820
575		while [ -n "$(ip -4 route show table "${table}" 2>/dev/null)" ] ||
576			      [ -n "$(ip -6 route show table "${table}" 2>/dev/null)" ]; do
577			table=$((table + 1))
578		done
579		cmd wg set "${INTERFACE}" fwmark "${table}"
580	fi
581	case "${1}" in
582		*:*)
583			proto='-6'
584			iptables='ip6tables'
585			pf='ip6'
586			;;
587		*)
588			proto='-4'
589			iptables='iptables'
590			pf='ip'
591			;;
592	esac
593	cmd ip "${proto}" route add "${1}" dev "${INTERFACE}" table "${table}"
594	cmd ip "${proto}" rule add not fwmark "${table}" table "${table}"
595	cmd ip "${proto}" rule add table main suppress_prefixlength 0
596
597	marker="-m comment --comment \"wg-quick(8) rule for ${INTERFACE}\""
598	restore="*raw${NL}"
599	nftable="wg-quick-${INTERFACE}"
600	nftcmd="${nftcmd:+${nftcmd}${NL}}add table ${pf} ${nftable}"
601	nftcmd="${nftcmd:+${nftcmd}${NL}}add chain ${pf} ${nftable} preraw { type filter hook prerouting priority -300; }"
602	nftcmd="${nftcmd:+${nftcmd}${NL}}add chain ${pf} ${nftable} premangle { type filter hook prerouting priority -150; }"
603	nftcmd="${nftcmd:+${nftcmd}${NL}}add chain ${pf} ${nftable} postmangle { type filter hook postrouting priority -150; }"
604	ip -o "${proto}" addr show dev "${INTERFACE}" 2>/dev/null | {
605		match=''
606		while read -r line; do
607			match="$(
608        printf %s "${line}" |
609          sed -ne 's/^.*inet6\? \([0-9a-f:.]\+\)/[0-9]\+.*$/\1/; t P; b; : P; p'
610      )"
611			[ -n "${match}" ] ||
612				continue
613			restore="${restore:+${restore}${NL}}-I PREROUTING ! -i ${INTERFACE} -d ${match} -m addrtype ! --src-type LOCAL -j DROP ${marker}"
614			nftcmd="${nftcmd:+${nftcmd}${NL}}add rule ${pf} ${nftable} preraw iifname != \"${INTERFACE}\" ${pf} daddr ${match} fib saddr type != local drop"
615		done
616		restore="${restore:+${restore}${NL}}COMMIT${NL}*mangle${NL}-I POSTROUTING -m mark --mark ${table} -p udp -j CONNMARK --save-mark ${marker}${NL}-I PREROUTING -p udp -j CONNMARK --restore-mark ${marker}${NL}COMMIT"
617		nftcmd="${nftcmd:+${nftcmd}${NL}}add rule ${pf} ${nftable} postmangle meta l4proto udp mark ${table} ct mark set mark"
618		nftcmd="${nftcmd:+${nftcmd}${NL}}add rule ${pf} ${nftable} premangle meta l4proto udp meta mark set ct mark"
619		! [ "${proto}" = '-4' ] ||
620			cmd sysctl -q net.ipv4.conf.all.src_valid_mark=1
621		if type_p nft >/dev/null; then
622			printf '%s\n' "${nftcmd}" |
623				cmd nft -f
624		else
625			printf '%s\n' "${restore}" |
626				cmd "${iptables}-restore" -n
627		fi
628		unset match
629	}
630	HAVE_SET_FIREWALL=1
631	unset table line proto iptables pf marker restore nftable nftcmd
632}
633
634set_config() {
635	if [ -e /dev/stdin ]; then
636		printf '%s\n' "${WG_CONFIG}" |
637			cmd wg setconf "${INTERFACE}" /dev/stdin
638	else
639		tempfile="$(mktemp)"
640		exit_trap push "rm -f \"${tempfile}\""
641		printf '%s\n' "${WG_CONFIG}" >"${tempfile}"
642		cmd wg setconf "${INTERFACE}" "${tempfile}"
643		rm -f "${tempfile}"
644		exit_trap pop
645		unset tempfile
646	fi
647}
648
649save_config() {
650	old_umask=''
651	new_config=''
652	current_config=''
653	address=''
654	cmd=''
655	addr_match="$(
656    ip -all -brief address show dev "${INTERFACE}" |
657      sed -ne 's#^'"${INTERFACE}"' \+[A-Z]\+ \+\(.\+\)$#\1#; t P; b; : P; p'
658  )"
659	new_config='[Interface]'
660	for address in ${addr_match}; do
661		new_config="${new_config:+${new_config}${NL}}Address = ${address}"
662	done
663	{
664		resolvconf -l "$(resolvconf_iface_prefix)${INTERFACE}" 2>/dev/null ||
665			cat "/etc/resolvconf/run/interface/$(resolvconf_iface_prefix)${INTERFACE}" 2>/dev/null
666	} | {
667		while read -r address; do
668			addr_match="$(
669        printf %s "${address}" |
670          sed -ne 's#^nameserver \([a-zA-Z0-9_=+:%.-]\+\)$#\1#; t P; b; : P; p'
671      )"
672			[ -z "${addr_match}" ] ||
673				new_config="${new_config:+${new_config}${NL}}DNS = ${addr_match}"
674		done
675		if [ -n "${MTU}" ]; then
676			mtu_match="$(
677        ip link show dev "${INTERFACE}" |
678          sed -ne 's/^.*mtu \([0-9]\+\).*$/\1/; t P; b; : P; p'
679      )"
680			[ -z "${mtu_match}" ] ||
681				new_config="${new_config:+${new_config}${NL}}MTU = ${mtu_match}"
682		fi
683		[ -z "${TABLE}" ] ||
684			new_config="${new_config:+${new_config}${NL}}Table = ${TABLE}"
685		[ "${SAVE_CONFIG}" -eq 0 ] ||
686			new_config="${new_config:+${new_config}${NL}}SaveConfig = true"
687		eval "set -- ${PRE_UP}"
688		for cmd; do
689			new_config="${new_config:+${new_config}${NL}}PreUp = ${cmd}"
690		done
691		eval "set -- ${POST_UP}"
692		for cmd; do
693			new_config="${new_config:+${new_config}${NL}}PostUp = ${cmd}"
694		done
695		eval "set -- ${PRE_DOWN}"
696		for cmd; do
697			new_config="${new_config:+${new_config}${NL}}PreDown = ${cmd}"
698		done
699		eval "set -- ${POST_DOWN}"
700		for cmd; do
701			new_config="${new_config:+${new_config}${NL}}PostDown = ${cmd}"
702		done
703		old_umask="$(umask)"
704		umask 077
705		current_config="$(cmd wg showconf "${INTERFACE}")"
706		exit_trap push "rm -f \"${CONFIG_FILE}.tmp\""
707		printf '%s\n' "${current_config}" |
708			sed -e "s#\\[Interface\\]\$#$(
709        printf %s "${new_config}" |
710          sed -e '$!s/$/\\n/' |
711          tr -d '\n'
712      )#" >"${CONFIG_FILE}.tmp" ||
713			die 'Could not write configuration file'
714		sync "${CONFIG_FILE}.tmp"
715		mv "${CONFIG_FILE}.tmp" "${CONFIG_FILE}" ||
716			die 'Could not move configuration file'
717		exit_trap pop
718		umask "${old_umask}"
719		unset new_config current_config old_umask cmd mtu_match addr_match address
720	}
721}
722
723execute_hooks() {
724	for hook; do
725		hook="$(
726      printf %s "${hook}" |
727        sed -e "s^%i^${INTERFACE}^g"
728    )"
729		printf '[#] %s\n' "${hook}" >&2
730		(eval "${hook}")
731	done
732	unset hook
733}
734
735cmd_usage() {
736	cat >&2 <<-_EOF
737	Usage: $PROGRAM [ up | down | save | strip ] [ CONFIG_FILE | INTERFACE ]
738	  CONFIG_FILE is a configuration file, whose filename is the interface name
739	  followed by \`.conf'. Otherwise, INTERFACE is an interface name, with
740	  configuration found at /etc/wireguard/INTERFACE.conf. It is to be readable
741	  by wg(8)'s \`setconf' sub-command, with the exception of the following additions
742	  to the [Interface] section, which are handled by $PROGRAM:
743	  - Address: may be specified one or more times and contains one or more
744	    IP addresses (with an optional CIDR mask) to be set for the interface.
745	  - DNS: an optional DNS server to use while the device is up.
746	  - MTU: an optional MTU for the interface; if unspecified, auto-calculated.
747	  - Table: an optional routing table to which routes will be added; if
748	    unspecified or \`auto', the default table is used. If \`off', no routes
749	    are added.
750	  - PreUp, PostUp, PreDown, PostDown: script snippets which will be executed
751	    by bash(1) at the corresponding phases of the link, most commonly used
752	    to configure DNS. The string \`%i' is expanded to INTERFACE.
753	  - SaveConfig: if set to \`true', the configuration is saved from the current
754	    state of the interface upon shutdown.
755	See wg-quick(8) for more info and examples.
756	_EOF
757}
758
759cmd_up() {
760	i=''
761	[ -z "$(ip link show dev "${INTERFACE}" 2>/dev/null)" ] ||
762		die "\`${INTERFACE}' already exists"
763	exit_trap push 'del_if'
764	eval "execute_hooks ${PRE_UP}"
765	add_if
766	set_config
767	set_mtu_up
768	eval "set -- ${ADDRESSES}"
769	for i; do
770		add_addr "${i}"
771	done
772	set_dns
773	for i in $(
774			      for k in `wg show "${INTERFACE}" allowed-ips`; do
775				      #shellcheck disable=SC2003
776				      ! expr match "${k}" '[0-9a-z:.]\+/[0-9]\+$' >/dev/null ||
777					      printf '%s\n' "${k}"
778			      done | sort -nr -k 2
779			      unset j k
780		      ); do
781		add_route "${i}"
782	done
783	eval "execute_hooks ${POST_UP}"
784	unset i
785	exit_trap pop
786}
787
788cmd_down() {
789	case " $(wg show interfaces) " in
790		*" ${INTERFACE} "*) : ;;
791		*) die "\`${INTERFACE}' is not a WireGuard interface" ;;
792	esac
793	eval "execute_hooks ${PRE_DOWN}"
794	[ "${SAVE_CONFIG}" -eq 0 ] ||
795		save_config
796	del_if
797	unset_dns || :
798	remove_firewall || :
799	eval "execute_hooks ${POST_DOWN}"
800}
801
802cmd_save() {
803	case " $(wg show interfaces) " in
804		*" ${INTERFACE} "*) : ;;
805		*) die "\`${INTERFACE}' is not a WireGuard interface" ;;
806	esac
807	save_config
808}
809
810cmd_strip() { printf '%s\n' "${WG_CONFIG}"; }
811
812##
813
814EXIT_TRAP=''
815LC_ALL=C
816SELF="$(readlink -f "${0}")"
817PATH="$(printf %s "${SELF}" | sed -e 's:/[^/]*$::'):${PATH}"
818export LC_ALL PATH
819[ -n "${UID-}" ] || UID="$(id -u)"
820[ -n "${CONFIG_FILE_BASE}" ] ||
821	CONFIG_FILE_BASE='/etc/wireguard'
822NL='
823'
824WG_CONFIG=''
825INTERFACE=''
826ADDRESSES=$(array_save)
827MTU=''
828DNS=$(array_save)
829DNS_SEARCH=$(array_save)
830TABLE=''
831PRE_UP=$(array_save)
832POST_UP=$(array_save)
833PRE_DOWN=$(array_save)
834POST_DOWN=$(array_save)
835SAVE_CONFIG=0
836CONFIG_FILE=''
837PROGRAM="$(printf %s "${0}" | sed -e 's:^.*/\([^/]*\)$:\1:')"
838ARGS=$(array_save "${@}")
839HAVE_SET_DNS=0
840HAVE_SET_FIREWALL=0
841
842# ~~ function override insertion point ~~
843
844case "${#}:${1}" in
845	1:--help | 1:-h | 1:help)
846		cmd_usage
847		;;
848	2:up | 2:down | 2:save | 2:strip)
849		auto_su
850		parse_options "${2}"
851		case "${1}" in
852			up)
853				cmd_up
854				;;
855			down)
856				cmd_down
857				;;
858			save)
859				cmd_save
860				;;
861			strip)
862				cmd_strip
863				;;
864			*)
865				:
866				;;
867		esac
868		;;
869	*)
870		cmd_usage
871		exit 1
872		;;
873esac