From 5b98b62883216cf9306f06c6b3c9dde81bcfe789 Mon Sep 17 00:00:00 2001 From: Daniel Lenski Date: Thu, 22 Apr 2021 19:49:53 -0700 Subject: [PATCH] GlobalProtect IPv6 ESP support MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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 , 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 `` is present in the config, we *must* send the magic pings via ICMPv6 to that address (rather than ICMPv4 to the ``) 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 Signed-off-by: David Woodhouse --- gpst.c | 221 ++++++++++++++++++++++++++++++----------- openconnect-internal.h | 4 +- 2 files changed, 164 insertions(+), 61 deletions(-) diff --git a/gpst.c b/gpst.c index f8ab8441..86f357c6 100644 --- a/gpst.c +++ b/gpst.c @@ -45,6 +45,8 @@ #include #include #include +#include +#include #endif #if defined(__linux__) @@ -347,10 +349,13 @@ 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; @@ -358,8 +363,7 @@ static int gpst_parse_config_xml(struct openconnect_info *vpninfo, xmlNode *xml_ 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; @@ -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 , 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 address (%s) differs from external\n" @@ -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")); @@ -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 .\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); @@ -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 . In most cases, this is the @@ -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; @@ -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")); @@ -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 */ diff --git a/openconnect-internal.h b/openconnect-internal.h index a6bffae8..9552c18b 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -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; -- 2.49.0