# Copyright (C) 2006-2010 OpenWrt.org
# Copyright (C) 2006 Carlos Sobrinho
-START=50
+START=19
STOP=50
USE_PROCD=1
-
-NAME=dropbear
PROG=/usr/sbin/dropbear
+NAME=dropbear
PIDCOUNT=0
-EXTRA_COMMANDS="killclients"
-EXTRA_HELP=" killclients Kill ${NAME} processes except servers and yourself"
-dropbear_instance()
+extra_command "killclients" "Kill ${NAME} processes except servers and yourself"
+
+# most of time real_stat() will be failing
+# due to missing "stat" binary (by default)
+real_stat() { env stat -L "$@" 2>/dev/null ; }
+dumb_stat() { ls -Ldln "$1" | tr -s '\t ' ' ' ; }
+stat_perm() { real_stat -c '%A' "$1" || dumb_stat "$1" | cut -d ' ' -f 1 ; }
+stat_owner() { real_stat -c '%u' "$1" || dumb_stat "$1" | cut -d ' ' -f 3 ; }
+
+_dropbearkey()
{
- append_ports()
- {
- local ifname="$1"
- local port="$2"
-
- grep -qs "^ *$ifname:" /proc/net/dev || {
- procd_append_param command -p "$port"
- return
- }
+ /usr/bin/dropbearkey "$@" </dev/null >/dev/null 2>&1
+}
+
+# $1 - file name (host key or config)
+file_verify()
+{
+ [ -f "$1" ] || return 1
+ # checking file ownership
+ [ "$(stat_owner "$1")" = "0" ] || {
+ chown 0 "$1"
+ [ "$(stat_owner "$1")" = "0" ] || return 2
+ }
+ # checking file permissions
+ [ "$(stat_perm "$1")" = "-rw-------" ] || {
+ chmod 0600 "$1"
+ [ "$(stat_perm "$1")" = "-rw-------" ] || return 3
+ }
+ # file is host key or not?
+ # if $2 is empty string - file is "host key"
+ # if $2 is non-empty string - file is "config"
+ [ -z "$2" ] || return 0
+ # checking file contents (finally)
+ [ -s "$1" ] || return 4
+ _dropbearkey -y -f "$1" || return 5
+ return 0
+}
+
+# $1 - file_verify() return code
+file_errmsg()
+{
+ case "$1" in
+ 0) ;;
+ 1) echo "file does not exist" ;;
+ 2) echo "file has wrong owner (must be owned by root)" ;;
+ 3) echo "file has wrong permissions (must not have group/other write bit)" ;;
+ 4) echo "file has zero length" ;;
+ 5) echo "file is not valid host key or not supported" ;;
+ *) echo "unknown error" ;;
+ esac
+}
+
+# $1 - config option
+# $2 - host key file name
+hk_config()
+{
+ local x m
+ file_verify "$2" ; x=$?
+ if [ "$x" = 0 ] ; then
+ procd_append_param command -r "$2"
+ return
+ fi
+ m=$(file_errmsg "$x")
+ logger -s -t "${NAME}" -p daemon.warn \
+ "Option '$1', skipping '$2': $m"
+}
+
+# $1 - host key file name
+hk_config__keyfile() { hk_config keyfile "$1" ; }
+
+ktype_all='ed25519 ecdsa rsa'
+
+hk_generate_as_needed()
+{
+ local hk_cfg_dir kgen ktype kfile hk_tmp_dir
+ hk_cfg_dir='/etc/dropbear'
- for addr in $(
- ifconfig "$ifname" | sed -ne '
- /addr: *fe[89ab][0-9a-f]:/d
- s/.* addr: *\([0-9a-f:\.]*\).*/\1/p
- '
- ); do
- procd_append_param command -p "$addr:$port"
+ [ -d "${hk_cfg_dir}" ] || mkdir -p "${hk_cfg_dir}"
+
+ kgen=
+ for ktype in ${ktype_all} ; do
+ kfile="${hk_cfg_dir}/dropbear_${ktype}_host_key"
+
+ if file_verify "${kfile}" ; then continue ; fi
+
+ kgen="${kgen}${kgen:+ }${ktype}"
+ done
+
+ # all keys are sane?
+ [ -n "${kgen}" ] || return 0
+
+ hk_tmp_dir=$(mktemp -d)
+ # system in bad state?
+ [ -n "${hk_tmp_dir}" ] || return 1
+
+ chmod 0700 "${hk_tmp_dir}"
+
+ for ktype in ${kgen} ; do
+ kfile="${hk_tmp_dir}/dropbear_${ktype}_host_key"
+
+ if ! _dropbearkey -t ${ktype} -f "${kfile}" ; then
+ # unsupported key type
+ rm -f "${kfile}"
+ continue
+ fi
+
+ chmod 0600 "${kfile}"
+ done
+
+ kgen=
+ for ktype in ${ktype_all} ; do
+ kfile="${hk_tmp_dir}/dropbear_${ktype}_host_key"
+
+ [ -s "${kfile}" ] || continue
+
+ kgen="${kgen}${kgen:+ }${ktype}"
+ done
+
+ if [ -n "${kgen}" ] ; then
+ for ktype in ${kgen} ; do
+ kfile="${hk_tmp_dir}/dropbear_${ktype}_host_key"
+ [ -s "${kfile}" ] || continue
+ mv -f "${kfile}" "${hk_cfg_dir}/"
done
+ fi
+
+ rm -rf "${hk_tmp_dir}"
+
+ # cleanup empty files
+ for ktype in ${ktype_all} ; do
+ kfile="${hk_cfg_dir}/dropbear_${ktype}_host_key"
+
+ [ -s "${kfile}" ] || rm -f "${kfile}"
+ done
+}
+
+# $1 - list with whitespace-separated elements
+normalize_list()
+{
+ printf '%s' "$1" | tr -s ' \r\n\t' ' ' | sed -E 's/^ //;s/ $//'
+}
+
+warn_multiple_interfaces()
+{
+ logger -t "${NAME}" -p daemon.warn \
+ "Option '$1' should specify SINGLE interface but instead it lists interfaces: $2"
+ logger -t "${NAME}" -p daemon.warn \
+ "Consider creating per-interface instances instead!"
+}
+
+validate_section_dropbear()
+{
+ uci_load_validate dropbear dropbear "$1" "$2" \
+ 'PasswordAuth:bool:1' \
+ 'enable:bool:1' \
+ 'DirectInterface:string' \
+ 'Interface:string' \
+ 'GatewayPorts:bool:0' \
+ 'ForceCommand:string' \
+ 'RootPasswordAuth:bool:1' \
+ 'RootLogin:bool:1' \
+ 'rsakeyfile:file' \
+ 'keyfile:list(file)' \
+ 'BannerFile:file' \
+ 'Port:port:22' \
+ 'SSHKeepAlive:uinteger:300' \
+ 'IdleTimeout:uinteger:0' \
+ 'MaxAuthTries:uinteger:3' \
+ 'RecvWindowSize:uinteger:262144' \
+ 'mdns:bool:1'
+}
+
+dropbear_instance()
+{
+ [ "$2" = 0 ] || {
+ echo "validation failed"
+ return 1
}
+ [ "${enable}" = "1" ] || return 1
- local section="$1"
+ local iface ndev ipaddrs
- # check if section is enabled (default)
- local enabled
- config_get_bool enabled "${section}" enable 1
- [ "${enabled}" -eq 0 ] && return 1
+ # 'DirectInterface' should specify single interface
+ # but end users may misinterpret this setting
+ DirectInterface=$(normalize_list "${DirectInterface}")
- # increase pid file count to handle multiple instances correctly
- PIDCOUNT="$(( ${PIDCOUNT} + 1))"
+ # 'Interface' should specify single interface
+ # but end users are often misinterpret this setting
+ Interface=$(normalize_list "${Interface}")
+
+ if [ -n "${Interface}" ] ; then
+ if [ -n "${DirectInterface}" ] ; then
+ logger -t "${NAME}" -p daemon.warn \
+ "Option 'DirectInterface' takes precedence over 'Interface'"
+ else
+ logger -t "${NAME}" -p daemon.info \
+ "Option 'Interface' binds to address(es) but not to interface"
+ logger -t "${NAME}" -p daemon.info \
+ "Consider using option 'DirectInterface' to bind directly to interface"
+ fi
+ fi
+
+ # handle 'DirectInterface'
+ iface=$(echo "${DirectInterface}" | awk '{print $1}')
+ case "${DirectInterface}" in
+ *\ *)
+ warn_multiple_interfaces DirectInterface "${DirectInterface}"
+ logger -t "${NAME}" -p daemon.warn \
+ "Using network interface '${iface}' for direct binding"
+ ;;
+ esac
+ while [ -n "${iface}" ] ; do
+ # if network is available (even during boot) - proceed
+ if network_is_up "${iface}" ; then break ; fi
+ # skip during boot
+ [ -z "${BOOT}" ] || return 0
+
+ logger -t "${NAME}" -p daemon.crit \
+ "Network interface '${iface}' is not available!"
+ return 1
+ done
+ while [ -n "${iface}" ] ; do
+ # ${iface} is logical (higher level) interface name
+ # ${ndev} is 'real' interface name
+ # e.g.: if ${iface} is 'lan' (default LAN interface) then ${ndev} is 'br-lan'
+ network_get_device ndev "${iface}"
+ [ -z "${ndev}" ] || break
+
+ logger -t "${NAME}" -p daemon.crit \
+ "Missing network device for network interface '${iface}'!"
+ return 1
+ done
+ if [ -n "${iface}" ] ; then
+ logger -t "${NAME}" -p daemon.info \
+ "Using network interface '${iface}' (network device '${ndev}') for direct binding"
+ fi
+ # handle 'Interface'
+ while [ -z "${iface}" ] ; do
+ [ -n "${Interface}" ] || break
+
+ # skip during boot
+ [ -z "${BOOT}" ] || return 0
+
+ case "${Interface}" in
+ *\ *)
+ warn_multiple_interfaces Interface "${Interface}"
+ ;;
+ esac
+
+ local c=0
+ # sysoptions.h
+ local DROPBEAR_MAX_PORTS=10
+
+ local a n if_ipaddrs
+ for n in ${Interface} ; do
+ [ -n "$n" ] || continue
+
+ if_ipaddrs=
+ network_get_ipaddrs_all if_ipaddrs "$n"
+ [ -n "${if_ipaddrs}" ] || {
+ logger -s -t "${NAME}" -p daemon.err \
+ "Network interface '$n' has no suitable IP address(es)!"
+ continue
+ }
+ [ $c -le ${DROPBEAR_MAX_PORTS} ] || {
+ logger -s -t "${NAME}" -p daemon.err \
+ "Network interface '$n' is NOT listened due to option limit exceed!"
+ continue
+ }
+
+ for a in ${if_ipaddrs} ; do
+ [ -n "$a" ] || continue
+
+ c=$((c+1))
+ if [ $c -le ${DROPBEAR_MAX_PORTS} ] ; then
+ ipaddrs="${ipaddrs} $a"
+ continue
+ fi
+
+ logger -t "${NAME}" -p daemon.err \
+ "Endpoint '$a:${Port}' on network interface '$n' is NOT listened due to option limit exceed!"
+ done
+ done
+ break
+ done
+
+ PIDCOUNT="$(( ${PIDCOUNT} + 1))"
local pid_file="/var/run/${NAME}.${PIDCOUNT}.pid"
procd_open_instance
procd_set_param command "$PROG" -F -P "$pid_file"
+ if [ -n "${iface}" ] ; then
+ # if ${iface} is non-empty then ${ndev} is non-empty too
+ procd_append_param command -l "${ndev}" -p "${Port}"
+ else
+ if [ -z "${ipaddrs}" ] ; then
+ procd_append_param command -p "${Port}"
+ else
+ local a
+ for a in ${ipaddrs} ; do
+ [ -n "$a" ] || continue
+ procd_append_param command -p "$a:${Port}"
+ done
+ fi
+ fi
+ [ "${PasswordAuth}" -eq 0 ] && procd_append_param command -s
+ [ "${GatewayPorts}" -eq 1 ] && procd_append_param command -a
+ [ -n "${ForceCommand}" ] && procd_append_param command -c "${ForceCommand}"
+ [ "${RootPasswordAuth}" -eq 0 ] && procd_append_param command -g
+ [ "${RootLogin}" -eq 0 ] && procd_append_param command -w
+ config_list_foreach "$1" 'keyfile' hk_config__keyfile
+ if [ -n "${rsakeyfile}" ]; then
+ logger -s -t "${NAME}" -p daemon.crit \
+ "Option 'rsakeyfile' is considered to be DEPRECATED and will be REMOVED in future releases, use 'keyfile' list instead"
+ sed -i.before-upgrade -E -e 's/option(\s+)rsakeyfile/list keyfile/' \
+ "/etc/config/${NAME}"
+ logger -s -t "${NAME}" -p daemon.crit \
+ "Auto-transition 'option rsakeyfile' => 'list keyfile' in /etc/config/${NAME} is done, please verify your configuration"
+ hk_config 'rsakeyfile' "${rsakeyfile}"
+ fi
+ [ -n "${BannerFile}" ] && procd_append_param command -b "${BannerFile}"
+ [ "${IdleTimeout}" -ne 0 ] && procd_append_param command -I "${IdleTimeout}"
+ [ "${SSHKeepAlive}" -ne 0 ] && procd_append_param command -K "${SSHKeepAlive}"
+ [ "${MaxAuthTries}" -ne 0 ] && procd_append_param command -T "${MaxAuthTries}"
+ [ "${RecvWindowSize}" -gt 0 ] && {
+ # NB: OpenWrt increases receive window size to increase throughput on high latency links
+ # ref: validate_section_dropbear()
+ # default receive window size is 24576 (DEFAULT_RECV_WINDOW in default_options.h)
- # prepare parameters (initialise with pid file)
- local val
-
- # A) password authentication
- config_get_bool val "${section}" PasswordAuth 1
- [ "${val}" -eq 0 ] && procd_append_param command -s
-
- # B) listen interface and port
- local port
- local interface
- config_get interface "${section}" Interface
- [ -n "$interface" ] && network_get_device interface "$interface"
- config_get port "${section}" Port 22
- append_ports "$interface" "$port"
- # C) banner file
- config_get val "${section}" BannerFile
- [ -f "${val}" ] && procd_append_param command -b "${val}"
- # D) gatewayports
- config_get_bool val "${section}" GatewayPorts 0
- [ "${val}" -eq 1 ] && procd_append_param command -a
- # E) root password authentication
- config_get_bool val "${section}" RootPasswordAuth 1
- [ "${val}" -eq 0 ] && procd_append_param command -g
- # F) root login
- config_get_bool val "${section}" RootLogin 1
- [ "${val}" -eq 0 ] && procd_append_param command -w
- # G) host keys
- config_get val "${section}" rsakeyfile
- [ -f "${val}" ] && procd_append_param command -r "${val}"
- config_get val "${section}" dsskeyfile
- [ -f "${val}" ] && procd_append_param command -d "${val}"
-
+ # sysoptions.h
+ local MAX_RECV_WINDOW=10485760
+ if [ "${RecvWindowSize}" -gt ${MAX_RECV_WINDOW} ] ; then
+ # separate logging is required because syslog misses dropbear's message
+ # Bad recv window '${RecvWindowSize}', using ${MAX_RECV_WINDOW}
+ # it's probably dropbear issue but we should handle this and notify user
+ logger -s -t "${NAME}" -p daemon.warn \
+ "Option 'RecvWindowSize' is too high (${RecvWindowSize}), limiting to ${MAX_RECV_WINDOW}"
+ RecvWindowSize=${MAX_RECV_WINDOW}
+ fi
+ procd_append_param command -W "${RecvWindowSize}"
+ }
+ [ "${mdns}" -ne 0 ] && procd_add_mdns "ssh" "tcp" "$Port" "daemon=dropbear"
+ procd_set_param respawn
procd_close_instance
}
-keygen()
+load_interfaces()
{
- for keytype in rsa dss; do
- # check for keys
- key=dropbear/dropbear_${keytype}_host_key
- [ -f /tmp/$key -o -s /etc/$key ] || {
- # generate missing keys
- mkdir -p /tmp/dropbear
- [ -x /usr/bin/dropbearkey ] && {
- /usr/bin/dropbearkey -t $keytype -f /tmp/$key 2>&- >&- && exec /etc/rc.common "$initscript" start
- } &
- exit 0
- }
- done
+ local enable
+ config_get enable "$1" enable 1
+ [ "${enable}" = "1" ] || return 0
- lock /tmp/.switch2jffs
- mkdir -p /etc/dropbear
- mv /tmp/dropbear/dropbear_* /etc/dropbear/
- lock -u /tmp/.switch2jffs
- chown root /etc/dropbear
- chmod 0700 /etc/dropbear
+ local direct_iface iface
+ config_get direct_iface "$1" DirectInterface
+ direct_iface=$(normalize_list "${direct_iface}")
+ # 'DirectInterface' takes precedence over 'Interface'
+ if [ -n "${direct_iface}" ] ; then
+ iface=$(echo "${direct_iface}" | awk '{print $1}')
+ else
+ config_get iface "$1" Interface
+ iface=$(normalize_list "${iface}")
+ fi
+ interfaces="${interfaces} ${iface}"
+}
+
+boot()
+{
+ BOOT=1
+ start "$@"
}
start_service()
{
- [ -s /etc/dropbear/dropbear_rsa_host_key -a \
- -s /etc/dropbear/dropbear_dss_host_key ] || keygen
+ hk_generate_as_needed
+ file_verify /etc/dropbear/authorized_keys config
. /lib/functions.sh
. /lib/functions/network.sh
config_load "${NAME}"
- config_foreach dropbear_instance dropbear
+ config_foreach validate_section_dropbear dropbear dropbear_instance
}
service_triggers()
{
- procd_add_config_trigger "dropbear" "/etc/init.d/dropbear" "restart"
+ local interfaces
+
+ procd_add_config_trigger "config.change" "${NAME}" /etc/init.d/dropbear reload
+
+ config_load "${NAME}"
+ config_foreach load_interfaces "${NAME}"
+
+ [ -n "${interfaces}" ] && {
+ local n
+ for n in $(printf '%s\n' ${interfaces} | sort -u) ; do
+ procd_add_interface_trigger "interface.*" $n /etc/init.d/dropbear reload
+ done
+ }
+
+ procd_add_validation validate_section_dropbear
+}
+
+shutdown() {
+ # close all open connections
+ killall dropbear
}
killclients()
while [ "${pid}" -ne 0 ]
do
# get parent process id
- pid=`cut -d ' ' -f 4 "/proc/${pid}/stat"`
+ pid=$(cut -d ' ' -f 4 "/proc/${pid}/stat")
[ "${pid}" -eq 0 ] && break
# check if client connection
done
# get all server pids that should be ignored
- for server in `cat /var/run/${NAME}.*.pid`
+ for server in $(cat /var/run/${NAME}.*.pid)
do
append ignore "${server}"
done
# get all running pids and kill client connections
local skip
- for pid in `pidof "${NAME}"`
+ for pid in $(pidof "${NAME}")
do
# check if correct program, otherwise process next pid
grep -F -q -e "${PROG}" "/proc/${pid}/cmdline" || {
skip=0
for server in ${ignore}
do
- if [ "${pid}" == "${server}" ]
+ if [ "${pid}" = "${server}" ]
then
skip=1
break