mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-22 05:27:08 +00:00
cmd/tailscale,ipn: add Unix socket support for serve
Based on PR #16700 by @lox, adapted to current codebase. Adds support for proxying HTTP requests to Unix domain sockets via tailscale serve unix:/path/to/socket, enabling exposure of services like Docker, containerd, PHP-FPM over Tailscale without TCP bridging. The implementation includes reasonable protections against exposure of tailscaled's own socket. Adaptations from original PR: - Use net.Dialer.DialContext instead of net.Dial for context propagation - Use http.Transport with Protocols API (current h2c approach, not http2.Transport) - Resolve conflicts with hasScheme variable in ExpandProxyTargetValue Updates #9771 Signed-off-by: Peter A. <ink.splatters@pm.me> Co-authored-by: Lachlan Donald <lachlan@ljd.cc>
This commit is contained in:
committed by
Brad Fitzpatrick
parent
557457f3c2
commit
f4d34f38be
@@ -138,6 +138,7 @@ var serveHelpCommon = strings.TrimSpace(`
|
||||
<target> can be a file, directory, text, or most commonly the location to a service running on the
|
||||
local machine. The location to the location service can be expressed as a port number (e.g., 3000),
|
||||
a partial URL (e.g., localhost:3000), or a full URL including a path (e.g., http://localhost:3000/foo).
|
||||
On Unix-like systems, you can also specify a Unix domain socket (e.g., unix:/tmp/myservice.sock).
|
||||
|
||||
EXAMPLES
|
||||
- Expose an HTTP server running at 127.0.0.1:3000 in the foreground:
|
||||
@@ -149,6 +150,9 @@ EXAMPLES
|
||||
- Expose an HTTPS server with invalid or self-signed certificates at https://localhost:8443
|
||||
$ tailscale %[1]s https+insecure://localhost:8443
|
||||
|
||||
- Expose a service listening on a Unix socket (Linux/macOS/BSD only):
|
||||
$ tailscale %[1]s unix:/var/run/myservice.sock
|
||||
|
||||
For more examples and use cases visit our docs site https://tailscale.com/kb/1247/funnel-serve-use-cases
|
||||
`)
|
||||
|
||||
@@ -1172,7 +1176,8 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
|
||||
}
|
||||
h.Path = target
|
||||
default:
|
||||
t, err := ipn.ExpandProxyTargetValue(target, []string{"http", "https", "https+insecure"}, "http")
|
||||
// Include unix in supported schemes for HTTP(S) serve
|
||||
t, err := ipn.ExpandProxyTargetValue(target, []string{"http", "https", "https+insecure", "unix"}, "http")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
86
cmd/tailscale/cli/serve_v2_unix_test.go
Normal file
86
cmd/tailscale/cli/serve_v2_unix_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build unix
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
func TestServeUnixSocketCLI(t *testing.T) {
|
||||
// Create a temporary directory for our socket path
|
||||
tmpDir := t.TempDir()
|
||||
socketPath := filepath.Join(tmpDir, "test.sock")
|
||||
|
||||
// Test that Unix socket targets are accepted by ExpandProxyTargetValue
|
||||
target := "unix:" + socketPath
|
||||
result, err := ipn.ExpandProxyTargetValue(target, []string{"http", "https", "https+insecure", "unix"}, "http")
|
||||
if err != nil {
|
||||
t.Fatalf("ExpandProxyTargetValue failed: %v", err)
|
||||
}
|
||||
|
||||
if result != target {
|
||||
t.Errorf("ExpandProxyTargetValue(%q) = %q, want %q", target, result, target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeUnixSocketConfigPreserved(t *testing.T) {
|
||||
// Test that Unix socket URLs are preserved in ServeConfig
|
||||
sc := &ipn.ServeConfig{
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "unix:/tmp/test.sock"},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
// Verify the proxy value is preserved
|
||||
handler := sc.Web["foo.test.ts.net:443"].Handlers["/"]
|
||||
if handler.Proxy != "unix:/tmp/test.sock" {
|
||||
t.Errorf("proxy = %q, want %q", handler.Proxy, "unix:/tmp/test.sock")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeUnixSocketVariousPaths(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "absolute-path",
|
||||
target: "unix:/var/run/docker.sock",
|
||||
},
|
||||
{
|
||||
name: "tmp-path",
|
||||
target: "unix:/tmp/myservice.sock",
|
||||
},
|
||||
{
|
||||
name: "relative-path",
|
||||
target: "unix:./local.sock",
|
||||
},
|
||||
{
|
||||
name: "home-path",
|
||||
target: "unix:/home/user/.local/service.sock",
|
||||
},
|
||||
{
|
||||
name: "empty-path",
|
||||
target: "unix:",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ipn.ExpandProxyTargetValue(tt.target, []string{"http", "https", "unix"}, "http")
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ExpandProxyTargetValue(%q) error = %v, wantErr %v", tt.target, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -401,6 +401,7 @@ func run() (err error) {
|
||||
// Install an event bus as early as possible, so that it's
|
||||
// available universally when setting up everything else.
|
||||
sys := tsd.NewSystem()
|
||||
sys.SocketPath = args.socketpath
|
||||
|
||||
// Parse config, if specified, to fail early if it's invalid.
|
||||
var conf *conffile.Config
|
||||
|
||||
@@ -76,6 +76,10 @@ const (
|
||||
// current etag of a resource.
|
||||
var ErrETagMismatch = errors.New("etag mismatch")
|
||||
|
||||
// ErrProxyToTailscaledSocket is returned when attempting to proxy
|
||||
// to the tailscaled socket itself, which would create a loop.
|
||||
var ErrProxyToTailscaledSocket = errors.New("cannot proxy to tailscaled socket")
|
||||
|
||||
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
|
||||
|
||||
type serveHTTPContext struct {
|
||||
@@ -812,6 +816,27 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
|
||||
// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port).
|
||||
func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, error) {
|
||||
targetURL, insecure := expandProxyArg(backend)
|
||||
|
||||
// Handle unix: scheme specially
|
||||
if strings.HasPrefix(targetURL, "unix:") {
|
||||
socketPath := strings.TrimPrefix(targetURL, "unix:")
|
||||
if socketPath == "" {
|
||||
return nil, fmt.Errorf("empty unix socket path")
|
||||
}
|
||||
if b.isTailscaledSocket(socketPath) {
|
||||
return nil, ErrProxyToTailscaledSocket
|
||||
}
|
||||
u, _ := url.Parse("http://localhost")
|
||||
return &reverseProxy{
|
||||
logf: b.logf,
|
||||
url: u,
|
||||
insecure: false,
|
||||
backend: backend,
|
||||
lb: b,
|
||||
socketPath: socketPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(targetURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
||||
@@ -826,6 +851,22 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, err
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// isTailscaledSocket reports whether socketPath refers to the same file
|
||||
// as the tailscaled socket. It uses os.SameFile to handle symlinks,
|
||||
// bind mounts, and other path variations.
|
||||
func (b *LocalBackend) isTailscaledSocket(socketPath string) bool {
|
||||
tailscaledSocket := b.sys.SocketPath
|
||||
if tailscaledSocket == "" {
|
||||
return false
|
||||
}
|
||||
fi1, err1 := os.Stat(socketPath)
|
||||
fi2, err2 := os.Stat(tailscaledSocket)
|
||||
if err1 != nil || err2 != nil {
|
||||
return false
|
||||
}
|
||||
return os.SameFile(fi1, fi2)
|
||||
}
|
||||
|
||||
// reverseProxy is a proxy that forwards a request to a backend host
|
||||
// (preconfigured via ipn.ServeConfig). If the host is configured with
|
||||
// http+insecure prefix, connection between proxy and backend will be over
|
||||
@@ -840,6 +881,7 @@ type reverseProxy struct {
|
||||
insecure bool
|
||||
backend string
|
||||
lb *LocalBackend
|
||||
socketPath string // path to unix socket, empty for TCP
|
||||
httpTransport lazy.SyncValue[*http.Transport] // transport for non-h2c backends
|
||||
h2cTransport lazy.SyncValue[*http.Transport] // transport for h2c backends
|
||||
// closed tracks whether proxy is closed/currently closing.
|
||||
@@ -880,7 +922,12 @@ func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r.Out.URL.RawPath = rp.url.RawPath
|
||||
}
|
||||
|
||||
r.Out.Host = r.In.Host
|
||||
// For Unix sockets, use the URL's host (localhost) instead of the incoming host
|
||||
if rp.socketPath != "" {
|
||||
r.Out.Host = rp.url.Host
|
||||
} else {
|
||||
r.Out.Host = r.In.Host
|
||||
}
|
||||
addProxyForwardedHeaders(r)
|
||||
rp.lb.addTailscaleIdentityHeaders(r)
|
||||
if err := rp.lb.addAppCapabilitiesHeader(r); err != nil {
|
||||
@@ -905,8 +952,16 @@ func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// to the backend. The Transport gets created lazily, at most once.
|
||||
func (rp *reverseProxy) getTransport() *http.Transport {
|
||||
return rp.httpTransport.Get(func() *http.Transport {
|
||||
dial := rp.lb.dialer.SystemDial
|
||||
if rp.socketPath != "" {
|
||||
dial = func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", rp.socketPath)
|
||||
}
|
||||
}
|
||||
|
||||
return &http.Transport{
|
||||
DialContext: rp.lb.dialer.SystemDial,
|
||||
DialContext: dial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: rp.insecure,
|
||||
},
|
||||
@@ -929,6 +984,10 @@ func (rp *reverseProxy) getH2CTransport() http.RoundTripper {
|
||||
tr := &http.Transport{
|
||||
Protocols: &p,
|
||||
DialTLSContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
||||
if rp.socketPath != "" {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "unix", rp.socketPath)
|
||||
}
|
||||
return rp.lb.dialer.SystemDial(ctx, "tcp", rp.url.Host)
|
||||
},
|
||||
}
|
||||
@@ -940,6 +999,10 @@ func (rp *reverseProxy) getH2CTransport() http.RoundTripper {
|
||||
// for a h2c server, but sufficient for our particular use case.
|
||||
func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
|
||||
contentType := r.Header.Get(contentTypeHeader)
|
||||
// For unix sockets, check if it's gRPC content to determine h2c
|
||||
if rp.socketPath != "" {
|
||||
return r.ProtoMajor == 2 && isGRPCContentType(contentType)
|
||||
}
|
||||
return r.ProtoMajor == 2 && strings.HasPrefix(rp.backend, "http://") && isGRPCContentType(contentType)
|
||||
}
|
||||
|
||||
@@ -1184,6 +1247,10 @@ func expandProxyArg(s string) (targetURL string, insecureSkipVerify bool) {
|
||||
if s == "" {
|
||||
return "", false
|
||||
}
|
||||
// Unix sockets - return as-is
|
||||
if strings.HasPrefix(s, "unix:") {
|
||||
return s, false
|
||||
}
|
||||
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
|
||||
return s, false
|
||||
}
|
||||
|
||||
218
ipn/ipnlocal/serve_unix_test.go
Normal file
218
ipn/ipnlocal/serve_unix_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build unix
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestExpandProxyArgUnix(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
wantURL string
|
||||
wantInsecure bool
|
||||
}{
|
||||
{
|
||||
input: "unix:/tmp/test.sock",
|
||||
wantURL: "unix:/tmp/test.sock",
|
||||
},
|
||||
{
|
||||
input: "unix:/var/run/docker.sock",
|
||||
wantURL: "unix:/var/run/docker.sock",
|
||||
},
|
||||
{
|
||||
input: "unix:./relative.sock",
|
||||
wantURL: "unix:./relative.sock",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
gotURL, gotInsecure := expandProxyArg(tt.input)
|
||||
if gotURL != tt.wantURL {
|
||||
t.Errorf("expandProxyArg(%q) url = %q, want %q", tt.input, gotURL, tt.wantURL)
|
||||
}
|
||||
if gotInsecure != tt.wantInsecure {
|
||||
t.Errorf("expandProxyArg(%q) insecure = %v, want %v", tt.input, gotInsecure, tt.wantInsecure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeUnixSocket(t *testing.T) {
|
||||
// Create a temporary directory for our socket
|
||||
tmpDir := t.TempDir()
|
||||
socketPath := filepath.Join(tmpDir, "test.sock")
|
||||
|
||||
// Create a test HTTP server on Unix socket
|
||||
listener, err := net.Listen("unix", socketPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create unix socket listener: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
testResponse := "Hello from Unix socket!"
|
||||
testServer := &http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprint(w, testResponse)
|
||||
}),
|
||||
}
|
||||
|
||||
go testServer.Serve(listener)
|
||||
defer testServer.Close()
|
||||
|
||||
// Wait for server to be ready
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Create LocalBackend with test logger
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
b := newTestBackend(t)
|
||||
b.logf = logf
|
||||
|
||||
// Test creating proxy handler for Unix socket
|
||||
handler, err := b.proxyHandlerForBackend("unix:" + socketPath)
|
||||
if err != nil {
|
||||
t.Fatalf("proxyHandlerForBackend failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify it's a reverseProxy with correct socketPath
|
||||
rp, ok := handler.(*reverseProxy)
|
||||
if !ok {
|
||||
t.Fatalf("expected *reverseProxy, got %T", handler)
|
||||
}
|
||||
if rp.socketPath != socketPath {
|
||||
t.Errorf("socketPath = %q, want %q", rp.socketPath, socketPath)
|
||||
}
|
||||
if rp.url.Host != "localhost" {
|
||||
t.Errorf("url.Host = %q, want %q", rp.url.Host, "localhost")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeUnixSocketErrors(t *testing.T) {
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
b := newTestBackend(t)
|
||||
b.logf = logf
|
||||
|
||||
// Test empty socket path
|
||||
_, err := b.proxyHandlerForBackend("unix:")
|
||||
if err == nil {
|
||||
t.Error("expected error for empty socket path")
|
||||
}
|
||||
|
||||
// Test non-existent socket - should create handler but fail on request
|
||||
nonExistentSocket := filepath.Join(t.TempDir(), "nonexistent.sock")
|
||||
handler, err := b.proxyHandlerForBackend("unix:" + nonExistentSocket)
|
||||
if err != nil {
|
||||
t.Fatalf("proxyHandlerForBackend failed: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", "http://foo.test.ts.net/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
// Should get a 502 Bad Gateway when socket doesn't exist
|
||||
if rec.Code != http.StatusBadGateway {
|
||||
t.Errorf("got status %d, want %d for non-existent socket", rec.Code, http.StatusBadGateway)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReverseProxyConfigurationUnix(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
|
||||
// Test that Unix socket backend creates proper reverseProxy
|
||||
backend := "unix:/var/run/test.sock"
|
||||
handler, err := b.proxyHandlerForBackend(backend)
|
||||
if err != nil {
|
||||
t.Fatalf("proxyHandlerForBackend failed: %v", err)
|
||||
}
|
||||
|
||||
rp, ok := handler.(*reverseProxy)
|
||||
if !ok {
|
||||
t.Fatalf("expected *reverseProxy, got %T", handler)
|
||||
}
|
||||
|
||||
// Verify configuration
|
||||
if rp.socketPath != "/var/run/test.sock" {
|
||||
t.Errorf("socketPath = %q, want %q", rp.socketPath, "/var/run/test.sock")
|
||||
}
|
||||
if rp.backend != backend {
|
||||
t.Errorf("backend = %q, want %q", rp.backend, backend)
|
||||
}
|
||||
if rp.insecure {
|
||||
t.Error("insecure should be false for unix sockets")
|
||||
}
|
||||
expectedURL := url.URL{Scheme: "http", Host: "localhost"}
|
||||
if rp.url.Scheme != expectedURL.Scheme || rp.url.Host != expectedURL.Host {
|
||||
t.Errorf("url = %v, want %v", rp.url, expectedURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeBlocksTailscaledSocket(t *testing.T) {
|
||||
// Use /tmp to avoid macOS socket path length limits
|
||||
tmpDir, err := os.MkdirTemp("/tmp", "ts-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
tailscaledSocket := filepath.Join(tmpDir, "ts.sock")
|
||||
|
||||
// Create actual socket file
|
||||
listener, err := net.Listen("unix", tailscaledSocket)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create tailscaled socket: %v", err)
|
||||
}
|
||||
defer listener.Close()
|
||||
|
||||
b := newTestBackend(t)
|
||||
b.sys.SocketPath = tailscaledSocket
|
||||
|
||||
// Direct path to tailscaled socket should be blocked
|
||||
_, err = b.proxyHandlerForBackend("unix:" + tailscaledSocket)
|
||||
if !errors.Is(err, ErrProxyToTailscaledSocket) {
|
||||
t.Errorf("direct path: got err=%v, want ErrProxyToTailscaledSocket", err)
|
||||
}
|
||||
|
||||
// Symlink to tailscaled socket should be blocked
|
||||
symlinkPath := filepath.Join(tmpDir, "link")
|
||||
if err := os.Symlink(tailscaledSocket, symlinkPath); err != nil {
|
||||
t.Fatalf("failed to create symlink: %v", err)
|
||||
}
|
||||
|
||||
_, err = b.proxyHandlerForBackend("unix:" + symlinkPath)
|
||||
if !errors.Is(err, ErrProxyToTailscaledSocket) {
|
||||
t.Errorf("symlink: got err=%v, want ErrProxyToTailscaledSocket", err)
|
||||
}
|
||||
|
||||
// Different socket should work
|
||||
otherSocket := filepath.Join(tmpDir, "ok.sock")
|
||||
listener2, err := net.Listen("unix", otherSocket)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create other socket: %v", err)
|
||||
}
|
||||
defer listener2.Close()
|
||||
|
||||
handler, err := b.proxyHandlerForBackend("unix:" + otherSocket)
|
||||
if err != nil {
|
||||
t.Errorf("legitimate socket should not be blocked: %v", err)
|
||||
}
|
||||
if handler == nil {
|
||||
t.Error("expected valid handler for legitimate socket")
|
||||
}
|
||||
}
|
||||
16
ipn/serve.go
16
ipn/serve.go
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -713,6 +714,21 @@ func ExpandProxyTargetValue(target string, supportedSchemes []string, defaultSch
|
||||
return fmt.Sprintf("%s://%s:%d", defaultScheme, host, port), nil
|
||||
}
|
||||
|
||||
// handle unix: scheme specially - it doesn't use standard URL format
|
||||
if strings.HasPrefix(target, "unix:") {
|
||||
if !slices.Contains(supportedSchemes, "unix") {
|
||||
return "", fmt.Errorf("unix sockets are not supported for this target type")
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
return "", fmt.Errorf("unix socket serve target is not supported on Windows")
|
||||
}
|
||||
path := strings.TrimPrefix(target, "unix:")
|
||||
if path == "" {
|
||||
return "", fmt.Errorf("unix socket path cannot be empty")
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
hasScheme := true
|
||||
// prepend scheme if not present
|
||||
if !strings.Contains(target, "://") {
|
||||
|
||||
82
ipn/serve_expand_test.go
Normal file
82
ipn/serve_expand_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpandProxyTargetValueUnix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
target string
|
||||
supportedSchemes []string
|
||||
defaultScheme string
|
||||
want string
|
||||
wantErr bool
|
||||
skipOnWindows bool
|
||||
}{
|
||||
{
|
||||
name: "unix-socket-absolute-path",
|
||||
target: "unix:/tmp/myservice.sock",
|
||||
supportedSchemes: []string{"http", "https", "unix"},
|
||||
defaultScheme: "http",
|
||||
want: "unix:/tmp/myservice.sock",
|
||||
skipOnWindows: true,
|
||||
},
|
||||
{
|
||||
name: "unix-socket-var-run",
|
||||
target: "unix:/var/run/docker.sock",
|
||||
supportedSchemes: []string{"http", "https", "unix"},
|
||||
defaultScheme: "http",
|
||||
want: "unix:/var/run/docker.sock",
|
||||
skipOnWindows: true,
|
||||
},
|
||||
{
|
||||
name: "unix-socket-relative-path",
|
||||
target: "unix:./myservice.sock",
|
||||
supportedSchemes: []string{"http", "https", "unix"},
|
||||
defaultScheme: "http",
|
||||
want: "unix:./myservice.sock",
|
||||
skipOnWindows: true,
|
||||
},
|
||||
{
|
||||
name: "unix-socket-empty-path",
|
||||
target: "unix:",
|
||||
supportedSchemes: []string{"http", "https", "unix"},
|
||||
defaultScheme: "http",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unix-socket-not-in-supported-schemes",
|
||||
target: "unix:/tmp/myservice.sock",
|
||||
supportedSchemes: []string{"http", "https"},
|
||||
defaultScheme: "http",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.skipOnWindows && runtime.GOOS == "windows" {
|
||||
t.Skip("skipping unix socket test on Windows")
|
||||
}
|
||||
|
||||
// On Windows, unix sockets should always error
|
||||
if runtime.GOOS == "windows" && !tt.wantErr {
|
||||
tt.wantErr = true
|
||||
}
|
||||
|
||||
got, err := ExpandProxyTargetValue(tt.target, tt.supportedSchemes, tt.defaultScheme)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ExpandProxyTargetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && got != tt.want {
|
||||
t.Errorf("ExpandProxyTargetValue() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -68,6 +68,10 @@ type System struct {
|
||||
// LocalBackend tracks the current config after any reloads.
|
||||
InitialConfig *conffile.Config
|
||||
|
||||
// SocketPath is the path to the tailscaled Unix socket.
|
||||
// It is used to prevent serve from proxying to our own socket.
|
||||
SocketPath string
|
||||
|
||||
// onlyNetstack is whether the Tun value is a fake TUN device
|
||||
// and we're using netstack for everything.
|
||||
onlyNetstack bool
|
||||
|
||||
Reference in New Issue
Block a user