Re: Activating a VPN connection without displaying the authentication dialog



On Sat, 2020-08-15 at 22:51 +0300, Ionuț Leonte via networkmanager-list 
wrote:
Hello,

The company I work for uses Cisco AnyConnect and I have been using
NetworkManager with the OpenConnect plugin to connect to it for some
time
now. Soon, however, they will be enabling SAML (for 2FA with Azure
AD)
which is not supported in the OpenConnect plugin for NetworkManager.

I've written a Python script that does the initial handshake/2FA and
obtains
the 3 secrets needed to connect - the cookie, gateway certificate
hash and
the gateway address. I am sure these secrets are correct because I
can
manually run openconnect, pass the secrets to it, and it successfully
connects. This is functional but not great with regards to 'user
experience'.

I would like to programmatically configure a NetworkManager VPN
connection
and activate it using the secrets I obtain via the manual handshake.
The
problem I am facing is that I always get the VPN authentication
dialog, even
though I set the secrets on the connection.

The question I have is: is there a way to tell NetworkManager that it
can
skip the authentication step and just bring up the connection with
the
secrets I already provided?

Here is the code I have come up with so far:

    import uuid
    import gi
    gi.require_version('NM', '1.0')
    from gi.repository import NM, GLib
    g_vpn_server = 'vpn.example.com'
    (cookie, gwcert) = do_saml_2fa(g_vpn_server)  # implemented
elsewhere

    def create_profile(name: str) -> NM.SimpleConnection:
        profile = NM.SimpleConnection.new()
        s_con = NM.SettingConnection.new()
        s_con.set_property(NM.SETTING_CONNECTION_ID, name)
        s_con.set_property(NM.SETTING_CONNECTION_UUID,
str(uuid.uuid4()))
        s_con.set_property(NM.SETTING_CONNECTION_TYPE, "vpn")

        s_ip4 = NM.SettingIP4Config.new()
        s_ip4.set_property(NM.SETTING_IP_CONFIG_METHOD, "auto")

        s_vpn = NM.SettingVpn.new()
        s_vpn.set_property(
           'service-type',
           'org.freedesktop.NetworkManager.openconnect')

        s_vpn.add_data_item('protocol', 'anyconnect')
        s_vpn.add_data_item('gateway', g_vpn_server)
        s_vpn.add_data_item('cacert', '<redacted>')
        s_vpn.add_data_item('usercert', '<redacted>')
        s_vpn.add_data_item('userkey', '<redacted>')
        s_vpn.add_data_item('enable_csd_trojan', 'no')
        s_vpn.add_data_item('pem_passphrase_fsid', 'no')

        s_vpn.add_secret('autoconnect', 'yes')
        s_vpn.add_secret('save_passwords', 'no')

        profile.add_setting(s_con)
        profile.add_setting(s_ip4)
        profile.add_setting(s_vpn)

        return profile

    def handle_connection_activate(nm, res, loop):
        try:
            ret = nm.activate_connection_finish(res)
            print(ret)
        except Exception as e:
            print(f'ERROR: {e}')
        loop.quit()

    def handle_changes_saved(rc, res, data):
        remote_conn, loop = data
        ret = remote_conn.commit_changes_finish(res)
        print(f'CHANGES SAVED: {ret}')

        if ret:
            nm.activate_connection_async(
                remote_conn,
                None,
                None,
                None,
                handle_connection_activate, loop)
        else:
            loop.quit()

    def handle_connection_add_finish(nm, res, data):
        loop, cookie, gateway, gwcert = data
        ret = nm.add_connection_finish(res)
        print(f'{type(ret)} - {ret.get_path()}')
        s_vpn = ret.get_setting_vpn()
        s_vpn.add_secret('cookie', cookie)
        s_vpn.add_secret('gateway', gateway)
        s_vpn.add_secret('gwcert', gwcert)

        s_vpn.set_secret_flags('cookie',
NM.SettingSecretFlags.NOT_SAVED)
        s_vpn.set_secret_flags('gateway',
NM.SettingSecretFlags.NOT_SAVED)
        s_vpn.set_secret_flags('gwcert',
NM.SettingSecretFlags.NOT_SAVED)

        print(ret.need_secrets(), ret.verify_secrets())
        ret.commit_changes_async(
            False,
            None,
            handle_changes_saved,
            (ret, loop))

    nm = NM.Client.new(None)
    p = create_profile('vpn-sso')
    print(p.verify_secrets(), p.verify())
    loop = GLib.MainLoop()
    nm.add_connection_async(
        p,
       True,
       None,
       handle_connection_add_finish,
       (loop, cookie, g_vpn_server, gwcert))
    loop.run()


hi,

if you set 

  s_vpn.set_secret_flags('cookie', NM.SettingSecretFlags.NOT_SAVED)

during connection add/update, then the secret will be lost right away.
I don't think you can provide ALWAYS_ASK secrets this way.

I think you need to run a "secret-agent". That is basically what nm-
applet, nmcli, nmtui can do.

They use the NMSecretAgentOld class ([1]). That is public (and stable)
API, despite the odd name. The problem is, that this libnm API is
rather arcane. You have to subclass the type. I think you can do that
with pygobject, but it's probably not entirely convenient.

The alternative is to use the D-Bus API directly. libnm is only a
wrapper around D-Bus, so you can achieve the same result. Still,
implementing the D-Bus API isn't entirely straight forward either. 
Your application needs to implement the D-Bus service [2] and you need
to register your application by calling [3]. 


[1] 
https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/80c93b0e5e1554fe894d5e94c59ebb29b702da42/libnm/nm-secret-agent-old.c
[2] 
https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/80c93b0e5e1554fe894d5e94c59ebb29b702da42/introspection/org.freedesktop.NetworkManager.SecretAgent.xml
[3] 
https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/blob/80c93b0e5e1554fe894d5e94c59ebb29b702da42/introspection/org.freedesktop.NetworkManager.AgentManager.xml


The Secret API isn't great, patches for improvements are welcome (but
it's not so easy).


best,
Thomas

Attachment: signature.asc
Description: This is a digitally signed message part



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]