[PATCH 2/5 v2] add PAN GlobalProtect protocol support (HTTPS tunnel only)
Daniel Lenski
dlenski at gmail.com
Wed Apr 11 23:58:01 PDT 2018
This includes support for generating an xmlconfig so that NetworkManager can
list all the gateway servers, when connecting to a GlobalProtect portal (not
gateway):
GlobalProtect distinguishes "portal" and "gateway" servers. Often the same
server supports both (/global-protect URLs are for the portal, /ssl-vpn URLs
are for the gateway). The official clients always connect through the
portal. Mostly, the portal configuration is not useful for OpenConnect; it
restricts the behavior of the official clients.
However, the portal configuration does contain a list of allowed gateways
(just as AnyConnect VPNs can list other servers). We therefore generate an
xmlconfig in the same format as AnyConnect VPNs, so that the NetworkManager
plugins can list all the supported gateways.
Signed-off-by: Daniel Lenski <dlenski at gmail.com>
Signed-off-by: David Woodhouse <dwmw2 at infradead.org>
---
Makefile.am | 5 +-
auth-globalprotect.c | 468 ++++++++++++++++++++++++++++
gpst.c | 802 ++++++++++++++++++++++++++++++++++++++++++++++++
http.c | 22 +-
library.c | 10 +
openconnect-internal.h | 17 +
openconnect.8.in | 7 +-
www/Makefile.am | 2 +-
www/globalprotect.xml | 64 ++++
www/mail.xml | 4 +-
www/menu2-protocols.xml | 1 +
11 files changed, 1394 insertions(+), 8 deletions(-)
create mode 100644 auth-globalprotect.c
create mode 100644 gpst.c
create mode 100644 www/globalprotect.xml
diff --git a/Makefile.am b/Makefile.am
index d3ae26a..fc36c59 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -30,6 +30,7 @@ endif
library_srcs = ssl.c http.c http-auth.c auth-common.c library.c compat.c lzs.c mainloop.c script.c ntlm.c digest.c
lib_srcs_cisco = auth.c cstp.c
lib_srcs_juniper = oncp.c lzo.c auth-juniper.c
+lib_srcs_globalprotect = gpst.c auth-globalprotect.c
lib_srcs_gnutls = gnutls.c gnutls_tpm.c
lib_srcs_openssl = openssl.c openssl-pkcs11.c
lib_srcs_win32 = tun-win32.c sspi.c
@@ -42,14 +43,14 @@ lib_srcs_stoken = stoken.c
lib_srcs_esp = esp.c esp-seqno.c
lib_srcs_dtls = dtls.c
-POTFILES = $(openconnect_SOURCES) $(lib_srcs_cisco) $(lib_srcs_juniper) \
+POTFILES = $(openconnect_SOURCES) $(lib_srcs_cisco) $(lib_srcs_juniper) $(lib_srcs_globalprotect) \
gnutls-esp.c gnutls-dtls.c openssl-esp.c openssl-dtls.c \
$(lib_srcs_esp) $(lib_srcs_dtls) \
$(lib_srcs_openssl) $(lib_srcs_gnutls) $(library_srcs) \
$(lib_srcs_win32) $(lib_srcs_posix) $(lib_srcs_gssapi) $(lib_srcs_iconv) \
$(lib_srcs_oath) $(lib_srcs_yubikey) $(lib_srcs_stoken) openconnect-internal.h
-library_srcs += $(lib_srcs_juniper) $(lib_srcs_cisco) $(lib_srcs_oath)
+library_srcs += $(lib_srcs_juniper) $(lib_srcs_cisco) $(lib_srcs_oath) $(lib_srcs_globalprotect)
if OPENCONNECT_LIBPCSCLITE
library_srcs += $(lib_srcs_yubikey)
endif
diff --git a/auth-globalprotect.c b/auth-globalprotect.c
new file mode 100644
index 0000000..a32dedb
--- /dev/null
+++ b/auth-globalprotect.c
@@ -0,0 +1,468 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Copyright © 2016-2017 Daniel Lenski
+ *
+ * Author: Dan Lenski <dlenski at 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 <errno.h>
+
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+
+#include "openconnect-internal.h"
+
+void gpst_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf)
+{
+ char *orig_ua = vpninfo->useragent;
+ vpninfo->useragent = (char *)"PAN GlobalProtect";
+
+ http_common_headers(vpninfo, buf);
+
+ vpninfo->useragent = orig_ua;
+}
+
+/* our "auth form" always has a username and password or challenge */
+static struct oc_auth_form *auth_form(struct openconnect_info *vpninfo,
+ const char *prompt, const char *auth_id)
+{
+ struct oc_auth_form *form;
+ struct oc_form_opt *opt, *opt2;
+
+ form = calloc(1, sizeof(*form));
+ if (!form)
+ return NULL;
+
+ if (prompt)
+ form->message = strdup(prompt);
+
+ form->auth_id = strdup(auth_id ? : "_gateway");
+
+ opt = form->opts = calloc(1, sizeof(*opt));
+ if (!opt) {
+ nomem:
+ free_auth_form(form);
+ return NULL;
+ }
+ opt->name = strdup("user");
+ opt->label = strdup(_("Username: "));
+ opt->type = OC_FORM_OPT_TEXT;
+
+ opt2 = opt->next = calloc(1, sizeof(*opt));
+ if (!opt2)
+ goto nomem;
+ opt2->name = strdup("passwd");
+ opt2->label = auth_id ? strdup(_("Challenge: ")) : strdup(_("Password: "));
+
+ /* XX: Some VPNs use a password in the first form, followed by a
+ * a token in the second ("challenge") form. Others use only a
+ * token. How can we distinguish these? */
+ if (!can_gen_tokencode(vpninfo, form, opt2))
+ opt2->type = OC_FORM_OPT_TOKEN;
+ else
+ opt2->type = OC_FORM_OPT_PASSWORD;
+
+ return form;
+}
+
+/* Return value:
+ * < 0, on error
+ * = 0, on success; *form is populated
+ */
+struct gp_login_arg {
+ const char *opt;
+ unsigned save:1;
+ unsigned show:1;
+ unsigned warn_missing:1;
+ unsigned err_missing:1;
+ const char *check;
+};
+static const struct gp_login_arg gp_login_args[] = {
+ { .opt="unknown-arg0", .show=1 },
+ { .opt="authcookie", .save=1, .err_missing=1 },
+ { .opt="persistent-cookie", .warn_missing=1 }, /* 40 hex digits; persists across sessions */
+ { .opt="portal", .save=1, .warn_missing=1 },
+ { .opt="user", .save=1, .err_missing=1 },
+ { .opt="authentication-source", .show=1 }, /* LDAP-auth, AUTH-RADIUS_RSA_OTP, etc. */
+ { .opt="configuration", .warn_missing=1 }, /* usually vsys1 (sometimes vsys2, etc.) */
+ { .opt="domain", .save=1, .warn_missing=1 },
+ { .opt="unknown-arg8", .show=1 },
+ { .opt="unknown-arg9", .show=1 },
+ { .opt="unknown-arg10", .show=1 },
+ { .opt="unknown-arg11", .show=1 },
+ { .opt="connection-type", .err_missing=1, .check="tunnel" },
+ { .opt="password-expiration-days", .show=1 }, /* days until password expires, if not -1 */
+ { .opt="clientVer", .err_missing=1, .check="4100" },
+ { .opt="preferred-ip", .save=1 },
+ { .opt=NULL },
+};
+
+static int parse_login_xml(struct openconnect_info *vpninfo, xmlNode *xml_node)
+{
+ struct oc_text_buf *cookie = buf_alloc();
+ char *value = NULL;
+ const struct gp_login_arg *arg;
+
+ if (!xmlnode_is_named(xml_node, "jnlp"))
+ goto err_out;
+
+ xml_node = xml_node->children;
+ if (!xmlnode_is_named(xml_node, "application-desc"))
+ goto err_out;
+
+ xml_node = xml_node->children;
+ for (arg = gp_login_args; arg->opt; arg++) {
+ if (xml_node && !xmlnode_is_named(xml_node, "argument"))
+ goto err_out;
+ else if (xml_node) {
+ value = (char *)xmlNodeGetContent(xml_node);
+ if (value && (!value[0] || !strcmp(value, "(null)") || !strcmp(value, "-1"))) {
+ free(value);
+ value = NULL;
+ }
+ xml_node = xml_node->next;
+ }
+
+ if (arg->check && (!value || strcmp(value, arg->check))) {
+ vpn_progress(vpninfo, arg->err_missing ? PRG_ERR : PRG_DEBUG,
+ _("GlobalProtect login returned %s=%s (expected %s)\n"),
+ arg->opt, value, arg->check);
+ if (arg->err_missing)
+ goto err_out;
+ } else if ((arg->err_missing || arg->warn_missing) && !value) {
+ vpn_progress(vpninfo, arg->err_missing ? PRG_ERR : PRG_DEBUG,
+ _("GlobalProtect login returned empty or missing %s\n"),
+ arg->opt);
+ if (arg->err_missing)
+ goto err_out;
+ } else if (value && arg->show) {
+ vpn_progress(vpninfo, PRG_INFO,
+ _("GlobalProtect login returned %s=%s\n"),
+ arg->opt, value);
+ }
+
+ if (value && arg->save)
+ append_opt(cookie, arg->opt, value);
+ free(value);
+ value = NULL;
+ }
+
+ vpninfo->cookie = cookie->data;
+ cookie->data = NULL;
+ return buf_free(cookie);
+
+err_out:
+ free(value);
+ buf_free(cookie);
+ return -EINVAL;
+}
+
+static int parse_portal_xml(struct openconnect_info *vpninfo, xmlNode *xml_node)
+{
+ struct oc_auth_form *form;
+ xmlNode *x = NULL;
+ struct oc_form_opt_select *opt;
+ struct oc_text_buf *buf = NULL;
+ int max_choices = 0, result;
+ char *portal = NULL;
+
+ form = calloc(1, sizeof(*form));
+ if (!form)
+ return -ENOMEM;
+
+ form->message = strdup(_("Please select GlobalProtect gateway."));
+ form->auth_id = strdup("_portal");
+
+ opt = form->authgroup_opt = calloc(1, sizeof(*opt));
+ if (!opt) {
+ result = -ENOMEM;
+ goto out;
+ }
+ opt->form.type = OC_FORM_OPT_SELECT;
+ opt->form.name = strdup("gateway");
+ opt->form.label = strdup(_("GATEWAY:"));
+ form->opts = (void *)opt;
+
+ /* The portal contains a ton of stuff, but basically none of it is useful to a VPN client
+ * that wishes to give control to the client user, as opposed to the VPN administrator.
+ * The exception is the list of gateways in policy/gateways/external/list
+ */
+ if (xmlnode_is_named(xml_node, "policy")) {
+ for (x = xml_node->children, xml_node = NULL; x; x = x->next) {
+ if (xmlnode_is_named(x, "portal-name"))
+ portal = (char *)xmlNodeGetContent(x);
+ else if (xmlnode_is_named(x, "gateways"))
+ xml_node = x;
+ }
+ }
+
+ if (xml_node) {
+ for (xml_node = xml_node->children; xml_node; xml_node = xml_node->next)
+ if (xmlnode_is_named(xml_node, "external"))
+ for (xml_node = xml_node->children; xml_node; xml_node = xml_node->next)
+ if (xmlnode_is_named(xml_node, "list"))
+ goto gateways;
+ }
+ result = -EINVAL;
+ goto out;
+
+gateways:
+ if (vpninfo->write_new_config) {
+ buf = buf_alloc();
+ buf_append(buf, "<GPPortal>\n <ServerList>\n");
+ if (portal) {
+ buf_append(buf, " <HostEntry><HostName>");
+ buf_append_xmlescaped(buf, portal);
+ buf_append(buf, "</HostName><HostAddress>%s", vpninfo->hostname);
+ if (vpninfo->port!=443)
+ buf_append(buf, ":%d", vpninfo->port);
+ buf_append(buf, "/global-protect</HostAddress></HostEntry>\n");
+ }
+ }
+
+ /* first, count the number of gateways */
+ for (x = xml_node->children; x; x = x->next)
+ if (xmlnode_is_named(x, "entry"))
+ max_choices++;
+
+ opt->choices = calloc(max_choices, sizeof(opt->choices[0]));
+ if (!opt->choices) {
+ result = -ENOMEM;
+ goto out;
+ }
+
+ /* each entry looks like <entry name="host[:443]"><description>Label</description></entry> */
+ vpn_progress(vpninfo, PRG_INFO, _("%d gateway servers available:\n"), max_choices);
+ for (xml_node = xml_node->children; xml_node; xml_node = xml_node->next) {
+ if (xmlnode_is_named(xml_node, "entry")) {
+ struct oc_choice *choice = calloc(1, sizeof(*choice));
+ if (!choice) {
+ result = -ENOMEM;
+ goto out;
+ }
+
+ xmlnode_get_prop(xml_node, "name", &choice->name);
+ for (x = xml_node->children; x; x=x->next)
+ if (xmlnode_is_named(x, "description")) {
+ choice->label = (char *)xmlNodeGetContent(x);
+ if (vpninfo->write_new_config) {
+ buf_append(buf, " <HostEntry><HostName>");
+ buf_append_xmlescaped(buf, choice->label);
+ buf_append(buf, "</HostName><HostAddress>%s/ssl-vpn</HostAddress></HostEntry>\n",
+ choice->name);
+ }
+ }
+
+ opt->choices[opt->nr_choices++] = choice;
+ vpn_progress(vpninfo, PRG_INFO, _(" %s (%s)\n"),
+ choice->label, choice->name);
+ }
+ }
+
+ if (vpninfo->write_new_config) {
+ buf_append(buf, " </ServerList>\n</GPPortal>\n");
+ if ((result = buf_error(buf)))
+ goto out;
+ if ((result = vpninfo->write_new_config(vpninfo, buf->data, buf->pos)))
+ goto out;
+ }
+
+ /* process auth form to select gateway */
+ result = process_auth_form(vpninfo, form);
+ if (result != OC_FORM_RESULT_NEWGROUP)
+ goto out;
+
+ /* redirect to the gateway (no-op if it's the same host) */
+ free(vpninfo->redirect_url);
+ if (asprintf(&vpninfo->redirect_url, "https://%s", vpninfo->authgroup) == 0) {
+ result = -ENOMEM;
+ goto out;
+ }
+ result = handle_redirect(vpninfo);
+
+out:
+ buf_free(buf);
+ free(portal);
+ free_auth_form(form);
+ return result;
+}
+
+static int gpst_login(struct openconnect_info *vpninfo, int portal)
+{
+ int result;
+
+ struct oc_auth_form *form = NULL;
+ struct oc_text_buf *request_body = buf_alloc();
+ const char *request_body_type = "application/x-www-form-urlencoded";
+ char *xml_buf = NULL, *orig_path;
+ char *prompt = NULL, *auth_id = NULL;
+
+#ifdef HAVE_LIBSTOKEN
+ /* Step 1: Unlock software token (if applicable) */
+ if (vpninfo->token_mode == OC_TOKEN_MODE_STOKEN) {
+ result = prepare_stoken(vpninfo);
+ if (result)
+ goto out;
+ }
+#endif
+
+ form = auth_form(vpninfo, _("Please enter your username and password"), NULL);
+ if (!form)
+ return -ENOMEM;
+
+ /* Ask the user to fill in the auth form; repeat as necessary */
+ for (;;) {
+ /* process auth form (username and password or challenge) */
+ result = process_auth_form(vpninfo, form);
+ if (result)
+ goto out;
+
+ redo_gateway:
+ buf_truncate(request_body);
+
+ /* generate token code if specified */
+ result = do_gen_tokencode(vpninfo, form);
+ if (result) {
+ vpn_progress(vpninfo, PRG_ERR, _("Failed to generate OTP tokencode; disabling token\n"));
+ vpninfo->token_bypassed = 1;
+ goto out;
+ }
+
+ /* submit gateway login (ssl-vpn/login.esp) or portal config (global-protect/getconfig.esp) request */
+ buf_truncate(request_body);
+ buf_append(request_body, "jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:");
+ if (!strcmp(vpninfo->platname, "win"))
+ append_opt(request_body, "clientos", "Windows");
+ else
+ append_opt(request_body, "clientos", vpninfo->platname);
+ append_opt(request_body, "server", vpninfo->hostname);
+ append_opt(request_body, "computer", vpninfo->localname);
+ /* Note: auth_id is non-NULL but freed, and an actual copy of it is in form->auth_id.
+ This checks if form->auth_id was explcitly set from auth_id and uses it if so. */
+ if (auth_id)
+ append_opt(request_body, "inputStr", form->auth_id);
+ append_form_opts(vpninfo, form, request_body);
+ if ((result = buf_error(request_body)))
+ goto out;
+
+ orig_path = vpninfo->urlpath;
+ vpninfo->urlpath = strdup(portal ? "global-protect/getconfig.esp" : "ssl-vpn/login.esp");
+ result = do_https_request(vpninfo, "POST", request_body_type, request_body,
+ &xml_buf, 0);
+ free(vpninfo->urlpath);
+ vpninfo->urlpath = orig_path;
+
+ /* Result could be either a JavaScript challenge or XML */
+ result = gpst_xml_or_error(vpninfo, result, xml_buf,
+ portal ? parse_portal_xml : parse_login_xml, &prompt, &auth_id);
+ if (result == -EAGAIN) {
+ free_auth_form(form);
+ form = auth_form(vpninfo, prompt, auth_id);
+ free(prompt);
+ free(auth_id);
+ if (!form)
+ return -ENOMEM;
+ continue;
+ } else if (portal && result == 0) {
+ portal = 0;
+ goto redo_gateway;
+ } else if (result == -EACCES) /* Invalid username/password */
+ continue;
+ else
+ break;
+ }
+
+out:
+ free_auth_form(form);
+ buf_free(request_body);
+ free(xml_buf);
+ return result;
+}
+
+int gpst_obtain_cookie(struct openconnect_info *vpninfo)
+{
+ int result;
+
+ if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "portal") || !strncmp(vpninfo->urlpath, "global-protect", 14))) {
+ /* assume the server is a portal */
+ return gpst_login(vpninfo, 1);
+ } else if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "gateway") || !strncmp(vpninfo->urlpath, "ssl-vpn", 7))) {
+ /* assume the server is a gateway */
+ return gpst_login(vpninfo, 0);
+ } else {
+ /* first try handling it as a gateway, then a portal */
+ result = gpst_login(vpninfo, 0);
+ if (result == -EEXIST) {
+ /* XX: Don't we want to start by trying the same username/password the user just
+ entered for the 'gateway' attempt? */
+ result = gpst_login(vpninfo, 1);
+ if (result == -EEXIST)
+ vpn_progress(vpninfo, PRG_ERR, _("Server is neither a GlobalProtect portal nor a gateway.\n"));
+ }
+ return result;
+ }
+}
+
+int gpst_bye(struct openconnect_info *vpninfo, const char *reason)
+{
+ char *orig_path;
+ int result;
+ struct oc_text_buf *request_body = buf_alloc();
+ const char *request_body_type = "application/x-www-form-urlencoded";
+ const char *method = "POST";
+ char *xml_buf = NULL;
+
+ /* In order to logout successfully, the client must send not only
+ * the session's authcookie, but also the portal, user, computer,
+ * and domain matching the values sent with the getconfig request.
+ *
+ * You read that right: the client must send a bunch of irrelevant
+ * non-secret values in its logout request. If they're wrong or
+ * missing, the logout will fail and the authcookie will remain
+ * valid -- which is a security hole.
+ *
+ * Don't blame me. I didn't design this.
+ */
+ append_opt(request_body, "computer", vpninfo->localname);
+ buf_append(request_body, "&%s", vpninfo->cookie);
+ if ((result = buf_error(request_body)))
+ goto out;
+
+ /* We need to close and reopen the HTTPS connection (to kill
+ * the tunnel session) and submit a new HTTPS request to
+ * logout.
+ */
+ orig_path = vpninfo->urlpath;
+ vpninfo->urlpath = strdup("ssl-vpn/logout.esp");
+ openconnect_close_https(vpninfo, 0);
+ result = do_https_request(vpninfo, method, request_body_type, request_body,
+ &xml_buf, 0);
+ free(vpninfo->urlpath);
+ vpninfo->urlpath = orig_path;
+
+ /* logout.esp returns HTTP status 200 and <response status="success"> when
+ * successful, and all manner of malformed junk when unsuccessful.
+ */
+ result = gpst_xml_or_error(vpninfo, result, xml_buf, NULL, NULL, NULL);
+ if (result < 0)
+ vpn_progress(vpninfo, PRG_ERR, _("Logout failed.\n"));
+ else
+ vpn_progress(vpninfo, PRG_INFO, _("Logout successful\n"));
+
+out:
+ buf_free(request_body);
+ free(xml_buf);
+ return result;
+}
diff --git a/gpst.c b/gpst.c
new file mode 100644
index 0000000..d3fb831
--- /dev/null
+++ b/gpst.c
@@ -0,0 +1,802 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Copyright © 2016-2017 Daniel Lenski
+ *
+ * Author: Daniel Lenski <dlenski at 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>
+#ifdef HAVE_LZ4
+#include <lz4.h>
+#endif
+
+#if defined(__linux__)
+/* For TCP_INFO */
+# include <linux/tcp.h>
+#endif
+
+#include <assert.h>
+
+#include "openconnect-internal.h"
+
+/*
+ * Data packets are encapsulated in the SSL stream as follows:
+ *
+ * 0000: Magic "\x1a\x2b\x3c\x4d"
+ * 0004: Big-endian EtherType (0x0800 for IPv4)
+ * 0006: Big-endian 16-bit length (not including 16-byte header)
+ * 0008: Always "\x01\0\0\0\0\0\0\0"
+ * 0010: data payload
+ */
+
+/* Strange initialisers here to work around GCC PR#10676 (which was
+ * fixed in GCC 4.6 but it takes a while for some systems to catch
+ * up. */
+static const struct pkt dpd_pkt = {
+ .next = NULL,
+ { .gpst.hdr = { 0x1a, 0x2b, 0x3c, 0x4d } }
+};
+
+/* similar to auth.c's xmlnode_get_text, including that *var should be freed by the caller,
+ but without the hackish param / %s handling that Cisco needs. And without freeing up
+ the old contents of *var, which is likely to lead to bugs? */
+static int xmlnode_get_text(xmlNode *xml_node, const char *name, char **var)
+{
+ char *str;
+
+ if (name && !xmlnode_is_named(xml_node, name))
+ return -EINVAL;
+
+ str = (char *)xmlNodeGetContent(xml_node);
+ if (!str)
+ return -ENOENT;
+
+ *var = str;
+ return 0;
+}
+
+/* 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, const 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;
+ new->next = vpninfo->cstp_options;
+ vpninfo->cstp_options = new;
+
+ return new->value;
+}
+
+static int filter_opts(struct oc_text_buf *buf, const char *query, const char *incexc, int include)
+{
+ const char *f, *endf, *eq;
+ const char *found, *comma;
+
+ for (f = query; *f; f=(*endf) ? endf+1 : endf) {
+ endf = strchr(f, '&') ? : f+strlen(f);
+ eq = strchr(f, '=');
+ if (!eq || eq > endf)
+ eq = endf;
+
+ for (found = incexc; *found; found=(*comma) ? comma+1 : comma) {
+ comma = strchr(found, ',') ? : found+strlen(found);
+ if (!strncmp(found, f, MAX(comma-found, eq-f)))
+ break;
+ }
+
+ if ((include && *found) || (!include && !*found)) {
+ if (buf->pos && buf->data[buf->pos-1] != '?' && buf->data[buf->pos-1] != '&')
+ buf_append(buf, "&");
+ buf_append_bytes(buf, f, (int)(endf-f));
+ }
+ }
+ return buf_error(buf);
+}
+
+/* Parse this JavaScript-y mess:
+
+ "var respStatus = \"Challenge|Error\";\n"
+ "var respMsg = \"<prompt>\";\n"
+ "thisForm.inputStr.value = "<inputStr>";\n"
+*/
+static int parse_javascript(char *buf, char **prompt, char **inputStr)
+{
+ const char *start, *end = buf;
+ int status;
+
+ const char *pre_status = "var respStatus = \"",
+ *pre_prompt = "var respMsg = \"",
+ *pre_inputStr = "thisForm.inputStr.value = \"";
+
+ /* Status */
+ while (isspace(*end))
+ end++;
+ if (strncmp(end, pre_status, strlen(pre_status)))
+ goto err;
+
+ start = end+strlen(pre_status);
+ end = strchr(start, '\n');
+ if (!end || end[-1] != ';' || end[-2] != '"')
+ goto err;
+
+ if (!strncmp(start, "Challenge", 8)) status = 0;
+ else if (!strncmp(start, "Error", 5)) status = 1;
+ else goto err;
+
+ /* Prompt */
+ while (isspace(*end))
+ end++;
+ if (strncmp(end, pre_prompt, strlen(pre_prompt)))
+ goto err;
+
+ start = end+strlen(pre_prompt);
+ end = strchr(start, '\n');
+ if (!end || end[-1] != ';' || end[-2] != '"')
+ goto err;
+
+ if (prompt)
+ *prompt = strndup(start, end-start-2);
+
+ /* inputStr */
+ while (isspace(*end))
+ end++;
+ if (strncmp(end, pre_inputStr, strlen(pre_inputStr)))
+ goto err2;
+
+ start = end+strlen(pre_inputStr);
+ end = strchr(start, '\n');
+ if (!end || end[-1] != ';' || end[-2] != '"')
+ goto err2;
+
+ if (inputStr)
+ *inputStr = strndup(start, end-start-2);
+
+ while (isspace(*end))
+ end++;
+ if (*end != '\0')
+ goto err3;
+
+ return status;
+
+err3:
+ if (inputStr) free(*inputStr);
+err2:
+ if (prompt) free(*prompt);
+err:
+ return -EINVAL;
+}
+
+int gpst_xml_or_error(struct openconnect_info *vpninfo, int result, char *response,
+ int (*xml_cb)(struct openconnect_info *, xmlNode *xml_node),
+ char **prompt, char **inputStr)
+{
+ xmlDocPtr xml_doc;
+ xmlNode *xml_node;
+ char *err = NULL;
+
+ /* custom error codes returned by /ssl-vpn/login.esp and maybe others */
+ if (result == -EACCES)
+ vpn_progress(vpninfo, PRG_ERR, _("Invalid username or password.\n"));
+ else if (result == -EBADMSG)
+ vpn_progress(vpninfo, PRG_ERR, _("Invalid client certificate.\n"));
+
+ if (result < 0)
+ return result;
+
+ if (!response) {
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("Empty response from server\n"));
+ return -EINVAL;
+ }
+
+ /* is it XML? */
+ xml_doc = xmlReadMemory(response, strlen(response), "noname.xml", NULL,
+ XML_PARSE_NOERROR);
+ if (!xml_doc) {
+ /* is it Javascript? */
+ char *p, *i;
+ result = parse_javascript(response, &p, &i);
+ switch (result) {
+ case 1:
+ vpn_progress(vpninfo, PRG_ERR, _("%s\n"), p);
+ break;
+ case 0:
+ vpn_progress(vpninfo, PRG_INFO, _("Challenge: %s\n"), p);
+ if (prompt && inputStr) {
+ *prompt=p;
+ *inputStr=i;
+ return -EAGAIN;
+ }
+ break;
+ default:
+ goto bad_xml;
+ }
+ free((char *)p);
+ free((char *)i);
+ goto out;
+ }
+
+ xml_node = xmlDocGetRootElement(xml_doc);
+
+ /* is it <response status="error"><error>..</error></response> ? */
+ if (xmlnode_is_named(xml_node, "response")
+ && !xmlnode_match_prop(xml_node, "status", "error")) {
+ for (xml_node=xml_node->children; xml_node; xml_node=xml_node->next) {
+ if (!xmlnode_get_text(xml_node, "error", &err))
+ goto out;
+ }
+ goto bad_xml;
+ }
+
+ if (xml_cb)
+ result = xml_cb(vpninfo, xml_node);
+
+ if (result == -EINVAL) {
+ bad_xml:
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Failed to parse server response\n"));
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("Response was:%s\n"), response);
+ }
+
+out:
+ if (err) {
+ if (!strcmp(err, "GlobalProtect gateway does not exist")
+ || !strcmp(err, "GlobalProtect portal does not exist")) {
+ vpn_progress(vpninfo, PRG_DEBUG, "%s\n", err);
+ result = -EEXIST;
+ } else if (!strcmp(err, "Invalid authentication cookie")) {
+ vpn_progress(vpninfo, PRG_ERR, "%s\n", err);
+ result = -EPERM;
+ } else {
+ vpn_progress(vpninfo, PRG_ERR, "%s\n", err);
+ result = -EINVAL;
+ }
+ free(err);
+ }
+ if (xml_doc)
+ xmlFreeDoc(xml_doc);
+ return result;
+}
+
+#define ESP_OVERHEAD (4 /* SPI */ + 4 /* sequence number */ + \
+ 20 /* biggest supported MAC (SHA1) */ + 32 /* biggest supported IV (AES-256) */ + \
+ 1 /* pad length */ + 1 /* next header */ + \
+ 16 /* max padding */ )
+#define UDP_HEADER_SIZE 8
+#define IPV4_HEADER_SIZE 20
+#define IPV6_HEADER_SIZE 40
+
+static int calculate_mtu(struct openconnect_info *vpninfo)
+{
+ int mtu = vpninfo->reqmtu, base_mtu = vpninfo->basemtu;
+
+#if defined(__linux__) && defined(TCP_INFO)
+ if (!mtu || !base_mtu) {
+ struct tcp_info ti;
+ socklen_t ti_size = sizeof(ti);
+
+ if (!getsockopt(vpninfo->ssl_fd, IPPROTO_TCP, TCP_INFO,
+ &ti, &ti_size)) {
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("TCP_INFO rcv mss %d, snd mss %d, adv mss %d, pmtu %d\n"),
+ ti.tcpi_rcv_mss, ti.tcpi_snd_mss, ti.tcpi_advmss, ti.tcpi_pmtu);
+
+ if (!base_mtu) {
+ base_mtu = ti.tcpi_pmtu;
+ }
+
+ if (!base_mtu) {
+ if (ti.tcpi_rcv_mss < ti.tcpi_snd_mss)
+ base_mtu = ti.tcpi_rcv_mss - 13;
+ else
+ base_mtu = ti.tcpi_snd_mss - 13;
+ }
+ }
+ }
+#endif
+#ifdef TCP_MAXSEG
+ if (!base_mtu) {
+ int mss;
+ socklen_t mss_size = sizeof(mss);
+ if (!getsockopt(vpninfo->ssl_fd, IPPROTO_TCP, TCP_MAXSEG,
+ &mss, &mss_size)) {
+ vpn_progress(vpninfo, PRG_DEBUG, _("TCP_MAXSEG %d\n"), mss);
+ base_mtu = mss - 13;
+ }
+ }
+#endif
+ if (!base_mtu) {
+ /* Default */
+ base_mtu = 1406;
+ }
+
+ if (base_mtu < 1280)
+ base_mtu = 1280;
+
+ if (!mtu) {
+ /* remove IP/UDP and ESP overhead from base MTU to calculate tunnel MTU */
+ mtu = base_mtu - ESP_OVERHEAD - UDP_HEADER_SIZE;
+ if (vpninfo->peer_addr->sa_family == AF_INET6)
+ mtu -= IPV6_HEADER_SIZE;
+ else
+ mtu -= IPV4_HEADER_SIZE;
+ }
+ return mtu;
+}
+
+/* Return value:
+ * < 0, on error
+ * = 0, on success; *form is populated
+ */
+static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_node)
+{
+ xmlNode *member;
+ char *s;
+ int ii;
+
+ if (!xml_node || !xmlnode_is_named(xml_node, "response"))
+ return -EINVAL;
+
+ /* 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->ip_info.mtu = 0;
+ vpninfo->ssl_times.rekey_method = REKEY_NONE;
+ 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);
+
+ /* Parse config */
+ for (xml_node = xml_node->children; xml_node; xml_node=xml_node->next) {
+ if (!xmlnode_get_text(xml_node, "ip-address", &s))
+ vpninfo->ip_info.addr = add_option(vpninfo, "ipaddr", s);
+ else if (!xmlnode_get_text(xml_node, "netmask", &s))
+ vpninfo->ip_info.netmask = add_option(vpninfo, "netmask", s);
+ else if (!xmlnode_get_text(xml_node, "mtu", &s)) {
+ vpninfo->ip_info.mtu = atoi(s);
+ free(s);
+ } else if (!xmlnode_get_text(xml_node, "ssl-tunnel-url", &s)) {
+ free(vpninfo->urlpath);
+ vpninfo->urlpath = s;
+ if (strcmp(s, "/ssl-tunnel-connect.sslvpn"))
+ vpn_progress(vpninfo, PRG_INFO, _("Non-standard SSL tunnel path: %s\n"), s);
+ } else if (!xmlnode_get_text(xml_node, "timeout", &s)) {
+ int sec = atoi(s);
+ vpn_progress(vpninfo, PRG_INFO, _("Tunnel timeout (rekey interval) is %d minutes.\n"), sec/60);
+ vpninfo->ssl_times.last_rekey = time(NULL);
+ vpninfo->ssl_times.rekey = sec - 60;
+ vpninfo->ssl_times.rekey_method = REKEY_TUNNEL;
+ free(s);
+ } else if (!xmlnode_get_text(xml_node, "gw-address", &s)) {
+ /* As remarked in oncp.c, "this is a tunnel; having a
+ * gateway is meaningless."
+ */
+ if (strcmp(s, vpninfo->ip_info.gateway_addr))
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("Gateway address in config XML (%s) differs from external gateway address (%s).\n"), s, vpninfo->ip_info.gateway_addr);
+ free(s);
+ } else if (xmlnode_is_named(xml_node, "dns")) {
+ for (ii=0, member = xml_node->children; member && ii<3; member=member->next)
+ if (!xmlnode_get_text(member, "member", &s))
+ vpninfo->ip_info.dns[ii++] = add_option(vpninfo, "DNS", s);
+ } else if (xmlnode_is_named(xml_node, "wins")) {
+ for (ii=0, member = xml_node->children; member && ii<3; member=member->next)
+ if (!xmlnode_get_text(member, "member", &s))
+ vpninfo->ip_info.nbns[ii++] = add_option(vpninfo, "WINS", s);
+ } else if (xmlnode_is_named(xml_node, "dns-suffix")) {
+ for (ii=0, member = xml_node->children; member && ii<1; member=member->next)
+ if (!xmlnode_get_text(member, "member", &s)) {
+ vpninfo->ip_info.domain = add_option(vpninfo, "search", s);
+ ii++;
+ }
+ } else if (xmlnode_is_named(xml_node, "access-routes")) {
+ for (member = xml_node->children; member; member=member->next) {
+ if (!xmlnode_get_text(member, "member", &s)) {
+ struct oc_split_include *inc = malloc(sizeof(*inc));
+ if (!inc)
+ continue;
+ inc->route = add_option(vpninfo, "split-include", s);
+ inc->next = vpninfo->ip_info.split_includes;
+ vpninfo->ip_info.split_includes = inc;
+ }
+ }
+ } else if (xmlnode_is_named(xml_node, "ipsec")) {
+ vpn_progress(vpninfo, PRG_DEBUG, _("Ignoring ESP keys since ESP support not available in this build\n"));
+ }
+ }
+
+ /* No IPv6 support for SSL VPN:
+ * https://live.paloaltonetworks.com/t5/Learning-Articles/IPv6-Support-on-the-Palo-Alto-Networks-Firewall/ta-p/52994 */
+ openconnect_disable_ipv6(vpninfo);
+
+ /* Set 10-second DPD/keepalive (same as Windows client) unless
+ * overridden with --force-dpd */
+ if (!vpninfo->ssl_times.dpd)
+ vpninfo->ssl_times.dpd = 10;
+ vpninfo->ssl_times.keepalive = vpninfo->ssl_times.dpd;
+
+ return 0;
+}
+
+static int gpst_get_config(struct openconnect_info *vpninfo)
+{
+ char *orig_path;
+ int result;
+ struct oc_text_buf *request_body = buf_alloc();
+ struct oc_vpn_option *old_cstp_opts = vpninfo->cstp_options;
+ const char *old_addr = vpninfo->ip_info.addr, *old_netmask = vpninfo->ip_info.netmask;
+ const char *request_body_type = "application/x-www-form-urlencoded";
+ const char *method = "POST";
+ char *xml_buf=NULL;
+
+ /* submit getconfig request */
+ buf_append(request_body, "client-type=1&protocol-version=p1&app-version=3.0.1-10");
+ append_opt(request_body, "os-version", vpninfo->platname);
+ if (!strcmp(vpninfo->platname, "win"))
+ append_opt(request_body, "clientos", "Windows");
+ else
+ append_opt(request_body, "clientos", vpninfo->platname);
+ append_opt(request_body, "hmac-algo", "sha1,md5");
+ append_opt(request_body, "enc-algo", "aes-128-cbc,aes-256-cbc");
+ if (old_addr) {
+ append_opt(request_body, "preferred-ip", old_addr);
+ filter_opts(request_body, vpninfo->cookie, "preferred-ip", 0);
+ } else
+ buf_append(request_body, "&%s", vpninfo->cookie);
+ if ((result = buf_error(request_body)))
+ goto out;
+
+ orig_path = vpninfo->urlpath;
+ vpninfo->urlpath = strdup("ssl-vpn/getconfig.esp");
+ result = do_https_request(vpninfo, method, request_body_type, request_body,
+ &xml_buf, 0);
+ free(vpninfo->urlpath);
+ vpninfo->urlpath = orig_path;
+
+ if (result < 0)
+ goto out;
+
+ /* parse getconfig result */
+ result = gpst_xml_or_error(vpninfo, result, xml_buf, gpst_parse_config_xml, NULL, NULL);
+ if (result)
+ return result;
+
+ if (!vpninfo->ip_info.mtu) {
+ /* FIXME: GP gateway config always seems to be <mtu>0</mtu> */
+ vpninfo->ip_info.mtu = calculate_mtu(vpninfo);
+ vpn_progress(vpninfo, PRG_ERR,
+ _("No MTU received. Calculated %d\n"), vpninfo->ip_info.mtu);
+ /* return -EINVAL; */
+ }
+ if (!vpninfo->ip_info.addr) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("No IP address received. Aborting\n"));
+ result = -EINVAL;
+ goto out;
+ }
+ if (old_addr) {
+ if (strcmp(old_addr, vpninfo->ip_info.addr)) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Reconnect gave different Legacy IP address (%s != %s)\n"),
+ vpninfo->ip_info.addr, old_addr);
+ result = -EINVAL;
+ goto out;
+ }
+ }
+ if (old_netmask) {
+ if (strcmp(old_netmask, vpninfo->ip_info.netmask)) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Reconnect gave different Legacy IP netmask (%s != %s)\n"),
+ vpninfo->ip_info.netmask, old_netmask);
+ result = -EINVAL;
+ goto out;
+ }
+ }
+
+out:
+ buf_free(request_body);
+ free_optlist(old_cstp_opts);
+ free(xml_buf);
+ return result;
+}
+
+static int gpst_connect(struct openconnect_info *vpninfo)
+{
+ int ret;
+ struct oc_text_buf *reqbuf;
+ const char start_tunnel[12] = "START_TUNNEL"; /* NOT zero-terminated */
+ char buf[256];
+
+ /* Connect to SSL VPN tunnel */
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("Connecting to HTTPS tunnel endpoint ...\n"));
+
+ ret = openconnect_open_https(vpninfo);
+ if (ret)
+ return ret;
+
+ reqbuf = buf_alloc();
+ buf_append(reqbuf, "GET %s?", vpninfo->urlpath);
+ filter_opts(reqbuf, vpninfo->cookie, "user,authcookie", 1);
+ buf_append(reqbuf, " HTTP/1.1\r\n\r\n");
+ if ((ret = buf_error(reqbuf)))
+ goto out;
+
+ if (vpninfo->dump_http_traffic)
+ dump_buf(vpninfo, '>', reqbuf->data);
+
+ vpninfo->ssl_write(vpninfo, reqbuf->data, reqbuf->pos);
+
+ if ((ret = vpninfo->ssl_read(vpninfo, buf, 12)) < 0) {
+ if (ret == -EINTR)
+ goto out;
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Error fetching GET-tunnel HTTPS response.\n"));
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (!strncmp(buf, start_tunnel, sizeof(start_tunnel))) {
+ ret = 0;
+ } else if (ret==0) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Gateway disconnected immediately after GET-tunnel request.\n"));
+ ret = -EPIPE;
+ } else {
+ if (ret==sizeof(start_tunnel)) {
+ ret = vpninfo->ssl_gets(vpninfo, buf+sizeof(start_tunnel), sizeof(buf)-sizeof(start_tunnel));
+ ret = (ret>0 ? ret : 0) + sizeof(start_tunnel);
+ }
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Got inappropriate HTTP GET-tunnel response: %.*s\n"), ret, buf);
+ ret = -EINVAL;
+ }
+
+ if (ret < 0)
+ openconnect_close_https(vpninfo, 0);
+ else {
+ monitor_fd_new(vpninfo, ssl);
+ monitor_read_fd(vpninfo, ssl);
+ monitor_except_fd(vpninfo, ssl);
+ vpninfo->ssl_times.last_rekey = vpninfo->ssl_times.last_rx = vpninfo->ssl_times.last_tx = time(NULL);
+ }
+
+out:
+ buf_free(reqbuf);
+ return ret;
+}
+
+int gpst_setup(struct openconnect_info *vpninfo)
+{
+ int ret;
+
+ /* Get configuration */
+ ret = gpst_get_config(vpninfo);
+ if (ret)
+ return ret;
+
+ ret = gpst_connect(vpninfo);
+ return ret;
+}
+
+int gpst_mainloop(struct openconnect_info *vpninfo, int *timeout)
+{
+ int ret;
+ int work_done = 0;
+ uint16_t ethertype;
+ uint32_t one, zero, magic;
+
+ if (vpninfo->ssl_fd == -1)
+ goto do_reconnect;
+
+ while (1) {
+ int receive_mtu = MAX(2048, vpninfo->ip_info.mtu + 256);
+ int len, payload_len;
+
+ if (!vpninfo->cstp_pkt) {
+ vpninfo->cstp_pkt = malloc(sizeof(struct pkt) + receive_mtu);
+ if (!vpninfo->cstp_pkt) {
+ vpn_progress(vpninfo, PRG_ERR, _("Allocation failed\n"));
+ break;
+ }
+ }
+
+ len = ssl_nonblock_read(vpninfo, vpninfo->cstp_pkt->gpst.hdr, receive_mtu + 16);
+ if (!len)
+ break;
+ if (len < 0) {
+ vpn_progress(vpninfo, PRG_ERR, _("Packet receive error: %s\n"), strerror(-len));
+ goto do_reconnect;
+ }
+ if (len < 16) {
+ vpn_progress(vpninfo, PRG_ERR, _("Short packet received (%d bytes)\n"), len);
+ vpninfo->quit_reason = "Short packet received";
+ return 1;
+ }
+
+ /* check packet header */
+ magic = load_be32(vpninfo->cstp_pkt->gpst.hdr);
+ ethertype = load_be16(vpninfo->cstp_pkt->gpst.hdr + 4);
+ payload_len = load_be16(vpninfo->cstp_pkt->gpst.hdr + 6);
+ one = load_le32(vpninfo->cstp_pkt->gpst.hdr + 8);
+ zero = load_le32(vpninfo->cstp_pkt->gpst.hdr + 12);
+
+ if (magic != 0x1a2b3c4d)
+ goto unknown_pkt;
+
+ if (len != 16 + payload_len) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Unexpected packet length. SSL_read returned %d (includes 16 header bytes) but header payload_len is %d\n"),
+ len, payload_len);
+ dump_buf_hex(vpninfo, PRG_ERR, '<', vpninfo->cstp_pkt->gpst.hdr, 16);
+ continue;
+ }
+
+ vpninfo->ssl_times.last_rx = time(NULL);
+ switch (ethertype) {
+ case 0:
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("Got GPST DPD/keepalive response\n"));
+
+ if (one != 0 || zero != 0) {
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("Expected 0000000000000000 as last 8 bytes of DPD/keepalive packet header, but got:\n"));
+ dump_buf_hex(vpninfo, PRG_DEBUG, '<', vpninfo->cstp_pkt->gpst.hdr + 8, 8);
+ }
+ continue;
+ case 0x0800:
+ vpn_progress(vpninfo, PRG_TRACE,
+ _("Received data packet of %d bytes\n"),
+ payload_len);
+ vpninfo->cstp_pkt->len = payload_len;
+ queue_packet(&vpninfo->incoming_queue, vpninfo->cstp_pkt);
+ vpninfo->cstp_pkt = NULL;
+ work_done = 1;
+
+ if (one != 1 || zero != 0) {
+ vpn_progress(vpninfo, PRG_DEBUG,
+ _("Expected 0100000000000000 as last 8 bytes of data packet header, but got:\n"));
+ dump_buf_hex(vpninfo, PRG_DEBUG, '<', vpninfo->cstp_pkt->gpst.hdr + 8, 8);
+ }
+ continue;
+ }
+
+ unknown_pkt:
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Unknown packet. Header dump follows:\n"));
+ dump_buf_hex(vpninfo, PRG_ERR, '<', vpninfo->cstp_pkt->gpst.hdr, 16);
+ vpninfo->quit_reason = "Unknown packet received";
+ return 1;
+ }
+
+
+ /* If SSL_write() fails we are expected to try again. With exactly
+ the same data, at exactly the same location. So we keep the
+ packet we had before.... */
+ if (vpninfo->current_ssl_pkt) {
+ handle_outgoing:
+ vpninfo->ssl_times.last_tx = time(NULL);
+ unmonitor_write_fd(vpninfo, ssl);
+
+ ret = ssl_nonblock_write(vpninfo,
+ vpninfo->current_ssl_pkt->gpst.hdr,
+ vpninfo->current_ssl_pkt->len + 16);
+ if (ret < 0)
+ goto do_reconnect;
+ else if (!ret) {
+ switch (ka_stalled_action(&vpninfo->ssl_times, timeout)) {
+ case KA_REKEY:
+ goto do_rekey;
+ case KA_DPD_DEAD:
+ goto peer_dead;
+ case KA_NONE:
+ return work_done;
+ }
+ }
+
+ if (ret != vpninfo->current_ssl_pkt->len + 16) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("SSL wrote too few bytes! Asked for %d, sent %d\n"),
+ vpninfo->current_ssl_pkt->len + 16, ret);
+ vpninfo->quit_reason = "Internal error";
+ return 1;
+ }
+ /* Don't free the 'special' packets */
+ if (vpninfo->current_ssl_pkt != &dpd_pkt)
+ free(vpninfo->current_ssl_pkt);
+
+ vpninfo->current_ssl_pkt = NULL;
+ }
+
+ switch (keepalive_action(&vpninfo->ssl_times, timeout)) {
+ case KA_REKEY:
+ do_rekey:
+ vpn_progress(vpninfo, PRG_INFO, _("GlobalProtect rekey due\n"));
+ goto do_reconnect;
+ case KA_DPD_DEAD:
+ peer_dead:
+ vpn_progress(vpninfo, PRG_ERR,
+ _("GPST Dead Peer Detection detected dead peer!\n"));
+ do_reconnect:
+ ret = ssl_reconnect(vpninfo);
+ if (ret) {
+ vpn_progress(vpninfo, PRG_ERR, _("Reconnect failed\n"));
+ vpninfo->quit_reason = "GPST reconnect failed";
+ return ret;
+ }
+ return 1;
+
+ case KA_KEEPALIVE:
+ /* No need to send an explicit keepalive
+ if we have real data to send */
+ if (vpninfo->dtls_state != DTLS_CONNECTED &&
+ vpninfo->outgoing_queue.head)
+ break;
+
+ case KA_DPD:
+ vpn_progress(vpninfo, PRG_DEBUG, _("Send GPST DPD/keepalive request\n"));
+
+ vpninfo->current_ssl_pkt = (struct pkt *)&dpd_pkt;
+ goto handle_outgoing;
+ }
+
+
+ /* Service outgoing packet queue */
+ while (vpninfo->dtls_state != DTLS_CONNECTED &&
+ (vpninfo->current_ssl_pkt = dequeue_packet(&vpninfo->outgoing_queue))) {
+ struct pkt *this = vpninfo->current_ssl_pkt;
+
+ /* store header */
+ store_be32(this->gpst.hdr, 0x1a2b3c4d);
+ store_be16(this->gpst.hdr + 4, 0x0800); /* IPv4 EtherType */
+ store_be16(this->gpst.hdr + 6, this->len);
+ store_le32(this->gpst.hdr + 8, 1);
+ store_le32(this->gpst.hdr + 12, 0);
+
+ vpn_progress(vpninfo, PRG_TRACE,
+ _("Sending data packet of %d bytes\n"),
+ this->len);
+
+ goto handle_outgoing;
+ }
+
+ /* Work is not done if we just got rid of packets off the queue */
+ return work_done;
+}
diff --git a/http.c b/http.c
index e53f3fc..a860ae4 100644
--- a/http.c
+++ b/http.c
@@ -54,6 +54,19 @@ void buf_append_urlencoded(struct oc_text_buf *buf, const char *str)
}
}
+void buf_append_xmlescaped(struct oc_text_buf *buf, const char *str)
+{
+ while (str && *str) {
+ unsigned char c = *str;
+ if (c=='<' || c=='>' || c=='&' || c=='"' || c=='\'')
+ buf_append(buf, "&#x%02x;", c);
+ else
+ buf_append_bytes(buf, str, 1);
+
+ str++;
+ }
+}
+
void buf_append_hex(struct oc_text_buf *buf, const void *str, unsigned len)
{
const unsigned char *data = str;
@@ -958,7 +971,14 @@ int do_https_request(struct openconnect_info *vpninfo, const char *method,
vpn_progress(vpninfo, PRG_ERR,
_("Unexpected %d result from server\n"),
result);
- result = -EINVAL;
+ if (result == 401 || result == 403)
+ result = -EPERM;
+ else if (result == 512) /* GlobalProtect invalid username/password */
+ result = -EACCES;
+ else if (result == 513) /* GlobalProtect invalid client cert */
+ result = -EBADMSG;
+ else
+ result = -EINVAL;
goto out;
}
diff --git a/library.c b/library.c
index b0d635b..6e503ab 100644
--- a/library.c
+++ b/library.c
@@ -141,6 +141,16 @@ const struct vpn_proto openconnect_protos[] = {
.udp_send_probes = oncp_esp_send_probes,
.udp_catch_probe = oncp_esp_catch_probe,
#endif
+ }, {
+ .name = "gp",
+ .pretty_name = N_("Palo Alto Networks GlobalProtect"),
+ .description = N_("Compatible with Palo Alto Networks (PAN) GlobalProtect SSL VPN"),
+ .flags = OC_PROTO_PROXY | OC_PROTO_AUTH_CERT | OC_PROTO_AUTH_OTP | OC_PROTO_AUTH_STOKEN,
+ .vpn_close_session = gpst_bye,
+ .tcp_connect = gpst_setup,
+ .tcp_mainloop = gpst_mainloop,
+ .add_http_headers = gpst_common_headers,
+ .obtain_cookie = gpst_obtain_cookie,
},
{ /* NULL */ }
};
diff --git a/openconnect-internal.h b/openconnect-internal.h
index e96610b..96bad7c 100644
--- a/openconnect-internal.h
+++ b/openconnect-internal.h
@@ -146,6 +146,10 @@ struct pkt {
unsigned char pad[16];
unsigned char hdr[8];
} cstp;
+ struct {
+ unsigned char pad[8];
+ unsigned char hdr[16];
+ } gpst;
};
unsigned char data[];
};
@@ -860,6 +864,18 @@ int oncp_bye(struct openconnect_info *vpninfo, const char *reason);
int oncp_esp_send_probes(struct openconnect_info *vpninfo);
int oncp_esp_catch_probe(struct openconnect_info *vpninfo, struct pkt *pkt);
+/* auth-globalprotect.c */
+int gpst_obtain_cookie(struct openconnect_info *vpninfo);
+void gpst_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf);
+int gpst_bye(struct openconnect_info *vpninfo, const char *reason);
+
+/* gpst.c */
+int gpst_xml_or_error(struct openconnect_info *vpninfo, int result, char *response,
+ int (*xml_cb)(struct openconnect_info *, xmlNode *xml_node),
+ char **prompt, char **inputStr);
+int gpst_setup(struct openconnect_info *vpninfo);
+int gpst_mainloop(struct openconnect_info *vpninfo, int *timeout);
+
/* lzs.c */
int lzs_decompress(unsigned char *dst, int dstlen, const unsigned char *src, int srclen);
int lzs_compress(unsigned char *dst, int dstlen, const unsigned char *src, int srclen);
@@ -1017,6 +1033,7 @@ int get_utf8char(const char **utf8);
void buf_append_from_utf16le(struct oc_text_buf *buf, const void *utf16);
void buf_truncate(struct oc_text_buf *buf);
void buf_append_urlencoded(struct oc_text_buf *buf, const char *str);
+void buf_append_xmlescaped(struct oc_text_buf *buf, const char *str);
int buf_error(struct oc_text_buf *buf);
int buf_free(struct oc_text_buf *buf);
char *openconnect_create_useragent(const char *base);
diff --git a/openconnect.8.in b/openconnect.8.in
index c97dec2..5e1b933 100644
--- a/openconnect.8.in
+++ b/openconnect.8.in
@@ -422,11 +422,12 @@ Select VPN protocol
.I PROTO
to be used for the connection. Supported protocols are
.I anyconnect
-for Cisco AnyConnect (the default), and
+for Cisco AnyConnect (the default),
.I nc
for experimental support for Juniper Network Connect (also supported
-by Junos Pulse servers).
-
+by Junos Pulse servers), and
+.I gp
+for experimental support for PAN GlobalProtect.
.TP
.B \-\-token\-mode=MODE
Enable one-time password generation using the
diff --git a/www/Makefile.am b/www/Makefile.am
index 51a242b..f791a00 100644
--- a/www/Makefile.am
+++ b/www/Makefile.am
@@ -6,7 +6,7 @@ CONV = "$(srcdir)/html.py"
FTR_PAGES = csd.html charset.html token.html pkcs11.html tpm.html features.html gui.html nonroot.html
START_PAGES = building.html connecting.html manual.html vpnc-script.html
INDEX_PAGES = changelog.html download.html index.html packages.html platforms.html
-PROTO_PAGES = anyconnect.html juniper.html
+PROTO_PAGES = anyconnect.html juniper.html globalprotect.html
TOPLEVEL_PAGES = contribute.html mail.html
ALL_PAGES = $(FTR_PAGES) $(START_PAGES) $(INDEX_PAGES) $(TOPLEVEL_PAGES) $(PROTO_PAGES)
diff --git a/www/globalprotect.xml b/www/globalprotect.xml
new file mode 100644
index 0000000..408eb2e
--- /dev/null
+++ b/www/globalprotect.xml
@@ -0,0 +1,64 @@
+<PAGE>
+ <INCLUDE file="inc/header.tmpl" />
+
+ <VAR match="VAR_SEL_PROTOCOLS" replace="selected" />
+ <VAR match="VAR_SEL_GLOBALPROTECT" replace="selected" />
+ <PARSE file="menu1.xml" />
+ <PARSE file="menu2-protocols.xml" />
+
+ <INCLUDE file="inc/content.tmpl" />
+
+<h1>PAN GlobalProtect</h1>
+
+<h2>How the VPN works</h2>
+
+<p>This VPN is based on HTTPS and <a
+href="https://tools.ietf.org/html/rfc3948">ESP</a>, with routing and
+configuration information distributed in XML format.</p>
+
+<p>To authenticate, you connect to the secure web server (<tt>POST
+/ssl-vpn/login.esp</tt>), provide a username, password, and (optionally) a
+certificate, and receive an authcookie. The username, authcookie, and a
+couple other bits of information obtained at login are combined into the
+OpenConnect cookie.</p>
+
+<p>To connect to the secure tunnel, the cookie is used to read routing and
+tunnel configuration information (<tt>POST /ssl-vpn/getconfig.esp</tt>).</p>
+
+<p>Finally, either an HTTPS-based or ESP-based tunnel is setup:</p>
+
+<ol>
+ <li>The cookie is used in a non-standard HTTP request (<tt>GET
+ /ssl-tunnel-connect.sslvpn</tt>, which acts more like a
+ <tt>CONNECT</tt>). Arbitrary IP packets can be passed over the
+ resulting tunnel.</li>
+ <li>The ESP keys provided by the configuration request are used to set up
+ a <a href="https://tools.ietf.org/html/rfc3948">UDP-encapsulated
+ ESP</a> tunnel.</li>
+</ol>
+
+<p>This version of OpenConnect supports <b>only</b> the HTTPS tunnel.</p>
+
+<h2>Quirks and issues</h2>
+
+<p>There appears to be no reasonable mechanism to negotiate the <a
+href="https://en.wikipedia.org/wiki/Maximum_transmission_unit">MTU</a> for
+the link, or discover the MTU of the accessed network. The configuration
+always shows <tt><![CDATA[<mtu>0</mtu>]]></tt>. OpenConnect attempts to
+calculate the MTU by starting from the base MTU with the overhead of
+encapsulating each packets within ESP, UDP, and IP.</p>
+
+<p>There is currently no IPv6 support. <a
+href="https://live.paloaltonetworks.com/t5/Learning-Articles/IPv6-Support-on-the-Palo-Alto-Networks-Firewall/ta-p/52994">PAN's
+documentation</a> suggests that recent versions of GlobalProtect may support
+IPv6 over the ESP tunnel, though not the HTTPS tunnel.</p>
+
+<p>Compared to the AnyConnect or Juniper protocols, the GlobalProtect
+protocol appears to have very little in the way of <a
+href="https://en.wikipedia.org/wiki/In-band_signaling">in-band
+signaling</a>. The HTTPS tunnel can only send or receive IPv4 packets and a
+simple DPD/keepalive packet (always sent by the client and echoed by the
+server).</p>
+
+ <INCLUDE file="inc/footer.tmpl" />
+</PAGE>
diff --git a/www/mail.xml b/www/mail.xml
index 3cb1a13..5ce2a13 100644
--- a/www/mail.xml
+++ b/www/mail.xml
@@ -43,7 +43,9 @@
automatically filter this out of the debugging output for you.
</p>
<p>For Juniper VPN, the equivalent is a <tt>DSID</tt> cookie, which is not yet filtered
- out of any output <i>(the authentication support in Juniper is still very new)</i>.</p>
+ out of any output <i>(the authentication support in Juniper is still very new)</i>.
+ For PAN GlobalConnect, the equivalent is a URL-encoded
+ <tt>authcookie</tt> parameter, which is also not filtered out of any output.</p>
<h1>Internet Relay Chat (IRC)</h1>
diff --git a/www/menu2-protocols.xml b/www/menu2-protocols.xml
index 6ac7e4f..2e51cb5 100644
--- a/www/menu2-protocols.xml
+++ b/www/menu2-protocols.xml
@@ -2,5 +2,6 @@
<STARTMENU level="2"/>
<MENU topic="AnyConnect" link="anyconnect.html" mode="VAR_SEL_ANYCONNECT" />
<MENU topic="Juniper" link="juniper.html" mode="VAR_SEL_JUNIPER" />
+ <MENU topic="GlobalProtect" link="globalprotect.html" mode="VAR_SEL_GLOBALPROTECT" />
<ENDMENU />
</PAGE>
--
2.7.4
More information about the openconnect-devel
mailing list