Skip to content

Commit

Permalink
Merge pull request #1134 from NordSecurity/LLT-5912-pcap-from-host
Browse files Browse the repository at this point in the history
Capture pcaps on host using tcpdump
  • Loading branch information
tomaszklak authored Feb 26, 2025
2 parents 4793715 + 6261214 commit c25c349
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/gitlab.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
with:
schedule: ${{ github.event_name == 'schedule' }}
cancel-outdated-pipelines: ${{ github.ref_name != 'main' }}
triggered-ref: v2.8.5 # REMEMBER to also update in .gitlab-ci.yml
triggered-ref: v2.8.6 # REMEMBER to also update in .gitlab-ci.yml
2 changes: 1 addition & 1 deletion .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ libtelio-build-pipeline:

trigger:
project: $LIBTELIO_BUILD_PROJECT_PATH
branch: v2.8.5 # REMEMBER to also update in .github/workflows/gitlab.yml
branch: v2.8.6 # REMEMBER to also update in .github/workflows/gitlab.yml
Empty file.
3 changes: 2 additions & 1 deletion nat-lab/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from utils.ping import ping
from utils.process import ProcessExecError
from utils.router import IPStack
from utils.tcpdump import make_tcpdump
from utils.tcpdump import make_tcpdump, make_local_tcpdump
from utils.vm import mac_vm_util, windows_vm_util

DERP_SERVER_1_ADDR = "http://10.0.10.1:8765"
Expand Down Expand Up @@ -498,6 +498,7 @@ async def async_context():
await SESSION_SCOPE_EXIT_STACK.enter_async_context(
make_tcpdump(connections, session=True)
)
await SESSION_SCOPE_EXIT_STACK.enter_async_context(make_local_tcpdump())

RUNNER.run(async_context())

Expand Down
12 changes: 12 additions & 0 deletions nat-lab/tests/utils/connection/connection.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import platform
from abc import ABC, abstractmethod
from enum import Enum, auto
from typing import List, Optional
Expand All @@ -9,6 +10,17 @@ class TargetOS(Enum):
Windows = auto()
Mac = auto()

@staticmethod
def local():
system = platform.system()
if system == "Windows":
return TargetOS.Windows
if system == "Linux":
return TargetOS.Linux
if system == "Darwin":
return TargetOS.Mac
raise ValueError(f"{system} is not supported")


class Connection(ABC):
_target_os: Optional[TargetOS]
Expand Down
165 changes: 115 additions & 50 deletions nat-lab/tests/utils/tcpdump.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import asyncio
import os
import secrets
import subprocess
from asyncio import Event, wait_for, sleep
from config import WINDUMP_BINARY_WINDOWS
from contextlib import asynccontextmanager, AsyncExitStack
Expand Down Expand Up @@ -47,46 +49,15 @@ def __init__(

self.output_notifier.notify_output("listening on", self.start_event)

command = [self.get_tcpdump_binary(connection.target_os), "-n"]

if self.output_file:
command += ["-w", self.output_file]
else:
command += ["-w", PCAP_FILE_PATH[self.connection.target_os]]

if self.interfaces:
if self.connection.target_os != TargetOS.Windows:
command += ["-i", ",".join(self.interfaces)]
else:
# TODO(gytsto). Windump itself only supports one interface at the time,
# but it supports multiple instances of Windump without any issues,
# so there is a workaround we can do for multiple interfaces:
# - create multiple process of windump for each interface
# - when finished with dump, just combine the pcap's with `mergecap` or smth
print(
"[Warning] Currently tcpdump for windows support only 1 interface"
)
command += ["-i", self.interfaces[0]]
else:
if self.connection.target_os != TargetOS.Windows:
command += ["-i", "any"]
else:
command += ["-i", "2"]

if self.count:
command += ["-c", str(self.count)]

if flags:
command += flags

if self.connection.target_os != TargetOS.Windows:
command += ["--immediate-mode"]
command += ["port not 22"]
else:
command += ["not port 22"]

if expressions:
command += expressions
command = build_tcpdump_command(
self.connection.target_os,
flags,
expressions,
self.interfaces,
self.output_file,
self.count,
False,
)

self.process = self.connection.create_process(
command,
Expand All @@ -97,16 +68,6 @@ def __init__(
kill_id="DO_NOT_KILL" + secrets.token_hex(8).upper() if session else None,
)

@staticmethod
def get_tcpdump_binary(target_os: TargetOS) -> str:
if target_os in [TargetOS.Linux, TargetOS.Mac]:
return "tcpdump"

if target_os == TargetOS.Windows:
return WINDUMP_BINARY_WINDOWS

raise ValueError(f"target_os not supported {target_os}")

def get_stdout(self) -> str:
return self.stdout

Expand Down Expand Up @@ -140,6 +101,72 @@ async def run(self) -> AsyncIterator["TcpDump"]:
await sleep(5)


def build_tcpdump_command(
target_os: TargetOS,
flags: Optional[list[str]] = None,
expressions: Optional[list[str]] = None,
interfaces: Optional[list[str]] = None,
output_file: Optional[str] = None,
count: Optional[int] = None,
include_ssh: bool = False,
using_sudo: bool = False,
):
def get_tcpdump_binary(target_os: TargetOS) -> str:
if target_os in [TargetOS.Linux, TargetOS.Mac]:
return "tcpdump"

if target_os == TargetOS.Windows:
return WINDUMP_BINARY_WINDOWS

raise ValueError(f"target_os not supported {target_os}")

if using_sudo:
command = ["sudo"]
else:
command = []
command += [get_tcpdump_binary(target_os), "-n"]

if output_file:
command += ["-w", output_file]
else:
command += ["-w", PCAP_FILE_PATH[target_os]]

if interfaces:
if target_os != TargetOS.Windows:
command += ["-i", ",".join(interfaces)]
else:
# TODO(gytsto). Windump itself only supports one interface at the time,
# but it supports multiple instances of Windump without any issues,
# so there is a workaround we can do for multiple interfaces:
# - create multiple process of windump for each interface
# - when finished with dump, just combine the pcap's with `mergecap` or smth
print("[Warning] Currently tcpdump for windows support only 1 interface")
command += ["-i", interfaces[0]]
else:
if target_os != TargetOS.Windows:
command += ["-i", "any"]
else:
command += ["-i", "2"]

if count:
command += ["-c", str(count)]

if flags:
command += flags

if not include_ssh:
if target_os != TargetOS.Windows:
command += ["--immediate-mode"]
command += ["port not 22"]
else:
command += ["not port 22"]

if expressions:
command += expressions

return command


def find_unique_path_for_tcpdump(log_dir, guest_name):
candidate_path = f"{log_dir}/{guest_name}.pcap"
counter = 1
Expand All @@ -152,6 +179,44 @@ def find_unique_path_for_tcpdump(log_dir, guest_name):
return candidate_path


@asynccontextmanager
async def make_local_tcpdump():
target_os = TargetOS.local()
using_sudo = target_os != TargetOS.Windows and os.geteuid() != 0
command = build_tcpdump_command(
target_os,
None,
None,
["any"],
"logs/local.pcap",
None,
include_ssh=False,
using_sudo=using_sudo,
)

os.makedirs("logs", exist_ok=True)

process = None
try:
process = await asyncio.create_subprocess_exec(
*command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
yield
except Exception:
if process:
print("tcpdump stderr:")
print(process.stderr)
print("tcpdump stdout:")
print(process.stdout)
raise
finally:
if process:
process.kill()
await process.wait()


@asynccontextmanager
async def make_tcpdump(
connection_list: list[Connection],
Expand Down

0 comments on commit c25c349

Please sign in to comment.