1#!/usr/local/bin/bash 2# SPDX-License-Identifier: GPL-2.0 3# 4# Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved. 5# 6 7set -e -o pipefail 8shopt -s extglob 9export LC_ALL=C 10 11SELF="$(readlink -f "${BASH_SOURCE[0]}")" 12export PATH="${SELF%/*}:$PATH" 13 14WG_CONFIG="" 15INTERFACE="" 16ADDRESSES=( ) 17MTU="" 18DNS=( ) 19TABLE="" 20PRE_UP=( ) 21POST_UP=( ) 22PRE_DOWN=( ) 23POST_DOWN=( ) 24SAVE_CONFIG=0 25CONFIG_FILE="" 26PROGRAM="${0##*/}" 27ARGS=( "$@" ) 28 29cmd() { 30 echo "[#] $*" >&2 31 "$@" 32} 33 34die() { 35 echo "$PROGRAM: $*" >&2 36 exit 1 37} 38 39CONFIG_SEARCH_PATHS=( /etc/wireguard /usr/local/etc/wireguard ) 40 41unset ORIGINAL_TMPDIR 42make_temp() { 43 local old_umask 44 45 [[ -v ORIGINAL_TMPDIR ]] && export TMPDIR="$ORIGINAL_TMPDIR" 46 ORIGINAL_TMPDIR="$TMPDIR" 47 [[ -z $TMPDIR ]] && unset TMPDIR 48 49 old_umask="$(umask)" 50 umask 077 51 export TMPDIR="$(mktemp -d)" 52 umask "$old_umask" 53 54 [[ -d $TMPDIR ]] || die "Unable to create safe temporary directory" 55 CLEANUP_TMPDIR="$TMPDIR" 56} 57 58clean_temp() { 59 [[ -n $CLEANUP_TMPDIR ]] && rm -rf "$CLEANUP_TMPDIR" 60} 61 62parse_options() { 63 local interface_section=0 line key value stripped path 64 CONFIG_FILE="$1" 65 if [[ $CONFIG_FILE =~ ^[a-zA-Z0-9_=+.-]{1,15}$ ]]; then 66 for path in "${CONFIG_SEARCH_PATHS[@]}"; do 67 CONFIG_FILE="$path/$1.conf" 68 [[ -e $CONFIG_FILE ]] && break 69 done 70 fi 71 [[ -e $CONFIG_FILE ]] || die "\`$CONFIG_FILE' does not exist" 72 [[ $CONFIG_FILE =~ (^|/)([a-zA-Z0-9_=+.-]{1,15})\.conf$ ]] || die "The config file must be a valid interface name, followed by .conf" 73 CONFIG_FILE="$(readlink -f "$CONFIG_FILE")" 74 ((($(stat -f '0%#p' "$CONFIG_FILE") & $(stat -f '0%#p' "${CONFIG_FILE%/*}") & 0007) == 0)) || echo "Warning: \`$CONFIG_FILE' is world accessible" >&2 75 INTERFACE="${BASH_REMATCH[2]}" 76 shopt -s nocasematch 77 while read -r line || [[ -n $line ]]; do 78 stripped="${line%%\#*}" 79 key="${stripped%%=*}"; key="${key##*([[:space:]])}"; key="${key%%*([[:space:]])}" 80 value="${stripped#*=}"; value="${value##*([[:space:]])}"; value="${value%%*([[:space:]])}" 81 [[ $key == "["* ]] && interface_section=0 82 [[ $key == "[Interface]" ]] && interface_section=1 83 if [[ $interface_section -eq 1 ]]; then 84 case "$key" in 85 Address) ADDRESSES+=( ${value//,/ } ); continue ;; 86 MTU) MTU="$value"; continue ;; 87 DNS) DNS+=( ${value//,/ } ); continue ;; 88 Table) TABLE="$value"; continue ;; 89 PreUp) PRE_UP+=( "$value" ); continue ;; 90 PreDown) PRE_DOWN+=( "$value" ); continue ;; 91 PostUp) POST_UP+=( "$value" ); continue ;; 92 PostDown) POST_DOWN+=( "$value" ); continue ;; 93 SaveConfig) read_bool SAVE_CONFIG "$value"; continue ;; 94 esac 95 fi 96 WG_CONFIG+="$line"$'\n' 97 done < "$CONFIG_FILE" 98 shopt -u nocasematch 99} 100 101read_bool() { 102 case "$2" in 103 true) printf -v "$1" 1 ;; 104 false) printf -v "$1" 0 ;; 105 *) die "\`$2' is neither true nor false" 106 esac 107} 108 109auto_su() { 110 [[ $UID == 0 ]] || exec sudo -p "$PROGRAM must be run as root. Please enter the password for %u to continue: " -- "$BASH" -- "$SELF" "${ARGS[@]}" 111} 112 113add_if() { 114 cmd "${WG_QUICK_USERSPACE_IMPLEMENTATION:-wireguard-go}" "$INTERFACE" 115} 116 117del_routes() { 118 local todelete=( ) destination gateway netif 119 while read -r destination _ _ _ _ netif _; do 120 [[ $netif == "$INTERFACE" ]] && todelete+=( "$destination" ) 121 done < <(netstat -nr -f inet) 122 for destination in "${todelete[@]}"; do 123 cmd route -q -n delete -inet "$destination" || true 124 done 125 todelete=( ) 126 while read -r destination gateway _ netif; do 127 [[ $netif == "$INTERFACE" || ( $netif == lo* && $gateway == "$INTERFACE" ) ]] && todelete+=( "$destination" ) 128 done < <(netstat -nr -f inet6) 129 for destination in "${todelete[@]}"; do 130 cmd route -q -n delete -inet6 "$destination" || true 131 done 132 for destination in "${ENDPOINTS[@]}"; do 133 if [[ $destination == *:* ]]; then 134 cmd route -q -n delete -inet6 "$destination" || true 135 else 136 cmd route -q -n delete -inet "$destination" || true 137 fi 138 done 139} 140 141if_exists() { 142 # HACK: The goal is simply to determine whether or not the interface exists. The 143 # straight-forward way of doing this would be `ifconfig $INTERFACE`, but this 144 # invokes the SIOCGIFSTATUS ioctl, which races with interface shutdown inside 145 # the tun driver, resulting in a kernel panic. So we work around it the stupid 146 # way by using the one utility that appears to call if_nametoindex fairly early 147 # and fails if it doesn't exist: `arp`. 148 if arp -i "$INTERFACE" -a -n >/dev/null 2>&1; then 149 return 0 150 else 151 return 1 152 fi 153} 154 155del_if() { 156 [[ $HAVE_SET_DNS -eq 0 ]] || unset_dns 157 cmd rm -f "/var/run/wireguard/$INTERFACE.sock" 158 while if_exists; do 159 # HACK: it would be nice to `route monitor` here and wait for RTM_IFANNOUNCE 160 # but it turns out that the announcement is made before the interface 161 # disappears so we sometimes get a hang. So, we're instead left with polling 162 # in a sleep loop like this. 163 sleep 0.1 164 done 165} 166 167up_if() { 168 cmd ifconfig "$INTERFACE" up 169} 170 171add_addr() { 172 if [[ $1 == *:* ]]; then 173 cmd ifconfig "$INTERFACE" inet6 "$1" alias 174 else 175 cmd ifconfig "$INTERFACE" inet "$1" "${1%%/*}" alias 176 fi 177} 178 179set_mtu() { 180 local mtu=0 endpoint output family 181 if [[ -n $MTU ]]; then 182 cmd ifconfig "$INTERFACE" mtu "$MTU" 183 return 184 fi 185 while read -r _ endpoint; do 186 [[ $endpoint =~ ^\[?([a-z0-9:.]+)\]?:[0-9]+$ ]] || continue 187 family=inet 188 [[ ${BASH_REMATCH[1]} == *:* ]] && family=inet6 189 output="$(route -n get "-$family" "${BASH_REMATCH[1]}" || true)" 190 [[ $output =~ interface:\ ([^ ]+)$'\n' && $(ifconfig "${BASH_REMATCH[1]}") =~ mtu\ ([0-9]+) && ${BASH_REMATCH[1]} -gt $mtu ]] && mtu="${BASH_REMATCH[1]}" 191 done < <(wg show "$INTERFACE" endpoints) 192 if [[ $mtu -eq 0 ]]; then 193 read -r output < <(route -n get default || true) || true 194 [[ $output =~ interface:\ ([^ ]+)$'\n' && $(ifconfig "${BASH_REMATCH[1]}") =~ mtu\ ([0-9]+) && ${BASH_REMATCH[1]} -gt $mtu ]] && mtu="${BASH_REMATCH[1]}" 195 fi 196 [[ $mtu -gt 0 ]] || mtu=1500 197 cmd ifconfig "$INTERFACE" mtu $(( mtu - 80 )) 198} 199 200 201collect_gateways() { 202 local destination gateway 203 204 GATEWAY4="" 205 while read -r destination gateway _; do 206 [[ $destination == default ]] || continue 207 GATEWAY4="$gateway" 208 break 209 done < <(netstat -nr -f inet) 210 211 GATEWAY6="" 212 while read -r destination gateway _; do 213 [[ $destination == default ]] || continue 214 GATEWAY6="$gateway" 215 break 216 done < <(netstat -nr -f inet6) 217} 218 219collect_endpoints() { 220 ENDPOINTS=( ) 221 while read -r _ endpoint; do 222 [[ $endpoint =~ ^\[?([a-z0-9:.]+)\]?:[0-9]+$ ]] || continue 223 ENDPOINTS+=( "${BASH_REMATCH[1]}" ) 224 done < <(wg show "$INTERFACE" endpoints) 225} 226 227set_endpoint_direct_route() { 228 local old_endpoints endpoint old_gateway4 old_gateway6 remove_all_old=0 added=( ) 229 old_endpoints=( "${ENDPOINTS[@]}" ) 230 old_gateway4="$GATEWAY4" 231 old_gateway6="$GATEWAY6" 232 collect_gateways 233 collect_endpoints 234 235 [[ $old_gateway4 != "$GATEWAY4" || $old_gateway6 != "$GATEWAY6" ]] && remove_all_old=1 236 237 if [[ $remove_all_old -eq 1 ]]; then 238 for endpoint in "${ENDPOINTS[@]}"; do 239 [[ " ${old_endpoints[*]} " == *" $endpoint "* ]] || old_endpoints+=( "$endpoint" ) 240 done 241 fi 242 243 for endpoint in "${old_endpoints[@]}"; do 244 [[ $remove_all_old -eq 0 && " ${ENDPOINTS[*]} " == *" $endpoint "* ]] && continue 245 if [[ $endpoint == *:* && $AUTO_ROUTE6 -eq 1 ]]; then 246 cmd route -q -n delete -inet6 "$endpoint" 2>/dev/null || true 247 elif [[ $AUTO_ROUTE4 -eq 1 ]]; then 248 cmd route -q -n delete -inet "$endpoint" 2>/dev/null || true 249 fi 250 done 251 252 for endpoint in "${ENDPOINTS[@]}"; do 253 if [[ $remove_all_old -eq 0 && " ${old_endpoints[*]} " == *" $endpoint "* ]]; then 254 added+=( "$endpoint" ) 255 continue 256 fi 257 if [[ $endpoint == *:* && $AUTO_ROUTE6 -eq 1 ]]; then 258 if [[ -n $GATEWAY6 ]]; then 259 cmd route -q -n add -inet6 "$endpoint" -gateway "$GATEWAY6" || true 260 else 261 # Prevent routing loop 262 cmd route -q -n add -inet6 "$endpoint" ::1 -blackhole || true 263 fi 264 added+=( "$endpoint" ) 265 elif [[ $AUTO_ROUTE4 -eq 1 ]]; then 266 if [[ -n $GATEWAY4 ]]; then 267 cmd route -q -n add -inet "$endpoint" -gateway "$GATEWAY4" || true 268 else 269 # Prevent routing loop 270 cmd route -q -n add -inet "$endpoint" 127.0.0.1 -blackhole || true 271 fi 272 added+=( "$endpoint" ) 273 fi 274 done 275 ENDPOINTS=( "${added[@]}" ) 276} 277 278monitor_daemon() { 279 echo "[+] Backgrounding route monitor" >&2 280 (make_temp 281 trap 'del_routes; clean_temp; exit 0' INT TERM EXIT 282 exec >/dev/null 2>&1 283 local event 284 # TODO: this should also check to see if the endpoint actually changes 285 # in response to incoming packets, and then call set_endpoint_direct_route 286 # then too. That function should be able to gracefully cleanup if the 287 # endpoints change. 288 while read -r event; do 289 [[ $event == RTM_* ]] || continue 290 [[ -e /var/run/wireguard/$INTERFACE.sock ]] || break 291 if_exists || break 292 [[ $AUTO_ROUTE4 -eq 1 || $AUTO_ROUTE6 -eq 1 ]] && set_endpoint_direct_route 293 # TODO: set the mtu as well, but only if up 294 done < <(route -n monitor)) & disown 295} 296 297HAVE_SET_DNS=0 298set_dns() { 299 [[ ${#DNS[@]} -gt 0 ]] || return 0 300 printf 'nameserver %s\n' "${DNS[@]}" | cmd resolvconf -a "$INTERFACE" -x 301 HAVE_SET_DNS=1 302} 303 304unset_dns() { 305 [[ ${#DNS[@]} -gt 0 ]] || return 0 306 cmd resolvconf -d "$INTERFACE" 307} 308 309add_route() { 310 [[ $TABLE != off ]] || return 0 311 312 local family=inet 313 [[ $1 == *:* ]] && family=inet6 314 315 if [[ -n $TABLE && $TABLE != auto ]]; then 316 cmd route -q -n add "-$family" -fib "$TABLE" "$1" -interface "$INTERFACE" 317 elif [[ $1 == */0 ]]; then 318 if [[ $1 == *:* ]]; then 319 AUTO_ROUTE6=1 320 cmd route -q -n add -inet6 ::/1 -interface "$INTERFACE" 321 cmd route -q -n add -inet6 8000::/1 -interface "$INTERFACE" 322 else 323 AUTO_ROUTE4=1 324 cmd route -q -n add -inet 0.0.0.0/1 -interface "$INTERFACE" 325 cmd route -q -n add -inet 128.0.0.0/1 -interface "$INTERFACE" 326 fi 327 else 328 [[ $(route -n get "-$family" "$1" 2>/dev/null) =~ interface:\ $INTERFACE$'\n' ]] || cmd route -q -n add "-$family" "$1" -interface "$INTERFACE" 329 fi 330} 331 332set_config() { 333 cmd wg setconf "$INTERFACE" <(echo "$WG_CONFIG") 334} 335 336save_config() { 337 local old_umask new_config current_config address cmd 338 new_config=$'[Interface]\n' 339 { read -r _; while read -r _ _ _ address _; do 340 new_config+="Address = $address"$'\n' 341 done } < <(netstat -I "$INTERFACE" -n -W -f inet) 342 { read -r _; while read -r _ _ _ address _; do 343 new_config+="Address = $address"$'\n' 344 done } < <(netstat -I "$INTERFACE" -n -W -f inet6) 345 while read -r address; do 346 [[ $address =~ ^nameserver\ ([a-zA-Z0-9_=+:%.-]+)$ ]] && new_config+="DNS = ${BASH_REMATCH[1]}"$'\n' 347 done < <(resolvconf -l "$INTERFACE" 2>/dev/null) 348 [[ -n $MTU ]] && new_config+="MTU = $MTU"$'\n' 349 [[ -n $TABLE ]] && new_config+="Table = $TABLE"$'\n' 350 [[ $SAVE_CONFIG -eq 0 ]] || new_config+=$'SaveConfig = true\n' 351 for cmd in "${PRE_UP[@]}"; do 352 new_config+="PreUp = $cmd"$'\n' 353 done 354 for cmd in "${POST_UP[@]}"; do 355 new_config+="PostUp = $cmd"$'\n' 356 done 357 for cmd in "${PRE_DOWN[@]}"; do 358 new_config+="PreDown = $cmd"$'\n' 359 done 360 for cmd in "${POST_DOWN[@]}"; do 361 new_config+="PostDown = $cmd"$'\n' 362 done 363 old_umask="$(umask)" 364 umask 077 365 current_config="$(cmd wg showconf "$INTERFACE")" 366 trap 'rm -f "$CONFIG_FILE.tmp"; clean_temp; exit' INT TERM EXIT 367 echo "${current_config/\[Interface\]$'\n'/$new_config}" > "$CONFIG_FILE.tmp" || die "Could not write configuration file" 368 sync "$CONFIG_FILE.tmp" 369 mv "$CONFIG_FILE.tmp" "$CONFIG_FILE" || die "Could not move configuration file" 370 trap 'clean_temp; exit' INT TERM EXIT 371 umask "$old_umask" 372} 373 374execute_hooks() { 375 local hook 376 for hook in "$@"; do 377 hook="${hook//%i/$INTERFACE}" 378 echo "[#] $hook" >&2 379 (eval "$hook") 380 done 381} 382 383cmd_usage() { 384 cat >&2 <<-_EOF 385 Usage: $PROGRAM [ up | down | save | strip ] [ CONFIG_FILE | INTERFACE ] 386 387 CONFIG_FILE is a configuration file, whose filename is the interface name 388 followed by \`.conf'. Otherwise, INTERFACE is an interface name, with 389 configuration found at: 390 ${CONFIG_SEARCH_PATHS[@]/%//INTERFACE.conf}. 391 It is to be readable by wg(8)'s \`setconf' sub-command, with the exception 392 of the following additions to the [Interface] section, which are handled 393 by $PROGRAM: 394 395 - Address: may be specified one or more times and contains one or more 396 IP addresses (with an optional CIDR mask) to be set for the interface. 397 - DNS: an optional DNS server to use while the device is up. 398 - MTU: an optional MTU for the interface; if unspecified, auto-calculated. 399 - Table: an optional routing table to which routes will be added; if 400 unspecified or \`auto', the default table is used. If \`off', no routes 401 are added. 402 - PreUp, PostUp, PreDown, PostDown: script snippets which will be executed 403 by bash(1) at the corresponding phases of the link, most commonly used 404 to configure DNS. The string \`%i' is expanded to INTERFACE. 405 - SaveConfig: if set to \`true', the configuration is saved from the current 406 state of the interface upon shutdown. 407 408 See wg-quick(8) for more info and examples. 409 _EOF 410} 411 412cmd_up() { 413 local i 414 [[ -z $(ifconfig "$INTERFACE" 2>/dev/null) ]] || die "\`$INTERFACE' already exists" 415 trap 'del_if; del_routes; clean_temp; exit' INT TERM EXIT 416 execute_hooks "${PRE_UP[@]}" 417 add_if 418 set_config 419 for i in "${ADDRESSES[@]}"; do 420 add_addr "$i" 421 done 422 set_mtu 423 up_if 424 set_dns 425 for i in $(while read -r _ i; do for i in $i; do [[ $i =~ ^[0-9a-z:.]+/[0-9]+$ ]] && echo "$i"; done; done < <(wg show "$INTERFACE" allowed-ips) | sort -nr -k 2 -t /); do 426 add_route "$i" 427 done 428 [[ $AUTO_ROUTE4 -eq 1 || $AUTO_ROUTE6 -eq 1 ]] && set_endpoint_direct_route 429 monitor_daemon 430 execute_hooks "${POST_UP[@]}" 431 trap 'clean_temp; exit' INT TERM EXIT 432} 433 434cmd_down() { 435 [[ " $(wg show interfaces) " == *" $INTERFACE "* ]] || die "\`$INTERFACE' is not a WireGuard interface" 436 execute_hooks "${PRE_DOWN[@]}" 437 [[ $SAVE_CONFIG -eq 0 ]] || save_config 438 del_if 439 unset_dns 440 execute_hooks "${POST_DOWN[@]}" 441} 442 443cmd_save() { 444 [[ " $(wg show interfaces) " == *" $INTERFACE "* ]] || die "\`$INTERFACE' is not a WireGuard interface" 445 save_config 446} 447 448cmd_strip() { 449 echo "$WG_CONFIG" 450} 451 452# ~~ function override insertion point ~~ 453 454make_temp 455trap 'clean_temp; exit' INT TERM EXIT 456 457if [[ $# -eq 1 && ( $1 == --help || $1 == -h || $1 == help ) ]]; then 458 cmd_usage 459elif [[ $# -eq 2 && $1 == up ]]; then 460 auto_su 461 parse_options "$2" 462 cmd_up 463elif [[ $# -eq 2 && $1 == down ]]; then 464 auto_su 465 parse_options "$2" 466 cmd_down 467elif [[ $# -eq 2 && $1 == save ]]; then 468 auto_su 469 parse_options "$2" 470 cmd_save 471elif [[ $# -eq 2 && $1 == strip ]]; then 472 auto_su 473 parse_options "$2" 474 cmd_strip 475else 476 cmd_usage 477 exit 1 478fi 479 480exit 0 481