[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