ipn/ipnlocal/serve: remove grant header truncation logic

Given that we filter based on the usercaps argument now, truncation
should not be necessary anymore.

Updates tailscale/corp/#28372

Signed-off-by: Gesa Stupperich <gesa@tailscale.com>
pull/17559/merge
Gesa Stupperich 2 months ago committed by Gesa Stupperich
parent 576aacd459
commit d6fa899eba

@ -173,7 +173,7 @@ type serveEnv struct {
service tailcfg.ServiceName // service name
tun bool // redirect traffic to OS for service
allServices bool // apply config file to all services
userCaps []tailcfg.PeerCapability // user capabilities to forward
acceptAppCaps []tailcfg.PeerCapability // app capabilities to forward
lc localServeClient // localClient interface, specific to serve
// optional stuff for tests:

@ -96,12 +96,12 @@ func (b *bgBoolFlag) String() string {
return strconv.FormatBool(b.Value)
}
type userCapsFlag struct {
type acceptAppCapsFlag struct {
Value *[]tailcfg.PeerCapability
}
// Set appends s to the list of userCaps.
func (u *userCapsFlag) Set(s string) error {
// Set appends s to the list of appCaps to accept.
func (u *acceptAppCapsFlag) Set(s string) error {
if s == "" {
return nil
}
@ -109,8 +109,8 @@ func (u *userCapsFlag) Set(s string) error {
return nil
}
// String returns the string representation of the userCaps slice.
func (u *userCapsFlag) String() string {
// String returns the string representation of the slice of appCaps to accept.
func (u *acceptAppCapsFlag) String() string {
s := make([]string, len(*u.Value))
for i, v := range *u.Value {
s[i] = string(v)
@ -221,7 +221,7 @@ func newServeV2Command(e *serveEnv, subcmd serveMode) *ffcli.Command {
fs.UintVar(&e.https, "https", 0, "Expose an HTTPS server at the specified port (default mode)")
if subcmd == serve {
fs.UintVar(&e.http, "http", 0, "Expose an HTTP server at the specified port")
fs.Var(&userCapsFlag{Value: &e.userCaps}, "usercaps", "User capability to forward to the server (can be specified multiple times)")
fs.Var(&acceptAppCapsFlag{Value: &e.acceptAppCaps}, "accept-app-caps", "App capability to forward to the server (can be specified multiple times)")
}
fs.UintVar(&e.tcp, "tcp", 0, "Expose a TCP forwarder to forward raw TCP packets at the specified port")
fs.UintVar(&e.tlsTerminatedTCP, "tls-terminated-tcp", 0, "Expose a TCP forwarder to forward TLS-terminated TCP packets at the specified port")
@ -492,7 +492,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc {
if len(args) > 0 {
target = args[0]
}
err = e.setServe(sc, dnsName, srvType, srvPort, mount, target, funnel, magicDNSSuffix, e.userCaps)
err = e.setServe(sc, dnsName, srvType, srvPort, mount, target, funnel, magicDNSSuffix, e.acceptAppCaps)
msg = e.messageForPort(sc, st, dnsName, srvType, srvPort)
}
if err != nil {
@ -1141,7 +1141,7 @@ func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort ui
return err
}
h.Proxy = t
h.UserCaps = caps
h.AcceptAppCaps = caps
}
// TODO: validation needs to check nested foreground configs

@ -861,42 +861,42 @@ func TestServeDevConfigMutations(t *testing.T) {
name: "forward_grant_header",
steps: []step{
{
command: cmd("serve --bg --usercaps=example.com/cap/foo 3000"),
command: cmd("serve --bg --accept-app-caps=example.com/cap/foo 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {
Proxy: "http://127.0.0.1:3000",
UserCaps: []tailcfg.PeerCapability{"example.com/cap/foo"},
Proxy: "http://127.0.0.1:3000",
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo"},
},
}},
},
},
},
{
command: cmd("serve --bg --usercaps=example.com/cap/foo --usercaps=example.com/cap/bar 3000"),
command: cmd("serve --bg --accept-app-caps=example.com/cap/foo --accept-app-caps=example.com/cap/bar 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {
Proxy: "http://127.0.0.1:3000",
UserCaps: []tailcfg.PeerCapability{"example.com/cap/foo", "example.com/cap/bar"},
Proxy: "http://127.0.0.1:3000",
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo", "example.com/cap/bar"},
},
}},
},
},
},
{
command: cmd("serve --bg --usercaps=example.com/cap/bar 3000"),
command: cmd("serve --bg --accept-app-caps=example.com/cap/bar 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {
Proxy: "http://127.0.0.1:3000",
UserCaps: []tailcfg.PeerCapability{"example.com/cap/bar"},
Proxy: "http://127.0.0.1:3000",
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/bar"},
},
}},
},

@ -232,16 +232,16 @@ func (src *HTTPHandler) Clone() *HTTPHandler {
}
dst := new(HTTPHandler)
*dst = *src
dst.UserCaps = append(src.UserCaps[:0:0], src.UserCaps...)
dst.AcceptAppCaps = append(src.AcceptAppCaps[:0:0], src.AcceptAppCaps...)
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HTTPHandlerCloneNeedsRegeneration = HTTPHandler(struct {
Path string
Proxy string
Text string
UserCaps []tailcfg.PeerCapability
Path string
Proxy string
Text string
AcceptAppCaps []tailcfg.PeerCapability
}{})
// Clone makes a deep copy of WebServerConfig.

@ -892,16 +892,16 @@ func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy }
func (v HTTPHandlerView) Text() string { return v.ж.Text }
// peer capabilities to forward in grant header, e.g. example.com/cap/mon
func (v HTTPHandlerView) UserCaps() views.Slice[tailcfg.PeerCapability] {
return views.SliceOf(v.ж.UserCaps)
func (v HTTPHandlerView) AcceptAppCaps() views.Slice[tailcfg.PeerCapability] {
return views.SliceOf(v.ж.AcceptAppCaps)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct {
Path string
Proxy string
Text string
UserCaps []tailcfg.PeerCapability
Path string
Proxy string
Text string
AcceptAppCaps []tailcfg.PeerCapability
}{})
// View returns a read-only view of WebServerConfig.

@ -65,7 +65,6 @@ func init() {
const (
contentTypeHeader = "Content-Type"
grpcBaseContentType = "application/grpc"
grantHeaderMaxSize = 15360 // 15 KiB
)
// ErrETagMismatch signals that the given
@ -932,7 +931,7 @@ func encTailscaleHeaderValue(v string) string {
}
func (b *LocalBackend) addTailscaleGrantHeader(r *httputil.ProxyRequest) {
r.Out.Header.Del("Tailscale-User-Capabilities")
r.Out.Header.Del("Tailscale-App-Capabilities")
c, ok := serveHTTPContextKey.ValueOk(r.Out.Context())
if !ok || c.Funnel != nil {
@ -954,37 +953,13 @@ func (b *LocalBackend) addTailscaleGrantHeader(r *httputil.ProxyRequest) {
}
}
serialized, truncated, err := serializeUpToNBytes(peerCapsFiltered, grantHeaderMaxSize)
peerCapsSerialized, err := json.Marshal(peerCapsFiltered)
if err != nil {
b.logf("serve: failed to serialize PeerCapMap: %v", err)
b.logf("serve: failed to serialize filtered PeerCapMap: %v", err)
return
}
if truncated {
b.logf("serve: serialized PeerCapMap exceeds %d bytes, forwarding truncated PeerCapMap", grantHeaderMaxSize)
}
r.Out.Header.Set("Tailscale-User-Capabilities", encTailscaleHeaderValue(serialized))
}
// serializeUpToNBytes serializes capMap. It arbitrarily truncates entries from the capMap
// if the size of the serialized capMap would exceed N bytes.
func serializeUpToNBytes(capMap tailcfg.PeerCapMap, N int) (string, bool, error) {
numBytes := 0
capped := false
result := tailcfg.PeerCapMap{}
for k, v := range capMap {
numBytes += len(k) + len(v)
if numBytes > N {
capped = true
break
}
result[k] = v
}
marshalled, err := json.Marshal(result)
if err != nil {
return "", false, err
}
return string(marshalled), capped, nil
r.Out.Header.Set("Tailscale-App-Capabilities", encTailscaleHeaderValue(string(peerCapsSerialized)))
}
// serveWebHandler is an http.HandlerFunc that maps incoming requests to the
@ -1010,12 +985,12 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "unknown proxy destination", http.StatusInternalServerError)
return
}
// Inject user capabilities to forward into the request context
// Inject app capabilities to forward into the request context
c, ok := serveHTTPContextKey.ValueOk(r.Context())
if !ok {
return
}
c.PeerCapsFilter = h.UserCaps()
c.PeerCapsFilter = h.AcceptAppCaps()
h := p.(http.Handler)
// Trim the mount point from the URL path before proxying. (#6571)
if r.URL.Path != "/" {

@ -828,8 +828,8 @@ func TestServeHTTPProxyGrantHeader(t *testing.T) {
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {
Proxy: testServ.URL,
UserCaps: []tailcfg.PeerCapability{"example.com/cap/interesting", "example.com/cap/boring"},
Proxy: testServ.URL,
AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/interesting", "example.com/cap/boring"},
},
}},
},
@ -858,7 +858,7 @@ func TestServeHTTPProxyGrantHeader(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-Capabilities", `{"example.com/cap/interesting":[{"role":"🐿"}]}`},
{"Tailscale-App-Capabilities", `{"example.com/cap/interesting":[{"role":"🐿"}]}`},
},
},
{
@ -871,7 +871,7 @@ func TestServeHTTPProxyGrantHeader(t *testing.T) {
{"Tailscale-User-Name", ""},
{"Tailscale-User-Profile-Pic", ""},
{"Tailscale-Headers-Info", ""},
{"Tailscale-User-Capabilities", `{"example.com/cap/boring":[{"role":"Viewer"}]}`},
{"Tailscale-App-Capabilities", `{"example.com/cap/boring":[{"role":"Viewer"}]}`},
},
},
{
@ -884,7 +884,7 @@ func TestServeHTTPProxyGrantHeader(t *testing.T) {
{"Tailscale-User-Name", ""},
{"Tailscale-User-Profile-Pic", ""},
{"Tailscale-Headers-Info", ""},
{"Tailscale-User-Capabilities", ""},
{"Tailscale-App-Capabilities", ""},
},
},
}
@ -1327,89 +1327,3 @@ func TestServeGRPCProxy(t *testing.T) {
})
}
}
func TestSerialisePeerCapMap(t *testing.T) {
var tests = []struct {
name string
capMap tailcfg.PeerCapMap
maxNumBytes int
wantOneOfSerialized []string
wantTruncated bool
}{
{
name: "empty cap map",
capMap: tailcfg.PeerCapMap{},
maxNumBytes: 50,
wantOneOfSerialized: []string{"{}"},
wantTruncated: false,
},
{
name: "cap map with one capability",
capMap: tailcfg.PeerCapMap{
"tailscale.com/cap/kubernetes": []tailcfg.RawMessage{
`{"impersonate": {"groups": ["tailnet-readers"]}}`,
},
},
maxNumBytes: 50,
wantOneOfSerialized: []string{
`{"tailscale.com/cap/kubernetes":[{"impersonate":{"groups":["tailnet-readers"]}}]}`,
},
wantTruncated: false,
},
{
name: "cap map with two capabilities",
capMap: tailcfg.PeerCapMap{
"foo.com/cap/something": []tailcfg.RawMessage{
`{"role": "Admin"}`,
},
"bar.com/cap/other-thing": []tailcfg.RawMessage{
`{"role": "Viewer"}`,
},
},
maxNumBytes: 50,
// Both cap map entries will be included, but they could appear in any order.
wantOneOfSerialized: []string{
`{"foo.com/cap/something":[{"role":"Admin"}],"bar.com/cap/other-thing":[{"role":"Viewer"}]}`,
`{"bar.com/cap/other-thing":[{"role":"Viewer"}],"foo.com/cap/something":[{"role":"Admin"}]}`,
},
wantTruncated: false,
},
{
name: "cap map that should be truncated to stay within size limits",
capMap: tailcfg.PeerCapMap{
"foo.com/cap/something": []tailcfg.RawMessage{
`{"role": "Admin"}`,
},
"bar.com/cap/other-thing": []tailcfg.RawMessage{
`{"role": "Viewer"}`,
},
},
maxNumBytes: 40,
// Only one cap map entry will be included, but we don't know which one.
wantOneOfSerialized: []string{
`{"foo.com/cap/something":[{"role":"Admin"}]}`,
`{"bar.com/cap/other-thing":[{"role":"Viewer"}]}`,
},
wantTruncated: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotSerialized, gotCapped, err := serializeUpToNBytes(tt.capMap, tt.maxNumBytes)
if err != nil {
t.Fatal(err)
}
if gotCapped != tt.wantTruncated {
t.Errorf("got %t, want %t", gotCapped, tt.wantTruncated)
}
for _, wantSerialized := range tt.wantOneOfSerialized {
if gotSerialized == wantSerialized {
return
}
}
t.Errorf("want one of %v, got %q", tt.wantOneOfSerialized, gotSerialized)
})
}
}

@ -160,7 +160,7 @@ type HTTPHandler struct {
Text string `json:",omitempty"` // plaintext to serve (primarily for testing)
UserCaps []tailcfg.PeerCapability `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon
AcceptAppCaps []tailcfg.PeerCapability `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon
// TODO(bradfitz): bool to not enumerate directories? TTL on mapping for
// temporary ones? Error codes? Redirects?

Loading…
Cancel
Save