xref: /dragonfly/etc/rc.d/wg (revision 7485684f)
1#!/bin/sh
2#
3# Copyright (c) 2024 The DragonFly Project.  All rights reserved.
4#
5# This code is derived from software contributed to The DragonFly Project
6# by Aaron LI <aly@aaronly.me>
7#
8# Redistribution and use in source and binary forms, with or without
9# modification, are permitted provided that the following conditions
10# are met:
11#
12# 1. Redistributions of source code must retain the above copyright
13#    notice, this list of conditions and the following disclaimer.
14# 2. Redistributions in binary form must reproduce the above copyright
15#    notice, this list of conditions and the following disclaimer in
16#    the documentation and/or other materials provided with the
17#    distribution.
18# 3. Neither the name of The DragonFly Project nor the names of its
19#    contributors may be used to endorse or promote products derived
20#    from this software without specific, prior written permission.
21#
22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
26# COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27# INCIDENTAL, SPECIAL, EXEMPLARY OR CONSEQUENTIAL DAMAGES (INCLUDING,
28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
30# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
32# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
33# SUCH DAMAGE.
34#
35
36# PROVIDE: wg wireguard
37# REQUIRE: NETWORKING
38# BEFORE:  DAEMON
39
40# uncomment to show extra debug logs
41#WG_DEBUG=yes
42# uncomment to not actually execute the commands
43#WG_DRYRUN=yes
44
45. /etc/rc.subr
46
47name="wg"
48rcvar=$(set_rcvar)
49start_cmd="${name}_start"
50stop_cmd="${name}_stop"
51status_cmd="${name}_status"
52extra_commands="status"
53
54# usage: wg_run cmd ...
55wg_run()
56{
57	if [ -n "${WG_DRYRUN}" ]; then
58		printf "[+] %s\n" "$*"
59		return
60	fi
61	debug "[+] $*"
62	"$@"
63}
64
65# similar to wg_run(), but exit if the command fails.
66wg_must_run()
67{
68	wg_run "$@"
69	local ret=$?
70	if [ ${ret} -ne 0 ]; then
71		err ${ret} "return code: ${ret}, command was: $*"
72	fi
73}
74
75# usage: wg_load_config <conffile>
76wg_load_config()
77{
78	local conffile=$1
79	local ifname=$(basename ${conffile} .conf)
80
81	debug "loading [${ifname}] configs from file: ${conffile}"
82	local configs=$(awk -v ifname="${ifname}" -v debugvar="${WG_DEBUG}" '
83	BEGIN {
84		SUBSEP = "_"
85
86		# conversion table for hex2dec()
87		xdigits = "0123456789abcdef"
88		for (i = 0; i < length(xdigits); i++) {
89			k = substr(xdigits, i + 1, 1)
90			decv[k] = i
91			decv[toupper(k)] = i
92		}
93	}
94
95	function debug(msg) {
96		if (!debugvar)
97			return
98		printf("wg [%s]: DEBUG: %s\n", ifname, msg) > "/dev/stderr"
99	}
100	function info(msg) {
101		printf("wg [%s]: INFO: %s\n", ifname, msg) > "/dev/stderr"
102	}
103	function warn(msg) {
104		printf("wg [%s]: WARNING: %s\n", ifname, msg) > "/dev/stderr"
105	}
106	function error(code, msg) {
107		printf("wg [%s]: ERROR: %s\n", ifname, msg) > "/dev/stderr"
108		exit code
109	}
110	function hex2dec(x,   v) {
111		# The 0x prefix is optional.
112		v = 0
113		for (i = 1; i <= length(x); i++)
114			v = 16 * v + decv[substr(x, i, 1)]
115		return v
116	}
117	function fix_integer(v) {
118		if (v == "off")
119			return 0
120		else if (v ~ /^0[xX][[:xdigit:]]+$/)
121			return hex2dec(v)
122		else
123			return v + 0
124	}
125	function fix_boolean(v) {
126		v = tolower(v)
127		if (v == "1" || v == "true" || v == "on" || v == "yes")
128			return "true"
129		else
130			return "false"
131	}
132	function fix_endpoint(v) {
133		if (v ~ /^\[/) {
134			# Assume IPv6: [ipv6]:port
135			sub(/\[/, "", v)
136			sub(/\]:/, " ", v)
137		} else {
138			# Assume IPv4 or domain: ipv4:port, domain:port
139			sub(/:/, " ", v)
140		}
141		return v
142	}
143	function fix_address(v,   n, a) {
144		# Comma-separated IPv4/IPv6, with optional CIDR masks
145		n = split(v, addrs, /[, ]+/)
146		v = ""
147		for (i = 1; i <= n; i++) {
148			a = addrs[i]
149			if (!index(a, "/")) {
150				if (index(a, ":"))
151					a = a "/128"
152				else
153					a = a "/32"
154			}
155			v = v " " a
156		}
157		return v
158	}
159	function fix_aips(v,   n) {
160		# Comma-separated IPv4/IPv6 with CIDR masks
161		n = split(v, aips, /[, ]+/)
162		v = ""
163		for (i = 1; i <= n; i++)
164			v = v " " aips[i]
165		return v
166	}
167	function trim(s) {
168		gsub(/^[ \t]+|[ \t]+$/, "", s)
169		return s
170	}
171	function quote(s) {
172		# NOTE: \047 is the single quote.
173		gsub(/\047/, "\047\\\047\047", s)
174		return "\047" s "\047"
175	}
176
177	NF == 0 || $1 ~ /^[#;]/ {
178		next
179	}
180	$1 ~ /^\[/ {
181		section = tolower($1)
182		if (section == "[interface]") {
183			is_interface = 1
184			is_peer = 0
185		} else if (section == "[peer]") {
186			is_interface = 0
187			is_peer = 1
188			peer_count++
189		} else {
190			is_interface = 0
191			is_peer = 0
192			warn(sprintf("unknown section: %s", section))
193		}
194		next
195	}
196	!(is_interface || is_peer) {
197		warn(sprintf("skip unknown %s: %s", section, $0))
198		next
199	}
200	$0 !~ /^[ \t]*[[:alnum:]]+[ \t]*=[ \t]*[^ \t].*$/ {
201		warn(sprintf("skip invalid line: %s", $0))
202		next
203	}
204	{
205		match($0, /^[ \t]*[[:alnum:]]+[ \t]*=/)
206		key = trim(tolower(substr($0, 1, RLENGTH - 1)))
207		value = trim(substr($0, RLENGTH + 1))
208		if (key == "" || value == "")
209			error(1, "code bug") # already skipped; cannot happen
210
211		# Join split lines.
212		while (value ~ /\\$/) {
213			if ((getline vline) <= 0) {
214				warn(sprintf("incomplete value of |%s|: %s",
215					     key, value))
216				break
217			}
218			value = substr(value, 1, length(value) - 1)
219			value = value " " trim(vline)
220		}
221
222		if (is_interface) {
223			debug(sprintf("interface: |%s| = |%s|", key, value))
224			if (key == "description" || key == "privatekey" ||
225			    key == "listenport" || key == "mtu") {
226				interface[key] = value
227			} else if (key == "cookie" || key == "fwmark") {
228				key = "cookie"
229				interface[key] = fix_integer(value)
230			} else if (key == "address") {
231				old = interface[key]
232				interface[key] = old " " fix_address(value)
233			} else if (key == "preup" || key == "postup" ||
234				   key == "predown" || key == "postdown") {
235				gsub(/%i/, ifname, value)
236				n = ++interface[key "_count"]
237				interface[key n] = value
238			} else {
239				info(sprintf("ignore unsupported interface " \
240					     "config: %s = %s", key, value))
241				next
242			}
243		} else {
244			debug(sprintf("peer[%d]: |%s| = |%s|",
245				      peer_count, key, value))
246			if (key == "description" || key == "publickey" ||
247			    key == "presharedkey") {
248				peers[peer_count, key] = value
249			} else if (key == "endpoint") {
250				peers[peer_count, key] = fix_endpoint(value)
251			} else if (key == "allowedips") {
252				old = peers[peer_count, key]
253				peers[peer_count, key] = old " " fix_aips(value)
254			} else if (key == "persistentkeepalive") {
255				peers[peer_count, key] = fix_integer(value)
256			} else if (key == "enabled") {
257				peers[peer_count, key] = fix_boolean(value)
258			} else {
259				info(sprintf("ignore unsupported peer " \
260					     "config: %s = %s", key, value))
261				next
262			}
263		}
264	}
265
266	END {
267		for (key in interface)
268			printf("_wg_interface_%s=%s;\n",
269			       key, quote(interface[key]))
270
271		peer_count += 0  # fix empty value to be 0
272		printf("_wg_peer_count=%s;\n", quote(peer_count))
273		for (key in peers)
274			printf("_wg_peer%s=%s;\n", key, quote(peers[key]))
275	}' "${conffile}") || exit $?
276
277	local msg=$(printf "eval configs: {{{\n%s\n}}}\n" "${configs}")
278	debug "${msg}"
279
280	eval "${configs}"
281}
282
283# usage: wg_set_interface <ifname>
284wg_set_interface()
285{
286	local ifname=$1
287
288	local privkey=${_wg_interface_privatekey}
289	local port=${_wg_interface_listenport}
290	local cookie=${_wg_interface_cookie}
291
292	local args=
293	if [ -z "${privkey}" ]; then
294		err 1 "interface is missing the private key"
295	else
296		args="wgkey ${privkey}"
297	fi
298	if [ -n "${port}" ]; then
299		args="${args} wgport ${port}"
300	fi
301	if [ -n "${cookie}" ]; then
302		args="${args} wgcookie ${cookie}"
303	fi
304	wg_must_run ifconfig ${ifname} ${args}
305
306	local addrs=${_wg_interface_address}
307	local addr af
308	for addr in ${addrs}; do
309		case ${addr} in
310		*:*)
311			af=inet6
312			;;
313		*)
314			af=inet
315			;;
316		esac
317		wg_run ifconfig ${ifname} ${af} ${addr} alias
318	done
319
320	local descr=${_wg_interface_description}
321	if [ -n "${descr}" ]; then
322		wg_run ifconfig ${ifname} description "${descr}"
323	fi
324
325	local mtu=${_wg_interface_mtu}
326	if [ -n "${mtu}" ]; then
327		wg_run ifconfig ${ifname} mtu ${mtu}
328	fi
329}
330
331# usage: wg_set_peer <ifname> <peerid>
332wg_set_peer()
333{
334	local ifname=$1
335	local peerid=$2
336
337	local enabled
338	eval 'enabled="${_wg_'${peerid}'_enabled}"'
339	if [ "${enabled}" = "false" ]; then
340		info "peer [${peerid}] is disabled"
341		return
342	fi
343
344	local publickey
345	eval 'publickey="${_wg_'${peerid}'_publickey}"'
346	if [ -z "${publickey}" ]; then
347		warn "peer [${peerid}] is missing the public key"
348		return
349	fi
350	local cmd="ifconfig ${ifname} wgpeer ${publickey}"
351
352	local descr
353	eval 'descr="${_wg_'${peerid}'_description}"'
354	if [ -n "${descr}" ]; then
355		wg_run ${cmd} wgdescription "${descr}"
356	fi
357
358	local psk endpoint pka aips
359	eval 'psk="${_wg_'${peerid}'_presharedkey}"'
360	eval 'endpoint="${_wg_'${peerid}'_endpoint}"'
361	eval 'pka="${_wg_'${peerid}'_persistentkeepalive}"'
362	eval 'aips="${_wg_'${peerid}'_allowedips}"'
363
364	local args= aip
365	if [ -n "${psk}" ]; then
366		args="${args} wgpsk ${psk}"
367	fi
368	if [ -n "${endpoint}" ]; then
369		args="${args} wgendpoint ${endpoint}"
370	fi
371	if [ -n "${pka}" ]; then
372		args="${args} wgpka ${pka}"
373	fi
374	# All allowed IPs must be configured at once.
375	for aip in ${aips}; do
376		args="${args} wgaip ${aip}"
377	done
378	wg_run ${cmd} ${args}
379}
380
381# usage: wg_exec_hook <preup|postup|predown|postdown>
382wg_exec_hook()
383{
384	local hook=$1
385	local count
386
387	case ${hook} in
388	preup|postup|predown|postdown)
389		eval 'count="${_wg_interface_'${hook}'_count:-0}"'
390		;;
391	*)
392		err 1 "unknown hook: ${hook}"
393		;;
394	esac
395
396	debug "executing [${hook}] hook (${count} actions) ..."
397
398	local i=1 cmd ret
399	while [ ${i} -le ${count} ]; do
400		eval 'cmd="${_wg_interface_'${hook}${i}'}"'
401		wg_run sh -c "${cmd}"
402		ret=$?
403		if [ ${ret} -ne 0 ]; then
404			warn "return code: ${ret}, command was: sh -c '${cmd}'"
405		fi
406		i=$((i + 1))
407	done
408}
409
410# usage: wg_start_interface <ifname>
411wg_start_interface()
412{
413	local ifname=$1
414	info "starting interface [${ifname}] ..."
415
416	local conffile="${wg_config_dir}/${ifname}.conf"
417	if [ ! -r "${conffile}" ]; then
418		err 1 "cannot read config file: ${conffile}"
419	fi
420
421	wg_load_config "${conffile}"
422
423	wg_exec_hook preup
424
425	local cmd
426	if expr "${ifname}" : '^wg[0-9]+$' > /dev/null; then
427		cmd="ifconfig ${ifname} create"
428	else
429		cmd="ifconfig wg create name ${ifname}"
430	fi
431	wg_must_run ${cmd} > /dev/null
432
433	wg_set_interface ${ifname}
434
435	local i=1
436	while [ ${i} -le ${_wg_peer_count:-0} ]; do
437		wg_set_peer ${ifname} "peer${i}"
438		i=$((i + 1))
439	done
440
441	wg_run ifconfig ${ifname} up
442
443	wg_exec_hook postup
444
445	info "interface [${ifname}] started."
446}
447
448# usage: wg_stop_interface <ifname>
449wg_stop_interface()
450{
451	local ifname=$1
452	info "stopping interface [${ifname}] ..."
453
454	local conffile="${wg_config_dir}/${ifname}.conf"
455	if [ ! -r "${conffile}" ]; then
456		err 1 "cannot read config file: ${conffile}"
457	fi
458
459	wg_load_config "${conffile}"
460
461	wg_exec_hook predown
462
463	wg_run ifconfig ${ifname} down
464	wg_run ifconfig ${ifname} destroy
465
466	wg_exec_hook postdown
467
468	info "interface [${ifname}] stopped."
469}
470
471wg_start()
472{
473	local ifname
474	for ifname in ${wg_interfaces}; do
475		if [ "${ifname}" = "wg" ]; then
476			warn "skip invalid interface name: ${ifname}"
477			continue
478		fi
479		if ifconfig -n ${ifname} >/dev/null 2>&1; then
480			warn "interface [${ifname}] already exists."
481			continue
482		fi
483		# Use a sub-shell to avoid mixing the configurations of
484		# different interfaces.
485		( wg_start_interface ${ifname} )
486	done
487}
488
489wg_stop()
490{
491	local ifname
492	for ifname in ${wg_interfaces}; do
493		if ! ifconfig -n ${ifname} >/dev/null 2>&1; then
494			warn "interface [${ifname}] does not exist."
495			continue
496		fi
497		( wg_stop_interface ${ifname} )
498	done
499}
500
501wg_status()
502{
503	local ifname
504	for ifname in ${wg_interfaces}; do
505		wg_run ifconfig -n ${ifname}
506	done
507}
508
509load_rc_config ${name}
510
511cmd=$1
512shift
513if [ $# -gt 0 ]; then
514	wg_interfaces="$@"
515fi
516debug "interfaces: ${wg_interfaces}"
517
518run_rc_command "${cmd}"
519