From b795ff35255e5dcd6b49a4692d42330e78cc955a Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Fri, 7 Jun 2019 20:32:07 +0100 Subject: [PATCH] Add Pulse Connect Secure support Signed-off-by: David Woodhouse --- Makefile.am | 15 +- gnutls.c | 60 ++ http.c | 4 +- library.c | 26 +- oncp.c | 1 + openconnect-internal.h | 29 + openssl-dtls.c | 2 +- openssl.c | 78 ++ pulse.c | 2039 +++++++++++++++++++++++++++++++++++++++ www/Makefile.am | 2 +- www/changelog.xml | 1 + www/menu2-protocols.xml | 1 + www/pulse.xml | 51 + 13 files changed, 2297 insertions(+), 12 deletions(-) create mode 100644 pulse.c create mode 100644 www/pulse.xml diff --git a/Makefile.am b/Makefile.am index 869cc68c..f81eddb6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -27,30 +27,33 @@ openconnect_LDADD = libopenconnect.la $(SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIB if OPENCONNECT_WIN32 openconnect_SOURCES += openconnect.rc endif -library_srcs = ssl.c http.c http-auth.c auth-common.c library.c compat.c lzs.c mainloop.c script.c ntlm.c digest.c +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 openconnect-internal.h lib_srcs_cisco = auth.c cstp.c lib_srcs_juniper = oncp.c lzo.c auth-juniper.c +lib_srcs_pulse = pulse.c lib_srcs_globalprotect = gpst.c auth-globalprotect.c +lib_srcs_oath = oath.c + +library_srcs += $(lib_srcs_juniper) $(lib_srcs_cisco) $(lib_srcs_oath) \ + $(lib_srcs_globalprotect) $(lib_srcs_pulse) + lib_srcs_gnutls = gnutls.c gnutls_tpm.c gnutls_tpm2.c lib_srcs_openssl = openssl.c openssl-pkcs11.c lib_srcs_win32 = tun-win32.c sspi.c lib_srcs_posix = tun.c lib_srcs_gssapi = gssapi.c lib_srcs_iconv = iconv.c -lib_srcs_oath = oath.c lib_srcs_yubikey = yubikey.c 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) $(lib_srcs_globalprotect) \ - gnutls-esp.c gnutls-dtls.c openssl-esp.c openssl-dtls.c \ +POTFILES = $(openconnect_SOURCES) gnutls-esp.c gnutls-dtls.c openssl-esp.c openssl-dtls.c \ $(lib_srcs_esp) $(lib_srcs_dtls) gnutls_tpm2_esys.c gnutls_tpm2_ibm.c \ $(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 + $(lib_srcs_yubikey) $(lib_srcs_stoken) -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/gnutls.c b/gnutls.c index 86f17755..4f915d62 100644 --- a/gnutls.c +++ b/gnutls.c @@ -2597,3 +2597,63 @@ int hotp_hmac(struct openconnect_info *vpninfo, const void *challenge) hpos = hash[hpos] & 15; return load_be32(&hash[hpos]) & 0x7fffffff; } + + +static int ttls_pull_timeout_func(gnutls_transport_ptr_t t, unsigned int ms) +{ + struct openconnect_info *vpninfo = t; + + vpn_progress(vpninfo, PRG_TRACE, _("ttls_pull_timeout_func %dms\n"), ms); + return 0; +} + +static ssize_t ttls_pull_func(gnutls_transport_ptr_t t, void *buf, size_t len) +{ + int ret = pulse_eap_ttls_recv(t, buf, len); + if (ret >= 0) + return ret; + else + return GNUTLS_E_PULL_ERROR; +} + +static ssize_t ttls_push_func(gnutls_transport_ptr_t t, const void *buf, size_t len) +{ + int ret = pulse_eap_ttls_send(t, buf, len); + if (ret >= 0) + return ret; + else + return GNUTLS_E_PUSH_ERROR; +} + +void *establish_eap_ttls(struct openconnect_info *vpninfo) +{ + gnutls_session_t ttls_sess = NULL; + int err; + + gnutls_init(&ttls_sess, GNUTLS_CLIENT); + gnutls_session_set_ptr(ttls_sess, (void *) vpninfo); + gnutls_transport_set_ptr(ttls_sess, (void *) vpninfo); + + gnutls_transport_set_push_function(ttls_sess, ttls_push_func); + gnutls_transport_set_pull_function(ttls_sess, ttls_pull_func); + gnutls_transport_set_pull_timeout_function(ttls_sess, ttls_pull_timeout_func); + + gnutls_credentials_set(ttls_sess, GNUTLS_CRD_CERTIFICATE, vpninfo->https_cred); + + err = gnutls_priority_set_direct(ttls_sess, + vpninfo->gnutls_prio, NULL); + + err = gnutls_handshake(ttls_sess); + if (!err) { + vpn_progress(vpninfo, PRG_TRACE, + _("Established EAP-TTLS session\n")); + return ttls_sess; + } + gnutls_deinit(ttls_sess); + return NULL; +} + +void destroy_eap_ttls(struct openconnect_info *vpninfo, void *sess) +{ + gnutls_deinit(sess); +} diff --git a/http.c b/http.c index 43499505..8495a480 100644 --- a/http.c +++ b/http.c @@ -507,8 +507,8 @@ int process_http_response(struct openconnect_info *vpninfo, int connect, if (result == 100) goto cont; - /* On successful CONNECT, there is no body. Return success */ - if (connect && result == 200) + /* On successful CONNECT or upgrade, there is no body. Return success */ + if (connect && (result == 200 || result == 101)) return result; /* Now the body, if there is one */ diff --git a/library.c b/library.c index c08da876..629e5e1d 100644 --- a/library.c +++ b/library.c @@ -127,7 +127,7 @@ const struct vpn_proto openconnect_protos[] = { }, { .name = "nc", .pretty_name = N_("Juniper Network Connect"), - .description = N_("Compatible with Juniper Network Connect / Pulse Secure SSL VPN"), + .description = N_("Compatible with Juniper Network Connect"), .flags = OC_PROTO_PROXY | OC_PROTO_CSD | OC_PROTO_AUTH_CERT | OC_PROTO_AUTH_OTP, .vpn_close_session = oncp_bye, .tcp_connect = oncp_connect, @@ -161,6 +161,25 @@ const struct vpn_proto openconnect_protos[] = { .udp_shutdown = esp_shutdown, .udp_send_probes = gpst_esp_send_probes, .udp_catch_probe = gpst_esp_catch_probe, +#endif + }, { + .name = "pulse", + .pretty_name = N_("Pulse Connect Secure"), + .description = N_("Compatible with Pulse Connect Secure SSL VPN"), + .flags = OC_PROTO_PROXY, + .vpn_close_session = pulse_bye, + .tcp_connect = pulse_connect, + .tcp_mainloop = pulse_mainloop, + .add_http_headers = http_common_headers, + .obtain_cookie = pulse_obtain_cookie, + .udp_protocol = "ESP", +#ifdef HAVE_ESPx + .udp_setup = esp_setup, + .udp_mainloop = esp_mainloop, + .udp_close = esp_close, + .udp_shutdown = esp_shutdown, + .udp_send_probes = pulse_esp_send_probes, + .udp_catch_probe = pulse_esp_catch_probe, #endif }, { /* NULL */ } @@ -363,7 +382,10 @@ void openconnect_vpninfo_free(struct openconnect_info *vpninfo) free(vpninfo->ifname); free(vpninfo->dtls_cipher); free(vpninfo->peer_cert_hash); -#ifdef OPENCONNECT_GNUTLS +#if defined(OPENCONNECT_OPENSSL) + if (vpninfo->ttls_bio_meth) + BIO_meth_free(vpninfo->ttls_bio_meth); +#elif defined(OPENCONNECT_GNUTLS) gnutls_free(vpninfo->cstp_cipher); /* In OpenSSL this is const */ #ifdef HAVE_DTLS gnutls_free(vpninfo->gnutls_dtls_cipher); diff --git a/oncp.c b/oncp.c index 24b1e56c..656118fa 100644 --- a/oncp.c +++ b/oncp.c @@ -1126,6 +1126,7 @@ int oncp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable) /* Don't free the 'special' packets */ if (vpninfo->current_ssl_pkt == vpninfo->deflate_pkt) { free(vpninfo->pending_deflated_pkt); + vpninfo->pending_deflated_pkt = NULL; } else if (vpninfo->current_ssl_pkt == &esp_enable_pkt) { /* Only set the ESP state to connected and actually start sending packets on it once the enable message has been diff --git a/openconnect-internal.h b/openconnect-internal.h index b7ac3ba9..75d93a79 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -137,6 +137,13 @@ struct pkt { unsigned char pad[8]; unsigned char hdr[16]; } gpst; + struct { + unsigned char pad[8]; + uint32_t vendor; + uint32_t type; + uint32_t len; + uint32_t ident; + } pulse; }; unsigned char data[]; }; @@ -500,8 +507,10 @@ struct openconnect_info { X509 *cert_x509; SSL_CTX *https_ctx; SSL *https_ssl; + BIO_METHOD *ttls_bio_meth; #elif defined(OPENCONNECT_GNUTLS) gnutls_session_t https_sess; + gnutls_session_t eap_ttls_sess; gnutls_certificate_credentials_t https_cred; gnutls_psk_client_credentials_t psk_cred; char local_cert_md5[MD5_SIZE * 2 + 1]; /* For CSD */ @@ -513,6 +522,12 @@ struct openconnect_info { struct oc_tpm2_ctx *tpm2; #endif #endif /* OPENCONNECT_GNUTLS */ + struct oc_text_buf *ttls_pushbuf; + uint8_t ttls_eap_ident; + unsigned char *ttls_recvbuf; + int ttls_recvpos; + int ttls_recvlen; + struct pin_cache *pin_cache; struct keepalive_info ssl_times; int owe_ssl_dpd_response; @@ -560,6 +575,8 @@ struct openconnect_info { unsigned char dtls_app_id[32]; unsigned dtls_app_id_size; + uint32_t ift_seq; + int cisco_dtls12; char *dtls_cipher; char *vpnc_script; @@ -830,6 +847,9 @@ int dtls_try_handshake(struct openconnect_info *vpninfo); unsigned dtls_set_mtu(struct openconnect_info *vpninfo, unsigned mtu); void dtls_ssl_free(struct openconnect_info *vpninfo); +void *establish_eap_ttls(struct openconnect_info *vpninfo); +void destroy_eap_ttls(struct openconnect_info *vpninfo, void *sess); + /* dtls.c */ int dtls_setup(struct openconnect_info *vpninfo, int dtls_attempt_period); int dtls_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable); @@ -863,6 +883,15 @@ void oncp_esp_close(struct openconnect_info *vpninfo); int oncp_esp_send_probes(struct openconnect_info *vpninfo); int oncp_esp_catch_probe(struct openconnect_info *vpninfo, struct pkt *pkt); +/* pulse.c */ +int pulse_obtain_cookie(struct openconnect_info *vpninfo); +void pulse_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf); +int pulse_connect(struct openconnect_info *vpninfo); +int pulse_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable); +int pulse_bye(struct openconnect_info *vpninfo, const char *reason); +int pulse_eap_ttls_send(struct openconnect_info *vpninfo, const void *data, int len); +int pulse_eap_ttls_recv(struct openconnect_info *vpninfo, void *data, int len); + /* auth-globalprotect.c */ int gpst_obtain_cookie(struct openconnect_info *vpninfo); void gpst_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf); diff --git a/openssl-dtls.c b/openssl-dtls.c index 8eab13b3..5086440f 100644 --- a/openssl-dtls.c +++ b/openssl-dtls.c @@ -524,7 +524,7 @@ int dtls_try_handshake(struct openconnect_info *vpninfo) /* For PSK-NEGOTIATE, we have to determine the tunnel MTU * for ourselves based on the base MTU */ int data_mtu = vpninfo->cstp_basemtu; - if (vpninfo->peer_addr->sa_family == IPPROTO_IPV6) + if (vpninfo->peer_addr->sa_family == AF_INET6) data_mtu -= 40; /* IPv6 header */ else data_mtu -= 20; /* Legacy IP header */ diff --git a/openssl.c b/openssl.c index 0cc67792..2b1f07bd 100644 --- a/openssl.c +++ b/openssl.c @@ -1978,3 +1978,81 @@ int hotp_hmac(struct openconnect_info *vpninfo, const void *challenge) hashlen = hash[hashlen - 1] & 15; return load_be32(&hash[hashlen]) & 0x7fffffff; } + +static int ttls_push_func(BIO *b, const char *buf, int len) +{ + struct openconnect_info *vpninfo = BIO_get_data(b); + int ret = pulse_eap_ttls_send(vpninfo, buf, len); + if (ret >= 0) + return ret; + + return 0; +} + +static int ttls_pull_func(BIO *b, char *buf, int len) +{ + struct openconnect_info *vpninfo = BIO_get_data(b); + int ret = pulse_eap_ttls_recv(vpninfo, buf, len); + if (ret >= 0) + return ret; + + return 0; +} + +static long ttls_ctrl_func(BIO *b, int cmd, long larg, void *iarg) +{ + switch(cmd) { + case BIO_CTRL_FLUSH: + return 1; + default: + return 0; + } +} + +void *establish_eap_ttls(struct openconnect_info *vpninfo) +{ + SSL *ttls_ssl = NULL; + BIO *bio; + int err; + + + if (!vpninfo->ttls_bio_meth) { + vpninfo->ttls_bio_meth = BIO_meth_new(BIO_get_new_index(), "EAP-TTLS"); + BIO_meth_set_write(vpninfo->ttls_bio_meth, ttls_push_func); + BIO_meth_set_read(vpninfo->ttls_bio_meth, ttls_pull_func); + BIO_meth_set_ctrl(vpninfo->ttls_bio_meth, ttls_ctrl_func); + } + + bio = BIO_new(vpninfo->ttls_bio_meth); + BIO_set_data(bio, vpninfo); + BIO_set_init(bio, 1); + ttls_ssl = SSL_new(vpninfo->https_ctx); + workaround_openssl_certchain_bug(vpninfo, ttls_ssl); + + SSL_set_bio(ttls_ssl, bio, bio); + + SSL_set_verify(ttls_ssl, SSL_VERIFY_PEER, NULL); + + vpn_progress(vpninfo, PRG_INFO, _("EAP-TTLS negotiation with %s\n"), + vpninfo->hostname); + + err = SSL_connect(ttls_ssl); + if (err == 1) { + vpn_progress(vpninfo, PRG_TRACE, + _("Established EAP-TTLS session\n")); + return ttls_ssl; + } + + err = SSL_get_error(ttls_ssl, err); + vpn_progress(vpninfo, PRG_ERR, _("EAP-TTLS connection failure %d\n"), err); + openconnect_report_ssl_errors(vpninfo); + SSL_free(ttls_ssl); + return NULL; +} + +void destroy_eap_ttls(struct openconnect_info *vpninfo, void *ttls) +{ + SSL_free(ttls); + /* Leave the BIO_METH for now. It may get reused and we don't want to + * have to call BIO_get_new_index() more times than is necessary */ +} diff --git a/pulse.c b/pulse.c new file mode 100644 index 00000000..fbd351b4 --- /dev/null +++ b/pulse.c @@ -0,0 +1,2039 @@ +/* + * OpenConnect (SSL + DTLS) VPN client + * + * Copyright © 2019 David Woodhouse. + * + * Author: David Woodhouse + * + * 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "openconnect-internal.h" + +#define VENDOR_JUNIPER 0xa4c +#define VENDOR_JUNIPER2 0x583 +#define VENDOR_TCG 0x5597 + +#define IFT_VERSION_REQUEST 1 +#define IFT_VERSION_RESPONSE 2 +#define IFT_CLIENT_AUTH_REQUEST 3 +#define IFT_CLIENT_AUTH_SELECTION 4 +#define IFT_CLIENT_AUTH_CHALLENGE 5 +#define IFT_CLIENT_AUTH_RESPONSE 6 +#define IFT_CLIENT_AUTH_SUCCESS 7 + +/* IF-T/TLS v1 authentication messages all start + * with the Auth Type Vendor (Juniper) + Type (1) */ +#define JUNIPER_1 ((VENDOR_JUNIPER << 8) | 1) + +#define AVP_VENDOR 0x80 +#define AVP_MANDATORY 0x40 + +#define EAP_REQUEST 1 +#define EAP_RESPONSE 2 +#define EAP_SUCCESS 3 +#define EAP_FAILURE 4 + +#define EAP_TYPE_IDENTITY 1 +#define EAP_TYPE_TTLS 0x15 +#define EAP_TYPE_EXPANDED 0xfe + +#define EXPANDED_JUNIPER ((EAP_TYPE_EXPANDED << 24) | VENDOR_JUNIPER) + +#define AVP_CODE_EAP_MESSAGE 79 + +#if defined(OPENCONNECT_OPENSSL) +#define TTLS_SEND SSL_write +#define TTLS_RECV SSL_read +#elif defined(OPENCONNECT_GNUTLS) +#define TTLS_SEND gnutls_record_send +#define TTLS_RECV gnutls_record_recv +#endif + +static void buf_append_be16(struct oc_text_buf *buf, uint16_t val) +{ + unsigned char b[2]; + + store_be16(b, val); + + buf_append_bytes(buf, b, 2); +} + +static void buf_append_be32(struct oc_text_buf *buf, uint32_t val) +{ + unsigned char b[4]; + + store_be32(b, val); + + buf_append_bytes(buf, b, 4); +} + +static void buf_append_ift_hdr(struct oc_text_buf *buf, uint32_t vendor, uint32_t type) +{ + uint32_t b[4]; + + store_be32(&b[0], vendor); + store_be32(&b[1], type); + b[2] = 0; /* Length will be filled in later. */ + b[3] = 0; + buf_append_bytes(buf, b, 16); +} + +/* Append EAP header, using VENDOR_JUNIPER and the given subtype if + * the main type is EAP_TYPE_EXPANDED */ +static int buf_append_eap_hdr(struct oc_text_buf *buf, uint8_t code, uint8_t ident, uint8_t type, + uint32_t subtype) +{ + unsigned char b[24]; + int len_ofs = -1; + + if (!buf_error(buf)) + len_ofs = buf->pos; + + b[0] = code; + b[1] = ident; + b[2] = b[3] = 0; /* Length is filled in later. */ + if (type == EAP_TYPE_EXPANDED) { + store_be32(b + 4, EXPANDED_JUNIPER); + store_be32(b + 8, subtype); + buf_append_bytes(buf, b, 12); + } else { + b[4] = type; + buf_append_bytes(buf, b, 5); + } + return len_ofs; +} + +/* For an IF-T/TLS auth frame containing the Juniper/1 Auth Type, + * the EAP header is at offset 0x14. Fill in the length field, + * based on the current length of the buf */ +static void buf_fill_eap_len(struct oc_text_buf *buf, int ofs) +{ + /* EAP length word is always at 0x16, and counts bytes from 0x14 */ + if (ofs >= 0 && !buf_error(buf) && buf->pos > ofs + 8) + store_be16(buf->data + ofs + 2, buf->pos - ofs); +} + +static void buf_append_avp(struct oc_text_buf *buf, uint32_t type, const void *bytes, int len) +{ + buf_append_be32(buf, type); + buf_append_be16(buf, 0x8000); + buf_append_be16(buf, len + 12); + buf_append_be32(buf, VENDOR_JUNIPER2); + buf_append_bytes(buf, bytes, len); + if (len & 3) { + uint32_t pad = 0; + buf_append_bytes(buf, &pad, 4 - ( len & 3 )); + } +} + +static void buf_append_avp_string(struct oc_text_buf *buf, uint32_t type, const char *str) +{ + buf_append_avp(buf, type, str, strlen(str)); +} + +static int valid_ift_success(unsigned char *bytes, int len) +{ + if (len != 0x18 || (load_be32(bytes) & 0xffffff) != VENDOR_TCG || + load_be32(bytes + 4) != IFT_CLIENT_AUTH_SUCCESS || + load_be32(bytes + 8) != len || + load_be32(bytes + 0x10) != JUNIPER_1 || + bytes[0x14] != EAP_SUCCESS || + load_be16(bytes + 0x16) != len - 0x14) + return 0; + + return 1; +} + +/* Check for a valid IF-T/TLS auth challenge of the Juniper/1 Auth Type */ +static int valid_ift_auth(unsigned char *bytes, int len) +{ + if (len < 0x14 || (load_be32(bytes) & 0xffffff) != VENDOR_TCG || + load_be32(bytes + 4) != IFT_CLIENT_AUTH_CHALLENGE || + load_be32(bytes + 8) != len || + load_be32(bytes + 0x10) != JUNIPER_1) + return 0; + + return 1; +} + + +static int valid_ift_auth_eap(unsigned char *bytes, int len) +{ + /* Needs to be a valid IF-T/TLS auth challenge with the + * expect Auth Type, *and* the payload has to be a valid + * EAP request with correct length field. */ + if (!valid_ift_auth(bytes, len) || len < 0x19 || + bytes[0x14] != EAP_REQUEST || + load_be16(bytes + 0x16) != len - 0x14) + return 0; + + return 1; +} + +static int valid_ift_auth_eap_exj1(unsigned char *bytes, int len) +{ + /* Also needs to be the Expanded Juniper/1 EAP Type */ + if (!valid_ift_auth_eap(bytes, len) || len < 0x20 || + load_be32(bytes + 0x18) != EXPANDED_JUNIPER || + load_be32(bytes + 0x1c) != 1) + return 0; + + return 1; +} + +/* 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. */ + +static const char *add_option(struct openconnect_info *vpninfo, const char *opt, + const char *val, int val_len) +{ + struct oc_vpn_option *new = malloc(sizeof(*new)); + if (!new) + return NULL; + + new->option = strdup(opt); + if (!new->option) { + free(new); + return NULL; + } + if (val_len >= 0) + new->value = strndup(val, val_len); + else + new->value = strdup(val); + if (!new->value) { + free(new->option); + free(new); + return NULL; + } + new->next = vpninfo->cstp_options; + vpninfo->cstp_options = new; + + return new->value; +} + +static int process_attr(struct openconnect_info *vpninfo, uint16_t type, + unsigned char *data, int attrlen) +{ + char buf[80]; + int i; + + switch (type) { + + case 0x0001: + if (attrlen != 4) + goto badlen; + snprintf(buf, sizeof(buf), "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + + vpn_progress(vpninfo, PRG_DEBUG, _("Received internal Legacy IP address %s\n"), buf); + vpninfo->ip_info.addr = add_option(vpninfo, "ipaddr", buf, -1); + break; + + case 0x0002: + if (attrlen != 4) + goto badlen; + snprintf(buf, sizeof(buf), "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + + vpn_progress(vpninfo, PRG_DEBUG, _("Received netmask %s\n"), buf); + vpninfo->ip_info.netmask = add_option(vpninfo, "netmask", buf, -1); + break; + + case 0x0003: + if (attrlen != 4) + goto badlen; + snprintf(buf, sizeof(buf), "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + + vpn_progress(vpninfo, PRG_DEBUG, _("Received DNS server %s\n"), buf); + + for (i = 0; i < 3; i++) { + if (!vpninfo->ip_info.dns[i]) { + vpninfo->ip_info.dns[i] = add_option(vpninfo, "DNS", buf, -1); + break; + } + } + break; + + case 0x0004: + if (attrlen != 4) + goto badlen; + snprintf(buf, sizeof(buf), "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + + vpn_progress(vpninfo, PRG_DEBUG, _("Received WINS server %s\n"), buf); + + for (i = 0; i < 3; i++) { + if (!vpninfo->ip_info.nbns[i]) { + vpninfo->ip_info.nbns[i] = add_option(vpninfo, "WINS", buf, -1); + break; + } + } + break; + + case 0x0008: + if (attrlen != 17) + goto badlen; + if (!inet_ntop(AF_INET6, data, buf, sizeof(buf))) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to handle IPv6 address\n")); + return -EINVAL; + } + i = strlen(buf); + snprintf(buf + i, sizeof(buf) - i, "/%d", data[16]); + vpn_progress(vpninfo, PRG_DEBUG, _("Received internal IPv6 address %s\n"), buf); + vpninfo->ip_info.addr6 = add_option(vpninfo, "ip6addr", buf, -1); + break; + + case 0x4005: + if (attrlen != 4) { + badlen: + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected length %d for attr 0x%x\n"), + attrlen, type); + return -EINVAL; + } + vpninfo->ip_info.mtu = load_be32(data); + vpn_progress(vpninfo, PRG_DEBUG, + _("Received MTU %d from server\n"), + vpninfo->ip_info.mtu); + break; + + case 0x4006: + if (!attrlen) + goto badlen; + if (!data[attrlen-1]) + attrlen--; + vpn_progress(vpninfo, PRG_DEBUG, _("Received DNS search domain %.*s\n"), + attrlen, (char *)data); + vpninfo->ip_info.domain = add_option(vpninfo, "search", (char *)data, attrlen); + if (vpninfo->ip_info.domain) { + char *p = (char *)vpninfo->ip_info.domain; + while ((p = strchr(p, ','))) + *p = ' '; + } + break; + + case 0x400b: + if (attrlen != 4) + goto badlen; + snprintf(buf, sizeof(buf), "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + + vpn_progress(vpninfo, PRG_DEBUG, _("Received internal gateway address %s\n"), buf); + /* Hm, what are we supposed to do with this? It's a tunnel; + having a gateway is meaningless. */ + add_option(vpninfo, "ipaddr", buf, -1); + break; + + case 0x4010: { + const char *enctype; + uint16_t val; + + if (attrlen != 2) + goto badlen; + val = load_be16(data); + if (val == ENC_AES_128_CBC) { + enctype = "AES-128"; + vpninfo->enc_key_len = 16; + } else if (val == ENC_AES_256_CBC) { + enctype = "AES-256"; + vpninfo->enc_key_len = 32; + } else + enctype = "unknown"; + vpn_progress(vpninfo, PRG_DEBUG, _("ESP encryption: 0x%04x (%s)\n"), + val, enctype); + vpninfo->esp_enc = val; + break; + } + + case 0x4011: { + const char *mactype; + uint16_t val; + + if (attrlen != 2) + goto badlen; + val = load_be16(data); + if (val == HMAC_MD5) { + mactype = "MD5"; + vpninfo->hmac_key_len = 16; + } else if (val == HMAC_SHA1) { + mactype = "SHA1"; + vpninfo->hmac_key_len = 20; + } else + mactype = "unknown"; + vpn_progress(vpninfo, PRG_DEBUG, _("ESP HMAC: 0x%04x (%s)\n"), + val, mactype); + vpninfo->esp_hmac = val; + break; + } + + case 0x4012: + if (attrlen != 4) + goto badlen; + vpninfo->esp_lifetime_seconds = load_be32(data); + vpn_progress(vpninfo, PRG_DEBUG, _("ESP key lifetime: %u seconds\n"), + vpninfo->esp_lifetime_seconds); + break; + + case 0x4013: + if (attrlen != 4) + goto badlen; + vpninfo->esp_lifetime_bytes = load_be32(data); + vpn_progress(vpninfo, PRG_DEBUG, _("ESP key lifetime: %u bytes\n"), + vpninfo->esp_lifetime_bytes); + break; + + case 0x4014: + if (attrlen != 4) + goto badlen; + vpninfo->esp_replay_protect = load_be32(data); + vpn_progress(vpninfo, PRG_DEBUG, _("ESP replay protection: %d\n"), + load_be32(data)); + break; + + case 0x4016: + if (attrlen != 2) + goto badlen; + i = load_be16(data); + udp_sockaddr(vpninfo, i); + vpn_progress(vpninfo, PRG_DEBUG, _("ESP port: %d\n"), i); + break; + + case 0x4017: + if (attrlen != 4) + goto badlen; + vpninfo->esp_ssl_fallback = load_be32(data); + vpn_progress(vpninfo, PRG_DEBUG, _("ESP to SSL fallback: %u seconds\n"), + vpninfo->esp_ssl_fallback); + break; + + case 0x401a: + if (attrlen != 1) + goto badlen; + /* Amusingly, this isn't enforced. It's client-only */ + vpn_progress(vpninfo, PRG_DEBUG, _("ESP only: %d\n"), + data[0]); + break; +#if 0 + case GRP_ATTR(7, 1): + if (attrlen != 4) + goto badlen; + memcpy(&vpninfo->esp_out.spi, data, 4); + vpn_progress(vpninfo, PRG_DEBUG, _("ESP SPI (outbound): %x\n"), + load_be32(data)); + break; + + case GRP_ATTR(7, 2): + if (attrlen != 0x40) + goto badlen; + /* data contains enc_key and hmac_key concatenated */ + memcpy(vpninfo->esp_out.enc_key, data, 0x40); + vpn_progress(vpninfo, PRG_DEBUG, _("%d bytes of ESP secrets\n"), + attrlen); + break; +#endif + /* 0x4022: disable proxy + 0x400a: preserve proxy + 0x4008: proxy (string) + 0x4000: disconnect when routes changed + 0x4015: tos copy + 0x4001: tunnel routes take precedence + 0x401f: tunnel routes with subnet access (also 4001 set) + 0x4020: Enforce IPv4 + 0x4021: Enforce IPv6 + 0x401e: Server IPv6 address + 0x000f: IPv6 netmask? + */ + + default: + buf[0] = 0; + for (i=0; i < 16 && i < attrlen; i++) + sprintf(buf + strlen(buf), " %02x", data[i]); + if (attrlen > 16) + sprintf(buf + strlen(buf), "..."); + + vpn_progress(vpninfo, PRG_DEBUG, + _("Unknown attr 0x%x len %d:%s\n"), + type, attrlen, buf); + } + return 0; +} + +static int recv_ift_packet(struct openconnect_info *vpninfo, void *buf, int len) +{ + int ret = vpninfo->ssl_read(vpninfo, buf, len); + if (ret > 0 && vpninfo->dump_http_traffic) { + vpn_progress(vpninfo, PRG_TRACE, + _("Read %d bytes of IF-T/TLS record\n"), ret); + dump_buf_hex(vpninfo, PRG_TRACE, '<', buf, ret); + } + return ret; +} + +static int send_ift_packet(struct openconnect_info *vpninfo, struct oc_text_buf *buf) +{ + int ret; + + if (buf_error(buf) || buf->pos < 16) { + vpn_progress(vpninfo, PRG_ERR, + _("Error creating IF-T packet\n")); + return buf_error(buf); + } + + /* Fill in the length word in the header with the full length of the buffer. + * Also populate the sequence number. */ + store_be32(buf->data + 8, buf->pos); + store_be32(buf->data + 12, vpninfo->ift_seq++); + + dump_buf_hex(vpninfo, PRG_DEBUG, '>', (void *)buf->data, buf->pos); + ret = vpninfo->ssl_write(vpninfo, buf->data, buf->pos); + if (ret != buf->pos) { + if (ret >= 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Short write to IF-T/TLS\n")); + ret = -EIO; + } + return ret; + } + return 0; +} + +/* We create packets with IF-T/TLS headers prepended because that's the + * larger header. In the case where they need to be sent over EAP-TTLS, + * convert the header to the EAP-Message AVP instead. */ +static int send_eap_packet(struct openconnect_info *vpninfo, void *ttls, struct oc_text_buf *buf) +{ + int ret; + + if (buf_error(buf) || buf->pos < 16) { + vpn_progress(vpninfo, PRG_ERR, + _("Error creating EAP packet\n")); + return buf_error(buf); + } + + if (!ttls) + return send_ift_packet(vpninfo, buf); + + /* AVP EAP-Message header */ + store_be32(buf->data + 0x0c, AVP_CODE_EAP_MESSAGE); + store_be32(buf->data + 0x10, buf->pos - 0xc); + dump_buf_hex(vpninfo, PRG_DEBUG, '.', (void *)(buf->data + 0x0c), buf->pos - 0x0c); + ret = TTLS_SEND(ttls, buf->data + 0x0c, buf->pos - 0x0c); + if (ret != buf->pos - 0x0c) + return -EIO; + return 0; +} + + +/* + * Using the given buffer, receive and validate an EAP request of the + * Expanded Juniper/1 type, either natively over IF-T/TLS or by EAP-TTLS + * over IF-T/TLS. Return a pointer to the EAP header, with its length and + * type already validated. + */ +static void *recv_eap_packet(struct openconnect_info *vpninfo, void *ttls, void *buf, int len) +{ + unsigned char *cbuf = buf; + int ret; + + if (!ttls) { + ret = recv_ift_packet(vpninfo, buf, len); + if (ret < 0) + return NULL; + if (!valid_ift_auth_eap_exj1(buf, ret)) { + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected IF-T/TLS authentication challenge:\n")); + dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)buf, ret); + return NULL; + } + return cbuf + 0x14; + } else { + ret = TTLS_RECV(ttls, buf, len); + if (ret <= 8) + return NULL; + if (/* EAP-Message AVP */ + load_be32(cbuf) != AVP_CODE_EAP_MESSAGE || + /* Ignore the mandatory bit */ + (load_be32(cbuf+0x04) & ~0x40000000) != ret || + cbuf[0x08] != EAP_REQUEST || + load_be16(cbuf+0x0a) != ret - 8 || + load_be32(cbuf+0x0c) != EXPANDED_JUNIPER || + load_be32(cbuf+0x10) != 1) { + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected EAP-TTLS payload:\n")); + dump_buf_hex(vpninfo, PRG_ERR, '<', buf, ret); + return NULL; + } + return cbuf + 0x08; + } +} + +static void dump_avp(struct openconnect_info *vpninfo, uint8_t flags, + uint32_t vendor, uint32_t code, void *p, int len) +{ + struct oc_text_buf *buf = buf_alloc(); + const char *pretty; + int i; + + for (i = 0; i < len; i++) + if (!isprint( ((char *)p)[i] )) + break; + + if (i == len) { + buf_append(buf, " '"); + buf_append_bytes(buf, p, len); + buf_append(buf, "'"); + } else { + for (i = 0; i < len; i++) + buf_append(buf, " %02x", ((unsigned char *)p)[i]); + } + if (buf_error(buf)) + pretty = " "; + else + pretty = buf->data; + + if (flags & AVP_VENDOR) + vpn_progress(vpninfo, PRG_TRACE, _("AVP 0x%x/0x%x:%s\n"), vendor, code, pretty); + else + vpn_progress(vpninfo, PRG_TRACE, _("AVP %d:%s\n"), code, pretty); + buf_free(buf); +} + +/* RFC5281 §10 */ +static int parse_avp(struct openconnect_info *vpninfo, void **pkt, int *pkt_len, + void **avp_out, int *avp_len, uint8_t *avp_flags, + uint32_t *avp_vendor, uint32_t *avp_code) +{ + unsigned char *p = *pkt; + int l = *pkt_len; + uint32_t code, len, vendor = 0; + uint8_t flags; + + if (l < 8) + return -EINVAL; + + code = load_be32(p); + len = load_be32(p + 4) & 0xffffff; + flags = p[4]; + + if (len > l || len < 8) + return -EINVAL; + + p += 8; + l -= 8; + len -= 8; + + /* Vendor field is optional. */ + if (flags & AVP_VENDOR) { + if (l < 4) + return -EINVAL; + vendor = load_be32(p); + p += 4; + l -= 4; + len -= 4; + } + + *avp_vendor = vendor; + *avp_flags = flags; + *avp_code = code; + *avp_out = p; + *avp_len = len; + + /* Now set up packet pointer and length for next AVP, + * aligned to 4 octets (if they exist in the packet) */ + len = (len + 3) & ~3; + if (len > l) + len = l; + *pkt = p + len; + *pkt_len = l - len; + + return 0; +} + + +static int pulse_request_realm_entry(struct openconnect_info *vpninfo, struct oc_text_buf *reqbuf) +{ + struct oc_auth_form f; + struct oc_form_opt o; + int ret; + + memset(&f, 0, sizeof(f)); + memset(&o, 0, sizeof(o)); + f.auth_id = (char *)"pulse_realm_entry"; + f.opts = &o; + + f.message = _("Enter Pulse user realm:"); + + o.next = NULL; + o.type = OC_FORM_OPT_TEXT; + o.name = (char *)"realm"; + o.label = (char *)_("Realm:"); + + ret = process_auth_form(vpninfo, &f); + if (ret) + return ret; + + if (o._value) { + buf_append_avp_string(reqbuf, 0xd50, o._value); + free_pass(&o._value); + return 0; + } + + return -EINVAL; +} + +static int pulse_request_realm_choice(struct openconnect_info *vpninfo, struct oc_text_buf *reqbuf, + int realms, unsigned char *eap) +{ + uint8_t avp_flags; + uint32_t avp_code; + uint32_t avp_vendor; + int avp_len; + void *avp_p; + struct oc_auth_form f; + struct oc_form_opt_select o; + int i = 0, ret; + void *p; + int l; + + l = load_be16(eap + 2) - 0x0c; /* Already validated */ + p = eap + 0x0c; + + memset(&f, 0, sizeof(f)); + memset(&o, 0, sizeof(o)); + f.auth_id = (char *)"pulse_realm_choice"; + f.opts = &o.form; + f.authgroup_opt = &o; + f.authgroup_selection = 1; + f.message = _("Choose Pulse user realm:"); + + o.form.next = NULL; + o.form.type = OC_FORM_OPT_SELECT; + o.form.name = (char *)"realm_choice"; + o.form.label = (char *)_("Realm:"); + + o.nr_choices = realms; + o.choices = calloc(realms, sizeof(*o.choices)); + if (!o.choices) + return -ENOMEM; + + while (l) { + if (parse_avp(vpninfo, &p, &l, &avp_p, &avp_len, &avp_flags, + &avp_vendor, &avp_code)) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to parse AVP\n")); + ret = -EINVAL; + goto out; + } + if (avp_vendor != VENDOR_JUNIPER2 || avp_code != 0xd4e) + continue; + + o.choices[i] = malloc(sizeof(struct oc_choice)); + if (!o.choices[i]) { + ret = -ENOMEM; + goto out; + } + o.choices[i]->name = o.choices[i]->label = strndup(avp_p, avp_len); + if (!o.choices[i]->name) { + ret = -ENOMEM; + goto out; + } + + i++; + } + + + /* We don't need to do anything on group changes. */ + do { + ret = process_auth_form(vpninfo, &f); + } while (ret == OC_FORM_RESULT_NEWGROUP); + + if (!ret) + buf_append_avp_string(reqbuf, 0xd50, o.form._value); + out: + if (o.choices) { + for (i = 0; i < realms; i++) { + if (o.choices[i]) { + free(o.choices[i]->name); + free(o.choices[i]); + } + } + free(o.choices); + } + return ret; +} + +static int pulse_request_session_kill(struct openconnect_info *vpninfo, struct oc_text_buf *reqbuf, + int sessions, unsigned char *eap) +{ + uint8_t avp_flags; + uint32_t avp_code; + uint32_t avp_vendor; + int avp_len, avp_len2; + void *avp_p, *avp_p2; + struct oc_auth_form f; + struct oc_form_opt_select o; + int i = 0, ret; + void *p; + int l; + struct oc_text_buf *form_msg = buf_alloc(); + char tmbuf[80]; + struct tm tm; + + l = load_be16(eap + 2) - 0x0c; /* Already validated */ + p = eap + 0x0c; + + memset(&f, 0, sizeof(f)); + memset(&o, 0, sizeof(o)); + f.auth_id = (char *)"pulse_session_kill"; + f.opts = &o.form; + + buf_append(form_msg, _("Session limit reached. Choose session to kill:\n")); + + o.form.next = NULL; + o.form.type = OC_FORM_OPT_SELECT; + o.form.name = (char *)"session_choice"; + o.form.label = (char *)_("Session:"); + + o.nr_choices = sessions; + o.choices = calloc(sessions, sizeof(*o.choices)); + if (!o.choices) + return -ENOMEM; + + while (l) { + char *from = NULL; + time_t when = 0; + char *sessid = NULL; + + if (parse_avp(vpninfo, &p, &l, &avp_p, &avp_len, &avp_flags, + &avp_vendor, &avp_code)) { + badlist: + vpn_progress(vpninfo, PRG_ERR, + _("Failed to parse session list\n")); + ret = -EINVAL; + goto out; + } + + if (avp_vendor != VENDOR_JUNIPER2 || avp_code != 0xd65) + continue; + + while (avp_len) { + if (parse_avp(vpninfo, &avp_p, &avp_len, &avp_p2, &avp_len2, + &avp_flags, &avp_vendor, &avp_code)) + goto badlist; + + dump_avp(vpninfo, avp_flags, avp_vendor, avp_code, avp_p2, avp_len2); + + if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd66) { + sessid = strndup(avp_p2, avp_len2); + } else if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd67) { + from = strndup(avp_p2, avp_len2); + } else if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd68 && + avp_len2 == 8) { + when = load_be32((char *)avp_p2 + 4); + if (sizeof(time_t) > 4) + when |= ((uint64_t)load_be32(avp_p2)) << 32; + } + } + + if (!from || !sessid || !when) { + free(from); + free(sessid); + goto badlist; + } + + localtime_r(&when, &tm); + strftime(tmbuf, 80, "%a, %d %b %Y %H:%M:%S %Z", &tm); + buf_append(form_msg, " - %s from %s at %s\n", sessid, from, tmbuf); + free(from); + o.choices[i] = malloc(sizeof(struct oc_choice)); + if (!o.choices[i]) { + ret = -ENOMEM; + goto out; + } + o.choices[i]->name = o.choices[i]->label = sessid; + if (!o.choices[i]->name) { + ret = -ENOMEM; + goto out; + } + i++; + } + ret = buf_error(form_msg); + if (ret) + goto out; + + f.message = form_msg->data; + + ret = process_auth_form(vpninfo, &f); + if (!ret) + buf_append_avp_string(reqbuf, 0xd69, o.form._value); + out: + if (o.choices) { + for (i = 0; i < sessions; i++) { + if (o.choices[i]) { + free(o.choices[i]->name); + free(o.choices[i]); + } + } + free(o.choices); + } + buf_free(form_msg); + return ret; +} + +static int pulse_request_user_auth(struct openconnect_info *vpninfo, struct oc_text_buf *reqbuf, + uint8_t eap_ident, char *user_prompt, char *pass_prompt) +{ + struct oc_auth_form f; + struct oc_form_opt o[2]; + int ret; + + memset(&f, 0, sizeof(f)); + memset(o, 0, sizeof(o)); + f.auth_id = (char *)"pulse_user"; + f.opts = &o[0]; + + f.message = _("Enter user credentials:"); + + o[0].next = &o[1]; + o[0].type = OC_FORM_OPT_TEXT; + o[0].name = (char *)"username"; + if (!user_prompt || asprintf(&o[0].label, "%s:", user_prompt) < 0) { + user_prompt = NULL; + o[0].label = (char *)_("Username:"); + } + + o[1].type = OC_FORM_OPT_PASSWORD; + o[1].name = (char *)"password"; + if (!pass_prompt || asprintf(&o[1].label, "%s:", pass_prompt) < 0) { + pass_prompt = NULL; + o[1].label = (char *)_("Password:"); + } + + ret = process_auth_form(vpninfo, &f); + if (ret) + goto out; + + if (o[0]._value) { + buf_append_avp_string(reqbuf, 0xd6d, o[0]._value); + free_pass(&o[0]._value); + } + if (o[1]._value) { + unsigned char eap_avp[23]; + int l = strlen(o[1]._value); + if (l > 253) { + free_pass(&o[1]._value); + return -EINVAL; + } + + /* AVP flags+mandatory+length */ + store_be32(eap_avp, AVP_CODE_EAP_MESSAGE); + store_be32(eap_avp + 4, (AVP_MANDATORY << 24) + sizeof(eap_avp) + l); + + /* EAP header: code/ident/len */ + eap_avp[8] = EAP_RESPONSE; + eap_avp[9] = eap_ident; + store_be16(eap_avp + 10, l + 15); /* EAP length */ + store_be32(eap_avp + 12, EXPANDED_JUNIPER); + store_be32(eap_avp + 16, 2); + + /* EAP Juniper/2 payload: 02 02 */ + eap_avp[20] = eap_avp[21] = 0x02; + eap_avp[22] = l + 2; /* Why 2? */ + buf_append_bytes(reqbuf, eap_avp, sizeof(eap_avp)); + buf_append_bytes(reqbuf, o[1]._value, l); + + /* Padding */ + if ((sizeof(eap_avp) + l) & 3) { + uint32_t pad = 0; + + buf_append_bytes(reqbuf, &pad, + 4 - ((sizeof(eap_avp) + l) & 3)); + } + free_pass(&o[1]._value); + } + ret = 0; + out: + if (user_prompt) + free(o[0].label); + if (pass_prompt) + free(o[1].label); + + return ret; +} + +/* IF-T/TLS session establishment is the same for both pulse_obtain_cookie() and + * pulse_connect(). We have to go through the EAP phase of the connection either + * way; it's just that we might do it with just the cookie, or we might need to + * use the password/cert etc. */ +static int pulse_authenticate(struct openconnect_info *vpninfo, int connecting) +{ + int ret; + struct oc_text_buf *reqbuf; + unsigned char bytes[16384]; + int eap_ofs; + uint8_t eap_ident, eap2_ident = 0; + uint8_t avp_flags; + uint32_t avp_code; + uint32_t avp_vendor; + int avp_len, l; + void *avp_p, *p; + unsigned char *eap; + int cookie_found = 0; + int j2_found = 0, realms_found = 0, realm_entry = 0, old_sessions = 0; + uint8_t j2_code = 0; + void *ttls = NULL; + char *user_prompt = NULL, *pass_prompt = NULL; + + /* XXX: We should do what cstp_connect() does to check that configuration + hasn't changed on a reconnect. */ + + ret = openconnect_open_https(vpninfo); + if (ret) + return ret; + + reqbuf = buf_alloc(); + + buf_append(reqbuf, "GET /%s HTTP/1.1\r\n", vpninfo->urlpath ?: ""); + http_common_headers(vpninfo, reqbuf); + buf_append(reqbuf, "Content-Type: EAP\r\n"); + buf_append(reqbuf, "Upgrade: IF-T/TLS 1.0\r\n"); + buf_append(reqbuf, "Content-Length: 0\r\n"); + buf_append(reqbuf, "\r\n"); + + if (buf_error(reqbuf)) { + vpn_progress(vpninfo, PRG_ERR, + _("Error creating Pulse connection request\n")); + ret = buf_error(reqbuf); + goto out; + } + if (vpninfo->dump_http_traffic) + dump_buf(vpninfo, '>', reqbuf->data); + + ret = vpninfo->ssl_write(vpninfo, reqbuf->data, reqbuf->pos); + if (ret < 0) + goto out; + + ret = process_http_response(vpninfo, 1, NULL, reqbuf); + if (ret < 0) + goto out; + + if (ret != 101) { + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected %d result from server\n"), + ret); + ret = -EINVAL; + goto out; + } + + vpninfo->ift_seq = 0; + /* IF-T version request. */ + buf_truncate(reqbuf); + buf_append_ift_hdr(reqbuf, VENDOR_TCG, IFT_VERSION_REQUEST); + /* min=1, max=1, preferred version=1 */ + buf_append_be32(reqbuf, 0x00010101); + ret = send_ift_packet(vpninfo, reqbuf); + if (ret) + goto out; + + ret = recv_ift_packet(vpninfo, (void *)bytes, sizeof(bytes)); + if (ret < 0) + goto out; + + if (ret != 0x14 || (load_be32(bytes) & 0xffffff) != VENDOR_TCG || + load_be32(bytes + 4) != IFT_VERSION_RESPONSE || + load_be32(bytes + 8) != 0x14) { + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected response to IF-T/TLS version negotiation:\n")); + dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)bytes, ret); + ret = -EINVAL; + goto out; + } + vpn_progress(vpninfo, PRG_TRACE, _("IF-T/TLS version from server: %d\n"), + bytes[0x13]); + + /* Client information packet over IF-T/TLS */ + buf_truncate(reqbuf); + buf_append_ift_hdr(reqbuf, VENDOR_JUNIPER, 0x88); + buf_append(reqbuf, "clientHostName=%s", vpninfo->localname); + bytes[0] = 0; + if (vpninfo->peer_addr && vpninfo->peer_addr->sa_family == AF_INET6) { + struct sockaddr_in6 a; + socklen_t l = sizeof(a); + if (!getsockname(vpninfo->ssl_fd, (void *)&a, &l)) + inet_ntop(AF_INET6, &a.sin6_addr, (void *)bytes, sizeof(bytes)); + } else if (vpninfo->peer_addr && vpninfo->peer_addr->sa_family == AF_INET) { + struct sockaddr_in a; + socklen_t l = sizeof(a); + if (!getsockname(vpninfo->ssl_fd, (void *)&a, &l)) + inet_ntop(AF_INET, &a.sin_addr, (void *)bytes, sizeof(bytes)); + } + if (bytes[0]) + buf_append(reqbuf, " clientIp=%s", bytes); + buf_append(reqbuf, "\n%c", 0); + ret = send_ift_packet(vpninfo, reqbuf); + if (ret) + goto out; + + /* Await start of auth negotiations */ + ret = recv_ift_packet(vpninfo, (void *)bytes, sizeof(bytes)); + if (ret < 0) + goto out; + + /* Basically an empty IF-T/TLS auth challenge packet of type Juniper/1, + * without even an EAP header in the payload. */ + if (!valid_ift_auth(bytes, ret) || ret != 0x14) { + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected IF-T/TLS authentication challenge:\n")); + dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)bytes, ret); + ret = -EINVAL; + goto out; + } + + /* Start by sending an EAP Identity of 'anonymous'. At this point we + * aren't yet very far down the rabbithole... + * + * -------------------------------------- + * | TCP/IP | + * |------------------------------------| + * | TLS | + * |------------------------------------| + * | IF-T/TLS | + * |------------------------------------| + * | EAP (IF-T/TLS Auth Type Juniper/1) | + * |------------------------------------| + * | EAP-Identity | + * -------------------------------------- + */ + buf_truncate(reqbuf); + buf_append_ift_hdr(reqbuf, VENDOR_TCG, IFT_CLIENT_AUTH_RESPONSE); + buf_append_be32(reqbuf, JUNIPER_1); /* IF-T/TLS Auth Type */ + eap_ofs = buf_append_eap_hdr(reqbuf, EAP_RESPONSE, 1, EAP_TYPE_IDENTITY, 0); + buf_append(reqbuf, "anonymous"); + buf_fill_eap_len(reqbuf, eap_ofs); + ret = send_ift_packet(vpninfo, reqbuf); + if (ret) + goto out; + + /* + * Phase 2 may continue directly with EAP within IF-T/TLS, or if certificate + * auth is enabled, the server may use EAP-TTLS. In that case, we end up + * with EAP within EAP-Message AVPs within EAP-TTLS within IF-T/TLS. + * The send_eap_packet() and recv_eap_packet() functions cope with both + * formats. The buffers have 0x14 bytes of header space, to allow for + * the IF-T/TLS header which is the larger of the two. + * + * -------------------------------------- + * | TCP/IP | + * |------------------------------------| + * | TLS | + * |------------------------------------| + * | IF-T/TLS | + * |------------------------------------| + * | EAP (IF-T/TLS Auth Type Juniper/1) | + * |------------------ | + * | EAP-TTLS | | + * |-----------------| (or directly) | + * | EAP-Message AVP | | + * |-----------------|------------------| + * | EAP-Juniper-1 | + * -------------------------------------- + */ + ret = recv_ift_packet(vpninfo, (void *)bytes, sizeof(bytes)); + if (ret < 0) + goto out; + + /* Check EAP header and length */ + if (!valid_ift_auth_eap(bytes, ret)) { + bad_ift: + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected IF-T/TLS authentication challenge:\n")); + dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)bytes, ret); + ret = -EINVAL; + goto out; + } + + /* + * We know the packet is valid at least down to the first layer of + * EAP in the diagram above, directly within the IF-T/TLS Auth Type + * of Juniper/1. Now, disambiguate between the two cases where the + * diagram diverges. Is it EAP-TTLS or is it EAP-Juniper-1 directly? + */ + if (valid_ift_auth_eap_exj1(bytes, ret)) { + eap = bytes + 0x14; + } else { + /* If it isn't that, it'd better be EAP-TTLS... */ + if (bytes[0x18] != EAP_TYPE_TTLS) + goto bad_ift; + + vpninfo->ttls_eap_ident = bytes[0x15]; + vpninfo->ttls_recvbuf = malloc(16384); + if (!vpninfo->ttls_recvbuf) + return -ENOMEM; + vpninfo->ttls_recvlen = 0; + vpninfo->ttls_recvpos = 0; + ttls = establish_eap_ttls(vpninfo); + if (!ttls) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to establish EAP-TTLS session\n")); + ret = -EINVAL; + goto out; + } + /* Resend the EAP Identity 'anonymous' packet within EAP-TTLS */ + ret = send_eap_packet(vpninfo, ttls, reqbuf); + if (ret) + goto out; + + /* + * The recv_eap_packet() function receives and validates the EAP + * packet of type Extended Juniper-1, either natively or within + * EAP-TTLS according to whether 'ttls' is set. + */ + eap = recv_eap_packet(vpninfo, ttls, bytes, sizeof(bytes)); + if (!eap) { + ret = -EIO; + goto out; + } + } + + /* Now we (hopefully) have the server information packet, in an EAP request + * from the server. Either it was received directly in IF-T/TLS, or within + * an EAP-Message within EAP-TTLS. Either way, the EAP message we're + * interested in will be at offset 0x14 in the packet, its header will + * have been checked, and is Expanded Juniper/1, and its payload thus + * starts at 0x20. And its length is sufficient that we won't underflow */ + eap_ident = eap[1]; + l = load_be16(eap + 2) - 0x0c; /* Already validated */ + p = eap + 0x0c; + + /* We don't actually use anything we get here. Typically it + * contains Juniper/0xd49 and Juniper/0xd4a word AVPs, and + * a Juniper/0xd56 AVP with server licensing information. */ + while (l) { + if (parse_avp(vpninfo, &p, &l, &avp_p, &avp_len, &avp_flags, + &avp_vendor, &avp_code)) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to parse AVP\n")); + bad_eap: + dump_buf_hex(vpninfo, PRG_ERR, 'E', eap, load_be16(eap + 2)); + ret = -EINVAL; + goto out; + } + dump_avp(vpninfo, avp_flags, avp_vendor, avp_code, avp_p, avp_len); + } + + /* Present the client information and auth cookie */ + buf_truncate(reqbuf); + buf_append_ift_hdr(reqbuf, VENDOR_TCG, IFT_CLIENT_AUTH_RESPONSE); + buf_append_be32(reqbuf, JUNIPER_1); /* IF-T/TLS Auth Type */ + eap_ofs = buf_append_eap_hdr(reqbuf, EAP_RESPONSE, eap_ident, EAP_TYPE_EXPANDED, 1); + /* Their client sends a lot of other stuff here, which we don't + * understand and which doesn't appear to be mandatory. So leave + * it out for now until/unless it becomes necessary. */ + buf_append_avp_string(reqbuf, 0xd70, vpninfo->useragent); + if (vpninfo->cookie) + buf_append_avp_string(reqbuf, 0xd53, vpninfo->cookie); + buf_fill_eap_len(reqbuf, eap_ofs); + ret = send_eap_packet(vpninfo, ttls, reqbuf); + if (ret) + goto out; + + + /* Await start of auth negotiations */ + auth_response: + realm_entry = realms_found = j2_found = old_sessions = 0; + eap = recv_eap_packet(vpninfo, ttls, (void *)bytes, sizeof(bytes)); + if (!eap) { + ret = -EIO; + goto out; + } + + eap_ident = eap[1]; + l = load_be16(eap + 2) - 0x0c; /* Already validated */ + p = eap + 0x0c; + + while (l) { + if (parse_avp(vpninfo, &p, &l, &avp_p, &avp_len, &avp_flags, + &avp_vendor, &avp_code)) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to parse AVP\n")); + goto bad_eap; + } + dump_avp(vpninfo, avp_flags, avp_vendor, avp_code, avp_p, avp_len); + + /* It's a bit late for this given that we don't get it until after + * we provide the password. */ + if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd55) { + char md5buf[MD5_SIZE * 2 + 1]; + get_cert_md5_fingerprint(vpninfo, vpninfo->peer_cert, md5buf); + if (avp_len != MD5_SIZE * 2 || strncasecmp(avp_p, md5buf, MD5_SIZE * 2)) { + vpn_progress(vpninfo, PRG_ERR, + _("Server certificate mismatch. Aborting due to suspected MITM attack\n")); + ret = -EPERM; + goto out; + } + } + if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd65) { + old_sessions++; + } else if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd80) { + free(user_prompt); + user_prompt = strndup(avp_p, avp_len); + } else if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd81) { + free(pass_prompt); + pass_prompt = strndup(avp_p, avp_len); + } else if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd4e) { + realms_found++; + } else if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd4f) { + realm_entry++; + } else if (avp_vendor == VENDOR_JUNIPER2 && avp_code == 0xd53) { + free(vpninfo->cookie); + vpninfo->cookie = strndup(avp_p, avp_len); + cookie_found = 1; + } else if (!avp_vendor && avp_code == AVP_CODE_EAP_MESSAGE) { + char *avp_c = avp_p; + + /* EAP within AVP within EAP within IF-T/TLS. + * The only thing we understand here is another form of Expanded EAP, + * this time with the type Juniper/2. */ + if (avp_len != 13 || avp_c[0] != EAP_REQUEST || + load_be16(avp_c + 2) != avp_len || + load_be32(avp_c + 4) != EXPANDED_JUNIPER || + load_be32(avp_c + 8) != 2) + goto auth_unknown; + + j2_found = 1; + j2_code = avp_c[12]; + eap2_ident = avp_c[1]; + } else if (avp_flags & AVP_MANDATORY) + goto auth_unknown; + } + + /* We want it to be precisely one type of request, not a mixture. */ + if (realm_entry + !!realms_found + j2_found + cookie_found + !!old_sessions != 1) { + auth_unknown: + vpn_progress(vpninfo, PRG_ERR, + _("Unhandled Pulse authenticationb packet, or authentication failure\n")); + goto bad_eap; + } + + /* Prepare next response packet */ + buf_truncate(reqbuf); + buf_append_ift_hdr(reqbuf, VENDOR_TCG, IFT_CLIENT_AUTH_RESPONSE); + buf_append_be32(reqbuf, JUNIPER_1); /* IF-T/TLS Auth Type */ + eap_ofs = buf_append_eap_hdr(reqbuf, EAP_RESPONSE, eap_ident, EAP_TYPE_EXPANDED, 1); + + if (!cookie_found) { + + /* No user interaction when called from pulse_connect(). + * We expect the cookie to work. */ + if (connecting) { + vpn_progress(vpninfo, PRG_ERR, + _("Pulse authentication cookie not accepted\n")); + ret = -EPERM; + goto out; + } + + if (realm_entry) { + vpn_progress(vpninfo, PRG_TRACE, _("Pulse realm entry\n")); + + ret = pulse_request_realm_entry(vpninfo, reqbuf); + if (ret) + goto out; + } else if (realms_found) { + vpn_progress(vpninfo, PRG_TRACE, _("Pulse realm choice\n")); + + ret = pulse_request_realm_choice(vpninfo, reqbuf, realms_found, eap); + if (ret) + goto out; + } else if (j2_found) { + vpn_progress(vpninfo, PRG_TRACE, + _("Pulse password auth request, code 0x%02x\n"), + j2_code); + + /* Present user/password form to user */ + ret = pulse_request_user_auth(vpninfo, reqbuf, eap2_ident, + user_prompt, pass_prompt); + if (ret) + goto out; + } else if (old_sessions) { + vpn_progress(vpninfo, PRG_TRACE, + _("Pulse session limit, %d sessions\n"), + old_sessions); + ret = pulse_request_session_kill(vpninfo, reqbuf, old_sessions, eap); + if (ret) + goto out; + } else { + vpn_progress(vpninfo, PRG_ERR, + _("Unhandled Pulse auth request\n")); + goto bad_eap; + } + + /* If we get here, something has filled in the next response */ + buf_fill_eap_len(reqbuf, eap_ofs); + ret = send_eap_packet(vpninfo, ttls, reqbuf); + if (ret) + goto out; + + goto auth_response; + } + + /* We're done, but need to send an empty response to the above information + * in order that the EAP session can complete with 'success'. Not quite + * sure why they didn't send it as payload on the success frame, mind you. */ + buf_fill_eap_len(reqbuf, eap_ofs); + ret = send_eap_packet(vpninfo, ttls, reqbuf); + if (ret) + goto out; + + if (ttls) { + /* Normally we don't actually send the EAP-TTLS frame until + * we're waiting for a response, which allows us to coalesce. + * This time, we need to flush the outbound frames. The empty + * EAP response (within EAP-TTLS) causes the server to close + * the EAP-TTLS session and the next response is plain IF-T/TLS + * IFT_CLIENT_AUTH_SUCCESS just like the non-certificate mode. */ + pulse_eap_ttls_recv(vpninfo, NULL, 0); + } + + ret = recv_ift_packet(vpninfo, (void *)bytes, sizeof(bytes)); + if (ret < 0) + goto out; + + if (!valid_ift_success(bytes, ret)) { + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected response instead of IF-T/TLS auth success:\n")); + dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)bytes, ret); + ret = -EINVAL; + goto out; + } + + ret = 0; + out: + if (ret) + openconnect_close_https(vpninfo, 0); + + buf_free(reqbuf); + if (ttls) + destroy_eap_ttls(vpninfo, ttls); + buf_free(vpninfo->ttls_pushbuf); + vpninfo->ttls_pushbuf = NULL; + free(vpninfo->ttls_recvbuf); + vpninfo->ttls_recvbuf = NULL; + free(user_prompt); + free(pass_prompt); + return ret; +} + +int pulse_eap_ttls_send(struct openconnect_info *vpninfo, const void *data, int len) +{ + struct oc_text_buf *buf = vpninfo->ttls_pushbuf; + + if (!buf) { + buf = vpninfo->ttls_pushbuf = buf_alloc(); + if (!buf) + return -ENOMEM; + } + + /* We concatenate sent data into a single EAP-TTLS frame which is + * sent just before we actually need to read something. */ + if (!buf->pos) { + buf_append_ift_hdr(buf, VENDOR_TCG, IFT_CLIENT_AUTH_RESPONSE); + buf_append_be32(buf, JUNIPER_1); /* IF-T/TLS Auth Type */ + buf_append_eap_hdr(buf, EAP_RESPONSE, vpninfo->ttls_eap_ident, + EAP_TYPE_TTLS, 0); + /* Flags byte for EAP-TTLS */ + buf_append_bytes(buf, "\0", 1); + } + buf_append_bytes(buf, data, len); + return len; +} + +int pulse_eap_ttls_recv(struct openconnect_info *vpninfo, void *data, int len) +{ + struct oc_text_buf *pushbuf= vpninfo->ttls_pushbuf; + int ret; + + if (!vpninfo->ttls_recvlen) { + uint8_t flags; + + if (pushbuf && !buf_error(pushbuf) && pushbuf->pos) { + buf_fill_eap_len(pushbuf, 0x14); + ret = send_ift_packet(vpninfo, pushbuf); + if (ret) + return ret; + buf_truncate(pushbuf); + } /* else send a continue? */ + if (!len) + return 0; + + vpninfo->ttls_recvlen = vpninfo->ssl_read(vpninfo, (void *)vpninfo->ttls_recvbuf, + 16384); + if (vpninfo->ttls_recvlen > 0 && vpninfo->dump_http_traffic) { + vpn_progress(vpninfo, PRG_TRACE, + _("Read %d bytes of IF-T/TLS EAP-TTLS record\n"), + vpninfo->ttls_recvlen); + dump_buf_hex(vpninfo, PRG_TRACE, '<', + (void *)vpninfo->ttls_recvbuf, + vpninfo->ttls_recvlen); + } + if (!valid_ift_auth_eap(vpninfo->ttls_recvbuf, vpninfo->ttls_recvlen) || + vpninfo->ttls_recvlen < 0x1a || + vpninfo->ttls_recvbuf[0x18] != EAP_TYPE_TTLS) { + bad_pkt: + vpn_progress(vpninfo, PRG_ERR, + _("Bad EAP-TTLS packet\n")); + return -EIO; + } + vpninfo->ttls_eap_ident = vpninfo->ttls_recvbuf[0x15]; + flags = vpninfo->ttls_recvbuf[0x19]; + if (flags & 0x7f) + goto bad_pkt; + if (flags & 0x80) { + /* Length bit. */ + if (vpninfo->ttls_recvlen < 0x1e || + load_be32(vpninfo->ttls_recvbuf + 0x1a) != vpninfo->ttls_recvlen - 0x1e) + goto bad_pkt; + vpninfo->ttls_recvpos = 0x1e; + vpninfo->ttls_recvlen -= 0x1e; + } else { + vpninfo->ttls_recvpos = 0x1a; + vpninfo->ttls_recvlen -= 0x1a; + } + } + + if (len > vpninfo->ttls_recvlen) { + memcpy(data, vpninfo->ttls_recvbuf + vpninfo->ttls_recvpos, + vpninfo->ttls_recvlen); + len = vpninfo->ttls_recvlen; + vpninfo->ttls_recvlen = 0; + return len; + } + memcpy(data, vpninfo->ttls_recvbuf + vpninfo->ttls_recvpos, len); + vpninfo->ttls_recvpos += len; + vpninfo->ttls_recvlen -= len; + return len; + +} + +int pulse_obtain_cookie(struct openconnect_info *vpninfo) +{ + return pulse_authenticate(vpninfo, 0); +} + +int pulse_connect(struct openconnect_info *vpninfo) +{ + unsigned char bytes[16384]; + int ret = 0, l; + unsigned char *p; + int routes_len = 0; + + /* If we already have a channel open, it's because we have just + * successfully authenticated on it from pulse_obtain_cookie(). */ + if (vpninfo->ssl_fd == -1) { + ret = pulse_authenticate(vpninfo, 1); + if (ret) + return ret; + } + + ret = recv_ift_packet(vpninfo, (void *)bytes, sizeof(bytes)); + if (ret < 0) + return ret; + + + /* Example config packet: + + < 0000: 00 00 0a 4c 00 00 00 01 00 00 01 80 00 00 01 fb |...L............| + < 0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| + < 0020: 2c 20 f0 00 00 00 00 00 00 00 01 70 2e 00 00 78 |, .........p...x| + < 0030: 07 00 00 00 07 00 00 10 00 00 ff ff 05 05 00 00 |................| + < 0040: 05 05 ff ff 07 00 00 10 00 00 ff ff 07 00 00 00 |................| + < 0050: 07 00 00 ff 07 00 00 10 00 00 ff ff 08 08 08 08 |................| + < 0060: 08 08 08 08 f1 00 00 10 00 00 ff ff 06 06 06 06 |................| + < 0070: 06 06 06 07 f1 00 00 10 00 00 ff ff 09 09 09 09 |................| + < 0080: 09 09 09 09 f1 00 00 10 00 00 ff ff 0a 0a 0a 0a |................| + < 0090: 0a 0a 0a 0a f1 00 00 10 00 00 ff ff 0b 0b 0b 0b |................| + < 00a0: 0b 0b 0b 0b 00 00 00 dc 03 00 00 00 40 00 00 01 |............@...| + < 00b0: 00 40 01 00 01 00 40 1f 00 01 00 40 20 00 01 00 |.@....@....@ ...| + < 00c0: 40 21 00 01 00 40 05 00 04 00 00 05 78 00 03 00 |@!...@......x...| + < 00d0: 04 08 08 08 08 00 03 00 04 08 08 04 04 40 06 00 |.............@..| + < 00e0: 0c 70 73 65 63 75 72 65 2e 6e 65 74 00 40 07 00 |.psecure.net.@..| + < 00f0: 04 00 00 00 00 00 04 00 04 01 01 01 01 40 19 00 |.............@..| + < 0100: 01 01 40 1a 00 01 00 40 0f 00 02 00 00 40 10 00 |..@....@.....@..| + < 0110: 02 00 05 40 11 00 02 00 02 40 12 00 04 00 00 04 |...@.....@......| + < 0120: b0 40 13 00 04 00 00 00 00 40 14 00 04 00 00 00 |.@.......@......| + < 0130: 01 40 15 00 04 00 00 00 00 40 16 00 02 11 94 40 |.@.......@.....@| + < 0140: 17 00 04 00 00 00 0f 40 18 00 04 00 00 00 3c 00 |.......@......<.| + < 0150: 01 00 04 0a 14 03 01 00 02 00 04 ff ff ff ff 40 |...............@| + < 0160: 0b 00 04 0a c8 c8 c8 40 0c 00 01 00 40 0d 00 01 |.......@....@...| + < 0170: 00 40 0e 00 01 00 40 1b 00 01 00 40 1c 00 01 00 |.@....@....@....| + + It starts as an IF-T/TLS packet of type Juniper/1. + + Lots of zeroes at the start, and at 0x20 there is a distinctive 0x2c20f000 + signature which appears to be in all config packets. + + At 0x28 it has the payload length (0x10 less than the full IF-T length). + 0x2c is the start of the routing information. The 0x2e byte always + seems to be there, and in this example 0x78 is the length of the + routing information block. The number of entries is in byte 0x30. + In the absence of IPv6 perhaps, the length at 0x2c seems always to be + the number of entries (in 0x30) * 0x10 + 8. + + Routing entries are 0x10 bytes each, starting at 0x34. The ones starting + with 0x07 are include, with 0xf1 are exclude. No idea what the following 7 + bytes 0f 00 00 10 00 00 ff ff mean; perhaps the 0010 is a length? The IP + address range is in bytes 8-11 (starting address) and the highest address + of the range (traditionally a broadcast address) is in bytes 12-15. + + After the routing inforamation (in this example at 0xa4) comes another + length field, this time for the information elements which comprise + the rest of the packet. Not sure what the 03 00 00 00 at 0xa8 means; + it *could* be an element type 0x3000 with payload length zero but if it + is, we don't know what it means. Following that, the elements all have + two bytes of type followed by two bytes length, then their payload. + + There follows an attempt to parse the packet based on the above + understanding. Having more examples, especially with IPv6 split includes + and excludes, would be useful... + */ + + if (ret < 0x50 || + /* IF-T/TLS header */ + load_be32(bytes) != VENDOR_JUNIPER || + load_be32(bytes + 4) != 1 || + load_be32(bytes + 8) != ret || + /* This appears to indicate the packet type (vs. ESP config) */ + load_be32(bytes + 0x20) != 0x2c20f000 || + /* A length field */ + load_be32(bytes + 0x28) != ret - 0x10 || + /* Start of routing information */ + load_be16(bytes + 0x2c) != 0x2e00 || + /* Routing length makes sense */ + (routes_len = load_be16(bytes + 0x2e)) != ((int)bytes[0x30] * 0x10 + 8) || + /* Make sure the next length field is actually present... */ + ret < 0x34 + 4 + routes_len || + /* Another length field (and maybe some adjacent zeroes) */ + load_be32(bytes + 0x2c + routes_len) + routes_len + 0x2c != ret) { + bad_config: + vpn_progress(vpninfo, PRG_ERR, + _("Unexpected Pulse config packet:\n")); + dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)bytes, ret); + return -EINVAL; + } + p = bytes + 0x34; + routes_len -= 8; + /* We know it's a multiple of 0x10 now. We checked. */ + while (routes_len) { + char buf[80]; + /* Probably not a whole be32 but let's see if anything ever changes */ + uint32_t type = load_be32(p); + uint32_t ffff = load_be32(p+4); + + if (ffff != 0xffff) + goto bad_config; + + /* Convert the range end into a netmask by xor. Mask out the + * bits in the network address, leaving only the low bits set, + * then invert what's left so that only the high bits are set + * as in a normal netmask. + * + * e.g. + * 10.0.0.0-10.0.63.255 becomes 0.0.63.255 becomes 255.255.192.0 + */ + snprintf(buf, sizeof(buf), "%d.%d.%d.%d/%d.%d.%d.%d", + p[8], p[9], p[10], p[11], + 255 ^ (p[8] ^ p[12]), 255 ^ (p[9] ^ p[13]), + 255 ^ (p[10] ^ p[14]), 255 ^ (p[11] ^ p[15])); + + if (type == 0x07000010) { + struct oc_split_include *inc; + + vpn_progress(vpninfo, PRG_DEBUG, _("Received split include route %s\n"), buf); + inc = malloc(sizeof(*inc)); + if (inc) { + inc->route = add_option(vpninfo, "split-include", buf, -1); + if (inc->route) { + inc->next = vpninfo->ip_info.split_includes; + vpninfo->ip_info.split_includes = inc; + } else + free(inc); + } + } else if (type == 0xf1000010) { + struct oc_split_include *exc; + + vpn_progress(vpninfo, PRG_DEBUG, _("Received split exclude route %s\n"), buf); + exc = malloc(sizeof(*exc)); + if (exc) { + exc->route = add_option(vpninfo, "split-exclude", buf, -1); + if (exc->route) { + exc->next = vpninfo->ip_info.split_excludes; + vpninfo->ip_info.split_excludes = exc; + } else + free(exc); + } + } else + goto bad_config; + + p += 0x10; + routes_len -= 0x10; + } + + /* p now points at the length field of the final elements, which + was already checked. */ + l = load_be32(p); + /* No idea what this is */ + if (l < 8 || load_be32(p + 4) != 0x03000000) + goto bad_config; + p += 8; + l -= 8; + + while (l) { + uint16_t type = load_be16(p); + uint16_t len = load_be16(p+2); + + if (len + 4 > l) + goto bad_config; + + p += 4; + l -= 4; + process_attr(vpninfo, type, p, len); + p += len; + l -= len; + if (l && l < 4) + goto bad_config; + } + + if (!vpninfo->ip_info.mtu || + (!vpninfo->ip_info.addr && !vpninfo->ip_info.addr6)) { + vpn_progress(vpninfo, PRG_ERR, "Insufficient configuration found\n"); + goto bad_config; + } + + ret = 0; + monitor_fd_new(vpninfo, ssl); + monitor_read_fd(vpninfo, ssl); + monitor_except_fd(vpninfo, ssl); + + free(vpninfo->cstp_pkt); + vpninfo->cstp_pkt = NULL; + + return ret; +} + + +int pulse_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable) +{ + int ret; + int work_done = 0; + + if (vpninfo->ssl_fd == -1) + goto do_reconnect; + + /* FIXME: The poll() handling here is fairly simplistic. Actually, + if the SSL connection stalls it could return a WANT_WRITE error + on _either_ of the SSL_read() or SSL_write() calls. In that case, + we should probably remove POLLIN from the events we're looking for, + and add POLLOUT. As it is, though, it'll just chew CPU time in that + fairly unlikely situation, until the write backlog clears. */ + while (readable) { + /* Some servers send us packets that are larger than + negotiated MTU. We reserve some extra space to + handle that */ + int receive_mtu = MAX(16384, vpninfo->deflate_pkt_size ? : vpninfo->ip_info.mtu); + 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->pulse.vendor, receive_mtu + 16); + if (!len) + break; + if (len < 0) + 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; + } + + if (load_be32(&vpninfo->cstp_pkt->pulse.vendor) != VENDOR_JUNIPER || + load_be32(&vpninfo->cstp_pkt->pulse.len) != len) + goto unknown_pkt; + + vpninfo->ssl_times.last_rx = time(NULL); + + switch(load_be32(&vpninfo->cstp_pkt->pulse.type)) { + case 4: + payload_len = len - 16; + 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; + continue; + + default: + unknown_pkt: + vpn_progress(vpninfo, PRG_ERR, + _("Unknown Pulse packet\n")); + dump_buf_hex(vpninfo, PRG_TRACE, '<', (void *)&vpninfo->cstp_pkt->pulse.vendor, len); + continue; + } + } + + + /* 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); + + + vpn_progress(vpninfo, PRG_TRACE, _("Packet outgoing:\n")); + dump_buf_hex(vpninfo, PRG_TRACE, '>', + (void *)&vpninfo->current_ssl_pkt->pulse.vendor, + vpninfo->current_ssl_pkt->len + 16); + + ret = ssl_nonblock_write(vpninfo, + &vpninfo->current_ssl_pkt->pulse.vendor, + vpninfo->current_ssl_pkt->len + 16); + if (ret < 0) { + do_reconnect: + /* XXX: Do we have to do this or can we leave it open? + * Perhaps we could even reconnect asynchronously while + * the ESP is still running? */ +#ifdef HAVE_ESP + esp_shutdown(vpninfo); +#endif + ret = ssl_reconnect(vpninfo); + if (ret) { + vpn_progress(vpninfo, PRG_ERR, _("Reconnect failed\n")); + vpninfo->quit_reason = "Pulse reconnect failed"; + return ret; + } + vpninfo->dtls_need_reconnect = 1; + return 1; + } else if (!ret) { +#if 0 /* Not for Pulse yet */ + /* -EAGAIN: ssl_nonblock_write() will have added the SSL + fd to ->select_wfds if appropriate, so we can just + return and wait. Unless it's been stalled for so long + that DPD kicks in and we kill the connection. */ + switch (ka_stalled_action(&vpninfo->ssl_times, timeout)) { + case KA_DPD_DEAD: + goto peer_dead; + case KA_REKEY: + goto do_rekey; + case KA_NONE: + return work_done; + default: + /* This should never happen */ + ; + } +#else + return work_done; +#endif + } + + 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 + 8, ret); + vpninfo->quit_reason = "Internal error"; + return 1; + } + /* Don't free the 'special' packets */ + if (vpninfo->current_ssl_pkt == vpninfo->deflate_pkt) { + free(vpninfo->pending_deflated_pkt); + vpninfo->pending_deflated_pkt = NULL; + } else + free(vpninfo->current_ssl_pkt); + + vpninfo->current_ssl_pkt = NULL; + } + +#if 0 /* Not understood for Pulse yet */ + if (vpninfo->owe_ssl_dpd_response) { + vpninfo->owe_ssl_dpd_response = 0; + vpninfo->current_ssl_pkt = (struct pkt *)&dpd_resp_pkt; + goto handle_outgoing; + } + + switch (keepalive_action(&vpninfo->ssl_times, timeout)) { + case KA_REKEY: + do_rekey: + /* Not that this will ever happen; we don't even process + the setting when we're asked for it. */ + vpn_progress(vpninfo, PRG_INFO, _("CSTP rekey due\n")); + if (vpninfo->ssl_times.rekey_method == REKEY_TUNNEL) + goto do_reconnect; + else if (vpninfo->ssl_times.rekey_method == REKEY_SSL) { + ret = cstp_handshake(vpninfo, 0); + if (ret) { + /* if we failed rehandshake try establishing a new-tunnel instead of failing */ + vpn_progress(vpninfo, PRG_ERR, _("Rehandshake failed; attempting new-tunnel\n")); + goto do_reconnect; + } + + goto do_dtls_reconnect; + } + break; + + case KA_DPD_DEAD: + peer_dead: + vpn_progress(vpninfo, PRG_ERR, + _("CSTP Dead Peer Detection detected dead peer!\n")); + goto do_reconnect; + do_reconnect: + ret = cstp_reconnect(vpninfo); + if (ret) { + vpn_progress(vpninfo, PRG_ERR, _("Reconnect failed\n")); + vpninfo->quit_reason = "CSTP reconnect failed"; + return ret; + } + + do_dtls_reconnect: + /* succeeded, let's rekey DTLS, if it is not rekeying + * itself. */ + if (vpninfo->dtls_state > DTLS_SLEEPING && + vpninfo->dtls_times.rekey_method == REKEY_NONE) { + vpninfo->dtls_need_reconnect = 1; + } + + return 1; + + case KA_DPD: + vpn_progress(vpninfo, PRG_DEBUG, _("Send CSTP DPD\n")); + + vpninfo->current_ssl_pkt = (struct pkt *)&dpd_pkt; + goto handle_outgoing; + + 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; + + vpn_progress(vpninfo, PRG_DEBUG, _("Send CSTP Keepalive\n")); + + vpninfo->current_ssl_pkt = (struct pkt *)&keepalive_pkt; + goto handle_outgoing; + + case KA_NONE: + ; + } +#endif + /* Service outgoing packet queue, if no DTLS */ + while (vpninfo->dtls_state != DTLS_CONNECTED && + (vpninfo->current_ssl_pkt = dequeue_packet(&vpninfo->outgoing_queue))) { + struct pkt *this = vpninfo->current_ssl_pkt; + + store_be32(&this->pulse.vendor, VENDOR_JUNIPER); + store_be32(&this->pulse.type, 4); + store_be32(&this->pulse.len, this->len + 16); + store_be32(&this->pulse.ident, vpninfo->ift_seq++); + + vpn_progress(vpninfo, PRG_TRACE, + _("Sending IF-T/TLS data packet of %d bytes\n"), + this->len); + + vpninfo->current_ssl_pkt = this; + goto handle_outgoing; + } + + /* Work is not done if we just got rid of packets off the queue */ + return work_done; +} + +int pulse_bye(struct openconnect_info *vpninfo, const char *reason) +{ + if (vpninfo->ssl_fd != -1) { + struct oc_text_buf *buf = buf_alloc(); + buf_append_ift_hdr(buf, VENDOR_JUNIPER, 0x89); + if (!buf_error(buf)) + send_ift_packet(vpninfo, buf); + buf_free(buf); + + openconnect_close_https(vpninfo, 0); + } + return 0; +} + +#ifdef HAVE_ESPx +void pulse_esp_close(struct openconnect_info *vpninfo) +{ + /* Tell server to stop sending on ESP channel */ + queue_esp_control(vpninfo, 0); + esp_close(vpninfo); +} + +int pulse_esp_send_probes(struct openconnect_info *vpninfo) +{ + struct pkt *pkt; + int pktlen, seq; + + 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); + } + + pkt = malloc(sizeof(*pkt) + 1 + vpninfo->pkt_trailer); + if (!pkt) + return -ENOMEM; + + for (seq=1; seq <= (vpninfo->dtls_state==DTLS_CONNECTED ? 1 : 2); seq++) { + pkt->len = 1; + pkt->data[0] = 0; + 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 pulse_esp_catch_probe(struct openconnect_info *vpninfo, struct pkt *pkt) +{ + return (pkt->len == 1 && pkt->data[0] == 0); +} +#endif /* HAVE_ESP */ diff --git a/www/Makefile.am b/www/Makefile.am index 680c6c2a..5674e4ed 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 hip.html tncc.html START_PAGES = building.html connecting.html manual.html vpnc-script.html INDEX_PAGES = changelog.html download.html index.html packages.html platforms.html licence.html -PROTO_PAGES = anyconnect.html juniper.html globalprotect.html +PROTO_PAGES = anyconnect.html juniper.html globalprotect.html pulse.html TOPLEVEL_PAGES = contribute.html mail.html ALL_PAGES = $(FTR_PAGES) $(START_PAGES) $(INDEX_PAGES) $(TOPLEVEL_PAGES) $(PROTO_PAGES) diff --git a/www/changelog.xml b/www/changelog.xml index b5fc0715..2a4887bd 100644 --- a/www/changelog.xml +++ b/www/changelog.xml @@ -16,6 +16,7 @@
  • OpenConnect HEAD
    • Rework DTLS MTU detection. (#10)
    • +
    • Add Pulse Connect Secure support.

  • OpenConnect v8.03 diff --git a/www/menu2-protocols.xml b/www/menu2-protocols.xml index 2e51cb5c..8f4d82f1 100644 --- a/www/menu2-protocols.xml +++ b/www/menu2-protocols.xml @@ -3,5 +3,6 @@ + diff --git a/www/pulse.xml b/www/pulse.xml new file mode 100644 index 00000000..94b15bae --- /dev/null +++ b/www/pulse.xml @@ -0,0 +1,51 @@ + + + + + + + + + + +

    Pulse Connect Secure

    + +

    Support for Pulse Connect Secure was added to OpenConnect in June 2019, +for the 8.04 release. In most cases it supersedes the older Juniper Network +Connect support. It is a much saner protocol.

    + +

    Pulse mode is requested by adding --protocol=pulse +to the command line: +

    +  openconnect --protocol=pulse vpn.example.com
    +

    + +

    The TCP transport for Pulse Connect Secure works over +IF-T/TLS, +first using EAP (and EAP-TTLS if certificates are being used) for +authentication and then passing traffic over IF-T messages over +the same transport. Just as with the older Juniper protocol, the UDP +transport is ESP.

    + +

    Authentication

    + +

    The authentication cookies are compatible with the +Juniper mode, which means that external +tools like juniper-vpn-py +should be usable with OpenConnect in Pulse mode too.

    + +

    Host Checker

    + +

    Not yet investigated and implemented for Pulse mode. The Juniper support may +suffice for some users.

    + +

    Connectivity

    + +

    Once authentication is complete, the VPN connection can be +established. Both Legacy IP and IPv6 should be working, although +test reports from someone with an IPv6-capable server would be greatly +appreciated as the freely available demo Virtual Appliance does not +support IPv6.

    + + +
    -- 2.49.0