Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@
# Go test artifacts.
*.test
*.out

# libtailcat / TailcatKit build outputs.
/libtailcat/build/
/swift/CTailcat.xcframework
/swift/.build
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -652,3 +652,12 @@ Go module client of the tailscale.com repo instead of a fork of it.

It was open sourced August 2026 at the
[TailscaleUp conference](https://tailscale.com/tailscaleup).

## Swift / C bindings

[`libtailcat/`](./libtailcat/) exports a small C API over the Go library,
built as static archives for macOS, iOS and the iOS simulator and packaged
as `CTailcat.xcframework`, and [`swift/`](./swift/) wraps it as the
`TailcatKit` Swift package (Swift 6, async/await servers, clients and
connections) with a demo tool and tests. See their READMEs to build and
use them.
75 changes: 75 additions & 0 deletions libtailcat/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright (c) Tailscale Inc & contributors
# SPDX-License-Identifier: BSD-3-Clause

# Builds libtailcat as Go c-archives for macOS (arm64 + x86_64), iOS
# devices (arm64) and the iOS simulator (arm64 + x86_64), and packages
# them with include/ as ../swift/CTailcat.xcframework.
#
# Every archive is built with the release build tags from
# ../build-tags.txt, which trim the tailscale.com dependency down to
# what tailcat needs.

GO ?= go
TAGS := $(shell cat ../build-tags.txt)
BUILD := build
SCRIPT := $(abspath script)
XCFRAMEWORK := ../swift/CTailcat.xcframework

# `?=` so the environment can override them.
MACOS_TARGET ?= 14.0

export CGO_ENABLED = 1
export MACOS_TARGET

.PHONY: all
all: xcframework ## Builds everything (the default)

$(BUILD):
mkdir -p $(BUILD)

# macOS: arm64 natively, x86_64 through the clang wrapper, then lipo.
.PHONY: macos
macos: $(BUILD) ## Builds the fat macOS archive build/libtailcat_macos.a
MACOSX_DEPLOYMENT_TARGET=$(MACOS_TARGET) GOOS=darwin GOARCH=arm64 \
CGO_CFLAGS="-mmacos-version-min=$(MACOS_TARGET)" CGO_LDFLAGS="-mmacos-version-min=$(MACOS_TARGET)" \
$(GO) build -buildmode=c-archive -tags "$(TAGS)" -ldflags "-s -w" -o $(BUILD)/libtailcat_macos_arm64.a .
MACOSX_DEPLOYMENT_TARGET=$(MACOS_TARGET) GOOS=darwin GOARCH=amd64 CC=$(SCRIPT)/clangwrap-macos-x86.sh \
$(GO) build -buildmode=c-archive -tags "$(TAGS)" -ldflags "-s -w" -o $(BUILD)/libtailcat_macos_x86_64.a .
lipo -create -output $(BUILD)/libtailcat_macos.a $(BUILD)/libtailcat_macos_arm64.a $(BUILD)/libtailcat_macos_x86_64.a

.PHONY: ios
ios: $(BUILD) ## Builds the iOS device archive build/libtailcat_ios.a (iOS SDK required)
GOOS=ios GOARCH=arm64 CC=$(SCRIPT)/clangwrap-ios.sh \
$(GO) build -buildmode=c-archive -tags "ios,$(TAGS)" -ldflags -w -o $(BUILD)/libtailcat_ios.a .

.PHONY: ios-sim
ios-sim: $(BUILD) ## Builds the fat iOS simulator archive build/libtailcat_iossim.a (iOS SDK required)
GOOS=ios GOARCH=arm64 CC=$(SCRIPT)/clangwrap-ios-sim-arm.sh \
$(GO) build -buildmode=c-archive -tags "ios,$(TAGS)" -ldflags -w -o $(BUILD)/libtailcat_iossim_arm64.a .
GOOS=ios GOARCH=amd64 CC=$(SCRIPT)/clangwrap-ios-sim-x86.sh \
$(GO) build -buildmode=c-archive -tags "ios,$(TAGS)" -ldflags -w -o $(BUILD)/libtailcat_iossim_x86_64.a .
lipo -create -output $(BUILD)/libtailcat_iossim.a $(BUILD)/libtailcat_iossim_arm64.a $(BUILD)/libtailcat_iossim_x86_64.a

.PHONY: xcframework
xcframework: macos ios ios-sim ## Builds ../swift/CTailcat.xcframework from the three archives
rm -rf $(XCFRAMEWORK)
mkdir -p $(dir $(XCFRAMEWORK))
xcodebuild -create-xcframework \
-library $(BUILD)/libtailcat_macos.a -headers include \
-library $(BUILD)/libtailcat_ios.a -headers include \
-library $(BUILD)/libtailcat_iossim.a -headers include \
-output $(XCFRAMEWORK)

.PHONY: test
test: ## Runs the offline Go tests of the exported functions
cd .. && $(GO) test ./libtailcat/ -run . -count=1

.PHONY: clean
clean: ## Removes build/ and the xcframework
rm -rf $(BUILD) $(XCFRAMEWORK)

.PHONY: help
help: ## Shows this help
@printf '\nSpecify a target. The choices are:\n\n'
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-12s %s\n", $$1, $$2}'
@printf '\n'
98 changes: 98 additions & 0 deletions libtailcat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# libtailcat

A C API over the [tailcat](../README.md) Go library, built with
`go build -buildmode=c-archive` for macOS, iOS and the iOS simulator and
packaged as `CTailcat.xcframework` for use from Swift (see `../swift/`,
the TailcatKit package) or plain C.

The API is in [`include/tailcat.h`](include/tailcat.h). The Go side is
`libtailcat.go`; `tailcat.c` maps each `tailcat_*` function to the Go
export of the same name.

## Building

Requires Go (see `../go.mod` for the version) and Xcode with the iOS SDK.

```sh
make xcframework # ../swift/CTailcat.xcframework (macOS, iOS, iOS simulator)
make macos # build/libtailcat_macos.a (arm64 + x86_64) only
make test # offline Go test of the exported functions
make clean
```

All archives are built with the release build tags from
`../build-tags.txt`. The minimum targets are macOS 14 and iOS 17
(`MACOS_TARGET` overrides the former; the iOS minimum is in
`script/clangwrap-*.sh`).

The cgo-generated `build/*.h` headers are build artifacts; only
`include/` ships in the xcframework. Linking the archive needs the
system frameworks the Go runtime uses on Darwin, typically
`CoreFoundation`, `Security` and `libresolv`.

## Using it from C

Every function is safe to call from any thread. Handle functions return
0 on success, `EBADF` for a bad handle, `ERANGE` for a too-small output
buffer and -1 for other errors, whose text `tailcat_errmsg` returns.
Blocking calls (server start, client ping, path, dial, address resolve)
do network work; keep them off UI threads.

`tailcat_server_start` returns once the server is configured and its
address is known; the relay connection completes in the background, and
pings resend until acknowledged, so a client's `tailcat_client_ping`
right after start succeeds within its timeout.

A server:

```c
#include <poll.h>
#include <unistd.h>
#include "tailcat.h"

tailcat_handle sd = tailcat_server_new();
tailcat_listener ln;
tailcat_server_listen(sd, 8080, &ln); // 0 = every port not otherwise listened on
if (tailcat_server_start(sd) != 0) { // blocks: DERP map, latency check
char err[256];
tailcat_errmsg(sd, err, sizeof err);
// ...
}
char addr[512];
tailcat_server_addr(sd, addr, sizeof addr); // the tailcat address; give this to clients

for (;;) {
struct pollfd pfd = {.fd = ln, .events = POLLIN};
poll(&pfd, 1, -1); // a connection is queued
tailcat_conn c;
if (tailcat_accept(ln, &c) != 0) break;
char remote[64];
int port;
tailcat_conn_info(ln, c, remote, sizeof remote, &port);
// c is a socket: read(2), write(2), shutdown(2) for half-close, close(2)
}
close(ln); // stops listening on the port
tailcat_server_close(sd);
```

A client:

```c
tailcat_handle cd = tailcat_client_new(addr); // 0 if the address is malformed
double ms;
tailcat_client_ping(cd, 10000, &ms); // blocks; brings the tunnel up
tailcat_conn c;
tailcat_client_dial(cd, 8080, 15000, &c); // TCP to the server's port 8080
write(c, "hello\n", 6);
shutdown(c, SHUT_WR); // half-close: the server sees EOF
// read(c, ...) until 0
close(c);
tailcat_client_close(cd);
```

Connections are one end of a socketpair pumped by Go, so they behave like
sockets; on Apple platforms they have `SO_NOSIGPIPE` set. Keys and
addresses can be handled without a handle: `tailcat_key_generate`,
`tailcat_key_public`, `tailcat_key_addr`, `tailcat_addr_parse` and
`tailcat_addr_resolve` return `NULL` or a malloc'd error string, and
their outputs are malloc'd too; `free()` all of them.
39 changes: 39 additions & 0 deletions libtailcat/cgo_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build unix && cgo

package main

// Go doesn't allow cgo in _test.go files, so the C types, constants and
// helpers that libtailcat_test.go needs to call the exported functions
// are given Go names here. Nothing else uses them.

/*
#include <errno.h>
#include <stdlib.h>
*/
import "C"

import "unsafe"

type (
cChar = C.char
cInt = C.int
cSize = C.size_t
cDouble = C.double
)

const (
cEBADF = C.EBADF
cERANGE = C.ERANGE
)

// cString returns s as a malloc'd C string; free it with cFree.
func cString(s string) *C.char { return C.CString(s) }

// cFree releases a malloc'd C string.
func cFree(p *C.char) { C.free(unsafe.Pointer(p)) }

// goString copies the C string p into a Go string.
func goString(p *C.char) string { return C.GoString(p) }
4 changes: 4 additions & 0 deletions libtailcat/include/module.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module CTailcat {
header "tailcat.h"
export *
}
Loading