]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
Initial shell of Array Networks SSL VPN support
authorDavid Woodhouse <dwmw2@infradead.org>
Thu, 19 Mar 2020 11:46:06 +0000 (11:46 +0000)
committerDavid Woodhouse <dwmw2@infradead.org>
Wed, 5 May 2021 22:12:21 +0000 (23:12 +0100)
Can't resist a packet dump...

https://gitlab.com/openconnect/openconnect/issues/102

Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Makefile.am
array.c [new file with mode: 0644]
library.c
openconnect-internal.h

index aba66c09e406f6edeb3bf14d039c94711f5f9be3..fc6fa44771c2b4aa24c535fd2b5db3ab76fdacab 100644 (file)
@@ -40,6 +40,7 @@ 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 win32-ipicmp.h auth-globalprotect.c
+lib_srcs_array = array.c
 lib_srcs_oath = oath.c
 lib_srcs_oidc = oidc.c
 lib_srcs_ppp = ppp.c ppp.h
@@ -51,7 +52,8 @@ lib_srcs_json = jsondump.c
 library_srcs += $(lib_srcs_juniper) $(lib_srcs_cisco) $(lib_srcs_oath) \
                $(lib_srcs_globalprotect) $(lib_srcs_pulse) \
                $(lib_srcs_oidc) $(lib_srcs_ppp) $(lib_srcs_nullppp) \
-               $(lib_srcs_f5) $(lib_srcs_fortinet) $(lib_srcs_json)
+               $(lib_srcs_f5) $(lib_srcs_fortinet) $(lib_srcs_json) \
+               $(lib_srcs_array)
 
 lib_srcs_gnutls = gnutls.c gnutls_tpm.c gnutls_tpm2.c
 lib_srcs_openssl = openssl.c openssl-pkcs11.c
diff --git a/array.c b/array.c
new file mode 100644 (file)
index 0000000..0456455
--- /dev/null
+++ b/array.c
@@ -0,0 +1,440 @@
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Copyright © 2020 David Woodhouse
+ *
+ * Author: David Woodhouse <dwmw2@infradead.org>
+ *
+ * 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 <config.h>
+
+#include <unistd.h>
+#include <fcntl.h>
+#include <time.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <stdarg.h>
+#include <sys/types.h>
+
+#include "openconnect-internal.h"
+
+int array_obtain_cookie(struct openconnect_info *vpninfo)
+{
+       return -EINVAL;
+}
+
+/* XXX: Lifted from oncp.c. Share it. */
+static int parse_cookie(struct openconnect_info *vpninfo)
+{
+       char *p = vpninfo->cookie;
+
+       /* We currenly expect the "cookie" to be contain multiple cookies:
+        * DSSignInUrl=/; DSID=xxx; DSFirstAccess=xxx; DSLastAccess=xxx
+        * Process those into vpninfo->cookies unless we already had them
+        * (in which case they'll may be newer. */
+       while (p && *p) {
+               char *semicolon = strchr(p, ';');
+               char *equals;
+
+               if (semicolon)
+                       *semicolon = 0;
+
+               equals = strchr(p, '=');
+               if (!equals) {
+                       vpn_progress(vpninfo, PRG_ERR, _("Invalid cookie '%s'\n"), p);
+                       return -EINVAL;
+               }
+               *equals = 0;
+               http_add_cookie(vpninfo, p, equals+1, 0);
+               *equals = '=';
+
+               p = semicolon;
+               if (p) {
+                       *p = ';';
+                       p++;
+                       while (*p && isspace((int)(unsigned char)*p))
+                               p++;
+               }
+       }
+
+       return 0;
+}
+
+/* No idea what these structures are yet... */
+static const unsigned char conf50[] = { 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+
+static const unsigned char conf54[] = { 0x54, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                       0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
+                                       0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x02, 0x0f,
+                                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                       0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+                                       0x52, 0x54, 0x00, 0xde, 0xa2, 0xa6, 0x00, 0x00 };
+
+static const unsigned char ipff[] = { 0x45, 0x00, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00,
+                                     0x00, 0xff, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00,
+                                     0x00, 0x00, 0x00, 0x00 };
+
+int array_connect(struct openconnect_info *vpninfo)
+{
+       int ret;
+       struct oc_text_buf *reqbuf;
+       unsigned char bytes[65536];
+
+       /* XXX: We should do what cstp_connect() does to check that configuration
+          hasn't changed on a reconnect. */
+
+       if (!vpninfo->cookies) {
+               ret = parse_cookie(vpninfo);
+               if (ret)
+                       return ret;
+       }
+
+       ret = openconnect_open_https(vpninfo);
+       if (ret)
+               return ret;
+
+       reqbuf = buf_alloc();
+
+       buf_append(reqbuf, "GET /vpntunnel HTTP/1.1\r\n");
+       http_common_headers(vpninfo, reqbuf);
+       buf_append(reqbuf, "appid: SSPVPN\r\n");
+       buf_append(reqbuf, "clientid: xx\r\n");
+       //      buf_append(reqbuf, "cpuid: FBFEDA5D-6603-451F-AC36-9231868A32D3\r\n");
+       buf_append(reqbuf, "hostname: %s\r\n", vpninfo->localname);
+       buf_append(reqbuf, "payload-ip-version: 6\r\n");
+       //      buf_append(reqbuf, "x-devtype: 6\r\n");
+       buf_append(reqbuf, "\r\n");
+
+       if (buf_error(reqbuf)) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Error creating array negotiation 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 != 201 && ret != 200) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Unexpected %d result from server\n"),
+                            ret);
+               ret = -EINVAL;
+               goto out;
+       }
+
+       buf_truncate(reqbuf);
+
+       /* Send first configuration request 'conf50' */
+       dump_buf_hex(vpninfo, PRG_DEBUG, '>', (void *)conf50, sizeof(conf50));
+       ret = vpninfo->ssl_write(vpninfo, (void *)conf50, sizeof(conf50));
+       if (ret != sizeof(conf50)) {
+               if (ret >= 0) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Short write in array negotiation\n"));
+                       ret = -EIO;
+               }
+               goto out;
+       }
+
+       ret = vpninfo->ssl_read(vpninfo, (void *)bytes, sizeof(bytes));
+       if (ret < 0) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to read conf50 response\n"));
+               goto out;
+       }
+
+       /* Parse it, learn what we need from it */
+       dump_buf_hex(vpninfo, PRG_DEBUG, '<', bytes, ret);
+
+       /* Send second configuration request 'conf54' */
+       dump_buf_hex(vpninfo, PRG_DEBUG, '>', (void *)conf54, sizeof(conf54));
+       ret = vpninfo->ssl_write(vpninfo, (void *)conf54, sizeof(conf54));
+       if (ret != sizeof(conf54)) {
+               if (ret >= 0) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Short write in array negotiation\n"));
+                       ret = -EIO;
+               }
+               goto out;
+       }
+
+       ret = vpninfo->ssl_read(vpninfo, (void *)bytes, sizeof(bytes));
+       if (ret < 0) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to read conf54 response\n"));
+               goto out;
+       }
+
+       /* Parse it, learn what we need from it */
+       dump_buf_hex(vpninfo, PRG_DEBUG, '<', bytes, ret);
+
+       /* Send third request 'ipff' */
+       dump_buf_hex(vpninfo, PRG_DEBUG, '>', (void *)ipff, sizeof(ipff));
+       ret = vpninfo->ssl_write(vpninfo,  (void *)ipff, sizeof(ipff));
+       if (ret != sizeof(ipff)) {
+               if (ret >= 0) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Short write in array negotiation\n"));
+                       ret = -EIO;
+               }
+               goto out;
+       }
+
+       ret = vpninfo->ssl_read(vpninfo, (void *)bytes, sizeof(bytes));
+       if (ret < 0) {
+               vpn_progress(vpninfo, PRG_ERR,
+                            _("Failed to read ipff response\n"));
+               goto out;
+       }
+
+       /* Parse it, learn what we need from it */
+       dump_buf_hex(vpninfo, PRG_DEBUG, '<', bytes, ret);
+
+       ret = 0; /* success */
+ out:
+       if (ret)
+               openconnect_close_https(vpninfo, 0);
+       else {
+               monitor_fd_new(vpninfo, ssl);
+               monitor_read_fd(vpninfo, ssl);
+               monitor_except_fd(vpninfo, ssl);
+       }
+       buf_free(reqbuf);
+
+       free(vpninfo->cstp_pkt);
+       vpninfo->cstp_pkt = NULL;
+
+       vpninfo->ip_info.mtu = 1400;
+
+       return ret;
+}
+
+int array_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;
+
+               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, 0, vpninfo->cstp_pkt->data, receive_mtu);
+               if (!len)
+                       break;
+               if (len < 0)
+                       goto do_reconnect;
+               if (len < 8) {
+                       vpn_progress(vpninfo, PRG_ERR, _("Short packet received (%d bytes)\n"), len);
+                       vpninfo->quit_reason = "Short packet received";
+                       return 1;
+               }
+
+               /* Check it looks like a valid IP packet, and then check for the special
+                * IP protocol 255 that is used for control stuff. Maybe also look at length
+                * and be prepared to *split* IP packets received in the same read() call. */
+
+               vpninfo->ssl_times.last_rx = time(NULL);
+
+               vpn_progress(vpninfo, PRG_TRACE,
+                            _("Received uncompressed data packet of %d bytes\n"),
+                            len);
+               vpninfo->cstp_pkt->len = len;
+               queue_packet(&vpninfo->incoming_queue, vpninfo->cstp_pkt);
+               vpninfo->cstp_pkt = NULL;
+               work_done = 1;
+               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);
+
+               ret = ssl_nonblock_write(vpninfo, 0,
+                                        vpninfo->current_ssl_pkt->data,
+                                        vpninfo->current_ssl_pkt->len);
+               if (ret < 0)
+                       goto do_reconnect;
+               else if (!ret) {
+                       /* -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 */
+                               ;
+                       }
+               }
+
+               if (ret != vpninfo->current_ssl_pkt->len) {
+                       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;
+               }
+
+               vpninfo->current_ssl_pkt = NULL;
+       }
+
+       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"));
+       do_reconnect:
+               ret = ssl_reconnect(vpninfo);
+               if (ret) {
+                       vpn_progress(vpninfo, PRG_ERR, _("TCP reconnect failed\n"));
+                       vpninfo->quit_reason = "TCP 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;
+               break;
+
+       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;
+               break;
+
+       case KA_NONE:
+               ;
+       }
+
+       /* 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;
+
+               vpn_progress(vpninfo, PRG_TRACE,
+                            _("Sending uncompressed 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 array_bye(struct openconnect_info *vpninfo, const char *reason)
+{
+       char *orig_path;
+       char *res_buf=NULL;
+       int ret;
+
+       /* We need to close and reopen the HTTPS connection (to kill
+        * the array tunnel) and submit a new HTTPS request to logout.
+        */
+       openconnect_close_https(vpninfo, 0);
+
+       orig_path = vpninfo->urlpath;
+       vpninfo->urlpath = strdup("prx/000/http/localhost/logout"); /* redirect segfaults without strdup */
+       ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0);
+       free(vpninfo->urlpath);
+       vpninfo->urlpath = orig_path;
+
+       if (ret < 0)
+               vpn_progress(vpninfo, PRG_ERR, _("Logout failed.\n"));
+       else
+               vpn_progress(vpninfo, PRG_INFO, _("Logout successful.\n"));
+
+       free(res_buf);
+       return ret;
+}
+
index 0aa0bd4da1921c4db7491f61dafc3b94ae2fc9ed..a7262b3448d2c8e880eb5c0d92c94bfb7941ffb0 100644 (file)
--- a/library.c
+++ b/library.c
@@ -242,6 +242,25 @@ static const struct vpn_proto openconnect_protos[] = {
                .tcp_mainloop = nullppp_mainloop,
                .add_http_headers = http_common_headers,
                .obtain_cookie = nullppp_obtain_cookie,
+       }, {
+               .name = "array",
+               .pretty_name = N_("Array SSL VPN"),
+               .description = N_("Compatible with Array Networks SSL VPN"),
+               .flags = OC_PROTO_PROXY | OC_PROTO_HIDDEN,
+               .vpn_close_session = array_bye,
+               .tcp_connect = array_connect,
+               .tcp_mainloop = array_mainloop,
+               .add_http_headers = http_common_headers,
+               .obtain_cookie = array_obtain_cookie,
+               .udp_protocol = "DTLS",
+#ifdef HAVE_DTLSx /* Not yet */
+               .udp_setup = esp_setup,
+               .udp_mainloop = esp_mainloop,
+               .udp_close = esp_close,
+               .udp_shutdown = esp_shutdown,
+               .udp_send_probes = oncp_esp_send_probes,
+               .udp_catch_probe = oncp_esp_catch_probe,
+#endif
        },
 };
 
index bb60911f9a078eee39b71c6055b0d56b3ab258b1..cc34885c67b16f3dd28dbef7bc3be5e5d7c64a08 100644 (file)
@@ -1066,6 +1066,12 @@ int openconnect_ppp_new(struct openconnect_info *vpninfo, int encap, int want_ip
 int ppp_reset(struct openconnect_info *vpninfo);
 int check_http_status(const char *buf, int len);
 
+/* array.c */
+int array_obtain_cookie(struct openconnect_info *vpninfo);
+int array_connect(struct openconnect_info *vpninfo);
+int array_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable);
+int array_bye(struct openconnect_info *vpninfo, const char *reason);
+
 /* auth-globalprotect.c */
 int gpst_obtain_cookie(struct openconnect_info *vpninfo);
 void gpst_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf);