Skip to content
FreeBSDDeep Dive Published Updated 7 min readViews unavailable

FreeBSD Accept Filters: Buffering Connections Before accept(2)

Learn how FreeBSD accept filters hold incoming sockets until useful data arrives, where accf_http helps, and why it is not an HTTP security layer.

An accept filter lets a FreeBSD listening socket delay delivery of a new connection to the application until a kernel-side condition is met. Instead of waking a server as soon as the TCP connection completes, the filter can wait for initial data. This can reduce repeated wakeups for protocols whose first request arrives immediately, but it changes when the application sees a connected descriptor; it does not replace normal application parsing or connection timeouts.

The HTTP filter has a narrow contract

FreeBSD’s accf_http filter is specifically for HTTP/1.0 and HTTP/1.1 GET and HEAD requests. It withholds the descriptor until a complete supported request has been buffered. Other request forms are passed through to accept() rather than being rejected. The filter is therefore not a validator, firewall, authentication check, or general-purpose HTTP parser. It must not be used to decide whether a request is safe.

The kernel option is ACCEPT_FILTER_HTTP; on a kernel with INET support, the module can be loaded at runtime. The listening socket enables the filter with setsockopt(SOL_SOCKET, SO_ACCEPTFILTER, …) and the name httpready:

#include <sys/socket.h>
#include <string.h>

int
install_http_filter(int listener)
{
    struct accept_filter_arg filter = {0};
    strlcpy(filter.af_name, "httpready", sizeof(filter.af_name));
    return setsockopt(listener, SOL_SOCKET, SO_ACCEPTFILTER,
        &filter, sizeof(filter));
}

This is a small API sketch: production code should check the exact structure and headers for its FreeBSD target, and decide whether an unavailable module should fail startup or disable the optimization.

Choose the condition to match the protocol

The generic data filter waits for data, while the HTTP filter recognizes only its documented request pattern. Neither can inspect encrypted HTTP application bytes inside TLS. For TLS listeners, accept the TCP connection and let the TLS stack perform the handshake; then apply application deadlines and parse the decrypted request in the normal protocol layer.

Buffered bytes and half-open application work are still resources. A peer that connects and sends slowly can occupy kernel state, so retain connection limits, handshake/request timeouts, and load tests with incomplete clients. Compare CPU wakeups, accepted-connection latency, and queue pressure against the plain accept() path. Keep an ordinary accept loop for protocols where the filter’s condition is a poor fit.

Accept filters are a FreeBSD-specific optimization at the socket boundary. Their benefit depends on workload and server architecture; benchmark before enabling one globally.

Install the filter only after the socket is listening

The ordering is part of the API contract: create a stream socket, bind it, call listen() and only then install SO_ACCEPTFILTER. The setsockopt(2) manual documents a failure when an accept filter is installed before the socket is listening. The option value is a struct accept_filter_arg containing a 16-byte filter name and optional argument bytes. The example above initializes the structure to zero before copying the short registered name, so unused argument bytes cannot contain uninitialized stack data.

At runtime, check that the filter module is actually available on the target kernel. The manual says that the module can be loaded when INET support is present; a custom kernel may instead compile in the corresponding option. A service should make this failure mode explicit: either fail startup because the deployment requires the filter, or continue without it and log that the optimization was unavailable. Silent assumptions create difficult comparisons between hosts that are supposedly configured the same way.

An attached filter can be removed from a listening socket by calling setsockopt(listener, SOL_SOCKET, SO_ACCEPTFILTER, NULL, 0). This is useful for a controlled A/B test or a runtime fallback, but the server must still own the listener and coordinate changes with all worker processes using it. If workers inherited the same listener, make the transition a deliberate deployment operation rather than changing the option behind concurrent acceptors.

Pick a filter by its documented first-read condition

The available filters are not interchangeable protocol validators. The generic accf_data filter waits until at least some data arrives. The HTTP filter waits for the documented complete HTTP/1.0 or HTTP/1.1 GET or HEAD request; it passes a different request form through to user space instead of rejecting it. The DNS filter is narrower in another direction: it reads the first two bytes to determine the DNS message size and waits for that first request to be available. These contracts concern delivery of the descriptor, not whether the application should accept, authorize, or execute the request.

This distinction matters for protocol evolution. A client may send a method or version that a filter does not recognize, a proxy may use a different connection setup, or a service may receive several application protocols on one port. The safe interpretation is that the filter can change when the application sees the socket; the application remains responsible for parsing every supported request, returning protocol errors, and closing unwanted connections. Test representative valid and invalid first messages against the actual server implementation before rollout.

TLS is an especially important boundary. An accept filter sees the connection before the user-space TLS implementation has decrypted application records, so it cannot infer that encrypted bytes contain an HTTP request. For HTTPS, keep the filter off unless its documented condition is independently appropriate to the pre-TLS protocol. Perform the TLS handshake, certificate checks, ALPN negotiation, HTTP parsing, and request validation in the layers that actually own those semantics.

Model waiting clients as resource consumers

An accepted descriptor is withheld while the filter’s condition is unmet. It follows that slow, silent, or malformed peers can remain in the pre-accept path longer than a client that sends a complete first message promptly. This is an operational consequence of the documented wait behavior, not a claim that the filter itself implements a timeout or a denial-of-service defense. Connection backlog limits, network policy, service-level admission controls, and timeout handling remain separate concerns.

Build a workload test that includes clients which connect and send nothing, send one byte at a time, disconnect mid-request, send an unsupported request, and send a complete request immediately. Observe connection establishment, application accept latency, worker utilization, memory pressure, and request error rates. Compare the same test with the option removed. A lower context switch count alone is not a service-level success if slow clients consume capacity for longer or errors move from one layer to another.

Do not infer a throughput gain from the API’s existence. The historical manual explains the intended reduction in work before the first parse; the real result depends on server architecture, traffic mix, kernel configuration, and measurement interval. Benchmark with the production concurrency model and include overload behavior, not just a warm single-client request.

A rollout and rollback checklist

Before changing a production listener, record the FreeBSD release, kernel configuration, loaded module state, application version, listener ownership, and whether workers share the socket. Confirm that ordinary HTTP parsing still handles unsupported methods and malformed request lines. Confirm that the TLS listener performs the handshake before application protocol decisions. Test filter availability on a staging host with the same kernel configuration.

Deploy to a small canary set and compare the same counters and request mix used in the baseline. Keep an immediate rollback path: stop new configuration rollouts, remove the option with a NULL optval where the service safely owns the listening socket, or restart workers with the normal listener setup. Verify that the unfiltered accept path works before relying on it as recovery. Record the result per server version; an optimization that is beneficial for one prefork model may be neutral or harmful for an event-loop server.

The reliable production contract is intentionally modest: a FreeBSD kernel filter may hold delivery of a connection until a defined first-data condition. It can be useful at a specific boundary, but it is not an application parser, a timeout policy, or a substitute for measured capacity controls.

Related:

Sources:

Comments