Add openconnect_get_connect_url(), use it in --authenticate output connect-url
authorDavid Woodhouse <dwmw2@infradead.org>
Fri, 14 May 2021 10:57:11 +0000 (11:57 +0100)
committerDavid Woodhouse <dwmw2@infradead.org>
Fri, 14 May 2021 15:56:51 +0000 (16:56 +0100)
As noted in the long comment in openconnect.h, we need to provide the
actual *URL* for the connection including the real hostname. And that
means we also need to provide a suitable --resolve argument to pass on
to the connecting openconnect.

Add openconnect_get_connect_url() for the GUI auth-dialogs to use, and
make 'openconnect --authenticate' emit the same. If we just put it into
$HOST that might break existing setups that don't use the newly-added
$RESOLVE variable too, so put it in another new variable $CONNECT_URL
and leave the original $HOST alone.

Signed-off-by: David Woodhouse <dwmw2@infradead.org>
java/src/org/infradead/libopenconnect/LibOpenConnect.java
jni.c
libopenconnect.map.in
library.c
main.c
openconnect-internal.h
openconnect.h
www/changelog.xml

index d55c7784bf9cd4fbb162326910437f8dd082aab3..4e283f482501004ed1817a926cb0404094d7f7d6 100644 (file)
@@ -156,6 +156,7 @@ public abstract class LibOpenConnect {
 
        /* connection info */
 
+       public synchronized native String getConnectUrl();
        public synchronized native String getHostname();
        public synchronized native String getDNSName();
        public synchronized native String getUrlpath();
diff --git a/jni.c b/jni.c
index 3573d852c3b1290f614cbbadbe92c433c9c0aee6..2c9d581d258b163fb0a763fb14eda95e992fcab1 100644 (file)
--- a/jni.c
+++ b/jni.c
@@ -1301,6 +1301,14 @@ JNIEXPORT jstring JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_getPr
        RETURN_STRING_END
 }
 
+JNIEXPORT jstring JNICALL Java_org_infradead_libopenconnect_LibOpenConnect_getConnectUrl(
+       JNIEnv *jenv, jobject jobj)
+{
+       RETURN_STRING_START
+       buf = openconnect_get_connect_url(ctx->vpninfo);
+       RETURN_STRING_END
+}
+
 #define SET_STRING_START()                      \
        struct libctx *ctx = getctx(jenv, jobj); \
        const char *arg = NULL;                  \
index 55aec62e161576650b594cdab94a650df5fb82f3..2e87f68e00866848dfa065d876b9dfdd242ce7ee 100644 (file)
@@ -114,6 +114,7 @@ OPENCONNECT_5_7 {
        openconnect_set_allow_insecure_crypto;
        openconnect_get_auth_expiration;
        openconnect_disable_dtls;
+       openconnect_get_connect_url;
 } OPENCONNECT_5_6;
 
 OPENCONNECT_PRIVATE {
index c8ec44df820f5cb7294d9741c310f251274d32db..e4bea9d048c891008b5d295b2dbe076795872c6d 100644 (file)
--- a/library.c
+++ b/library.c
@@ -577,6 +577,7 @@ void openconnect_vpninfo_free(struct openconnect_info *vpninfo)
        free_split_routes(&vpninfo->ip_info);
        free(vpninfo->hostname);
        free(vpninfo->unique_hostname);
+       buf_free(vpninfo->connect_urlbuf);
        free(vpninfo->urlpath);
        free(vpninfo->redirect_url);
        free_pass(&vpninfo->cookie);
@@ -694,6 +695,39 @@ void openconnect_vpninfo_free(struct openconnect_info *vpninfo)
        free(vpninfo);
 }
 
+
+const char *openconnect_get_connect_url(struct openconnect_info *vpninfo)
+{
+       struct oc_text_buf *urlbuf = vpninfo->connect_urlbuf;
+
+       if (!urlbuf)
+               urlbuf = buf_alloc();
+
+       buf_append(urlbuf, "https://%s", vpninfo->hostname);
+       if (vpninfo->port != 443)
+               buf_append(urlbuf, ":%d", vpninfo->port);
+       buf_append(urlbuf, "/");
+
+       /* Other protocols don't care and just leave noise from the
+        * authentication process in ->urlpath. Pulse does care, and
+        * you have to *connect* to a given usergroup at the correct
+        * path, not just authenticate.
+        *
+        * https://gitlab.gnome.org/GNOME/NetworkManager-openconnect/-/issues/53
+        * https://gitlab.gnome.org/GNOME/NetworkManager-openconnect/-/merge_requests/22
+        */
+       if (vpninfo->proto->proto == PROTO_PULSE)
+               buf_append(urlbuf, "%s", vpninfo->urlpath);
+       if (buf_error(urlbuf)) {
+               buf_free(urlbuf);
+               vpninfo->connect_urlbuf = NULL;
+               return NULL;
+       }
+
+       vpninfo->connect_urlbuf = urlbuf;
+       return urlbuf->data;
+}
+
 const char *openconnect_get_hostname(struct openconnect_info *vpninfo)
 {
        return vpninfo->unique_hostname?:vpninfo->hostname;
diff --git a/main.c b/main.c
index 504030c5d41bc3861a90c478cfb38938ed518b9e..a0809defeea812371e89998073367bebe0b0d26c 100644 (file)
--- a/main.c
+++ b/main.c
@@ -2097,8 +2097,21 @@ int main(int argc, char **argv)
                /* --authenticate */
                printf("COOKIE='%s'\n", vpninfo->cookie);
                printf("HOST='%s'\n", openconnect_get_hostname(vpninfo));
+               printf("CONNECT_URL='%s'\n", openconnect_get_connect_url(vpninfo));
                printf("FINGERPRINT='%s'\n",
                       openconnect_get_peer_cert_hash(vpninfo));
+               if (vpninfo->unique_hostname) {
+                       char *p = vpninfo->unique_hostname;
+                       int l = strlen(p);
+
+                       if (vpninfo->unique_hostname[0] == '[' &&
+                           vpninfo->unique_hostname[l-1] == ']') {
+                               p++;
+                               l -=2;
+                       }
+                       printf("RESOLVE='%s:%.*s'\n", vpninfo->hostname, l, p);
+               } else
+                       printf("RESOLVE=");
                openconnect_vpninfo_free(vpninfo);
                exit(0);
        } else if (cookieonly) {
index 1ba62fbd42d5007d189f50ec57e4e5eeb99a17fd..fb0a580b055e716848398c887c6f47fc18bac0b0 100644 (file)
@@ -493,10 +493,22 @@ struct openconnect_info {
        struct http_auth_state proxy_auth[MAX_AUTH_TYPES];
 
        char *localname;
-       char *hostname;
-       char *unique_hostname;
+
+       char *hostname; /* This is the original hostname (or IP address)
+                        * we were asked to connect to */
+
+       char *unique_hostname; /* This is the IP address of the actual host
+                               * that we connected to; the result of the
+                               * DNS lookup. We do this so that we can be
+                               * sure we reconnect to the same server we
+                               * authenticated to. */
        int port;
        char *urlpath;
+
+       /* The application might ask us to recreate a connection URL,
+        * and we own the string so cache it for later freeing. */
+       struct oc_text_buf *connect_urlbuf;
+
        int cert_expire_warning;
 
        struct cert_info certinfo[1];
index 389d5a871af6a5655e4508d02ce4d24c21b83192..8dcccc8fc3f8207b0d195c35c263e247cc3aa252 100644 (file)
@@ -37,6 +37,7 @@ extern "C" {
 
 /*
  * API version 5.7:
+ *  - Add openconnect_get_connect_url()
  *  - Add openconnect_set_cookie()
  *  - Add openconnect_set_allow_insecure_crypto()
  *  - Add openconnect_get_auth_expiration()
@@ -453,6 +454,77 @@ const char *openconnect_get_dtls_cipher(struct openconnect_info *);
 const char *openconnect_get_cstp_compression(struct openconnect_info *);
 const char *openconnect_get_dtls_compression(struct openconnect_info *);
 
+
+/*
+ * Since authentication can run in a separate environment to the connection
+ * itself, there is a simple set of information which needs to be passed
+ * from one to the other. Basically it's just the server to connect to, and
+ * the cookie we need for authentication. And luckily, as we've added more
+ * and more protocols over the years, the "cookie" part has remained true
+ * and we haven't needed to use client certificates for the *connection*.
+ *
+ * The *server* part is a little more complex. Firstly, the certificate
+ * might not be valid and may have been accepted manually by the user,
+ * so we pass the certificate fingerprint separately, as a second piece
+ * of information.
+
+ * In the beginning, we passed the server hostname as the third piece of
+ * information, and all was well.
+ *
+ * Then we found servers on a non-standard port, so the authentication
+ * dialogs would use openconnect_get_port() and just append it (":%d")
+ * to the hostname string they passed on.
+ *
+ * Then we encountered servers with round-robin or geo-DNS, which gave
+ * different IP addresses for a given hostname, and we switched the
+ * openconnect_get_hostname() function to return the *IP* address instead,
+ * since the actual name didn't matter when it wasn't being used to check
+ * the server's certificate anyway.
+ *
+ * At some point later, openconnect_get_dnsname() was added to return the
+ * actual hostname, during the authentication phase where the cert was
+ * being presented to the user for manual acceptance. But that wasn't
+ * really important for the authentication → connection handoff.
+ *
+ * Later still, Legacy IP addresses got scarce and SNI was invented, and
+ * we started to see servers behind proxies that forward a connection
+ * based on the SNI in the incoming ClientHello. So returning just the
+ * IP address from openconnect_get_hostname() now made things break.
+ *
+ * So... we need to pass *both* the actual hostname *and* the IP address
+ * to the connecting openconnect invocation. As well as the port.
+ *
+ * In addition, the Pulse protocol introduced a new requirement for the
+ * connection. Instead of connecting to a fixed endpoint on the server,
+ * we must connect to the appropriate *path*, which varies. So in fact
+ * it isn't just the "hostname" any more, but the full URL.
+ *
+ * So, now we have openconnect_get_connect_url() which gets the full URL
+ * including the port and path, and the original hostname.
+ *
+ * Since we're now back to giving openconnect a hosthame, we need to add
+ * a '--resolve' argument to avoid the round robin DNS problem and ensure
+ * that we actually connect to the same server we authenticated to. The
+ * arguments for that can be obtained from openconnect_get_dnsname() and
+ * openconnect_get_hostname() — the latter of which, as noted, was changed
+ * years ago to return a numeric address. We end up invoking openconnect
+ * to make the connection as follows:
+ *
+ * openconnect $CONNECT_URL --servercert $FINGERPRINT --cookie $COOKIE \
+ *             --resolve $DNSNAME:$HOSTNAME
+ *
+ * ... where '$HOSTNAME', as noted, isn't actually a hostname. Sorry.
+ *
+ * In fact, what you get back from openconnect_get_hostname() is the
+ * IP literal in the form it would appear in a URL. So IPv6 addresses
+ * are wrapped in [], and that needs to be *stripped* in order to pass
+ * it to openconnect's --resolve argument. As I realise and type this,
+ * it doesn't seem particularly useful to provide yet another function
+ * that will return the non-[]-wrapped version, as we'd still need UI
+ * tools to do it themselves for backward compatibility. Sorry again :)
+ */
+const char *openconnect_get_connect_url(struct openconnect_info *);
+
 /* Returns the IP address of the exact host to which the connection
  * was made. In --cookieonly mode or in any other scenario involving
  * a "two stage" connection, it is important to reconnect by IP because
index c84e0030ad5d5b56e00309d959e4fbb651c11bd1..f1aac03fcccfc3d0dcbe5ffe42b71b4e666fbad3 100644 (file)
        <li>Ignore failures to fetch the NC landing page if the authentication was successful.</li>
        <li>Add support for <a href="https://arraynetworks.com/products-secure-access-gateways-ag-series.html">Array Networks SSL VPN</a> (<a href="https://gitlab.com/openconnect/openconnect/-/issues/102">#102</a>)</li>
        <li>Support TLSv1.3 with TPMv2 EC and RSA keys, add test cases for swtpm and hardware TPM.</li>
+       <li>Add <tt>openconnect_get_connect_url()</tt> to simplify passing correct server information to
+       to the connecting <tt>openconnect</tt> process. <i>(NetworkManager-openconnect
+       <a href="https://gitlab.gnome.org/GNOME/NetworkManager-openconnect/-/issues/46">#46</a>,
+       <a href="https://gitlab.gnome.org/GNOME/NetworkManager-openconnect/-/issues/53">#53</a>)</i></li>
      </ul><br/>
   </li>
   <li><b><a href="ftp://ftp.infradead.org/pub/openconnect/openconnect-8.10.tar.gz">OpenConnect v8.10</a></b>