HTTP reverse proxy for Tailscale

add IP allowlist support for funnel

+65 -1
+65 -1
tsproxy.go
··· 12 12 "net" 13 13 "net/http" 14 14 "net/http/httputil" 15 + "net/netip" 15 16 "net/url" 16 17 "os" 17 18 "path/filepath" ··· 31 32 "github.com/tailscale/hujson" 32 33 "tailscale.com/client/local" 33 34 "tailscale.com/client/tailscale/apitype" 35 + "tailscale.com/ipn" 34 36 "tailscale.com/tsnet" 35 37 tslogger "tailscale.com/types/logger" 36 38 ) 39 + 40 + // ctxConn is a key to look up a net.Conn stored in an HTTP request's context. 41 + type ctxConn struct{} 37 42 38 43 var ( 39 44 requestsInFlight = promauto.NewGaugeVec( ··· 79 84 ClientSecret string 80 85 User string 81 86 Password string 87 + IP []string 82 88 } 83 89 84 90 type target struct { ··· 340 346 default: 341 347 return fmt.Errorf("upstream %s must set funnel.insecure or funnel.issuer", upstream.Name) 342 348 } 343 - srv = &http.Server{Handler: instrument(redirect(st.Self.DNSName, true, handler))} 349 + 350 + handler = redirect(st.Self.DNSName, true, handler) 351 + 352 + if len(funnel.IP) > 0 { 353 + var allow []netip.Prefix 354 + for _, ip := range funnel.IP { 355 + allow = append(allow, netip.MustParsePrefix(ip)) 356 + } 357 + handler = restrictNetworks(log, allow, handler) 358 + } 359 + 360 + srv = &http.Server{ 361 + Handler: instrument(handler), 362 + ConnContext: func(ctx context.Context, c net.Conn) context.Context { 363 + return context.WithValue(ctx, ctxConn{}, c) 364 + }, 365 + } 344 366 345 367 ln, err := ts.ListenFunnel("tcp", ":443", tsnet.FunnelOnly()) 346 368 if err != nil { ··· 481 503 next.ServeHTTP(w, req) 482 504 }) 483 505 } 506 + 507 + // restrictNetworks will only allow clients from the provided IP networks to 508 + // access the given handler. If skip prefixes are set, paths that match any 509 + // of the regular expressions will not have restrictions applied. 510 + func restrictNetworks(logger *slog.Logger, allowedNetworks []netip.Prefix, next http.Handler) http.Handler { 511 + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 512 + // If the funneled connection is from tsnet, then the net.Conn will be of 513 + // type ipn.FunnelConn. 514 + netConn := r.Context().Value(ctxConn{}) 515 + // if the conn is wrapped inside TLS, unwrap it 516 + if tlsConn, ok := netConn.(*tls.Conn); ok { 517 + netConn = tlsConn.NetConn() 518 + } 519 + var remote netip.AddrPort 520 + if fconn, ok := netConn.(*ipn.FunnelConn); ok { 521 + remote = fconn.Src 522 + } else if v, err := netip.ParseAddrPort(r.RemoteAddr); err == nil { 523 + remote = v 524 + } else { 525 + logger.Error("restrictNetworks: cannot parse client IP:port", lerr(err), slog.String("remote", r.RemoteAddr)) 526 + w.WriteHeader(http.StatusUnauthorized) 527 + return 528 + } 529 + 530 + for _, wl := range allowedNetworks { 531 + if wl.Contains(remote.Addr()) { 532 + next.ServeHTTP(w, r) 533 + return 534 + } 535 + } 536 + 537 + w.WriteHeader(http.StatusForbidden) 538 + _, _ = fmt.Fprint(w, badNetwork) 539 + }) 540 + } 541 + 542 + const badNetwork = ` 543 + <html> 544 + <head><title>Untrusted network</title></head> 545 + <body><h1>Access from untrusted networks not permitted</h1></body> 546 + </html> 547 + ` 484 548 485 549 type tailscaleLocalClient interface { 486 550 WhoIs(context.Context, string) (*apitype.WhoIsResponse, error)