Threat Research

Don't Get Meshed Up: Detecting Mesh VPN Abuse as C2

Published: August 21, 2026
Updated: August 25, 2026
Experts:
  • Sebastian Lacatusu

Summary:
Mesh VPN platforms such as Tailscale, Headscale, ZeroTier, Nebula and Netmaker provide secure, encrypted connectivity between devices. While valuable for legitimate remote access, those same capabilities can also be abused by attackers seeking persistent access to compromised systems. Because these tools use trusted infrastructure, standard reputation-based detection is often ineffective. This research examines the Windows and Linux artefacts mesh VPNs leave behind, highlights what was confirmed through sandbox analysis versus vendor documentation, and provides KQL and YARA examples to support threat hunting.

A note on evidence

To keep the confidence level clear, the article uses three labels:

  • Sandbox-confirmed: we saw it directly in the Windows or Linux ANY.RUN run.
  • Documentation-confirmed: the vendor or operating-system documentation supports it, but our sandbox did not reproduce it.
  • Not verified in testing: it is plausible or expected, but the telemetry available to us could not prove it.

Background: what mesh means

Direct and relayed connections

Why reputation-based detection struggles

DNS and conditional routing

Windows

HKLM\SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig\{<rule-GUID>}
HKLM\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient\DnsPolicyConfig
C:\Windows\System32\drivers\etc\hosts

Linux

/etc/resolv.pre-tailscale-backup.conf

Sandbox findings

Windows 10 Standalone Host

C:\Program Files\Tailscale\
  • tailscale.exe
  • tailscaled.exe
  • tailscale-ipn.exe
  • wintun.dll
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp\Tailscale.lnk
Tailscale-In
Tailscale-Process
controlplane.tailscale.com/ts2021

Ubunto 22.04.2 Standalone Host

/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=41641
ts-input
ts-forward
ts-postrouting

Detection artefact summary

Windows

Linux

Network

Threat hunting with KQL

1. Local NRPT rule creation

let Lookback = 30d;
DeviceRegistryEvents
| where Timestamp > ago(Lookback)
| where ActionType == "RegistryValueSet"
| where RegistryKey has @"SYSTEM\CurrentControlSet\Services\Dnscache\Parameters\DnsPolicyConfig"
| where RegistryValueName in ("GenericDNSServers", "Name")
    or RegistryValueData has ".ts.net"
    or RegistryValueData has "100.100.100.100"
| project Timestamp, DeviceName, RegistryKey, RegistryValueName,
          RegistryValueData, InitiatingProcessFileName,
          InitiatingProcessAccountName, InitiatingProcessFolderPath
| order by Timestamp desc

2. Hosts-file writes

let Lookback = 30d;
DeviceFileEvents
| where Timestamp > ago(Lookback)
| where FolderPath endswith @"System32\drivers\etc"
| where FileName =~ "hosts"
| where ActionType in ("FileModified", "FileCreated")
| summarize WriteCount = count(), FirstWrite = min(Timestamp),
            LastWrite = max(Timestamp),
            Writers = make_set(InitiatingProcessFileName, 10)
    by DeviceName
| where WriteCount >= 3
| order by WriteCount desc

3. Tailscale and Headscale process activity

let Lookback = 30d;
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where FileName in~ ("tailscale.exe", "tailscaled.exe", "tailscale-ipn.exe", "headscale.exe")
    or ProcessCommandLine has_any ("tailscale up", "tailscale login", "headscale nodes", "--config headscale")
| project Timestamp, DeviceName, AccountName, FileName, FolderPath,
          ProcessCommandLine, InitiatingProcessFileName
| order by Timestamp desc

4. Mesh addressing and hosted infrastructure

let Lookback = 30d;
DeviceNetworkEvents
| where Timestamp > ago(Lookback)
| extend Host = tostring(parse_url(RemoteUrl).Host)
| where ipv4_is_in_range(RemoteIP, "100.64.0.0/10")
    or Host endswith ".ts.net"
    or Host has_any ("controlplane.tailscale.com", "login.tailscale.com",
                     "console.tailscale.com", "log.tailscale.com", "log.tailscale.io")
| project Timestamp, DeviceName, InitiatingProcessFileName,
          InitiatingProcessAccountName, RemoteIP, RemoteUrl, RemotePort
| order by Timestamp desc

5. Firewall-rule creation

let Lookback = 30d;
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where FileName =~ "netsh.exe"
| where ProcessCommandLine has "advfirewall firewall"
    and ProcessCommandLine has_any ("Tailscale-In", "Tailscale-Process")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine,
          InitiatingProcessFileName
| order by Timestamp desc

6. UDP/3478 fan-out

let Lookback = 7d;
DeviceNetworkEvents
| where Timestamp > ago(Lookback)
| where RemotePort == 3478
| summarize DistinctRemoteIPs = dcount(RemoteIP),
            FirstSeen = min(Timestamp), LastSeen = max(Timestamp)
    by DeviceName, InitiatingProcessFileName, bin(Timestamp, 5m)
| where DistinctRemoteIPs >= 15
| order by DistinctRemoteIPs desc

7. Linux-specific activity

let Lookback = 30d;
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where DeviceOSPlatform == "Linux"
| where FileName in~ ("iptables", "ip6tables", "xtables-nft-multi", "nft")
| where ProcessCommandLine has_any ("ts-input", "ts-forward", "ts-postrouting")
| project Timestamp, DeviceName, AccountName, ProcessCommandLine,
          InitiatingProcessFileName
| order by Timestamp desc

8. Shadow-IT correlation

let Lookback = 30d;
let KnownMeshHosts = dynamic([]); // Populate from the CMDB or approved software inventory.
DeviceProcessEvents
| where Timestamp > ago(Lookback)
| where FileName in~ ("tailscale.exe", "tailscaled.exe", "headscale.exe", "tailscaled", "tailscale")
| distinct DeviceName
| where DeviceName !in (KnownMeshHosts)

Threat hunting with YARA

import "pe"

rule Tailscale_Windows_Client_Binary
{
    meta:
        description = "Identifies likely Tailscale Windows client binaries"
        category = "policy-compliance"
        severity = "informational"
    strings:
        $path1 = "Tailscale\\tailscale.exe" nocase
        $path2 = "Tailscale\\tailscaled.exe" nocase
        $path3 = "Tailscale\\tailscale-ipn.exe" nocase
        $path4 = "Tailscale IPN\\tailscale.exe" nocase
        $svc1 = "Tailscale IPN Service" wide ascii
        $svc2 = "Tailscale" wide ascii fullword
        $ctrl1 = "login.tailscale.com" nocase
        $ctrl2 = "controlplane.tailscale.com" nocase
        $dns = "100.100.100.100" ascii
        $suffix = ".ts.net" nocase
        $magicdns = "MagicDNS" nocase
    condition:
        uint16(0) == 0x5A4D and
        (any of ($path*) or 2 of ($svc*, $ctrl*, $dns, $suffix, $magicdns))
}

rule Tailscale_Windows_Firewall_And_Persistence_Artifact
{
    meta:
        description = "Matches Tailscale firewall names and Startup shortcut text"
        category = "host-artifact"
        severity = "informational"
    strings:
        $fwrule1 = "Tailscale-In" ascii wide
        $fwrule2 = "Tailscale-Process" ascii wide
        $startup = "StartUp\\Tailscale.lnk" nocase ascii wide
    condition:
        any of them
}

rule Tailscale_NRPT_Registry_Artifact
{
    meta:
        description = "Matches text exports containing Tailscale-related NRPT artefacts"
        category = "host-artifact"
        severity = "informational"
    strings:
        $local = "SYSTEM\\CurrentControlSet\\Services\\Dnscache\\Parameters\\DnsPolicyConfig" nocase
        $gpo = "SOFTWARE\\Policies\\Microsoft\\Windows NT\\DNSClient\\DnsPolicyConfig" nocase
        $genericdns = "GenericDNSServers" ascii wide
        $tsip = "100.100.100.100" ascii wide
    condition:
        ($local and 1 of ($genericdns, $tsip)) or ($gpo and $genericdns)
}

rule Headscale_Server_Artifact
{
    meta:
        description = "Identifies likely Headscale server binaries and configuration artefacts"
        category = "policy-compliance"
        severity = "informational"
    strings:
        $bin = "headscale" fullword nocase
        $server = "server_url:" ascii
        $noise = "noise:" ascii
        $derp = "derp:" ascii
        $db = "headscale.db" nocase
        $module = "github.com/juanfont/headscale" ascii
    condition:
        $module or ($bin and 2 of ($server, $noise, $derp, $db))
}

rule Tailscale_Linux_Artifact
{
    meta:
        description = "Matches confirmed Linux process, netfilter and package artefacts"
        category = "host-artifact"
        severity = "informational"
    strings:
        $chain1 = "ts-input" ascii fullword
        $chain2 = "ts-forward" ascii fullword
        $chain3 = "ts-postrouting" ascii fullword
        $state = "tailscaled.state" ascii
        $socket = "tailscale/tailscaled.sock" ascii
        $port = "--port=41641" ascii
        $repo = "/etc/apt/sources.list.d/tailscale.list" ascii
    condition:
        2 of them
}

rule Tailscale_Linux_DNS_Config_Artifact
{
    meta:
        description = "Matches Linux DNS-integration artefacts in text or log exports"
        category = "host-artifact"
        severity = "informational"
    strings:
        $iface = "tailscale0" ascii
        $resolved = "org.freedesktop.resolve1" ascii
        $backup = "resolv.pre-tailscale-backup.conf" nocase ascii
        $tsip = "100.100.100.100" ascii
    condition:
        $backup or ($iface and $resolved) or ($iface and $tsip)
}

Testing status

Methodology and limitations

MITRE ATT&CK considerations

Conclusion

Our Latest Insights

Previous
Previous