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
implement oidc funnel handler
sr.aux1.dev
10 months ago
4d24fc97
333695d9
+95
-8
3 changed files
expand all
collapse all
unified
split
go.mod
go.sum
main.go
+3
go.mod
···
56
56
github.com/klauspost/compress v1.17.11 // indirect
57
57
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect
58
58
github.com/kylelemons/godebug v1.1.0 // indirect
59
59
+
github.com/lstoll/oidc v1.0.0-beta.4.0.20250106123456-6ffce62670fe // indirect
59
60
github.com/mdlayher/genetlink v1.3.2 // indirect
60
61
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
61
62
github.com/mdlayher/sdnotify v1.0.0 // indirect
···
75
76
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
76
77
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
77
78
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 // indirect
79
79
+
github.com/tink-crypto/tink-go/v2 v2.2.0 // indirect
78
80
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
79
81
github.com/vishvananda/netns v0.0.4 // indirect
80
82
github.com/x448/float16 v0.8.4 // indirect
···
84
86
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
85
87
golang.org/x/mod v0.23.0 // indirect
86
88
golang.org/x/net v0.36.0 // indirect
89
89
+
golang.org/x/oauth2 v0.26.0 // indirect
87
90
golang.org/x/sync v0.11.0 // indirect
88
91
golang.org/x/sys v0.30.0 // indirect
89
92
golang.org/x/term v0.29.0 // indirect
+6
go.sum
···
117
117
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
118
118
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
119
119
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
120
120
+
github.com/lstoll/oidc v1.0.0-beta.4.0.20250106123456-6ffce62670fe h1:QBlUtM+Rv9P+W3k9C6+xLgpssfxcKd8Ir+pvNM7E23Y=
121
121
+
github.com/lstoll/oidc v1.0.0-beta.4.0.20250106123456-6ffce62670fe/go.mod h1:H1Y2Ektfl9aWzSHYT1qf6lXpE9mdil6ZavkI/5+N5Qg=
120
122
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
121
123
github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
122
124
github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
···
183
185
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
184
186
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
185
187
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
188
188
+
github.com/tink-crypto/tink-go/v2 v2.2.0 h1:L2Da0F2Udh2agtKztdr69mV/KpnY3/lGTkMgLTVIXlA=
189
189
+
github.com/tink-crypto/tink-go/v2 v2.2.0/go.mod h1:JJ6PomeNPF3cJpfWC0lgyTES6zpJILkAX0cJNwlS3xU=
186
190
github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
187
191
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
188
192
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
···
208
212
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
209
213
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
210
214
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
215
215
+
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
216
216
+
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
211
217
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
212
218
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
213
219
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+86
-8
main.go
···
19
19
"strings"
20
20
"syscall"
21
21
22
22
+
"github.com/lstoll/oidc"
23
23
+
"github.com/lstoll/oidc/middleware"
22
24
"github.com/oklog/run"
23
25
"github.com/prometheus/client_golang/prometheus"
24
26
versioncollector "github.com/prometheus/client_golang/prometheus/collectors/version"
···
67
69
Name string
68
70
Backend string
69
71
Prometheus bool
70
70
-
Funnel bool
72
72
+
Funnel *funnelConfig `json:"funnel,omitempty"`
73
73
+
}
74
74
+
75
75
+
type funnelConfig struct {
76
76
+
Insecure bool
77
77
+
Issuer string
78
78
+
ClientID string
79
79
+
ClientSecret string
71
80
}
72
81
73
82
type target struct {
···
301
310
cancel()
302
311
})
303
312
}
304
304
-
if upstream.Funnel {
313
313
+
if funnel := upstream.Funnel; funnel != nil {
314
314
+
if !funnel.Insecure && funnel.Issuer == "" {
315
315
+
return fmt.Errorf("upstream %s: funnel must set issuer or insecure", upstream.Name)
316
316
+
}
305
317
{
306
318
var srv *http.Server
307
319
g.Add(func() error {
···
309
321
if err != nil {
310
322
return fmt.Errorf("tailscale: wait for tsnet %s to be ready: %w", upstream.Name, err)
311
323
}
312
312
-
srv = &http.Server{
313
313
-
Handler: instrument(insecureFunnelHandler(log, lc, proxy)),
324
324
+
325
325
+
srv = &http.Server{}
326
326
+
if funnel.Issuer != "" {
327
327
+
handler, err := oidcFunnelHandler(ctx, log, lc, funnel, proxy)
328
328
+
if err != nil {
329
329
+
return fmt.Errorf("oidc: %w", err)
330
330
+
}
331
331
+
srv.Handler = handler
332
332
+
} else if funnel.Insecure {
333
333
+
srv.Handler = insecureFunnelHandler(log, lc, proxy)
334
334
+
} else {
335
335
+
panic("funnel misconfigured")
314
336
}
337
337
+
srv.Handler = instrument(srv.Handler)
315
338
316
339
ln, err := ts.ListenFunnel("tcp", ":443", tsnet.FunnelOnly())
317
340
if err != nil {
···
339
362
whois, err := tsWhoIs(lc, r)
340
363
if err != nil {
341
364
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
342
342
-
logger.Error("tailscale whois", lerr(err))
365
365
+
logger.ErrorContext(r.Context(), "tailscale whois", lerr(err))
343
366
return
344
367
}
345
368
···
364
387
)
365
388
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
366
389
if r.TLS == nil {
367
367
-
panic("TLS handler wants TLS")
390
390
+
panic("tailnet handler wants tls")
368
391
}
369
392
370
393
if dnsName == "" {
371
394
st, err := lc.StatusWithoutPeers(r.Context())
372
395
if err != nil {
373
396
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
374
374
-
logger.Error("tailscale status", slog.Any("err", err))
397
397
+
logger.ErrorContext(r.Context(), "tailscale status", slog.Any("err", err))
375
398
return
376
399
}
377
400
dnsName = strings.TrimSuffix(st.Self.DNSName, ".")
···
390
413
// This is marked insecure because the upstream is exposed to the public Internet.
391
414
// The upstream is responsible for implementing authentication.
392
415
func insecureFunnelHandler(logger *slog.Logger, lc tailscaleLocalClient, next http.Handler) http.Handler {
393
393
-
return localTailnetHandler(logger, lc, next)
416
416
+
return localTailnetTLSHandler(logger, lc, next)
417
417
+
}
418
418
+
419
419
+
// oidcFunnelHandlers serves Funnel requests, requiring authentication via the configured OIDC issuer.
420
420
+
func oidcFunnelHandler(ctx context.Context, logger *slog.Logger, lc tailscaleLocalClient, cfg *funnelConfig, next http.Handler) (http.Handler, error) {
421
421
+
st, err := lc.StatusWithoutPeers(ctx)
422
422
+
if err != nil {
423
423
+
return nil, fmt.Errorf("tailscale status: %w", err)
424
424
+
}
425
425
+
426
426
+
redir := &url.URL{Scheme: "https", Path: ".oidc-callback"}
427
427
+
redir.Host = strings.TrimSuffix(st.Self.DNSName, ".")
428
428
+
429
429
+
wrapper, err := middleware.NewFromDiscovery(ctx, nil, cfg.Issuer, cfg.ClientID, cfg.ClientSecret, redir.String())
430
430
+
if err != nil {
431
431
+
return nil, fmt.Errorf("oidc middleware: %w", err)
432
432
+
}
433
433
+
wrapper.OAuth2Config.Scopes = append(wrapper.OAuth2Config.Scopes, oidc.ScopeProfile)
434
434
+
435
435
+
return wrapper.Wrap(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
436
436
+
if r.TLS == nil {
437
437
+
panic("oidc handler wants tls")
438
438
+
}
439
439
+
440
440
+
_, err := tsWhoIs(lc, r)
441
441
+
if err != nil {
442
442
+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
443
443
+
logger.ErrorContext(r.Context(), "tailscale whois", lerr(err))
444
444
+
return
445
445
+
}
446
446
+
447
447
+
tok := middleware.IDJWTFromContext(r.Context())
448
448
+
if tok == nil {
449
449
+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
450
450
+
logger.ErrorContext(r.Context(), "jwt token missing")
451
451
+
return
452
452
+
}
453
453
+
email, err := tok.StringClaim("email")
454
454
+
if err != nil {
455
455
+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
456
456
+
logger.ErrorContext(r.Context(), "claim missing", slog.String("claim", "email"))
457
457
+
return
458
458
+
}
459
459
+
name, err := tok.StringClaim("name")
460
460
+
if err != nil {
461
461
+
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
462
462
+
logger.ErrorContext(r.Context(), "claim missing", slog.String("claim", "name"))
463
463
+
return
464
464
+
}
465
465
+
466
466
+
req := r.Clone(r.Context())
467
467
+
req.Header.Set("X-Webauth-User", email)
468
468
+
req.Header.Set("X-Webauth-Name", name)
469
469
+
470
470
+
next.ServeHTTP(w, r)
471
471
+
})), nil
394
472
}
395
473
396
474
type tailscaleLocalClient interface {