From 9b0fce23d1a60d6cc6e241b7996451fa08e776ee Mon Sep 17 00:00:00 2001 From: Aaron Goodman Date: Fri, 22 May 2020 18:57:42 -0400 Subject: [PATCH] openfortivpn: Use netifd for script rather than init script By using the netifd for open fortivpn we are able to set up multiple VPN connections and manage them through the netifd toolset. This also adds support for binding an openfortivpn client to a given interface, in which case when that interface comes online, the vpn will be initiated via a hotplug script. This is a breaking commit and configurations will need to be migrated from openfortivpn.config into the /etc/config/networks. Example configuration via /etc/config/network: config interface 'ftvpn' option proto 'openfortivpn' option server 'example.com' option username 'USERNAME' option password 'PASSWORD' # optional arguments follow option local_ip '192.0.5.1' option port '443' option iface_name 'wan' option trusted_cert 'CERT_HASH' option set_dns '0' option pppd_use_peerdns '0' option metric '10' Signed-off-by: Aaron Goodman --- net/openfortivpn/Makefile | 15 +- net/openfortivpn/files/14-openforticlient | 18 ++ net/openfortivpn/files/openfortivpn-wrapper | 13 ++ net/openfortivpn/files/openfortivpn.config | 12 -- net/openfortivpn/files/openfortivpn.init | 75 -------- net/openfortivpn/files/openfortivpn.sh | 139 +++++++++++++ net/openfortivpn/patches/010-bind-iface.patch | 182 ++++++++++++++++++ 7 files changed, 358 insertions(+), 96 deletions(-) create mode 100644 net/openfortivpn/files/14-openforticlient create mode 100755 net/openfortivpn/files/openfortivpn-wrapper delete mode 100644 net/openfortivpn/files/openfortivpn.config delete mode 100644 net/openfortivpn/files/openfortivpn.init create mode 100755 net/openfortivpn/files/openfortivpn.sh create mode 100644 net/openfortivpn/patches/010-bind-iface.patch diff --git a/net/openfortivpn/Makefile b/net/openfortivpn/Makefile index 1ae52eb80..d81897995 100644 --- a/net/openfortivpn/Makefile +++ b/net/openfortivpn/Makefile @@ -32,7 +32,7 @@ define Package/openfortivpn CATEGORY:=Network TITLE:=Fortinet SSL VPN client URL:=https://github.com/adrienverge/openfortivpn - DEPENDS:=+ppp +libopenssl + DEPENDS:=+ppp +libopenssl +resolveip endef define Package/openfortivpn/description @@ -50,19 +50,16 @@ CONFIGURE_ARGS += \ TARGET_LDFLAGS += -Wl,--gc-sections,--as-needed -define Package/openfortivpn/conffiles -/etc/config/openfortivpn -endef - define Package/openfortivpn/install $(INSTALL_DIR) \ $(1)/usr/sbin \ - $(1)/etc/config \ - $(1)/etc/init.d + $(1)/lib/netifd/proto \ + $(1)/etc/hotplug.d/iface $(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/openfortivpn $(1)/usr/sbin/ - $(INSTALL_DATA) ./files/openfortivpn.config $(1)/etc/config/openfortivpn - $(INSTALL_BIN) ./files/openfortivpn.init $(1)/etc/init.d/openfortivpn + $(INSTALL_BIN) ./files/openfortivpn-wrapper $(1)/usr/sbin/ + $(INSTALL_BIN) ./files/openfortivpn.sh $(1)/lib/netifd/proto/ + $(INSTALL_BIN) ./files/14-openforticlient $(1)/etc/hotplug.d/iface/ endef $(eval $(call BuildPackage,openfortivpn)) diff --git a/net/openfortivpn/files/14-openforticlient b/net/openfortivpn/files/14-openforticlient new file mode 100644 index 000000000..336e05a9c --- /dev/null +++ b/net/openfortivpn/files/14-openforticlient @@ -0,0 +1,18 @@ +#!/bin/sh +. /usr/share/libubox/jshn.sh +[ "$ACTION" != ifup ] && exit + +networks=$(uci show network | sed "s/network.\([^.]*\).proto='openfortivpn'/\1/;t;d") +for i in $networks; do + iface=$(uci get "network.${i}.iface_name") + iface_success=$? + [ $? -eq 0 ] && [ $INTERFACE == "$iface" ] && { + logger -t "openfortivpnhotplug" "$ACTION on $INTERFACE to bring up $i" + json_load "$(ifstatus $i)" + json_get_var autostart autostart + [ "$autostart" -eq 0 ] && { + logger -t "openfortivpnhotplug" "auto-start was false. bringing $i up" + ubus call network.interface up "{ \"interface\" : \"$i\" }" + } + } +done diff --git a/net/openfortivpn/files/openfortivpn-wrapper b/net/openfortivpn/files/openfortivpn-wrapper new file mode 100755 index 000000000..a64d94d83 --- /dev/null +++ b/net/openfortivpn/files/openfortivpn-wrapper @@ -0,0 +1,13 @@ +#!/bin/sh + +# This script wraps openfortivpn in order to obtain the password +# file from cmd and to daemonize + +# $1 password file +# $2... are passed to openconnect + +test -z "$1" && exit 1 + +pwfile=$1 +shift +exec /usr/sbin/openfortivpn "$@" < $pwfile \ No newline at end of file diff --git a/net/openfortivpn/files/openfortivpn.config b/net/openfortivpn/files/openfortivpn.config deleted file mode 100644 index 108e3eb7e..000000000 --- a/net/openfortivpn/files/openfortivpn.config +++ /dev/null @@ -1,12 +0,0 @@ -config service 'openfortivpn' - option 'enabled' '0' - option 'host' 'vpn-gateway' - option 'port' '10443' - option 'set_routes' '0' - option 'set_dns' '0' - option 'pppd_use_peerdns' '0' - option 'username' 'foo' - option 'password' 'bar' -config 'certs' -# example X509 certificate sha256 sum, trust only defined one(s)! - option 'trusted_cert' 'e46d4aff08ba6914e64daa85bc6112a422fa7ce16631bff0b592a28556f993db' diff --git a/net/openfortivpn/files/openfortivpn.init b/net/openfortivpn/files/openfortivpn.init deleted file mode 100644 index e9fdc20d5..000000000 --- a/net/openfortivpn/files/openfortivpn.init +++ /dev/null @@ -1,75 +0,0 @@ -#!/bin/sh /etc/rc.common - -START=99 -USE_PROCD=1 -BIN=/usr/sbin/openfortivpn -CONFIG=/var/etc/openfortivpn.config - - -validate_certs_section() { - uci_load_validate openfortivpn certs "$1" "$2" \ - 'trusted_cert:string' -} - -validate_openfortivpn_section() { - uci_load_validate openfortivpn service "$1" "$2" \ - 'enabled:uinteger' \ - 'host:string' \ - 'port:uinteger' \ - 'username:string' \ - 'password:string' \ - 'set_routes:uinteger' \ - 'set_dns:uinteger' \ - 'pppd_use_peerdns:uinteger' -} - -setup_certs() { - [ "$2" = 0 ] || { - echo "validation failed" - return 1 - } - - [ -n "$trusted_cert" ] || return 0 - echo "trusted-cert = $trusted_cert" >> $CONFIG -} - -setup_config() { - [ "$2" = 0 ] || { - echo "validation failed" - return 1 - } - - [ "$enabled" -eq 0 ] && return 1 - - mkdir -p /var/etc - echo '# auto-generated config file from /etc/config/openfortivpn' > $CONFIG - - [ -n "$host" ] && echo "host = $host" >> $CONFIG - [ -n "$port" ] && echo "port = $port" >> $CONFIG - [ -n "$username" ] && echo "username = $username" >> $CONFIG - [ -n "$password" ] && echo "password = $password" >> $CONFIG - [ -n "$set_routes" ] && echo "set-routes = $set_routes" >> $CONFIG - [ -n "$set_dns" ] && echo "set-dns = $set_dns" >> $CONFIG - [ -n "$pppd_use_peerdns" ] && echo "pppd-use-peerdns = $pppd_use_peerdns" >> $CONFIG - return 0 -} - -start_service() { - config_load openfortivpn - validate_openfortivpn_section openfortivpn setup_config || return - config_foreach validate_certs_section certs setup_certs - - procd_open_instance - procd_set_param stderr 1 - procd_set_param command $BIN -c $CONFIG --use-syslog - procd_close_instance -} - -service_triggers () { - procd_add_reload_trigger "openfortivpn" - - procd_open_validate - validate_openfortivpn_section - validate_certs_section - procd_close_validate -} diff --git a/net/openfortivpn/files/openfortivpn.sh b/net/openfortivpn/files/openfortivpn.sh new file mode 100755 index 000000000..d69575230 --- /dev/null +++ b/net/openfortivpn/files/openfortivpn.sh @@ -0,0 +1,139 @@ +#!/bin/sh +. /lib/functions.sh +. ../netifd-proto.sh +init_proto "$@" + +append_args() { + while [ $# -gt 0 ]; do + append cmdline "'${1//\'/\'\\\'\'}'" + shift + done +} + +proto_openfortivpn_init_config() { + proto_config_add_string "server" + proto_config_add_int "port" + proto_config_add_string "iface_name" + proto_config_add_string "local_ip" + proto_config_add_string "username" + proto_config_add_string "password" + proto_config_add_string "trusted_cert" + proto_config_add_int "set_dns" + proto_config_add_int "pppd_use_peerdns" + proto_config_add_int "metric" + no_device=1 + available=1 +} + +proto_openfortivpn_setup() { + local config="$1" + + json_get_vars host server port iface_name local_ip username password trusted_cert set_dns pppd_use_peerdns metric + + ifname="vpn-$config" + + logger -t openfortivpn "$config: initializing..." + + [ -n "$iface_name" ] && { + json_load "$(ifstatus $iface_name)" + json_get_var iface_device_name device + json_get_var iface_device_up up + } + + logger -t "openfortivpn" "$config: $iface_name is status $iface_device_up" + [ "$iface_device_up" -eq 1 ] || { + logger -t "openfortivpn" "$config: $iface_name is not up $iface_device_up" + proto_notify_error "$config" "$iface_name is not up $iface_device_up" + proto_block_restart "$config" + exit 1 + } + + + server_ip=$(resolveip -t 10 "$server") + + [ $? -eq 0 ] || { + logger -t "openfortivpn" "$config: failed to resolve server ip for $server" + sleep 10 + proto_notify_error "$config" "failed to resolve server ip for $server" + proto_setup_failed "$config" + exit 1 + } + + [ -n $iface_name ] && { + ping -I $iface_device_name -c 1 -w 10 $server_ip > /dev/null 2>&1 || { + logger -t "openfortivpn" "$config: failed to ping $server_ip on $iface_device_name" + sleep 10 + proto_notify_error "$config" "failed to ping $server_ip on $iface_device_name" + proto_setup_failed "$config" + exit 1 + } + } + + for ip in $(resolveip -t 10 "$server"); do + logger -t "openfortivpn" "$config: adding host dependency for $ip on $iface_name at $config" + proto_add_host_dependency "$config" "$ip" "$iface_name" + done + + + + [ -n "$port" ] && port=":$port" + + append_args "$server$port" --pppd-ifname="$ifname" --use-syslog -c /dev/null + append_args "--set-dns=$set_dns" + append_args "--no-routes" + append_args "--pppd-use-peerdns=$pppd_use_peerdns" + + [ -n "$iface_name" ] && { + append_args "--ifname=$iface_device_name" + } + + [ -n "$trusted_cert" ] && append_args "--trusted-cert=$trusted_cert" + [ -n "$username" ] && append_args -u "$username" + [ -n "$password" ] && { + umask 077 + mkdir -p /var/etc + pwfile="/var/etc/openfortivpn-$config.passwd" + echo "$password" > "$pwfile" + } + + [ -n "$local_ip" ] || local_ip=192.0.2.1 + mkdir -p '/etc/ppp/peers' + callfile="/etc/ppp/peers/$config" + echo "115200 +:$local_ip +noipdefault +noaccomp +noauth +default-asyncmap +nopcomp +receive-all +defaultroute +nodetach +ipparam $config +lcp-max-configure 40 +ip-up-script /lib/netifd/ppp-up +ip-down-script /lib/netifd/ppp-down +mru 1354" > $callfile + append_args "--pppd-call=$config" + + proto_export INTERFACE="$ifname" + logger -t openfortivpn "$config: executing 'openfortivpn $cmdline'" + logger -t openfortivpn "$config: metric is $metric" + + eval "proto_run_command '$config' /usr/sbin/openfortivpn-wrapper '$pwfile' $cmdline" + +} + +proto_openfortivpn_teardown() { + local config="$1" + + pwfile="/var/etc/openfortivpn-$config.passwd" + callfile="/etc/ppp/peers/$config" + + rm -f $pwfile + rm -f $callfile + logger -t openfortivpn "$config: bringing down openfortivpn" + proto_kill_command "$config" 2 +} + +add_protocol openfortivpn diff --git a/net/openfortivpn/patches/010-bind-iface.patch b/net/openfortivpn/patches/010-bind-iface.patch new file mode 100644 index 000000000..76bc8c84f --- /dev/null +++ b/net/openfortivpn/patches/010-bind-iface.patch @@ -0,0 +1,182 @@ +--- a/doc/openfortivpn.1.in ++++ b/doc/openfortivpn.1.in +@@ -12,6 +12,7 @@ openfortivpn \- Client for PPP+SSL VPN t + [\fB\-\-otp\-prompt=\fI\fR] + [\fB\-\-otp\-delay=\fI\fR] + [\fB\-\-realm=\fI\fR] ++[\fB\-\-ifname=\fI\fR] + [\fB\-\-set\-routes=\fR] + [\fB\-\-no\-routes\fR] + [\fB\-\-set\-dns=\fR] +@@ -83,6 +84,9 @@ no wait (this is the default). + Connect to the specified authentication realm. Defaults to empty, which + is usually what you want. + .TP ++\fB\-\-ifname=\fI\fR ++Bind the connection to the specified network interface. ++.TP + \fB\-\-set\-routes=\fI\fR, \fB\-\-no-routes\fR + Set if openfortivpn should try to configure IP routes through the VPN when + tunnel is up. If used multiple times, the last one takes priority. +--- a/src/config.c ++++ b/src/config.c +@@ -50,6 +50,7 @@ const struct vpn_config invalid_cfg = { + .otp_delay = -1, + .pinentry = NULL, + .realm = {'\0'}, ++ .iface_name = {'\0'}, + .set_routes = -1, + .set_dns = -1, + .pppd_use_peerdns = -1, +@@ -490,6 +491,8 @@ void merge_config(struct vpn_config *dst + } + if (src->realm[0]) + strcpy(dst->realm, src->realm); ++ if (src->iface_name[0]) ++ strcpy(dst->iface_name, src->iface_name); + if (src->set_routes != invalid_cfg.set_routes) + dst->set_routes = src->set_routes; + if (src->set_dns != invalid_cfg.set_dns) +--- a/src/config.h ++++ b/src/config.h +@@ -86,6 +86,7 @@ struct vpn_config { + char *otp_prompt; + unsigned int otp_delay; + char *pinentry; ++ char iface_name[FIELD_SIZE + 1]; + char realm[FIELD_SIZE + 1]; + + int set_routes; +--- a/src/main.c ++++ b/src/main.c +@@ -51,16 +51,16 @@ + " resolver and routes directly.\n" \ + " --pppd-ifname= Set the pppd interface name, if supported by pppd.\n" \ + " --pppd-ipparam= Provides an extra parameter to the ip-up, ip-pre-up\n" \ +-" and ip-down scripts. See man (8) pppd\n" \ ++" and ip-down scripts. See man (8) pppd.\n" \ + " --pppd-call= Move most pppd options from pppd cmdline to\n" \ + " /etc/ppp/peers/ and invoke pppd with\n" \ +-" 'call '\n" ++" 'call '.\n" + #elif HAVE_USR_SBIN_PPP + #define PPPD_USAGE \ + " [--ppp-system=]\n" + #define PPPD_HELP \ + " --ppp-system= Connect to the specified system as defined in\n" \ +-" /etc/ppp/ppp.conf\n" ++" /etc/ppp/ppp.conf.\n" + #else + #error "Neither HAVE_USR_SBIN_PPPD nor HAVE_USR_SBIN_PPP have been defined." + #endif +@@ -69,7 +69,7 @@ + #define RESOLVCONF_USAGE \ + "[--use-resolvconf=<0|1>] " + #define RESOLVCONF_HELP \ +-" --use-resolvconf=[01] If possible use resolvconf to update /etc/resolv.conf\n" ++" --use-resolvconf=[01] If possible use resolvconf to update /etc/resolv.conf.\n" + #else + #define RESOLVCONF_USAGE "" + #define RESOLVCONF_HELP "" +@@ -77,14 +77,14 @@ + + #define usage \ + "Usage: openfortivpn [[:]] [-u ] [-p ]\n" \ +-" [--pinentry=]\n" \ +-" [--realm=] [--otp=] [--otp-delay=]\n" \ +-" [--otp-prompt=] [--set-routes=<0|1>]\n" \ ++" [--otp=] [--otp-delay=] [--otp-prompt=]\n" \ ++" [--pinentry=] [--realm=]\n" \ ++" [--ifname=] [--set-routes=<0|1>]\n" \ + " [--half-internet-routes=<0|1>] [--set-dns=<0|1>]\n" \ + PPPD_USAGE \ + " " RESOLVCONF_USAGE "[--ca-file=]\n" \ + " [--user-cert=] [--user-key=]\n" \ +-" [--trusted-cert=] [--use-syslog]\n" \ ++" [--use-syslog] [--trusted-cert=]\n" \ + " [--persistent=] [-c ] [-v|-q]\n" \ + " openfortivpn --help\n" \ + " openfortivpn --version\n" \ +@@ -115,10 +115,11 @@ PPPD_USAGE \ + " -u , --username= VPN account username.\n" \ + " -p , --password= VPN account password.\n" \ + " -o , --otp= One-Time-Password.\n" \ +-" --otp-prompt= Search for the OTP prompt starting with this string\n" \ ++" --otp-prompt= Search for the OTP prompt starting with this string.\n" \ + " --otp-delay= Wait seconds before sending the OTP.\n" \ +-" --pinentry= Use the program to supply a secret instead of asking for it\n" \ ++" --pinentry= Use the program to supply a secret instead of asking for it.\n" \ + " --realm= Use specified authentication realm.\n" \ ++" --ifname= Bind to interface.\n" \ + " --set-routes=[01] Set if openfortivpn should configure routes\n" \ + " when tunnel is up.\n" \ + " --no-routes Do not configure routes, same as --set-routes=0.\n" \ +@@ -127,7 +128,7 @@ PPPD_USAGE \ + " --set-dns=[01] Set if openfortivpn should add DNS name servers\n" \ + " and domain search list in /etc/resolv.conf.\n" \ + " If installed resolvconf is used for the update.\n" \ +-" --no-dns Do not reconfigure DNS, same as --set-dns=0\n" \ ++" --no-dns Do not reconfigure DNS, same as --set-dns=0.\n" \ + " --ca-file= Use specified PEM-encoded certificate bundle\n" \ + " instead of system-wide store to verify the gateway\n" \ + " certificate.\n" \ +@@ -199,6 +200,7 @@ int main(int argc, char **argv) + .otp_delay = 0, + .pinentry = NULL, + .realm = {'\0'}, ++ .iface_name = {'\0'}, + .set_routes = 1, + .set_dns = 1, + .use_syslog = 0, +@@ -245,6 +247,7 @@ int main(int argc, char **argv) + {"otp", required_argument, NULL, 'o'}, + {"otp-prompt", required_argument, NULL, 0}, + {"otp-delay", required_argument, NULL, 0}, ++ {"ifname", required_argument, NULL, 0}, + {"set-routes", required_argument, NULL, 0}, + {"no-routes", no_argument, &cli_cfg.set_routes, 0}, + {"half-internet-routes", required_argument, NULL, 0}, +@@ -427,6 +430,12 @@ int main(int argc, char **argv) + break; + } + if (strcmp(long_options[option_index].name, ++ "ifname") == 0) { ++ strncpy(cli_cfg.iface_name, optarg, FIELD_SIZE); ++ cli_cfg.iface_name[FIELD_SIZE] = '\0'; ++ break; ++ } ++ if (strcmp(long_options[option_index].name, + "set-routes") == 0) { + int set_routes = strtob(optarg); + +--- a/src/tunnel.c ++++ b/src/tunnel.c +@@ -523,12 +523,28 @@ static int tcp_connect(struct tunnel *tu + int ret, handle; + struct sockaddr_in server; + char *env_proxy; ++ const int iface_len = strnlen(tunnel->config->iface_name, IFNAMSIZ); + + handle = socket(AF_INET, SOCK_STREAM, 0); ++ + if (handle == -1) { + log_error("socket: %s\n", strerror(errno)); + goto err_socket; + } ++ if (iface_len == IFNAMSIZ) { ++ log_error("socket: Too long iface name"); ++ goto err_socket; ++ } ++ if (iface_len > 0) { ++ ret = setsockopt(handle, SOL_SOCKET, SO_BINDTODEVICE, ++ tunnel->config->iface_name, iface_len); ++ if (ret) { ++ log_error("socket: setting interface name failed with error: %d", ++ errno); ++ goto err_socket; ++ } ++ } ++ + env_proxy = getenv("https_proxy"); + if (env_proxy == NULL) + env_proxy = getenv("HTTPS_PROXY");