Skip to content

Commit

Permalink
GlobalProtect IPv6 ESP support
Browse files Browse the repository at this point in the history
ESP for IPv6 is be initiated in the same way as for Legacy IP: send a
ping (ICMPv6 echo request) to the "magic address" from <gw-address-v6>,
with the same "magic payload."

With a hop limit of 128 and random ICMPv6 ID (I think), this exactly
matches what the Windows client sends out. (Good job by me with the PAN
GlobalProtect mind-meld. 🧠😝)

Based on my testing, it appears that the resulting tunnel works for
*both* IPv4-over-ESP and IPv6-over-ESP. (Whew! No need to do 2 separate
sets of "magic pings.") It appears that when `<gw-address-v6/>` is
present in the config, we *must* send the magic pings via ICMPv6 to that
address (rather than ICMPv4 to the `<gw-address/>`) in order for the
IPv6-over-ESP tunnel to work.

Given the pre-existing weirdness of GP ESP, it could be worse.  ¯\_(ツ)_/¯

[ dwmw2: Fix type aliasing abuse, update for new ip_info handling ]

From the original separate commit fixing the aliasing issues:

I believe we get away with it for the address fields and the icmph
because all those structures have uint16_t members, and the compiler
realises that in valid C code, those *might* alias to the uint16_t*
argument to csum_partial().

But the pseudo header, being an array of bytes in Dan's original
version, couldn't *possibly* have been what csum_partial() was going to
look at, so the compiler was free to reorder the writes into it so they
happen *after* the call to csum_partial(). With predictably unhappy
results.

Setting it up as a type which some uint16_t members would make it work,
but really there's not a lot of point. Just add the length and the
next_header field directly to the checksum.

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
  • Loading branch information
dlenski authored and dwmw2 committed Apr 28, 2021
1 parent 18fc690 commit 5b98b62
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 61 deletions.
221 changes: 161 additions & 60 deletions gpst.c
Expand Up @@ -45,6 +45,8 @@
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_icmp.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#endif

#if defined(__linux__)
Expand Down Expand Up @@ -347,19 +349,21 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
char *s = NULL, *deferred_netmask = NULL;
struct oc_split_include *inc;
int split_route_is_default_route = 0;
int n_dns = 0, got_esp = 0;
int n_dns = 0, esp_keys = 0, esp_v4 = 0, esp_v6 = 0;
int ret = 0;
int ii;

uint32_t esp_magic = 0;
struct in6_addr esp6_magic;

if (!xml_node || !xmlnode_is_named(xml_node, "response"))
return -EINVAL;


struct oc_vpn_option *new_opts = NULL;
struct oc_ip_info new_ip_info = {};

if (vpninfo->ip_info.gateway_addr)
vpninfo->esp_magic = inet_addr(vpninfo->ip_info.gateway_addr);
memset(vpninfo->esp_magic, 0, sizeof(vpninfo->esp_magic));
vpninfo->esp_replay_protect = 1;
vpninfo->ssl_times.rekey_method = REKEY_NONE;

Expand Down Expand Up @@ -405,16 +409,19 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
* gateway is meaningless." See esp_send_probes_gp for the
* gory details of what this field actually means.
*/
if (vpninfo->ip_info.gateway_addr && strcmp(s, vpninfo->ip_info.gateway_addr))
if (vpninfo->peer_addr->sa_family == IPPROTO_IP &&
vpninfo->ip_info.gateway_addr && 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, new_ip_info.gateway_addr);
vpninfo->esp_magic = inet_addr(s);
_("Gateway address in config XML (%s) differs from external gateway address (%s).\n"), s, new_ip_info.gateway_addr);
esp_magic = inet_addr(s);
esp_v4 = 1;
} else if (!xmlnode_get_val(xml_node, "gw-address-v6", &s)) {
/* This is probably used analogously to <gw-address>, but
* we haven't yet been able to test.
*/
vpn_progress(vpninfo, PRG_ERR,
_("WARNING: IPv6 gateway address set in config XML (%s). IPv6 ESP may not yet be functional.\n"), s);
if (vpninfo->peer_addr->sa_family == IPPROTO_IPV6 &&
vpninfo->ip_info.gateway_addr && strcmp(s, vpninfo->ip_info.gateway_addr))
vpn_progress(vpninfo, PRG_DEBUG,
_("IPv6 gateway address in config XML (%s) differs from external gateway address (%s).\n"), s, vpninfo->ip_info.gateway_addr);
inet_pton(AF_INET6, s, &esp6_magic);
esp_v6 = 1;
} else if (!xmlnode_get_val(xml_node, "connected-gw-ip", &s)) {
if (vpninfo->ip_info.gateway_addr && strcmp(s, vpninfo->ip_info.gateway_addr))
vpn_progress(vpninfo, PRG_DEBUG, _("Config XML <connected-gw-ip> address (%s) differs from external\n"
Expand Down Expand Up @@ -499,14 +506,8 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
}
if (!(vpninfo->esp_enc > 0 && vpninfo->esp_hmac > 0 && vpninfo->enc_key_len > 0 && vpninfo->hmac_key_len > 0))
vpn_progress(vpninfo, PRG_ERR, "Server's ESP configuration is incomplete or uses unknown algorithms.\n");
else if (openconnect_setup_esp_keys(vpninfo, 0))
vpn_progress(vpninfo, PRG_ERR, "Failed to setup ESP keys.\n");
else {
got_esp = 1;
/* prevent race condition between esp_mainloop() and gpst_mainloop() timers */
vpninfo->dtls_times.last_rekey = time(&vpninfo->new_dtls_started);
vpninfo->delay_tunnel_reason = "awaiting GPST ESP connection";
}
else
esp_keys = 1;
}
#else
vpn_progress(vpninfo, PRG_DEBUG, _("Ignoring ESP keys since ESP support not available in this build\n"));
Expand Down Expand Up @@ -572,9 +573,27 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_
vpn_progress(vpninfo, PRG_ERR,
_("GlobalProtect IPv6 support is experimental. Please report results to <openconnect-devel@lists.infradead.org>.\n"));
#ifdef HAVE_ESP
if (!got_esp)
vpn_progress(vpninfo, vpninfo->dtls_state != DTLS_DISABLED ? PRG_ERR : PRG_DEBUG,
_("Did not receive ESP keys in GlobalProtect config; tunnel will be TLS only.\n"));
if (esp_keys && esp_v6 && new_ip_info.addr6) {
/* We got ESP keys, an IPv6 esp_magic address, and an IPv6 address */
vpninfo->esp_magic_af = AF_INET6;
memcpy(vpninfo->esp_magic, &esp6_magic, sizeof(esp6_magic));

setup_esp_keys:
if (openconnect_setup_esp_keys(vpninfo, 0)) {
vpn_progress(vpninfo, PRG_ERR, "Failed to setup ESP keys.\n");
} else {
/* prevent race condition between esp_mainloop() and gpst_mainloop() timers */
vpninfo->dtls_times.last_rekey = time(&vpninfo->new_dtls_started);
vpninfo->delay_tunnel_reason = "awaiting GPST ESP connection";
}
} else if (esp_keys && esp_v4 && new_ip_info.addr) {
/* We got ESP keys, an IPv4 esp_magic address, and an IPv4 address */
vpninfo->esp_magic_af = AF_INET;
memcpy(vpninfo->esp_magic, &esp_magic, sizeof(esp_magic));
goto setup_esp_keys;
} else if (vpninfo->dtls_state != DTLS_DISABLED)
vpn_progress(vpninfo, PRG_ERR,
_("Did not receive ESP keys and matching gateway in GlobalProtect config; tunnel will be TLS only.\n"));
#endif

free(s);
Expand Down Expand Up @@ -1339,7 +1358,7 @@ int gpst_esp_send_probes(struct openconnect_info *vpninfo)
* 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";
* "monitor\x00\x00pan ha 0123456789:;<=>? !\"#$%&\'()*+,-./\x10\x11\x12\x13\x14\x15\x16\x17";
*
* 2) The ping packets are addressed to the IP supplied in the
* config XML as as <gw-address>. In most cases, this is the
Expand All @@ -1349,11 +1368,14 @@ int gpst_esp_send_probes(struct openconnect_info *vpninfo)
*
* Don't blame me. I didn't design this.
*/
int pktlen, seq;
struct pkt *pkt = malloc(sizeof(*pkt) + sizeof(struct ip) + ICMP_MINLEN + sizeof(magic_ping_payload) + 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);
const int icmplen = ICMP_MINLEN + sizeof(magic_ping_payload);
int plen, seq;

if (vpninfo->esp_magic_af == AF_INET6)
plen = sizeof(struct ip6_hdr) + icmplen;
else
plen = sizeof(struct ip) + icmplen;
struct pkt *pkt = malloc(sizeof(*pkt) + plen + vpninfo->pkt_trailer);
if (!pkt)
return -ENOMEM;

Expand All @@ -1372,29 +1394,98 @@ int gpst_esp_send_probes(struct openconnect_info *vpninfo)
}

for (seq=1; seq <= (vpninfo->dtls_state==DTLS_ESTABLISHED ? 1 : 3); seq++) {
memset(pkt, 0, sizeof(*pkt) + sizeof(*iph) + ICMP_MINLEN + sizeof(magic_ping_payload));
pkt->len = sizeof(struct ip) + ICMP_MINLEN + sizeof(magic_ping_payload);

/* IP Header */
iph->ip_hl = 5;
iph->ip_v = 4;
iph->ip_len = htons(sizeof(*iph) + ICMP_MINLEN + sizeof(magic_ping_payload));
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 = IPPROTO_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_ping_payload, sizeof(magic_ping_payload)); /* required to get gateway to respond */
icmph->icmp_cksum = csum((uint16_t *)icmph, (ICMP_MINLEN+sizeof(magic_ping_payload))/2);

pktlen = construct_esp_packet(vpninfo, pkt, IPPROTO_IPIP);
if (vpninfo->esp_magic_af == AF_INET6) {
memset(pkt, 0, sizeof(*pkt) + plen);
pkt->len = plen;
struct ip6_hdr *iph = (void *)pkt->data;
struct icmp6_hdr *icmph = (void *)(pkt->data + sizeof(*iph));

/* IPv6 Header */
iph->ip6_flow = htonl((6 << 28) + /* version 6 */
(0 << 20) + /* traffic class; match Windows client */
(0 << 0)); /* flow ID; match Windows client */
iph->ip6_nxt = IPPROTO_ICMPV6;
iph->ip6_plen = htons(icmplen);
iph->ip6_hlim = 128; /* what the Windows client uses */
inet_pton(AF_INET6, vpninfo->ip_info.addr6, &iph->ip6_src);
memcpy(&iph->ip6_dst, vpninfo->esp_magic, 16);

/* ICMPv6 echo request */
icmph->icmp6_type = ICMP6_ECHO_REQUEST;
icmph->icmp6_code = 0;
/* Windows client seemingly uses random IDs here but fall back to
* 0x4747 even if only to keep Coverity happy about error checking. */
if (openconnect_random(&icmph->icmp6_data16[0], 2))
icmph->icmp6_data16[0] = htons(0x4747);
icmph->icmp6_data16[1] = htons(seq); /* sequence */

/* required to get gateway to respond */
memcpy(&icmph[1], magic_ping_payload, sizeof(magic_ping_payload));

/*
* IPv6 upper-layer checksums include a pseudo-header
* for IPv6 which contains the source address, the
* destination address, the upper-layer packet length
* and next-header field. See RFC8200 §8.1. The
* checksum is as follows:
*
* checksum 32 bytes of real IPv6 header:
* src addr (16 bytes)
* dst addr (16 bytes)
* 8 bytes more:
* length of ICMPv6 in bytes (be32)
* 3 bytes of 0
* next header byte (IPPROTO_ICMPV6)
* Then the actual ICMPv6 bytes
*/
uint32_t sum = csum_partial((uint16_t *)&iph->ip6_src, 8); /* 8 uint16_t */
sum += csum_partial((uint16_t *)&iph->ip6_dst, 8); /* 8 uint16_t */

/* The easiest way to checksum the following 8-byte
* part of the pseudo-header without horridly violating
* C type aliasing rules is *not* to build it in memory
* at all. We know the length fits in 16 bits so the
* partial checksum of 00 00 LL LL 00 00 00 NH ends up
* being just LLLL + NH.
*/
sum += IPPROTO_ICMPV6;
sum += ICMP_MINLEN + sizeof(magic_ping_payload);

sum += csum_partial((uint16_t *)icmph, icmplen / 2);
icmph->icmp6_cksum = csum_finish(sum);
} else {
memset(pkt, 0, sizeof(*pkt) + plen);
pkt->len = plen;
struct ip *iph = (void *)pkt->data;
struct icmp *icmph = (void *)(pkt->data + sizeof(*iph));
char *pmagic = (void *)(pkt->data + sizeof(*iph) + ICMP_MINLEN);

/* IP Header */
iph->ip_hl = 5;
iph->ip_v = 4;
iph->ip_len = htons(sizeof(*iph) + icmplen);
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 = IPPROTO_ICMP;
iph->ip_src.s_addr = inet_addr(vpninfo->ip_info.addr);
memcpy(&iph->ip_dst.s_addr, vpninfo->esp_magic, 4);
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_ping_payload, sizeof(magic_ping_payload)); /* required to get gateway to respond */
}

if (vpninfo->dtls_state != DTLS_ESTABLISHED) {
vpn_progress(vpninfo, PRG_TRACE, _("ICMPv%d probe packet (seq %d) for GlobalProtect ESP:\n"),
vpninfo->esp_magic_af == AF_INET6 ? 6 : 4, seq);
dump_buf_hex(vpninfo, PRG_TRACE, '>', pkt->data, pkt->len);
}

int pktlen = construct_esp_packet(vpninfo, pkt, vpninfo->esp_magic_af == AF_INET6 ? IPPROTO_IPV6 : IPPROTO_IPIP);
if (pktlen < 0 ||
send(vpninfo->dtls_fd, (void *)&pkt->esp, pktlen, 0) < 0)
vpn_progress(vpninfo, PRG_DEBUG, _("Failed to send ESP probe\n"));
Expand All @@ -1409,14 +1500,24 @@ int gpst_esp_send_probes(struct openconnect_info *vpninfo)

int gpst_esp_catch_probe(struct openconnect_info *vpninfo, struct pkt *pkt)
{
struct ip *iph = (void *)(pkt->data);

return ( pkt->len >= 21 && iph->ip_v==4 /* IPv4 header */
&& iph->ip_p==IPPROTO_ICMP /* IPv4 protocol field == ICMP */
&& iph->ip_src.s_addr == vpninfo->esp_magic /* source == magic address */
&& pkt->len >= (iph->ip_hl<<2) + ICMP_MINLEN + sizeof(magic_ping_payload) /* No short-packet segfaults */
&& pkt->data[iph->ip_hl<<2]==ICMP_ECHOREPLY /* ICMP reply */
&& !memcmp(&pkt->data[(iph->ip_hl<<2) + ICMP_MINLEN], magic_ping_payload, sizeof(magic_ping_payload)) /* Same magic payload in response */
);
if (vpninfo->esp_magic_af == AF_INET6) {
struct ip6_hdr *iph = (void *)(pkt->data);
return ( pkt->len >= 41 && (ntohl(iph->ip6_flow) >> 28)==6 /* IPv6 header */
&& iph->ip6_nxt == IPPROTO_ICMPV6 /* IPv6 next header field = ICMPv6 */
&& !memcmp(&iph->ip6_src, vpninfo->esp_magic, 16) /* source == magic address */
&& pkt->len >= 40 + ICMP_MINLEN + sizeof(magic_ping_payload) /* No short-packet segfaults */
&& pkt->data[40]==ICMP6_ECHO_REPLY /* ICMPv6 reply */
&& !memcmp(&pkt->data[40 + ICMP_MINLEN], magic_ping_payload, sizeof(magic_ping_payload)) /* Same magic payload in response */
);
} else {
struct ip *iph = (void *)(pkt->data);
return ( pkt->len >= 21 && iph->ip_v==4 /* IPv4 header */
&& iph->ip_p==IPPROTO_ICMP /* IPv4 protocol field == ICMP */
&& !memcmp(&iph->ip_src.s_addr, vpninfo->esp_magic, 4) /* source == magic address */
&& pkt->len >= (iph->ip_hl<<2) + ICMP_MINLEN + sizeof(magic_ping_payload) /* No short-packet segfaults */
&& pkt->data[iph->ip_hl<<2]==ICMP_ECHOREPLY /* ICMP reply */
&& !memcmp(&pkt->data[(iph->ip_hl<<2) + ICMP_MINLEN], magic_ping_payload, sizeof(magic_ping_payload)) /* Same magic payload in response */
);
}
}
#endif /* HAVE_ESP */
4 changes: 3 additions & 1 deletion openconnect-internal.h
Expand Up @@ -432,7 +432,9 @@ struct openconnect_info {
int enc_key_len;
int hmac_key_len;
int hmac_out_len;
uint32_t esp_magic; /* GlobalProtect magic ping address (network-endian) */

int esp_magic_af;
unsigned char esp_magic[16]; /* GlobalProtect magic ping address (network-endian) */

struct oc_ppp *ppp;
struct oc_text_buf *ppp_tls_connect_req;
Expand Down

0 comments on commit 5b98b62

Please sign in to comment.