tailscale

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: MIT Imports: 23 Imported by: 0

README

wgo-tailscale

wgo-tailscale connects an existing wgo device to a Tailscale compatible control plane. It is a controller library, not a VPN application: the host owns the wgo device, its TUN, its private node key, and every operating-system setting.

[!WARNING] This project is experimental. APIs and behavior can change without notice. Do not use it for production systems without your own review and tests.

It was reviewed against Tailscale capability version 145 but advertises capability version 119. That is the last version before peer-hosted UDP relay semantics, which this basic client intentionally leaves to standard DERP fallback.

Implemented features:

  • ts2021 Noise control authentication, registration, and streaming map updates;
  • direct UDP, STUN endpoint discovery, authenticated DISCO probing, and DERP over TLS fallback with latency-aware home-region selection;
  • publication of complete peer specifications through wgo's multi-controller UpsertPeer/DeletePeer API and a named transport, with an option to use wgo's default transport for direct UDP peer endpoints;
  • optional local peer confirmation and optional AmneziaWG configuration;
  • UI-neutral authentication interactions and change subscriptions;
  • versioned callback-based cache state;
  • immutable client, node, peer, DERP, MagicDNS, ACL, and desired-network views;
  • in-memory MagicDNS lookup without system resolver integration.

The library does not create a TUN, create or close the wgo device, change the wgo node key, install routes or addresses, change system DNS, or administer a tailnet.

Every control-plane, DNS, STUN, DISCO, and DERP operation requires a gonnect.Network passed to tailscale.New. Direct peer traffic also uses that network by default. Set Options.UseDefaultTransportForDirectPeers when wgo's default transport already sends packets through the wanted network.

Minimal controller setup

network := (&gonnect.NativeConfig{}).Build()

// dev is an existing *device.Device. The host has already assigned its one
// WireGuard private key. The same dev may be used by other controllers.
client, err := tailscale.New(network, dev, tailscale.Options{
    Hostname:    "my-vpn-node",
    ControlURL: tailscale.DefaultControlURL,
    TLSConfig:  &tls.Config{MinVersion: tls.VersionTLS12},
    ConfirmPeers: true,
})
if err != nil {
    return err
}
if err := client.Start(ctx); err != nil {
    return err
}
defer client.Close()

Start returns after attaching the named wgo transport. If registration needs a person, Snapshot().Interaction contains an authorization URL while the client continues running and remains cancellable.

See the application usage guide for shared-device ownership, interactions, DNS, ACLs, cache handling, and desired network configuration. See ARCHITECTURE.md for protocol and component design. The basic-scope conformance matrix maps each requested capability to its API and tests.

Status

The requested basic controller scope is implemented. Unit and integration checks run with:

go test ./...
go test -race ./...
go vet ./...

The Headscale Docker scenario and optional hosted-service test are under tests/e2e; run the container cycle with ./tests/e2e/run.sh (it skips if Docker is unavailable). Protocol extensions such as Tailscale file transfer, SSH, Funnel, exit-node policy UI, Tailnet Lock key rotation, and administration APIs are intentionally outside this library.

License and attribution

The project is MIT licensed. Small independent implementations of Tailscale's Noise, DISCO, STUN, DERP, and netcheck behavior retain Tailscale's BSD-3-Clause copyright and attribution headers in their source files. No Tailscale Go package is imported.

Documentation

Overview

Package tailscale connects an existing wgo device to a Tailscale-compatible control plane without taking ownership of the device, TUN, or operating system network configuration.

Every control, DNS, STUN, DISCO, and DERP socket or lookup made by the package is performed through the gonnect.Network supplied to New. Direct peer traffic uses that network by default, but Options can leave direct endpoints on wgo's default transport. The client reads the wgo device's existing private key and never generates, replaces, or rotates it.

Index

Constants

View Source
const (
	// DefaultControlURL is the hosted Tailscale coordination service.
	DefaultControlURL = "https://controlplane.tailscale.com"
	// DefaultTransportID is the named wgo transport owned by a Client.
	DefaultTransportID device.TransportID = "tailscale"
)

Variables

View Source
var (
	ErrAlreadyStarted         = errors.New("tailscale: client already started")
	ErrNotStarted             = errors.New("tailscale: client not started")
	ErrClosed                 = errors.New("tailscale: client closed")
	ErrZeroNodeKey            = errors.New("tailscale: wgo device has no private node key")
	ErrNodeIdentityChanged    = errors.New("tailscale: cached node identity does not match the wgo device")
	ErrControlNodeKeyMismatch = errors.New("tailscale: control returned a self node key that does not match the wgo device")
	ErrNodeKeyExpired         = errors.New("tailscale: control requires node-key rotation, but the wgo device key is immutable to this client")
	ErrPeerNotFound           = errors.New("tailscale: peer not found")
	ErrPeerConflict           = errors.New("tailscale: peer public key is already owned by another wgo controller")
	ErrInteractionNotFound    = errors.New("tailscale: interaction not found")
)

Functions

This section is empty.

Types

type ACLDestination

type ACLDestination struct {
	IP    string
	Bits  *int
	Ports PortRange
}

type ACLRule

type ACLRule struct {
	SourceIPs        []string
	SourceBits       []int
	Destinations     []ACLDestination
	IPProtocols      []int
	CapabilityGrants []json.RawMessage
}

type ACLView

type ACLView struct {
	// Rules is the deterministic, name-sorted flattening used by ACLAllows.
	Rules []ACLRule
	// NamedRules preserves control's PacketFilters chunks. The legacy
	// PacketFilter field is represented by the "base" chunk.
	NamedRules map[string][]ACLRule
	Revision   uint64
}

ACLView is the latest packet filter delivered by control. It is descriptive; this library does not install or enforce OS firewall rules.

func (ACLView) FirewallConfig

func (view ACLView) FirewallConfig() *gonnect.FirewallConfig

FirewallConfig converts view to a gonnect incoming firewall policy. Capability grants have no FirewallConfig equivalent and are not included.

type CacheCallbacks

type CacheCallbacks struct {
	Load  func(context.Context) ([]byte, error)
	Store func(context.Context, []byte) error
}

CacheCallbacks persist private machine/discovery identity and peer confirmations. The blob is versioned JSON owned by this package. Callbacks must treat it as sensitive data and replace it atomically. Calls are serialized; a Store callback may inspect the client but must not re-enter a confirmation method.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client coordinates one Tailscale control-plane identity with one named transport and a set of peers on an existing wgo device. It never changes the device private key and never owns the device lifecycle.

func New

func New(network gonnect.Network, dev WGODevice, options Options) (*Client, error)

New constructs a client. Network is mandatory for control, DNS, STUN, DISCO, and DERP. Direct peer traffic also uses it unless Options selects wgo's default transport for direct peers.

func (*Client) ACL

func (c *Client) ACL() ACLView

ACL returns the current read-only packet-filter rules.

func (*Client) ACLAllows

func (c *Client) ACLAllows(source, destination netip.Addr, protocol int, port uint16) bool

ACLAllows reports whether the latest read-only packet filter contains a matching rule. It is an application helper, not an enforcement mechanism.

func (*Client) ACLFirewallConfig

func (c *Client) ACLFirewallConfig() *gonnect.FirewallConfig

ACLFirewallConfig converts the latest packet filter to a gonnect incoming firewall policy. The returned config is independent of the client state.

The config does not restrict outgoing traffic. Tailscale packet filters are allow lists, but gonnect FirewallConfig.Exclude is an outgoing deny list.

func (*Client) Close

func (c *Client) Close() error

Close stops only resources and peers owned by this client. It does not stop the shared wgo device or alter peers owned by other controllers.

func (*Client) ConfirmPeer

func (c *Client) ConfirmPeer(ctx context.Context, id string) error

ConfirmPeer permits one control-provided peer to be published to wgo when confirmation mode is enabled. The ID is PeerInfo.PeerID, normally the control server's stable node ID.

func (*Client) CurrentInteraction

func (c *Client) CurrentInteraction() *Interaction

CurrentInteraction returns a copy of the active UI-neutral interaction.

func (*Client) DERP

func (c *Client) DERP() DERPView

DERP returns the current relay directory and selected home region.

func (*Client) DNS

func (c *Client) DNS() DNSView

DNS returns the current read-only MagicDNS data.

func (*Client) DesiredNetworkConfiguration

func (c *Client) DesiredNetworkConfiguration() NetworkConfiguration

DesiredNetworkConfiguration returns desired interface/address/route state; it never performs operating-system configuration.

func (*Client) Done

func (c *Client) Done() <-chan struct{}

Done is closed after client resources have been fully released.

func (*Client) Info

func (c *Client) Info() ClientInfo

Info returns current public client identity and timestamps.

func (*Client) Peer

func (c *Client) Peer(id string) (PeerInfo, bool)

Peer finds a peer by stable PeerID.

func (*Client) Peers

func (c *Client) Peers() []PeerInfo

Peers returns all peers, including peers awaiting confirmation.

func (*Client) Resolver

func (c *Client) Resolver() *MagicDNSResolver

Resolver returns a live resolver view backed by Client snapshots.

func (*Client) ResumeInteraction

func (c *Client) ResumeInteraction(id uint64) error

ResumeInteraction nudges pending authentication immediately after an application has opened its URL or completed an out-of-band admin action.

func (*Client) RevokePeerConfirmation

func (c *Client) RevokePeerConfirmation(ctx context.Context, id string) error

RevokePeerConfirmation removes a prior confirmation and withdraws that peer from wgo. It is useful when application policy changes locally.

func (*Client) Snapshot

func (c *Client) Snapshot() Snapshot

Snapshot returns a coherent deep copy. Callers may retain and mutate it.

func (*Client) Start

func (c *Client) Start(parent context.Context) error

Start installs the client's named transport and starts asynchronous control synchronization. Authentication that needs a person is surfaced through Interaction and events; Start itself does not wait for that person.

func (*Client) State

func (c *Client) State() State

State returns the current lifecycle state.

func (*Client) Subscribe

func (c *Client) Subscribe(buffer int) (<-chan Event, func())

Subscribe returns coalescable change notifications. Snapshot is the authoritative view to read after each event.

func (*Client) Wait

func (c *Client) Wait() error

Wait waits for Close, including automatic Close when the Start context ends.

type ClientInfo

type ClientInfo struct {
	ControlURL        string
	Hostname          string
	NodePublicKey     device.NoisePublicKey
	MachinePublicKey  string
	DiscoPublicKey    string
	BackendLogID      string
	TransportID       device.TransportID
	StartedAt         time.Time
	AuthenticatedAt   time.Time
	UserID            int64
	LoginName         string
	DisplayName       string
	ProfilePicURL     string
	Ephemeral         bool
	PeerConfirmation  bool
	CapabilityVersion int
	MachineAuthorized bool
	MapSessionHandle  string
	MapSequence       int64
	PreferredDERP     int64
}

ClientInfo describes this client identity. Private keys are deliberately not exposed; the node private key remains solely on the wgo device.

type ConfirmationState

type ConfirmationState string

ConfirmationState reports whether a peer may be published to wgo.

const (
	PeerConfirmationNotRequired ConfirmationState = "not-required"
	PeerAwaitingConfirmation    ConfirmationState = "awaiting-confirmation"
	PeerConfirmed               ConfirmationState = "confirmed"
)

type DERPLatencySource

type DERPLatencySource string

DERPLatencySource identifies how a DERP-region RTT was measured.

const (
	DERPLatencySTUN  DERPLatencySource = "stun-udp"
	DERPLatencyHTTPS DERPLatencySource = "https"
)

type DERPNode

type DERPNode struct {
	Name             string
	RegionID         int64
	HostName         string
	CertName         string
	IPv4             string
	IPv6             string
	STUNPort         int
	DERPPort         int
	STUNOnly         bool
	InsecureForTests bool
	STUNTestIP       string
}

DERPNode and DERPRegion are public, read-only views of the relay map.

type DERPRegion

type DERPRegion struct {
	ID                int64
	Code              string
	Name              string
	Latitude          float64
	Longitude         float64
	NoMeasureNoHome   bool
	Latency           time.Duration
	LatencySource     DERPLatencySource
	LatencyMeasuredAt time.Time
	Nodes             []DERPNode
}

type DERPView

type DERPView struct {
	Regions  []DERPRegion
	Home     int64
	Revision uint64
}

type DNSRecord

type DNSRecord struct {
	Name  string
	Type  string
	Value string
}

DNSRecord is a control-provided MagicDNS record.

type DNSView

type DNSView struct {
	Proxied       bool
	SearchDomains []string
	CertDomains   []string
	Nameservers   []netip.Addr
	Resolvers     []json.RawMessage
	Routes        map[string][]json.RawMessage
	Records       []DNSRecord
	Revision      uint64
}

DNSView is an immutable snapshot used by MagicDNSResolver.

type Event

type Event struct {
	Kind     EventKind
	Revision uint64
	At       time.Time
	Err      error
}

Event is a coalescable notification. Call Snapshot after receiving it to obtain one consistent, immutable view of all client state.

type EventKind

type EventKind string

EventKind identifies a mutable part of the client view.

const (
	EventState       EventKind = "state"
	EventInteraction EventKind = "interaction"
	EventSelf        EventKind = "self"
	EventPeers       EventKind = "peers"
	EventPeerPath    EventKind = "peer-path"
	EventDNS         EventKind = "dns"
	EventACL         EventKind = "acl"
	EventNetwork     EventKind = "network"
	EventDERP        EventKind = "derp"
	EventUsers       EventKind = "users"
	EventMetadata    EventKind = "metadata"
	EventError       EventKind = "error"
)

type Interaction

type Interaction struct {
	ID      uint64
	Kind    InteractionKind
	URL     string
	Message string
	Since   time.Time
}

Interaction is a UI-neutral request. ResumeInteraction either expedites the next authentication attempt or acknowledges a one-shot control URL. The client continues independently, so no application goroutine waits on a user.

type InteractionKind

type InteractionKind string

InteractionKind identifies an action that application UI may present.

const (
	InteractionAuthenticate   InteractionKind = "authenticate"
	InteractionNodeKeyExpired InteractionKind = "node-key-expired"
	InteractionControlURL     InteractionKind = "control-url"
)

type LocalEndpoint

type LocalEndpoint struct {
	Address netip.AddrPort
	Source  string
}

LocalEndpoint is an address currently advertised to control and how it was learned. Source is one of "local", "stun", "port-mapped", or "unknown".

type MagicDNSResolver

type MagicDNSResolver struct {
	// contains filtered or unexported fields
}

MagicDNSResolver resolves only the control-provided in-memory DNS view. It never changes system resolver settings and never sends a DNS packet.

func (*MagicDNSResolver) LookupAddr

func (r *MagicDNSResolver) LookupAddr(ctx context.Context, address string) ([]string, error)

LookupAddr performs an in-memory reverse lookup over node A/AAAA records.

func (*MagicDNSResolver) LookupHost

func (r *MagicDNSResolver) LookupHost(ctx context.Context, host string) ([]string, error)

LookupHost returns textual addresses for a MagicDNS name.

func (*MagicDNSResolver) LookupNetIP

func (r *MagicDNSResolver) LookupNetIP(ctx context.Context, network, host string) ([]netip.Addr, error)

LookupNetIP implements the shape used by net.Resolver and gonnect resolvers.

type NetworkConfiguration

type NetworkConfiguration struct {
	InterfaceName string
	Up            bool
	MTU           int
	Addresses     []netip.Prefix
	Routes        []Route
	DNS           DNSView
	Revision      uint64
}

NetworkConfiguration is desired state for the host application. No entry is installed by this package.

type NodeInfo

type NodeInfo struct {
	ID                            int64
	StableID                      string
	Name                          string
	UserID                        int64
	SharerID                      int64
	PublicKey                     device.NoisePublicKey
	MachinePublicKey              string
	DiscoPublicKey                string
	Addresses                     []netip.Prefix
	AllowedIPs                    []netip.Prefix
	Endpoints                     []netip.AddrPort
	HomeDERP                      int64
	LegacyDERPString              string
	CapabilityVersion             int
	KeySignature                  []byte
	KeyExpiry                     time.Time
	Created                       time.Time
	LastSeen                      *time.Time
	Online                        *bool
	MachineAuthorized             bool
	Tags                          []string
	PrimaryRoutes                 []netip.Prefix
	Capabilities                  []string
	CapabilityMap                 map[string][]json.RawMessage
	UnsignedPeerAPIOnly           bool
	ComputedName                  string
	ComputedNameWithHost          string
	DataPlaneAuditLogID           string
	Expired                       bool
	SelfNodeV4MasqAddrForThisPeer *netip.Addr
	SelfNodeV6MasqAddrForThisPeer *netip.Addr
	IsWireGuardOnly               bool
	IsJailed                      bool
	ExitNodeDNSResolvers          []json.RawMessage
	HostinfoJSON                  json.RawMessage
	RawJSON                       json.RawMessage
}

NodeInfo is a read-only copy of control-plane node data. RawJSON preserves fields this library version does not yet understand.

type Options

type Options struct {
	// ControlURL is a Tailscale-compatible coordination server URL.
	ControlURL string
	// Hostname is the node name advertised to control. It is required.
	Hostname string
	// AuthKey is an optional reusable or one-shot pre-authentication key.
	AuthKey string
	// Ephemeral asks control to remove the node after it goes offline.
	Ephemeral bool

	// ConfirmPeers holds newly observed peers outside wgo until ConfirmPeer is
	// called. Confirmations are stable-ID based and can be cached.
	ConfirmPeers bool
	// Obfuscation is copied onto every wgo peer owned by this client. All peers
	// must have a compatible AmneziaWG configuration.
	Obfuscation *device.AmneziaWGConfig

	// TransportID names the wgo transport owned by this client. It must not be
	// the empty default-UDP transport ID.
	TransportID device.TransportID
	// UseDefaultTransportForDirectPeers uses wgo's default transport for direct
	// UDP peer endpoints. DERP traffic still uses this client's named transport.
	UseDefaultTransportForDirectPeers bool
	// ListenPort requests a local UDP port. Zero asks the network for one.
	ListenPort uint16
	// InterfaceName and MTU describe the desired interface in
	// NetworkConfiguration. They are never applied to the operating system.
	InterfaceName string
	MTU           int

	// DisableDERP disables the TLS DERP fallback. Direct UDP remains enabled.
	DisableDERP bool
	// DisableDiscovery disables local endpoint advertisement, STUN, and Disco.
	// Control-provided direct endpoints remain available; WireGuard-only peers
	// use them as their primary path and other peers use them if DERP is
	// unavailable.
	DisableDiscovery bool

	// TLSConfig configures all outgoing TLS connections. It is required and is
	// cloned before use.
	TLSConfig *tls.Config
	// Cache is optional. Without it a new machine/discovery identity is made on
	// each process run, while the wgo node identity remains unchanged.
	Cache CacheCallbacks
	// Logger receives diagnostic messages. The default discards logs.
	Logger *slog.Logger

	// AuthenticationPollInterval controls follow-up registration while user
	// authorization is pending.
	AuthenticationPollInterval time.Duration
	// ReconnectMin and ReconnectMax bound map-stream retry backoff.
	ReconnectMin time.Duration
	ReconnectMax time.Duration
}

Options configures a Client. Zero values select conservative defaults.

type PathKind

type PathKind string

PathKind is the path currently preferred for a peer.

const (
	PathNone   PathKind = "none"
	PathDirect PathKind = "direct-udp"
	PathDERP   PathKind = "derp-tls"
)

type PeerInfo

type PeerInfo struct {
	Node         NodeInfo
	PeerID       string
	Confirmation ConfirmationState
	AppliedToWGO bool
	Path         PathKind
	Direct       netip.AddrPort
	PathLatency  time.Duration
	PathUpdated  time.Time
	LastError    string
}

PeerInfo combines a control-plane node with local confirmation, publication, and path state.

type PortRange

type PortRange struct {
	First uint16
	Last  uint16
}

PortRange and ACLRule retain the control server's read-only packet filter.

type Route

type Route struct {
	Prefix        netip.Prefix
	PeerID        string
	PeerPublicKey device.NoisePublicKey
	Primary       bool
}

Route describes one desired route and its owning Tailscale peer.

type Snapshot

type Snapshot struct {
	Revision       uint64
	At             time.Time
	State          State
	LastError      string
	Interaction    *Interaction
	Client         ClientInfo
	Self           *NodeInfo
	Peers          []PeerInfo
	Users          []UserProfile
	DNS            DNSView
	ACL            ACLView
	Network        NetworkConfiguration
	DERP           DERPView
	Domain         string
	Health         []string
	LocalEndpoints []LocalEndpoint
	ControlTime    *time.Time
}

Snapshot is a coherent deep copy of all mutable client information.

type State

type State string

State is the client's lifecycle/control-plane state.

const (
	StateNew                 State = "new"
	StateStarting            State = "starting"
	StateNeedsAuthentication State = "needs-authentication"
	StateRunning             State = "running"
	StateDegraded            State = "degraded"
	StateStopping            State = "stopping"
	StateStopped             State = "stopped"
)

type UserProfile

type UserProfile struct {
	ID            int64
	LoginName     string
	DisplayName   string
	ProfilePicURL string
	Groups        []string
}

UserProfile is display data delivered with network maps.

type WGODevice

type WGODevice interface {
	PrivateKey() device.NoisePrivateKey
	UpsertPeer(device.PeerSpec) error
	DeletePeer(device.NoisePublicKey) (bool, error)
	PeerSpec(device.NoisePublicKey) (device.PeerSpec, bool)
	AddTransport(device.TransportID, device.TransportConfig) error
	RemoveTransport(device.TransportID) error
}

WGODevice is the portion of a master-branch wgo device used by Client. A *device.Device implements this interface. The narrow interface also makes it possible to test control-plane behavior without creating a TUN.

Directories

Path Synopsis
cmd
internal
tests
e2e/cmd/node command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL