Syscall Sidequest: TCP Fast Open

Using TCP Fast Open to bypass syscall-based networking rules

╔═══╦═══╦═══╦═══╦═══╦═══╗
║╔═╗║╔═╗║╔═╗║╔═╗║╔═╗║╔═╗║
╚══╝╚══╝╚══╝║╚══╝╚══╝╚══╝

Overview

TCP Fast Open (TFO) is a method of initializing and using a TCP connection where the client can send data in the initial SYN packet sent to the server. This extension to the TCP protocol was implemented for speed benefits because the back and forth of the initial TCP handshake can be skipped for repeated connections. There’s more to the implementation details if you care about using it in a production-level setting (you can read more of the original spec proposed in 2014 here), but it’s useful for evading some syscall-based rule engines because it’s one way of opening TCP connections without using the connect syscall explicitly.

I kind of stumbled into this as a side quest while finding bypasses for a Falco rule that generates alerts for Kubernetes API requests coming from inside a pod (which could be anomalous behavior a SOC analyst would want to know about). In doing more research on TFO, CVE-2026-63828/CVE-2026-72243 came up as a (recent) known bypass specific to networking-confined processes in AppArmor/SELinux. It applies to more than just AppArmor and SELinux though (eBPF-based monitoring engines like Falco, Tetragon, and Tracee rely on syscall-based hooking as well) so I thought I’d elaborate on the technique since I thought it was an interesting little rabbit hole.

Case Study: Falco Rule Evasion

The use cases for making arbitrary TCP connections from a restricted environment are near infinite, but let’s use the example that caught my interest to begin with: say you wanted to query a Kubernetes API server from inside a container as part of post-ex recon where there are eBPF-, AppArmor-, SELinux-, or some other syscall-based monitoring/blocking rules in place. If you don’t want to use a more heavy networking implementation/library in userland and other syscall evasion primitives like io_uring aren’t available, TFO might be useful.

Falco’s default ruleset generates an alert when a non-whitelisted user/process/container attempts to contact the Kubernetes API server.

- rule: Contact K8S API Server From Container
...
  condition: >
    evt.type=connect
    and (fd.typechar=4 or fd.typechar=6)
    and container
    and k8s_api_server
    and not k8s_containers
    and not user_known_contact_k8s_api_server_activities

This boils down to:

  1. Look for the connect() syscall
  2. Check that it’s an ipv4/ipv6 connection (excluding things like pipes and files)
  3. Check if the address being connected to resolves to the K8s API server macro definition (“kubernetes.default.svc.cluster.local” by default, which is usually the FQDN of the KUBERNETES_SERVICE_HOST environment variable IP set in a pod)
  4. Check if the call is coming from a predefined whitelist of containers/namespaces
  5. Check if the call is coming from some other whitelisted activity (left unimplemented by default)

In using TFO to initialize a TCP socket, we can sidestep the connect syscall that Falco and other syscall-based security solutions are checking for.

Basic Mechanics/Usage

From a syscall perspective, a basic TCP connection generally looks like:

socket() -> connect() -> write()/read()

A TFO connection, however, is initialized using a sendto syscall with the MSG_FASTOPEN flag:

socket() -> sendto(...,MSG_FASTOPEN,...) -> write()/read()

TFO is enabled at the kernel level in /proc/sys/net/ipv4/tcp_fastopen, where:

1 seems to be the standard setting but I’m not sure if there’s a universal default. I did my testing using a Kubernetes node running locally on a Debian host and 1 was the default setting, with which I was able to query the k8s API using a TFO-initialized socket.

I didn’t know about TFO before writing writing this (and I’m definitely oblivious to most of the finer implementation details) but I wanted to implement a proof of concept for the specific use-case of probing a kubernetes API without using the connect syscall Falco is checking for. There are libraries which provide TFO support to varying degrees, but I wanted to write something myself to keep the implementation minimal and to force myself to learn the basic mechanics. The tricky part was choosing the level of abstraction at which to insert the TFO-based logic from a practicality perspective. Client-go is a common library for including kubectl-like utilities in a Go application, which would be useful for a custom implant capable of doing kubernetes recon. For maximum flexibility with the technique, you could provide TFO overrides for the default net.Dialer/net.Conn Go uses for making TCP connections and force client-go to use the custom TFO context you define:

client-go -> net.Dialer/net.Conn -> custom TFO context

Doing all that is overkill for a basic proof of concept, but I thought I’d at least mention it as an example of how you can overlay existing networking-reliant utilities on top of the TFO-initialization primitive. This repo has a super basic PoC I made for making GET requests to the kubernetes API which:

  1. Implements Go’s net.Conn interface with some TFO-specific init and control flow logic that we can tweak at the syscall level
  2. Passes the custom connection context to Go’s TLS client (TLS is needed since the k8s API is https)
  3. Makes sure the Write() function uses the sendto() syscall with MSG_FASTOPEN flag for the initial socket write (the TLS Client Hello), establishing a TCP connection without the connect() syscall

Again, this is more meant to describe the overall bypass technique rather than provide a specific implementation guide. If you peeked at the TFO spec you’ll also notice TFO cookies are a key part of how TFO connections are made/managed, but it wasn’t needed for this since we’re just initializing a socket using TFO rather than trying to make use of the full TFO protocol as designed. Fixes exist for the AppArmor/SELinux CVEs mentioned earlier, but in other engines with more free-form rule definitions like Falco, techniques like this can be a useful evasion primitive against hardcoded syscall-based detection logic.

References