From 04671ef02ecd8adc1df8430c49df87956237db1c Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Tue, 13 Apr 2021 05:52:14 +0100 Subject: [PATCH] Add full DTLS support for PPP Based on the logic in gpst_mainloop() for ESP deferral, the TCP mainloop first waits and does nothing, giving DTLS 5 seconds to connect. Only if/when DTLS is out of the picture does it start trying to establish PPP for itself. If the PPP state is still PPPS_DEAD, that means that the protocol's ->tcp_connect() method never actually established the tunnel, so call it again directly; falling back to a full ssl_reconnect() if needed. This time round (based on the DTLS state) the protocol's ->tcp_connect() method is then expected to establish the tunnel *and* call ppp_start_tcp_mainloop() which will do one pass through the state machine and shift from PPPS_DEAD to PPPS_ESTABLISH. Subsequent calls to ppp_tcp_mainloop() see a state other than PPPS_DEAD (as well as DTLS disabled) and call directly through to the core PPP mainloop for processing. Signed-off-by: David Woodhouse --- f5.c | 5 + fortinet.c | 5 + library.c | 2 + nullppp.c | 6 +- openconnect-internal.h | 5 + ppp.c | 284 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 304 insertions(+), 3 deletions(-) diff --git a/f5.c b/f5.c index 8c1b7fd5..03bbb61e 100644 --- a/f5.c +++ b/f5.c @@ -598,6 +598,11 @@ int f5_connect(struct openconnect_info *vpninfo) goto out; ret = openconnect_ppp_new(vpninfo, hdlc ? PPP_ENCAP_F5_HDLC : PPP_ENCAP_F5, ipv4, ipv6); + if (!ret) { + /* Trigger the first PPP negotiations and ensure the PPP state + * is PPPS_ESTABLISH so that ppp_tcp_mainloop() knows we've started. */ + ppp_start_tcp_mainloop(vpninfo); + } out: free_optlist(old_cstp_opts); diff --git a/fortinet.c b/fortinet.c index c081e885..d917ae6d 100644 --- a/fortinet.c +++ b/fortinet.c @@ -521,6 +521,11 @@ int fortinet_connect(struct openconnect_info *vpninfo) */ ret = openconnect_ppp_new(vpninfo, PPP_ENCAP_FORTINET, ipv4, ipv6); + if (!ret) { + /* Trigger the first PPP negotiations and ensure the PPP state + * is PPPS_ESTABLISH so that ppp_tcp_mainloop() knows we've started. */ + ppp_start_tcp_mainloop(vpninfo); + } out: if (ret) diff --git a/library.c b/library.c index 7dbe0cbf..bc814df5 100644 --- a/library.c +++ b/library.c @@ -461,6 +461,8 @@ void openconnect_vpninfo_free(struct openconnect_info *vpninfo) } free(vpninfo->ppp); + buf_free(vpninfo->ppp_tls_connect_req); + buf_free(vpninfo->ppp_dtls_connect_req); #ifdef HAVE_ICONV if (vpninfo->ic_utf8_to_legacy != (iconv_t)-1) diff --git a/nullppp.c b/nullppp.c index c6e882cd..ca13d1ba 100644 --- a/nullppp.c +++ b/nullppp.c @@ -58,7 +58,11 @@ int nullppp_connect(struct openconnect_info *vpninfo) ret = openconnect_ppp_new(vpninfo, hdlc ? PPP_ENCAP_RFC1662_HDLC : PPP_ENCAP_RFC1661, ipv4, ipv6); - + if (!ret) { + /* Trigger the first PPP negotiations and ensure the PPP state + * is PPPS_ESTABLISH so that ppp_tcp_mainloop() knows we've started. */ + ppp_start_tcp_mainloop(vpninfo); + } out: if (ret) openconnect_close_https(vpninfo, 0); diff --git a/openconnect-internal.h b/openconnect-internal.h index b5f1083c..5edb4a09 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -435,6 +435,8 @@ struct openconnect_info { uint32_t esp_magic; /* GlobalProtect magic ping address (network-endian) */ struct oc_ppp *ppp; + struct oc_text_buf *ppp_tls_connect_req; + struct oc_text_buf *ppp_dtls_connect_req; int tncc_fd; /* For Juniper TNCC */ char *platname; @@ -1049,7 +1051,10 @@ struct oc_ppp; void buf_append_ppphdlc(struct oc_text_buf *buf, const unsigned char *bytes, int len, uint32_t asyncmap); void buf_append_ppp_hdr(struct oc_text_buf *buf, struct oc_ppp *ppp, uint16_t proto, uint8_t code, uint8_t id); int ppp_negotiate_config(struct openconnect_info *vpninfo); +int ppp_tcp_should_connect(struct openconnect_info *vpninfo); +int ppp_start_tcp_mainloop(struct openconnect_info *vpninfo); int ppp_tcp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable); +int ppp_udp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable); int openconnect_ppp_new(struct openconnect_info *vpninfo, int encap, int want_ipv4, int want_ipv6); int ppp_reset(struct openconnect_info *vpninfo); diff --git a/ppp.c b/ppp.c index 57e1b287..6b6096c6 100644 --- a/ppp.c +++ b/ppp.c @@ -1474,10 +1474,290 @@ static int ppp_mainloop(struct openconnect_info *vpninfo, int dtls, return work_done; } +/* This function in designed to be called from a PPP protocol's + * ->tcp_connect() function, to allow it to determine whether it + * should establish the PPP connection immediately or wait for + * DTLS to have a turn. */ +int ppp_tcp_should_connect(struct openconnect_info *vpninfo) +{ + switch (vpninfo->dtls_state) { + case DTLS_DISABLED: + case DTLS_NOSECRET: + /* No DTLS here. Connect PPP immediately over TCP */ + return 1; + + case DTLS_SECRET: + /* When openconnect_make_cstp_connection() is first called, + * before openconnect_setup_dtls() is called, the state is + * DTLS_SECRET. In that case, defer the connection to allow + * DTLS to have a turn. */ + return 0; + + case DTLS_SLEEPING: + /* On a DTLS connection timeout, the TCP mainloop calls + * dtls_close() before calling this function to reconnect, + * so establish the PPP immediately over PPP. + * + * (After a connection pause, DTLS_SLEEPING is also seen but + * the UDP mainloop runs first and the state gets changed + * to DTLS_CONNECTING before the TCP mainloop runs, so that + * variant of DTLS_SLEEPING is never seen here.) */ + return 1; + + case DTLS_CONNECTING: + /* After pause/SIGUSR2, the UDP mainloop will run first and + * shifts from DTLS_SLEEPING to DTLS_CONNECTING state, before + * the TCP mainloop invokes this function. So defer the + * connection to allow DTLS to connect. */ + return 0; + + default: + case DTLS_CONNECTED: + case DTLS_ESTABLISHED: + /* These should never be seen. */ + vpn_progress(vpninfo, PRG_ERR, + _("PPP connect called with invalid DTLS state %d\n"), + vpninfo->dtls_state); + return -EIO; + } +} + +int ppp_start_tcp_mainloop(struct openconnect_info *vpninfo) +{ + int timeout = 0; + + return ppp_mainloop(vpninfo, 0, &vpninfo->ssl_times, &timeout, 1); +} + int ppp_tcp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable) { - if (vpninfo->dtls_state >= DTLS_SLEEPING) + /* If we're still attempting DTLS, do nothing yet. */ + switch (vpninfo->dtls_state) { + case DTLS_ESTABLISHED: + if (vpninfo->ssl_fd != -1) { + openconnect_close_https(vpninfo, 0); /* don't keep stale HTTPS socket */ + vpn_progress(vpninfo, PRG_INFO, + _("DTLS tunnel connected; exiting HTTPS mainloop.\n")); + } + + /* Now that we are connected, let's ensure timeout is less than + * or equal to DTLS DPD/keepalive else we might over sleep, e.g. + * if timeout is set to DTLS attempt period from DTLS mainloop, + * and falsely detect dead peer. */ + if (vpninfo->dtls_times.dpd) + if (*timeout > vpninfo->dtls_times.dpd * 1000) + *timeout = vpninfo->dtls_times.dpd * 1000; + return 0; - return ppp_mainloop(vpninfo, 0, &vpninfo->ssl_times, timeout, readable); + case DTLS_CONNECTED: + case DTLS_CONNECTING: + case DTLS_SECRET: + if (vpninfo->ppp->ppp_state == PPPS_DEAD) { + /* Allow 5 seconds after configuration for DTLS to start */ + if (!ka_check_deadline(timeout, time(NULL), vpninfo->new_dtls_started + 5)) { + vpninfo->delay_tunnel_reason = "awaiting PPP DTLS connection"; + return 0; + } + /* It'll try again in a while, but we want it to be in DTLS_SLEEPING + * so that the tcp_connect() function actually establishes the PPP + * over TCP. */ + dtls_close(vpninfo); + } + + /* Fall through */ + case DTLS_SLEEPING: + /* Should only be seen by the mainloop if DTLS actually *failed* + * (which includes timing out). */ + if (vpninfo->ppp->ppp_state == PPPS_DEAD) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to connect DTLS tunnel; using HTTPS instead (state %d).\n"), + vpninfo->dtls_state); + } + /* Fall through */ + case DTLS_NOSECRET: + case DTLS_DISABLED: + /* The state is PPPS_DEAD until the first time ppp_tcp_mainloop() + * gets invoked. When f5_connect() actually establishes the tunnel, + * it does so to start the PPP state machine for the TCP connection. + */ + if (vpninfo->ssl_fd != -1 && vpninfo->ppp->ppp_state != PPPS_DEAD) + return ppp_mainloop(vpninfo, 0, &vpninfo->ssl_times, timeout, readable); + + /* This will call *back* into the protocol's ->tcp_connect() + * but this time DTLS is disabled so it'll actually establish + * the connection there. We want to make use of the retry + * handling logic in ssl_reconnect() in the cases where it + * doesn't succeed immediately. + * + * If the connection is already open, try using it directly + * first, before falling back to a full ssl_reconnect(). + */ + if (vpninfo->ssl_fd == -1 || vpninfo->proto->tcp_connect(vpninfo)) { + int ret = ssl_reconnect(vpninfo); + if (ret) { + vpn_progress(vpninfo, PRG_ERR, _("Establishing PPP tunnel over TLS failed\n")); + vpninfo->quit_reason = "PPP TLS connect failed"; + return ret; + } + vpninfo->delay_tunnel_reason = "DTLS connection pending"; + return 1; + } + vpninfo->delay_tunnel_reason = "DTLS connection pending"; + return 1; + } + + vpn_progress(vpninfo, PRG_ERR, _("Invalid DTLS state %d\n"), vpninfo->dtls_state); + vpninfo->quit_reason = "Invalid DTLS state"; + return 1; +} + +int ppp_udp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable) +{ + int work_done = 0; + time_t now = time(NULL); + + switch(vpninfo->dtls_state) { + case DTLS_CONNECTING: + if (vpninfo->ppp->ppp_state == PPPS_DEAD) + vpninfo->delay_tunnel_reason = "DTLS connecting"; + + dtls_try_handshake(vpninfo, timeout); + if (vpninfo->dtls_state == DTLS_CONNECTED) + goto newly_connected; + return 0; + + case DTLS_CONNECTED: + /* First, see if there's a response for us. */ + while(readable) { + int receive_mtu = MAX(16384, vpninfo->ip_info.mtu); + int len; + + /* cstp_pkt is used by PPP over either transport, and TCP + * may be in active use while we attempt to connect DTLS. + * So use vpninfo->dtls_pkt for this. */ + if (!vpninfo->dtls_pkt) + vpninfo->dtls_pkt = malloc(sizeof(struct pkt) + receive_mtu); + if (!vpninfo->dtls_pkt) { + vpn_progress(vpninfo, PRG_ERR, _("Allocation failed\n")); + dtls_close(vpninfo); + vpninfo->dtls_state = DTLS_DISABLED; + return 1; + } + + struct pkt *this = vpninfo->dtls_pkt; + len = ssl_nonblock_read(vpninfo, 1, this->data, receive_mtu); + if (!len) + break; + if (len < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to receive authentication response from DTLS\n")); + dtls_close(vpninfo); + return 1; + } + + this->len = len; + + if (vpninfo->dump_http_traffic) + dump_buf_hex(vpninfo, PRG_DEBUG, '<', this->data, len); + + int ret = vpninfo->proto->udp_catch_probe(vpninfo, this); + if (ret < 0) { + dtls_close(vpninfo); + return 1; + } else if (ret > 0) { + vpninfo->dtls_state = DTLS_ESTABLISHED; + vpninfo->dtls_pkt = NULL; + free(this); + + /* We are going to take over the PPP now; reset the TCP one */ + ret = ppp_reset(vpninfo); + if (ret) { + /* This should never happen */ + vpn_progress(vpninfo, PRG_ERR, _("Reset PPP failed\n")); + vpninfo->quit_reason = "PPP DTLS connect failed"; + return ret; + } + goto established; + } + /* This is the ret==0 case, where the packet was recognised + * neither as a success nor failure. In that case it could + * be a PPP frame which has been received out of order and + * made it to us before the OK response. Drop it and keep + * waiting. */ + } + + /* On first connection, the TCP mainloop will give us five seconds + * to do the whole exchange and reach DTLS_ESTABLISHED before it + * gives up and connects over TCP instead. But for opportunistic + * attempts to "upgrade" to DTLS later, it won't get involved. + * We still want to time out and give up on this DTLS connection + * if we failed to authenticate though. So do it here too. */ + if (ka_check_deadline(timeout, now, vpninfo->dtls_times.last_rekey + 5)) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to authenticate DTLS session\n")); + dtls_close(vpninfo); + return 1; + } + + /* Resend the connect request every second */ + if (ka_check_deadline(timeout, now, vpninfo->dtls_times.last_tx + 1)) { + newly_connected: + if (buf_error(vpninfo->ppp_dtls_connect_req)) { + vpn_progress(vpninfo, PRG_ERR, + _("Error creating connect request for DTLS session\n")); + dtls_close(vpninfo); + vpninfo->dtls_state = DTLS_DISABLED; + return 1; + } + + if (vpninfo->dump_http_traffic) + dump_buf_hex(vpninfo, PRG_DEBUG, '>', + (void *)vpninfo->ppp_dtls_connect_req->data, + vpninfo->ppp_dtls_connect_req->pos); + + int ret = ssl_nonblock_write(vpninfo, 1, + vpninfo->ppp_dtls_connect_req->data, + vpninfo->ppp_dtls_connect_req->pos); + if (ret < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to write connect request to DTLS session\n")); + dtls_close(vpninfo); + vpninfo->dtls_state = DTLS_DISABLED; + return 1; + } + vpninfo->dtls_times.last_tx = now; + } + + if (vpninfo->ppp->ppp_state == PPPS_DEAD) + vpninfo->delay_tunnel_reason = "DTLS establishing"; + + return 0; + + case DTLS_ESTABLISHED: + established: + work_done = ppp_mainloop(vpninfo, 1, &vpninfo->dtls_times, timeout, readable); + if (vpninfo->dtls_state != DTLS_SLEEPING) + break; + + /* Fall through */ + case DTLS_SLEEPING: + /* If the SSL connection isn't open, that must mean we've been paused + * and resumed. So reconnect immediately regardless of whether we'd + * just done so, *and* reset the PPP state so that the TCP mainloop + * doesn't get confused. */ + if (vpninfo->ssl_fd == -1) { + ppp_reset(vpninfo); + if (now < vpninfo->new_dtls_started + vpninfo->dtls_attempt_period) + now = vpninfo->new_dtls_started + vpninfo->dtls_attempt_period; + } + + if (ka_check_deadline(timeout, now, vpninfo->new_dtls_started + vpninfo->dtls_attempt_period)) { + vpn_progress(vpninfo, PRG_DEBUG, _("Attempt new DTLS connection\n")); + dtls_reconnect(vpninfo, timeout); + work_done = 1; + } + } + + return work_done; } -- 2.50.1