]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
make F5 and Fortinet tests go through config-pulling (up to the point of tunnel conne...
authorDaniel Lenski <dlenski@gmail.com>
Mon, 22 Feb 2021 02:44:22 +0000 (18:44 -0800)
committerDaniel Lenski <dlenski@gmail.com>
Mon, 29 Mar 2021 03:57:25 +0000 (20:57 -0700)
The fake servers don't actually implement the tunnel, but they do implement
the pre-tunnel configuration endpoints, and then simulate a "cookie rejected"
response upon tunnel connection.

OpenConnect distinguishes “cookie rejected” errors via an exit status of 2,
whereas any other failure results in an exit status of 1.

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
f5.c
ppp.c
tests/Makefile.am
tests/f5-auth-and-config [moved from tests/auth-f5 with 76% similarity]
tests/fake-f5-server.py
tests/fake-fortinet-server.py
tests/fortinet-auth-and-config [moved from tests/auth-fortinet with 84% similarity]

diff --git a/f5.c b/f5.c
index e2210fe176d198b5a177265895b217a0f94743c3..b36cdfafd4bb2221b6218f7f393f490a207c980f 100644 (file)
--- a/f5.c
+++ b/f5.c
@@ -512,7 +512,7 @@ int f5_connect(struct openconnect_info *vpninfo)
                vpn_progress(vpninfo, PRG_ERR,
                             _("Unexpected %d result from server\n"),
                             ret);
-               ret = -EINVAL;
+               ret = (ret == 504) ? -EPERM : -EINVAL;
                goto out;
        }
 
diff --git a/ppp.c b/ppp.c
index ec7a8805f695a3de959820b97657c66d8335fd8f..72ec1d40801479f20b2ccf986f0a032b04e09944 100644 (file)
--- a/ppp.c
+++ b/ppp.c
@@ -1029,16 +1029,20 @@ int ppp_mainloop(struct openconnect_info *vpninfo, int *timeout, int readable)
                 * of the first packet
                 */
                if (ppp->check_http_response) {
+                       const char *eol;
                        ppp->check_http_response = 0;
                        if (!memcmp(eh, "HTTP/", 5)) {
                                const char *sol = (const char *)eh;
                                const char *eol = memchr(sol, '\r', len) ?: memchr(sol, '\n', len);
+                               const char *sp1 = memchr(sol, ' ', len);
+                               const char *sp2 = memchr(sp1+1, ' ', len - (sp1-sol) + 1);
+                               int status = sp1 && sp2 ? atoi(sp1+1) : -1;
                                if (eol)
                                        len = eol - sol;
                                vpn_progress(vpninfo, PRG_ERR,
                                             _("Got unexpected HTTP response: %.*s\n"), len, sol);
                                vpninfo->quit_reason = "Received HTTP response (not a PPP packet)";
-                               return -EINVAL;
+                               return (status >= 400 && status <= 499) ? -EPERM : -EINVAL;
                        }
                }
 
index 6b16a7a2859b2551fe8b7e5b6fe1e2a1f720daf2..505aee098c2aacb9eaeff437948462e5faa3b11d 100644 (file)
@@ -62,8 +62,8 @@ if HAVE_CWRAP
 dist_check_SCRIPTS += auth-username-pass auth-certificate auth-nonascii cert-fingerprint id-test obsolete-server-crypto pfs
 
 if HAVE_PYTHON36_FLASK
-dist_check_SCRIPTS += auth-fortinet
-dist_check_SCRIPTS += auth-f5
+dist_check_SCRIPTS += fortinet-auth-and-config
+dist_check_SCRIPTS += f5-auth-and-config
 endif
 
 if TEST_PKCS11
similarity index 76%
rename from tests/auth-f5
rename to tests/f5-auth-and-config
index 8f76b79861088f627ab13afbcf0008e2b16fea07..c70eeb7690ca2e70d499f622afec8f6e2db02f8c 100755 (executable)
@@ -41,6 +41,13 @@ echo -n "Authenticating with username/password... "
 
 echo ok
 
+echo -n "Authenticating with username/password, then proceeding to tunnel stage... "
+echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=f5 -q $ADDRESS:443 -u test $FINGERPRINT >/dev/null 2>&1
+test $? = 2 || # what OpenConnect returns when server rejects cookie upon tunnel connection, as the fake server does
+    fail $PID "Something went wrong in fake F5 server (other than the expected rejection of cookie)"
+
+echo ok
+
 cleanup
 
 exit 0
index abb5c02a574f51c3fcf5670d92fe5fef94630868..5981ce75090b68759ef7bbc27324c63d0954d003 100755 (executable)
@@ -105,6 +105,91 @@ def post_policy():
     return resp
 
 
+# Respond to 'GET /vdesk/vpn/index.php3?outform=xml&client_version=2.0 with an XML config
+# [Save VPN resource name in the session for verification of client state later]
+@app.route('/vdesk/vpn/index.php3')
+@require_MRHSession
+def profile_params():
+    print(request.args)
+    assert request.args.get('outform') == 'xml' and request.args.get('client_version') == '2.0'
+    vpn_name = 'demo%d_vpn_resource' % random.randint(1, 100)
+    session.update(step='GET-profile-params', resourcename='/Common/'+vpn_name)
+    # print(session)
+
+    return (f'''
+            <?xml version="1.0" encoding="utf-8"?>
+            <favorites type="VPN" limited="YES">
+              <favorite id="/Common/{vpn_name}">
+                <caption>{vpn_name}</caption>
+                <name>/Common/{vpn_name}</name>
+                <params>resourcename=/Common/{vpn_name}</params>
+              </favorite>
+            </favorites>''',
+            {'content-type': 'application/xml'})
+
+# Respond to 'GET /vdesk/vpn/connect.php3?outform=xml&client_version=2.0&resourcename=RESOURCENAME
+# with an ugliest-XML-you've-ever-seen config.
+# [Save random HDLC flag and ur_Z for verification later.]
+@app.route('/vdesk/vpn/connect.php3')
+@require_MRHSession
+@check_form_against_session('resourcename', use_query=True)
+def options():
+    assert request.args.get('outform') == 'xml' and request.args.get('client_version') == '2.0'
+    session.update(hdlc_framing=['no','yes'][random.randint(0, 1)],
+                   Z=session['resourcename'] + str(random.randint(1, 100)),
+                   ipv4='yes', ipv6=['no','yes'][random.randint(0, 1)],
+                   sess=request.cookies['MRHSession'] + str(random.randint(1, 100)))
+
+    return (f'''
+            <?xml version="1.0" encoding="UTF-8" ?><favorite>
+            <object ID="ur_Host" CLASSID="CLSID:CC85ACDF-B277-486F-8C70-2C9B2ED2A4E7"
+             CODEBASE="https://{app.config['HOST']}:{app.config['PORT']}/vdesk/terminal/urxshost.cab"
+             WIDTH="320" HEIGHT="240">
+                <ur_Z>{session['Z']}</ur_Z>
+                <Session_ID>{session['sess']}</Session_ID>
+                <ur_name>{session['resourcename']}</ur_name>
+                <host0>{app.config['HOST']}</host0>
+                <port0>{app.config['PORT']}</port0>
+                <tunnel_host0>{app.config['HOST']}</tunnel_host0>
+                <tunnel_port0>{app.config['PORT']}</tunnel_port0>
+                <tunnel_protocol0>https</tunnel_protocol0>
+                <idle_session_timeout>900</idle_session_timeout>
+                <IPV4_0>{int(session['ipv4']=='yes')}</IPV4_0>
+                <IPV6_0>{int(session['ipv6']=='yes')}</IPV6_0>
+                <tunnel_dtls>1</tunnel_dtls>
+                <tunnel_port_dtls>{app.config['PORT']}</tunnel_port_dtls>
+
+                <DNS0>1.1.1.1</DNS0>
+                <DNS1>8.8.8.8</DNS1>
+                <DNS6_0>2606:4700:4700::1111</DNS6_0>
+                <DNS6_1>2001:4860:4860::8888</DNS6_1>
+                <WINS0></WINS0>
+                <DNSSuffix0>foo.com</DNSSuffix0>
+                <SplitTunneling0>2</SplitTunneling0>
+                <LAN0>10.11.10.10/32 10.11.1.0/24</LAN0>
+                <LAN6_0>::/1 8000::/1</LAN6_0>
+
+                <DNS_SPLIT0>*</DNS_SPLIT0>
+
+                <hdlc_framing>{session['hdlc_framing']}</hdlc_framing>
+            </object>
+            </favorite>''',
+            {'content-type': 'application/xml'})
+
+
+# Respond to faux-CONNECT 'GET /myvpn' with 504 Gateway Timeout
+# (what the real F5 server responds with when it doesn't like the parameters, intended
+# to trigger "cookie rejected" error in OpenConnect)
+@app.route('/myvpn')
+@check_form_against_session('sess', 'hdlc_framing', 'ipv4', 'ipv6', 'Z', use_query=True)
+def tunnel():
+    try:
+        base64.urlsafe_b64decode(request.args.get('hostname') or None)
+    except (ValueError, TypeError):
+        raise AssertionError('Hostname is not a base64 string')
+    return make_response('', 504, {'X-VPN-Client-IP': '10.11.1.2', 'X-VPN-Client-IPv6': '2601::f00f:1234'})
+
+
 # Respond to 'GET /remote/logout' by clearing session and MRHSession
 @app.route('/remote/logout')
 @require_MRHSession
index 9d51bfbde1f37bd567a02d86bf0c6ba6da049f59..1f3be2dc66e8f5fea192e3c2fb22644d752a07e1 100755 (executable)
@@ -152,6 +152,49 @@ def complete_non_2fa():
     return resp
 
 
+# Respond to 'GET /fortisslvpn with a placeholder stub (since OpenConnect doesn't even try to parse this)
+@app.route('/remote/fortisslvpn')
+@require_SVPNCOOKIE
+def html_config():
+    return 'VPN config in HTML format'
+
+
+# Respond to 'GET /fortisslvpn_xml with a fake config
+@app.route('/remote/fortisslvpn_xml')
+@require_SVPNCOOKIE
+def xml_config():
+    return ('''
+            <?xml version="1.0" encoding="utf-8"?>
+            <sslvpn-tunnel ver="2" dtls="1" patch="1">
+              <dtls-config heartbeat-interval="10" heartbeat-fail-count="10" heartbeat-idle-timeout="10" client-hello-timeout="10"/>
+              <tunnel-method value="ppp"/>
+              <tunnel-method value="tun"/>
+              <fos platform="FakeFortigate" major="1" minor="2" patch="3" build="4567" branch="4567"/>
+              <ipv4>
+                <dns ip="1.1.1.1"/>
+                <dns ip="8.8.8.8" domain="foo.com"/>
+                <split-dns domains='mydomain1.local,mydomain2.local' dnsserver1='10.10.10.10' dnsserver2='10.10.10.11' />
+                <assigned-addr ipv4="10.11.1.123"/>
+                <split-tunnel-info>
+                  <addr ip="10.11.10.10" mask="255.255.255.255"/>
+                  <addr ip="10.11.1.0" mask="255.255.255.0"/>
+                </split-tunnel-info>
+              </ipv4>
+              <idle-timeout val="3600"/>
+              <auth-timeout val="18000"/>
+            </sslvpn-tunnel>''',
+            {'content-type': 'application/xml'})
+
+
+# Respond to faux-CONNECT 'GET /remote/sslvpn-tunnel' with 403 Forbidden
+# (what the real Fortinet server sends when it doesn't like the parameters,
+# intended to trigger "cookie rejected" error in OpenConnect)
+@app.route('/remote/sslvpn-tunnel')
+@require_SVPNCOOKIE
+def tunnel():
+    abort(403)
+
+
 # Respond to 'GET /remote/logout' by clearing session and SVPNCOOKIE
 @app.route('/remote/logout')
 @require_SVPNCOOKIE
similarity index 84%
rename from tests/auth-fortinet
rename to tests/fortinet-auth-and-config
index 60d97ad193bdc2829362dca5820e98665486eb3b..79b86e6f14c0d215fcce461e4e75620cca6ab95a 100755 (executable)
@@ -59,6 +59,13 @@ echo -n "Authenticating with username/password/token and NON-DEFAULT path... "
 
 echo ok
 
+echo -n "Authenticating with username/password, then proceeding to tunnel stage... "
+echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=fortinet -q $ADDRESS:443 -u test $FINGERPRINT >/dev/null 2>&1
+test $? = 2 || # what OpenConnect returns when server rejects cookie upon tunnel connection, as the fake server does
+    fail $PID "Something went wrong in fake Fortinet server (other than the expected rejection of cookie)"
+
+echo ok
+
 cleanup
 
 exit 0