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
+69
-2
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user