dropbear.init 12 KB

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