|
@@ -76,6 +76,10 @@ const (
|
|
|
// current etag of a resource.
|
|
// current etag of a resource.
|
|
|
var ErrETagMismatch = errors.New("etag mismatch")
|
|
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]
|
|
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
|
|
|
|
|
|
|
|
type serveHTTPContext struct {
|
|
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).
|
|
// we serve requests for. `backend` is a HTTPHandler.Proxy string (url, hostport or just port).
|
|
|
func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, error) {
|
|
func (b *LocalBackend) proxyHandlerForBackend(backend string) (http.Handler, error) {
|
|
|
targetURL, insecure := expandProxyArg(backend)
|
|
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)
|
|
u, err := url.Parse(targetURL)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
|
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
|
|
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
|
|
// reverseProxy is a proxy that forwards a request to a backend host
|
|
|
// (preconfigured via ipn.ServeConfig). If the host is configured with
|
|
// (preconfigured via ipn.ServeConfig). If the host is configured with
|
|
|
// http+insecure prefix, connection between proxy and backend will be over
|
|
// http+insecure prefix, connection between proxy and backend will be over
|
|
@@ -840,6 +881,7 @@ type reverseProxy struct {
|
|
|
insecure bool
|
|
insecure bool
|
|
|
backend string
|
|
backend string
|
|
|
lb *LocalBackend
|
|
lb *LocalBackend
|
|
|
|
|
+ socketPath string // path to unix socket, empty for TCP
|
|
|
httpTransport lazy.SyncValue[*http.Transport] // transport for non-h2c backends
|
|
httpTransport lazy.SyncValue[*http.Transport] // transport for non-h2c backends
|
|
|
h2cTransport lazy.SyncValue[*http.Transport] // transport for h2c backends
|
|
h2cTransport lazy.SyncValue[*http.Transport] // transport for h2c backends
|
|
|
// closed tracks whether proxy is closed/currently closing.
|
|
// 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.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)
|
|
addProxyForwardedHeaders(r)
|
|
|
rp.lb.addTailscaleIdentityHeaders(r)
|
|
rp.lb.addTailscaleIdentityHeaders(r)
|
|
|
if err := rp.lb.addAppCapabilitiesHeader(r); err != nil {
|
|
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.
|
|
// to the backend. The Transport gets created lazily, at most once.
|
|
|
func (rp *reverseProxy) getTransport() *http.Transport {
|
|
func (rp *reverseProxy) getTransport() *http.Transport {
|
|
|
return rp.httpTransport.Get(func() *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{
|
|
return &http.Transport{
|
|
|
- DialContext: rp.lb.dialer.SystemDial,
|
|
|
|
|
|
|
+ DialContext: dial,
|
|
|
TLSClientConfig: &tls.Config{
|
|
TLSClientConfig: &tls.Config{
|
|
|
InsecureSkipVerify: rp.insecure,
|
|
InsecureSkipVerify: rp.insecure,
|
|
|
},
|
|
},
|
|
@@ -929,6 +984,10 @@ func (rp *reverseProxy) getH2CTransport() http.RoundTripper {
|
|
|
tr := &http.Transport{
|
|
tr := &http.Transport{
|
|
|
Protocols: &p,
|
|
Protocols: &p,
|
|
|
DialTLSContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
|
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)
|
|
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.
|
|
// for a h2c server, but sufficient for our particular use case.
|
|
|
func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
|
|
func (rp *reverseProxy) shouldProxyViaH2C(r *http.Request) bool {
|
|
|
contentType := r.Header.Get(contentTypeHeader)
|
|
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)
|
|
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 == "" {
|
|
if s == "" {
|
|
|
return "", false
|
|
return "", false
|
|
|
}
|
|
}
|
|
|
|
|
+ // Unix sockets - return as-is
|
|
|
|
|
+ if strings.HasPrefix(s, "unix:") {
|
|
|
|
|
+ return s, false
|
|
|
|
|
+ }
|
|
|
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
|
|
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
|
|
|
return s, false
|
|
return s, false
|
|
|
}
|
|
}
|