tangled
alpha
login
or
join now
sr.aux1.dev
/
tsproxy
2
fork
atom
HTTP reverse proxy for Tailscale
2
fork
atom
overview
issues
pulls
1
pipelines
add IP allowlist support for funnel
sr.aux1.dev
9 months ago
006820fc
60871e31
+65
-1
1 changed file
expand all
collapse all
unified
split
tsproxy.go
+65
-1
tsproxy.go
···
12
12
"net"
13
13
"net/http"
14
14
"net/http/httputil"
15
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
35
+
"tailscale.com/ipn"
34
36
"tailscale.com/tsnet"
35
37
tslogger "tailscale.com/types/logger"
36
38
)
39
39
+
40
40
+
// ctxConn is a key to look up a net.Conn stored in an HTTP request's context.
41
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
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
343
-
srv = &http.Server{Handler: instrument(redirect(st.Self.DNSName, true, handler))}
349
349
+
350
350
+
handler = redirect(st.Self.DNSName, true, handler)
351
351
+
352
352
+
if len(funnel.IP) > 0 {
353
353
+
var allow []netip.Prefix
354
354
+
for _, ip := range funnel.IP {
355
355
+
allow = append(allow, netip.MustParsePrefix(ip))
356
356
+
}
357
357
+
handler = restrictNetworks(log, allow, handler)
358
358
+
}
359
359
+
360
360
+
srv = &http.Server{
361
361
+
Handler: instrument(handler),
362
362
+
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
363
363
+
return context.WithValue(ctx, ctxConn{}, c)
364
364
+
},
365
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
506
+
507
507
+
// restrictNetworks will only allow clients from the provided IP networks to
508
508
+
// access the given handler. If skip prefixes are set, paths that match any
509
509
+
// of the regular expressions will not have restrictions applied.
510
510
+
func restrictNetworks(logger *slog.Logger, allowedNetworks []netip.Prefix, next http.Handler) http.Handler {
511
511
+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
512
512
+
// If the funneled connection is from tsnet, then the net.Conn will be of
513
513
+
// type ipn.FunnelConn.
514
514
+
netConn := r.Context().Value(ctxConn{})
515
515
+
// if the conn is wrapped inside TLS, unwrap it
516
516
+
if tlsConn, ok := netConn.(*tls.Conn); ok {
517
517
+
netConn = tlsConn.NetConn()
518
518
+
}
519
519
+
var remote netip.AddrPort
520
520
+
if fconn, ok := netConn.(*ipn.FunnelConn); ok {
521
521
+
remote = fconn.Src
522
522
+
} else if v, err := netip.ParseAddrPort(r.RemoteAddr); err == nil {
523
523
+
remote = v
524
524
+
} else {
525
525
+
logger.Error("restrictNetworks: cannot parse client IP:port", lerr(err), slog.String("remote", r.RemoteAddr))
526
526
+
w.WriteHeader(http.StatusUnauthorized)
527
527
+
return
528
528
+
}
529
529
+
530
530
+
for _, wl := range allowedNetworks {
531
531
+
if wl.Contains(remote.Addr()) {
532
532
+
next.ServeHTTP(w, r)
533
533
+
return
534
534
+
}
535
535
+
}
536
536
+
537
537
+
w.WriteHeader(http.StatusForbidden)
538
538
+
_, _ = fmt.Fprint(w, badNetwork)
539
539
+
})
540
540
+
}
541
541
+
542
542
+
const badNetwork = `
543
543
+
<html>
544
544
+
<head><title>Untrusted network</title></head>
545
545
+
<body><h1>Access from untrusted networks not permitted</h1></body>
546
546
+
</html>
547
547
+
`
484
548
485
549
type tailscaleLocalClient interface {
486
550
WhoIs(context.Context, string) (*apitype.WhoIsResponse, error)