]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
Add basic attempt at Fortinet support
authorDavid Woodhouse <dwmw2@infradead.org>
Wed, 13 May 2020 13:58:56 +0000 (14:58 +0100)
committerDavid Woodhouse <dwmw2@infradead.org>
Wed, 13 May 2020 13:59:25 +0000 (14:59 +0100)
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Makefile.am
fortinet.c [new file with mode: 0644]
library.c
openconnect-internal.h
ppp.c

index 4d48d4c99f1a5bae3f169df17a0e43fd43ebfa10..f8c57574ab794f957af0bfa874459ea358a4ec42 100644 (file)
@@ -31,14 +31,16 @@ library_srcs = ssl.c http.c http-auth.c auth-common.c library.c compat.c lzs.c m
 lib_srcs_cisco = auth.c cstp.c
 lib_srcs_juniper = oncp.c lzo.c auth-juniper.c
 lib_srcs_pulse = pulse.c
-lib_srcs_f5 = f5.c ppp.c ppp.h
+lib_srcs_f5 = f5.c
+lib_srcs_ppp = ppp.c ppp.h
+lib_srcs_fortinet = fortinet.c
 lib_srcs_globalprotect = gpst.c auth-globalprotect.c
 lib_srcs_oath = oath.c
 lib_srcs_oidc = oidc.c
 
 library_srcs += $(lib_srcs_juniper) $(lib_srcs_cisco) $(lib_srcs_oath) \
                $(lib_srcs_globalprotect) $(lib_srcs_pulse) $(lib_srcs_f5) \
-               $(lib_srcs_oidc)
+               $(lib_srcs_ppp) $(lib_srcs_fortinet) $(lib_srcs_oidc)
 
 lib_srcs_gnutls = gnutls.c gnutls_tpm.c gnutls_tpm2.c
 lib_srcs_openssl = openssl.c openssl-pkcs11.c
diff --git a/fortinet.c b/fortinet.c
new file mode 100644 (file)
index 0000000..7644774
--- /dev/null
@@ -0,0 +1,445 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Copyright © 2020 David Woodhouse
+ *
+ * Author: David Woodhouse <dwmw2@infradead.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * 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
+ * Lesser General Public License for more details.
+ */
+
+#include <config.h>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <stdarg.h>
+#include <sys/types.h>
+
+#include "openconnect-internal.h"
+
+#define XCAST(x) ((const xmlChar *)(x))
+
+int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
+{
+       return -EINVAL;
+}
+
+static int xmlnode_bool_or_int_value(struct openconnect_info *vpninfo, xmlNode *node)
+{
+       int ret = -1;
+       char *content = (char *)xmlNodeGetContent(node);
+       if (!content)
+               return -1;
+
+       if (isdigit(content[0]))
+               ret = atoi(content);
+       if (!strcasecmp(content, "yes") || !strcasecmp(content, "on"))
+               ret = 1;
+       if (!strcasecmp(content, "no") || !strcasecmp(content, "off"))
+               ret = 0;
+
+       free(content);
+       return ret;
+}
+
+/* We behave like CSTP — create a linked list in vpninfo->cstp_options
+ * with the strings containing the information we got from the server,
+ * and oc_ip_info contains const copies of those pointers.
+ *
+ * (unlike version in oncp.c, val is stolen rather than strdup'ed) */
+
+static const char *add_option(struct openconnect_info *vpninfo, const char *opt, char **val)
+{
+       struct oc_vpn_option *new = malloc(sizeof(*new));
+       if (!new)
+               return NULL;
+
+       new->option = strdup(opt);
+       if (!new->option) {
+               free(new);
+               return NULL;
+       }
+       new->value = *val;
+       *val = NULL;
+       new->next = vpninfo->cstp_options;
+       vpninfo->cstp_options = new;
+
+       return new->value;
+}
+
+static int parse_fortinet_xml_config(struct openconnect_info *vpninfo, struct oc_text_buf *buf,
+                                    int *ipv4, int *ipv6)
+{
+       xmlNode *fav_node, *obj_node, *xml_node;
+       xmlDocPtr xml_doc;
+       int ret = 0, ii, n_dns = 0, n_nbns = 0, default_route = 0;
+       char *s = NULL;
+       struct oc_text_buf *domains = NULL;
+
+       if (buf_error(buf))
+               return buf_error(buf);
+
+       xml_doc = xmlReadMemory(buf->data, buf->pos, "noname.xml", NULL,
+                               XML_PARSE_NOERROR|XML_PARSE_RECOVER);
+       if (!xml_doc) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to parse Fortinet options response\n"));
+               vpn_progress(vpninfo, PRG_DEBUG,
+                            _("Response was:%s\n"), buf->data);
+               return -EINVAL;
+       }
+       fav_node = xmlDocGetRootElement(xml_doc);
+       if (!xmlnode_is_named(fav_node, "favorite"))
+               goto err;
+
+       obj_node = xmlFirstElementChild(fav_node);
+       if (!xmlnode_is_named(obj_node, "object"))
+               goto err;
+
+       /* Clear old options which will be overwritten */
+       vpninfo->ip_info.addr = vpninfo->ip_info.netmask = NULL;
+       vpninfo->ip_info.addr6 = vpninfo->ip_info.netmask6 = NULL;
+       vpninfo->ip_info.domain = NULL;
+       vpninfo->cstp_options = NULL;
+       for (ii = 0; ii < 3; ii++)
+               vpninfo->ip_info.dns[ii] = vpninfo->ip_info.nbns[ii] = NULL;
+       free_split_routes(vpninfo);
+
+       domains = buf_alloc();
+
+       for (xml_node = xmlFirstElementChild(obj_node);
+            xml_node;
+            xml_node = xmlNextElementSibling(xml_node)) {
+               if (xmlnode_is_named(xml_node, "IPV4_0"))
+                       *ipv4 = xmlnode_bool_or_int_value(vpninfo, xml_node);
+               else if (xmlnode_is_named(xml_node, "IPV6_0")) {
+                       if (!vpninfo->disable_ipv6)
+                               *ipv6 = xmlnode_bool_or_int_value(vpninfo, xml_node);
+               } else if (xmlnode_is_named(xml_node, "idle_session_timeout")) {
+                       int sec = vpninfo->idle_timeout = xmlnode_bool_or_int_value(vpninfo, xml_node);
+                       vpn_progress(vpninfo, PRG_INFO, _("Idle timeout is %d minutes.\n"), sec/60);
+               } else if (xmlnode_is_named(xml_node, "tunnel_port_dtls")) {
+                       int port = xmlnode_bool_or_int_value(vpninfo, xml_node);
+                       udp_sockaddr(vpninfo, port);
+                       vpn_progress(vpninfo, PRG_INFO, _("DTLS port is %d.\n"), port);
+               } else if (xmlnode_is_named(xml_node, "UseDefaultGateway0")) {
+                       default_route = xmlnode_bool_or_int_value(vpninfo, xml_node);
+                       vpn_progress(vpninfo, PRG_INFO, _("Got UseDefaultGateway0 value of %d.\n"), default_route);
+               } else if (xmlnode_is_named(xml_node, "SplitTunneling0")) {
+                       int st = xmlnode_bool_or_int_value(vpninfo, xml_node);
+                       vpn_progress(vpninfo, PRG_INFO, _("Got SplitTunneling0 value of %d.\n"), st);
+                }
+               /* XX: This is an objectively stupid way to use XML, a hierarchical data format. */
+               else if (   (!strncmp((char *)xml_node->name, "DNS", 3) && isdigit(xml_node->name[3]))
+                        || (!strncmp((char *)xml_node->name, "DNS6_", 5) && isdigit(xml_node->name[5])) ) {
+                       s = (char *)xmlNodeGetContent(xml_node);
+                       if (s && *s) {
+                               vpn_progress(vpninfo, PRG_INFO, _("Got IPv%d DNS server %s.\n"),
+                                            xml_node->name[4]=='_' ? 6 : 4, s);
+                               if (n_dns < 3) vpninfo->ip_info.dns[n_dns++] = add_option(vpninfo, "DNS", &s);
+                       }
+               } else if (!strncmp((char *)xml_node->name, "WINS", 4) && isdigit(xml_node->name[4])) {
+                       s = (char *)xmlNodeGetContent(xml_node);
+                       if (s && *s) {
+                               vpn_progress(vpninfo, PRG_INFO, _("Got WINS/NBNS server %s.\n"), s);
+                               if (n_nbns < 3) vpninfo->ip_info.dns[n_nbns++] = add_option(vpninfo, "WINS", &s);
+                       }
+               } else if (!strncmp((char *)xml_node->name, "DNSSuffix", 9) && isdigit(xml_node->name[9])) {
+                       s = (char *)xmlNodeGetContent(xml_node);
+                       if (s && *s) {
+                               vpn_progress(vpninfo, PRG_INFO, _("Got search domain %s.\n"), s);
+                               buf_append(domains, "%s ", s);
+                       }
+               } else if (   (!strncmp((char *)xml_node->name, "LAN", 3) && isdigit((char)xml_node->name[3]))
+                          || (!strncmp((char *)xml_node->name, "LAN6_", 5) && isdigit((char)xml_node->name[5]))) {
+                       s = (char *)xmlNodeGetContent(xml_node);
+                       if (s && *s) {
+                               char *word, *next;
+                               struct oc_split_include *inc;
+
+                               for (word = (char *)add_option(vpninfo, "route-list", &s);
+                                    *word; word = next) {
+                                       for (next = word; *next && !isspace(*next); next++);
+                                       if (*next)
+                                               *next++ = 0;
+                                       if (next == word + 1)
+                                               continue;
+
+                                       inc = malloc(sizeof(*inc));
+                                       inc->route = word;
+                                       inc->next = vpninfo->ip_info.split_includes;
+                                       vpninfo->ip_info.split_includes = inc;
+                                       vpn_progress(vpninfo, PRG_INFO, _("Got IPv%d route %s.\n"),
+                                                    xml_node->name[4]=='_' ? 6 : 4, word);
+                               }
+                       }
+               }
+       }
+
+       if (default_route && ipv4)
+               vpninfo->ip_info.netmask = strdup("0.0.0.0");
+       if (default_route && ipv6)
+               vpninfo->ip_info.netmask6 = strdup("::");
+       if (buf_error(domains) == 0 && domains->pos > 0) {
+               domains->data[domains->pos-1] = '\0';
+               vpninfo->ip_info.domain = add_option(vpninfo, "search", &domains->data);
+       }
+       buf_free(domains);
+
+       if (*ipv4 < 1 && *ipv6 < 1) {
+       err:
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to find VPN options\n"));
+               vpn_progress(vpninfo, PRG_DEBUG,
+                            _("Response was:%s\n"), buf->data);
+               ret = -EINVAL;
+       }
+       xmlFreeDoc(xml_doc);
+       free(s);
+       return ret;
+}
+
+int fortinet_connect(struct openconnect_info *vpninfo)
+{
+       struct oc_text_buf *reqbuf;
+       int ret, ipv4 = -1, ipv6 = -1;
+
+       /* XXX: We should do what cstp_connect() does to check that configuration
+          hasn't changed on a reconnect. */
+
+       if (!vpninfo->cookies && vpninfo->cookie)
+               http_add_cookie(vpninfo, "SVPNCOOKIE", vpninfo->cookie, 1);
+
+       ret = openconnect_open_https(vpninfo);
+       if (ret)
+               return ret;
+
+       reqbuf = buf_alloc();
+
+       /* Request VPN allocation
+        *
+        * XXX: Should this be done on every reconnect, or should it have
+        * been part of fortinet_obtain_cookie(). For the moment while
+        * we're letting the auth happen externally for now, let's do it
+        * here...
+        */
+
+       buf_append(reqbuf, "GET /remote/index HTTP/1.1\r\n");
+       http_common_headers(vpninfo, reqbuf);
+       buf_append(reqbuf, "\r\n");
+
+       if (buf_error(reqbuf)) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Error creating Fortinet index request\n"));
+               ret = buf_error(reqbuf);
+               goto out;
+       }
+       if (vpninfo->dump_http_traffic)
+               dump_buf(vpninfo, '>', reqbuf->data);
+
+       ret = vpninfo->ssl_write(vpninfo, reqbuf->data, reqbuf->pos);
+       if (ret < 0)
+               goto out;
+
+       ret = process_http_response(vpninfo, 0, NULL, reqbuf);
+       if (ret < 0)
+               goto out;
+
+       if (ret != 201 && ret != 200) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Unexpected %d result from server\n"),
+                            ret);
+               ret = -EINVAL;
+               goto out;
+       }
+
+       /* We don't care about what it returns */
+       if (vpninfo->dump_http_traffic)
+               dump_buf(vpninfo, '<', reqbuf->data);
+       buf_truncate(reqbuf);
+
+       /* XXX: Why was auth_request_vpn_allocation() doing this anyway?
+        * It's fetching the legacy non-XML configuration, isn't it?
+        * Do we *actually* have to do this, before fetching the XML config?
+        */
+       buf_append(reqbuf, "GET /remote/fortisslvpn HTTP/1.1\r\n");
+       http_common_headers(vpninfo, reqbuf);
+       buf_append(reqbuf, "\r\n");
+
+       if (buf_error(reqbuf)) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Error creating Fortinet legacy config request\n"));
+               ret = buf_error(reqbuf);
+               goto out;
+       }
+       if (vpninfo->dump_http_traffic)
+               dump_buf(vpninfo, '>', reqbuf->data);
+
+       ret = vpninfo->ssl_write(vpninfo, reqbuf->data, reqbuf->pos);
+       if (ret < 0)
+               goto out;
+
+       ret = process_http_response(vpninfo, 0, NULL, reqbuf);
+       if (ret < 0)
+               goto out;
+
+       if (ret != 201 && ret != 200) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Unexpected %d result from server\n"),
+                            ret);
+               ret = -EINVAL;
+               goto out;
+       }
+
+       /* ... and auth_request_vpn_allocation() just throws it away,
+        * before fetching the XML version. Or refetching this and
+        * *actually* looking at it, if the legacy mode is enabled.
+        * WTF? */
+       if (vpninfo->dump_http_traffic)
+               dump_buf(vpninfo, '<', reqbuf->data);
+       buf_truncate(reqbuf);
+
+       /* Now fetch the XML version of the config for real */
+       ret = openconnect_open_https(vpninfo);
+       if (ret)
+               goto out;
+       buf_append(reqbuf, "GET /remote/fortisslvpn_xml HTTP/1.1\r\n");
+       http_common_headers(vpninfo, reqbuf);
+       buf_append(reqbuf, "\r\n");
+
+       if (buf_error(reqbuf)) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Error creating fortinet options request\n"));
+               ret = buf_error(reqbuf);
+               goto out;
+       }
+       if (vpninfo->dump_http_traffic)
+               dump_buf(vpninfo, '>', reqbuf->data);
+       ret = vpninfo->ssl_write(vpninfo, reqbuf->data, reqbuf->pos);
+       if (ret < 0)
+               goto out;
+
+       ret = process_http_response(vpninfo, 0, NULL, reqbuf);
+       if (ret < 0)
+               goto out;
+
+       if (ret != 201 && ret != 200) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Unexpected %d result from server\n"),
+                            ret);
+               ret = -EINVAL;
+               goto out;
+       }
+
+       if (vpninfo->dump_http_traffic)
+               dump_buf(vpninfo, '<', reqbuf->data);
+       ret = parse_fortinet_xml_config(vpninfo, reqbuf, &ipv4, &ipv6);
+       if (ret)
+               goto out;
+
+       buf_truncate(reqbuf);
+
+       if (ipv4 == -1)
+               ipv4 = 0;
+       if (ipv6 == -1)
+               ipv6 = 0;
+
+       /* Now fetch the connection options */
+       ret = openconnect_open_https(vpninfo);
+       if (ret)
+               goto out;
+       buf_append(reqbuf, "GET /remopte/sslvpn-tunnel HTTP/1.1\r\n");
+       http_common_headers(vpninfo, reqbuf);
+       buf_append(reqbuf, "\r\n");
+
+       if (buf_error(reqbuf)) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Error creating fortinet connection request\n"));
+               ret = buf_error(reqbuf);
+               goto out;
+       }
+       if (vpninfo->dump_http_traffic)
+               dump_buf(vpninfo, '>', reqbuf->data);
+       ret = vpninfo->ssl_write(vpninfo, reqbuf->data, reqbuf->pos);
+       if (ret < 0)
+               goto out;
+
+       ret = process_http_response(vpninfo, 1, NULL, reqbuf);
+       if (ret < 0)
+               goto out;
+
+       if (ret != 201 && ret != 200) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Unexpected %d result from server\n"),
+                            ret);
+               ret = -EINVAL;
+               goto out;
+       }
+
+       vpninfo->ppp = openconnect_ppp_new(PPP_ENCAP_FORTINET_HDLC, ipv4, ipv6);
+       if (!vpninfo->ppp) {
+               ret = -ENOMEM;
+               goto out;
+       }
+
+       ret = 0; /* success */
+ out:
+       if (ret)
+               openconnect_close_https(vpninfo, 0);
+       else {
+               monitor_fd_new(vpninfo, ssl);
+               monitor_read_fd(vpninfo, ssl);
+               monitor_except_fd(vpninfo, ssl);
+       }
+       buf_free(reqbuf);
+
+       free(vpninfo->cstp_pkt);
+       vpninfo->cstp_pkt = NULL;
+
+       vpninfo->ip_info.mtu = 1400;
+
+       return ret;
+}
+
+int fortinet_bye(struct openconnect_info *vpninfo, const char *reason)
+{
+       char *orig_path;
+       char *res_buf=NULL;
+       int ret;
+
+       /* XX: handle clean PPP termination?
+          ppp_bye(vpninfo); */
+
+       /* We need to close and reopen the HTTPS connection (to kill
+        * the fortinet tunnel) and submit a new HTTPS request to logout.
+        */
+       openconnect_close_https(vpninfo, 0);
+
+       orig_path = vpninfo->urlpath;
+       vpninfo->urlpath = strdup("vdesk/hangup.php3?hangup_error=1"); /* redirect segfaults without strdup */
+       ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0);
+       free(vpninfo->urlpath);
+       vpninfo->urlpath = orig_path;
+
+       if (ret < 0)
+               vpn_progress(vpninfo, PRG_ERR, _("Logout failed.\n"));
+       else
+               vpn_progress(vpninfo, PRG_INFO, _("Logout successful.\n"));
+
+       free(res_buf);
+       return ret;
+}
index 31709cb4ffd19c59997860c398e8c95bb97a1bd7..f6b5c87d334ee2269444ae848f9da2c1c459037c 100644 (file)
--- a/library.c
+++ b/library.c
@@ -203,6 +203,25 @@ static const struct vpn_proto openconnect_protos[] = {
                .udp_shutdown = esp_shutdown,
                .udp_send_probes = oncp_esp_send_probes,
                .udp_catch_probe = oncp_esp_catch_probe,
+#endif
+       }, {
+               .name = "fortinet",
+               .pretty_name = N_("Fortinet SSL VPN"),
+               .description = N_("Compatible with FortiGate SSL VPN"),
+               .flags = OC_PROTO_PROXY,
+               .vpn_close_session = fortinet_bye,
+               .tcp_connect = fortinet_connect,
+               .tcp_mainloop = ppp_mainloop,
+               .add_http_headers = http_common_headers,
+               .obtain_cookie = fortinet_obtain_cookie,
+               .udp_protocol = "DTLS",
+#ifdef HAVE_DTLSx /* Not yet... */
+               .udp_setup = esp_setup,
+               .udp_mainloop = esp_mainloop,
+               .udp_close = esp_close,
+               .udp_shutdown = esp_shutdown,
+               .udp_send_probes = oncp_esp_send_probes,
+               .udp_catch_probe = oncp_esp_catch_probe,
 #endif
        },
 };
index ad66d09947fa32cc63298e8aa34ff067db032554..8a04992b6a4ce3d5c7b48e7a4aa7f4a38c65c55d 100644 (file)
@@ -176,8 +176,9 @@ struct pkt {
 #define DTLS_CONNECTING        4       /* ESP probe received; must tell server */
 #define DTLS_CONNECTED 5       /* Server informed and should be sending ESP */
 
-#define PPP_ENCAP_F5       1   /* F5 BigIP no HDLC */
-#define PPP_ENCAP_F5_HDLC   2  /* F5 BigIP HDLC */
+#define PPP_ENCAP_F5           1       /* F5 BigIP no HDLC */
+#define PPP_ENCAP_F5_HDLC      2       /* F5 BigIP HDLC */
+#define PPP_ENCAP_FORTINET_HDLC        3       /* Fortinet HDLC */
 
 #define COMPR_DEFLATE  (1<<0)
 #define COMPR_LZS      (1<<1)
@@ -937,6 +938,11 @@ int f5_obtain_cookie(struct openconnect_info *vpninfo);
 int f5_connect(struct openconnect_info *vpninfo);
 int f5_bye(struct openconnect_info *vpninfo, const char *reason);
 
+/* fortinet.c */
+int fortinet_obtain_cookie(struct openconnect_info *vpninfo);
+int fortinet_connect(struct openconnect_info *vpninfo);
+int fortinet_bye(struct openconnect_info *vpninfo, const char *reason);
+
 /* ppp.c */
 struct oc_ppp;
 void buf_append_ppphdlc(struct oc_text_buf *buf, const unsigned char *bytes, int len, uint32_t asyncmap);
diff --git a/ppp.c b/ppp.c
index d38f7ac63e80a55b4fabb8fd0cd00c036ea1d3d1..54646d49fbaaf1e540acc50188be299f712ba8b2 100644 (file)
--- a/ppp.c
+++ b/ppp.c
@@ -177,6 +177,7 @@ struct oc_ppp *openconnect_ppp_new(int encap, int want_ipv4, int want_ipv6)
                break;
 
        case PPP_ENCAP_F5_HDLC:
+       case PPP_ENCAP_FORTINET_HDLC:
                ppp->encap_len = 0;
                ppp->hdlc = 1;
                break;
@@ -660,6 +661,7 @@ int ppp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable)
                        break;
 
                case PPP_ENCAP_F5_HDLC:
+               case PPP_ENCAP_FORTINET_HDLC:
                        payload_len = unhdlc_in_place(vpninfo, ph, len, &pp);
                        if (payload_len < 0)
                                continue; /* unhdlc_in_place already logged */
@@ -857,6 +859,7 @@ int ppp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable)
                        this->ppp.hlen = -n + 4;
                        break;
                case PPP_ENCAP_F5_HDLC:
+               case PPP_ENCAP_FORTINET_HDLC:
                        /* XX: use worst-case escaping for LCP */
                        this = hdlc_into_new_pkt(vpninfo, this->data + n, this->len - n,
                                                 proto == PPP_LCP ? ASYNCMAP_LCP : ppp->out_asyncmap);