Skip to content

tailcat: add Server.AllowClient hook for admitting clients - #120

Open
jormenjanssen wants to merge 1 commit into
tailscale:mainfrom
riwo:jormen/allow-client-callback
Open

jormenjanssen wants to merge 1 commit into
tailscale:mainfrom
riwo:jormen/allow-client-callback

Conversation

@jormenjanssen

Copy link
Copy Markdown

Adds Server.AllowClient, a func(key.NodePublic) bool hook in the shape of AllowProxy, for programs embedding tailcat that decide at runtime which clients may connect and can't list every node key in AllowedClients up front.

Semantics: a key in AllowedClients is admitted without consulting the hook, any other key is admitted if the hook approves it, and with neither configured the server stays open to all clients as before. Rejected clients are ignored the same way unlisted ones are, and are asked about again when they retry their meow. onMeow now recognizes an already connected client before the admission check, so a connected client's later pings never reach the hook.

Design choice for review. The hook runs under the backend's mutex, which peerByIP and peerForIP also take, so it is documented as non-blocking: return quickly, no I/O, cache decisions from an external service for the hook to look up. That kept the change small. The alternative is a blocking-capable func(ctx context.Context, k key.NodePublic) bool, which needs the check moved out of the lock, a ctx canceled at Server.Close, and deduplication of the client's once-a-second meow retries while a decision is pending (the client's 10 second ping timeout bounds it either way). Happy to rework to that variant if you'd rather have it; say which and I'll update this PR.

Tests: TestAllowClient covers a listed key admitted without the hook, an approved key admitted with exactly one hook call across two pings, and a rejected key that gets no meowed and does reach the hook. TestAllowClientOnly checks that a hook without any AllowedClients still closes the server to rejected clients. Both run against the local DERP and STUN from tstest/integration like the other server tests. go test ./..., go vet ./... and make tidy are clean on Windows; the CI matrix covers the rest.

No CLI change; --allow is untouched.

Updates #119

Server.AllowedClients only admits a fixed list of node keys, so a
program that decides at runtime which clients may connect, from a
database or a directory service say, has to know every key before
Start or keep the list current itself with AddAllowedClient, and can
never turn a key down once it is on the list.

Add Server.AllowClient, a func(key.NodePublic) bool in the shape of
AllowProxy, consulted when a client that is not already connected and
not in AllowedClients announces itself. A listed key is admitted
without the hook; any other key is admitted if the hook approves it;
with neither configured the server stays open to all clients, as
before. A rejected client is ignored like an unlisted one and asked
about again when it retries its meow.

The hook runs under the backend's mutex, which peerByIP and peerForIP
on the data path also take, so it must return quickly and not do I/O.
That keeps this change small. A blocking-capable variant would need
the check moved out of the lock, a context canceled at Close, and
deduplication of the client's once-a-second meow retries while a
decision is pending; a program gating clients on another service can
instead decide ahead of time and have the hook look the decision up.

onMeow now recognizes an already connected client before the
admission check rather than after it, so a connected client's later
pings never reach the hook. For the list this changes nothing, since
a key can't be removed from it.

Updates tailscale#119

Signed-off-by: Jormen Janssen <j.janssen@riwo.eu>
@jormenjanssen
jormenjanssen force-pushed the jormen/allow-client-callback branch from 45a0a2b to a95bb65 Compare September 16, 2026 12:29
Comment thread tailcat.go
Comment on lines 449 to +470
// AllowedClients, if non-empty, restricts which client node keys
// may connect; all others are silently ignored. If empty, all
// clients are allowed. See [Server.AddAllowedClient] to add more
// at runtime.
// at runtime, and AllowClient to decide per client instead of
// from a fixed list.
AllowedClients []key.NodePublic

// AllowClient, if non-nil, reports whether the client with node
// key k may connect. It is consulted when a client that is not
// already connected and not in AllowedClients announces itself;
// a client it rejects is silently ignored, like an unlisted one,
// and is asked about again if it retries. If AllowClient is nil
// and AllowedClients is empty, all clients are allowed.
//
// It is called with the server's internal lock held, so it must
// return quickly and must not block on I/O: a slow call stalls
// every connected client's traffic. A program gating clients on
// another service should decide ahead of time and have AllowClient
// look the decision up.
//
// It must be set before calling Start.
AllowClient func(k key.NodePublic) bool

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to tighten the docs on the precedence here about when we fail open.

Actually, maybe we should just ditch the slice and only use the func? We can provide a tiny little adapter for people migrating from the slice style (probably very few people)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Replied on the issue too)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants