]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
Add full DTLS support for PPP
authorDavid Woodhouse <dwmw2@infradead.org>
Tue, 13 Apr 2021 04:52:14 +0000 (05:52 +0100)
committerDavid Woodhouse <dwmw2@infradead.org>
Wed, 21 Apr 2021 15:14:49 +0000 (16:14 +0100)
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 <dwmw2@infradead.org>
f5.c
fortinet.c
library.c
nullppp.c
openconnect-internal.h
ppp.c

diff --git a/f5.c b/f5.c
index 8c1b7fd50faec24163dbbe0007668c47df625d97..03bbb61ecc167d23e627674a1046f50be17073c9 100644 (file)
--- 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);
index c081e8855ef238a4998a3c60e51b126553641e8b..d917ae6d7d22d756fbea80f54a66bd89e16f1108 100644 (file)
@@ -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)
index 7dbe0cbfb3b3eba4da3897ea5d282e9ae7626745..bc814df5f8d1005c66e4630322554b79c2c85cd4 100644 (file)
--- 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)
index c6e882cd985a3ec7229dcb374e77cb712bc612b2..ca13d1baa48f6a2793ebefa0d33eb792bc847969 100644 (file)
--- 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);
index b5f1083c0391391d7e1084b660266d5c60be7cd8..5edb4a09ea641b10617ad6805c78d34f924cd8e4 100644 (file)
@@ -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 57e1b2877444696b71be1b3f2c24191cfe3695eb..6b6096c69f00273bb82ccfdf97cadb310a55f210 100644 (file)
--- 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;
 }