From a6937b6631554d4f53b644f4330bab36d17553bf Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Wed, 6 May 2020 12:26:45 +0100 Subject: [PATCH] First attempt at F5 support F5-specific code squashed from b072adc530ce74b1141e0a529d02fecb916c631c..08aa6af9e9bd93a71200631d38c84a6ac994a73e Signed-off-by: David Woodhouse Signed-off-by: Daniel Lenski --- Makefile.am | 4 +- f5.c | 461 +++++++++++++++++++++++++++++++++++++++++ library.c | 22 +- openconnect-internal.h | 9 +- ppp.c | 47 ++++- 5 files changed, 532 insertions(+), 11 deletions(-) create mode 100644 f5.c diff --git a/Makefile.am b/Makefile.am index c293b20a..dcbf2565 100644 --- a/Makefile.am +++ b/Makefile.am @@ -39,10 +39,12 @@ lib_srcs_oath = oath.c lib_srcs_oidc = oidc.c lib_srcs_ppp = ppp.c ppp.h lib_srcs_nullppp = nullppp.c +lib_srcs_f5 = f5.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_oidc) $(lib_srcs_ppp) $(lib_srcs_nullppp) \ + $(lib_srcs_f5) lib_srcs_gnutls = gnutls.c gnutls_tpm.c gnutls_tpm2.c diff --git a/f5.c b/f5.c new file mode 100644 index 00000000..976a66f1 --- /dev/null +++ b/f5.c @@ -0,0 +1,461 @@ +/* + * 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 f5_obtain_cookie(struct openconnect_info *vpninfo) +{ + return -EINVAL; +} + +/* + * Parse the 'favorites' profile information from + * /vdesk/vpn/index.php3?outform=xml&client_version=2.0 + * which looks something like this: + * + * + * + * + * demo_vpn_resource + * /Common/demo_vpn_resource + * resourcename=/Common/demo_vpn_resource + * + * + * + * Extract the content of the "params" node which is needed for the + * next request. + */ +static int parse_profile(struct openconnect_info *vpninfo, char *buf, int len, + char **params) +{ + xmlDocPtr xml_doc; + xmlNode *xml_node, *xml_node2, *xml_node3; + char *type = NULL; + int ret; + + 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 F5 profile response\n")); + vpn_progress(vpninfo, PRG_DEBUG, + _("Response was:%s\n"), buf); + return -EINVAL; + } + xml_node = xmlDocGetRootElement(xml_doc); + for (; xml_node; xml_node = xml_node->next) { + if (xml_node->type != XML_ELEMENT_NODE) + continue; + + if (!xmlnode_is_named(xml_node, "favorites")) + continue; + + type = (char *)xmlGetProp(xml_node, XCAST("type")); + if (!type) + continue; + + if (strcmp(type, "VPN")) { + free(type); + continue; + } + free(type); + + for (xml_node2 = xmlFirstElementChild(xml_node); + xml_node2; + xml_node2 = xmlNextElementSibling(xml_node2)) { + if (!xmlnode_is_named(xml_node2, "favorite")) + continue; + + for (xml_node3 = xmlFirstElementChild(xml_node2); + xml_node3; + xml_node3 = xmlNextElementSibling(xml_node3)) { + if (!xmlnode_is_named(xml_node3, "params")) + continue; + *params = (char *)xmlNodeGetContent(xml_node3); + ret = 0; + goto out; + } + } + } + + vpn_progress(vpninfo, PRG_ERR, + _("Failed to find VPN profile parameters\n")); + vpn_progress(vpninfo, PRG_DEBUG, + _("Response was:%s\n"), buf); + ret = -EINVAL; + out: + xmlFreeDoc(xml_doc); + return ret; +} + +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_options(struct openconnect_info *vpninfo, char *buf, int len, + char **session_id, char **ur_z, int *ipv4, int *ipv6, int *hdlc) +{ + 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 F5 options response\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, "ur_Z")) + *ur_z = (char *)xmlNodeGetContent(xml_node); + else if (xmlnode_is_named(xml_node, "Session_ID")) + *session_id = (char *)xmlNodeGetContent(xml_node); + else 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, "hdlc_framing")) + *hdlc = 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])) ) { + free(s); + 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])) { + free(s); + 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])) { + free(s); + 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("::/0"); + 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) || !*ur_z || !*session_id) { + 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; +} + +static int get_ip_address(struct openconnect_info *vpninfo, char *header, char *val) { + char *s; + if (!strcasecmp(header, "X-VPN-client-IP")) { + vpn_progress(vpninfo, PRG_INFO, + _("Got legacy IP address %s\n"), val); + vpninfo->ip_info.addr = s = strdup(val); + if (!s) return -ENOMEM; + } else if (!strcasecmp(header, "X-VPN-client-IPv6")) { + vpn_progress(vpninfo, PRG_INFO, + _("Got IPv6 address %s\n"), val); + /* XX: Should we treat this as a /64 netmask? Or an /128 address? */ + vpninfo->ip_info.addr6 = s = strdup(val); + if (!s) return -ENOMEM; + } + /* XX: The server's IP address(es) X-VPN-server-{IP,IPv6} are also + * sent, but the utility of these is unclear. As remarked in oncp.c, + * "this is a tunnel; having a gateway is meaningless." */ + return 0; +} + +int f5_connect(struct openconnect_info *vpninfo) +{ + int ret; + struct oc_text_buf *reqbuf = NULL; + char *profile_params = NULL; + char *sid = NULL, *ur_z = NULL; + int ipv4 = -1, ipv6 = -1, hdlc = -1; + char *res_buf = NULL; + + /* 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, "MRHSession", vpninfo->cookie, 1); + + free(vpninfo->urlpath); + vpninfo->urlpath = strdup("vdesk/vpn/index.php3?outform=xml&client_version=2.0"); + ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0); + if (ret < 0) + goto out; + + ret = parse_profile(vpninfo, res_buf, ret, &profile_params); + if (ret) + goto out; + + vpn_progress(vpninfo, PRG_DEBUG, + _("Got profile parameters '%s'\n"), profile_params); + + free(res_buf); + res_buf = NULL; + + free(vpninfo->urlpath); + if (asprintf(&vpninfo->urlpath, "vdesk/vpn/connect.php3?%s&outform=xml&client_version=2.0", + profile_params) == -1) { + ret = -ENOMEM; + goto out; + } + ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0); + if (ret < 0) + goto out; + + ret = parse_options(vpninfo, res_buf, ret, &sid, &ur_z, &ipv4, &ipv6, &hdlc); + if (ret) + goto out; + + vpn_progress(vpninfo, PRG_DEBUG, + _("Got ipv4 %d ipv6 %d hdlc %d ur_Z '%s'\n"), ipv4, ipv6, hdlc, ur_z); + + if (ipv4 == -1) + ipv4 = 0; + if (ipv6 == -1) + ipv6 = 0; + + /* Now establish the actual connection */ + ret = openconnect_open_https(vpninfo); + if (ret) + goto out; + + reqbuf = buf_alloc(); + buf_append(reqbuf, "GET /myvpn?sess=%s&hdlc_framing=%s&ipv4=%s&ipv6=%s&Z=%s&hostname=", + sid, hdlc?"yes":"no", ipv4?"yes":"no", ipv6?"yes":"no", ur_z); + buf_append_base64(reqbuf, vpninfo->localname, strlen(vpninfo->localname)); + buf_append(reqbuf, " 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 establishing F5 connection\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, get_ip_address, 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, hdlc ? PPP_ENCAP_F5_HDLC : PPP_ENCAP_F5, ipv4, ipv6); + + out: + free(res_buf); + free(profile_params); + free(sid); + free(ur_z); + 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); + + return ret; +} + +int f5_bye(struct openconnect_info *vpninfo, const char *reason) +{ + char *orig_path; + char *res_buf = NULL; + int ret; + + /* We need to close and reopen the HTTPS connection (to kill + * the f5 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; +} diff --git a/library.c b/library.c index 63193e57..f0b4a3cb 100644 --- a/library.c +++ b/library.c @@ -187,6 +187,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 = "f5", + .pretty_name = N_("F5 BIG-IP SSL VPN"), + .description = N_("Compatible with F5 BIG-IP SSL VPN"), + .flags = OC_PROTO_PROXY, + .vpn_close_session = f5_bye, + .tcp_connect = f5_connect, + .tcp_mainloop = ppp_mainloop, + .add_http_headers = http_common_headers, + .obtain_cookie = f5_obtain_cookie, + .secure_cookie = "MRHSession", + .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", @@ -197,7 +217,7 @@ static const struct vpn_proto openconnect_protos[] = { .tcp_mainloop = nullppp_mainloop, .add_http_headers = http_common_headers, .obtain_cookie = nullppp_obtain_cookie, - } + }, }; #define NR_PROTOS (sizeof(openconnect_protos)/sizeof(*openconnect_protos)) diff --git a/openconnect-internal.h b/openconnect-internal.h index 923109c1..bd6391fd 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -188,7 +188,9 @@ struct pkt { /* All supported PPP packet framings/encapsulations */ #define PPP_ENCAP_RFC1661 1 /* Plain/synchronous/pre-framed PPP (RFC1661) */ #define PPP_ENCAP_RFC1662_HDLC 2 /* PPP with HDLC-like framing (RFC1662) */ -#define PPP_ENCAP_MAX PPP_ENCAP_RFC1662_HDLC +#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 COMPR_DEFLATE (1<<0) #define COMPR_LZS (1<<1) @@ -982,6 +984,11 @@ int nullppp_obtain_cookie(struct openconnect_info *vpninfo); int nullppp_connect(struct openconnect_info *vpninfo); int nullppp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable); +/* f5.c */ +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); + /* 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 86d9682d..a7c39ae2 100644 --- a/ppp.c +++ b/ppp.c @@ -170,6 +170,8 @@ static const char *encap_names[PPP_ENCAP_MAX+1] = { NULL, "RFC1661", "RFC1662 HDLC", + "F5", + "F5 HDLC", }; static const char *lcp_names[] = { @@ -239,6 +241,11 @@ int openconnect_ppp_new(struct openconnect_info *vpninfo, ppp->encap = encap; switch (encap) { + case PPP_ENCAP_F5: + ppp->encap_len = 4; + break; + + case PPP_ENCAP_F5_HDLC: case PPP_ENCAP_RFC1662_HDLC: ppp->encap_len = 0; ppp->hdlc = 1; @@ -1015,6 +1022,31 @@ int ppp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable) /* Deencapsulate from pre-PPP header */ switch (ppp->encap) { + case PPP_ENCAP_F5: + magic = load_be16(eh); + payload_len = load_be16(eh + 2); + next = eh + 4 + payload_len; + + if (magic != 0xf500) { + bad_encap_header: + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected pre-PPP packet header for encap %d.\n"), + ppp->encap); + dump_buf_hex(vpninfo, PRG_ERR, '<', eh, len); + continue; + } + + if (len < 4 + payload_len) { + incomplete_pkt: + vpn_progress(vpninfo, PRG_ERR, + _("Packet is incomplete. Received %d bytes on wire (includes %d encap) but header payload_len is %d\n"), + len, ppp->encap_len, payload_len); + dump_buf_hex(vpninfo, PRG_ERR, '<', eh, len); + continue; + } + break; + + case PPP_ENCAP_F5_HDLC: case PPP_ENCAP_RFC1662_HDLC: payload_len = unhdlc_in_place(vpninfo, eh + ppp->encap_len, len - ppp->encap_len, &next); if (payload_len < 0) @@ -1269,14 +1301,13 @@ int ppp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable) } /* Encapsulate into pre-PPP header */ - /* Nothing here until we add protocols that require pre-PPP - * header encapsulation. Such a protocol would store the - * pre-PPP header into the range of memory: - * eh to (eh + ppp->encap_len) - * - * Commented out so that sanitizer doesn't complain: - * eh = this->data - this->ppp.hlen - ppp->encap_len; - */ + eh = this->data - this->ppp.hlen - ppp->encap_len; + switch (ppp->encap) { + case PPP_ENCAP_F5: + store_be16(eh, 0xf500); + store_be16(eh + 2, this->len + this->ppp.hlen); + break; + } this->ppp.hlen += ppp->encap_len; if (lcp) -- 2.49.0