#!/bin/bash # VPN Client app for YunoHost # Copyright (C) 2015 Julien Vaubourg # Contribute at https://github.com/labriqueinternet/vpnclient_ynh # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . ################################################################################### # Logging helpers # ################################################################################### LOGFILE="/var/log/ynh-vpnclient.log" touch $LOGFILE chown root:root $LOGFILE chmod 600 $LOGFILE function success() { echo "[ OK ] $1" | tee -a $LOGFILE } function info() { echo "[INFO] $1" | tee -a $LOGFILE } function warn() { echo "[WARN] $1" | tee -a $LOGFILE >&2 } function error() { echo "[FAIL] $1" | tee -a $LOGFILE >&2 } function critical() { echo "[CRIT] $1" | tee -a $LOGFILE >&2 exit 1 } ################################################################################### # IPv6 and route config stuff # ################################################################################### has_nativeip6() { ip -6 route | grep -q "default via" } has_ip6delegatedprefix() { [ "${ynh_ip6_addr}" != none ] && [ "${ynh_ip6_addr}" != "" ] } is_ip6addr_set() { ip address show dev tun0 2> /dev/null | grep -q "${ynh_ip6_addr}/" } set_ip6addr() { info "Adding IPv6 from VPN configuration" ip address add "${ynh_ip6_addr}/128" dev tun0 } unset_ip6addr() { info "Removing IPv6 from VPN configuration" ip address delete "${ynh_ip6_addr}/128" dev tun0 } # # Server IPv6 route # is_serverip6route_set() { server_ip6s=${1} if [ -z "${server_ip6}" ]; then false else for server_ip6 in ${server_ip6s}; do ip -6 route | grep -q "${server_ip6}/" || return 1 done fi } set_serverip6route() { server_ip6s=${1} ip6_gw=${2} wired_device=${3} info "Adding IPv6 server route" for server_ip6 in ${server_ip6s}; do ip route add "${server_ip6}/128" via "${ip6_gw}" dev "${wired_device}" done } unset_serverip6route() { server_ip6s=${1} ip6_gw=${2} wired_device=${3} info "Removing IPv6 server route" for server_ip6 in ${server_ip6s}; do ip route delete "${server_ip6}/128" via "${ip6_gw}" dev "${wired_device}" done } ################################################################################### # DNS rules # ################################################################################### is_dns_set() { if [[ "$ynh_dns_method" == "custom" ]] then current_dns=$(grep -o -P '\s*nameserver\s+\K[abcdefabcdef\d.:]+' /etc/resolv.dnsmasq.conf | sort | uniq) wanted_dns=$(echo "${ynh_dns}" | sed 's/,/\n/g' | sort | uniq) [ -e /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient ]\ && [[ "$current_dns" == "$wanted_dns" ]] else true fi } set_dns() { info "Enforcing custom DNS resolvers from vpnclient" resolvconf=/etc/resolv.dnsmasq.conf cp -fa "${resolvconf}" "${resolvconf}.ynh" if [[ "$ynh_dns_method" == "custom" ]] then cat << EOF > /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient echo "${ynh_dns}" | sed 's/,/\n/g' | sort | uniq | sed 's/^/nameserver /g' > ${resolvconf} EOF bash /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient fi } unset_dns() { resolvconf=/etc/resolv.dnsmasq.conf info "Removing custom DNS resolvers from vpnclient" rm -f /etc/dhcp/dhclient-exit-hooks.d/ynh-vpnclient [ -e "${resolvconf}.ynh" ] && mv "${resolvconf}.ynh" "${resolvconf}" # FIXME : this situation happened to a user ... # We could try to force regen the dns conf # (though for now it's tightly coupled to dnsmasq) grep -q "^nameserver\s" "${resolvconf}" || error "${resolvconf} does not have any nameserver line !?" } ################################################################################### # Firewall rules management # ################################################################################### is_firewall_set() { wired_device=$(ip route | awk '/default via/ { print $5; }') ip6tables -w -nvL OUTPUT | grep vpnclient_out | grep -q "${wired_device}"\ && iptables -w -nvL OUTPUT | grep vpnclient_out | grep -q "${wired_device}" } set_firewall() { info "Adding vpnclient custom rules to the firewall" cp /etc/yunohost/hooks.d/{90-vpnclient.tpl,post_iptable_rules/90-vpnclient} info "Restarting yunohost firewall..." yunohost firewall reload >/dev/null && success "Firewall restarted!" } unset_firewall() { info "Cleaning vpnclient custom rules from the firewall" rm -f /etc/yunohost/hooks.d/post_iptable_rules/90-vpnclient info "Restarting yunohost firewall..." yunohost firewall reload >/dev/null && success "Firewall restarted!" } ################################################################################### # Time sync # ################################################################################### sync_time() { info "Now synchronizing time using ntp..." systemctl stop ntp timeout 20 ntpd -qg &> /dev/null # Some networks drop ntp port (udp 123). # Try to get the date with an http request on the internetcube web site if [ $? -ne 0 ]; then info "ntp synchronization failed, falling back to curl method" http_date=$(curl --max-time 5 -sD - labriqueinter.net | grep '^Date:' | cut -d' ' -f3-6) http_date_seconds=$(date -d "${http_date}" +%s) curr_date_seconds=$(date +%s) # Set the new date if it's greater than the current date # So it does if 1970 year or if old fake-hwclock date is used if [ $http_date_seconds -ge $curr_date_seconds ]; then date -s "${http_date}" fi fi systemctl start ntp } ################################################################################### # OpenVPN client start/stop procedures # ################################################################################### is_openvpn_running() { systemctl is-active openvpn@client.service &> /dev/null } start_openvpn() { # Unset firewall to let DNS and NTP resolution works # Firewall is reset after vpn is mounted (more details on #1016) unset_firewall sync_time info "Now actually starting OpenVPN client..." if systemctl start openvpn@client.service then info "OpenVPN client started ... waiting for tun0 interface to show up" else tail -n 20 /var/log/openvpn-client.log | tee -a $LOGFILE critical "Failed to start OpenVPN :/" fi for attempt in $(seq 0 20) do sleep 1 if ip link show dev tun0 &> /dev/null then success "tun0 interface is up!" return 0 fi done 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" tail -n 20 /var/log/openvpn-client.log | tee -a $LOGFILE stop_openvpn critical "Failed to start OpenVPN client : tun0 interface did not show up" } stop_openvpn() { # FIXME : isn't openvpn@client ? (idk) info "Stopping OpenVPN service" systemctl stop openvpn.service for attempt in $(seq 0 20) do if ip link show dev tun0 &> /dev/null then info "(Waiting for tun0 to disappear if it was up)" sleep 1 fi done } ################################################################################### # Yunohost settings interface # ################################################################################### ynh_setting_get() { app=${1} setting=${2} grep "^${setting}:" "/etc/yunohost/apps/${app}/settings.yml" | sed s/^[^:]\\+:\\s*[\"\']\\?// | sed s/\\s*[\"\']\$// # '" } ynh_setting_set() { app=${1} setting=${2} value=${3} yunohost app setting "${app}" "${setting}" -v "${value}" } ################################################################################### # The actual ynh vpnclient management thing # ################################################################################### is_running() { ((has_nativeip6 && is_serverip6route_set "${new_server_ip6}") || ! has_nativeip6)\ && ((has_ip6delegatedprefix && is_ip6addr_set) || ! has_ip6delegatedprefix)\ && is_dns_set && is_firewall_set && is_openvpn_running } if [ "$1" != restart ]; then # Check configuration consistency if [[ ! "${1}" =~ stop ]]; then if [ ! -e /etc/openvpn/keys/ca-server.crt ]; then critical "You need a CA server (you can add it through the web admin)" fi empty=$(find /etc/openvpn/keys/ -empty -name credentials &> /dev/null | wc -l) if [ "${empty}" -gt 0 -a ! -e /etc/openvpn/keys/user.key ]; then critical "You need either a client certificate, either a username, or both (you can add one through the web admin)" fi fi # Variables info "Retrieving Yunohost settings... " ynh_service_enabled=$(ynh_setting_get vpnclient service_enabled) ynh_ip6_addr=$(ynh_setting_get vpnclient ip6_addr) ynh_dns_method=$(ynh_setting_get vpnclient dns_method) ynh_dns=$(ynh_setting_get vpnclient nameservers) old_ip6_gw=$(ynh_setting_get vpnclient ip6_gw) old_wired_device=$(ynh_setting_get vpnclient wired_device) old_server_ip6=$(ynh_setting_get vpnclient server_ip6) new_ip6_gw=$(ip -6 route | awk '/default via/ { print $3 }') new_wired_device=$(ip route | awk '/default via/ { print $5; }') ynh_server_names=$(grep -o -P '^\s*remote\s+\K([^\s]+)' /etc/openvpn/client.conf | sort | uniq) new_server_ip6=$(dig AAAA +short $ynh_server_names @127.0.0.1 | grep -v '\.$' | grep -v "timed out" | sort | uniq) for i in $ynh_server_names; do if [[ "${i}" =~ : ]] && [[ ! "$new_server_ip6" == *"${i}"* ]] ; then new_server_ip6+=" ${i}" fi done success "Settings retrieved" fi ################################################################################### # Start / stop / restart / status handling # ################################################################################### case "${1}" in # ########## # # Starting # # ########## # start) if is_running; then info "Service is already running" exit 0 elif [ "${ynh_service_enabled}" -eq 0 ]; then warn "Service is disabled, not starting it" exit 0 fi if [ -e /etc/openvpn/keys/user.crt ] && ! cat /etc/openvpn/keys/user.crt | openssl x509 -noout -checkend 0 >/dev/null then critical "Failed to start OpenVPN client : user certificate expired" fi info "[vpnclient] Starting..." touch /tmp/.ynh-vpnclient-started # Run openvpn if is_openvpn_running; then info "(openvpn is already running)" else start_openvpn fi # Check old state of the server ipv6 route if [ ! -z "${old_server_ip6}" -a ! -z "${old_ip6_gw}" -a ! -z "${old_wired_device}"\ -a \( "${new_server_ip6}" != "${old_server_ip6}" -o "${new_ip6_gw}" != "${old_ip6_gw}"\ -o "${new_wired_device}" != "${old_wired_device}" \) ]\ && is_serverip6route_set "${old_server_ip6}" then unset_serverip6route "${old_server_ip6}" "${old_ip6_gw}" "${old_wired_device}" fi # Set the new server ipv6 route if has_nativeip6 && ! is_serverip6route_set "${new_server_ip6}" then set_serverip6route "${new_server_ip6}" "${new_ip6_gw}" "${new_wired_device}" fi # Set the ipv6 address if has_ip6delegatedprefix && ! is_ip6addr_set then set_ip6addr fi # Set host DNS resolvers if ! is_dns_set then set_dns fi # Set ipv6/ipv4 firewall if ! is_firewall_set then set_firewall fi # Update dynamic settings info "Saving settings..." ynh_setting_set vpnclient server_ip6 "${new_server_ip6}" ynh_setting_set vpnclient ip6_gw "${new_ip6_gw}" ynh_setting_set vpnclient wired_device "${new_wired_device}" ping -c1 -w5 debian.org >/dev/null ipv4=$(ping -w3 -c1 ip.yunohost.org >/dev/null 2>&1 && curl --max-time 5 https://ip.yunohost.org --silent) ipv6=$(ping -w3 -c1 ip6.yunohost.org >/dev/null 2>&1 && curl --max-time 5 https://ip6.yunohost.org --silent) info "Validating that VPN is up and the server is connected to internet..." if ip route get 1.2.3.4 | grep -q tun0; then if ping -c1 -w5 debian.org >/dev/null; then success "YunoHost VPN client started!" else critical "The VPN is up but debian.org cannot be reached, indicating that something is probably misconfigured/blocked." fi else critical "IPv4 routes are misconfigured !?" fi ;; # ########## # # Stopping # # ########## # stop) info "[vpnclient] Stopping..." rm -f /tmp/.ynh-vpnclient-started if has_ip6delegatedprefix && is_ip6addr_set; then unset_ip6addr fi if is_serverip6route_set "${old_server_ip6}"; then unset_serverip6route "${old_server_ip6}" "${old_ip6_gw}" "${old_wired_device}" fi is_firewall_set && unset_firewall is_dns_set && unset_dns is_openvpn_running && stop_openvpn ;; # ########## # # Restart # # ########## # restart) $0 stop $0 start ;; # ########## # # Status # # ########## # status) exitcode=0 if [ "${ynh_service_enabled}" -eq 0 ]; then error "VPN Client Service disabled" exitcode=1 fi info "Autodetected internet interface: ${new_wired_device} (last start: ${old_wired_device})" info "Autodetected IPv6 address for the VPN server: ${new_server_ip6} (last start: ${old_server_ip6})" if has_ip6delegatedprefix; then info "IPv6 delegated prefix found" info "IPv6 address computed from the delegated prefix: ${ynh_ip6_addr}" if is_ip6addr_set; then success "IPv6 address correctly set" else error "No IPv6 address set" exitcode=1 fi else info "No IPv6 delegated prefix found" fi if has_nativeip6; then info "Native IPv6 detected" info "Autodetected native IPv6 gateway: ${new_ip6_gw} (last start: ${old_ip6_gw})" if is_serverip6route_set "${new_server_ip6}"; then success "IPv6 server route correctly set" else error "No IPv6 server route set" exitcode=1 fi else info "No native IPv6 detected" info "No IPv6 server route to set" fi if is_firewall_set; then success "IPv6/IPv4 firewall set" else info "No IPv6/IPv4 firewall set" exitcode=1 fi if is_dns_set; then success "Host DNS correctly set" else error "No host DNS set" exitcode=1 fi if is_openvpn_running; then success "Openvpn is running" else error "Openvpn is not running" exitcode=1 fi exit ${exitcode} ;; # ########## # # Halp # # ########## # *) echo "Usage: $0 {start|stop|restart|status}" exit 1 ;; esac exit 0