[PATCH v2 3/8] add PAN GlobalProtect protocol support (HTTPS tunnel only)

Daniel Lenski dlenski at gmail.com
Mon Aug 14 21:32:03 PDT 2017


Signed-off-by: Daniel Lenski <dlenski at gmail.com>
---
 Makefile.am             |   5 +-
 auth-globalprotect.c    | 385 +++++++++++++++++++++++++
 gpst.c                  | 742 ++++++++++++++++++++++++++++++++++++++++++++++++
 http.c                  |   9 +-
 library.c               |  10 +
 openconnect-internal.h  |  16 ++
 openconnect.8.in        |   7 +-
 www/Makefile.am         |   2 +-
 www/globalprotect.xml   |  64 +++++
 www/mail.xml            |   4 +-
 www/menu2-protocols.xml |   1 +
 11 files changed, 1237 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 bb0f377..bcd8f5b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -27,6 +27,7 @@ openconnect_LDADD = libopenconnect.la $(SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIB
 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
@@ -39,14 +40,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..b855b82
--- /dev/null
+++ b/auth-globalprotect.c
@@ -0,0 +1,385 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * 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)
+{
+	http_common_headers(vpninfo, buf);
+}
+
+/* our "auth form" always has a username and password or challenge */
+static struct oc_auth_form *auth_form(struct openconnect_info *vpninfo, char *prompt, char *auth_id)
+{
+	static struct oc_auth_form *form;
+	static 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)
+		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)
+		return NULL;
+	opt2->name = strdup("passwd");
+	opt2->label = auth_id ? strdup(_("Challenge: ")) : strdup(_("Password: "));
+	opt2->type = vpninfo->token_mode!=OC_TOKEN_MODE_NONE ? OC_FORM_OPT_TOKEN : OC_FORM_OPT_PASSWORD;
+
+	form->opts = opt;
+	return form;
+}
+
+/* Return value:
+ *  < 0, on error
+ *  = 0, on success; *form is populated
+ */
+struct gp_login_arg { const char *opt; int save:1; int show:1; int warn_missing:1; int err_missing:1; const char *check; };
+static const struct gp_login_arg gp_login_args[] = {
+    [0] = { .opt="unknown-arg0", .show=1 },
+    [1] = { .opt="authcookie", .save=1, .err_missing=1 },
+    [2] = { .opt="persistent-cookie", .warn_missing=1 },  /* 40 hex digits; persists across sessions */
+    [3] = { .opt="portal", .save=1, .warn_missing=1 },
+    [4] = { .opt="user", .save=1, .err_missing=1 },
+    [5] = { .opt="authentication-source", .show=1 },      /* LDAP-auth, AUTH-RADIUS_RSA_OTP, etc. */
+    [6] = { .opt="configuration", .warn_missing=1 },      /* usually vsys1 (sometimes vsys2, etc.) */
+    [7] = { .opt="domain", .save=1, .warn_missing=1 },
+    [8] = { .opt="unknown-arg8", .show=1 },
+    [9] = { .opt="unknown-arg9", .show=1 },
+    [10] = { .opt="unknown-arg10", .show=1 },
+    [11] = { .opt="unknown-arg11", .show=1 },
+    [12] = { .opt="connection-type", .err_missing=1, .check="tunnel" },
+    [13] = { .opt="minus1", .err_missing=1, .check="-1" },
+    [14] = { .opt="clientVer", .err_missing=1, .check="4100" },
+    [15] = { .opt="preferred-ip", .save=1 },
+};
+const int gp_login_nargs = (sizeof(gp_login_args)/sizeof(*gp_login_args));
+
+static int parse_login_xml(struct openconnect_info *vpninfo, xmlNode *xml_node)
+{
+	struct oc_text_buf *cookie = buf_alloc();
+	const 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<gp_login_args+gp_login_nargs; arg++) {
+		if (!arg->opt)
+			continue;
+
+		if (!xml_node)
+			value = NULL;
+		else if (!xmlnode_is_named(xml_node, "argument"))
+			goto err_out;
+		else {
+			value = (const char *)xmlNodeGetContent(xml_node);
+			if (value && (!strlen(value) || !strcmp(value, "(null)"))) {
+				free((void *)value);
+				value = NULL;
+			}
+			xml_node = xml_node->next;
+		}
+
+		if (arg->check && (value==NULL || 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==NULL) {
+			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((void *)value);
+	}
+
+	vpninfo->cookie = strdup(cookie->data);
+	buf_free(cookie);
+	return 0;
+
+err_out:
+	free((void *)value);
+	buf_free(cookie);
+	return -EINVAL;
+}
+
+static int parse_portal_xml(struct openconnect_info *vpninfo, xmlNode *xml_node)
+{
+	static struct oc_auth_form form = {.message=(char *)"Please select GlobalProtect gateway.", .auth_id=(char *)"_portal"};
+
+	xmlNode *x;
+	struct oc_form_opt_select *opt;
+	int max_choices = 0, result;
+
+	opt = calloc(1, sizeof(*opt));
+	if (!opt)
+		return -ENOMEM;
+	opt->form.type = OC_FORM_OPT_SELECT;
+	opt->form.name = strdup("gateway");
+	opt->form.label = strdup(_("GATEWAY:"));
+
+	/* 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 (xml_node = xml_node->children; xml_node; xml_node=xml_node->next)
+			if (xmlnode_is_named(xml_node, "gateways"))
+				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:
+	/* 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(1, max_choices * sizeof(struct oc_choice *));
+	if (!opt->choices) {
+		free_opt((struct oc_form_opt *)opt);
+		return -ENOMEM;
+	}
+
+	/* 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) {
+				free_opt((struct oc_form_opt *)opt);
+				return -ENOMEM;
+			}
+
+			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);
+
+			opt->choices[opt->nr_choices++] = choice;
+			vpn_progress(vpninfo, PRG_INFO, _("  %s (%s)\n"),
+						 choice->label, choice->name);
+		}
+	}
+
+	/* process static auth form to select gateway */
+	form.opts = (struct oc_form_opt *)(form.authgroup_opt = opt);
+	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) */
+	if ((vpninfo->redirect_url = malloc(strlen(vpninfo->authgroup) + 9)) == NULL) {
+		result = -ENOMEM;
+		goto out;
+	}
+	sprintf(vpninfo->redirect_url, "https://%s", vpninfo->authgroup);
+	result = handle_redirect(vpninfo);
+
+out:
+	free_opt((struct oc_form_opt *)opt);
+	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";
+	const char *method = "POST";
+	char *xml_buf=NULL, *orig_path, *orig_ua;
+	char *prompt=_("Please enter your username and password"), *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, prompt, auth_id);
+	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:");
+		append_opt(request_body, "server", vpninfo->hostname);
+		append_opt(request_body, "computer", vpninfo->localname);
+		if (form->auth_id && form->auth_id[0]!='_')
+			append_opt(request_body, "inputStr", form->auth_id);
+		append_form_opts(vpninfo, form, request_body);
+
+		orig_path = vpninfo->urlpath;
+		orig_ua = vpninfo->useragent;
+		vpninfo->useragent = (char *)"PAN GlobalProtect";
+		vpninfo->urlpath = strdup(portal ? "global-protect/getconfig.esp" : "ssl-vpn/login.esp");
+		result = do_https_request(vpninfo, method, request_body_type, request_body,
+					  &xml_buf, 0);
+		free(vpninfo->urlpath);
+		vpninfo->urlpath = orig_path;
+		vpninfo->useragent = orig_ua;
+
+		/* 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);
+			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) {
+			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, *orig_ua;
+	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);
+
+	/* 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;
+	orig_ua = vpninfo->useragent;
+	vpninfo->useragent = (char *)"PAN GlobalProtect";
+	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;
+	vpninfo->useragent = orig_ua;
+
+	/* 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"));
+
+	buf_free(request_body);
+	free(xml_buf);
+	return result;
+}
diff --git a/gpst.c b/gpst.c
new file mode 100644
index 0000000..474817c
--- /dev/null
+++ b/gpst.c
@@ -0,0 +1,742 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * 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, except that *var should be freed by the caller */
+static int xmlnode_get_text(xmlNode *xml_node, const char *name, const char **var)
+{
+	const char *str;
+
+	if (name && !xmlnode_is_named(xml_node, name))
+		return -EINVAL;
+
+	str = (const 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 = strdup(val);
+	new->next = vpninfo->cstp_options;
+	vpninfo->cstp_options = new;
+
+	return new->value;
+}
+
+/* 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((void *)*inputStr);
+err2:
+	if (prompt) free((void *)*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;
+	const 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((void *)err);
+	}
+	if (xml_doc)
+		xmlFreeDoc(xml_doc);
+	return result;
+}
+
+#define ESP_OVERHEAD (4 /* SPI */ + 4 /* sequence number */ + \
+         20 /* biggest supported MAC (SHA1) */ + 16 /* biggest supported IV (AES-128) */ + \
+	 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;
+	const 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->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((void *)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((void *)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 = 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, *orig_ua;
+	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);
+	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);
+	buf_append(request_body, "&%s", vpninfo->cookie);
+
+	orig_path = vpninfo->urlpath;
+	orig_ua = vpninfo->useragent;
+	vpninfo->useragent = (char *)"PAN GlobalProtect";
+	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;
+	vpninfo->useragent = orig_ua;
+
+	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;
+	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 /ssl-tunnel-connect.sslvpn?%s HTTP/1.1\r\n\r\n", vpninfo->cookie);
+
+	if (vpninfo->dump_http_traffic)
+		dump_buf(vpninfo, '>', reqbuf->data);
+
+	vpninfo->ssl_write(vpninfo, reqbuf->data, reqbuf->pos);
+	buf_free(reqbuf);
+
+	if ((ret = vpninfo->ssl_read(vpninfo, buf, 12)) < 0) {
+		if (ret == -EINTR)
+			return ret;
+		vpn_progress(vpninfo, PRG_ERR,
+		             _("Error fetching GET-tunnel HTTPS response.\n"));
+		return -EINVAL;
+	}
+
+	if (!strncmp(buf, "START_TUNNEL", 12)) {
+		ret = 0;
+	} else if (ret==0) {
+		vpn_progress(vpninfo, PRG_ERR,
+			     _("Gateway disconnected immediately after GET-tunnel request.\n"));
+		ret = -EPIPE;
+	} else {
+		if (ret==12) {
+			ret = vpninfo->ssl_gets(vpninfo, buf+12, 244);
+			ret = (ret>0 ? ret : 0) + 12;
+		}
+		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);
+	}
+
+	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_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_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 59f93e5..812e002 100644
--- a/http.c
+++ b/http.c
@@ -953,7 +953,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 daa1f01..52126cd 100644
--- a/library.c
+++ b/library.c
@@ -141,6 +141,16 @@ const struct vpn_proto openconnect_protos[] = {
 		.udp_send_probes = esp_send_probes,
 		.udp_catch_probe = 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 b70085d..734168b 100644
--- a/openconnect-internal.h
+++ b/openconnect-internal.h
@@ -143,6 +143,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[];
 };
@@ -855,6 +859,18 @@ int oncp_connect(struct openconnect_info *vpninfo);
 int oncp_mainloop(struct openconnect_info *vpninfo, int *timeout);
 int oncp_bye(struct openconnect_info *vpninfo, const char *reason);
 
+/* 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);
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