Someone wants to inspect what clients send through the tunnel, finds the connect hook in the documentation, and starts writing a script that will never receive a single packet. The hook is real and useful, but its unit of work is a session, not a byte. Choosing the right component is a comparison between three places work can happen, and the deciding question is which unit each one can actually see.

What the hook receives, and what it never receives

A client-connect hook runs once when an authenticated client establishes a session, in the server's process context, with documented arguments and environment values such as the common name and assigned address. It can write per-client configuration, make an admission decision, allocate an external resource or record that the connection happened. What it cannot do is observe traffic, because by the time the first packet crosses the tunnel the hook has already returned.

That is not a limitation to work around. A session callback that tried to process a stream would have to run for the lifetime of the connection, and the daemon's scripting contract is not built for that shape. Ask what the hook is for before deciding it is in your way.

Reconnects matter as well. A client that drops and reconnects is a new session, so anything the hook allocates has to be idempotent or paired with a disconnect event that cleans up. State that leaks on every reconnect is the usual first bug in a hook-based design.

Session policy belongs in the hook

Admission is the hook's natural job. You know who connected, from where, and with which certificate, and you can decide whether this session should proceed or be refused. That decision is cheap, it happens once, and it does not require looking at a single payload byte.

Configuration assignment fits the same shape. A hook can select the client's address, push options appropriate to that identity, or write a fragment the server reads for this client. Because the decision is per session, it stays comprehensible when something goes wrong and someone has to explain why a particular client got a particular address.

Recording is the third fit. An audit entry that says a session was admitted, with its identity and time, belongs exactly here. It is not a traffic log and should not pretend to be one. If someone later asks what a client did inside the tunnel, the answer has to come from somewhere else.

Packet work belongs on a packet path

If the requirement is to see or modify bytes in flight, the component has to be in the forwarding path: a firewall rule with a queue, a traffic-control hook, or a userspace forwarder. Those mechanisms see packets, and they have their own state, their own failure modes and their own performance profile.

Editing payload at that layer is harder than reading it. Changing a byte can invalidate a length field, a checksum or a protocol state machine, and a layer-three tunnel does not protect you from any of that. It only means the outer headers are not the ones you are editing. Budget for that repair work before promising anyone a content rewrite.

An interface capture is the cheapest tool in this category, and it is an observation rather than an editor. People conflate the two because both are described as seeing traffic, but a capture produces a file and a packet path produces a decision.

Application changes belong in the application

When the goal is to change what a request contains or how a response is formed, the right place is the service itself or a proxy that terminates the protocol on purpose. Those components already understand requests, responses and the authentication the application expects, which is precisely the context a packet filter lacks.

Address translation sits at the bottom of this list for a reason. It rewrites addresses and ports and leaves the payload alone, so it is not a content tool no matter how often it appears in a recipe that also mentions content.

Keeping these boundaries separate is what makes a design explainable. Session policy in the hook, packet handling on a packet path, and application change where the protocol is deliberately terminated: three sentences that also serve as a review checklist.

Where TLS still ends the conversation

One thing survives all of the above: traffic inside the tunnel that carries HTTPS keeps its own transport encryption. Holding the VPN's keys gives you the inner IP packet, and it does not give you the session keys for the website at the other end.

That is why a hook cannot inspect content even in principle, and why a packet path sees ciphertext unless the deployment terminates the inner TLS on purpose. The scripting contract describes what the hook is given; it says nothing about payload because there is no payload to give.

Makomi ya ndenge moko