Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ find_nuget_package(Microsoft.WSL.DeviceHost WSL_DEVICE_HOST /build/native)
find_nuget_package(Microsoft.WSL.Kernel KERNEL /build/native)
find_nuget_package(Microsoft.WSL.bsdtar BSDTARD /build/native/bin)
find_nuget_package(Microsoft.WSL.LinuxSdk LINUXSDK /)
#find_nuget_package(Microsoft.WSL.BPF WSL_BPF /)
set(WSL_BPF_SOURCE_DIR "${CMAKE_SOURCE_DIR}/packages/Microsoft.WSL.BPF.1.0.0")
find_nuget_package(Microsoft.WSL.TestDistro TEST_DISTRO /)
find_nuget_package(Microsoft.WSLg WSLG /build/native/bin)
find_nuget_package(vswhere VSWHERE /tools)
Expand Down
1 change: 1 addition & 0 deletions packages.config
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<package id="Microsoft.Windows.SDK.NET.Ref" version="10.0.26100.81" />
<package id="Microsoft.WindowsAppSDK" version="1.8.251106002" />
<package id="Microsoft.WSL.bsdtar" version="0.0.2-2" />
<!-- <package id="Microsoft.WSL.BPF" version="1.0.0" /> -->
<package id="Microsoft.WSL.Dependencies.amd64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
<package id="Microsoft.WSL.Dependencies.arm64fre" version="10.0.27820.1000-250318-1700.rs-base2-hyp" targetFramework="native" />
<package id="Microsoft.WSL.DeviceHost" version="1.2.10-0" />
Expand Down
124 changes: 124 additions & 0 deletions src/linux/bpf/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Copyright (C) Microsoft Corporation. All rights reserved.
#
# CMakeLists.txt for building BPF skeleton headers.
# This is intended to be run on a Linux build machine, NOT as part of the main Windows build.
#
# Required parameters:
# -DKERNEL_IMAGE_PATH=<path> Path to the WSL kernel binary (bzImage/Image) with embedded BTF
# -DBPFTOOL_PATH=<path> Path to the bpftool binary
#
# Optional parameters:
# -DCLANG_BPF=<path> Path to clang (default: clang)
# -DOUTPUT_DIR=<path> Output directory for generated headers (default: ${CMAKE_BINARY_DIR}/output)
#
# Usage:
# cmake -S . -B build -DKERNEL_IMAGE_PATH=/path/to/bzImage -DBPFTOOL_PATH=/usr/sbin/bpftool
# cmake --build build
#
# Output:
# ${OUTPUT_DIR}/bind_monitor.skel.h - BPF skeleton header (embed in NuGet)
# ${OUTPUT_DIR}/bind_monitor.h - Shared event struct header (embed in NuGet)

cmake_minimum_required(VERSION 3.20)
project(wsl-bpf LANGUAGES NONE)

# Validate required parameters
if(NOT DEFINED KERNEL_IMAGE_PATH)
message(FATAL_ERROR "KERNEL_IMAGE_PATH is required. Pass -DKERNEL_IMAGE_PATH=<path to WSL kernel binary>")
endif()

if(NOT EXISTS "${KERNEL_IMAGE_PATH}")
message(FATAL_ERROR "Kernel image not found: ${KERNEL_IMAGE_PATH}")
endif()

if(NOT DEFINED BPFTOOL_PATH)
message(FATAL_ERROR "BPFTOOL_PATH is required. Pass -DBPFTOOL_PATH=<path to bpftool>")
endif()

if(NOT EXISTS "${BPFTOOL_PATH}")
message(FATAL_ERROR "bpftool not found: ${BPFTOOL_PATH}")
endif()

# Optional parameters
if(NOT DEFINED CLANG_BPF)
set(CLANG_BPF "clang")
endif()

if(NOT DEFINED OUTPUT_DIR)
set(OUTPUT_DIR "${CMAKE_BINARY_DIR}/output")
endif()

file(MAKE_DIRECTORY "${OUTPUT_DIR}")

set(SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(VMLINUX_H "${CMAKE_BINARY_DIR}/vmlinux.h")
set(BPF_OBJ "${CMAKE_BINARY_DIR}/bind_monitor.bpf.o")
set(SKEL_H "${OUTPUT_DIR}/bind_monitor.skel.h")
set(EVENT_H "${OUTPUT_DIR}/bind_monitor.h")

# Detect target architecture for BPF
execute_process(
COMMAND uname -m
OUTPUT_VARIABLE HOST_ARCH
OUTPUT_STRIP_TRAILING_WHITESPACE)

if(HOST_ARCH STREQUAL "x86_64")
set(BPF_TARGET_ARCH "x86")
elseif(HOST_ARCH STREQUAL "aarch64")
set(BPF_TARGET_ARCH "arm64")
else()
message(FATAL_ERROR "Unsupported architecture: ${HOST_ARCH}")
endif()

# Locate libbpf headers (needed for BPF compilation)
find_path(LIBBPF_INCLUDE_DIR "bpf/bpf_helpers.h"
PATHS /usr/include /usr/local/include
REQUIRED)

# Step 1: Generate vmlinux.h from kernel image
add_custom_command(
OUTPUT "${VMLINUX_H}"
COMMAND bash "${SRC_DIR}/generate-vmlinux-header.sh" "${KERNEL_IMAGE_PATH}" "${VMLINUX_H}"
DEPENDS "${KERNEL_IMAGE_PATH}" "${SRC_DIR}/generate-vmlinux-header.sh"
COMMENT "Generating vmlinux.h from kernel image"
VERBATIM)

# Step 2: Compile BPF program
add_custom_command(
OUTPUT "${BPF_OBJ}"
COMMAND "${CLANG_BPF}" -O2 -g -target bpf
-D__TARGET_ARCH_${BPF_TARGET_ARCH}
-I "${CMAKE_BINARY_DIR}"
-I "${LIBBPF_INCLUDE_DIR}"
-I "${SRC_DIR}"
-c "${SRC_DIR}/bind_monitor.bpf.c"
-o "${BPF_OBJ}"
DEPENDS "${SRC_DIR}/bind_monitor.bpf.c" "${SRC_DIR}/bind_monitor.h" "${VMLINUX_H}"
COMMENT "Compiling bind_monitor.bpf.c"
VERBATIM)

# Step 3: Generate skeleton header
add_custom_command(
OUTPUT "${SKEL_H}"
COMMAND "${BPFTOOL_PATH}" gen skeleton "${BPF_OBJ}" > "${SKEL_H}"
DEPENDS "${BPF_OBJ}"
COMMENT "Generating bind_monitor.skel.h"
VERBATIM)

# Step 4: Copy shared header to output
add_custom_command(
OUTPUT "${EVENT_H}"
COMMAND ${CMAKE_COMMAND} -E copy "${SRC_DIR}/bind_monitor.h" "${EVENT_H}"
DEPENDS "${SRC_DIR}/bind_monitor.h"
COMMENT "Copying bind_monitor.h to output"
VERBATIM)

# Main target
add_custom_target(bpf_headers ALL DEPENDS "${SKEL_H}" "${EVENT_H}")

message(STATUS "BPF build configuration:")
message(STATUS " Kernel image: ${KERNEL_IMAGE_PATH}")
message(STATUS " bpftool: ${BPFTOOL_PATH}")
message(STATUS " clang: ${CLANG_BPF}")
message(STATUS " Target arch: ${BPF_TARGET_ARCH}")
message(STATUS " Output dir: ${OUTPUT_DIR}")
78 changes: 78 additions & 0 deletions src/linux/bpf/bind_monitor.bpf.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-License-Identifier: GPL-2.0

#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
#include "bind_monitor.h"

struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, BIND_MONITOR_RINGBUF_SIZE);
} events SEC(".maps");

static __always_inline int emit_bind_event(struct sock *sk, int ret)
{
struct bind_event *e;

if (!sk || ret != 0)
return 0;

__u16 family = BPF_CORE_READ(sk, __sk_common.skc_family);
if (family != AF_INET && family != AF_INET6)
return 0;

__u16 protocol = BPF_CORE_READ(sk, sk_protocol);
if (protocol != IPPROTO_TCP && protocol != IPPROTO_UDP)
return 0;

e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
if (!e)
return 0;

e->family = family;
e->protocol = protocol;
e->port = BPF_CORE_READ(sk, __sk_common.skc_num);
e->pad = 0;

if (family == AF_INET)
{
struct inet_sock *inet = (struct inet_sock *)sk;
e->addr4 = BPF_CORE_READ(inet, inet_saddr);
__builtin_memset(e->addr6, 0, sizeof(e->addr6));
}
else
{
struct ipv6_pinfo *pinet6;
e->addr4 = 0;
pinet6 = BPF_CORE_READ((struct inet_sock *)sk, pinet6);
if (pinet6)
BPF_CORE_READ_INTO(e->addr6, pinet6, saddr.in6_u.u6_addr8);
else
__builtin_memset(e->addr6, 0, sizeof(e->addr6));
}

bpf_ringbuf_submit(e, 0);
return 0;
}

// fexit/inet_bind: called after inet_bind() returns.
SEC("fexit/inet_bind")
int BPF_PROG(fexit_inet_bind, struct socket *sock, struct sockaddr *uaddr,
int addr_len, int ret)
{
struct sock *sk = BPF_CORE_READ(sock, sk);
return emit_bind_event(sk, ret);
}

// fexit/inet6_bind: called after inet6_bind() returns.
SEC("fexit/inet6_bind")
int BPF_PROG(fexit_inet6_bind, struct socket *sock, struct sockaddr *uaddr,
int addr_len, int ret)
{
struct sock *sk = BPF_CORE_READ(sock, sk);
return emit_bind_event(sk, ret);
}

char LICENSE[] SEC("license") = "GPL";
35 changes: 35 additions & 0 deletions src/linux/bpf/bind_monitor.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright (C) Microsoft Corporation. All rights reserved.

#pragma once

#ifndef __u8
typedef unsigned char __u8;
typedef unsigned short __u16;
typedef unsigned int __u32;
typedef unsigned long long __u64;
#endif

#ifndef AF_INET
#define AF_INET 2
#endif
#ifndef AF_INET6
#define AF_INET6 10
#endif

#ifndef IPPROTO_TCP
#define IPPROTO_TCP 6
#endif
#ifndef IPPROTO_UDP
#define IPPROTO_UDP 17
#endif

#define BIND_MONITOR_RINGBUF_SIZE (1 << 16) /* 64 KB */

struct bind_event {
__u32 family; /* AF_INET or AF_INET6 */
__u32 protocol; /* IPPROTO_TCP or IPPROTO_UDP */
__u16 port; /* host byte order */
__u16 pad;
__u32 addr4; /* IPv4 address (network byte order) */
__u8 addr6[16]; /* IPv6 address */
};
111 changes: 111 additions & 0 deletions src/linux/bpf/generate-vmlinux-header.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#!/bin/bash
set -e

if [ $# -lt 2 ]; then
echo "Usage: $0 <bzImage> <output-vmlinux.h>"
exit 1
fi

BZIMAGE="$1"
OUTPUT="$2"
BPFTOOL="${BPFTOOL:-bpftool}"

TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT

VMLINUX="$TMPDIR/vmlinux"

echo "Extracting vmlinux from $BZIMAGE..."

# First, try bpftool directly (works if input is already an ELF with BTF)
if "$BPFTOOL" btf dump file "$BZIMAGE" format c > "$OUTPUT" 2>/dev/null; then
LINES=$(wc -l < "$OUTPUT")
if [ "$LINES" -gt 100 ]; then
echo "Done. Generated $OUTPUT ($LINES lines)"
exit 0
fi
fi

# Try to find an ELF embedded in the image (e.g., ARM64 Image)
ELF_OFFSET=$(binwalk -y elf "$BZIMAGE" 2>/dev/null | grep -oP '^\d+' | head -1) || true
if [ -n "$ELF_OFFSET" ]; then
echo "Found ELF at offset $ELF_OFFSET"
tail -c +$((ELF_OFFSET + 1)) "$BZIMAGE" > "$VMLINUX"
if file "$VMLINUX" | grep -q 'ELF' && "$BPFTOOL" btf dump file "$VMLINUX" format c > "$OUTPUT" 2>/dev/null; then
LINES=$(wc -l < "$OUTPUT")
if [ "$LINES" -gt 100 ]; then
echo "Done. Generated $OUTPUT ($LINES lines)"
exit 0
fi
fi
fi

# Try to find raw BTF data in the image (ARM64 Image stores BTF as raw data)
BTF_RAW="$TMPDIR/btf.raw"
BTF_OFFSET=$(python3 -c "
import struct, sys
data = open(sys.argv[1], 'rb').read()
magic = b'\x9f\xeb'
idx = 0
while True:
idx = data.find(magic, idx)
if idx == -1: break
if data[idx+2] == 1: # version 1
hdr_len = struct.unpack_from('<I', data, idx+4)[0]
if hdr_len == 24: # standard BTF header
_, _, _, _, type_off, type_len, str_off, str_len = struct.unpack_from('<HBBI IIII', data, idx)
total = hdr_len + type_len + str_len
if total > 1000:
print(f'{idx} {total}')
break
idx += 1
" "$BZIMAGE" 2>/dev/null) || true

if [ -n "$BTF_OFFSET" ]; then
BTF_OFF=$(echo "$BTF_OFFSET" | awk '{print $1}')
BTF_SIZE=$(echo "$BTF_OFFSET" | awk '{print $2}')
echo "Found raw BTF at offset $BTF_OFF ($BTF_SIZE bytes)"
tail -c +$((BTF_OFF + 1)) "$BZIMAGE" | head -c "$BTF_SIZE" > "$BTF_RAW"
if "$BPFTOOL" btf dump file "$BTF_RAW" format c > "$OUTPUT" 2>/dev/null; then
LINES=$(wc -l < "$OUTPUT")
if [ "$LINES" -gt 100 ]; then
echo "Done. Generated $OUTPUT ($LINES lines)"
exit 0
fi
fi
fi

# Find gzip offset and decompress to get the vmlinux ELF
GZIP_OFFSET=$(binwalk -y gzip "$BZIMAGE" 2>/dev/null | grep -oP '^\d+' | head -1) || true

if [ -z "$GZIP_OFFSET" ]; then
echo "Error: no gzip or ELF payload found in $BZIMAGE" >&2
exit 1
fi

echo "Found gzip payload at offset $GZIP_OFFSET"
tail -c +$((GZIP_OFFSET + 1)) "$BZIMAGE" | zcat > "$VMLINUX" 2>/dev/null || true

if ! file "$VMLINUX" | grep -q 'ELF'; then
# The gzip payload might contain another layer; try to find ELF inside
INNER="$TMPDIR/inner"
tail -c +$((GZIP_OFFSET + 1)) "$BZIMAGE" | zcat 2>/dev/null > "$INNER" || true

# Search for ELF magic in decompressed data
ELF_OFFSET=$(grep -a -b -o -P '\x7fELF' "$INNER" 2>/dev/null | head -1 | cut -d: -f1) || true
if [ -n "$ELF_OFFSET" ]; then
tail -c +$((ELF_OFFSET + 1)) "$INNER" > "$VMLINUX"
fi
fi

if ! file "$VMLINUX" | grep -q 'ELF'; then
echo "Error: could not extract a valid ELF vmlinux from $BZIMAGE" >&2
exit 1
fi

echo "Extracted vmlinux: $(file "$VMLINUX")"
echo "Generating vmlinux.h with bpftool..."
"$BPFTOOL" btf dump file "$VMLINUX" format c > "$OUTPUT"

LINES=$(wc -l < "$OUTPUT")
echo "Done. Generated $OUTPUT ($LINES lines)"
Loading
Loading