diff --git a/net/openssh/Makefile b/net/openssh/Makefile index b49f85ae3fbb6..8a6c20a324810 100644 --- a/net/openssh/Makefile +++ b/net/openssh/Makefile @@ -235,12 +235,16 @@ define Package/openssh-server/install sed -r -i 's,^#(HostKey /etc/ssh/ssh_host_(rsa|ed25519)_key)$$$$,\1,' $(1)/etc/ssh/sshd_config $(INSTALL_DIR) $(1)/etc/init.d $(INSTALL_BIN) ./files/sshd.init $(1)/etc/init.d/sshd + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_BIN) ./files/sshd.config $(1)/etc/config/sshd $(INSTALL_DIR) $(1)/lib/preinit $(INSTALL_BIN) ./files/sshd.failsafe $(1)/lib/preinit/99_10_failsafe_sshd $(INSTALL_DIR) $(1)/usr/sbin $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/sbin/sshd $(1)/usr/sbin/ $(INSTALL_DIR) $(1)/usr/lib $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/lib/sshd-session $(1)/usr/lib/ + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_DATA) ./files/sshd.ucidefault $(1)/etc/uci-defaults/99-generate-sshd-config endef define Package/openssh-server-pam/install diff --git a/net/openssh/files/sshd.config b/net/openssh/files/sshd.config new file mode 100644 index 0000000000000..67b210fb3ac62 --- /dev/null +++ b/net/openssh/files/sshd.config @@ -0,0 +1,6 @@ +config sshd + option enable '1' + option Port '22' + option RootLogin '1' + option PasswordAuth '1' + option RootPasswordAuth '1' diff --git a/net/openssh/files/sshd.init b/net/openssh/files/sshd.init index 0b859e146e03e..eca528aa4e8b9 100644 --- a/net/openssh/files/sshd.init +++ b/net/openssh/files/sshd.init @@ -6,8 +6,266 @@ STOP=50 USE_PROCD=1 PROG=/usr/sbin/sshd +NAME=sshd +BASECONFIGFILE="/var/etc/sshd.conf" -start_service() { +. /lib/functions.sh +. /lib/functions/network.sh + +validate_section_sshd() +{ + uci_load_validate sshd sshd "$1" "$2" \ + 'AllowUsers:list(string)' \ + 'BannerFile:file' \ + 'Ciphers:list(string)' \ + 'ClientAliveCountMax:uinteger' \ + 'ClientAliveInterval:uinteger' \ + 'enable:bool:1' \ + 'enabled:bool:1' \ + 'GatewayPorts:bool:0' \ + 'HostKeyAlgorithms:list(string)' \ + 'HostKeyFiles:list(file)' \ + 'IdleTimeout:uinteger:0' \ + 'IncludeConfigFile:file:/etc/ssh/sshd_config' \ + 'Interface:string' \ + 'KexAlgorithms:list(string)' \ + 'MacAlgorithms:list(string)' \ + 'MaxAuthTries:uinteger:6' \ + 'mdns:bool:1' \ + 'PasswordAuth:bool:1' \ + 'Port:port:22' \ + 'RootLogin:bool:1' \ + 'RootPasswordAuth:bool:1' +} + +append_config() +{ + local Param="${1}" + local Value="${2}" + + if [ -n "${Param}" ]; then + echo "${Param} ${Value}" >> "${TEMPCONF}" + fi +} + +set_option_list_comma() +{ + local OptionName="${1}" + local Values="${2}" + local ValueList="" + + for Value in $Values; do + # if this is the first iteration, then ValueList + # should be empty, and we only want to add a + # comma if there are multiple values, + # this is to avoid extra comma in the end + if [ -z "${ValueList}" ]; then + ValueList="${Value}" + else + ValueList="${Value},${ValueList}" + fi + done + + append_config "${OptionName}" "${ValueList}" +} + +set_option_list_space() +{ + local OptionName="${1}" + local Values="${2}" + local ValueList="" + + for Value in $Values; do + ValueList="${Value} ${ValueList}" + done + + append_config "${OptionName}" "${ValueList}" +} + +set_gateway_ports() +{ + local GatewayPorts="${1}" + + if [ -n "${GatewayPorts}" ]; then + if [ "${GatewayPorts}" -eq 0 ]; then + append_config "GatewayPorts" "no" + else + append_config "GatewayPorts" "yes" + fi + fi +} + +# because sshd does not have an option for specifying an interface +# but only for specifying listen address +# we get the addresses of interface and add them +set_listen_addresses() +{ + local Port="${1}" + local IpAddrs="${2}" + + [ "${Port}" -gt 0 ] && append_config "Port" "${Port}" + + for Addr in $IpAddrs; do + append_config "ListenAddress" "${Addr}" + done +} + +set_idle_timeout() +{ + local IdleTimeout="${1}" + + # from https://www.man7.org/linux/man-pages/man5/sshd_config.5.html + # ClientAliveCountMax: + # The default value is 3. If ClientAliveInterval is set to + # 15, and ClientAliveCountMax is left at the default, + # unresponsive SSH clients will be disconnected after + # approximately 45 seconds. Setting a zero + # ClientAliveCountMax disables connection termination. + # + # Therefore, to mimic IdleTimeout from dropbear, we set + # ClientAliveCountMax to 1 and ClientAliveInterval to + # the time we want to wait + if [ "${IdleTimeout}" -ne 0 ]; then + append_config "ClientAliveCountMax" "1" + append_config "ClientAliveInterval" "${IdleTimeout}" + fi +} + +publish_ssh_service_over_mdns() +{ + local mdns="${1}" + local Port="${2}" + + if [ "${mdns}" -ne 0 ] && [ "${Port}" -gt 0 ]; then + procd_add_mdns "ssh" "tcp" "${Port}" "daemon=sshd" + fi +} + +set_root_login() +{ + local RootLogin="${1}" + local RootPasswordAuth="${2}" + + if [ "${RootLogin}" -eq 0 ]; then + append_config "PermitRootLogin" "no" + else + if [ "${RootPasswordAuth}" -eq 0 ]; then + append_config "PermitRootLogin" "prohibit-password" + else + append_config "PermitRootLogin" "yes" + fi + fi +} + +set_password_auth() +{ + local PasswordAuth="${1}" + + if [ "${PasswordAuth}" -eq 0 ]; then + append_config "PasswordAuthentication" "no" + else + append_config "PasswordAuthentication" "yes" + fi +} + +set_params() +{ + local ConfigFile="${1}" + + # start fresh + rm -rf "${TEMPCONF}" + touch "${TEMPCONF}" + + [ -n "${AllowUsers}" ] && set_option_list_space "AllowUsers" "${AllowUsers}" + [ -n "${BannerFile}" ] && append_config "Banner" "${BannerFile}" + [ -n "${Ciphers}" ] && set_option_list_comma "Ciphers" "${Ciphers}" + [ -n "${ClientAliveCountMax}" ] && append_config "ClientAliveCountMax" "${ClientAliveCountMax}" + [ -n "${ClientAliveInterval}" ] && append_config "ClientAliveInterval" "${ClientAliveInterval}" + [ -n "${IncludeConfigFile}" ] && append_config "Include" "${IncludeConfigFile}" + [ -n "${HostKeyAlgorithms}" ] && set_option_list_comma "HostKeyAlgorithms" "${HostKeyAlgorithms}" + [ -n "${HostKeyFiles}" ] && set_option_list_comma "HostKey" "${HostKeyFiles}" + [ -n "${KexAlgorithms}" ] && set_option_list_comma "KexAlgorithms" "${KexAlgorithms}" + [ -n "${MacAlgorithms}" ] && set_option_list_comma "MACs" "${MacAlgorithms}" + [ "${MaxAuthTries}" -gt 0 ] && append_config "MaxAuthTries" "${MaxAuthTries}" + + # these require a little handling + set_gateway_ports "${GatewayPorts}" + set_listen_addresses "${Port}" "${IpAddrs}" + set_password_auth "${PasswordAuth}" + set_root_login "${RootLogin}" "${RootPasswordAuth}" + + # if ClientAliveCountMax or ClientAliveInterval are set explicitly + # then IdleTimeout cannot be used because IdleTimeout also uses them + # IdleTimeout is present in dropbear and not in sshd anyways + # so user can go with dropbear style (IdleTimeout) or sshd style (explicit) + [ -z "${ClientAliveCountMax}" ] && [ -z "${ClientAliveInterval}" ] && set_idle_timeout "${IdleTimeout}" + + # finalize + mv -f "${TEMPCONF}" "${ConfigFile}" +} + +sshd_instance() +{ + local IpAddrs + local Cfg="$1" + local ValidationResult="${2}" + + [ "${ValidationResult}" = 0 ] || { + echo "validation failed" + return 1 + } + + # user can use either of enable or enabled + [ "${enable}" -eq 0 ] || [ "${enabled}" -eq 0 ] && return 0 + + [ -n "${Interface}" ] && { + network_get_ipaddrs_all IpAddrs "${Interface}" || { + echo "interface ${Interface} has no physdev or physdev has no suitable ip" + return 1 + } + } + + local PidFile="/var/run/${NAME}.${Cfg}.pid" + # ConfigFile is BASECONFIGFILE + uci section name + local ConfigFile="${BASECONFIGFILE}.${Cfg}" + # global + TEMPCONF="${ConfigFile}.tmp" + + # create directory if not present + mkdir -p $(dirname $ConfigFile) + + # create config file + set_params "$ConfigFile" + + # set up procd instance + procd_open_instance $Cfg + procd_set_param command $PROG -D + procd_set_param file "$ConfigFile" + procd_append_param command -o "PidFile $PidFile" + + # pass config to daemon + procd_append_param command -f "$ConfigFile" + + # announce mdns service + publish_ssh_service_over_mdns "${mdns}" "${Port}" + + procd_set_param respawn + procd_close_instance +} + +# for adding trigger +load_interfaces() +{ + config_get Interface "$1" Interface + config_get_bool enable "$1" enable 1 + config_get_bool enabled "$1" enabled 1 + + # user can use either of enable or enabled + [ "${enable}" = "1" ] && [ "${enabled}" = "1" ] && Interfaces=" ${Interface} ${Interfaces}" +} + +start_service() +{ for type in rsa ed25519 do # check for keys @@ -20,19 +278,34 @@ start_service() { } done mkdir -m 0700 -p /var/empty + mkdir -m 0700 -p /root/.ssh - local lport=$(awk '/^Port / { print $2; exit }' /etc/ssh/sshd_config) - [ -z "$lport" ] && lport=22 + config_load "${NAME}" + config_foreach validate_section_sshd sshd sshd_instance +} - procd_open_instance - procd_add_mdns "ssh" "tcp" "$lport" - procd_set_param command $PROG -D - procd_set_param respawn - procd_close_instance +reload_service() +{ + rc_procd start_service "$@" + procd_send_signal sshd "$@" } -reload_service() { - procd_send_signal sshd +service_triggers() +{ + local Interfaces + + procd_add_config_trigger "config.change" "sshd" /etc/init.d/sshd reload + + config_load "${NAME}" + config_foreach load_interfaces sshd + + [ -n "${Interfaces}" ] && { + for n in $Interfaces ; do + procd_add_interface_trigger "interface.*" $n /etc/init.d/sshd reload + done + } + + procd_add_validation validate_section_sshd } shutdown() { @@ -44,6 +317,6 @@ shutdown() { for pid in $(pidof sshd) do [ "$pid" = "$$" ] && continue - [ -e "/proc/$pid/stat" ] && kill $pid + [ -e "/proc/$pid/stat" ] && kill "$pid" done } diff --git a/net/openssh/files/sshd.ucidefault b/net/openssh/files/sshd.ucidefault new file mode 100644 index 0000000000000..ab73f4bdf517b --- /dev/null +++ b/net/openssh/files/sshd.ucidefault @@ -0,0 +1,150 @@ +#!/bin/sh + +# the purpose of this script is to generate sshd config +# from a dropbear config, if present, or, if sshd config is not present +# then a default sshd config is generated + +. /lib/functions.sh + +set_option_list() +{ + local SectionName="$1" + local OptionName="$2" + local Values="$3" + + if [ -n "$SectionName" -a -n "$OptionName" -a -n "$Values" ]; then + for Value in $Values; do + uci -q add_list "sshd.${SectionName}.${OptionName}"="${Value}" + done + fi +} + +set_option() +{ + local SectionName="$1" + local OptionName="$2" + local Value="$3" + + if [ -n "$SectionName" -a -n "$OptionName" -a -n "$Value" ]; then + uci -q set "sshd.${SectionName}.${OptionName}"="${Value}" + fi +} + +generate_sshd_section() +{ + local AllowUsers="root" + local BannerFile="" + local Ciphers="aes128-ctr aes192-ctr aes256-ctr" + local Enable=1 + local ForceCommand="" + local GatewayPorts=0 + local HostKeyAlgorithms="ecdsa-sha2-nistp256 ecdsa-sha2-nistp384 ecdsa-sha2-nistp521 ssh-rsa ssh-dss" + local KexAlgorithms="ecdh-sha2-nistp256 ecdh-sha2-nistp384 ecdh-sha2-nistp521 diffie-hellman-group14-sha1 diffie-hellman-group-exchange-sha256" + local IdleTimeout=0 + local Interface="" + local MacAlgorithms="hmac-sha1 hmac-sha2-256 hmac-sha2-512" + local MaxAuthTries=6 + local mdns="" + local PasswordAuth=1 + local Port=22 + local RecvWindowSize="" + local RootLogin=1 + local RootPasswordAuth=1 + local SSHKeepAlive="" + local Verbose="" + + local Cfg="$1" + local SectionName="$Cfg" + local Migrate="$2" + + # if not the default case, then config_get can be used + # as dropbear config is present + if [ "$Migrate" = "true" ]; then + config_get BannerFile "$Cfg" BannerFile + config_get_bool Enable "$Cfg" enable 1 + config_get ForceCommand "$Cfg" ForceCommand + config_get_bool GatewayPorts "$Cfg" GatewayPorts 0 + config_get IdleTimeout "$Cfg" IdleTimeout 0 + config_get Interface "$Cfg" Interface + config_get MaxAuthTries "$Cfg" MaxAuthTries 6 + config_get_bool mdns "$Cfg" mdns 1 + config_get_bool PasswordAuth "$Cfg" PasswordAuth 1 + config_get Port "$Cfg" Port 22 + config_get_bool RecvWindowSize "$Cfg" RecvWindowSize 24576 + config_get_bool RootLogin "$Cfg" RootLogin 1 + config_get_bool RootPasswordAuth "$Cfg" RootPasswordAuth 1 + config_get SSHKeepAlive "$Cfg" SSHKeepAlive 300 + config_get_bool Verbose "$Cfg" verbose 0 + fi + + # using uci set instead of uci add because they achieve the same thing + # but uci set allows us to control section name at the same time + uci -q set "sshd.${SectionName}=sshd" + + # set options + set_option_list "$SectionName" "AllowUsers" "$AllowUsers" + set_option "$SectionName" "BannerFile" "$BannerFile" + set_option_list "$SectionName" "Ciphers" "$Ciphers" + set_option "$SectionName" "enable" "$Enable" + set_option "$SectionName" "GatewayPorts" "$GatewayPorts" + set_option_list "$SectionName" "HostKeyAlgorithms" "$HostKeyAlgorithms" + set_option "$SectionName" "IdleTimeout" "$IdleTimeout" + set_option "$SectionName" "Interface" "$Interface" + set_option_list "$SectionName" "KexAlgorithms" "$KexAlgorithms" + set_option_list "$SectionName" "MacAlgorithms" "$MacAlgorithms" + set_option "$SectionName" "MaxAuthTries" "$MaxAuthTries" + set_option "$SectionName" "PasswordAuth" "$PasswordAuth" + set_option "$SectionName" "Port" "$Port" + set_option "$SectionName" "mdns" "$mdns" + set_option "$SectionName" "RootLogin" "$RootLogin" + set_option "$SectionName" "RootPasswordAuth" "$RootPasswordAuth" +} + +migrate_config() +{ + rm -f "/etc/config/sshd" + touch "/etc/config/sshd" + + config_load "dropbear" + # for all sections, call generate_sshd_section with migrate flag as true + config_foreach generate_sshd_section "dropbear" "true" +} + +migrate_authorized_keys() +{ + local SourceAuthorizedKeysFile="/etc/dropbear/authorized_keys" + local DestAuthorizedKeysFile="/root/.ssh/authorized_keys" + + if [ -s "$SourceAuthorizedKeysFile" ]; then + # create key directory if not present + mkdir -m 0700 -p /root/.ssh + # copy dropbear key file + cp "$SourceAuthorizedKeysFile" "$DestAuthorizedKeysFile" + # remove dropbear key file + rm -f "$SourceAuthorizedKeysFile" + fi +} + +# if dropbear config is present, then it over-writes sshd config +# the assumption is that there was a working dropbear config +if [ -s "/etc/config/dropbear" ]; then + # migrate dropbear config + migrate_config + migrate_authorized_keys + # remove config + rm -f "/etc/config/dropbear" +elif uci -q get "sshd.@sshd[0]" >/dev/null 2>&1; then + # return if there is any valid content + exit +else + # no valid content + rm -f "/etc/config/sshd" + # create sshd config file so that uci set can work + touch "/etc/config/sshd" + # generate a default sshd config if sshd UCI not found + # pass a section name and migrate flag as false + generate_sshd_section "sshd1" "false" +fi + +# commit sshd +uci commit "sshd"