Daniel Lenski [Fri, 23 Apr 2021 23:39:04 +0000 (16:39 -0700)]
F5, Fortinet: ignore errors in landing page once we've got a cookie
Like Juniper, F5 and Fortinet redirect the user to a landing page (“webtop”)
after a successful authentication. We should ignore HTTP errors in fetching
the landing page if/when we've received a valid cookie, as done for Juniper
in 3e77943692b511719d9217d2ecc43588b7c6c08b.
Tests for {f5,fortinet}-auth-and-config are still passing with these
changes.
David Woodhouse [Thu, 22 Apr 2021 15:46:17 +0000 (16:46 +0100)]
Rework cstp_options and ip_info handling
Build up the new opt list and ip_info first, then have a function to
"install" them into the vpninfo, doing the sanity check at that point.
Should make the whole thing a little bit less vile.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Wed, 21 Apr 2021 04:17:29 +0000 (21:17 -0700)]
GlobalProtect IPv6 support
Turns out that I got *almost* everything right in d6db0ec03394234d41fbec7ffc794ceeb486a8f0, including the Ethertype field; the
only part I overlooked is that `ipv6-support=yes` also needs to be included
in the `/ssl-vpn/getconfig.esp` request parameters.
Sincere thanks to the user and GlobalProtect VPN administrator who provided
me with access to a GlobalProtect VPN supporting IPv6. This was a huge
help! 🙏
Still unknown/TODO: ensure that GlobalProtect IPv6 works sanely with ESP.
- For Fortinet, vpninfo->ppp_dtls_connect_req differs from
vpninfo->ppp_tls_connect_req. For F5 they are identical.
- For Fortinet, the DTLS/UDP port always matches the TLS/TCP port.
For F5 they can differ.
Daniel Lenski [Wed, 14 Apr 2021 19:40:37 +0000 (12:40 -0700)]
Fortinet: don't keep retrying if cookie is invalid on reconnect
The fortinet_configure() requests return 302 redirects to '/remote/login'
if the auth session/cookie is no longer valid. We should detect this and
return -EPERM rather than -EINVAL, so that ssl_reconnect() doesn't keep
trying to reconnect.
NB: Detecting this redirect is perhaps a bit harder than it should be,
because do_https_request() returns 0, rather than the real HTTP status
code (e.g. 302), in the case of an successful-but-unfetched redirect.
David Woodhouse [Mon, 12 Apr 2021 16:40:17 +0000 (17:40 +0100)]
Add F5 DTLS support
Add a new f5_udp_mainloop() which handles the DTLS handshakes and then
sends the GET request to start the tunnel, and handles the HTTP response
with the IP addresses (using some more crappy HTTP parsing of its own).
Instead of storing all the parameters required to create that request,
it's created in advance and stored in a new vpninfo->ppp_tls_connect_req
field by the f5_configure() function which has been split out from
f5_connect(). (Originally intended not to be called a second time, but
actually the server doesn't send packets down a newly-established
tunnel to us unless we refetch the profile etc.)
There's also a new vpninfo->ppp_dtls_connect_req field which is unused
for now but Fortinet will want it since its request format differs for
TLS vs. DTLS.
What's left of the f5_connect() function now only actually establishes
the PPP connection over TLS if DTLS is disabled, as expected by the
recent changes to ppp_tcp_mainloop().
NB: If there is packet loss and we drop the *response* to the GET
request, the server won't resend it; it just goes on to PPP negotiation.
We might actually be able to cope with that at least for Legacy IP,
since it'll give us our address that way. Not for IPv6 though, as the LL
address it gives us in PPP isn't useful.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Tue, 13 Apr 2021 04:52:14 +0000 (05:52 +0100)]
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 <dwmw2@infradead.org>
David Woodhouse [Wed, 14 Apr 2021 22:42:50 +0000 (23:42 +0100)]
Fix handling of lost TERMACK
The comment said we'd send a TERMREQ "once more" after 3 seconds if we
didn't get a TERMACK in response to the first one. It transpires this
claim was false. If we didn't get a response to the first one, we would
send a *flood* of TERMREQs until we filled the socket sndbuf, with no
delay between them at all because we weren't updating ->lcp.last_req.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Sun, 11 Apr 2021 16:46:09 +0000 (17:46 +0100)]
ppp: Clean up negotiated IP/DNS option handling
We were leaking these strings. Fix it up to properly attempt negotiation
and set options in the option list with the results.
Also make it abort IPCP if the server doesn't accept a Legacy IP address
which had already been configured. We can't do the same for IPv6 as PPP
only actually configures the link-local address.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Fri, 12 Feb 2021 01:48:40 +0000 (17:48 -0800)]
Add start_dtls_anon_handshake() for PPP protocols
Instead of PSK or fake-resumed sessions, F5 and Fortinet just do normal
DTLS.
Which isn't actually "normal" for us, so add start_dtls_anon_handshake()
to support it. This needs to add certificate validation, which wasn't
necessary with PSK or session resumption which were effectively mutual
authentication anyway.
It is invoked whenever the vpninfo->dtls_cipher field is empty. The
Cisco AnyConnect support *always* sets that; the normal DTLS protocols
never will.
Rename and repurpose the vpninfo->cisco_dtls12 field to ->dtls12. For
Cisco it doesn't change its semantics, but for normal DTLS it indicates
whether we can try DTLSv1.2 or not. It doesn't guarantee that the server
*will* do DTLSv1.2; it only lets us *try*. So it should be set
everywhere except for the older F5 BIG-IP servers (v15 and earlier)
which don't set the <dtls_v1_2_supported> tag and which actually crap
themselves and abort the connection if we try DTLSv1.2 instead of
negotiating down to DTLSv1.0 sanely.
Signed-off-by: Daniel Lenski <dlenski@gmail.com> Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Tue, 20 Apr 2021 12:52:44 +0000 (13:52 +0100)]
Add DTLS_ESTABLISHED state
Cisco DTLS is preauthenticated by PSK or the session resume hack, but
with anonymous DTLS there will be authentication over the DTLS channel
once it has been connected. Add a new state DTLS_ESTABLISHED, and let
DTLS_CONNECTED mean merely *connected* at the transport level.
ESP used to use DTLS_CONNECTING state to indicate that the ESP probes
had been received and we were ready to tell the server (over the TCP
channel) to switch over to use ESP. Use DTLS_CONNECTED for that state
now, since it's a better fit. And DTLS_ESTABLISHED when we've actually
told the server to change.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Wed, 14 Apr 2021 10:22:39 +0000 (11:22 +0100)]
Fix timeout handling for DTLS handshake retries
We weren't actually waking the mainloop up when the TLS library wanted
to (re)send a DTLS handshake. Plumb the timeout pointer through
dtls_try_handshake() to fix that.
Also feed it to dtls_reconnect(), which helps us ensure that we wake up
after dtls_attempt_period() to try again.
Export dtls_reconnect() while we're at it, since PPP will want to be
able to invoke it.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Sat, 17 Apr 2021 14:12:45 +0000 (07:12 -0700)]
Fix f5-auth-and-config tests not to depend on cookies
Tests depended on cookies propagating all the way to the tunnel
connection ('GET /tmyvpn'). Real servers don't need to see the cookie
at this point though, and for the DTLS connection we don't *want* to
include it because the resulting packet could end up too large. So
don't require it.
Daniel Lenski [Wed, 21 Apr 2021 00:26:30 +0000 (17:26 -0700)]
Speculatively enable no_terminate_on_pause for Fortinet
Given what we *have* learned so far about Fortinet reconnects
(https://gitlab.com/openconnect/openconnect/-/issues/235#note_552995833), it
appears likely that, as for F5, the PPP connection should *not* be
Terminated prior to a pause-and-reconnect.
Daniel Lenski [Sat, 17 Apr 2021 09:59:51 +0000 (02:59 -0700)]
Pulse should fallback to Juniper logout
The Pulse logout function, pulse_bye(), can fail to logout the session if the IFT-T tunnel is already closed. As a fallback, use oncp_bye() which logs out via a new HTTP request.
David Woodhouse [Mon, 12 Apr 2021 11:02:36 +0000 (12:02 +0100)]
Add DTLS support to ssl_nonblock_read() / ssl_nonblock_write()
We previously lived with #ifdef hacks for OpenSSL vs. GnuTLS in dtls.c
because there were only a few call sites and only one each for send/recv
where the -EAGAIN handling actually matters.
Now that we're going to want to send/receive DTLS frames for PPP too,
clean it up a bit.
While we're at it, fix up the OpenSSL version of ssl_nonblock_read()
so that it only ignores SSL_ERROR_WANT_READ and SSL_ERROR_WANT_WRITE.
Previously it ignored *every* error except SSL_ERROR_ZERO_RETURN and
SSL_ERROR_SYSCALL.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Wed, 14 Apr 2021 22:32:02 +0000 (15:32 -0700)]
Handle F5 split-exclude routes
Minor related changes:
• Align the cstp_options names for netmask and split {in,ex}clude routes
with other protocols
• Stop logging IP version for DNS servers and split routes, since we store
them in the same place regardless of version.
David Woodhouse [Mon, 12 Apr 2021 09:57:23 +0000 (10:57 +0100)]
Remove Cisco-specific option handling from dtls_setup()
As we start to use DTLS for protocols other than Cisco AnyConnect, we
need to start disentangling the underlying DTLS support from the Cisco
protocol. Start by moving the X-DTLS-Foo: header processing into cstp.c
where half of it was anyway.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Thu, 15 Apr 2021 15:36:15 +0000 (16:36 +0100)]
Fix -EAGAIN on writing DTLS socket for PPP mainloop
If the socket sndbuf is full, we fail to send a packet. This should
actually happen less often for UDP than it does for TCP because of the
lack of back pressure from the peer, but it's still a fairly normal
condition and needs to be handled properly.
The correct handling is basically to do nothing and wait for the jam to
clear, unless it's been *so* long that DPD has caused us to think the
peer is dead, or we need a rekey (which we do be reconnecting the
transport channel). The ka_stalled_action() function exists specifically
to handle this case, and returns one of KA_NONE, KA_DPD_DEAD or KA_REKEY.
There's no ponit in it returning KA_DPD or KA_KEEPALIVE like the normal
keepalive_action() does because the point here is that we *can't* send
anything at the moment anyway.
The problem here is that the PPP mainloop doesn't handle those three
cases correctly, and KA_REKEY and KA_NONE were just falling through to
end up at the fatal error condition for a *short* write, which should
never happen. I went too far when commenting out the cut and pasted
mainloop to create ppp_mainloop.
Fix KA_REKEY to do a reconnect, and KA_NONE to just return work_done
as it should.
Fixes: a47406b43 ("add support for PPP-based protocols") Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Tue, 13 Apr 2021 16:49:44 +0000 (09:49 -0700)]
Don't call connection script in ssl_reconnect if tunnel is not up
This refines the DTLS-to-TLS fallback in ppp_tcp_mainloop.
Calling the connection script (with the attempt-reconnect and reconnect
reasons) is confusing if this is actually the *initial* connection, and
the tunnel isn't up yet.
Daniel Lenski [Thu, 15 Apr 2021 00:55:42 +0000 (17:55 -0700)]
NC/Pulse idle timeout
Per #234, it appears that:
- oNCP server termination reason 8 indicates idle timeout
- Pulse AVP 0x583/0xd75 indicates the idle timeout in seconds
- No sign of an idle timeout TLV for oNCP
Daniel Lenski [Tue, 13 Apr 2021 20:49:14 +0000 (13:49 -0700)]
Update 'Getting Started / Connecting' docs
- Remove section on patching for DTLS support (thankfully, long-obsolete)
- Clarify meaning of "certificate" as TLS/SSL client certificate; lots of
users appear to be confused by this
- Mention both -c and -k options
- Refer to manual for more automation of authentication
- Clarify when Jailbreak might be needed to extract certificates on
Windows (when the private key is marked as "non-exportable")
Daniel Lenski [Mon, 12 Apr 2021 23:49:00 +0000 (16:49 -0700)]
Accept IPv6 netmasks like /dead:beef::, in addition to /N
Some F5 server configurations appear to require an IPv6 netmask in this
form.
This adds a netmasklen6() function, analogous to the Legacy IP version in
netmasklen(). It also adds gcc-optimized forms, using __builtin_clz, for
both netmasklen functions.
[dwmw2: Detect __builtin_clz() with autoconf, wrap it once in cls()] Signed-off-by: Daniel Lenski <dlenski@gmail.com> Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Mon, 12 Apr 2021 22:05:07 +0000 (23:05 +0100)]
Handle empty response buf in process_http_response()
If there is no body and the 'body' buf has never had any data in it, then
body->data can be NULL. So don't dereference it when trying to ensure a
NUL at the end.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Mon, 12 Apr 2021 03:23:11 +0000 (20:23 -0700)]
Set Fortinet DPD interval from server's config
The name of the tag implies that its purpose is DTLS-specific (<dtls-config heartbeat-interval="X">)
and that it requires the use of the DTLS heartbeat extension (https://tools.ietf.org/html/rfc6520).
However, PPP already contains natural analogues for keepalive (Discard-Request) and DPD (Echo-Request
and Echo-Reply), which appear to work just fine with Fortinet servers. We may as well use the server's
heartbeat/keepalive interval for a more generic, transport-agnostic DPD mechanism, since OpenConnect
and PPP already support it.
The official Fortinet client software for Windows/Mac appears to be very deficient at dead peer
detection, so this may represent a substantial improvement in functionality for some users.
David Woodhouse [Sun, 11 Apr 2021 17:02:39 +0000 (18:02 +0100)]
Fix --disable-ipv6 option
Commit 7e2129f8a9 ("ensure that openconnect_disable_{dtls,ipv6} do
nothing if vpninfo has ever been connected") added a check to disallow
setting this... if it wasn't already set, OR the connection had
previously been established. Which seems wrong; I think that was
supposed to have been *AND*.
Not that I object very much to this not working, a whole quarter of a
century after RFC1883 was published. But I wanted to use it for testing
PPP negotiation, and it wasn't working.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Fri, 9 Apr 2021 12:09:11 +0000 (13:09 +0100)]
Add 'proto' integer value to struct vpn_proto
Despite making no mention of this in the commit message, commit 423449253 ("rename (resp_buf, form_buf) → (req_buf, resp_buf) in f5.c
and fortinet.c") added a 'flavor' field to a bunch of functions in
auth-html.c and passed it through from the protocol code to indicate
some protocol-specific quirks for form parsing.
Until then, we had mostly structured the code so that there weren't many
cases where 'core' code needed to behave differently depending on the
protocol. There was only one case, in fact, to handle the stupid Pulse
ESP address family bug. That one *used* to check which protocol was in
use by comparing proto->udp_send_probes with &oncp_esp_send_probes.
So, add an integer 'proto' field to struct vpn_proto so it can be
checked without a strcmp. Remove the now-redundant 'flavor' field from
all those functions in auth-html.c since it's now in the vpninfo, and
make the ESP code check that for NC|PULSE too instead of the trick of
checking the proto->udp_send_probes function. Which was kind of icky.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Thu, 8 Apr 2021 21:32:32 +0000 (14:32 -0700)]
Multi-protocol support documentation
Mention F5 and Fortinet on the index page, explain the multi-protocol design
philosophy a bit more, and add a page with a concise list of the supported
protocols.
Daniel Lenski [Thu, 8 Apr 2021 21:35:08 +0000 (14:35 -0700)]
Expand F5 and Fortinet documentation
Explain currently-supported authentication modes, and request feedback on additional modes.
Explain Fortinet's misfeature/design flaw, which prevents it from automatically reconnecting after a dropped or roamed connection, without a new authentication. Request feedback if anyone has acceess to a Fortinet VPN that *doesn't* have this flaw.
Daniel Lenski [Mon, 5 Apr 2021 15:37:00 +0000 (08:37 -0700)]
Use oc_text_buf for internal_get_url()
Commit c8ca180f ("factor out internal_get_url function") consolidated
two pieces of code which reconstruct the URL from its constituent parts
into a single internal_get_url() function.
Both the original versions used oc_text_buf() and built up the URL
string in a simple and readable fashion. The new internal_get_url()
function used asprintf with "%s%s%s" and ternary operators ?"/":"" in an
apparent attempt to win an obfuscated C contest.
Unsurprisingly, the first attempt was buggy; this was subsequently fixed
in commit f78f0eccc ("bugfix internal_get_url") but the horridness
remained.
In addition, the original two versions had slightly different behaviour —
the one in auth-juniper.c would *always* append a trailing / at the end
of the URL string even if vpninfo->urlpath was NULL, while the one in
AnyConnect's auth.c would only do so if it was non-NULL (including when
it's set to "").
As well as switching back to using oc_text_buf to build up the string so
that we can read the code without our eyes bleeding, fix it to always
add the trailing / even when vpninfo->urlpath is NULL.
Signed-off-by: Daniel Lenski <dlenski@gmail.com> Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Fri, 2 Apr 2021 07:57:14 +0000 (00:57 -0700)]
add test path including frmSelectRoles
frmSelectRoles is a "form" that sort of acts like a realm dropdown except that…
(1) It comes AFTER the credentials have been submitted (¯\_(ツ)_/¯)
(2) It doesn't actually contain any form fields. It only contains links to
a small number of role choices. It then redirects to 'GET login.cgi'.
Inane, but apparently used in the real world. See examples at:
https://gitlab.gnome.org/GNOME/NetworkManager-openconnect/-/issues/30#note_988366
David Woodhouse [Sat, 27 Mar 2021 16:17:57 +0000 (16:17 +0000)]
Add Wintun support
Refactor the device discovery a little bit to accommodate it. We need
to scan the registry as we did for TAP-Windows anyway, in order to
find the TUNIDX that vpnc-script-win.js will use to configure routes.
So run through the scan as before, but *if* nothing is found and the
user specified an ifname, attempt to create a Wintun device with that
name. Then scan again and presumably we'll find it this time.
This should hopefully make it relatively harmless to existing setups
using TAP-Windows.
There's lots of memcpy here as Wintun *really* wants us to use its pool.
More thought required for that one, as it can't even handle stripping
packet headers so even if we decrypt directly into its buffer that
wouldn't be enough.
The configuration is also fairly horrid, as vpnc-script-win.js seems
to depend on the Legacy IP address being set on the interface before
it will set up the routes. Which of course assumes that there *is*
Legacy IP at all, and it isn't an IPv6-only tunnel.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Mon, 29 Mar 2021 23:59:08 +0000 (16:59 -0700)]
bugfix !165 Juniper forms handling
I missed a small piece of my original "fix for forms with 'id' but no 'name'" (this one:
https://gitlab.com/openconnect/openconnect/-/commit/c8a6ee94180a09339eafefe0a2d889e7aa508e56#5503a3e92468be6db7c76e16b82bb5f3e9b14131_260_259)
in rebasing the ppp_core branch to merge cleanly with the changes from !171.
Caused this crash: https://gitlab.com/openconnect/openconnect/-/merge_requests/169#note_540411103
TODO: add Flask-based tests of Juniper auth to verify Juniper form handling.
David Woodhouse [Mon, 29 Mar 2021 16:44:21 +0000 (17:44 +0100)]
Fix installer deps
Just including .openconnect.exe.d from the main Makefile means it always
has to be up to date. Which means we always have to *build* openconnect,
even if it was just invoked for 'make clean'. That's bogus.
So shift the DLL dependency tracking out into its own Makefile, to be
invoked recursively. Much as I hate recursive make in the general case,
this is one of the few cases where it makes sense.
The automatic dependencies weren't working very well anyway, as it was
happy to 'discover' openconnect.exe and libopenconnect-5.dll in the MinGW
sysroot instead of using the locally built ones, *and* libtool messes up
the filenames so you can't just depend on .libs/openconnect.exe anyway.
Fix up $(DISTCLEANFILES) to be a bit more complete while we're at it.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Mon, 29 Mar 2021 16:09:32 +0000 (17:09 +0100)]
Fix handling of downloaded files
We want the MinGW package builds to have the downloads provided in advance
as part of the source RPM, instead of requiring network access and fetching
them once for each of the win32 and win64 builds.
So let make find the file in its VPATH and then use the full pathname that
it puts into $< for the recipe. And use that recipe to validate the hash
of the file too.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
David Woodhouse [Mon, 29 Mar 2021 10:17:21 +0000 (11:17 +0100)]
Turn off -Wdeclaration-after-statement and allow C99
I think this far into the 21st century we can allow declarations to be
in the middle of code blocks. Even MSVC supports that, even if it doesn't
do other parts of C99 like named struct initialisers.
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
Daniel Lenski [Mon, 29 Mar 2021 03:41:59 +0000 (20:41 -0700)]
flask-based tests: give up on CentOS7
For reasons that are not clear, OpenConnect fails to handshake a TLS connection
to Python 3.6 and Flask (from CentOS 7 EPEL), and I don't want to install
a 7-year-old distribution on a VM just to figure out why.
See https://gitlab.com/openconnect/openconnect/-/jobs/1135245618#L520 for
verification of the issue, and 9df05119a30dc3405ab56edbcab2576f46cac799
for a possibly-related issue with even older CentOS 6.
Daniel Lenski [Sun, 14 Mar 2021 18:38:38 +0000 (11:38 -0700)]
don't require F5 forms other than first one to have any particular name/ID
This allow-list of known forms is a relic of the HTML-form-parsing code
which was originally written for the Juniper protocol, and requires a bunch
of special-casing. I don't think it's particularly relevant for F5 forms.