[PATCH 7/8] Add support for GlobalProtect ESP tunnel

Daniel Lenski dlenski at gmail.com
Sat May 20 15:43:28 PDT 2017


Most of the existing ESP support code (written for Juniper/nc) can be reused
for GlobalProtect ESP. The ESP algorithms, SPIs, and keys are sent as part of the
getconfig XML response.

GlobalProtect requires a fairly awkward "tap dance" between the TCP mainloop and
the UDP mainloop in order to support ESP:

* Prior to the getconfig XML request, the HTTPS tunnel will not work (even though
  the authcookie is already known from the login response) and the ESP tunnel
  also will not work (because the ESP keys are not known).
* After the getconfig XML request, either the ESP tunnel or the HTTPS tunnel can
  be connected, but not both.  As soon as the HTTPS tunnel is disconnected,
  the ESP keys are invalidated.  On the other hand, if the ESP tunnel stops
  responding due to some firewall that interferes with UDP, the HTTPS tunnel
  can still be connected.
* Therefore, in order to allow the ESP tunnel to start, the TCP mainloop must
  refrain from actually connecting to the HTTPS tunnel unless the ESP tunnel
  is disabled or has failed to connect... but it can't wait *too* long
  because then the HTTPS keepalive connection may be dropped, and the user
  will wonder why no traffic is flowing even though the VPN has allegedly
  started.  The wait time is currently hard-coded at 5 seconds (half the DPD
  interval used by the official clients).

Another quirk of the GlobalProtect ESP support: it uses specially
constructed ICMP request/reply ("ping") packets as the probes for ESP
initiation and DPD.

* These packets must contain a "magic payload" in order to work.
* In most GlobalProtect VPNs, the packets are addressed to the public, external IPv4
  address of the VPN gateway server even though they are sent over the ESP
  tunnel (???), but in some cases they must be addressed to a different address
  which is misleading described as <gw-address> in the getconfig XML response.

Don't blame me. I didn't design this.

Signed-off-by: Daniel Lenski <dlenski at gmail.com>
---
 esp.c                  | 104 +++++++++++++++++++++++++++++++++++++++++
 gpst.c                 | 122 +++++++++++++++++++++++++++++++++++++++++++++++--
 library.c              |   8 ++++
 openconnect-internal.h |   4 ++
 www/globalprotect.xml  |  17 +++++--
 5 files changed, 249 insertions(+), 6 deletions(-)

diff --git a/esp.c b/esp.c
index f705aa3..42d5185 100644
--- a/esp.c
+++ b/esp.c
@@ -23,6 +23,8 @@
 #include <string.h>
 #include <stdlib.h>
 #include <errno.h>
+#include <netinet/ip.h>
+#include <netinet/ip_icmp.h>
 
 #include "openconnect-internal.h"
 #include "lzo.h"
@@ -112,11 +114,107 @@ int esp_send_probes(struct openconnect_info *vpninfo)
 	return 0;
 };
 
+static uint16_t csum(uint16_t *buf, int nwords)
+{
+	uint32_t sum = 0;
+	for(sum=0; nwords>0; nwords--)
+		sum += ntohs(*buf++);
+	sum = (sum >> 16) + (sum &0xffff);
+	sum += (sum >> 16);
+	return htons((uint16_t)(~sum));
+}
+
+int esp_send_probes_gp(struct openconnect_info *vpninfo)
+{
+	/* The GlobalProtect VPN initiates and maintains the ESP connection
+	 * using specially-crafted ICMP ("ping") packets.
+	 *
+	 * 1) These ping packets have a special magic payload. It must
+	 *    include at least the 16 bytes below. The Windows client actually
+	 *    sends this 56-byte version, but the remaining bytes don't
+	 *    seem to matter:
+	 *
+	 *    "monitor\x00\x00pan ha 0123456789:;<=>? !\"#$%&\'()*+,-./\x10\x11\x12\x13\x14\x15\x16\x18";
+	 *
+	 * 2) The ping packets are addressed to the IP supplied in the
+	 *    config XML as as <gw-address>. In most cases, this is the
+	 *    same as the *external* IP address of the VPN gateway
+	 *    (vpninfo->ip_info.gateway_addr), but in some cases it is a
+	 *    separate address.
+	 *
+	 *    Don't blame me. I didn't design this.
+	 */
+	static char magic[16] = "monitor\x00\x00pan ha ";
+
+	int pktlen, seq;
+	struct pkt *pkt = malloc(sizeof(*pkt) + sizeof(struct ip) + ICMP_MINLEN + sizeof(magic) + vpninfo->pkt_trailer);
+	struct ip *iph = (void *)pkt->data;
+	struct icmp *icmph = (void *)(pkt->data + sizeof(*iph));
+	char *pmagic = (void *)(pkt->data + sizeof(*iph) + ICMP_MINLEN);
+	if (!pkt)
+		return -ENOMEM;
+
+	if (vpninfo->dtls_fd == -1) {
+		int fd = udp_connect(vpninfo);
+		if (fd < 0)
+			return fd;
+
+		/* We are not connected until we get an ESP packet back */
+		vpninfo->dtls_state = DTLS_SLEEPING;
+		vpninfo->dtls_fd = fd;
+		monitor_fd_new(vpninfo, dtls);
+		monitor_read_fd(vpninfo, dtls);
+		monitor_except_fd(vpninfo, dtls);
+	}
+
+	for (seq=1; seq <= (vpninfo->dtls_state==DTLS_CONNECTED ? 1 : 3); seq++) {
+		memset(pkt, 0, sizeof(*pkt) + sizeof(*iph) + ICMP_MINLEN + sizeof(magic));
+		pkt->len = sizeof(struct ip) + ICMP_MINLEN + sizeof(magic);
+
+		/* IP Header */
+		iph->ip_hl = 5;
+		iph->ip_v = 4;
+		iph->ip_len = htons(sizeof(*iph) + ICMP_MINLEN + sizeof(magic));
+		iph->ip_id = htons(0x4747); /* what the Windows client uses */
+		iph->ip_off = htons(IP_DF); /* don't fragment, frag offset = 0 */
+		iph->ip_ttl = 64; /* hops */
+		iph->ip_p = 1; /* ICMP */
+		iph->ip_src.s_addr = inet_addr(vpninfo->ip_info.addr);
+		iph->ip_dst.s_addr = vpninfo->esp_magic;
+		iph->ip_sum = csum((uint16_t *)iph, sizeof(*iph)/2);
+
+		/* ICMP echo request */
+		icmph->icmp_type = ICMP_ECHO;
+		icmph->icmp_hun.ih_idseq.icd_id = htons(0x4747);
+		icmph->icmp_hun.ih_idseq.icd_seq = htons(seq);
+		memcpy(pmagic, magic, sizeof(magic)); /* required to get gateway to respond */
+		icmph->icmp_cksum = csum((uint16_t *)icmph, (ICMP_MINLEN+sizeof(magic))/2);
+
+		pktlen = encrypt_esp_packet(vpninfo, pkt);
+		if (pktlen >= 0)
+			send(vpninfo->dtls_fd, (void *)&pkt->esp, pktlen, 0);
+	}
+
+	free(pkt);
+
+	vpninfo->dtls_times.last_tx = time(&vpninfo->new_dtls_started);
+
+	return 0;
+}
+
 int esp_catch_probe(struct openconnect_info *vpninfo, struct pkt *pkt)
 {
 	return (pkt->len == 1 && pkt->data[0] == 0);
 }
 
+int esp_catch_probe_gp(struct openconnect_info *vpninfo, struct pkt *pkt)
+{
+	return ( pkt->len >= 21
+		 && pkt->data[9]==1 /* IPv4 protocol field == ICMP */
+		 && *((in_addr_t *)(pkt->data + 12)) == vpninfo->esp_magic /* source == magic address */
+		 && pkt->data[20]==0 /* ICMP reply */ );
+}
+
 int esp_setup(struct openconnect_info *vpninfo, int dtls_attempt_period)
 {
 	if (vpninfo->dtls_state == DTLS_DISABLED ||
@@ -351,6 +449,12 @@ void esp_close(struct openconnect_info *vpninfo)
 		vpninfo->dtls_state = DTLS_SLEEPING;
 }
 
+void esp_close_secret(struct openconnect_info *vpninfo)
+{
+	esp_close(vpninfo);
+	vpninfo->dtls_state = DTLS_NOSECRET;
+}
+
 void esp_shutdown(struct openconnect_info *vpninfo)
 {
 	destroy_esp_ciphers(&vpninfo->esp_in[0]);
diff --git a/gpst.c b/gpst.c
index 474817c..44fec7e 100644
--- a/gpst.c
+++ b/gpst.c
@@ -328,6 +328,39 @@ static int calculate_mtu(struct openconnect_info *vpninfo)
 	return mtu;
 }
 
+static int set_esp_algo(struct openconnect_info *vpninfo, const char *s, int hmac)
+{
+	if (hmac) {
+		if (!strcmp(s, "sha1"))		{ vpninfo->esp_hmac = HMAC_SHA1; vpninfo->hmac_key_len = 20; return 0; }
+		if (!strcmp(s, "md5"))		{ vpninfo->esp_hmac = HMAC_MD5;  vpninfo->hmac_key_len = 16; return 0; }
+	} else {
+		if (!strcmp(s, "aes128") || !strcmp(s, "aes-128-cbc"))
+		                                { vpninfo->esp_enc = ENC_AES_128_CBC; vpninfo->enc_key_len = 16; return 0; }
+		if (!strcmp(s, "aes-256-cbc"))	{ vpninfo->esp_enc = ENC_AES_256_CBC; vpninfo->enc_key_len = 32; return 0; }
+	}
+	vpn_progress(vpninfo, PRG_ERR, _("Unknown ESP %s algorithm: %s"), hmac ? "MAC" : "encryption", s);
+	return -ENOENT;
+}
+
+static int get_key_bits(xmlNode *xml_node, unsigned char *dest)
+{
+	int bits = -1;
+	xmlNode *child;
+	const char *s, *p;
+
+	for (child = xml_node->children; child; child=child->next) {
+		if (xmlnode_get_text(child, "bits", &s) == 0) {
+			bits = atoi(s);
+			free((void *)s);
+		} else if (xmlnode_get_text(child, "val", &s) == 0) {
+			for (p=s; *p && *(p+1) && (bits-=8)>=0; p+=2)
+				*dest++ = unhex(p);
+			free((void *)s);
+		}
+	}
+	return (bits == 0) ? 0 : -EINVAL;
+}
+
 /* Return value:
  *  < 0, on error
  *  = 0, on success; *form is populated
@@ -346,6 +379,7 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
 	vpninfo->ip_info.addr6 = vpninfo->ip_info.netmask6 = NULL;
 	vpninfo->ip_info.domain = NULL;
 	vpninfo->ip_info.mtu = 0;
+	vpninfo->esp_magic = inet_addr(vpninfo->ip_info.gateway_addr);
 	vpninfo->cstp_options = NULL;
 
 	for (ii = 0; ii < 3; ii++)
@@ -363,11 +397,13 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
 			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."
+			 * gateway is meaningless." See esp_send_probes_gp for the
+			 * gory details of what this field actually means.
 			 */
 			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);
+			vpninfo->esp_magic = inet_addr(s);
 			free((void *)s);
 		} else if (xmlnode_is_named(xml_node, "dns")) {
 			for (ii=0, member = xml_node->children; member && ii<3; member=member->next)
@@ -395,7 +431,32 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
 				}
 			}
 		} else if (xmlnode_is_named(xml_node, "ipsec")) {
+#ifdef HAVE_ESP
+			if (vpninfo->dtls_state != DTLS_DISABLED) {
+				int c = (vpninfo->current_esp_in ^= 1);
+				for (member = xml_node->children; member; member=member->next) {
+					s = NULL;
+					if (!xmlnode_get_text(member, "udp-port", &s))		udp_sockaddr(vpninfo, atoi(s));
+					else if (!xmlnode_get_text(member, "enc-algo", &s)) 	set_esp_algo(vpninfo, s, 0);
+					else if (!xmlnode_get_text(member, "hmac-algo", &s))	set_esp_algo(vpninfo, s, 1);
+					else if (!xmlnode_get_text(member, "c2s-spi", &s))	vpninfo->esp_out.spi = htonl(strtoul(s, NULL, 16));
+					else if (!xmlnode_get_text(member, "s2c-spi", &s))	vpninfo->esp_in[c].spi = htonl(strtoul(s, NULL, 16));
+					else if (xmlnode_is_named(member, "ekey-c2s"))		get_key_bits(member, vpninfo->esp_out.enc_key);
+					else if (xmlnode_is_named(member, "ekey-s2c"))		get_key_bits(member, vpninfo->esp_in[c].enc_key);
+					else if (xmlnode_is_named(member, "akey-c2s"))		get_key_bits(member, vpninfo->esp_out.hmac_key);
+					else if (xmlnode_is_named(member, "akey-s2c"))		get_key_bits(member, vpninfo->esp_in[c].hmac_key);
+					else if (!xmlnode_get_text(member, "ipsec-mode", &s) && strcmp(s, "esp-tunnel"))
+						vpn_progress(vpninfo, PRG_ERR, _("GlobalProtect config sent ipsec-mode=%s (expected esp-tunnel)\n"), s);
+					free((void *)s);
+				}
+				if (setup_esp_keys(vpninfo, 0))
+					vpn_progress(vpninfo, PRG_ERR, "Failed to setup ESP keys.\n");
+				else
+					vpninfo->dtls_times.last_rekey = time(NULL);
+			}
+#else
 			vpn_progress(vpninfo, PRG_DEBUG, _("Ignoring ESP keys since ESP support not available in this build\n"));
+#endif
 		}
 	}
 
@@ -407,7 +468,7 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
 	 * overridden with --force-dpd */
 	if (!vpninfo->ssl_times.dpd)
 		vpninfo->ssl_times.dpd = 10;
-	vpninfo->ssl_times.keepalive = vpninfo->ssl_times.dpd;
+	vpninfo->ssl_times.keepalive = vpninfo->esp_ssl_fallback = vpninfo->ssl_times.dpd;
 
 	return 0;
 }
@@ -544,6 +605,8 @@ static int gpst_connect(struct openconnect_info *vpninfo)
 		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);
+		if (vpninfo->dtls_state != DTLS_DISABLED)
+			vpninfo->dtls_state = DTLS_NOSECRET;
 	}
 
 	return ret;
@@ -558,7 +621,24 @@ int gpst_setup(struct openconnect_info *vpninfo)
 	if (ret)
 		return ret;
 
-	ret = gpst_connect(vpninfo);
+	/* We do NOT actually start the HTTPS tunnel yet if we want to
+	 * use ESP, because the ESP tunnel won't work if the HTTPS tunnel
+	 * is connected! >:-(
+	 */
+	if (vpninfo->dtls_state == DTLS_DISABLED || vpninfo->dtls_state == DTLS_NOSECRET)
+		ret = gpst_connect(vpninfo);
+	else {
+		/* We want to prevent the mainloop timers from frantically
+		 * calling the GPST mainloop.
+		 */
+		vpninfo->ssl_times.last_rx = vpninfo->ssl_times.last_tx = time(NULL);
+
+		/* Using (abusing?) last_rekey as the time when the SSL tunnel
+		 * was brought up.
+		 */
+		vpninfo->ssl_times.last_rekey = 0;
+	}
+
 	return ret;
 }
 
@@ -569,6 +649,41 @@ int gpst_mainloop(struct openconnect_info *vpninfo, int *timeout)
 	uint16_t ethertype;
 	uint32_t one, zero, magic;
 
+	/* Starting the HTTPS tunnel kills ESP, so we avoid starting
+	 * it if the ESP tunnel is connected or connecting.
+	 */
+	switch (vpninfo->dtls_state) {
+	case DTLS_CONNECTING:
+		openconnect_close_https(vpninfo, 0); /* don't keep stale HTTPS socket */
+		vpn_progress(vpninfo, PRG_INFO,
+			     _("ESP tunnel connected; exiting HTTPS mainloop.\n"));
+		vpninfo->dtls_state = DTLS_CONNECTED;
+	case DTLS_CONNECTED:
+		return 0;
+	case DTLS_SECRET:
+	case DTLS_SLEEPING:
+		if (time(NULL) < vpninfo->dtls_times.last_rekey + 5) {
+			/* Allow 5 seconds after configuration for ESP to start */
+			if (*timeout > 5000)
+				*timeout = 5000;
+			return 0;
+		} else if (!vpninfo->ssl_times.last_rekey) {
+			/* ... before we switch to HTTPS instead */
+			vpn_progress(vpninfo, PRG_ERR,
+				     _("Failed to connect ESP tunnel; using HTTPS instead.\n"));
+			if (gpst_connect(vpninfo)) {
+				vpninfo->quit_reason = "GPST connect failed";
+				return 1;
+			}
+		}
+		break;
+	case DTLS_NOSECRET:
+		/* HTTPS tunnel already started, or getconfig.esp did not provide any ESP keys */
+	case DTLS_DISABLED:
+		/* ESP is disabled */
+		;
+	}
+
 	if (vpninfo->ssl_fd == -1)
 		goto do_reconnect;
 
@@ -701,6 +816,7 @@ int gpst_mainloop(struct openconnect_info *vpninfo, int *timeout)
 			vpninfo->quit_reason = "GPST reconnect failed";
 			return ret;
 		}
+		esp_setup(vpninfo, vpninfo->dtls_attempt_period);
 		return 1;
 
 	case KA_KEEPALIVE:
diff --git a/library.c b/library.c
index 52126cd..46ab659 100644
--- a/library.c
+++ b/library.c
@@ -151,6 +151,14 @@ const struct vpn_proto openconnect_protos[] = {
 		.tcp_mainloop = gpst_mainloop,
 		.add_http_headers = gpst_common_headers,
 		.obtain_cookie = gpst_obtain_cookie,
+#ifdef HAVE_ESP
+		.udp_setup = esp_setup,
+		.udp_mainloop = esp_mainloop,
+		.udp_close = esp_close_secret,
+		.udp_shutdown = esp_shutdown,
+		.udp_send_probes = esp_send_probes_gp,
+		.udp_catch_probe = esp_catch_probe_gp,
+#endif
 	},
 	{ /* NULL */ }
 };
diff --git a/openconnect-internal.h b/openconnect-internal.h
index a16b05f..7b3284d 100644
--- a/openconnect-internal.h
+++ b/openconnect-internal.h
@@ -375,6 +375,7 @@ struct openconnect_info {
 	struct esp esp_out;
 	int enc_key_len;
 	int hmac_key_len;
+	in_addr_t esp_magic; /* GlobalProtect magic ping address (network-endian) */
 
 	int tncc_fd; /* For Juniper TNCC */
 	const char *csd_xmltag;
@@ -924,10 +925,13 @@ int verify_packet_seqno(struct openconnect_info *vpninfo,
 int esp_setup(struct openconnect_info *vpninfo, int dtls_attempt_period);
 int esp_mainloop(struct openconnect_info *vpninfo, int *timeout);
 void esp_close(struct openconnect_info *vpninfo);
+void esp_close_secret(struct openconnect_info *vpninfo);
 void esp_shutdown(struct openconnect_info *vpninfo);
 int print_esp_keys(struct openconnect_info *vpninfo, const char *name, struct esp *esp);
 int esp_send_probes(struct openconnect_info *vpninfo);
+int esp_send_probes_gp(struct openconnect_info *vpninfo);
 int esp_catch_probe(struct openconnect_info *vpninfo, struct pkt *pkt);
+int esp_catch_probe_gp(struct openconnect_info *vpninfo, struct pkt *pkt);
 
 /* {gnutls,openssl}-esp.c */
 int setup_esp_keys(struct openconnect_info *vpninfo, int new_keys);
diff --git a/www/globalprotect.xml b/www/globalprotect.xml
index 408eb2e..6de116e 100644
--- a/www/globalprotect.xml
+++ b/www/globalprotect.xml
@@ -37,7 +37,10 @@ tunnel configuration information (<tt>POST /ssl-vpn/getconfig.esp</tt>).</p>
       ESP</a> tunnel.</li>
 </ol>
 
-<p>This version of OpenConnect supports <b>only</b> the HTTPS tunnel.</p>
+<p>Since <a href="http://sites.inka.de/~W1011/devel/tcp-tcp.html">TCP over
+TCP is very suboptimal</a>, OpenConnect tries to always use ESP-over-ESP,
+and will only fall over to the HTTPS tunnel if that fails, or if disabled
+via the <tt>--no-dtls</tt> argument.</p>
 
 <h2>Quirks and issues</h2>
 
@@ -51,14 +54,22 @@ 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>
+IPv6 over the ESP tunnel, though not the SSL tunnel.</p>
+
+<p>The ESP and HTTPS tunnels cannot be connected simultaneously.  The ESP
+tunnel becomes unresponsive as soon as the HTTPS tunnel is started, and
+remains so unless/until the tunnel is closed and the configuration is
+re-fetched.</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>
+server).  The ESP tunnel does not have any special DPD/keepalive packet, but
+uses an <a
+href="https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol">ICMP</a>
+("ping") request to the server with a magic payload for this purpose</p>
 
 	<INCLUDE file="inc/footer.tmpl" />
 </PAGE>
-- 
2.7.4




More information about the openconnect-devel mailing list