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