libtailcat, swift: add C bindings and a TailcatKit Swift package - #64
Open
josuediazflores wants to merge 4 commits into
Open
libtailcat, swift: add C bindings and a TailcatKit Swift package#64josuediazflores wants to merge 4 commits into
josuediazflores wants to merge 4 commits into
Conversation
With CGO_ENABLED=0 the file that imports "C" drops out of the build but the helper files did not, leaving a package main with no main function. Constrain every file on cgo so the package is skipped instead.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #5 (C bindings), and goes one step further with a Swift package on top so macOS and iOS apps can embed tailcat without a Go toolchain in the app project.
What's in the PR
libtailcat/: a C API over thetailcatlibrary, built withgo build -buildmode=c-archive. It follows the design of libtailscale: integer handles, connections and listeners handed to C as one end of asocketpair(2)(so callers justread/write/poll/close), and a hand-writteninclude/tailcat.hfronted by a smalltailcat.cshim for const-correct signatures. 30 functions:tailcat_server_new,set_key(atailcat genkeykey file),set_region_id,set_relay_hosts(bring your own DERP),set_derpmap_url,set_embed_relay,allow_client,listen(a port, or 0 as a catch-all; before or after start),start,token,public_key,status_json,close, plustailcat_acceptandtailcat_conn_info(peer address and the port dialed)tailcat_client_new(a token),set_key,set_derpmap_url,public_key,ping,path_json(DiscoPing: direct endpoint or relay),dial,closetailcat_key_generate,key_public,key_token,token_parse,token_resolve(thegenkey/parse/resolvesubcommands)tailcat_errmsgandtailcat_set_logfdThe server start sequence mirrors
cmd/tailcat(region resolution, the embed decision beforeExpand, the same region field cleanup, and the short token built by hand, sinceServer.ConnBlob()always embeds the relay).OnTCPis a single dispatcher installed at start that consults a port table per connection, so listeners can be added after start. AMakefilebuilds macOS (arm64 + x86_64), iOS and iOS simulator (arm64 + x86_64) archives withbuild-tags.txtand packages them asCTailcat.xcframework.swift/:TailcatKit, a SwiftPM package (Swift 6 language mode, strict concurrency, macOS 14 / iOS 17) wrapping that header in async/await actors:TailcatServer,TailcatClient,Listener, aDispatchIO-backedConnection(half-close viacloseWrite()),Identity(the key file),ConnectionToken, andRelaySelection. Blocking C calls run on a dedicated dispatch queue, never on an actor or the cooperative pool. It ships atailcat-demoexecutable that doubles as the interop check against the Go CLI, and 35 tests.No existing Go files are touched. The commits are separable: the first is the C bindings alone, if the Swift package is more than this repo wants to carry.
Design notes worth a look
shutdown(SHUT_WR)from C close both directions. Here each direction's EOF propagates as a half-close (CloseWrite/CloseRead) and teardown waits for both, liketailcat.ProxyConns, so a netcat-style client can send, half-close, and still read the reply.SO_NOSIGPIPEis set on every socketpair end on Darwin, and every descriptor is close-on-exec (viaForkLock+CloseOnExec, since Darwin has noSOCK_CLOEXEC), so a C or Swift thread writing to a dead peer cannot kill the process and spawned children do not inherit tunnels.tailcat_client_newsopublic_keynever blocks behind an in-flight first dial (which holdsstartMu).cgo, soCGO_ENABLED=0 go build ./...skips the package instead of failing.Testing
go test ./libtailcat/ -race: an offline end-to-end test overtstest/integration.RunDERPAndSTUNexercising the whole surface through the exported functions and real descriptors: allow-list rejection then acceptance, ping, dial, accept,conn_info, half-close in both directions, catch-all listener, RST for an unregistered port, close-on-exec, key and token helpers (including the README token), and that no handles, listeners or connections leak after close.go test ./...,go vet ./...,go mod tidy(no diff), and cross-builds for linux/windows with cgo off and js/wasm./weball pass.cd libtailcat && make xcframeworkproduces the three slices;swift build -c releaseandswift build --build-testswith-warnings-as-errorsare clean;swift testpasses offline, andTAILCAT_E2E=1 swift testadds a server/client round trip over the public relays.xcodebuildforgeneric/platform=iOSand the simulator both succeed.Known limitations and possible follow-ups
tailcat_server_startreturns whenServer.Startdoes, before the relay connection is up, and there is no exported readiness signal (the package's own tests use an unexported hook on the health tracker). A client's very first ping can therefore time out; the demo and tests retry. A small exported wait, something likeServer.WaitForDERP(ctx), would letstartblock until reachable. Happy to send that separately if wanted.ListenerandConnectionallow oneaccept/receivein flight at a time.binaryTargetURL would make the package a plainPackage.swiftdependency for apps.Written with Claude Code; I reviewed the design and ran the verification above on an Apple silicon Mac (Go 1.27, Xcode 26.6).
🤖 Generated with Claude Code