@@ -, +, @@ conformance (and more besides) --- net/l2tp.sh | 349 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 206 insertions(+), 143 deletions(-) --- a/net/l2tp.sh +++ a/net/l2tp.sh @@ -1,6 +1,11 @@ # Copyright (c) 2016 Emeric Verschuur +# Copyright (c) 2023 Kerin Millar # All rights reserved. Released under the 2-clause BSD license. -# shellcheck shell=sh disable=SC1008 + +# Don't complain about local, even though POSIX does not define its behaviour. +# This is unwise but, as things stand, it is being used extensively by netifrc. +# Also, SC2034 and SC2316 are muted because they produce false-positives. +# shellcheck shell=sh disable=SC3043,SC2034,SC2316 l2tp_depend() { @@ -8,166 +13,224 @@ l2tp_depend() before bridge interface macchanger } -# Extract parameter list to shell vars -# 1. variable prefix -# 2. string to parse -_l2tp_eval_props() { - local prop_pref=$1 - local prop_list=$2 - eval set -- "$3" - while [ -n "$1" ]; do - eval "case $1 in - $prop_list) - $prop_pref$1=\"$2\" - shift - shift - ;; - *) - l2tp_err=\"invalid property $1\" - return 1 - ;; - - esac" || return 1 - done - return 0 +_l2tp_parse_opts() +{ + # Parses lt2psession or l2tptunnel options using xargs(1), conveying + # them as arguments to awk(1). The awk program interprets the arguments + # as a series of key/value pairs and safely prints those specified as + # being required as variable declarations for evaluation by sh(1). + # Other keys are handled similarly, only in a way that renders them a + # no-op. For the program to exit successfully, all key names must be + # well-formed, all required keys must be seen, and all values must be + # non-blank. Note that assigning 1 to ARGC prevents awk from treating + # its arguments as the names of files to be opened. + printf %s "$1" \ + | LC_CTYPE=C xargs -E '' awk -v q="'" -v required_keys="$2" -v other_keys="$3" ' + function shquote(str) { + gsub(q, q "\\" q q, str) + return q str q + } + BEGIN { + argc = ARGC + ARGC = 1 + gsub(" ", "|", required_keys) + gsub(" ", "|", other_keys) + re = "^(" required_keys "|" other_keys ")$" + sorter = "sort" + for (i = 1; i < argc; i += 2) { + key = ARGV[i] + val = ARGV[i + 1] + if (key !~ /^[[:alpha:]][_[:alnum:]]+$/) { + system("ewarn " shquote("Skipping malformed parameter: " key)) + } else if (key ~ re) { + print key "=" shquote(val) | sorter + val_by[key] = val + } else { + print ": " key "=" shquote(val) | sorter + } + } + close(sorter) + split(required_keys, keys, "|") + missing = 0 + for (i in keys) { + key = keys[i] + if (! (key in val_by)) { + system("eerror " shquote("The \"" key "\" parameter is missing")) + missing += 1 + } else if (val_by[key] ~ /^[[:blank:]]*$/) { + system("eerror " shquote("The \"" key "\" parameter has a blank value")) + missing += 1 + } + } + exit(!!missing) + } + ' } -_is_l2tp() { - # Check for L2TP support in kernel - ip l2tp show session 2>/dev/null 1>/dev/null || return 1 - - eval "$(ip l2tp show session | \ - awk "match(\$0, /^Session ([0-9]+) in tunnel ([0-9]+)\$/, ret) {sid=ret[1]; tid=ret[2]} - match(\$0, /^[ ]*interface name: ${IFACE}\$/) {print \"session_id=\"sid\";tunnel_id=\"tid; exit}")" - test -n "$session_id" +_l2tp_parse_existing_session() { + ip l2tp show session \ + | LC_CTYPE=C awk -v iface="${IFACE:?}" ' + BEGIN { found = 0 } + /^Session [0-9]+ in tunnel [0-9]+$/ { + session_id = $2 + tunnel_id = $5 + } + /^[[:blank:]]*interface name:/ && "" $NF == "" iface { + print "session_id=" session_id + print "tunnel_id=" tunnel_id + found = 1 + exit + } + END { exit(!found) } + ' } -# Get tunnel info -# 1. Output variable prefix -# 2. Tunnel ID to find -_l2tp_get_tunnel_info() { - local found - eval "$(ip l2tp show tunnel | \ - awk -v id=$2 -v prefix=$1 ' - match($0, /^Tunnel ([0-9]+), encap (IP|UDP)$/, ret) { - if (found == "1") exit; - if (ret[1] == id) { - print "found=1;" - print prefix "tunnel_id=" ret[1] ";" - print prefix "encap=" ret[2] ";"; - found="1" - } - } - match($0, /^[ ]*From ([^ ]+) to ([^ ]+)$/, ret) { - if (found == "1") { - print prefix "local=" ret[1] ";"; - print prefix "remote=" ret[2] ";"; - } + +_l2tp_parse_existing_tunnel() { + ip l2tp show tunnel \ + | LC_CTYPE=C awk -v q="'" -v id="$1" ' + function shquote(str) { + gsub(q, q "\\" q q, str) + return q str q } - match($0, /^[ ]*Peer tunnel ([0-9]+)$/, ret) { - if (found == "1") { - print prefix "peer_tunnel_id=" ret[1] ";"; - } + BEGIN { + found = 0 + sorter = "sort" } - match($0, /^[ ]*UDP source \/ dest ports: ([0-9]+)\/([0-9]+)$/, ret) { - if (found == "1") { - print prefix "udp_sport=" ret[1] ";"; - print prefix "udp_dport=" ret[2] ";"; + /^Tunnel [0-9]+, encap (IP|UDP)$/ { + if (found) exit + tunnel_id = substr($2, 0, length($2) - 1) + if (tunnel_id == id) { + found = 1 + print "tunnel_id=" shquote(tunnel_id) | sorter + print "encap=" shquote(tolower($4)) | sorter } - }')" - test -n "$found" + } + found && /^[[:blank:]]*From [^[:blank:]]+ to [^[:blank:]]+$/ { + print "local=" shquote($2) | sorter + print "remote=" shquote($4) | sorter + } + found && /^[[:blank:]]*Peer tunnel [0-9]+$/ { + print "peer_tunnel_id=" shquote($NF) | sorter + } + found && /^[[:blank:]]*UDP source \/ dest ports: [0-9]+\/[0-9]+$/ { + split($NF, ports, "/") + print ": udp_sport=" shquote(ports[1]) | sorter + print ": udp_dport=" shquote(ports[2]) | sorter + } + END { + close(sorter) + exit(!found) + } + ' } -_ip_l2tp_add() { - local e - e="$(LC_ALL=C ip l2tp add "$@" 2>&1 1>/dev/null)" - case $e in - "") - return 0 - ;; - "RTNETLINK answers: No such process") - # seems to not be a fatal error but I don't know why I have this error... hmmm - ewarn "ip l2tp add $2 error: $e" - return 0 - ;; - *) - eend 1 "ip l2tp add $2 error: $e" - return 1 - ;; - esac - +_l2tp_should_add_tunnel() { + local existing_tunnel + + if ! existing_tunnel=$(_l2tp_parse_existing_tunnel "$1"); then + return 0 + elif [ "$2" = "${existing_tunnel}" ]; then + return 1 + else + return 2 + fi } +_l2tp_has_tunnel() { + _l2tp_parse_existing_tunnel "$1" >/dev/null +} + +_l2tp_in_session() { + ip l2tp show session | { + LC_CTYPE=C + while read -r line; do + case ${line} in + "Session "*" in tunnel $1") return 0 + esac + done + } + return 1 +} + +_is_blank() ( + LC_CTYPE=C + case $1 in + *[![:blank:]]*) return 1 + esac +) + l2tp_pre_start() { - local l2tpsession= - eval l2tpsession=\$l2tpsession_${IFVAR} - test -n "${l2tpsession}" || return 0 - - ebegin "Creating L2TPv3 link ${IFVAR}" - local l2tp_err s_name s_tunnel_id s_session_id s_peer_session_id s_cookie s_peer_cookie s_offset s_peer_offset s_l2spec_type - if ! _l2tp_eval_props s_ "name|tunnel_id|session_id|peer_session_id|cookie|peer_cookie|offset|peer_offset|l2spec_type" "${l2tpsession}"; then - eend 1 "l2tpsession_${IFVAR} syntax error: $l2tp_err" - return 1 - fi - if [ -n "$s_name" ]; then - eend 1 "l2tpsession_${IFVAR} error: please remove the \"name\" parameter (this parameter is managed by the system)" - return 1 - fi - # Try to load mendatory l2tp_eth kernel module - if ! modprobe l2tp_eth; then - eend 1 "l2tp_eth module not present in your kernel (please enable CONFIG_L2TP_ETH option in your kernel config)" - return 1 - fi - local l2tptunnel= - eval l2tptunnel=\$l2tptunnel_${IFVAR} - if [ -n "${l2tptunnel}" ]; then - local t_tunnel_id t_encap t_local t_remote t_peer_tunnel_id t_udp_sport t_udp_dport - _l2tp_eval_props t_ "remote|local|encap|tunnel_id|peer_tunnel_id|encap|udp_sport|udp_dport" "${l2tptunnel}" - # if encap=ip we need l2tp_ip kernel module - if [ "${t_encap^^}" = "IP" ] && ! modprobe l2tp_ip; then - eend 1 "l2tp_ip module not present in your kernel (please enable CONFIG_L2TP_IP option in your kernel config)" - return 1 - fi - # Search for an existing tunnel with the same ID - local f_tunnel_id f_encap f_local f_remote f_peer_tunnel_id f_udp_sport f_udp_dport - if _l2tp_get_tunnel_info f_ $t_tunnel_id; then - # check if the existing tunnel has the same property than expected - if [ "tunnel_id:$f_tunnel_id;encap:$f_encap;local:$f_local;remote:$f_remote; - peer_tunnel_id:$f_peer_tunnel_id;udp_sport:$f_udp_sport;udp_dport:$f_udp_dport" \ - != "tunnel_id:$t_tunnel_id;encap:${t_encap^^};local:$t_local;remote:$t_remote; - peer_tunnel_id:$t_peer_tunnel_id;udp_sport:$t_udp_sport;udp_dport:$t_udp_dport" ]; then - eend 1 "There are an existing tunnel with id=$s_tunnel_id, but the properties mismatch with the one you want to create" - return 1 - fi + local declared_session declared_tunnel l2tpsession l2tptunnel + local name peer_session_id session_id tunnel_id + local encap local peer_tunnel_id remote + local key + + if key="l2tpsession_${IFVAR:?}"; ! eval "[ \${${key}+set} ]"; then + return + elif eval "l2tpsession=\$${key}"; _is_blank "${l2tpsession}"; then + eend 1 "${key} is defined but its value is blank" + elif ! declared_session=$(_l2tp_parse_opts "${l2tpsession}" "peer_session_id session_id tunnel_id" "name"); then + eend 1 "${key} is missing at least one required parameter" + elif eval "${declared_session}"; [ "${name+set}" ]; then + eend 1 "${key} defines a \"name\" parameter, which is forbidden by netifrc" + elif ! modprobe l2tp_eth; then + eend 1 "Couldn't load the l2tp_eth module (perhaps the CONFIG_L2TP_ETH kernel option is disabled)" + elif key="l2tptunnel_${IFVAR}"; eval "[ \${${key}+set} ]"; then + if eval "l2tptunnel=\$${key}"; _is_blank "${l2tptunnel}"; then + eend 1 "${key} is defined but its value is blank" + elif ! declared_tunnel=$(_l2tp_parse_opts "${l2tptunnel}" "local peer_tunnel_id remote tunnel_id" "encap"); then + eend 1 "${key} is missing at least one required parameter" + elif set -- "${tunnel_id}"; eval "${declared_tunnel}"; [ "$1" != "${tunnel_id}" ]; then + eend 1 "${key} defines a \"tunnel_id\" parameter that contradicts l2tpsession_${IFVAR}" + elif _l2tp_should_add_tunnel "${tunnel_id}" "${declared_tunnel}"; set -- $?; [ "$1" -eq 2 ]; then + eend 1 "Tunnel #${tunnel_id} exists but its properties mismatch those defined by ${key}" + elif [ "$1" -eq 1 ]; then + # The config matches an existing tunnel. + true + elif [ "${encap}" = ip ] && ! modprobe l2tp_ip; then + eend 1 "Couldn't load the l2tp_ip module (perhaps the CONFIG_L2TP_IP kernel option is disabled)" else - veinfo ip l2tp add tunnel ${l2tptunnel} - _ip_l2tp_add tunnel ${l2tptunnel} || return 1 + ebegin "Creating L2TPv3 tunnel (tunnel_id ${tunnel_id})" + printf %s "l2tp add tunnel ${l2tptunnel}" \ + | xargs -E '' ip + eend $? fi - elif ! ip l2tp show tunnel | grep -Eq "^Tunnel $s_tunnel_id,"; then - # no l2tptunnel_ declaration, assume that the tunnel is already present - # checking if tunnel_id exists otherwise raise an error - eend 1 "Tunnel id=$s_tunnel_id no found (you may have to set l2tptunnel_${IFVAR})" - return 1 - fi - veinfo ip l2tp add session ${l2tpsession} name "${IFACE}" - _ip_l2tp_add session ${l2tpsession} name "${IFACE}" || return 1 - _up -} + elif ! _l2tp_has_tunnel "${tunnel_id}"; then + # A tunnel may incorporate more than one session (link). This + # module allows for the user not to define a tunnel for a given + # session. In that case, it will be expected that the required + # tunnel has already been created to satisfy some other session. + eend 1 "Tunnel #${tunnel_id} not found (defining ${key} may be required)" + fi || return + ebegin "Creating L2TPv3 session (session_id ${session_id} tunnel_id ${tunnel_id})" + printf %s "l2tp add session ${l2tpsession} name ${IFACE:?}" \ + | xargs -E '' ip && _up + eend $? +} l2tp_post_stop() { - local session_id tunnel_id - _is_l2tp || return 0 - - ebegin "Destroying L2TPv3 link ${IFACE}" - veinfo ip l2tp del session tunnel_id $tunnel_id session_id $session_id - ip l2tp del session tunnel_id $tunnel_id session_id $session_id - if ! ip l2tp show session | grep -Eq "^Session [0-9]+ in tunnel $tunnel_id\$"; then - #tunnel $tunnel_id no longer used, destoying it... - veinfo ip l2tp del tunnel tunnel_id $tunnel_id - ip l2tp del tunnel tunnel_id $tunnel_id + local existing_session session_id tunnel_id + + # This function may be invoked for every interface. If not a virtual + # interface, it can't possibly be one that's managed by this module, in + # which case running ip(8) and awk(1) would be a needless expense. + [ -e /sys/devices/virtual/net/"${IFACE:?}" ] \ + && existing_session=$(_l2tp_parse_existing_session 2>/dev/null) \ + || return 0 + + eval "${existing_session}" + set -- session_id "${session_id}" tunnel_id "${tunnel_id}" + ebegin "Destroying L2TPv3 session ($*)" + ip l2tp del session "$@" + eend $? && + if ! _l2tp_in_session "${tunnel_id}"; then + shift 2 + ebegin "Destroying L2TPv3 tunnel ($*)" + ip l2tp del tunnel "$@" + eend $? fi - eend $? } --