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