dlenski / vpn-slice

vpnc-script replacement for easy and secure split-tunnel VPN setup
GNU General Public License v3.0
726 stars 87 forks source link

Add support for Dynamic Split Include Tunneling #68

Open Vanuan opened 3 years ago

Vanuan commented 3 years ago

When authenticating to Cisco ASA it responds with the following section in xml:

<custom-attr>
<dynamic-split-include-domains><![CDATA[domain1.com, domain2.com]]></dynamic-split-include-domains>
</custom-attr>

Is it possible to add those automatically when connecting via openconnect?

Vanuan commented 3 years ago

Documentation for this feature is here: https://www.cisco.com/c/en/us/support/docs/security/anyconnect-secure-mobility-client/215383-asa-anyconnect-dynamic-split-tunneling.html

dlenski commented 3 years ago

When authenticating to Cisco ASA it responds with the following section in xml:

<custom-attr>
<dynamic-split-include-domains><![CDATA[domain1.com, domain2.com]]></dynamic-split-include-domains>
</custom-attr>

I have never seen this XML before. Can you give more information? Exactly when/where does this show up in the Cisco ASA's response?

Can you show a log of openconnect -vvv --dump-http-traffic with as much detail as possible, so that I can understand better where it appears?

I want to know if these split-DNS domains also show up in the X-CSTP-Split-DNS headers, or if they're separate; if the latter, we would need to modify openconnect as well to notice them.

Is it possible to add those automatically when connecting via openconnect?

I believe this would fall under the general topic of #fancy-split-dns which we have been discussing for a while now.

Also see https://gitlab.com/openconnect/vpnc-scripts/-/issues/5 for some further discussion of how split-DNS can be handled.

Vanuan commented 3 years ago
Connected to HTTPS on example.com
> POST / HTTP/1.1
> Host: example.com
> User-Agent: 
> Accept: */*
> Accept-Encoding: identity
> X-Transcend-Version: 1
> X-Aggregate-Auth: 1
> X-AnyConnect-Platform: 
> X-Support-HTTP-Auth: true
> X-Pad: 0000000000000000000000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 231
> 
> <?xml version="1.0" encoding="UTF-8"?>
> <config-auth client="vpn" type="init"><version who="vpn">***</version><device-id>***</device-id><group-access>https://example.com</group-access></config-auth>
Got HTTP response: HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-store
Pragma: no-cache
Connection: Keep-Alive
Date: Tue, 06 Oct 2020 16:43:25 GMT
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Aggregate-Auth: 1
HTTP body chunked (-2)
< <?xml version="1.0" encoding="UTF-8"?>
< <config-auth client="vpn" type="auth-request" aggregate-auth-version="2">
< <opaque is-for="sg">
< <tunnel-group>***</tunnel-group>
< <group-alias>***</group-alias>
< <config-hash>***</config-hash>
< </opaque>
< <auth id="main">
< <title>Login</title>
< <message>Please enter your username and password.</message>
< <banner></banner>
< <form>
< <input type="text" name="username" label="Username:"></input>
< <input type="password" name="password" label="Password:"></input>
< <select name="group_list" label="GROUP:">
< <option selected="true">***</option>
< </select>
< </form>
< </auth>
< </config-auth>
XML POST enabled
Please enter your username and password.
GROUP: []:
POST https://example.com/
> POST / HTTP/1.1
> Host: example.com
> User-Agent:  
> Accept: */*
> Accept-Encoding: identity
> X-Transcend-Version: 1
> X-Aggregate-Auth: 1
> X-AnyConnect-Platform: 
> X-Support-HTTP-Auth: true
> X-Pad: 0000000000000000000000000000000000000000000000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 271
> 
> <?xml version="1.0" encoding="UTF-8"?>
> <config-auth client="vpn" type="init"><version who="vpn">***</version><device-id>***</device-id><group-access>https://example.com/</group-access><group-select>***</group-select></config-auth>
Got HTTP response: HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-store
Pragma: no-cache
Connection: Keep-Alive
Date: Tue, 06 Oct 2020 16:43:25 GMT
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Aggregate-Auth: 1
HTTP body chunked (-2)
< <?xml version="1.0" encoding="UTF-8"?>
< <config-auth client="vpn" type="auth-request" aggregate-auth-version="2">
< <opaque is-for="sg">
< <tunnel-group>***</tunnel-group>
< <group-alias>***</group-alias>
< <config-hash>***</config-hash>
< </opaque>
< <auth id="main">
< <title>Login</title>
< <message>Please enter your username and password.</message>
< <banner></banner>
< <form>
< <input type="text" name="username" label="Username:"></input>
< <input type="password" name="password" label="Password:"></input>
< <select name="group_list" label="GROUP:">
< <option selected="true">***</option>
< </select>
< </form>
< </auth>
< </config-auth>
XML POST enabled
Please enter your username and password.
Password:
POST https://example.com/
> POST / HTTP/1.1
> Host: example.com
> User-Agent: 
> Accept: */*
> Accept-Encoding: identity
> X-Transcend-Version: 1
> X-Aggregate-Auth: 1
> X-AnyConnect-Platform: ***
> X-Support-HTTP-Auth: true
> X-Pad: 00000000000000000000000000000
> Content-Type: application/x-www-form-urlencoded
> Content-Length: 483
> 
> <?xml version="1.0" encoding="UTF-8"?>
> <config-auth client="vpn" type="auth-reply"><version who="vpn">***</version><device-id>***</device-id><opaque is-for="sg">
> <tunnel-group>***</tunnel-group>
> <group-alias>***</group-alias>
> <config-hash>***</config-hash>
> </opaque><auth><username>***</username><password>***</password></auth><group-select>***</group-select></config-auth>
Got HTTP response: HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Cache-Control: no-store
Pragma: no-cache
Connection: Keep-Alive
Date: Tue, 06 Oct 2020 16:43:37 GMT
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Aggregate-Auth: 1
HTTP body chunked (-2)
< <?xml version="1.0" encoding="UTF-8"?>
< <config-auth client="vpn" type="complete" aggregate-auth-version="2">
< <session-id>***</session-id>
< <session-token>***</session-token>
< <auth id="success">
< <message id="0" param1="" param2=""></message>
< </auth>
< <capabilities>
< <crypto-supported>***</crypto-supported>
< </capabilities>
< <config client="vpn" type="private">
< <vpn-base-config>
< <base-package-uri>***</base-package-uri>
< <server-cert-hash>***</server-cert-hash>
< </vpn-base-config>
< <opaque is-for="vpn-client"><service-profile-manifest>
< <ServiceProfiles rev="1.0">
<   <Profile service-type="user">
<     <FileName></FileName>
<     <FileExtension>xml</FileExtension>
<     <Directory></Directory>
<     <DeployDirectory></DeployDirectory>
<     <Description>***</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="nam">
<     <FileName>configuration.xml</FileName>
<     <FileExtension>nsp</FileExtension>
<     <Directory>Network Access Manager\system</Directory>
<     <DeployDirectory>***</DeployDirectory>
<     <Description>NAM Service Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="feedback">
<     <FileName>CustomerExperience_Feedback.xml</FileName>
<     <FileExtension>fsp</FileExtension>
<     <Directory>CustomerExperienceFeedback</Directory>
<     <DeployDirectory>CustomerExperienceFeedback</DeployDirectory>
<     <Description>Feedback Service Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="telemetry">
<     <FileName>Telemetry_ServiceProfile.xml</FileName>
<     <FileExtension>tsp</FileExtension>
<     <Directory>Telemetry</Directory>
<     <DeployDirectory>Telemetry</DeployDirectory>
<     <Description>Telemetry Service Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="websecurity">
<     <FileName>WebSecurity_ServiceProfile.wso</FileName>
<     <FileExtension>wsp</FileExtension>
<     <DerivedFileExtension>wso</DerivedFileExtension>
<     <Directory>websecurity</Directory>
<     <DeployDirectory>websecurity</DeployDirectory>
<     <Description>Web Security Service Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="iseposture">
<     <FileName>ISEPostureCFG.xml</FileName>
<     <FileExtension>isp</FileExtension>
<     <Directory>iseposture</Directory>
<     <DeployDirectory>iseposture</DeployDirectory>
<     <Description>ISE Posture Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="iseposturejson">
<     <FileName>ISEPosture.json</FileName>
<     <FileExtension>json</FileExtension>
<     <Directory>iseposture</Directory>
<     <DeployDirectory>iseposture</DeployDirectory>
<     <Description>ISE Posture JSON Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="ampenabler">
<     <FileName>AMPEnabler_ServiceProfile.xml</FileName>
<     <FileExtension>asp</FileExtension>
<     <Directory>AMPEnabler</Directory>
<     <DeployDirectory>AMPEnabler</DeployDirectory>
<     <Description>AMP Enabler Service Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="nvm">
<     <FileName>NVM_ServiceProfile.xml</FileName>
<     <FileExtension>nvmsp</FileExtension>
<     <Directory>NVM</Directory>
<     <DeployDirectory>NVM</DeployDirectory>
<     <Description>Network Visibility Service Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
<   <Profile service-type="umbrella">
<     <FileName>OrgInfo.json</FileName>
<     <FileExtension>json</FileExtension>
<     <Directory>umbrella</Directory>
<     <DeployDirectory>umbrella</DeployDirectory>
<     <Description>Umbrella Roaming Security Profile</Description>
<     <DownloadRemoveEmpty>false</DownloadRemoveEmpty>
<   </Profile>
< </ServiceProfiles>
< </service-profile-manifest>
< <vpn-client-pkg-version>
< <pkgversion>***</pkgversion>
< </vpn-client-pkg-version>
< <vpn-core-manifest>
< <vpn rev="1.0">
<   <file version="***" id="VPNCore" is_core="yes" type="" action="install">
<     <uri>***</uri>
<     <display-name>AnyConnect Secure Mobility Client</display-name>
<   </file>
< </vpn>
< </vpn-core-manifest>
< 
< <custom-attr>
< <dynamic-split-include-domains><![CDATA[example.com, example.com,example.com,example.com]]></dynamic-split-include-domains>
< </custom-attr>
< </opaque>
< </config>
< </config-auth>
TCP_INFO rcv mss 1379, snd mss 1448, adv mss 1448, pmtu 1500
> CONNECT /CSCOSSLC/tunnel HTTP/1.1
> Host: example.com
> User-Agent: ***
> Cookie: ***
> X-CSTP-Version: 1
> X-CSTP-Hostname: ***
> X-CSTP-Accept-Encoding: ***
> X-CSTP-Base-MTU: 1500
> X-CSTP-MTU: 1406
> X-CSTP-Address-Type: IPv6,IPv4
> X-CSTP-Full-IPv6-Capability: true
Got CONNECT response: HTTP/1.1 200 OK
X-CSTP-Version: 1
X-CSTP-Protocol: Copyright (c) 2004 Cisco Systems, Inc.
X-CSTP-Address: ***
X-CSTP-Netmask: 255.255.255.0
X-CSTP-Hostname: ***
X-CSTP-DNS: ***
X-CSTP-DNS: ***
X-CSTP-Lease-Duration: 1209600
X-CSTP-Session-Timeout: none
X-CSTP-Session-Timeout-Alert-Interval: 60
X-CSTP-Session-Timeout-Remaining: none
X-CSTP-Idle-Timeout: 129600
X-CSTP-Disconnected-Timeout: 129600
X-CSTP-Default-Domain: ***
X-CSTP-Split-Include: ***/255.255.0.0
X-CSTP-Keep: true
X-CSTP-Tunnel-All-DNS: false
X-CSTP-DPD: 30
X-CSTP-Keepalive: 20
X-CSTP-MSIE-Proxy-Lockdown: true
X-CSTP-Smartcard-Removal-Disconnect: true
X-DTLS-Session-ID: ***
X-DTLS-Port: 443
X-DTLS-Keepalive: 20
X-DTLS-DPD: 30
X-CSTP-MTU: 1300
X-DTLS-MTU: 1300
X-CSTP-Client-Bypass-Protocol: true
X-CSTP-TCP-Keepalive: true
X-CSTP-Post-Auth-XML: <elided>
CSTP connected. DPD 30, Keepalive 20
CSTP Ciphersuite: 
DTLS option X-DTLS-Session-ID : 
DTLS option X-DTLS-Port : 443
DTLS option X-DTLS-Keepalive : 20
DTLS option X-DTLS-DPD : 30
DTLS option X-DTLS-MTU : 1300
DTLS initialised. DPD 30, Keepalive 20
Vanuan commented 3 years ago

Here's what documentation says:

Additionally, AnyConnect release 4.6 added an enhanced dynamic split tunneling, where both dynamic split exclude and dynamic split include domains are specified for enhanced domain name matching.

Dynamic Split Include Tunneling—With dynamic split include tunneling, you can dynamically provision split include tunneling after tunnel establishment, based on the host DNS domain name. For example, a VPN administrator could configure domain.com to be included into the VPN tunnel at runtime. When the VPN tunnel is up and an application attempts to connect to www.domain.com, the VPN client automatically changes the system routing table and filters to allow the connection inside the VPN tunnel.

So apparently, it's not a run-once script, it resolves and adds IPs to the routing table dynamically. So there should be some kind of agent that watches for DNS requests and adds a route whenever it matches the configured domain names inclusions/exclusions. This way subdomains are handled. Since it's impossible to know which subdomains exist.

The official Cisco Anyconnect on Linux doesn't seem to support this feature either (only Mac and WIndows). Maybe something to do with how the resolver daemon works.

Vanuan commented 3 years ago

Here's a tutorial how to set it up: https://woland.com/2020/03/30/dynamic-split-tunneling-a-covid-19-best-practice/

Vanuan commented 3 years ago

Found this in release notes:

image

DynamicSplit Include Tunneling (Windows and macOSonly)

dlenski commented 3 years ago

Thanks for the log. This is quite strange. Basically, the split domains are appearing as part of the <opaque> blob in the authentication response…

<opaque is-for="vpn-client">
 <custom-attr>
 <dynamic-split-include-domains><![CDATA[example.com, example.com,example.com,example.com]]></dynamic-split-include-domains>
 </custom-attr>
</opaque>

They are not, however, appearing in the X-CSTP-Split-DNS headers from the connection response, which is where we have always expected them to appear. It's not 100% clear that the header version and the XML tag version are equivalent… but it sure sounds like they are from the tutorial you sent.

This is an extremely badly-designed place for the server to send this information, because it's part of the authentication response, not the connection response. That means that it won't necessarily even be seen by the client software upon reconnection, and won't work at all with 2-phase authentication.

Nevertheless, as long as you do the authentication and connection in a single invocation it's fairly easy to make OpenConnect shoehorn the list of domains into its connection information.

Please test this modified version of OpenConnect where I've added the relevant functionality; it should cause the relevant domains to get exported to vpn-slice/vpnc-script in the CISCO_SPLIT_DNS variable.


As for actually getting the functionality that you want working, someone who actually wants this feature needs to implement a working split-DNS handler for their platform, and integrate it into vpn-slice and/or the "full" vpnc-script. See:

Vanuan commented 3 years ago

They are not, however, appearing in the X-CSTP-Split-DNS headers from the connection response, which is where we have always expected them to appear. It's not 100% clear that the header version and the XML tag version are equivalent… but it sure sounds like they are from the tutorial you sent.

Are you talking about X-CSTP-Split-Include? The X-CSTP-Split-DNS header seems to be for specifying the search domain which has nothing to do with routes.

Well, maybe there's a different between the "Split DNS" feature and the "Dynamic Split Include Tunneling" one. The difference between the Split Include and the Dynamic Split Include appears to be that the former specifies routes that are added once per connection (static ip addresses), while the latter are "dynamic", that is should be maintained in runtime whenever there's a DNS query (domain-based IP routes).

dlenski commented 3 years ago

They are not, however, appearing in the X-CSTP-Split-DNS headers from the connection response, which is where we have always expected them to appear. It's not 100% clear that the header version and the XML tag version are equivalent… but it sure sounds like they are from the tutorial you sent.

Are you talking about X-CSTP-Split-Include? The X-CSTP-Split-DNS header seems to be for specifying the search domain which has nothing to do with routes.

Again, see the discussion here, and the blog post it links to: https://gitlab.com/openconnect/vpnc-scripts/-/issues/5

Per @dwmw2's comment here:

Well, maybe there's a different between the "Split DNS" feature and the "Dynamic Split Include Tunneling" one.

You tell me. I have not seen any explanation of how/why X-CSTP-Split-DNS should differ from dynamic-split-include-domains, but there's certainly a tremendous amount of confusion about how either of them should be handled. :man_shrugging:

As I wrote in the other thread, the idea of doing split-{in,ex}cludes at the hostname level is both flawed conceptually and rather difficult to execute in a reasonable way.

The difference between the Split Include and the Dynamic Split Include appears to be that the former specifies routes that are added once per connection (static ip addresses), while the latter are "dynamic", that is should be maintained in runtime whenever there's a DNS query (domain-based IP routes).

Yes. We already know how to handle IP-based split-{in,ex}cludes. Those are quite straightforward to understand, because they operate at the IP layer, just as the VPN tunnel itself does.

Vanuan commented 3 years ago

@dlenski Here's what the documentation says:

Split DNS - The DNS queries which matches the domain names, are configured on the Cisco Adaptive Security Appliance (ASA). They move through the tunnel (to the DNS servers that are defined on the ASA, for example) while others do not.

Tunnel-all-DNS - Only DNS traffic to the DNS servers which are defined by the ASA is allowed. This setting is configured in the group policy.

Standard DNS - All of the DNS queries move through the DNS servers which are defined by the ASA. In the case of a negative response, the DNS queries might also go to the DNS servers which are configured on the physical adapter.

So it still related to DNS queries, that is how to route the DNS traffic (when split routing is enabled). It isn't related to how IP route table is configured. Bacically, it's a "deep packet inspection"-based routing for DNS queries. That is traffic is split before hitting the DNS server. This is to prevent DNS leakage.

The dynamic-split-include-domains feature is different. It's about adding and removing IP routes on the fly based on the DNS cache entries.

Vanuan commented 3 years ago

Maybe the issue should be raised in https://github.com/systemd/systemd to request a feature to be used by VPN clients so that they can spy for DNS queries and either update the routing table accordingly or select which DNS to send the request to: public or private.

It already has some support for split-tunneling DNS queries: https://fedoraproject.org/wiki/Changes/systemd-resolved#Split_DNS Something needs to be added to support the IP routes based on DNS queries they are resolved from.

dlenski commented 3 years ago

Here's what the documentation says:

Interesting.

Maybe the issue should be raised in https://github.com/systemd/systemd to request a feature to be used by VPN clients so that they can spy for DNS queries and either update the routing table accordingly or select which DNS to send the request to: public or private.

Is this not already possible via dbus, as you wrote?

Vanuan commented 3 years ago

Is this not already possible via dbus, as you wrote?

I don't know. If it is, it's not documented: https://www.freedesktop.org/wiki/Software/systemd/resolved/

You can only set SetLinkDNS+SetLinkDomains to route DNS queries to your custom DNS server. It doesn't provide any help with setting up DNS resolution chains.

Vanuan commented 3 years ago

I've created an issue: https://github.com/systemd/systemd/issues/17265

Vanuan commented 3 years ago

Yeah, maybe setting up a CoreDNS server and writing a plugin for it would be the most realistic option.

This is how it might work:

  1. All the VPN configuration (xml files) is passed on to the script
  2. The script would generate a Corefile (with domain names read from xml, and system-resolved DNS server as a next resolver in a chain), start a CoreDNS service with a custom plugin, configure system-resolved to use CoreDNS.
  3. The plugin would spy on DNS queries and update the routing table accordingly
  4. DNS configuration and routing table is restored when the vpn link is down.
Vanuan commented 3 years ago

@dlenski could you help me out with a first step? How do I get the XML passed on from openconnect to my custom script?

dlenski commented 3 years ago

@dlenski could you help me out with a first step? How do I get the XML passed on from openconnect to my custom script?

As I wrote above, I already patched OpenConnect to do just this, which you should test. https://github.com/dlenski/vpn-slice/issues/68#issuecomment-704538279

It is intended to pass the list of domains from the <dynamic-split-include-domains> to the vpnc-script in the CISCO_SPLIT_DNS variable.

(Per the docs you linked, we probably want to put them in a new variable called something like CISCO_DYNAMIC_SPLIT_INCLUDES, but for now…)

markcellus commented 2 years ago

Thank you for opening this @Vanuan! I'd love it if vpn-slice handled this out of the box with GlobalProtect as well.

Were you ever able to test the patched version of openconnect @dlenski suggested? Or have you decided on some other approach?

Thanks for any information :heart:

Vanuan commented 2 years ago

No, I left the company that used this VPN configuration. So I don't need this anymore.