From 03f5511342c84092ecfe170ede278aca803ba30e Mon Sep 17 00:00:00 2001 From: Gesa Stupperich Date: Mon, 15 Sep 2025 15:42:36 +0100 Subject: [PATCH] WIP Signed-off-by: Gesa Stupperich --- .../builddir/tailscale.com/go.mod | 2 +- .../natlabapp/builddir/tailscale.com/go.mod | 2 +- ipn/ipnlocal/serve.go | 26 +++++++++++++++++++ ipn/ipnlocal/serve_test.go | 24 +++++++++++++++++ 4 files changed, 52 insertions(+), 2 deletions(-) diff --git a/gokrazy/natlabapp.arm64/builddir/tailscale.com/go.mod b/gokrazy/natlabapp.arm64/builddir/tailscale.com/go.mod index da21a1439..a4bd216b2 100644 --- a/gokrazy/natlabapp.arm64/builddir/tailscale.com/go.mod +++ b/gokrazy/natlabapp.arm64/builddir/tailscale.com/go.mod @@ -1,6 +1,6 @@ module gokrazy/build/tsapp -go 1.23.1 +go 1.25.1 replace tailscale.com => ../../../.. diff --git a/gokrazy/natlabapp/builddir/tailscale.com/go.mod b/gokrazy/natlabapp/builddir/tailscale.com/go.mod index da21a1439..a4bd216b2 100644 --- a/gokrazy/natlabapp/builddir/tailscale.com/go.mod +++ b/gokrazy/natlabapp/builddir/tailscale.com/go.mod @@ -1,6 +1,6 @@ module gokrazy/build/tsapp -go 1.23.1 +go 1.25.1 replace tailscale.com => ../../../.. diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index cbf84fb29..cf65fe044 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -806,6 +806,7 @@ func (rp *reverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Out.Host = r.In.Host addProxyForwardedHeaders(r) rp.lb.addTailscaleIdentityHeaders(r) + rp.lb.addCustomGrantHeaders(r) }} // There is no way to autodetect h2c as per RFC 9113 @@ -912,6 +913,31 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) { r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers") } +func (b *LocalBackend) addCustomGrantHeaders(r *httputil.ProxyRequest) { + c, ok := serveHTTPContextKey.ValueOk(r.Out.Context()) + if !ok { + return + } + peerCaps := b.PeerCaps(c.SrcAddr.Addr()) + + // TODO: make configurable + capType := tailcfg.PeerCapability("neinkeinkaffee.com/cap/grafana") + type capStruct struct { + Role string `json:"role,omitempty"` + } + + unmarshalled, err := tailcfg.UnmarshalCapJSON[capStruct](peerCaps, capType) + if err != nil { + b.logf("couldn't parse capability %s: %v", capType, err) + return + } + if len(unmarshalled) > 0 { + // TODO: make configurable + value := unmarshalled[0].Role + r.Out.Header.Set("Tailscale-User-Role", encTailscaleHeaderValue(value)) + } +} + // encTailscaleHeaderValue cleans or encodes as necessary v, to be suitable in // an HTTP header value. See // https://github.com/tailscale/tailscale/issues/11603. diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index a081ed27b..38a58c51d 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -23,6 +23,7 @@ import ( "path/filepath" "reflect" "strings" + "tailscale.com/types/views" "testing" "time" @@ -699,6 +700,9 @@ func TestServeHTTPProxyHeaders(t *testing.T) { want string } + peercaps := b.PeerCaps(netip.MustParsePrefix("100.150.151.152/32").Addr()) + _ = peercaps + tests := []struct { name string srcIP string @@ -714,6 +718,7 @@ func TestServeHTTPProxyHeaders(t *testing.T) { {"Tailscale-User-Name", "Some One"}, {"Tailscale-User-Profile-Pic", "https://example.com/photo.jpg"}, {"Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers"}, + {"Tailscale-User-Role", "Admin"}, }, }, { @@ -925,6 +930,9 @@ func newTestBackend(t *testing.T, opts ...any) *LocalBackend { b.currentNode().SetNetMap(&netmap.NetworkMap{ SelfNode: (&tailcfg.Node{ Name: "example.ts.net", + Addresses: []netip.Prefix{ + netip.MustParsePrefix("100.150.151.151/32"), + }, }).View(), UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{ tailcfg.UserID(1): (&tailcfg.UserProfile{ @@ -933,6 +941,19 @@ func newTestBackend(t *testing.T, opts ...any) *LocalBackend { ProfilePicURL: "https://example.com/photo.jpg", }).View(), }, + PacketFilterRules: views.SliceOf([]tailcfg.FilterRule{{ + SrcIPs: []string{"100.150.151.152"}, // first peer in the list below, the one without tags + CapGrant: []tailcfg.CapGrant{{ + Dsts: []netip.Prefix{ + netip.MustParsePrefix("100.150.151.151/32"), // TODO: try anywhere instead? + }, + CapMap: tailcfg.PeerCapMap{ + "neinkeinkaffee.com/cap/grafana": []tailcfg.RawMessage{ + "{\"role\":[\"Admin\"]}", + }, + }, + }}, + }}), Peers: []tailcfg.NodeView{ (&tailcfg.Node{ ID: 152, @@ -942,6 +963,9 @@ func newTestBackend(t *testing.T, opts ...any) *LocalBackend { Addresses: []netip.Prefix{ netip.MustParsePrefix("100.150.151.152/32"), }, + CapMap: map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + "neinkeinkaffee.com/cap/grafana": {"{\"role\":[\"Admin\"]}"}, + }, }).View(), (&tailcfg.Node{ ID: 153,