]> 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)
committerDaniel Lenski <dlenski@gmail.com>
Mon, 29 Mar 2021 03:13:30 +0000 (20:13 -0700)
Squashed Fortinet-specific code from 64eb98f068ceac0985d4356890c69b6a0bac0bdf..61edd9e6284aad30a5eb43ec84ff72171d46ca5f

Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Signed-off-by: Daniel Lenski <dlenski@gmail.com>
Makefile.am
fortinet.c [new file with mode: 0644]
library.c
openconnect-internal.h
ppp.c

index dcbf2565961b98c62c8bcfba9f2b0011ddc633dd..b15bfec924ce58fad0370953390e5844cd3d799a 100644 (file)
@@ -40,11 +40,12 @@ lib_srcs_oidc = oidc.c
 lib_srcs_ppp = ppp.c ppp.h
 lib_srcs_nullppp = nullppp.c
 lib_srcs_f5 = f5.c
+lib_srcs_fortinet = fortinet.c
 
 library_srcs += $(lib_srcs_juniper) $(lib_srcs_cisco) $(lib_srcs_oath) \
                $(lib_srcs_globalprotect) $(lib_srcs_pulse) \
                $(lib_srcs_oidc) $(lib_srcs_ppp) $(lib_srcs_nullppp) \
-               $(lib_srcs_f5)
+               $(lib_srcs_f5) $(lib_srcs_fortinet)
 
 
 lib_srcs_gnutls = gnutls.c gnutls_tpm.c gnutls_tpm2.c
diff --git a/fortinet.c b/fortinet.c
new file mode 100644 (file)
index 0000000..fd1bf03
--- /dev/null
@@ -0,0 +1,358 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Copyright © 2020-2021 David Woodhouse, Daniel Lenski
+ *
+ * Author: David Woodhouse <dwmw2@infradead.org>, Daniel Lenski <dlenski@gmail.com>
+ *
+ * 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, char *buf, int len,
+                                    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 || !len)
+               return -EINVAL;
+
+       xml_doc = xmlReadMemory(buf, len, "noname.xml", NULL,
+                               XML_PARSE_NOERROR|XML_PARSE_RECOVER);
+       if (!xml_doc) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to parse Fortinet config XML\n"));
+               vpn_progress(vpninfo, PRG_DEBUG,
+                            _("Response was:%s\n"), buf);
+               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);
+               ret = -EINVAL;
+       }
+       xmlFreeDoc(xml_doc);
+       free(s);
+       return ret;
+}
+
+int fortinet_connect(struct openconnect_info *vpninfo)
+{
+       char *res_buf = NULL;
+       struct oc_text_buf *reqbuf = NULL;
+       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) {
+               /* XX: This will happen if authentication was separate/external */
+               ret = internal_split_cookies(vpninfo, 1, "SVPNCOOKIE");
+               if (ret)
+                       return ret;
+       }
+
+       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...
+        */
+       free(vpninfo->urlpath);
+       vpninfo->urlpath = strdup("remote/index");
+       ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0);
+       if (ret < 0)
+               goto out;
+       /* We don't care what it returned as long as it was successful */
+       free(res_buf);
+       res_buf = NULL;
+
+       /* 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?
+        */
+       free(vpninfo->urlpath);
+       vpninfo->urlpath = strdup("remote/fortisslvpn");
+       ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0);
+       if (ret < 0)
+               goto out;
+       /* We don't care what it returned as long as it was successful */
+       free(res_buf);
+       res_buf = NULL;
+
+       free(vpninfo->urlpath);
+       vpninfo->urlpath = strdup("remote/fortisslvpn_xml");
+       ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0);
+       if (ret < 0)
+               goto out;
+
+       ret = parse_fortinet_xml_config(vpninfo, res_buf, ret, &ipv4, &ipv6);
+       if (ret)
+               goto out;
+
+       if (ipv4 == -1)
+               ipv4 = 0;
+       if (ipv6 == -1)
+               ipv6 = 0;
+
+       /* Now fetch the connection options */
+       ret = openconnect_open_https(vpninfo);
+       if (ret)
+               goto out;
+       reqbuf = buf_alloc();
+       buf_append(reqbuf, "GET /remote/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;
+       }
+
+       ret = openconnect_ppp_new(vpninfo, PPP_ENCAP_FORTINET_HDLC, ipv4, ipv6);
+
+ 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(res_buf);
+
+       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("remote/logout");
+       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 f0b4a3cb5242a1734ffb1fc3f32349422cee4c5b..f9f203a0b1814ca15d487de421dca47f71a1d8ee 100644 (file)
--- a/library.c
+++ b/library.c
@@ -207,6 +207,26 @@ 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,
+               .secure_cookie = "SVPNCOOKIE",
+               .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
        }, {
                .name = "nullppp",
index bd6391fd803840c97bf0b41b1a61b9f7d5555143..544a4f4e16c482042198c6742f7aa1dace4a2b85 100644 (file)
@@ -190,7 +190,8 @@ struct pkt {
 #define PPP_ENCAP_RFC1662_HDLC 2       /* PPP with HDLC-like framing (RFC1662) */
 #define PPP_ENCAP_F5           3       /* F5 BigIP no HDLC */
 #define PPP_ENCAP_F5_HDLC      4       /* F5 BigIP HDLC */
-#define PPP_ENCAP_MAX          PPP_ENCAP_F5_HDLC
+#define PPP_ENCAP_FORTINET_HDLC 5      /* Fortinet HDLC */
+#define PPP_ENCAP_MAX          PPP_ENCAP_FORTINET_HDLC
 
 #define COMPR_DEFLATE  (1<<0)
 #define COMPR_LZS      (1<<1)
@@ -989,6 +990,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 e80b4d2290684acc31e6c82ca3cfdaa36e9c2b71..3115092761073b32aeb661dadb96710baec4bd3e 100644 (file)
--- a/ppp.c
+++ b/ppp.c
@@ -172,6 +172,7 @@ static const char *encap_names[PPP_ENCAP_MAX+1] = {
        "RFC1662 HDLC",
        "F5",
        "F5 HDLC",
+       "FORTINET HDLC",
 };
 
 static const char *lcp_names[] = {
@@ -253,6 +254,7 @@ int openconnect_ppp_new(struct openconnect_info *vpninfo,
                /* fall through */
 
        case PPP_ENCAP_RFC1662_HDLC:
+       case PPP_ENCAP_FORTINET_HDLC:
                ppp->encap_len = 0;
                ppp->hdlc = 1;
                break;