ynh-vpnclient 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #!/bin/bash
  2. # VPN Client app for YunoHost
  3. # Copyright (C) 2015 Julien Vaubourg <julien@vaubourg.com>
  4. # Contribute at https://github.com/labriqueinternet/vpnclient_ynh
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU Affero General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU Affero General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU Affero General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. ###################################################################################
  19. # Logging helpers #
  20. ###################################################################################
  21. LOGFILE="/var/log/ynh-vpnclient.log"
  22. touch $LOGFILE
  23. chown root:root $LOGFILE
  24. chmod 600 $LOGFILE
  25. function success()
  26. {
  27. echo "[ OK ] $1" | tee -a $LOGFILE
  28. }
  29. function info()
  30. {
  31. echo "[INFO] $1" | tee -a $LOGFILE
  32. }
  33. function warn()
  34. {
  35. echo "[WARN] $1" | tee -a $LOGFILE >&2
  36. }
  37. function error()
  38. {
  39. echo "[FAIL] $1" | tee -a $LOGFILE >&2
  40. }
  41. function critical()
  42. {
  43. echo "[CRIT] $1" | tee -a $LOGFILE >&2
  44. exit 1
  45. }
  46. ###################################################################################
  47. # IPv6 and route config stuff #
  48. ###################################################################################
  49. has_nativeip6() {
  50. ip -6 route | grep -q "default via"
  51. }
  52. has_ip6delegatedprefix() {
  53. [[ -n "${ynh_ip6_addr}" ]] && [[ "${ynh_ip6_addr}" != none ]]
  54. }
  55. is_ip6addr_set() {
  56. ip address show dev tun0 2> /dev/null | grep -q "${ynh_ip6_addr}/"
  57. }
  58. #
  59. # Server IPv6 route
  60. #
  61. is_serverip6route_set() {
  62. server_ip6s=${1}
  63. if [[ -z "${server_ip6s}" ]]; then
  64. return 0
  65. fi
  66. for server_ip6 in ${server_ip6s}; do
  67. if ! ip -6 route | grep -q "^${server_ip6}"; then
  68. return 1
  69. fi
  70. done
  71. }
  72. set_serverip6route() {
  73. server_ip6s=${1}
  74. ip6_gw=${2}
  75. wired_device=${3}
  76. info "Adding IPv6 server route"
  77. for server_ip6 in ${server_ip6s};
  78. do
  79. ip route add "${server_ip6}/128" via "${ip6_gw}" dev "${wired_device}"
  80. done
  81. }
  82. unset_serverip6route() {
  83. server_ip6s=${1}
  84. ip6_gw=${2}
  85. wired_device=${3}
  86. info "Removing IPv6 server route"
  87. for server_ip6 in ${server_ip6s};
  88. do
  89. ip route delete "${server_ip6}/128" via "${ip6_gw}" dev "${wired_device}"
  90. done
  91. }
  92. ###################################################################################
  93. # DNS rules #
  94. ###################################################################################
  95. is_dns_set() {
  96. if [[ "$ynh_dns_method" == "custom" ]]
  97. then
  98. current_dns=$(grep -o -P '\s*nameserver\s+\K[abcdefabcdef\d.:]+' /etc/resolv.dnsmasq.conf | sort | uniq)
  99. wanted_dns=$(echo "${ynh_dns}" | sed 's/,/\n/g' | sort | uniq)
  100. [ -e /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient ]\
  101. && [[ "$current_dns" == "$wanted_dns" ]]
  102. else
  103. true
  104. fi
  105. }
  106. set_dns() {
  107. info "Enforcing custom DNS resolvers from vpnclient"
  108. resolvconf=/etc/resolv.dnsmasq.conf
  109. cp -fa "${resolvconf}" "${resolvconf}.ynh"
  110. if [[ "$ynh_dns_method" == "custom" ]]
  111. then
  112. cat << EOF > /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient
  113. echo "${ynh_dns}" | sed 's/,/\n/g' | sort | uniq | sed 's/^/nameserver /g' > ${resolvconf}
  114. EOF
  115. bash /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient
  116. fi
  117. }
  118. unset_dns() {
  119. resolvconf=/etc/resolv.dnsmasq.conf
  120. info "Removing custom DNS resolvers from vpnclient"
  121. rm -f /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient
  122. [ -e "${resolvconf}.ynh" ] && mv "${resolvconf}.ynh" "${resolvconf}"
  123. # FIXME : this situation happened to a user ...
  124. # We could try to force regen the dns conf
  125. # (though for now it's tightly coupled to dnsmasq)
  126. grep -q "^nameserver\s" "${resolvconf}" || error "${resolvconf} does not have any nameserver line !?"
  127. }
  128. ###################################################################################
  129. # Firewall rules management #
  130. ###################################################################################
  131. is_firewall_set() {
  132. wired_device=$(ip route | awk '/default via/ { print $5; }')
  133. ip6tables -w -nvL OUTPUT | grep vpnclient_out | grep -q "${wired_device}"\
  134. && iptables -w -nvL OUTPUT | grep vpnclient_out | grep -q "${wired_device}"
  135. }
  136. set_firewall() {
  137. info "Adding vpnclient custom rules to the firewall"
  138. cp /etc/yunohost/hooks.d/{90-vpnclient.tpl,post_iptable_rules/90-vpnclient}
  139. info "Restarting yunohost firewall..."
  140. yunohost firewall reload >/dev/null && success "Firewall restarted!"
  141. }
  142. unset_firewall() {
  143. info "Cleaning vpnclient custom rules from the firewall"
  144. rm -f /etc/yunohost/hooks.d/post_iptable_rules/90-vpnclient
  145. info "Restarting yunohost firewall..."
  146. yunohost firewall reload >/dev/null && success "Firewall restarted!"
  147. }
  148. ###################################################################################
  149. # Time sync #
  150. ###################################################################################
  151. sync_time() {
  152. info "Now synchronizing time using ntp..."
  153. systemctl stop ntp
  154. timeout 20 ntpd -qg &> /dev/null
  155. # Some networks drop ntp port (udp 123).
  156. # Try to get the date with an http request on the internetcube web site
  157. if [ $? -ne 0 ]; then
  158. info "ntp synchronization failed, falling back to curl method"
  159. http_date=$(curl --max-time 5 -sD - labriqueinter.net | grep '^Date:' | cut -d' ' -f3-6)
  160. http_date_seconds=$(date -d "${http_date}" +%s)
  161. curr_date_seconds=$(date +%s)
  162. # Set the new date if it's greater than the current date
  163. # So it does if 1970 year or if old fake-hwclock date is used
  164. if [ $http_date_seconds -ge $curr_date_seconds ]; then
  165. date -s "${http_date}"
  166. fi
  167. fi
  168. systemctl start ntp
  169. }
  170. ###################################################################################
  171. # OpenVPN client start/stop procedures #
  172. ###################################################################################
  173. is_openvpn_running() {
  174. systemctl is-active openvpn@client.service &> /dev/null
  175. }
  176. start_openvpn() {
  177. # Unset firewall to let DNS and NTP resolution works
  178. # Firewall is reset after vpn is mounted (more details on #1016)
  179. unset_firewall
  180. sync_time
  181. info "Now actually starting OpenVPN client..."
  182. if systemctl start openvpn@client.service
  183. then
  184. info "OpenVPN client started ... waiting for tun0 interface to show up"
  185. else
  186. tail -n 20 /var/log/openvpn-client.log | tee -a $LOGFILE
  187. critical "Failed to start OpenVPN :/"
  188. fi
  189. for attempt in $(seq 0 20)
  190. do
  191. sleep 1
  192. if ip link show dev tun0 &> /dev/null
  193. then
  194. success "tun0 interface is up!"
  195. return 0
  196. fi
  197. done
  198. error "Tun0 interface did not show up ... most likely an issue happening in OpenVPN client ... below is an extract of the log that might be relevant to pinpoint the issue"
  199. tail -n 20 /var/log/openvpn-client.log | tee -a $LOGFILE
  200. stop_openvpn
  201. critical "Failed to start OpenVPN client : tun0 interface did not show up"
  202. }
  203. stop_openvpn() {
  204. info "Stopping OpenVPN service"
  205. systemctl stop openvpn@client.service
  206. for attempt in $(seq 0 20)
  207. do
  208. if ip link show dev tun0 &> /dev/null
  209. then
  210. info "(Waiting for tun0 to disappear if it was up)"
  211. sleep 1
  212. fi
  213. done
  214. }
  215. ###################################################################################
  216. # Yunohost settings interface #
  217. ###################################################################################
  218. ynh_setting_get() {
  219. app=${1}
  220. setting=${2}
  221. grep "^${setting}:" "/etc/yunohost/apps/${app}/settings.yml" | sed s/^[^:]\\+:\\s*[\"\']\\?// | sed s/\\s*[\"\']\$//
  222. # '"
  223. }
  224. ynh_setting_set() {
  225. app=${1}
  226. setting=${2}
  227. value=${3}
  228. yunohost app setting "${app}" "${setting}" -v "${value}"
  229. }
  230. ###################################################################################
  231. # The actual ynh vpnclient management thing #
  232. ###################################################################################
  233. is_running() {
  234. ((has_nativeip6 && is_serverip6route_set "${new_server_ip6}") || ! has_nativeip6)\
  235. && ((has_ip6delegatedprefix && is_ip6addr_set) || ! has_ip6delegatedprefix)\
  236. && is_dns_set && is_firewall_set && is_openvpn_running
  237. }
  238. check_config() {
  239. if [[ ! -e /etc/openvpn/keys/ca-server.crt ]]; then
  240. critical "You need a CA server (you can add it through the web admin)"
  241. fi
  242. if ! openssl x509 -in /etc/openvpn/keys/ca-server.crt -noout -checkend 0 >/dev/null; then
  243. ca_server_cert_expired_date=$(openssl x509 -in /etc/openvpn/keys/ca-server.crt -noout -enddate | cut -d '=' -f 2)
  244. critical "The CA server expired on $ca_server_cert_expired_date"
  245. fi
  246. if [[ ! -e /etc/openvpn/keys/user.crt || ! -e /etc/openvpn/keys/user.key ]]; then
  247. if [[ -s /etc/openvpn/keys/credentials ]]; then
  248. login_user=$(sed -n 1p /etc/openvpn/keys/credentials)
  249. login_passphrase=$(sed -n 2p /etc/openvpn/keys/credentials)
  250. else
  251. login_user=""
  252. login_passphrase=""
  253. fi
  254. if [[ $login_user == "" || $login_passphrase == "" ]]; then
  255. critical "You need either a client certificate, either a username, or both (you can add one through the web admin)"
  256. fi
  257. elif [[ -e /etc/openvpn/keys/user.crt ]] && ! openssl x509 -in /etc/openvpn/keys/user.crt -noout -checkend 0 >/dev/null; then
  258. user_cert_expired_date=$(openssl x509 -in /etc/openvpn/keys/user.crt -noout -enddate | cut -d '=' -f 2)
  259. critical "The client certificate expired on $user_cert_expired_date"
  260. fi
  261. }
  262. if [ "$1" != restart ]; then
  263. # Variables
  264. info "Retrieving Yunohost settings... "
  265. ynh_service_enabled=$(ynh_setting_get vpnclient service_enabled)
  266. ynh_ip6_addr=$(ynh_setting_get vpnclient ip6_addr)
  267. ynh_dns_method=$(ynh_setting_get vpnclient dns_method)
  268. ynh_dns=$(ynh_setting_get vpnclient nameservers)
  269. old_ip6_gw=$(ynh_setting_get vpnclient ip6_gw)
  270. old_wired_device=$(ynh_setting_get vpnclient wired_device)
  271. old_server_ip6=$(ynh_setting_get vpnclient server_ip6)
  272. new_ip6_gw=$(ip -6 route | awk '/default via/ { print $3 }')
  273. new_wired_device=$(ip route | awk '/default via/ { print $5; }')
  274. ynh_server_names=$(grep -o -P '^\s*remote\s+\K([^\s]+)' /etc/openvpn/client.conf | sort | uniq)
  275. new_server_ip6=$(dig AAAA +short $ynh_server_names @127.0.0.1 | grep -v '\.$' | grep -v "timed out" | sort | uniq)
  276. for i in $ynh_server_names; do
  277. if [[ "${i}" =~ : ]] && [[ ! "$new_server_ip6" == *"${i}"* ]] ; then
  278. new_server_ip6+=" ${i}"
  279. fi
  280. done
  281. success "Settings retrieved"
  282. fi
  283. ###################################################################################
  284. # Start / stop / restart / status handling #
  285. ###################################################################################
  286. case "${1}" in
  287. # ########## #
  288. # Starting #
  289. # ########## #
  290. start)
  291. if is_running; then
  292. info "Service is already running"
  293. exit 0
  294. elif [ "${ynh_service_enabled}" -eq 0 ]; then
  295. warn "Service is disabled, not starting it"
  296. exit 0
  297. fi
  298. check_config
  299. info "[vpnclient] Starting..."
  300. touch /tmp/.ynh-vpnclient-started
  301. # Run openvpn
  302. if is_openvpn_running; then
  303. info "(openvpn is already running)"
  304. else
  305. start_openvpn
  306. fi
  307. # Check old state of the server ipv6 route
  308. if [ ! -z "${old_server_ip6}" -a ! -z "${old_ip6_gw}" -a ! -z "${old_wired_device}"\
  309. -a \( "${new_server_ip6}" != "${old_server_ip6}" -o "${new_ip6_gw}" != "${old_ip6_gw}"\
  310. -o "${new_wired_device}" != "${old_wired_device}" \) ]\
  311. && is_serverip6route_set "${old_server_ip6}"
  312. then
  313. unset_serverip6route "${old_server_ip6}" "${old_ip6_gw}" "${old_wired_device}"
  314. fi
  315. # Set the new server ipv6 route
  316. if has_nativeip6 && ! is_serverip6route_set "${new_server_ip6}"
  317. then
  318. set_serverip6route "${new_server_ip6}" "${new_ip6_gw}" "${new_wired_device}"
  319. fi
  320. # Set host DNS resolvers
  321. if ! is_dns_set; then
  322. set_dns
  323. fi
  324. # Set ipv6/ipv4 firewall
  325. if ! is_firewall_set; then
  326. set_firewall
  327. fi
  328. # Update dynamic settings
  329. info "Saving settings..."
  330. ynh_setting_set vpnclient server_ip6 "${new_server_ip6}"
  331. ynh_setting_set vpnclient ip6_gw "${new_ip6_gw}"
  332. ynh_setting_set vpnclient wired_device "${new_wired_device}"
  333. ipv4=$(ping -w3 -c1 ip.yunohost.org >/dev/null 2>&1 && curl --max-time 5 https://ip.yunohost.org --silent)
  334. ipv6=$(ping -w3 -c1 ip6.yunohost.org >/dev/null 2>&1 && curl --max-time 5 https://ip6.yunohost.org --silent)
  335. info "Validating that VPN is up and the server is connected to internet..."
  336. if ip route get 1.2.3.4 | grep -q tun0; then
  337. if ping -c1 -w5 debian.org >/dev/null; then
  338. success "YunoHost VPN client started!"
  339. info "IPv4 address is $ipv4"
  340. info "IPv6 address is $ipv6"
  341. else
  342. critical "The VPN is up but debian.org cannot be reached, indicating that something is probably misconfigured/blocked."
  343. fi
  344. else
  345. critical "IPv4 routes are misconfigured !?"
  346. fi
  347. ;;
  348. # ########## #
  349. # Stopping #
  350. # ########## #
  351. stop)
  352. info "[vpnclient] Stopping..."
  353. rm -f /tmp/.ynh-vpnclient-started
  354. if is_serverip6route_set "${old_server_ip6}"; then
  355. unset_serverip6route "${old_server_ip6}" "${old_ip6_gw}" "${old_wired_device}"
  356. fi
  357. is_firewall_set && unset_firewall
  358. is_dns_set && unset_dns
  359. is_openvpn_running && stop_openvpn
  360. ;;
  361. # ########## #
  362. # Restart #
  363. # ########## #
  364. restart)
  365. $0 stop
  366. $0 start
  367. ;;
  368. # ########## #
  369. # Status #
  370. # ########## #
  371. status)
  372. exitcode=0
  373. if [ "${ynh_service_enabled}" -eq 0 ]; then
  374. error "VPN Client Service disabled"
  375. exitcode=1
  376. fi
  377. info "Autodetected internet interface: ${new_wired_device} (last start: ${old_wired_device})"
  378. info "Autodetected IPv6 address for the VPN server: ${new_server_ip6} (last start: ${old_server_ip6})"
  379. if has_ip6delegatedprefix; then
  380. info "IPv6 delegated prefix found"
  381. info "IPv6 address computed from the delegated prefix: ${ynh_ip6_addr}"
  382. if is_ip6addr_set; then
  383. success "IPv6 address correctly set"
  384. else
  385. error "No IPv6 address set"
  386. exitcode=1
  387. fi
  388. else
  389. info "No IPv6 delegated prefix found"
  390. fi
  391. if has_nativeip6; then
  392. info "Native IPv6 detected"
  393. info "Autodetected native IPv6 gateway: ${new_ip6_gw} (last start: ${old_ip6_gw})"
  394. if is_serverip6route_set "${new_server_ip6}"; then
  395. success "IPv6 server route correctly set"
  396. else
  397. error "No IPv6 server route set"
  398. exitcode=1
  399. fi
  400. else
  401. info "No native IPv6 detected"
  402. info "No IPv6 server route to set"
  403. fi
  404. if is_firewall_set; then
  405. success "IPv6/IPv4 firewall set"
  406. else
  407. info "No IPv6/IPv4 firewall set"
  408. exitcode=1
  409. fi
  410. if is_dns_set; then
  411. success "Host DNS correctly set"
  412. else
  413. error "No host DNS set"
  414. exitcode=1
  415. fi
  416. if is_openvpn_running; then
  417. success "Openvpn is running"
  418. else
  419. error "Openvpn is not running"
  420. exitcode=1
  421. fi
  422. exit ${exitcode}
  423. ;;
  424. # ########## #
  425. # Halp #
  426. # ########## #
  427. *)
  428. echo "Usage: $0 {start|stop|restart|status}"
  429. exit 1
  430. ;;
  431. esac
  432. exit 0