From: David Woodhouse Date: Wed, 13 May 2020 13:58:56 +0000 (+0100) Subject: Add basic attempt at Fortinet support X-Git-Tag: v8.20~325^2~33 X-Git-Url: https://www.infradead.org/git/?a=commitdiff_plain;h=e7eb9ef6323722e0e5219298a60db0878be4536c;p=users%2Fdwmw2%2Fopenconnect.git Add basic attempt at Fortinet support Squashed Fortinet-specific code from 64eb98f068ceac0985d4356890c69b6a0bac0bdf..61edd9e6284aad30a5eb43ec84ff72171d46ca5f Signed-off-by: David Woodhouse Signed-off-by: Daniel Lenski --- diff --git a/Makefile.am b/Makefile.am index dcbf2565..b15bfec9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 index 00000000..fd1bf037 --- /dev/null +++ b/fortinet.c @@ -0,0 +1,358 @@ +/* + * OpenConnect (SSL + DTLS) VPN client + * + * Copyright © 2020-2021 David Woodhouse, Daniel Lenski + * + * Author: David Woodhouse , Daniel Lenski + * + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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; +} diff --git a/library.c b/library.c index f0b4a3cb..f9f203a0 100644 --- 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", diff --git a/openconnect-internal.h b/openconnect-internal.h index bd6391fd..544a4f4e 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -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 e80b4d22..31150927 100644 --- 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;