]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
add PAN GlobalProtect protocol support (HTTPS tunnel only)
authorDaniel Lenski <dlenski@gmail.com>
Tue, 15 Aug 2017 04:32:03 +0000 (21:32 -0700)
committerDavid Woodhouse <dwmw2@infradead.org>
Tue, 27 Feb 2018 15:25:03 +0000 (16:25 +0100)
Signed-off-by: Daniel Lenski <dlenski@gmail.com>
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Makefile.am
auth-globalprotect.c [new file with mode: 0644]
gpst.c [new file with mode: 0644]
http.c
library.c
openconnect-internal.h
openconnect.8.in
www/Makefile.am
www/globalprotect.xml [new file with mode: 0644]
www/mail.xml
www/menu2-protocols.xml

index d3ae26ab8d8d86f4bfd637662b6b5ed8cad84c8c..fc36c590958356d8f70165df880ec377bcc3563e 100644 (file)
@@ -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 (file)
index 0000000..b855b82
--- /dev/null
@@ -0,0 +1,385 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Author: Dan Lenski <dlenski@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 (file)
index 0000000..474817c
--- /dev/null
+++ b/gpst.c
@@ -0,0 +1,742 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Author: Daniel Lenski <dlenski@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 e53f3fce8aebc721bb82e049047330a30096bc67..5307d824d2365b66cc34eac1b47c00c51cf21ba0 100644 (file)
--- a/http.c
+++ b/http.c
@@ -958,7 +958,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;
        }
 
index b0d635b308725fc36b26b8eebf7a036f6bb7ea2e..6e503ab5f44d8e22dfceed7076c69bc9ec9c062d 100644 (file)
--- 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 */ }
 };
index e96610b81e0d31b4a7cb56959ec8bee4b8bf301d..70fb052efc0fa4918ef33149c589165ce559df10 100644 (file)
@@ -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);
index c97dec2b3b69573f5dc20a254ceec7b3698f9490..5e1b9335f0ec16c4b868ee296257ebfbeb298a58 100644 (file)
@@ -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
index 51a242babe4c9e8f236226426bacba1243b447b3..f791a004632604a2abebede95dc3eacd9543904c 100644 (file)
@@ -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 (file)
index 0000000..408eb2e
--- /dev/null
@@ -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[&lt;mtu&gt;0&lt;/mtu&gt;]]></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>
index 3cb1a1368b6348087d731e7fbbaca069adc187d9..5ce2a134382e49e21455fe2077c4ee3cb8967bfb 100644 (file)
@@ -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>
 
index 6ac7e4f5460f7a7296116e46c97951a33f6f8e49..2e51cb5c3c94cb872ea1ebd53dc7509a4404bb2a 100644 (file)
@@ -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>