pkg/tailscale Package
About 1568 wordsAbout 5 min
2025-01-27
The pkg/tailscale package provides a Tailscale-network-only HTTP server that is a true drop-in replacement for Go's standard http.Server. This creates HTTP servers that are only accessible via the Tailscale network (tailnet), providing automatic security, TLS certificates, and identity resolution.
Go Documentation: pkg.go.dev/github.com/spechtlabs/tka/pkg/tailscale
Overview
This package provides a clean, modern interface for Tailscale HTTP servers with multiple usage patterns:
- Network Isolation: HTTP server only accessible via Tailscale network
- Automatic TLS: HTTPS certificates handled by Tailscale
- Identity Resolution: Built-in user identity and capability checking
- Funnel Detection: Ability to detect and reject public Funnel traffic
- Drop-in Replacement: True drop-in replacement for
http.Server - Flexible Interface: Multiple usage patterns from simple to advanced
Architecture
Key Design:
tailscale.Serverembedshttp.Serverdirectly - it IS anhttp.Servertailscale.Serverconnects to Tailscale network viatsnettailscale.Serverprovides standardnet.Listenerinstances from Tailscale- No abstraction layers - direct integration with standard Go HTTP patterns
Usage Patterns
The package supports two usage patterns to match different complexity needs:
1. High-Level Usage (All-in-One)
Use the Serve() method for a complete solution:
server := tailscale.NewServer("myapp",
tailscale.WithPort(443),
tailscale.WithDebug(true),
)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
info, err := server.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Hello, %s!", info.LoginName)
})
if err := server.Serve(ctx, handler); err != nil {
log.Fatal(err)
}Tips
See the Example: High-Level Usage (All-in-One) for more info
2. Low-Level Usage (Maximum Control)
Use Start() + ListenTCP() with standard http.Server for maximum control:
server := tailscale.NewServer("myapp")
if err := server.Start(ctx); err != nil {
log.Fatal(err)
}
listener, err := server.ListenTCP(":8080")
if err != nil {
log.Fatal(err)
}
// Use any http.Server - server is just for connection setup
httpServer := &http.Server{
Handler: myHandler,
ReadTimeout: 30 * time.Second,
}
go func() {
if err := httpServer.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
log.Printf("HTTP server error: %v", err)
}
}()
// Graceful shutdown
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}
if err := server.Stop(shutdownCtx); err != nil {
log.Printf("Tailscale shutdown error: %v", err)
}Tips
See the Example: Low-Level Usage (Maximum Control) for more info
Core Types
Server
The main server type that embeds http.Server and adds Tailscale functionality:
type Server struct {
*http.Server // Embedded - IS an http.Server
// Configuration
debug bool
port int
hostname string
// Tailscale components
ts *tsnet.Server
lc *local.Client
st *ipnstate.Status
serverURL string
started bool
}WhoIsInfo
Identity information extracted from Tailscale WhoIs lookups:
type WhoIsInfo struct {
LoginName string // User's login name (e.g., "[email protected]")
CapMap tailcfg.PeerCapMap // Capability grants from ACL
IsTagged bool // Whether the source is a tagged device
}WhoIsResolver
Interface for identity resolution:
type WhoIsResolver interface {
WhoIs(ctx context.Context, remoteAddr string) (*WhoIsInfo, humane.Error)
}This interface is implemented by the tailscale.Server and can be mocked for testing with the pkg/tailscale/mock package.
API Reference
Constructor
NewServer
func NewServer(hostname string, opts ...Option) *ServerCreates a new Tailscale HTTP server with the specified hostname.
Parameters:
hostname: The hostname for this server on the tailnet (e.g., "tka")opts: Configuration options
Returns: A configured *Server ready to serve HTTP traffic
Example:
server := tailscale.NewServer("myapp",
tailscale.WithPort(443),
tailscale.WithDebug(true),
tailscale.WithStateDir("/var/lib/myapp/ts-state"),
)Configuration Options
WithPort
func WithPort(port int) OptionSets the listening port. Default is 443 (HTTPS).
WithDebug
func WithDebug(debug bool) OptionEnables debug logging for Tailscale operations.
WithStateDir
func WithStateDir(dir string) OptionSets the directory for Tailscale state storage. If empty, uses automatic directory selection.
HTTP Timeout Options
Configure standard HTTP server timeouts:
func WithReadTimeout(timeout time.Duration) Option
func WithReadHeaderTimeout(timeout time.Duration) Option
func WithWriteTimeout(timeout time.Duration) Option
func WithIdleTimeout(timeout time.Duration) OptionServer Methods
Start
func (s *Server) Start(ctx context.Context) humane.ErrorConnects to the Tailscale network and prepares the server for accepting connections. This method separates connection setup from serving.
Example:
server := tailscale.NewServer("myapp")
if err := server.Start(ctx); err != nil {
log.Fatal(err.Display())
}ListenTCP
func (s *Server) ListenTCP(address string) (net.Listener, humane.Error)Creates a TCP listener on the Tailscale network. Returns a standard net.Listener that can be used with any http.Server.
Note: The server must be started with Start() before calling this method.
Example:
listener, err := server.ListenTCP(":8080")
if err != nil {
log.Fatal(err.Display())
}
httpServer := &http.Server{
Handler: myHandler,
ReadTimeout: 30 * time.Second,
}
go httpServer.Serve(listener)Stop
func (s *Server) Stop(ctx context.Context) humane.ErrorGracefully stops the Tailscale server.
Example:
if err := server.Stop(ctx); err != nil {
log.Printf("Stop error: %v", err)
}Serve
func (s *Server) Serve(ctx context.Context, handler http.Handler) humane.ErrorStarts the server with the provided HTTP handler. This is the high-level method that handles everything automatically.
Parameters:
ctx: Context for server lifecyclehandler: HTTP handler to serve requests
Returns: humane.Error if startup or serving fails
Example:
ctx := context.Background()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from tailnet!")
})
if err := server.Serve(ctx, handler); err != nil {
log.Fatal("Server failed:", err.Display())
}ListenAndServe
func (s *Server) ListenAndServe() errorStandard library compatible method. Uses s.Handler and background context.
Example:
server.Handler = myHandler
if err := server.ListenAndServe(); err != nil {
log.Fatal("Server failed:", err)
}Shutdown
func (s *Server) Shutdown(ctx context.Context) humane.ErrorGracefully shuts down the server.
Example:
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("Shutdown error: %v", err)
}WhoIs
func (s *Server) WhoIs(ctx context.Context, remoteAddr string) (*WhoIsInfo, error)Directly resolves identity for a remote address.
Example:
info, err := server.WhoIs(ctx, "100.64.1.2:12345")
if err != nil {
return fmt.Errorf("identity lookup failed: %w", err)
}
// Check capabilities
if caps, ok := info.CapMap["example.com/cap/admin"]; ok {
// User has admin capabilities
}Utility Functions
IsFunnelRequest
func IsFunnelRequest(r *http.Request) boolDetects if an HTTP request is coming through Tailscale Funnel (public internet).
Example:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if tailscale.IsFunnelRequest(r) {
http.Error(w, "Access denied: Funnel requests not allowed", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}Tips
See the Example: Authentication Middleware for more info
CtxConnKey
type CtxConnKey struct{}Context key for retrieving the underlying net.Conn from request context.
Examples
Example: High-Level Usage (All-in-One)
package main
import (
"context"
"fmt"
"net/http"
"github.com/spechtlabs/tka/pkg/tshttp"
)
func main() {
// Create server
server := tailscale.NewServer("myapp",
tailscale.WithPort(443),
tailscale.WithDebug(true),
)
// Simple handler
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from %s!", r.Host)
})
// Start server
ctx := context.Background()
if err := server.Serve(ctx, handler); err != nil {
panic(err.Display())
}
}Example: Low-Level Usage (Maximum Control)
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/spechtlabs/tka/pkg/tshttp"
)
func main() {
// Create server
server := tailscale.NewServer("myapp")
// Start with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Start(ctx); err != nil {
log.Fatal(err.Display())
}
// Create HTTP server
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
info, err := server.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
fmt.Printf("Request from %s: %s %s\n", info.LoginName, r.Method, r.URL.Path)
fmt.Fprintf(w, "Hello %s from Tailscale!", info.LoginName)
})
// Listen on Tailscale network
listener, err := server.ListenTCP(":8080")
if err != nil {
log.Fatal(err.Display())
}
// Standard HTTP server
httpServer := &http.Server{Handler: mux}
go func() {
if err := httpServer.Serve(listener); !errors.Is(err, http.ErrServerClosed) {
log.Printf("HTTP server error: %v", err)
}
}()
log.Printf("HTTP server listening on Tailscale network port 8080")
// Handle shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}
if err := server.Stop(shutdownCtx); err != nil {
log.Printf("Tailscale shutdown error: %v", err)
}
}Example: Authentication Middleware
func authMiddleware(server *tailscale.Server) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Reject Funnel requests
if tailscale.IsFunnelRequest(r) {
http.Error(w, "Access denied", http.StatusForbidden)
return
}
// Get user identity
info, err := server.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
http.Error(w, "Authentication failed", http.StatusUnauthorized)
return
}
// Check for required capability
if _, ok := info.CapMap["example.com/cap/api-access"]; !ok {
http.Error(w, "Insufficient permissions", http.StatusForbidden)
return
}
// Add user info to context
ctx := context.WithValue(r.Context(), "user", info.LoginName)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}Tips
To see a Gin-Authentication Middleware in action, check out the tka source code on GitHub
Security Considerations
- Tailnet Only: Server is never exposed to internet directly, only via your tailnet
- Device Authentication: All clients must be authenticated members of your tailnet
- Network ACLs: Tailscale ACLs control which devices can reach the server
- Public Traffic: Your server can still receive unauthenticated traffic from the public internet through Tailscale Funnel
- Public traffic can easily identified by using the
IsFunnelRequest()function - Applications should reject Funnel requests for sensitive operations
- Public traffic can easily identified by using the
Dependencies
- tailscale.com/tsnet Embedded Tailscale networking
- tailscale.com/client/local Local Tailscale client for WhoIs
- tailscale.com/ipn/ipnstate Status and state management
- github.com/sierrasoftworks/humane-errors-go Human-friendly error handling
Related Documentation
- TKA Architecture - How this fits into TKA
- Security Model - Security implications
- Production Deployment - Production usage patterns
