aboutsummaryrefslogtreecommitdiff
blob: dde86af4f2409183034cdfe5533c6248ff5221a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
# Copyright (c) 2016 Emeric Verschuur <emeric@mbedsys.org>
# Copyright (c) 2023 Kerin Millar <kfm@plushkava.net>
# All rights reserved. Released under the 2-clause BSD license.

# 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() {
	program ip
	before bridge interface macchanger
}

l2tp_pre_start() {
	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
		# 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.
		if ! _l2tp_has_tunnel "${tunnel_id}"; then
			eend 1 "Tunnel #${tunnel_id} not found (defining ${key} may be required)"
		fi
	elif 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_${IFACE:?}"
	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
		ebegin "Creating L2TPv3 tunnel (tunnel_id ${tunnel_id})"
		printf %s "l2tp add tunnel ${l2tptunnel}" \
		| xargs -E '' ip
		eend $?
	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 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
}

_is_blank() (
	LC_CTYPE=C
	case $1 in
		*[![:blank:]]*) return 1
	esac
)

_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)
		}
	'
}

_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) }
	'
}


_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
		}
		BEGIN {
			found = 0
			sorter = "sort"
		}
		/^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
			}
		}
		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)
		}
	'
}

_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
}