mirror of
https://github.com/tailscale/tailscale.git
synced 2025-12-05 01:12:11 +01:00
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>
This commit is contained in:
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…
x
Reference in New Issue
Block a user