dropbear: better handle interfaces
[openwrt/staging/robimarko.git] / package / network / services / dropbear / files / dropbear.init
index 19aab3653bf7ba356c6ed8abe9c3157b37f1e08b..21570987c439b18cb50026bbbc0101e5a51a4f78 100755 (executable)
 # Copyright (C) 2006-2010 OpenWrt.org
 # Copyright (C) 2006 Carlos Sobrinho
 
-START=50
+START=19
 STOP=50
 
 USE_PROCD=1
 PROG=/usr/sbin/dropbear
 NAME=dropbear
 PIDCOUNT=0
-EXTRA_COMMANDS="killclients"
-EXTRA_HELP="   killclients Kill ${NAME} processes except servers and yourself"
 
-append_ports()
+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()
 {
-       local ifname="$1"
-       local port="$2"
+       /usr/bin/dropbearkey "$@" </dev/null >/dev/null 2>&1
+}
 
-       grep -qs "^ *$ifname:" /proc/net/dev || {
-               procd_append_param command -p "$port"
-               return
+# $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"
+}
 
-       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"
+# $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'
+
+       [ -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_validate_section dropbear dropbear "${1}" \
+       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' \
-               'dsskeyfile:file' \
+               'keyfile:list(file)' \
                'BannerFile:file' \
-               'Port:list(port):22' \
+               'Port:port:22' \
                'SSHKeepAlive:uinteger:300' \
-               'IdleTimeout:uinteger:0'
-       return $?
+               'IdleTimeout:uinteger:0' \
+               'MaxAuthTries:uinteger:3' \
+               'RecvWindowSize:uinteger:262144' \
+               'mdns:bool:1'
 }
 
 dropbear_instance()
 {
-       local PasswordAuth enable Interface GatewayPorts \
-               RootPasswordAuth RootLogin rsakeyfile \
-               dsskeyfile BannerFile Port
-
-       validate_section_dropbear "${1}" || {
+       [ "$2" = 0 ] || {
                echo "validation failed"
                return 1
        }
 
-       [ "${enable}" = "0" ] && return 1
+       [ "${enable}" = "1" ] || return 1
+
+       local iface ndev ipaddrs
+
+       # 'DirectInterface' should specify single interface
+       # but end users may misinterpret this setting
+       DirectInterface=$(normalize_list "${DirectInterface}")
+
+       # '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
-       [ -n "${rsakeyfile}" ] && procd_append_param command -r "${rsakeyfile}"
-       [ -n "${dsskeyfile}" ] && procd_append_param command -d "${dsskeyfile}"
+       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}"
-       [ -n "${Interface}" ] && network_get_device Interface "${Interface}"
-       append_ports "${Interface}" "${Port}"
        [ "${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)
+
+               # 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_reload_trigger "dropbear"
+       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()
 {
        local ignore=''
@@ -133,7 +430,7 @@ 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
@@ -144,14 +441,14 @@ killclients()
        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" || {
@@ -162,7 +459,7 @@ killclients()
                skip=0
                for server in ${ignore}
                 do
-                       if [ "${pid}" == "${server}" ]
+                       if [ "${pid}" = "${server}" ]
                         then
                                skip=1
                                break