dropbear: better handle interfaces
[openwrt/staging/jow.git] / package / network / services / dropbear / files / dropbear.init
1 #!/bin/sh /etc/rc.common
2 # Copyright (C) 2006-2010 OpenWrt.org
3 # Copyright (C) 2006 Carlos Sobrinho
4
5 START=19
6 STOP=50
7
8 USE_PROCD=1
9 PROG=/usr/sbin/dropbear
10 NAME=dropbear
11 PIDCOUNT=0
12
13 extra_command "killclients" "Kill ${NAME} processes except servers and yourself"
14
15 # most of time real_stat() will be failing
16 # due to missing "stat" binary (by default)
17 real_stat() { env stat -L "$@" 2>/dev/null ; }
18 dumb_stat() { ls -Ldln "$1" | tr -s '\t ' ' ' ; }
19 stat_perm() { real_stat -c '%A' "$1" || dumb_stat "$1" | cut -d ' ' -f 1 ; }
20 stat_owner() { real_stat -c '%u' "$1" || dumb_stat "$1" | cut -d ' ' -f 3 ; }
21
22 _dropbearkey()
23 {
24 /usr/bin/dropbearkey "$@" </dev/null >/dev/null 2>&1
25 }
26
27 # $1 - file name (host key or config)
28 file_verify()
29 {
30 [ -f "$1" ] || return 1
31 # checking file ownership
32 [ "$(stat_owner "$1")" = "0" ] || {
33 chown 0 "$1"
34 [ "$(stat_owner "$1")" = "0" ] || return 2
35 }
36 # checking file permissions
37 [ "$(stat_perm "$1")" = "-rw-------" ] || {
38 chmod 0600 "$1"
39 [ "$(stat_perm "$1")" = "-rw-------" ] || return 3
40 }
41 # file is host key or not?
42 # if $2 is empty string - file is "host key"
43 # if $2 is non-empty string - file is "config"
44 [ -z "$2" ] || return 0
45 # checking file contents (finally)
46 [ -s "$1" ] || return 4
47 _dropbearkey -y -f "$1" || return 5
48 return 0
49 }
50
51 # $1 - file_verify() return code
52 file_errmsg()
53 {
54 case "$1" in
55 0) ;;
56 1) echo "file does not exist" ;;
57 2) echo "file has wrong owner (must be owned by root)" ;;
58 3) echo "file has wrong permissions (must not have group/other write bit)" ;;
59 4) echo "file has zero length" ;;
60 5) echo "file is not valid host key or not supported" ;;
61 *) echo "unknown error" ;;
62 esac
63 }
64
65 # $1 - config option
66 # $2 - host key file name
67 hk_config()
68 {
69 local x m
70 file_verify "$2" ; x=$?
71 if [ "$x" = 0 ] ; then
72 procd_append_param command -r "$2"
73 return
74 fi
75 m=$(file_errmsg "$x")
76 logger -s -t "${NAME}" -p daemon.warn \
77 "Option '$1', skipping '$2': $m"
78 }
79
80 # $1 - host key file name
81 hk_config__keyfile() { hk_config keyfile "$1" ; }
82
83 ktype_all='ed25519 ecdsa rsa'
84
85 hk_generate_as_needed()
86 {
87 local hk_cfg_dir kgen ktype kfile hk_tmp_dir
88 hk_cfg_dir='/etc/dropbear'
89
90 [ -d "${hk_cfg_dir}" ] || mkdir -p "${hk_cfg_dir}"
91
92 kgen=
93 for ktype in ${ktype_all} ; do
94 kfile="${hk_cfg_dir}/dropbear_${ktype}_host_key"
95
96 if file_verify "${kfile}" ; then continue ; fi
97
98 kgen="${kgen}${kgen:+ }${ktype}"
99 done
100
101 # all keys are sane?
102 [ -n "${kgen}" ] || return 0
103
104 hk_tmp_dir=$(mktemp -d)
105 # system in bad state?
106 [ -n "${hk_tmp_dir}" ] || return 1
107
108 chmod 0700 "${hk_tmp_dir}"
109
110 for ktype in ${kgen} ; do
111 kfile="${hk_tmp_dir}/dropbear_${ktype}_host_key"
112
113 if ! _dropbearkey -t ${ktype} -f "${kfile}" ; then
114 # unsupported key type
115 rm -f "${kfile}"
116 continue
117 fi
118
119 chmod 0600 "${kfile}"
120 done
121
122 kgen=
123 for ktype in ${ktype_all} ; do
124 kfile="${hk_tmp_dir}/dropbear_${ktype}_host_key"
125
126 [ -s "${kfile}" ] || continue
127
128 kgen="${kgen}${kgen:+ }${ktype}"
129 done
130
131 if [ -n "${kgen}" ] ; then
132 for ktype in ${kgen} ; do
133 kfile="${hk_tmp_dir}/dropbear_${ktype}_host_key"
134 [ -s "${kfile}" ] || continue
135 mv -f "${kfile}" "${hk_cfg_dir}/"
136 done
137 fi
138
139 rm -rf "${hk_tmp_dir}"
140
141 # cleanup empty files
142 for ktype in ${ktype_all} ; do
143 kfile="${hk_cfg_dir}/dropbear_${ktype}_host_key"
144
145 [ -s "${kfile}" ] || rm -f "${kfile}"
146 done
147 }
148
149 # $1 - list with whitespace-separated elements
150 normalize_list()
151 {
152 printf '%s' "$1" | tr -s ' \r\n\t' ' ' | sed -E 's/^ //;s/ $//'
153 }
154
155 warn_multiple_interfaces()
156 {
157 logger -t "${NAME}" -p daemon.warn \
158 "Option '$1' should specify SINGLE interface but instead it lists interfaces: $2"
159 logger -t "${NAME}" -p daemon.warn \
160 "Consider creating per-interface instances instead!"
161 }
162
163 validate_section_dropbear()
164 {
165 uci_load_validate dropbear dropbear "$1" "$2" \
166 'PasswordAuth:bool:1' \
167 'enable:bool:1' \
168 'DirectInterface:string' \
169 'Interface:string' \
170 'GatewayPorts:bool:0' \
171 'ForceCommand:string' \
172 'RootPasswordAuth:bool:1' \
173 'RootLogin:bool:1' \
174 'rsakeyfile:file' \
175 'keyfile:list(file)' \
176 'BannerFile:file' \
177 'Port:port:22' \
178 'SSHKeepAlive:uinteger:300' \
179 'IdleTimeout:uinteger:0' \
180 'MaxAuthTries:uinteger:3' \
181 'RecvWindowSize:uinteger:262144' \
182 'mdns:bool:1'
183 }
184
185 dropbear_instance()
186 {
187 [ "$2" = 0 ] || {
188 echo "validation failed"
189 return 1
190 }
191
192 [ "${enable}" = "1" ] || return 1
193
194 local iface ndev ipaddrs
195
196 # 'DirectInterface' should specify single interface
197 # but end users may misinterpret this setting
198 DirectInterface=$(normalize_list "${DirectInterface}")
199
200 # 'Interface' should specify single interface
201 # but end users are often misinterpret this setting
202 Interface=$(normalize_list "${Interface}")
203
204 if [ -n "${Interface}" ] ; then
205 if [ -n "${DirectInterface}" ] ; then
206 logger -t "${NAME}" -p daemon.warn \
207 "Option 'DirectInterface' takes precedence over 'Interface'"
208 else
209 logger -t "${NAME}" -p daemon.info \
210 "Option 'Interface' binds to address(es) but not to interface"
211 logger -t "${NAME}" -p daemon.info \
212 "Consider using option 'DirectInterface' to bind directly to interface"
213 fi
214 fi
215
216 # handle 'DirectInterface'
217 iface=$(echo "${DirectInterface}" | awk '{print $1}')
218 case "${DirectInterface}" in
219 *\ *)
220 warn_multiple_interfaces DirectInterface "${DirectInterface}"
221 logger -t "${NAME}" -p daemon.warn \
222 "Using network interface '${iface}' for direct binding"
223 ;;
224 esac
225 while [ -n "${iface}" ] ; do
226 # if network is available (even during boot) - proceed
227 if network_is_up "${iface}" ; then break ; fi
228 # skip during boot
229 [ -z "${BOOT}" ] || return 0
230
231 logger -t "${NAME}" -p daemon.crit \
232 "Network interface '${iface}' is not available!"
233 return 1
234 done
235 while [ -n "${iface}" ] ; do
236 # ${iface} is logical (higher level) interface name
237 # ${ndev} is 'real' interface name
238 # e.g.: if ${iface} is 'lan' (default LAN interface) then ${ndev} is 'br-lan'
239 network_get_device ndev "${iface}"
240 [ -z "${ndev}" ] || break
241
242 logger -t "${NAME}" -p daemon.crit \
243 "Missing network device for network interface '${iface}'!"
244 return 1
245 done
246 if [ -n "${iface}" ] ; then
247 logger -t "${NAME}" -p daemon.info \
248 "Using network interface '${iface}' (network device '${ndev}') for direct binding"
249 fi
250 # handle 'Interface'
251 while [ -z "${iface}" ] ; do
252 [ -n "${Interface}" ] || break
253
254 # skip during boot
255 [ -z "${BOOT}" ] || return 0
256
257 case "${Interface}" in
258 *\ *)
259 warn_multiple_interfaces Interface "${Interface}"
260 ;;
261 esac
262
263 local c=0
264 # sysoptions.h
265 local DROPBEAR_MAX_PORTS=10
266
267 local a n if_ipaddrs
268 for n in ${Interface} ; do
269 [ -n "$n" ] || continue
270
271 if_ipaddrs=
272 network_get_ipaddrs_all if_ipaddrs "$n"
273 [ -n "${if_ipaddrs}" ] || {
274 logger -s -t "${NAME}" -p daemon.err \
275 "Network interface '$n' has no suitable IP address(es)!"
276 continue
277 }
278
279 [ $c -le ${DROPBEAR_MAX_PORTS} ] || {
280 logger -s -t "${NAME}" -p daemon.err \
281 "Network interface '$n' is NOT listened due to option limit exceed!"
282 continue
283 }
284
285 for a in ${if_ipaddrs} ; do
286 [ -n "$a" ] || continue
287
288 c=$((c+1))
289 if [ $c -le ${DROPBEAR_MAX_PORTS} ] ; then
290 ipaddrs="${ipaddrs} $a"
291 continue
292 fi
293
294 logger -t "${NAME}" -p daemon.err \
295 "Endpoint '$a:${Port}' on network interface '$n' is NOT listened due to option limit exceed!"
296 done
297 done
298 break
299 done
300
301 PIDCOUNT="$(( ${PIDCOUNT} + 1))"
302 local pid_file="/var/run/${NAME}.${PIDCOUNT}.pid"
303
304 procd_open_instance
305 procd_set_param command "$PROG" -F -P "$pid_file"
306 if [ -n "${iface}" ] ; then
307 # if ${iface} is non-empty then ${ndev} is non-empty too
308 procd_append_param command -l "${ndev}" -p "${Port}"
309 else
310 if [ -z "${ipaddrs}" ] ; then
311 procd_append_param command -p "${Port}"
312 else
313 local a
314 for a in ${ipaddrs} ; do
315 [ -n "$a" ] || continue
316 procd_append_param command -p "$a:${Port}"
317 done
318 fi
319 fi
320 [ "${PasswordAuth}" -eq 0 ] && procd_append_param command -s
321 [ "${GatewayPorts}" -eq 1 ] && procd_append_param command -a
322 [ -n "${ForceCommand}" ] && procd_append_param command -c "${ForceCommand}"
323 [ "${RootPasswordAuth}" -eq 0 ] && procd_append_param command -g
324 [ "${RootLogin}" -eq 0 ] && procd_append_param command -w
325 config_list_foreach "$1" 'keyfile' hk_config__keyfile
326 if [ -n "${rsakeyfile}" ]; then
327 logger -s -t "${NAME}" -p daemon.crit \
328 "Option 'rsakeyfile' is considered to be DEPRECATED and will be REMOVED in future releases, use 'keyfile' list instead"
329 sed -i.before-upgrade -E -e 's/option(\s+)rsakeyfile/list keyfile/' \
330 "/etc/config/${NAME}"
331 logger -s -t "${NAME}" -p daemon.crit \
332 "Auto-transition 'option rsakeyfile' => 'list keyfile' in /etc/config/${NAME} is done, please verify your configuration"
333 hk_config 'rsakeyfile' "${rsakeyfile}"
334 fi
335 [ -n "${BannerFile}" ] && procd_append_param command -b "${BannerFile}"
336 [ "${IdleTimeout}" -ne 0 ] && procd_append_param command -I "${IdleTimeout}"
337 [ "${SSHKeepAlive}" -ne 0 ] && procd_append_param command -K "${SSHKeepAlive}"
338 [ "${MaxAuthTries}" -ne 0 ] && procd_append_param command -T "${MaxAuthTries}"
339 [ "${RecvWindowSize}" -gt 0 ] && {
340 # NB: OpenWrt increases receive window size to increase throughput on high latency links
341 # ref: validate_section_dropbear()
342 # default receive window size is 24576 (DEFAULT_RECV_WINDOW in default_options.h)
343
344 # sysoptions.h
345 local MAX_RECV_WINDOW=10485760
346 if [ "${RecvWindowSize}" -gt ${MAX_RECV_WINDOW} ] ; then
347 # separate logging is required because syslog misses dropbear's message
348 # Bad recv window '${RecvWindowSize}', using ${MAX_RECV_WINDOW}
349 # it's probably dropbear issue but we should handle this and notify user
350 logger -s -t "${NAME}" -p daemon.warn \
351 "Option 'RecvWindowSize' is too high (${RecvWindowSize}), limiting to ${MAX_RECV_WINDOW}"
352 RecvWindowSize=${MAX_RECV_WINDOW}
353 fi
354 procd_append_param command -W "${RecvWindowSize}"
355 }
356 [ "${mdns}" -ne 0 ] && procd_add_mdns "ssh" "tcp" "$Port" "daemon=dropbear"
357 procd_set_param respawn
358 procd_close_instance
359 }
360
361 load_interfaces()
362 {
363 local enable
364 config_get enable "$1" enable 1
365 [ "${enable}" = "1" ] || return 0
366
367 local direct_iface iface
368 config_get direct_iface "$1" DirectInterface
369 direct_iface=$(normalize_list "${direct_iface}")
370 # 'DirectInterface' takes precedence over 'Interface'
371 if [ -n "${direct_iface}" ] ; then
372 iface=$(echo "${direct_iface}" | awk '{print $1}')
373 else
374 config_get iface "$1" Interface
375 iface=$(normalize_list "${iface}")
376 fi
377 interfaces="${interfaces} ${iface}"
378 }
379
380 boot()
381 {
382 BOOT=1
383 start "$@"
384 }
385
386 start_service()
387 {
388 hk_generate_as_needed
389 file_verify /etc/dropbear/authorized_keys config
390
391 . /lib/functions.sh
392 . /lib/functions/network.sh
393
394 config_load "${NAME}"
395 config_foreach validate_section_dropbear dropbear dropbear_instance
396 }
397
398 service_triggers()
399 {
400 local interfaces
401
402 procd_add_config_trigger "config.change" "${NAME}" /etc/init.d/dropbear reload
403
404 config_load "${NAME}"
405 config_foreach load_interfaces "${NAME}"
406
407 [ -n "${interfaces}" ] && {
408 local n
409 for n in $(printf '%s\n' ${interfaces} | sort -u) ; do
410 procd_add_interface_trigger "interface.*" $n /etc/init.d/dropbear reload
411 done
412 }
413
414 procd_add_validation validate_section_dropbear
415 }
416
417 shutdown() {
418 # close all open connections
419 killall dropbear
420 }
421
422 killclients()
423 {
424 local ignore=''
425 local server
426 local pid
427
428 # if this script is run from inside a client session, then ignore that session
429 pid="$$"
430 while [ "${pid}" -ne 0 ]
431 do
432 # get parent process id
433 pid=$(cut -d ' ' -f 4 "/proc/${pid}/stat")
434 [ "${pid}" -eq 0 ] && break
435
436 # check if client connection
437 grep -F -q -e "${PROG}" "/proc/${pid}/cmdline" && {
438 append ignore "${pid}"
439 break
440 }
441 done
442
443 # get all server pids that should be ignored
444 for server in $(cat /var/run/${NAME}.*.pid)
445 do
446 append ignore "${server}"
447 done
448
449 # get all running pids and kill client connections
450 local skip
451 for pid in $(pidof "${NAME}")
452 do
453 # check if correct program, otherwise process next pid
454 grep -F -q -e "${PROG}" "/proc/${pid}/cmdline" || {
455 continue
456 }
457
458 # check if pid should be ignored (servers, ourself)
459 skip=0
460 for server in ${ignore}
461 do
462 if [ "${pid}" = "${server}" ]
463 then
464 skip=1
465 break
466 fi
467 done
468 [ "${skip}" -ne 0 ] && continue
469
470 # kill process
471 echo "${initscript}: Killing ${pid}..."
472 kill -KILL ${pid}
473 done
474 }