While working with DevOps projects that utilize a Consul KV store and it’s service discovery features, I found it useful to replicate a local agent to test against. To that end, getting a Consul agent up and running and accessible from both Windows and WSL2 proved usesful.

TBH, this is a brittle setup; it’s far easier to just manage your Consul environment variables to point where needed instead.

Consul Agent Setup in Windows

  1. Download the Consul binaries from Hashicorp for Windows here.

  2. Create a client config for the Consul Agent. An example is provided below.

    I am using a go template for the client bind address to include the vEthernet (WSL) interface that WSL uses. I configure this to be a deterministic address, see here. This methodology allows queries from WSL2 guests to be serviced. I also use a go template to ensure that the LAN and WAN serf ports are bound to my actual network interface.
    server = false
    datacenter = "${MY_DATACENTER}"
    node_name = "${MY_AGENT_NAME}"
    log_file = "${CONSUL_LOG_DIR}\\agent.log"
    data_dir = "${CONSUL_DATA_DIR>\\data"
    client_addr = "{{ GetPrivateInterfaces | include \"network\" \"192.168.254.0/24\" | attr \"address\" }} 127.0.0.1"
    bind_addr = "{{ GetPrivateInterfaces | include \"network\" \"192.168.1.0/24\" | attr \"address\" }}
    retry_join = ["consul.service.consul"]
  3. Use sc.exe to create a Windows service for the Consul Agent. A simple example:

    sc.exe create “Consul Agent” binPath=${CONSUL_DIR}\consul.exe agent -config-dir ${CONSUL_CONFIG_DIR}” start= auto    

    Why Run the Agent in Windows if you’ve got everything else in WSL2?

    Consul networking requires direct TCP and UDP endpoints for connecting to the Consul cluster – and the WSL2 guests run in a virtual machine, with a virtual network adapter, and without the networking being run in bridged mode. I was unable to find a workaround to configure the Consul client this way. The closest I got was creating a Windows v4tov4 portproxy through netsh, but that is unable to proxy UDP traffic.

  4. Start the Consul Agent service. You will find that on a typical workstation setup the Windows Firewall will prompt you for allowing access to Consul. It’s important to understand what is happening here: When you approve access, Windows Firewall will create inbound allow and deny rules that are targeted directly at the consul.exe process – we’re going to have to modfiy these in a bit.

Getting “clever” to access the Consul Agent from WSL2

  1. As mentioned above, there are now explict firewall rules created that, if you chose the defaults, will allow access to Consul through the Private and Domain networks, and explicitly deny access through Public-tagged networks. These deny rules are going to cause us issues when attempting to access the Consul Agent via the WSL network interface, as it’s defined as a public interface. I have been unable to determine how to map that interface to private like you would physical network interfaces. So, if there are any explict deny rules for Consul, delete those.

  2. A broad rule as described in this Github issue shows how to create an inbound allow rule that will allow traffic from the WSL interface to access the Windows host machine. Copied here for posterity:

    New-NetFirewallRule -DisplayName "WSL" -Direction Inbound  -InterfaceAlias "vEthernet (WSL)"  -Action Allow
  3. Now that access to the Consul Agent is available through the WSL network interface, a little bit of trickery is involved into presenting that consul agent as if it were running at the standard loopback address. A iptables DNAT rule with some masquerading will be used to redirect any query to localhost:8500 to the Consul agent that is listening on the WSL2 network interface.

    sysctl -w net.ipv4.conf.eth0.route_localnet=1
    iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 8500 -j DNAT --to 192.168.254.1:8500      # HTTP
    iptables -t nat -A OUTPUT -p udp -d 127.0.0.1 --dport 8600 -j DNAT --to 192.168.254.1:8600      # DNS
    iptables -t nat -A POSTROUTING -m addrtype --src-type LOCAL --dst-type UNICAST -j MASQUERADE

    Remember, as WSL2 guests all share a single kernel these networking steps only need to be done once. It will be present for each WSL2 guest thereafter.

Now, any operation program or service that is running in WSL2 and utilizes the HTTP API endpoints of Consul can do so as if there was a local agent running on the default port and listen address (notice how when the host API endpoint is queryied it shows Windows 11 as the platform. Similarily, if you were to query the self API endpoint you’d see that there is no WSL interface or IP addresses).

[12:45:00 PM] ~ on  master [!] took 5s >  uname -a
Linux persephone 5.10.60.1-microsoft-standard-WSL2 #1 SMP Wed Aug 25 23:20:18 UTC 2021 x86_64 GNU/Linux

[12:46:05 PM] ~ on  master [!] took 1m4s >  hostname
persephone

[12:46:07 PM] ~ on  master [!] >  curl -s http://localhost:8500/v1/agent/host | yq -P '.Host'

hostname: Persephone
uptime: 687534
bootTime: 1647200838
procs: 375
os: windows
platform: Microsoft Windows 11 Pro
platformFamily: Standalone Workstation
platformVersion: 10.0.22000 Build 22000
kernelVersion: 10.0.22000 Build 22000
kernelArch: x86_64
virtualizationSystem: ""
virtualizationRole: ""
hostId: 141af2a0-e749-4a3c-8e21-e0873684507a

To ensure that this configuration is always present, place the sysctl and iptables rules in your .profile or similar. The Windows firewall rule will persist between reboots.