proxy.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build !plan9
  4. package apiproxy
  5. import (
  6. "bytes"
  7. "context"
  8. "crypto/tls"
  9. "encoding/json"
  10. "errors"
  11. "fmt"
  12. "io"
  13. "net"
  14. "net/http"
  15. "net/http/httputil"
  16. "net/netip"
  17. "net/url"
  18. "strings"
  19. "time"
  20. "go.uber.org/zap"
  21. "k8s.io/apimachinery/pkg/util/sets"
  22. "k8s.io/apiserver/pkg/endpoints/request"
  23. "k8s.io/client-go/rest"
  24. "k8s.io/client-go/transport"
  25. "tailscale.com/client/local"
  26. "tailscale.com/client/tailscale/apitype"
  27. "tailscale.com/envknob"
  28. ksr "tailscale.com/k8s-operator/sessionrecording"
  29. "tailscale.com/kube/kubetypes"
  30. "tailscale.com/net/netx"
  31. "tailscale.com/sessionrecording"
  32. "tailscale.com/tailcfg"
  33. "tailscale.com/tsnet"
  34. "tailscale.com/util/clientmetric"
  35. "tailscale.com/util/ctxkey"
  36. "tailscale.com/util/set"
  37. )
  38. var (
  39. // counterNumRequestsproxies counts the number of API server requests proxied via this proxy.
  40. counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
  41. whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
  42. )
  43. // NewAPIServerProxy creates a new APIServerProxy that's ready to start once Run
  44. // is called. No network traffic will flow until Run is called.
  45. //
  46. // authMode controls how the proxy behaves:
  47. // - true: the proxy is started and requests are impersonated using the
  48. // caller's Tailscale identity and the rules defined in the tailnet ACLs.
  49. // - false: the proxy is started and requests are passed through to the
  50. // Kubernetes API without any auth modifications.
  51. func NewAPIServerProxy(zlog *zap.SugaredLogger, restConfig *rest.Config, ts *tsnet.Server, mode kubetypes.APIServerProxyMode, https bool) (*APIServerProxy, error) {
  52. if mode == kubetypes.APIServerProxyModeNoAuth {
  53. restConfig = rest.AnonymousClientConfig(restConfig)
  54. }
  55. cfg, err := restConfig.TransportConfig()
  56. if err != nil {
  57. return nil, fmt.Errorf("could not get rest.TransportConfig(): %w", err)
  58. }
  59. tr := http.DefaultTransport.(*http.Transport).Clone()
  60. tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
  61. if err != nil {
  62. return nil, fmt.Errorf("could not get transport.TLSConfigFor(): %w", err)
  63. }
  64. tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
  65. rt, err := transport.HTTPWrappersForConfig(cfg, tr)
  66. if err != nil {
  67. return nil, fmt.Errorf("could not get rest.TransportConfig(): %w", err)
  68. }
  69. u, err := url.Parse(restConfig.Host)
  70. if err != nil {
  71. return nil, fmt.Errorf("failed to parse URL %w", err)
  72. }
  73. if u.Scheme == "" || u.Host == "" {
  74. return nil, fmt.Errorf("the API server proxy requires host and scheme but got: %q", restConfig.Host)
  75. }
  76. lc, err := ts.LocalClient()
  77. if err != nil {
  78. return nil, fmt.Errorf("could not get local client: %w", err)
  79. }
  80. ap := &APIServerProxy{
  81. log: zlog,
  82. lc: lc,
  83. authMode: mode == kubetypes.APIServerProxyModeAuth,
  84. https: https,
  85. upstreamURL: u,
  86. ts: ts,
  87. sendEventFunc: sessionrecording.SendEvent,
  88. eventsEnabled: envknob.Bool("TS_EXPERIMENTAL_KUBE_API_EVENTS"),
  89. }
  90. ap.rp = &httputil.ReverseProxy{
  91. Rewrite: func(pr *httputil.ProxyRequest) {
  92. ap.addImpersonationHeadersAsRequired(pr.Out)
  93. },
  94. Transport: rt,
  95. }
  96. return ap, nil
  97. }
  98. // Run starts the HTTP server that authenticates requests using the
  99. // Tailscale LocalAPI and then proxies them to the Kubernetes API.
  100. // It listens on :443 and uses the Tailscale HTTPS certificate.
  101. //
  102. // It return when ctx is cancelled or ServeTLS fails.
  103. func (ap *APIServerProxy) Run(ctx context.Context) error {
  104. mux := http.NewServeMux()
  105. mux.HandleFunc("/", ap.serveDefault)
  106. mux.HandleFunc("POST /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecSPDY)
  107. mux.HandleFunc("GET /api/v1/namespaces/{namespace}/pods/{pod}/exec", ap.serveExecWS)
  108. mux.HandleFunc("POST /api/v1/namespaces/{namespace}/pods/{pod}/attach", ap.serveAttachSPDY)
  109. mux.HandleFunc("GET /api/v1/namespaces/{namespace}/pods/{pod}/attach", ap.serveAttachWS)
  110. ap.hs = &http.Server{
  111. Handler: mux,
  112. ErrorLog: zap.NewStdLog(ap.log.Desugar()),
  113. TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
  114. }
  115. mode := "noauth"
  116. if ap.authMode {
  117. mode = "auth"
  118. }
  119. var proxyLn net.Listener
  120. var serve func(ln net.Listener) error
  121. if ap.https {
  122. var err error
  123. proxyLn, err = ap.ts.Listen("tcp", ":443")
  124. if err != nil {
  125. return fmt.Errorf("could not listen on :443: %w", err)
  126. }
  127. serve = func(ln net.Listener) error {
  128. return ap.hs.ServeTLS(ln, "", "")
  129. }
  130. // Kubernetes uses SPDY for exec and port-forward, however SPDY is
  131. // incompatible with HTTP/2; so disable HTTP/2 in the proxy.
  132. ap.hs.TLSConfig = &tls.Config{
  133. GetCertificate: ap.lc.GetCertificate,
  134. NextProtos: []string{"http/1.1"},
  135. }
  136. } else {
  137. var err error
  138. proxyLn, err = net.Listen("tcp", "localhost:80")
  139. if err != nil {
  140. return fmt.Errorf("could not listen on :80: %w", err)
  141. }
  142. serve = ap.hs.Serve
  143. }
  144. errs := make(chan error)
  145. go func() {
  146. ap.log.Infof("API server proxy in %s mode is listening on %s", mode, proxyLn.Addr())
  147. if err := serve(proxyLn); err != nil && err != http.ErrServerClosed {
  148. errs <- fmt.Errorf("error serving: %w", err)
  149. }
  150. }()
  151. select {
  152. case <-ctx.Done():
  153. case err := <-errs:
  154. ap.hs.Close()
  155. return err
  156. }
  157. // Graceful shutdown with a timeout of 10s.
  158. shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  159. defer cancel()
  160. return ap.hs.Shutdown(shutdownCtx)
  161. }
  162. // APIServerProxy is an [net/http.Handler] that authenticates requests using the Tailscale
  163. // LocalAPI and then proxies them to the Kubernetes API.
  164. type APIServerProxy struct {
  165. log *zap.SugaredLogger
  166. lc *local.Client
  167. rp *httputil.ReverseProxy
  168. authMode bool // Whether to run with impersonation using caller's tailnet identity.
  169. https bool // Whether to serve on https for the device hostname; true for k8s-operator, false (and localhost) for k8s-proxy.
  170. ts *tsnet.Server
  171. hs *http.Server
  172. upstreamURL *url.URL
  173. sendEventFunc func(ap netip.AddrPort, event io.Reader, dial netx.DialFunc) error
  174. // Flag used to enable sending API requests as events to tsrecorder.
  175. eventsEnabled bool
  176. }
  177. // serveDefault is the default handler for Kubernetes API server requests.
  178. func (ap *APIServerProxy) serveDefault(w http.ResponseWriter, r *http.Request) {
  179. who, err := ap.whoIs(r)
  180. if err != nil {
  181. ap.authError(w, err)
  182. return
  183. }
  184. if err = ap.recordRequestAsEvent(r, who); err != nil {
  185. msg := fmt.Sprintf("error recording Kubernetes API request: %v", err)
  186. ap.log.Errorf(msg)
  187. http.Error(w, msg, http.StatusBadGateway)
  188. return
  189. }
  190. counterNumRequestsProxied.Add(1)
  191. ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  192. }
  193. // serveExecSPDY serves '/exec' requests for sessions streamed over SPDY,
  194. // optionally configuring the kubectl exec sessions to be recorded.
  195. func (ap *APIServerProxy) serveExecSPDY(w http.ResponseWriter, r *http.Request) {
  196. ap.sessionForProto(w, r, ksr.ExecSessionType, ksr.SPDYProtocol)
  197. }
  198. // serveExecWS serves '/exec' requests for sessions streamed over WebSocket,
  199. // optionally configuring the kubectl exec sessions to be recorded.
  200. func (ap *APIServerProxy) serveExecWS(w http.ResponseWriter, r *http.Request) {
  201. ap.sessionForProto(w, r, ksr.ExecSessionType, ksr.WSProtocol)
  202. }
  203. // serveAttachSPDY serves '/attach' requests for sessions streamed over SPDY,
  204. // optionally configuring the kubectl exec sessions to be recorded.
  205. func (ap *APIServerProxy) serveAttachSPDY(w http.ResponseWriter, r *http.Request) {
  206. ap.sessionForProto(w, r, ksr.AttachSessionType, ksr.SPDYProtocol)
  207. }
  208. // serveAttachWS serves '/attach' requests for sessions streamed over WebSocket,
  209. // optionally configuring the kubectl exec sessions to be recorded.
  210. func (ap *APIServerProxy) serveAttachWS(w http.ResponseWriter, r *http.Request) {
  211. ap.sessionForProto(w, r, ksr.AttachSessionType, ksr.WSProtocol)
  212. }
  213. func (ap *APIServerProxy) sessionForProto(w http.ResponseWriter, r *http.Request, sessionType ksr.SessionType, proto ksr.Protocol) {
  214. const (
  215. podNameKey = "pod"
  216. namespaceNameKey = "namespace"
  217. upgradeHeaderKey = "Upgrade"
  218. )
  219. who, err := ap.whoIs(r)
  220. if err != nil {
  221. ap.authError(w, err)
  222. return
  223. }
  224. if err = ap.recordRequestAsEvent(r, who); err != nil {
  225. msg := fmt.Sprintf("error recording Kubernetes API request: %v", err)
  226. ap.log.Errorf(msg)
  227. http.Error(w, msg, http.StatusBadGateway)
  228. return
  229. }
  230. counterNumRequestsProxied.Add(1)
  231. failOpen, addrs, err := determineRecorderConfig(who)
  232. if err != nil {
  233. ap.log.Errorf("error trying to determine whether the 'kubectl %s' session needs to be recorded: %v", sessionType, err)
  234. return
  235. }
  236. if failOpen && len(addrs) == 0 { // will not record
  237. ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  238. return
  239. }
  240. ksr.CounterSessionRecordingsAttempted.Add(1) // at this point we know that users intended for this session to be recorded
  241. if !failOpen && len(addrs) == 0 {
  242. msg := fmt.Sprintf("forbidden: 'kubectl %s' session must be recorded, but no recorders are available.", sessionType)
  243. ap.log.Error(msg)
  244. http.Error(w, msg, http.StatusForbidden)
  245. return
  246. }
  247. wantsHeader := upgradeHeaderForProto[proto]
  248. if h := r.Header.Get(upgradeHeaderKey); h != wantsHeader {
  249. msg := fmt.Sprintf("[unexpected] unable to verify that streaming protocol is %s, wants Upgrade header %q, got: %q", proto, wantsHeader, h)
  250. if failOpen {
  251. msg = msg + "; failure mode is 'fail open'; continuing session without recording."
  252. ap.log.Warn(msg)
  253. ap.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  254. return
  255. }
  256. ap.log.Error(msg)
  257. msg += "; failure mode is 'fail closed'; closing connection."
  258. http.Error(w, msg, http.StatusForbidden)
  259. return
  260. }
  261. opts := ksr.HijackerOpts{
  262. Req: r,
  263. W: w,
  264. Proto: proto,
  265. SessionType: sessionType,
  266. TS: ap.ts,
  267. Who: who,
  268. Addrs: addrs,
  269. FailOpen: failOpen,
  270. Pod: r.PathValue(podNameKey),
  271. Namespace: r.PathValue(namespaceNameKey),
  272. Log: ap.log,
  273. }
  274. h := ksr.NewHijacker(opts)
  275. ap.rp.ServeHTTP(h, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
  276. }
  277. func (ap *APIServerProxy) recordRequestAsEvent(req *http.Request, who *apitype.WhoIsResponse) error {
  278. if !ap.eventsEnabled {
  279. return nil
  280. }
  281. failOpen, addrs, err := determineRecorderConfig(who)
  282. if err != nil {
  283. return fmt.Errorf("error trying to determine whether the kubernetes api request needs to be recorded: %w", err)
  284. }
  285. if len(addrs) == 0 {
  286. if failOpen {
  287. return nil
  288. } else {
  289. return fmt.Errorf("forbidden: kubernetes api request must be recorded, but no recorders are available")
  290. }
  291. }
  292. factory := &request.RequestInfoFactory{
  293. APIPrefixes: sets.NewString("api", "apis"),
  294. GrouplessAPIPrefixes: sets.NewString("api"),
  295. }
  296. reqInfo, err := factory.NewRequestInfo(req)
  297. if err != nil {
  298. return fmt.Errorf("error parsing request %s %s: %w", req.Method, req.URL.Path, err)
  299. }
  300. kubeReqInfo := sessionrecording.KubernetesRequestInfo{
  301. IsResourceRequest: reqInfo.IsResourceRequest,
  302. Path: reqInfo.Path,
  303. Verb: reqInfo.Verb,
  304. APIPrefix: reqInfo.APIPrefix,
  305. APIGroup: reqInfo.APIGroup,
  306. APIVersion: reqInfo.APIVersion,
  307. Namespace: reqInfo.Namespace,
  308. Resource: reqInfo.Resource,
  309. Subresource: reqInfo.Subresource,
  310. Name: reqInfo.Name,
  311. Parts: reqInfo.Parts,
  312. FieldSelector: reqInfo.FieldSelector,
  313. LabelSelector: reqInfo.LabelSelector,
  314. }
  315. event := &sessionrecording.Event{
  316. Timestamp: time.Now().Unix(),
  317. Kubernetes: kubeReqInfo,
  318. Type: sessionrecording.KubernetesAPIEventType,
  319. UserAgent: req.UserAgent(),
  320. Request: sessionrecording.Request{
  321. Method: req.Method,
  322. Path: req.URL.String(),
  323. QueryParameters: req.URL.Query(),
  324. },
  325. Source: sessionrecording.Source{
  326. NodeID: who.Node.StableID,
  327. Node: strings.TrimSuffix(who.Node.Name, "."),
  328. },
  329. }
  330. if !who.Node.IsTagged() {
  331. event.Source.NodeUser = who.UserProfile.LoginName
  332. event.Source.NodeUserID = who.UserProfile.ID
  333. } else {
  334. event.Source.NodeTags = who.Node.Tags
  335. }
  336. bodyBytes, err := io.ReadAll(req.Body)
  337. if err != nil {
  338. return fmt.Errorf("failed to read body: %w", err)
  339. }
  340. req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
  341. event.Request.Body = bodyBytes
  342. var errs []error
  343. // TODO: ChaosInTheCRD ensure that if there are multiple addrs timing out we don't experience slowdown on client waiting for response.
  344. fail := true
  345. for _, addr := range addrs {
  346. data := new(bytes.Buffer)
  347. if err := json.NewEncoder(data).Encode(event); err != nil {
  348. return fmt.Errorf("error marshaling request event: %w", err)
  349. }
  350. if err := ap.sendEventFunc(addr, data, ap.ts.Dial); err != nil {
  351. if apiSupportErr, ok := err.(sessionrecording.EventAPINotSupportedErr); ok {
  352. ap.log.Warnf(apiSupportErr.Error())
  353. fail = false
  354. } else {
  355. err := fmt.Errorf("error sending event to recorder with address %q: %v", addr.String(), err)
  356. errs = append(errs, err)
  357. }
  358. } else {
  359. return nil
  360. }
  361. }
  362. merr := errors.Join(errs...)
  363. if fail && failOpen {
  364. msg := fmt.Sprintf("[unexpected] failed to send event to recorders with errors: %s", merr.Error())
  365. msg = msg + "; failure mode is 'fail open'; continuing request without recording."
  366. ap.log.Warn(msg)
  367. return nil
  368. }
  369. return merr
  370. }
  371. func (ap *APIServerProxy) addImpersonationHeadersAsRequired(r *http.Request) {
  372. r.URL.Scheme = ap.upstreamURL.Scheme
  373. r.URL.Host = ap.upstreamURL.Host
  374. if !ap.authMode {
  375. // If we are not providing authentication, then we are just
  376. // proxying to the Kubernetes API, so we don't need to do
  377. // anything else.
  378. return
  379. }
  380. // We want to proxy to the Kubernetes API, but we want to use
  381. // the caller's identity to do so. We do this by impersonating
  382. // the caller using the Kubernetes User Impersonation feature:
  383. // https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
  384. // Out of paranoia, remove all authentication headers that might
  385. // have been set by the client.
  386. r.Header.Del("Authorization")
  387. r.Header.Del("Impersonate-Group")
  388. r.Header.Del("Impersonate-User")
  389. r.Header.Del("Impersonate-Uid")
  390. for k := range r.Header {
  391. if strings.HasPrefix(k, "Impersonate-Extra-") {
  392. r.Header.Del(k)
  393. }
  394. }
  395. // Now add the impersonation headers that we want.
  396. if err := addImpersonationHeaders(r, ap.log); err != nil {
  397. ap.log.Errorf("failed to add impersonation headers: %v", err)
  398. }
  399. }
  400. func (ap *APIServerProxy) whoIs(r *http.Request) (*apitype.WhoIsResponse, error) {
  401. who, remoteErr := ap.lc.WhoIs(r.Context(), r.RemoteAddr)
  402. if remoteErr == nil {
  403. ap.log.Debugf("WhoIs from remote addr: %s", r.RemoteAddr)
  404. return who, nil
  405. }
  406. var fwdErr error
  407. fwdFor := r.Header.Get("X-Forwarded-For")
  408. if fwdFor != "" && !ap.https {
  409. who, fwdErr = ap.lc.WhoIs(r.Context(), fwdFor)
  410. if fwdErr == nil {
  411. ap.log.Debugf("WhoIs from X-Forwarded-For header: %s", fwdFor)
  412. return who, nil
  413. }
  414. }
  415. return nil, errors.Join(remoteErr, fwdErr)
  416. }
  417. func (ap *APIServerProxy) authError(w http.ResponseWriter, err error) {
  418. ap.log.Errorf("failed to authenticate caller: %v", err)
  419. http.Error(w, "failed to authenticate caller", http.StatusInternalServerError)
  420. }
  421. const (
  422. // oldCapabilityName is a legacy form of
  423. // tailfcg.PeerCapabilityKubernetes capability. The only capability rule
  424. // that is respected for this form is group impersonation - for
  425. // backwards compatibility reasons.
  426. // TODO (irbekrm): determine if anyone uses this and remove if possible.
  427. oldCapabilityName = "https://" + tailcfg.PeerCapabilityKubernetes
  428. )
  429. // addImpersonationHeaders adds the appropriate headers to r to impersonate the
  430. // caller when proxying to the Kubernetes API. It uses the WhoIsResponse stashed
  431. // in the context by the apiserverProxy.
  432. func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
  433. log = log.With("remote", r.RemoteAddr)
  434. who := whoIsKey.Value(r.Context())
  435. rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
  436. if len(rules) == 0 && err == nil {
  437. // Try the old capability name for backwards compatibility.
  438. rules, err = tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, oldCapabilityName)
  439. }
  440. if err != nil {
  441. return fmt.Errorf("failed to unmarshal capability: %v", err)
  442. }
  443. var groupsAdded set.Slice[string]
  444. for _, rule := range rules {
  445. if rule.Impersonate == nil {
  446. continue
  447. }
  448. for _, group := range rule.Impersonate.Groups {
  449. if groupsAdded.Contains(group) {
  450. continue
  451. }
  452. r.Header.Add("Impersonate-Group", group)
  453. groupsAdded.Add(group)
  454. log.Debugf("adding group impersonation header for user group %s", group)
  455. }
  456. }
  457. if !who.Node.IsTagged() {
  458. r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
  459. log.Debugf("adding user impersonation header for user %s", who.UserProfile.LoginName)
  460. return nil
  461. }
  462. // "Impersonate-Group" requires "Impersonate-User" to be set, so we set it
  463. // to the node FQDN for tagged nodes.
  464. nodeName := strings.TrimSuffix(who.Node.Name, ".")
  465. r.Header.Set("Impersonate-User", nodeName)
  466. log.Debugf("adding user impersonation header for node name %s", nodeName)
  467. // For legacy behavior (before caps), set the groups to the nodes tags.
  468. if groupsAdded.Slice().Len() == 0 {
  469. for _, tag := range who.Node.Tags {
  470. r.Header.Add("Impersonate-Group", tag)
  471. log.Debugf("adding group impersonation header for node tag %s", tag)
  472. }
  473. }
  474. return nil
  475. }
  476. // determineRecorderConfig determines recorder config from requester's peer
  477. // capabilities. Determines whether a 'kubectl exec' session from this requester
  478. // needs to be recorded and what recorders the recording should be sent to.
  479. func determineRecorderConfig(who *apitype.WhoIsResponse) (failOpen bool, recorderAddresses []netip.AddrPort, _ error) {
  480. if who == nil {
  481. return false, nil, errors.New("[unexpected] cannot determine caller")
  482. }
  483. failOpen = true
  484. rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes)
  485. if err != nil {
  486. return failOpen, nil, fmt.Errorf("failed to unmarshal Kubernetes capability: %w", err)
  487. }
  488. if len(rules) == 0 {
  489. return failOpen, nil, nil
  490. }
  491. for _, rule := range rules {
  492. if len(rule.RecorderAddrs) != 0 {
  493. // TODO (irbekrm): here or later determine if the
  494. // recorders behind those addrs are online - else we
  495. // spend 30s trying to reach a recorder whose tailscale
  496. // status is offline.
  497. recorderAddresses = append(recorderAddresses, rule.RecorderAddrs...)
  498. }
  499. if rule.EnforceRecorder {
  500. failOpen = false
  501. }
  502. }
  503. return failOpen, recorderAddresses, nil
  504. }
  505. var upgradeHeaderForProto = map[ksr.Protocol]string{
  506. ksr.SPDYProtocol: "SPDY/3.1",
  507. ksr.WSProtocol: "websocket",
  508. }