Skip to content

Instantly share code, notes, and snippets.

@pyther
Last active April 24, 2024 20:25
Show Gist options
  • Save pyther/b7c03579a5ea55fe431561b502ec1ba8 to your computer and use it in GitHub Desktop.
Save pyther/b7c03579a5ea55fe431561b502ec1ba8 to your computer and use it in GitHub Desktop.

WSL 2 Cisco AnyConnect Networking Workaround

Overview

WSL 2 uses a Hyper-V Virtual Network adapter. Network connectivity works without any issue when a VPN is not in use. However when a Cisco AnyConnect VPN session is established Firewall Rules and Routes are added which breaks connectivity within the WSL 2 VM. This issue is tracked WSL/issues/4277

Below outline steps to automatically configure the Interface metric on VPN connect and update DNS settings (/etc/resolv.conf) on connect/disconnect.

Manual Configuration

Set Interface Metrics

After connecting to the VPN, you'll want to modify the Interface Metric of the Cisco VPN Adapter

PS C:\Users\gyurgyik> Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match "Cisco AnyConnect"} | Set-NetIPInterface -InterfaceMetric 6000

Run the following command in Powershell with Administrative permission.

At this point you should have connectivity in your container (but without name resolution). You can test this by running ping 8.8.8.8.

Set DNS servers in Linux VM

Once connected the VPN determine the DNS servers that are configured:

PS C:\Users\gyurgyik> (Get-NetAdapter | Where-Object InterfaceDescription -like "Cisco AnyConnect*" | Get-DnsClientServerAddress).ServerAddresses
10.10.0.124
10.10.0.132

Update /etc/resolv.conf

N-20S5PF20MB4R:~$ cat /etc/resolv.conf
nameserver 10.10.0.124
nameserver 10.10.0.132

Verify Connectivity

ping google.com -c 4

Automatic Configuration

Create Scripts

Save the following scripts to %homepath%\wsl\scripts

setCiscoVpnMetric.ps1

Get-NetAdapter | Where-Object {$_.InterfaceDescription -Match "Cisco AnyConnect"} | Set-NetIPInterface -InterfaceMetric 6000

setDns.ps1

$dnsServers = (Get-NetAdapter | Where-Object InterfaceDescription -like "Cisco AnyConnect*" | Get-DnsClientServerAddress).ServerAddresses -join ','
$searchSuffix = (Get-DnsClientGlobalSetting).SuffixSearchList -join ','

function set-DnsWsl($distro) {
    if ( $dnsServers ) {    
        wsl.exe -d $distro -u root /opt/wsl_dns.py --servers $dnsServers --search $searchSuffix
    }
    else {
        wsl.exe -d $distro -u root /opt/wsl_dns.py
    }
}

set-DnsWsl fedora

wsl_dns.py

Download from: https://gist.githubusercontent.com/pyther/20bba4aee1a7e1485dd065adbf736891/raw/bb00f46d30da1b1eb7ba0fec4e6946a654c7b186/wsl_dns.py

Disable WSL Resolv Update

For each Linux instance:

  1. Disable automatic updating of resolv.conf by WSL
    $ cat <<EOF > /etc/wsl.conf
    [network]
    generateResolvConf = false
    EOF
    
  2. Restart/Shutdown WSL: wsl --shutdown (WARNING: this will kill all current sessions!)

Copy wsl_dns.py to Linux VMs

For each VM, run:

$ cp /mnt/c/Users/$username/wsl/scripts/wsl_dns.py /opt/wsl_dns.py
$ chmod +x /opt/wsl_dns.py

Create Scheduled Tasks

Windows Scheduled Tasks allows you to trigger an action when a certain log event comes in. The Cisco AnyConnect VPN client generates a number of log events.

We will create two tasks. The first task, will configure the interface metric when the VPN connects. The second task, will execute the dns update script inside of your Linux VM when the VPN Connects and Disconnects.

Cisco AnyConnect Events

  • 2039: VPN Established and Passing Data
  • 2061: Network Interface for the VPN has gone down
  • 2010: VPN Termination
  • 2041: The entire VPN connection has been re-established.

Procedure

  1. Open Task Scheduler
  2. Create a Folder called WSL (Optional, but easier to find rules later)
  3. Create Rules
    1. Update AnyConnect Adapter Interface Metric for WSL2
      • General: Check: Run with highest privileges
      • Triggers:
        • On an Event, Log: Cisco AnyConnect Secure Mobility Client, Source: acvpnagent, Event ID: 2039
        • On an Event, Log: Cisco AnyConnect Secure Mobility Client, Source: acvpnagent, Event ID: 2041
      • Action: Start a program, Program: Powershell.exe, Add arguments: -WindowStyle Hidden -NonInteractive -ExecutionPolicy Bypass -File %HOMEPATH%\wsl\scripts\setCiscoVpnMetric.ps1
      • Condition: Uncheck: Start the task only if the computer is on AC power
    2. Update DNS in WSL2 Linux VMs
      • Triggers:
        • On an Event, Log: Cisco AnyConnect Secure Mobility Client, Source: acvpnagent, Event ID: 2039
        • On an Event, Log: Cisco AnyConnect Secure Mobility Client, Source: acvpnagent, Event ID: 2010
        • On an Event, Log: Cisco AnyConnect Secure Mobility Client, Source: acvpnagent, Event ID: 2061
        • On an Event, Log: Cisco AnyConnect Secure Mobility Client, Source: acvpnagent, Event ID: 2041
        • At log on: At log on of $USER
      • Action: Start a program, Program: Powershell.exe, Add arguments: -WindowStyle Hidden -NonInteractive -ExecutionPolicy Bypass -File %HOMEPATH%\wsl\scripts\setDns.ps1
      • Condition: Uncheck: Start the task only if the computer is on AC power
  4. Test: Connect to the VPN, a powershell window should pop-up briefly

FAQ

Q: Does traffic orginating from the Linux VM still route through the VPN?
A: Yes, I believe so. I did not see any leaked traffic when running a tcpdump on my router.

Q: Are VPN resources accessible from the Linux VM?
A: Yes

Q: Can the Linux VM communicate with Windows?
A: No, it appears a firewall rule is preventing traffic between Windows and the Linux VM.

Q: Can I still run WSL1 instances?
A: Yes, you can run WSL1 and WSL2 insatnces simutaneously

Q: How do I revert/disable these changes?
A: Disable scheduled Tasks, remove/modify /etc/wsl.conf from each WSL Instance, Reboot

@exenza
Copy link

exenza commented Mar 17, 2021

the windows script is not functioning anymore:

setDns.ps1:1 char:95
+ ... iption -like "Cisco AnyConnect*" | Get-DnsClientServerAddress).Server ...
+                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (13:UInt32) [Get-DnsClientServerAddress], CimJobException
    + FullyQualifiedErrorId : CmdletizationQuery_NotFound_InterfaceIndex,Get-DnsClientServerAddress

@ScottTaftPotter
Copy link

I have the Ubuntu-20.04 distribution in WSL2.
/etc/resolv.conf is a symlink to ../run/resolvconf/resolv.conf. This path does not exist when generateResolvConf = false so the python script fails:

$ sudo python3 wsl_dns.py
Generated resolv.conf

Generated by wsl_dns.py at 2021-03-26 10:00:09.481642

nameserver 172.22.16.1
search cisco.com,hsd1.ct.comcast.net
options timeout:1 retries:1
Traceback (most recent call last):
File "wsl_dns.py", line 100, in
main()
File "wsl_dns.py", line 98, in main
write_resolv(config)
File "wsl_dns.py", line 69, in write_resolv
with open('/etc/resolv.conf', 'w') as fd:
FileNotFoundError: [Errno 2] No such file or directory: '/etc/resolv.conf'

@jmcp
Copy link

jmcp commented Mar 26, 2021

@ScottTaftPotter I got past that problem by removing the symlink and creating a normal file.

@ScottTaftPotter
Copy link

ScottTaftPotter commented Mar 27, 2021

@ScottTaftPotter I got past that problem by removing the symlink and creating a normal file.

Thanks @JCMP. Yeah, that works, but if you ever want to revert to generateResolvConf I am wondering if the symlink would be regenerated. Not happy with having to experiment with all this. I supposed to be using this "tool" to do real work. Its starting to become the work!

@swirle13
Copy link

swirle13 commented Apr 15, 2021

I had issues with the json.loads(p.stdout.decode("utf-8") section as my code kept returning \nSUCCESS: Specified value was saved.\n at the beginning of p.stdout of my CompletedProcess object, p, that was returned back from the subprocess.run(...) calls. This kept throwing json.decoder.JSONDecodeError: Expecting value: line 2 column 1 (char 1) and I didn't realize that I had a second line due to that line showing up at the beginning and there being no comma at the end of that line meant it wasn't valid JSON.

What I did to fix this were a few things (some of which are probably pointless, but clean the code up a bit more):

  • in each p = subprocess.run(...), I added universal_newlines=True as an argument so that the type of p.stdout wasn't a byte literal that needed decoding and was instead converted to a string, saving some ugliness/redundancy in the return statement (arguments to decode() are deprecated and ignored, so "utf-8" didn't do anything).
  • due to my situation having that "Success" line at the beginning, I had to cut that value out of the return object, if it showed up.

So I sliced on the newline chars and grabbed the data after the split but then I had to also account for if/when people don't run into this issue either, so I wrote a method to handle this and replace in the dns methods:

def clean_up_output(data, index):
    splitInput = data.split('\n')
    fixedInput = splitInput[2] if len(splitInput) > 3 else splitInput[1]
    return json.loads(fixedInput)[index]

This allows me to check the returned values. The string appears to always start and end with a newline character, so if there's no extra line, grab index 1 that contains the values we want. If there is an extra line, grab index 2. Expected data formats:
With success message:

>>> word4 = '\nSUCCESS\nthis is a test\n'
>>> print(word4.split('\n'))
['', 'SUCCESS', 'this is a test', '']

With no success message:

>>> word5 = '\nthis is a test\n'
>>> print(word5.split('\n'))
['', 'this is a test', '']

My get_cisco_vpn_dns_servers() and get_dns_search_list() now look like this:

def get_cisco_vpn_dns_servers():
    p = subprocess.run([PSCMD, 'Get-NetAdapter| Where-Object InterfaceDescription -like \"Cisco AnyConnect*\" | Get-DnsClientServerAddress | Where-Object AddressFamily -eq 2 | ConvertTo-Json -Compress'],
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       universal_newlines=True)

    if not p.stdout:
        return None

    return clean_up_output(p.stdout, 'ServerAddresses')


def get_dns_search_list():
    p = subprocess.run([PSCMD, 'Get-DnsClientGlobalSetting | ConvertTo-Json -Compress'],
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       universal_newlines=True)

    if not p.stdout:
        return None

    return clean_up_output(p.stdout, 'SuffixSearchList')

Also, with open('/etc/resolv.conf', 'w') as fd: doesn't work when the resolv.conf is a symlink. In my case, when I lld the /etc/ directory, I got a red highlighted lrwxrwxrwx 1 root root 29 Dec 6 2019 resolv.conf -> ../run/resolvconf/resolv.conf as the linked location didn't exist. This didn't matter to open as resolv.conf existed, so it tried to write to it, unsuccessfully. I fixed this by checking if the file is a link, rename it to resolvOLD.conf to maintain the symlink, in case you still want to use it.

Before:
image

After:
image

My resolv1.conf is a backup of my current resolv.conf file for testing purposes. The contents of write_resolv(data) is now as follows:

def write_resolv(data):
    if os.path.islink('/etc/resolv.conf'):
        # rename link so `with open()` can work
        os.rename('/etc/resolv.conf', '/etc/resolvOLD.conf')
    with open('/etc/resolv.conf', 'w') as fd:
        fd.write(data)
        fd.write('\n')

Hopefully, this will fix the issue @ScottTaftPotter was running into above with deleting and possibly reverting resolv.conf

Full text of my fixed wsl_dns.py file:

#!/usr/bin/env python3

# Disabling WSL DNS Management
# Add the following entry to /etc/wsl.conf:
#   [network]
#   generateResolvConf = false
#
# Change permissions on /etc/resolv.conf
#   sudo chown $USER:root /etc/resolv.conf

import os
import re
import datetime
import ipaddress
import socket
import subprocess
import json
import sys

PSCMD = '/mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe'

def get_iface_networks(dev):
    # IPv6 is cool, but Nokia doesn't use it
    return re.search(re.compile(r'(?<=inet )(.*)(?=\ brd)', re.M), os.popen(f'ip addr show {dev}').read()).groups()


def get_first_network_address(net):
    # Expects IP/Netmask (172.16.20.164/28)
    return ipaddress.IPv4Interface(net).network[1]


def get_default_gateway():
    return re.search(re.compile(r'via ([0-9.]+)', re.M), os.popen(f'ip route list default').read()).groups()[0]


def get_cisco_vpn_dns_servers():
    p = subprocess.run([PSCMD, 'Get-NetAdapter| Where-Object InterfaceDescription -like \"Cisco AnyConnect*\" | Get-DnsClientServerAddress | Where-Object AddressFamily -eq 2 | ConvertTo-Json -Compress'],
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       universal_newlines=True)

    if not p.stdout:
        return None

    return clean_up_output(p.stdout, 'ServerAddresses')


def get_dns_search_list():
    p = subprocess.run([PSCMD, 'Get-DnsClientGlobalSetting | ConvertTo-Json -Compress'],
                       stdout=subprocess.PIPE,
                       stderr=subprocess.PIPE,
                       universal_newlines=True)

    if not p.stdout:
        return None

    return clean_up_output(p.stdout, 'SuffixSearchList')


def generate_resolv(servers, search_list):
    out = []
    out.append(f"# Generated by {sys.argv[0]} at {str(datetime.datetime.now())}")
    for x in servers:
        out.append(f"nameserver {x}")

    if search_list:
        search_str = ','.join(search_list)
        out.append(f"search {search_str}")

    out.append("options timeout:1 retries:1")
    return '\n'.join(out)


def write_resolv(data):
    if os.path.islink('/etc/resolv.conf'):
        # rename link so `with open()` can work
        os.rename('/etc/resolv.conf', '/etc/resolvOLD.conf')
    with open('/etc/resolv.conf', 'w') as fd:
        fd.write(data)
        fd.write('\n')


def clean_up_output(data, index):
    splitInput = data.split('\n')
    fixedInput = splitInput[2] if len(splitInput) > 3 else splitInput[1]
    return json.loads(fixedInput)[index]



def main():
    # Assumes we only have one IP configured on the Linux eth0 interface
    eth0_network = get_iface_networks('eth0')[0]
    eth0_gateway = str(get_first_network_address(eth0_network))
    default_gateway = get_default_gateway()

    # Sanity Checks
    if eth0_gateway != default_gateway:
        print("Warning: default gateway does not match guessed gateway of eth0")
        print("Warning: script makes assumptions that may not be true on your system!")

    # Calling powershell from Linux seems weird, so... yeah...
    vpn_dns_servers = get_cisco_vpn_dns_servers()
    dns_search = get_dns_search_list()

    # Always include the hyper-v resolver, as if the VPN disconnects, linux will try all resolvers (albit slower)
    if vpn_dns_servers:
        dns_servers = vpn_dns_servers + [eth0_gateway]
    else:
        dns_servers = [eth0_gateway]

    config = generate_resolv(dns_servers, dns_search)
    print("Generated resolv.conf")
    print(config)
    write_resolv(config)

main()
sys.exit()

Almost forgot, I exported each of these tasks as an xml file to use for importing. This does require some user input to change the values I removed from mine as it contained my username and UserID. You'll need to replace ${username} with your local windows username and ${UserID} with your uid. You can fetch your uid by pasting this into a cmd.exe window:
for /F "tokens=1,2 delims=: " %A in ('WhoAmI /USER /FO LIST') do @if /I "%~A" == "SID" echo %B

I'll be uploading these to a public repo and hyperlinking them here so my comment isn't so massive, but until then, here's the raw text.

Update AnyConnect Adapter Interface Metric for WSL2.xml

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2021-04-14T12:43:45.5292999</Date>
    <Author>${username}</Author>
    <URI>\WSL\Update AnyConnect Adapter Interface Metric for WSL2</URI>
  </RegistrationInfo>
  <Triggers>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Cisco AnyConnect Secure Mobility Client"&gt;&lt;Select Path="Cisco AnyConnect Secure Mobility Client"&gt;*[System[Provider[@Name='acvpnagent'] and EventID=2039]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Cisco AnyConnect Secure Mobility Client"&gt;&lt;Select Path="Cisco AnyConnect Secure Mobility Client"&gt;*[System[Provider[@Name='acvpnagent'] and EventID=2041]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <UserId>${UserId}</UserId>
      <LogonType>InteractiveToken</LogonType>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>Powershell.exe</Command>
      <Arguments>-WindowStyle Hidden -NonInteractive -ExecutionPolicy Bypass -File "C:\Users\${username}\wsl\scripts\setCiscoVpnMetric.ps1"</Arguments>
    </Exec>
  </Actions>
</Task>

Update DNS in WSL2 Linux VMs.xml

<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
  <RegistrationInfo>
    <Date>2021-04-14T12:47:08.2121461</Date>
    <Author>${username}</Author>
    <URI>\WSL\Update DNS in WSL2 Linux VMs</URI>
  </RegistrationInfo>
  <Triggers>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Cisco AnyConnect Secure Mobility Client"&gt;&lt;Select Path="Cisco AnyConnect Secure Mobility Client"&gt;*[System[Provider[@Name='acvpnagent'] and EventID=2039]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Cisco AnyConnect Secure Mobility Client"&gt;&lt;Select Path="Cisco AnyConnect Secure Mobility Client"&gt;*[System[Provider[@Name='acvpnagent'] and EventID=2010]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Cisco AnyConnect Secure Mobility Client"&gt;&lt;Select Path="Cisco AnyConnect Secure Mobility Client"&gt;*[System[Provider[@Name='acvpnagent'] and EventID=2061]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
    <EventTrigger>
      <Enabled>true</Enabled>
      <Subscription>&lt;QueryList&gt;&lt;Query Id="0" Path="Cisco AnyConnect Secure Mobility Client"&gt;&lt;Select Path="Cisco AnyConnect Secure Mobility Client"&gt;*[System[Provider[@Name='acvpnagent'] and EventID=2041]]&lt;/Select&gt;&lt;/Query&gt;&lt;/QueryList&gt;</Subscription>
    </EventTrigger>
    <LogonTrigger>
      <Enabled>true</Enabled>
      <UserId>${username}</UserId>
    </LogonTrigger>
  </Triggers>
  <Principals>
    <Principal id="Author">
      <UserId>${UserId}</UserId>
      <LogonType>InteractiveToken</LogonType>
      <RunLevel>HighestAvailable</RunLevel>
    </Principal>
  </Principals>
  <Settings>
    <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
    <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
    <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
    <AllowHardTerminate>true</AllowHardTerminate>
    <StartWhenAvailable>false</StartWhenAvailable>
    <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
    <IdleSettings>
      <StopOnIdleEnd>true</StopOnIdleEnd>
      <RestartOnIdle>false</RestartOnIdle>
    </IdleSettings>
    <AllowStartOnDemand>true</AllowStartOnDemand>
    <Enabled>true</Enabled>
    <Hidden>false</Hidden>
    <RunOnlyIfIdle>false</RunOnlyIfIdle>
    <WakeToRun>false</WakeToRun>
    <ExecutionTimeLimit>PT72H</ExecutionTimeLimit>
    <Priority>7</Priority>
  </Settings>
  <Actions Context="Author">
    <Exec>
      <Command>Powershell.exe</Command>
      <Arguments>-WindowStyle Hidden -NonInteractive -ExecutionPolicy Bypass -File "C:\Users\${username}\wsl\scripts\setDns.ps1"</Arguments>
    </Exec>
  </Actions>
</Task>

@peterneorr
Copy link

Any hints on this this python error?

# /opt/wsl_dns.py
  File "/opt/wsl_dns.py", line 24
    return re.search(re.compile(r'(?<=inet )(.*)(?=\ brd)', re.M), os.popen(f'ip addr show {dev}').read()).groups()
                                                                                                ^
SyntaxError: invalid syntax

@jimdibb
Copy link

jimdibb commented Apr 28, 2021

Are you using a current python3 version? This uses f strings, not available in older (2.x) versions

@peterneorr
Copy link

peterneorr commented Apr 28, 2021

I'm not doing anything special as far as I know. This is just whatever came with with Ubuntu 16.04 for WSL:

peter@WE36783:~$ python3 --version
Python 3.5.2
peter@WE36783:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.7 LTS
Release:        16.04
Codename:       xenial

@jimdibb
Copy link

jimdibb commented Apr 28, 2021

You actually need 3.6+ (Sorry...)

or you could modify the os.popen bit to

... os.popen('ip addr show ' + dev).read()).groups()

Probably - but then you'll likely run into it somewhere else.

@peterneorr
Copy link

You actually need 3.6+ (Sorry...)

I updated my distro and this worked like a champ! Thanks!

@arussu
Copy link

arussu commented Jul 20, 2021

Hi guys,

Does anyone run a X server on your win machine ?
Unfortunately, this fix does not work for it.
My VcXsrv is listening on TCP port 6000:

C:\WINDOWS\system32>netstat -abno|findstr 6000
  TCP    0.0.0.0:6000           0.0.0.0:0              LISTENING       23416
  TCP    127.0.0.1:6000         127.0.0.1:52976        ESTABLISHED     23416
  TCP    127.0.0.1:6000         127.0.0.1:52977        ESTABLISHED     23416
  TCP    127.0.0.1:6000         127.0.0.1:52978        ESTABLISHED     23416
  TCP    127.0.0.1:52976        127.0.0.1:6000         ESTABLISHED     23416
  TCP    127.0.0.1:52977        127.0.0.1:6000         ESTABLISHED     23416
  TCP    127.0.0.1:52978        127.0.0.1:6000         ESTABLISHED     23416
  TCP    [::]:6000              [::]:0                 LISTENING       23416

Route metric is updated:

C:\WINDOWS\system32>route print | findstr 172.19
       172.19.0.0    255.255.240.0         On-link        172.19.0.1   5256
       172.19.0.0    255.255.240.0         On-link    10.209.213.249   6001
       172.19.0.1  255.255.255.255         On-link        172.19.0.1   5256
    172.19.15.255  255.255.255.255         On-link        172.19.0.1   5256
    172.19.15.255  255.255.255.255         On-link    10.209.213.249   6256
        224.0.0.0        240.0.0.0         On-link        172.19.0.1   5256
  255.255.255.255  255.255.255.255         On-link        172.19.0.1   5256

even if localnet 172.19.0.0/20 now has lower metric and is preferred, I cannot reach it from within WSL itself:

~ ping 172.19.0.1
PING 172.19.0.1 (172.19.0.1) 56(84) bytes of data.
^C
--- 172.19.0.1 ping statistics ---
383 packets transmitted, 0 received, 100% packet loss, time 397270ms

or TCP:

~ telnet 172.19.0.1 6000
Trying 172.19.0.1...
^C

If trying to ping from WIN to WSL:

C:\WINDOWS\system32>ping 172.19.9.243

Pinging 172.19.9.243 with 32 bytes of data:
General failure.
General failure.
General failure.
General failure.

Ping statistics for 172.19.9.243:
    Packets: Sent = 4, Received = 0, Lost = 4 (100% loss),

Adding firewall rules, restarting did not help.
Disconnecting Cisco Anyconnect fixes this immediatelly.
Any suggestions are appreciated, thanks

@c094728
Copy link

c094728 commented Jul 20, 2021

How is setting the metric for cisco vpn higher to 6000 (not preferred) make wsl work? Doesn't WSL need to pass through the VPN adapter?

@arussu
Copy link

arussu commented Jul 20, 2021

@c094728 traffic is not NAT-ed by vEthernet (WSL). Hence you have private network traffic entering into your VPN, which will be blackholed.
By routing it through your router, it will get NATed, hence Internet

@c094728
Copy link

c094728 commented Jul 20, 2021

@arussu So are you saying that wsl traffic does not use vpn adapter? Using these settings along with proxy settings I can get to our corporate network from inside wsl and that is only accessible over vpn.

@arussu
Copy link

arussu commented Jul 21, 2021

@c094728 for accessing corporate resources there are specific routes that point to the VPN tunnel. When sending a packet, a route table lookup will be performed, if there is a specific route for the destination IP that point to the VPN then that will be used.
But in case WSL, you want it to access the internet through you local router, but there are 2 default routes in the routing table, one pointing to your router and the other to the VPN.
The tiebreaker is the metric. Raising the metric for VPN routes will make them less preferred, including the default route, as such, all your traffic not destined to VPN will go through your local router.

@c094728
Copy link

c094728 commented Jul 27, 2021

@arussu isn't what you are describing called split tunneling. That is not allowed with our vpn configuration. When connected to the VPN, all traffic must go through the VPN. I can access both internal vpn only corporate addresses as well as outside web sites from within WSL so I don't think that is what is happening.

@dg424
Copy link

dg424 commented Aug 19, 2021

Stopped working today. Any ideas ? There was a windows update on my pc and it rebooted overnight.

@tl24
Copy link

tl24 commented Sep 9, 2021

so the resolv.conf that is generated from wsl_dns.py when the vpn is not connected did not work for me. I added a function to read the dns nameservers from the windows host and add them at the front.

I added this function:

def get_dns_client_server_addresses():
    p = subprocess.run(
        [
            PSCMD,
            "Get-DnsClientServerAddress -AddressFamily ipv4 | Select InterfaceAlias,InterfaceIndex,ServerAddresses | ConvertTo-Json -Compress",
        ],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    if not p.stdout:
        return None

    data = json.loads(p.stdout.decode("utf-8"))
    result = []
    for item in sorted(data, key=lambda i: i["InterfaceIndex"]):
        if "Cisco" in item["InterfaceAlias"]:
            continue
        server_addresses = item["ServerAddresses"]
        for server_address in server_addresses:
            if server_address not in result:
                result.append(server_address)
    return result

And added it to main like this:

def main():
    # Assumes we only have one IP configured on the Linux eth0 interface
    eth0_network = get_iface_networks("eth0")[0]
    eth0_gateway = str(get_first_network_address(eth0_network))
    default_gateway = get_default_gateway()
    windows_dns_servers = get_dns_client_server_addresses() # <--- added
    # Sanity Checks
    if eth0_gateway != default_gateway:
        print("Warning: default gateway does not match guessed gateway of eth0")
        print("Warning: script makes assumptions that may not be true on your system!")

    # Calling powershell from Linux seems weird, so... yeah...
    vpn_dns_servers = get_cisco_vpn_dns_servers()
    dns_search = get_dns_search_list()

    # Always include the hyper-v resolver, as if the VPN disconnects, linux will try all resolvers (albit slower)
    if vpn_dns_servers:
        dns_servers = vpn_dns_servers + windows_dns_servers + [eth0_gateway]  # <-- modified
    else:
        dns_servers = windows_dns_servers + [eth0_gateway] # <-- modified

    config = generate_resolv(dns_servers, dns_search)
    print("Generated resolv.conf")
    print(config)
    write_resolv(config)

@kmpizmad
Copy link

kmpizmad commented Nov 23, 2021

Had to use dos2unix /opt/wsl_dns.py to run the script successfully on Debian, otherwise I get the error: /usr/bin/env: ‘python3\r’: No such file or directory
Instead I got the following syntax error:

File "/opt/wsl_dns.py", line 24
    return re.search(re.compile(r'(?<=inet )(.*)(?=\ brd)', re.M), os.popen(f'ip addr show {dev}').read()).groups()

                ^
SyntaxError: invalid syntax

It works on Ubuntu and Ubuntu 18.04 following the steps, altoughdos2unix had to be used on Ubuntu (same problem as on Debian).

setDns.ps1 also throws the following warning:

Warning: default gateway does not match guessed gateway of eth0
Warning: script makes assumptions that may not be true on your system!

Any idea?

@jimdibb
Copy link

jimdibb commented Nov 23, 2021 via email

@liaxiang
Copy link

the windows script is not functioning anymore:

setDns.ps1:1 char:95
+ ... iption -like "Cisco AnyConnect*" | Get-DnsClientServerAddress).Server ...
+                                        ~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (13:UInt32) [Get-DnsClientServerAddress], CimJobException
    + FullyQualifiedErrorId : CmdletizationQuery_NotFound_InterfaceIndex,Get-DnsClientServerAddress

You should connect to the vpn(cisco any connect), then the function will be work

@tsia
Copy link

tsia commented Apr 7, 2022

works great but i added an additional trigger with Event ID 2040. That should re-run the script if AnyConnect decides to reconnect the VPN Tunnel. In my case this caused the Interface Metrics to be wrong again

@jacob-pro
Copy link

jacob-pro commented Jun 6, 2022

Thanks for this @pyther

It has inspired me to create a slightly easier to use / more reliable version of the wsl_dns.py utility: https://github.com/jacob-pro/wsl2-dns-agent

One of the main things I found was this bug: microsoft/WSL#6977
It causes /etc/resolv.conf to get overwritten when a WSL2 distribution shuts down, this is especially a problem in Windows 11, because they get shutdown automatically due to vmIdleTimeout. I was able to work around it by setting (and unsetting) the read only flag on the file.

Another problem I noticed is that you have to be very careful with modifying the metric on the anyconnect adapter, because it will then have a lower internet metric than your Ethernet/WiFi - for me this caused DNS in Windows to take up to 10s as it tried those DNS servers first - the solution was to increase those metrics to then be higher than the anyconnect adapter!

@brianott
Copy link

I think it's really awesome you posted this to help people!

@PrinceS17
Copy link

Thanks @pyther. Want to add something in case someone needs it. Have been using this for a while, but recently stopped working. I figured out that in my case the name of the application and source should be changed to "Cisco Secure Client - AnyConnect VPN" and csc_vpnagent. Not sure if it's general since some cisco update. These can be double-checked using Event Viewer.

@ikorchynskyi
Copy link

Thanks @PrinceS17,
This was exactly my case. It is clearly connected with the recent Cisco VPN client update

@dg424
Copy link

dg424 commented Oct 20, 2023

I have disabled ipv6 on the adapters to get around this issue.

@markcaudill
Copy link

I have disabled ipv6 on the adapters to get around this issue.

Are you saying disabling IPv6 on your interfaces complete fixed the entire AnyConnect WSL2 issue or something more specific?

@dg424
Copy link

dg424 commented Oct 20, 2023

Yep, fixed everything.

@dusweaver
Copy link

This worked for me. Thank you so much

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment