web.go 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package web provides the Tailscale client for web.
  4. package web
  5. import (
  6. "cmp"
  7. "context"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "log"
  13. "net/http"
  14. "net/netip"
  15. "net/url"
  16. "os"
  17. "path"
  18. "slices"
  19. "strings"
  20. "sync"
  21. "time"
  22. "tailscale.com/client/local"
  23. "tailscale.com/client/tailscale/apitype"
  24. "tailscale.com/envknob"
  25. "tailscale.com/envknob/featureknob"
  26. "tailscale.com/feature"
  27. "tailscale.com/feature/buildfeatures"
  28. "tailscale.com/hostinfo"
  29. "tailscale.com/ipn"
  30. "tailscale.com/ipn/ipnstate"
  31. "tailscale.com/licenses"
  32. "tailscale.com/net/netutil"
  33. "tailscale.com/net/tsaddr"
  34. "tailscale.com/tailcfg"
  35. "tailscale.com/types/logger"
  36. "tailscale.com/types/views"
  37. "tailscale.com/util/httpm"
  38. "tailscale.com/util/syspolicy/policyclient"
  39. "tailscale.com/version"
  40. "tailscale.com/version/distro"
  41. )
  42. // ListenPort is the static port used for the web client when run inside tailscaled.
  43. // (5252 are the numbers above the letters "TSTS" on a qwerty keyboard.)
  44. const ListenPort = 5252
  45. // Server is the backend server for a Tailscale web client.
  46. type Server struct {
  47. mode ServerMode
  48. logf logger.Logf
  49. polc policyclient.Client // must be non-nil
  50. lc *local.Client
  51. timeNow func() time.Time
  52. // devMode indicates that the server run with frontend assets
  53. // served by a Vite dev server, allowing for local development
  54. // on the web client frontend.
  55. devMode bool
  56. cgiMode bool
  57. pathPrefix string
  58. // originOverride is the origin that the web UI is accessible from.
  59. // This value is used in the fallback CSRF checks when Sec-Fetch-Site is not
  60. // available. In this case the application will compare Host and Origin
  61. // header values to determine if the request is from the same origin.
  62. originOverride string
  63. apiHandler http.Handler // serves api endpoints; csrf-protected
  64. assetsHandler http.Handler // serves frontend assets
  65. assetsCleanup func() // called from Server.Shutdown
  66. // browserSessions is an in-memory cache of browser sessions for the
  67. // full management web client, which is only accessible over Tailscale.
  68. //
  69. // Users obtain a valid browser session by connecting to the web client
  70. // over Tailscale and verifying their identity by authenticating on the
  71. // control server.
  72. //
  73. // browserSessions get reset on every Server restart.
  74. //
  75. // The map provides a lookup of the session by cookie value
  76. // (browserSession.ID => browserSession).
  77. browserSessions sync.Map
  78. // newAuthURL creates a new auth URL that can be used to validate
  79. // a browser session to manage this web client.
  80. newAuthURL func(ctx context.Context, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
  81. // waitWebClientAuthURL blocks until the associated auth URL has
  82. // been completed by its user, or until ctx is canceled.
  83. waitAuthURL func(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
  84. }
  85. // ServerMode specifies the mode of a running web.Server.
  86. type ServerMode string
  87. const (
  88. // LoginServerMode serves a read-only login client for logging a
  89. // node into a tailnet, and viewing a read-only interface of the
  90. // node's current Tailscale settings.
  91. //
  92. // In this mode, API calls are authenticated via platform auth.
  93. LoginServerMode ServerMode = "login"
  94. // ReadOnlyServerMode is identical to LoginServerMode,
  95. // but does not present a login button to switch to manage mode,
  96. // even if the management client is running and reachable.
  97. //
  98. // This is designed for platforms where the device is configured by other means,
  99. // such as Home Assistant's declarative YAML configuration.
  100. ReadOnlyServerMode ServerMode = "readonly"
  101. // ManageServerMode serves a management client for editing tailscale
  102. // settings of a node.
  103. //
  104. // This mode restricts the app to only being assessible over Tailscale,
  105. // and API calls are authenticated via browser sessions associated with
  106. // the source's Tailscale identity. If the source browser does not have
  107. // a valid session, a read-only version of the app is displayed.
  108. ManageServerMode ServerMode = "manage"
  109. )
  110. // ServerOpts contains options for constructing a new Server.
  111. type ServerOpts struct {
  112. // Mode specifies the mode of web client being constructed.
  113. Mode ServerMode
  114. // CGIMode indicates if the server is running as a CGI script.
  115. CGIMode bool
  116. // PathPrefix is the URL prefix added to requests by CGI or reverse proxy.
  117. PathPrefix string
  118. // LocalClient is the local.Client to use for this web server.
  119. // If nil, a new one will be created.
  120. LocalClient *local.Client
  121. // TimeNow optionally provides a time function.
  122. // time.Now is used as default.
  123. TimeNow func() time.Time
  124. // Logf optionally provides a logger function.
  125. // If nil, log.Printf is used as default.
  126. Logf logger.Logf
  127. // PolicyClient, if non-nil, will be used to fetch policy settings.
  128. // If nil, the default policy client will be used.
  129. PolicyClient policyclient.Client
  130. // The following two fields are required and used exclusively
  131. // in ManageServerMode to facilitate the control server login
  132. // check step for authorizing browser sessions.
  133. // NewAuthURL should be provided as a function that generates
  134. // a new tailcfg.WebClientAuthResponse.
  135. // This field is required for ManageServerMode mode.
  136. NewAuthURL func(ctx context.Context, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
  137. // WaitAuthURL should be provided as a function that blocks until
  138. // the associated tailcfg.WebClientAuthResponse has been marked
  139. // as completed.
  140. // This field is required for ManageServerMode mode.
  141. WaitAuthURL func(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
  142. // OriginOverride specifies the origin that the web UI will be accessible from if hosted behind a reverse proxy or CGI.
  143. OriginOverride string
  144. }
  145. // NewServer constructs a new Tailscale web client server.
  146. // If err is empty, s is always non-nil.
  147. // ctx is only required to live the duration of the NewServer call,
  148. // and not the lifespan of the web server.
  149. func NewServer(opts ServerOpts) (s *Server, err error) {
  150. switch opts.Mode {
  151. case LoginServerMode, ReadOnlyServerMode, ManageServerMode:
  152. // valid types
  153. case "":
  154. return nil, fmt.Errorf("must specify a Mode")
  155. default:
  156. return nil, fmt.Errorf("invalid Mode provided")
  157. }
  158. if opts.LocalClient == nil {
  159. opts.LocalClient = &local.Client{}
  160. }
  161. s = &Server{
  162. mode: opts.Mode,
  163. polc: cmp.Or(opts.PolicyClient, policyclient.Get()),
  164. logf: opts.Logf,
  165. devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
  166. lc: opts.LocalClient,
  167. cgiMode: opts.CGIMode,
  168. pathPrefix: opts.PathPrefix,
  169. timeNow: opts.TimeNow,
  170. newAuthURL: opts.NewAuthURL,
  171. waitAuthURL: opts.WaitAuthURL,
  172. originOverride: opts.OriginOverride,
  173. }
  174. if opts.PathPrefix != "" {
  175. // Enforce that path prefix always has a single leading '/'
  176. // so that it is treated as a relative URL path.
  177. // We strip multiple leading '/' to prevent schema-less offsite URLs like "//example.com".
  178. //
  179. // See https://github.com/tailscale/corp/issues/16268.
  180. s.pathPrefix = "/" + strings.TrimLeft(path.Clean(opts.PathPrefix), "/\\")
  181. }
  182. if s.mode == ManageServerMode {
  183. if opts.NewAuthURL == nil {
  184. return nil, fmt.Errorf("must provide a NewAuthURL implementation")
  185. }
  186. if opts.WaitAuthURL == nil {
  187. return nil, fmt.Errorf("must provide WaitAuthURL implementation")
  188. }
  189. }
  190. if s.timeNow == nil {
  191. s.timeNow = time.Now
  192. }
  193. if s.logf == nil {
  194. s.logf = log.Printf
  195. }
  196. s.assetsHandler, s.assetsCleanup = assetsHandler(s.devMode)
  197. var metric string
  198. s.apiHandler, metric = s.modeAPIHandler(s.mode)
  199. s.apiHandler = s.csrfProtect(s.apiHandler)
  200. // Don't block startup on reporting metric.
  201. // Report in separate go routine with 5 second timeout.
  202. go func() {
  203. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  204. defer cancel()
  205. s.lc.IncrementCounter(ctx, metric, 1)
  206. }()
  207. return s, nil
  208. }
  209. func (s *Server) csrfProtect(h http.Handler) http.Handler {
  210. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  211. // CSRF is not required for GET, HEAD, or OPTIONS requests.
  212. if slices.Contains([]string{"GET", "HEAD", "OPTIONS"}, r.Method) {
  213. h.ServeHTTP(w, r)
  214. return
  215. }
  216. // first attempt to use Sec-Fetch-Site header (sent by all modern
  217. // browsers to "potentially trustworthy" origins i.e. localhost or those
  218. // served over HTTPS)
  219. secFetchSite := r.Header.Get("Sec-Fetch-Site")
  220. if secFetchSite == "same-origin" {
  221. h.ServeHTTP(w, r)
  222. return
  223. } else if secFetchSite != "" {
  224. http.Error(w, fmt.Sprintf("CSRF request denied with Sec-Fetch-Site %q", secFetchSite), http.StatusForbidden)
  225. return
  226. }
  227. // if Sec-Fetch-Site is not available we presume we are operating over HTTP.
  228. // We fall back to comparing the Origin & Host headers.
  229. // use the Host header to determine the expected origin
  230. // (use the override if set to allow for reverse proxying)
  231. host := r.Host
  232. if host == "" {
  233. http.Error(w, "CSRF request denied with no Host header", http.StatusForbidden)
  234. return
  235. }
  236. if s.originOverride != "" {
  237. host = s.originOverride
  238. }
  239. originHeader := r.Header.Get("Origin")
  240. if originHeader == "" {
  241. http.Error(w, "CSRF request denied with no Origin header", http.StatusForbidden)
  242. return
  243. }
  244. parsedOrigin, err := url.Parse(originHeader)
  245. if err != nil {
  246. http.Error(w, fmt.Sprintf("CSRF request denied with invalid Origin %q", r.Header.Get("Origin")), http.StatusForbidden)
  247. return
  248. }
  249. origin := parsedOrigin.Host
  250. if origin == "" {
  251. http.Error(w, "CSRF request denied with no host in the Origin header", http.StatusForbidden)
  252. return
  253. }
  254. if origin != host {
  255. http.Error(w, fmt.Sprintf("CSRF request denied with mismatched Origin %q and Host %q", origin, host), http.StatusForbidden)
  256. return
  257. }
  258. h.ServeHTTP(w, r)
  259. })
  260. }
  261. func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
  262. switch mode {
  263. case LoginServerMode:
  264. return http.HandlerFunc(s.serveLoginAPI), "web_login_client_initialization"
  265. case ReadOnlyServerMode:
  266. return http.HandlerFunc(s.serveLoginAPI), "web_readonly_client_initialization"
  267. case ManageServerMode:
  268. return http.HandlerFunc(s.serveAPI), "web_client_initialization"
  269. default: // invalid mode
  270. log.Fatalf("invalid mode: %v", mode)
  271. }
  272. return nil, ""
  273. }
  274. func (s *Server) Shutdown() {
  275. s.logf("web.Server: shutting down")
  276. if s.assetsCleanup != nil {
  277. s.assetsCleanup()
  278. }
  279. }
  280. // ServeHTTP processes all requests for the Tailscale web client.
  281. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  282. handler := s.serve
  283. // if path prefix is defined, strip it from requests.
  284. if s.cgiMode && s.pathPrefix != "" {
  285. handler = enforcePrefix(s.pathPrefix, handler)
  286. }
  287. handler(w, r)
  288. }
  289. func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
  290. if s.mode == ManageServerMode {
  291. // In manage mode, requests must be sent directly to the bare Tailscale IP address.
  292. // If a request comes in on any other hostname, redirect.
  293. if s.requireTailscaleIP(w, r) {
  294. return // user was redirected
  295. }
  296. // serve HTTP 204 on /ok requests as connectivity check
  297. if r.Method == httpm.GET && r.URL.Path == "/ok" {
  298. w.WriteHeader(http.StatusNoContent)
  299. return
  300. }
  301. if !s.devMode {
  302. // This hash corresponds to the inline script in index.html that runs when the react app is unavailable.
  303. // It was generated from https://csplite.com/csp/sha/.
  304. // If the contents of the script are changed, this hash must be updated.
  305. const indexScriptHash = "sha384-CW2AYVfS14P7QHZN27thEkMLKiCj3YNURPoLc1elwiEkMVHeuYTWkJOEki1F3nZc"
  306. w.Header().Set("X-Frame-Options", "DENY")
  307. w.Header().Set("Content-Security-Policy", "default-src 'self'; img-src * data:; script-src 'self' '"+indexScriptHash+"'")
  308. w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
  309. }
  310. }
  311. if r.URL.Path == "/metrics" {
  312. r.URL.Path = "/api/local/v0/usermetrics"
  313. s.proxyRequestToLocalAPI(w, r)
  314. return
  315. }
  316. if strings.HasPrefix(r.URL.Path, "/api/") {
  317. switch {
  318. case r.URL.Path == "/api/auth" && r.Method == httpm.GET:
  319. s.serveAPIAuth(w, r) // serve auth status
  320. return
  321. case r.URL.Path == "/api/auth/session/new" && r.Method == httpm.GET:
  322. s.serveAPIAuthSessionNew(w, r) // create new session
  323. return
  324. case r.URL.Path == "/api/auth/session/wait" && r.Method == httpm.GET:
  325. s.serveAPIAuthSessionWait(w, r) // wait for session to be authorized
  326. return
  327. }
  328. if ok := s.authorizeRequest(w, r); !ok {
  329. http.Error(w, "not authorized", http.StatusUnauthorized)
  330. return
  331. }
  332. // Pass API requests through to the API handler.
  333. s.apiHandler.ServeHTTP(w, r)
  334. return
  335. }
  336. s.assetsHandler.ServeHTTP(w, r)
  337. }
  338. // requireTailscaleIP redirects an incoming request if the HTTP request was not made to a bare Tailscale IP address.
  339. // The request will be redirected to the Tailscale IP, port 5252, with the original request path.
  340. // This allows any custom hostname to be used to access the device, but protects against DNS rebinding attacks.
  341. // Returns true if the request has been fully handled, either be returning a redirect or an HTTP error.
  342. func (s *Server) requireTailscaleIP(w http.ResponseWriter, r *http.Request) (handled bool) {
  343. const (
  344. ipv4ServiceHost = tsaddr.TailscaleServiceIPString
  345. ipv6ServiceHost = "[" + tsaddr.TailscaleServiceIPv6String + "]"
  346. )
  347. // allow requests on quad-100 (or ipv6 equivalent)
  348. host := strings.TrimSuffix(r.Host, ":80")
  349. if host == ipv4ServiceHost || host == ipv6ServiceHost {
  350. return false
  351. }
  352. st, err := s.lc.StatusWithoutPeers(r.Context())
  353. if err != nil {
  354. s.logf("error getting status: %v", err)
  355. http.Error(w, "internal error", http.StatusInternalServerError)
  356. return true
  357. }
  358. ipv4, ipv6 := s.selfNodeAddresses(r, st)
  359. if r.Host == fmt.Sprintf("%s:%d", ipv4.String(), ListenPort) {
  360. return false // already accessing over Tailscale IP
  361. }
  362. if r.Host == fmt.Sprintf("[%s]:%d", ipv6.String(), ListenPort) {
  363. return false // already accessing over Tailscale IP
  364. }
  365. // Not currently accessing via Tailscale IP,
  366. // redirect them.
  367. var preferV6 bool
  368. if ap, err := netip.ParseAddrPort(r.Host); err == nil {
  369. // If Host was already ipv6, keep them on same protocol.
  370. preferV6 = ap.Addr().Is6()
  371. }
  372. newURL := *r.URL
  373. if (preferV6 && ipv6.IsValid()) || !ipv4.IsValid() {
  374. newURL.Host = fmt.Sprintf("[%s]:%d", ipv6.String(), ListenPort)
  375. } else {
  376. newURL.Host = fmt.Sprintf("%s:%d", ipv4.String(), ListenPort)
  377. }
  378. http.Redirect(w, r, newURL.String(), http.StatusMovedPermanently)
  379. return true
  380. }
  381. // selfNodeAddresses return the Tailscale IPv4 and IPv6 addresses for the self node.
  382. // st is expected to be a status with peers included.
  383. func (s *Server) selfNodeAddresses(r *http.Request, st *ipnstate.Status) (ipv4, ipv6 netip.Addr) {
  384. for _, ip := range st.Self.TailscaleIPs {
  385. if ip.Is4() {
  386. ipv4 = ip
  387. } else if ip.Is6() {
  388. ipv6 = ip
  389. }
  390. if ipv4.IsValid() && ipv6.IsValid() {
  391. break // found both IPs
  392. }
  393. }
  394. if whois, err := s.lc.WhoIs(r.Context(), r.RemoteAddr); err == nil {
  395. // The source peer connecting to this node may know it by a different
  396. // IP than the node knows itself as. Specifically, this may be the case
  397. // if the peer is coming from a different tailnet (sharee node), as IPs
  398. // are specific to each tailnet.
  399. // Here, we check if the source peer knows the node by a different IP,
  400. // and return the peer's version if so.
  401. if knownIPv4 := whois.Node.SelfNodeV4MasqAddrForThisPeer; knownIPv4 != nil {
  402. ipv4 = *knownIPv4
  403. }
  404. if knownIPv6 := whois.Node.SelfNodeV6MasqAddrForThisPeer; knownIPv6 != nil {
  405. ipv6 = *knownIPv6
  406. }
  407. }
  408. return ipv4, ipv6
  409. }
  410. // authorizeRequest reports whether the request from the web client
  411. // is authorized to be completed.
  412. // It reports true if the request is authorized, and false otherwise.
  413. // authorizeRequest manages writing out any relevant authorization
  414. // errors to the ResponseWriter itself.
  415. func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bool) {
  416. if s.mode == ManageServerMode { // client using tailscale auth
  417. session, _, _, err := s.getSession(r)
  418. switch {
  419. case errors.Is(err, errNotUsingTailscale):
  420. // All requests must be made over tailscale.
  421. http.Error(w, "must access over tailscale", http.StatusUnauthorized)
  422. return false
  423. case r.URL.Path == "/api/data" && r.Method == httpm.GET:
  424. // Readonly endpoint allowed without valid browser session.
  425. return true
  426. case r.URL.Path == "/api/device-details-click" && r.Method == httpm.POST:
  427. // Special case metric endpoint that is allowed without a browser session.
  428. return true
  429. case strings.HasPrefix(r.URL.Path, "/api/"):
  430. // All other /api/ endpoints require a valid browser session.
  431. if err != nil || !session.isAuthorized(s.timeNow()) {
  432. http.Error(w, "no valid session", http.StatusUnauthorized)
  433. return false
  434. }
  435. return true
  436. default:
  437. // No additional auth on non-api (assets, index.html, etc).
  438. return true
  439. }
  440. }
  441. // Client using system-specific auth.
  442. switch distro.Get() {
  443. case distro.Synology:
  444. if !buildfeatures.HasSynology {
  445. // Synology support not built in.
  446. return false
  447. }
  448. authorized, _ := authorizeSynology(r)
  449. return authorized
  450. case distro.QNAP:
  451. authorized, _ := authorizeQNAP(r)
  452. return authorized
  453. default:
  454. return true // no additional auth for this distro
  455. }
  456. }
  457. // serveLoginAPI serves requests for the web login client.
  458. // It should only be called by Server.ServeHTTP, via Server.apiHandler,
  459. // which protects the handler using gorilla csrf.
  460. func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
  461. switch {
  462. case r.URL.Path == "/api/data" && r.Method == httpm.GET:
  463. s.serveGetNodeData(w, r)
  464. case r.URL.Path == "/api/up" && r.Method == httpm.POST:
  465. s.serveTailscaleUp(w, r)
  466. case r.URL.Path == "/api/device-details-click" && r.Method == httpm.POST:
  467. s.serveDeviceDetailsClick(w, r)
  468. default:
  469. http.Error(w, "invalid endpoint or method", http.StatusNotFound)
  470. }
  471. }
  472. type apiHandler[data any] struct {
  473. s *Server
  474. w http.ResponseWriter
  475. r *http.Request
  476. // permissionCheck allows for defining whether a requesting peer's
  477. // capabilities grant them access to make the given data update.
  478. // If permissionCheck reports false, the request fails as unauthorized.
  479. permissionCheck func(data data, peer peerCapabilities) bool
  480. }
  481. // newHandler constructs a new api handler which restricts the given request
  482. // to the specified permission check. If the permission check fails for
  483. // the peer associated with the request, an unauthorized error is returned
  484. // to the client.
  485. func newHandler[data any](s *Server, w http.ResponseWriter, r *http.Request, permissionCheck func(data data, peer peerCapabilities) bool) *apiHandler[data] {
  486. return &apiHandler[data]{
  487. s: s,
  488. w: w,
  489. r: r,
  490. permissionCheck: permissionCheck,
  491. }
  492. }
  493. // alwaysAllowed can be passed as the permissionCheck argument to newHandler
  494. // for requests that are always allowed to complete regardless of a peer's
  495. // capabilities.
  496. func alwaysAllowed[data any](_ data, _ peerCapabilities) bool { return true }
  497. func (a *apiHandler[data]) getPeer() (peerCapabilities, error) {
  498. // TODO(tailscale/corp#16695,sonia): We also call StatusWithoutPeers and
  499. // WhoIs when originally checking for a session from authorizeRequest.
  500. // Would be nice if we could pipe those through to here so we don't end
  501. // up having to re-call them to grab the peer capabilities.
  502. status, err := a.s.lc.StatusWithoutPeers(a.r.Context())
  503. if err != nil {
  504. return nil, err
  505. }
  506. whois, err := a.s.lc.WhoIs(a.r.Context(), a.r.RemoteAddr)
  507. if err != nil {
  508. return nil, err
  509. }
  510. peer, err := toPeerCapabilities(status, whois)
  511. if err != nil {
  512. return nil, err
  513. }
  514. return peer, nil
  515. }
  516. type noBodyData any // empty type, for use from serveAPI for endpoints with empty body
  517. // handle runs the given handler if the source peer satisfies the
  518. // constraints for running this request.
  519. //
  520. // handle is expected for use when `data` type is empty, or set to
  521. // `noBodyData` in practice. For requests that expect JSON body data
  522. // to be attached, use handleJSON instead.
  523. func (a *apiHandler[data]) handle(h http.HandlerFunc) {
  524. peer, err := a.getPeer()
  525. if err != nil {
  526. http.Error(a.w, err.Error(), http.StatusInternalServerError)
  527. return
  528. }
  529. var body data // not used
  530. if !a.permissionCheck(body, peer) {
  531. http.Error(a.w, "not allowed", http.StatusUnauthorized)
  532. return
  533. }
  534. h(a.w, a.r)
  535. }
  536. // handleJSON manages decoding the request's body JSON and passing
  537. // it on to the provided function if the source peer satisfies the
  538. // constraints for running this request.
  539. func (a *apiHandler[data]) handleJSON(h func(ctx context.Context, data data) error) {
  540. defer a.r.Body.Close()
  541. var body data
  542. if err := json.NewDecoder(a.r.Body).Decode(&body); err != nil {
  543. http.Error(a.w, err.Error(), http.StatusInternalServerError)
  544. return
  545. }
  546. peer, err := a.getPeer()
  547. if err != nil {
  548. http.Error(a.w, err.Error(), http.StatusInternalServerError)
  549. return
  550. }
  551. if !a.permissionCheck(body, peer) {
  552. http.Error(a.w, "not allowed", http.StatusUnauthorized)
  553. return
  554. }
  555. if err := h(a.r.Context(), body); err != nil {
  556. http.Error(a.w, err.Error(), http.StatusInternalServerError)
  557. return
  558. }
  559. a.w.WriteHeader(http.StatusOK)
  560. }
  561. // serveAPI serves requests for the web client api.
  562. // It should only be called by Server.ServeHTTP, via Server.apiHandler,
  563. // which protects the handler using gorilla csrf.
  564. func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
  565. if r.Method == httpm.PATCH {
  566. // Enforce that PATCH requests are always application/json.
  567. if ct := r.Header.Get("Content-Type"); ct != "application/json" {
  568. http.Error(w, "invalid request", http.StatusBadRequest)
  569. return
  570. }
  571. }
  572. path := strings.TrimPrefix(r.URL.Path, "/api")
  573. switch {
  574. case path == "/data" && r.Method == httpm.GET:
  575. newHandler[noBodyData](s, w, r, alwaysAllowed).
  576. handle(s.serveGetNodeData)
  577. return
  578. case path == "/exit-nodes" && r.Method == httpm.GET:
  579. newHandler[noBodyData](s, w, r, alwaysAllowed).
  580. handle(s.serveGetExitNodes)
  581. return
  582. case path == "/routes" && r.Method == httpm.POST:
  583. peerAllowed := func(d postRoutesRequest, p peerCapabilities) bool {
  584. if d.SetExitNode && !p.canEdit(capFeatureExitNodes) {
  585. return false
  586. } else if d.SetRoutes && !p.canEdit(capFeatureSubnets) {
  587. return false
  588. }
  589. return true
  590. }
  591. newHandler[postRoutesRequest](s, w, r, peerAllowed).
  592. handleJSON(s.servePostRoutes)
  593. return
  594. case path == "/device-details-click" && r.Method == httpm.POST:
  595. newHandler[noBodyData](s, w, r, alwaysAllowed).
  596. handle(s.serveDeviceDetailsClick)
  597. return
  598. case path == "/local/v0/logout" && r.Method == httpm.POST:
  599. peerAllowed := func(_ noBodyData, peer peerCapabilities) bool {
  600. return peer.canEdit(capFeatureAccount)
  601. }
  602. newHandler[noBodyData](s, w, r, peerAllowed).
  603. handle(s.proxyRequestToLocalAPI)
  604. return
  605. case path == "/local/v0/prefs" && r.Method == httpm.PATCH:
  606. peerAllowed := func(data maskedPrefs, peer peerCapabilities) bool {
  607. if data.RunSSHSet && !peer.canEdit(capFeatureSSH) {
  608. return false
  609. }
  610. return true
  611. }
  612. newHandler[maskedPrefs](s, w, r, peerAllowed).
  613. handleJSON(s.serveUpdatePrefs)
  614. return
  615. case path == "/local/v0/update/check" && r.Method == httpm.GET:
  616. newHandler[noBodyData](s, w, r, alwaysAllowed).
  617. handle(s.proxyRequestToLocalAPI)
  618. return
  619. case path == "/local/v0/update/check" && r.Method == httpm.POST:
  620. peerAllowed := func(_ noBodyData, peer peerCapabilities) bool {
  621. return peer.canEdit(capFeatureAccount)
  622. }
  623. newHandler[noBodyData](s, w, r, peerAllowed).
  624. handle(s.proxyRequestToLocalAPI)
  625. return
  626. case path == "/local/v0/update/progress" && r.Method == httpm.POST:
  627. newHandler[noBodyData](s, w, r, alwaysAllowed).
  628. handle(s.proxyRequestToLocalAPI)
  629. return
  630. case path == "/local/v0/upload-client-metrics" && r.Method == httpm.POST:
  631. newHandler[noBodyData](s, w, r, alwaysAllowed).
  632. handle(s.proxyRequestToLocalAPI)
  633. return
  634. }
  635. http.Error(w, "invalid endpoint", http.StatusNotFound)
  636. }
  637. type authResponse struct {
  638. ServerMode ServerMode `json:"serverMode"`
  639. Authorized bool `json:"authorized"` // has an authorized management session
  640. ViewerIdentity *viewerIdentity `json:"viewerIdentity,omitempty"`
  641. NeedsSynoAuth bool `json:"needsSynoAuth,omitempty"`
  642. }
  643. // viewerIdentity is the Tailscale identity of the source node
  644. // connected to this web client.
  645. type viewerIdentity struct {
  646. LoginName string `json:"loginName"`
  647. NodeName string `json:"nodeName"`
  648. NodeIP string `json:"nodeIP"`
  649. ProfilePicURL string `json:"profilePicUrl,omitempty"`
  650. Capabilities peerCapabilities `json:"capabilities"` // features peer is allowed to edit
  651. }
  652. // serverAPIAuth handles requests to the /api/auth endpoint
  653. // and returns an authResponse indicating the current auth state and any steps the user needs to take.
  654. func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
  655. var resp authResponse
  656. resp.ServerMode = s.mode
  657. session, whois, status, sErr := s.getSession(r)
  658. var caps peerCapabilities
  659. if whois != nil {
  660. var err error
  661. caps, err = toPeerCapabilities(status, whois)
  662. if err != nil {
  663. http.Error(w, sErr.Error(), http.StatusInternalServerError)
  664. return
  665. }
  666. resp.ViewerIdentity = &viewerIdentity{
  667. LoginName: whois.UserProfile.LoginName,
  668. NodeName: whois.Node.Name,
  669. ProfilePicURL: whois.UserProfile.ProfilePicURL,
  670. Capabilities: caps,
  671. }
  672. if addrs := whois.Node.Addresses; len(addrs) > 0 {
  673. resp.ViewerIdentity.NodeIP = addrs[0].Addr().String()
  674. }
  675. }
  676. // First verify platform auth.
  677. // If platform auth is needed, this should happen first.
  678. if s.mode == LoginServerMode || s.mode == ReadOnlyServerMode {
  679. switch distro.Get() {
  680. case distro.Synology:
  681. authorized, err := authorizeSynology(r)
  682. if err != nil {
  683. http.Error(w, err.Error(), http.StatusUnauthorized)
  684. return
  685. }
  686. if !authorized {
  687. resp.NeedsSynoAuth = true
  688. writeJSON(w, resp)
  689. return
  690. }
  691. case distro.QNAP:
  692. if _, err := authorizeQNAP(r); err != nil {
  693. http.Error(w, err.Error(), http.StatusUnauthorized)
  694. return
  695. }
  696. default:
  697. // no additional auth for this distro
  698. }
  699. }
  700. switch {
  701. case sErr != nil && errors.Is(sErr, errNotUsingTailscale):
  702. s.lc.IncrementCounter(r.Context(), "web_client_viewing_local", 1)
  703. resp.Authorized = false // restricted to the read-only view
  704. case sErr != nil && errors.Is(sErr, errNotOwner):
  705. s.lc.IncrementCounter(r.Context(), "web_client_viewing_not_owner", 1)
  706. resp.Authorized = false // restricted to the read-only view
  707. case sErr != nil && errors.Is(sErr, errTaggedLocalSource):
  708. s.lc.IncrementCounter(r.Context(), "web_client_viewing_local_tag", 1)
  709. resp.Authorized = false // restricted to the read-only view
  710. case sErr != nil && errors.Is(sErr, errTaggedRemoteSource):
  711. s.lc.IncrementCounter(r.Context(), "web_client_viewing_remote_tag", 1)
  712. resp.Authorized = false // restricted to the read-only view
  713. case sErr != nil && !errors.Is(sErr, errNoSession):
  714. // Any other error.
  715. http.Error(w, sErr.Error(), http.StatusInternalServerError)
  716. return
  717. case session.isAuthorized(s.timeNow()):
  718. if whois.Node.StableID == status.Self.ID {
  719. s.lc.IncrementCounter(r.Context(), "web_client_managing_local", 1)
  720. } else {
  721. s.lc.IncrementCounter(r.Context(), "web_client_managing_remote", 1)
  722. }
  723. // User has a valid session. They're now authorized to edit if they
  724. // have any edit capabilities. In practice, they won't be sent through
  725. // the auth flow if they don't have edit caps, but their ACL granted
  726. // permissions may change at any time. The frontend views and backend
  727. // endpoints are always restricted to their current capabilities in
  728. // addition to a valid session.
  729. //
  730. // But, we also check the caps here for a better user experience on
  731. // the frontend login toggle, which uses resp.Authorized to display
  732. // "viewing" vs "managing" copy. If they don't have caps, we want to
  733. // display "viewing" even if they have a valid session.
  734. resp.Authorized = !caps.isEmpty()
  735. default:
  736. if whois == nil || (whois.Node.StableID == status.Self.ID) {
  737. // whois being nil implies local as the request did not come over Tailscale.
  738. s.lc.IncrementCounter(r.Context(), "web_client_viewing_local", 1)
  739. } else {
  740. s.lc.IncrementCounter(r.Context(), "web_client_viewing_remote", 1)
  741. }
  742. resp.Authorized = false // not yet authorized
  743. }
  744. writeJSON(w, resp)
  745. }
  746. type newSessionAuthResponse struct {
  747. AuthURL string `json:"authUrl,omitempty"`
  748. }
  749. // serveAPIAuthSessionNew handles requests to the /api/auth/session/new endpoint.
  750. func (s *Server) serveAPIAuthSessionNew(w http.ResponseWriter, r *http.Request) {
  751. session, whois, _, err := s.getSession(r)
  752. if err != nil && !errors.Is(err, errNoSession) {
  753. // Source associated with request not allowed to create
  754. // a session for this web client.
  755. http.Error(w, err.Error(), http.StatusUnauthorized)
  756. return
  757. }
  758. if session == nil {
  759. // Create a new session.
  760. // If one already existed, we return that authURL rather than creating a new one.
  761. session, err = s.newSession(r.Context(), whois)
  762. if err != nil {
  763. http.Error(w, err.Error(), http.StatusInternalServerError)
  764. return
  765. }
  766. // Set the cookie on browser.
  767. http.SetCookie(w, &http.Cookie{
  768. Name: sessionCookieName,
  769. Value: session.ID,
  770. Raw: session.ID,
  771. Path: "/",
  772. HttpOnly: true,
  773. SameSite: http.SameSiteStrictMode,
  774. Expires: session.expires(),
  775. // We can't set Secure to true because we serve over HTTP
  776. // (but only on Tailscale IPs, hence over encrypted
  777. // connections that a LAN-local attacker cannot sniff).
  778. // In the future, we could support HTTPS requests using
  779. // the full MagicDNS hostname, and could set this.
  780. // Secure: true,
  781. })
  782. }
  783. writeJSON(w, newSessionAuthResponse{AuthURL: session.AuthURL})
  784. }
  785. // serveAPIAuthSessionWait handles requests to the /api/auth/session/wait endpoint.
  786. func (s *Server) serveAPIAuthSessionWait(w http.ResponseWriter, r *http.Request) {
  787. session, _, _, err := s.getSession(r)
  788. if err != nil {
  789. http.Error(w, err.Error(), http.StatusUnauthorized)
  790. return
  791. }
  792. if session.isAuthorized(s.timeNow()) {
  793. return // already authorized
  794. }
  795. if err := s.awaitUserAuth(r.Context(), session); err != nil {
  796. http.Error(w, err.Error(), http.StatusUnauthorized)
  797. return
  798. }
  799. }
  800. type nodeData struct {
  801. ID tailcfg.StableNodeID
  802. Status string
  803. DeviceName string
  804. TailnetName string // TLS cert name
  805. DomainName string
  806. IPv4 netip.Addr
  807. IPv6 netip.Addr
  808. OS string
  809. IPNVersion string
  810. Profile tailcfg.UserProfile
  811. IsTagged bool
  812. Tags []string
  813. KeyExpiry string // time.RFC3339
  814. KeyExpired bool
  815. TUNMode bool
  816. IsSynology bool
  817. DSMVersion int // 6 or 7, if IsSynology=true
  818. IsUnraid bool
  819. UnraidToken string
  820. URLPrefix string // if set, the URL prefix the client is served behind
  821. UsingExitNode *exitNode
  822. AdvertisingExitNode bool
  823. AdvertisingExitNodeApproved bool // whether running this node as an exit node has been approved by an admin
  824. AdvertisedRoutes []subnetRoute // excludes exit node routes
  825. RunningSSHServer bool
  826. ClientVersion *tailcfg.ClientVersion
  827. // whether tailnet ACLs allow access to port 5252 on this device
  828. ACLAllowsAnyIncomingTraffic bool
  829. ControlAdminURL string
  830. LicensesURL string
  831. // Features is the set of available features for use on the
  832. // current platform. e.g. "ssh", "advertise-exit-node", etc.
  833. // Map value is true if the given feature key is available.
  834. //
  835. // See web.availableFeatures func for population of this field.
  836. // Contents are expected to match values defined in node-data.ts
  837. // on the frontend.
  838. Features map[string]bool
  839. }
  840. type subnetRoute struct {
  841. Route string
  842. Approved bool // approved by control server
  843. }
  844. func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
  845. st, err := s.lc.Status(r.Context())
  846. if err != nil {
  847. http.Error(w, err.Error(), http.StatusInternalServerError)
  848. return
  849. }
  850. prefs, err := s.lc.GetPrefs(r.Context())
  851. if err != nil {
  852. http.Error(w, err.Error(), http.StatusInternalServerError)
  853. return
  854. }
  855. filterRules, _ := s.lc.DebugPacketFilterRules(r.Context())
  856. ipv4, ipv6 := s.selfNodeAddresses(r, st)
  857. data := &nodeData{
  858. ID: st.Self.ID,
  859. Status: st.BackendState,
  860. DeviceName: strings.Split(st.Self.DNSName, ".")[0],
  861. IPv4: ipv4,
  862. IPv6: ipv6,
  863. OS: st.Self.OS,
  864. IPNVersion: strings.Split(st.Version, "-")[0],
  865. Profile: st.User[st.Self.UserID],
  866. IsTagged: st.Self.IsTagged(),
  867. KeyExpired: st.Self.Expired,
  868. TUNMode: st.TUN,
  869. IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
  870. DSMVersion: distro.DSMVersion(),
  871. IsUnraid: distro.Get() == distro.Unraid,
  872. UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
  873. RunningSSHServer: prefs.RunSSH,
  874. URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
  875. ControlAdminURL: prefs.AdminPageURL(s.polc),
  876. LicensesURL: licenses.LicensesURL(),
  877. Features: availableFeatures(),
  878. ACLAllowsAnyIncomingTraffic: s.aclsAllowAccess(filterRules),
  879. }
  880. if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn && data.URLPrefix == "" {
  881. // X-Ingress-Path is the path prefix in use for Home Assistant
  882. // https://developers.home-assistant.io/docs/add-ons/presentation#ingress
  883. data.URLPrefix = r.Header.Get("X-Ingress-Path")
  884. }
  885. cv, err := s.lc.CheckUpdate(r.Context())
  886. if err != nil {
  887. s.logf("could not check for updates: %v", err)
  888. } else {
  889. data.ClientVersion = cv
  890. }
  891. profile, _, err := s.lc.ProfileStatus(r.Context())
  892. if err != nil {
  893. s.logf("error fetching profiles: %v", err)
  894. // If for some reason we can't fetch profiles,
  895. // continue to use st.CurrentTailnet if set.
  896. if st.CurrentTailnet != nil {
  897. data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
  898. data.DomainName = st.CurrentTailnet.Name
  899. }
  900. } else {
  901. data.TailnetName = profile.NetworkProfile.MagicDNSName
  902. data.DomainName = profile.NetworkProfile.DisplayNameOrDefault()
  903. }
  904. if st.Self.Tags != nil {
  905. data.Tags = st.Self.Tags.AsSlice()
  906. }
  907. if st.Self.KeyExpiry != nil {
  908. data.KeyExpiry = st.Self.KeyExpiry.Format(time.RFC3339)
  909. }
  910. routeApproved := func(route netip.Prefix) bool {
  911. if st.Self == nil || st.Self.AllowedIPs == nil {
  912. return false
  913. }
  914. return st.Self.AllowedIPs.ContainsFunc(func(p netip.Prefix) bool {
  915. return p == route
  916. })
  917. }
  918. data.AdvertisingExitNodeApproved = routeApproved(tsaddr.AllIPv4()) || routeApproved(tsaddr.AllIPv6())
  919. for _, r := range prefs.AdvertiseRoutes {
  920. if tsaddr.IsExitRoute(r) {
  921. data.AdvertisingExitNode = true
  922. } else {
  923. data.AdvertisedRoutes = append(data.AdvertisedRoutes, subnetRoute{
  924. Route: r.String(),
  925. Approved: routeApproved(r),
  926. })
  927. }
  928. }
  929. if e := st.ExitNodeStatus; e != nil {
  930. data.UsingExitNode = &exitNode{
  931. ID: e.ID,
  932. Online: e.Online,
  933. }
  934. for _, ps := range st.Peer {
  935. if ps.ID == e.ID {
  936. data.UsingExitNode.Name = ps.DNSName
  937. data.UsingExitNode.Location = ps.Location
  938. break
  939. }
  940. }
  941. if data.UsingExitNode.Name == "" {
  942. // Falling back to TailscaleIP/StableNodeID when the peer
  943. // is no longer included in status.
  944. if len(e.TailscaleIPs) > 0 {
  945. data.UsingExitNode.Name = e.TailscaleIPs[0].Addr().String()
  946. } else {
  947. data.UsingExitNode.Name = string(e.ID)
  948. }
  949. }
  950. }
  951. writeJSON(w, *data)
  952. }
  953. func availableFeatures() map[string]bool {
  954. features := map[string]bool{
  955. "advertise-exit-node": true, // available on all platforms
  956. "advertise-routes": true, // available on all platforms
  957. "use-exit-node": featureknob.CanUseExitNode() == nil,
  958. "ssh": featureknob.CanRunTailscaleSSH() == nil,
  959. "auto-update": version.IsUnstableBuild() && feature.CanAutoUpdate(),
  960. }
  961. return features
  962. }
  963. // aclsAllowAccess returns whether tailnet ACLs (as expressed in the provided filter rules)
  964. // permit any devices to access the local web client.
  965. // This does not currently check whether a specific device can connect, just any device.
  966. func (s *Server) aclsAllowAccess(rules []tailcfg.FilterRule) bool {
  967. for _, rule := range rules {
  968. for _, dp := range rule.DstPorts {
  969. if dp.Ports.Contains(ListenPort) {
  970. return true
  971. }
  972. }
  973. }
  974. return false
  975. }
  976. type exitNode struct {
  977. ID tailcfg.StableNodeID
  978. Name string
  979. Location *tailcfg.Location
  980. Online bool
  981. }
  982. func (s *Server) serveGetExitNodes(w http.ResponseWriter, r *http.Request) {
  983. st, err := s.lc.Status(r.Context())
  984. if err != nil {
  985. http.Error(w, err.Error(), http.StatusInternalServerError)
  986. return
  987. }
  988. var exitNodes []*exitNode
  989. for _, ps := range st.Peer {
  990. if !ps.ExitNodeOption {
  991. continue
  992. }
  993. exitNodes = append(exitNodes, &exitNode{
  994. ID: ps.ID,
  995. Name: ps.DNSName,
  996. Location: ps.Location,
  997. Online: ps.Online,
  998. })
  999. }
  1000. writeJSON(w, exitNodes)
  1001. }
  1002. // maskedPrefs is the subset of ipn.MaskedPrefs that are
  1003. // allowed to be editable via the web UI.
  1004. type maskedPrefs struct {
  1005. RunSSHSet bool
  1006. RunSSH bool
  1007. }
  1008. func (s *Server) serveUpdatePrefs(ctx context.Context, prefs maskedPrefs) error {
  1009. _, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
  1010. RunSSHSet: prefs.RunSSHSet,
  1011. Prefs: ipn.Prefs{
  1012. RunSSH: prefs.RunSSH,
  1013. },
  1014. })
  1015. return err
  1016. }
  1017. type postRoutesRequest struct {
  1018. SetExitNode bool // when set, UseExitNode and AdvertiseExitNode values are applied
  1019. SetRoutes bool // when set, AdvertiseRoutes value is applied
  1020. UseExitNode tailcfg.StableNodeID
  1021. AdvertiseExitNode bool
  1022. AdvertiseRoutes []string
  1023. }
  1024. func (s *Server) servePostRoutes(ctx context.Context, data postRoutesRequest) error {
  1025. prefs, err := s.lc.GetPrefs(ctx)
  1026. if err != nil {
  1027. return err
  1028. }
  1029. var currNonExitRoutes []string
  1030. var currAdvertisingExitNode bool
  1031. for _, r := range prefs.AdvertiseRoutes {
  1032. if tsaddr.IsExitRoute(r) {
  1033. currAdvertisingExitNode = true
  1034. continue
  1035. }
  1036. currNonExitRoutes = append(currNonExitRoutes, r.String())
  1037. }
  1038. // Set non-edited fields to their current values.
  1039. if data.SetExitNode {
  1040. data.AdvertiseRoutes = currNonExitRoutes
  1041. } else if data.SetRoutes {
  1042. data.AdvertiseExitNode = currAdvertisingExitNode
  1043. data.UseExitNode = prefs.ExitNodeID
  1044. }
  1045. // Calculate routes.
  1046. routesStr := strings.Join(data.AdvertiseRoutes, ",")
  1047. routes, err := netutil.CalcAdvertiseRoutes(routesStr, data.AdvertiseExitNode)
  1048. if err != nil {
  1049. return err
  1050. }
  1051. if !data.UseExitNode.IsZero() && tsaddr.ContainsExitRoutes(views.SliceOf(routes)) {
  1052. return errors.New("cannot use and advertise exit node at same time")
  1053. }
  1054. // Make prefs update.
  1055. p := &ipn.MaskedPrefs{
  1056. AdvertiseRoutesSet: true,
  1057. ExitNodeIDSet: true,
  1058. Prefs: ipn.Prefs{
  1059. ExitNodeID: data.UseExitNode,
  1060. AdvertiseRoutes: routes,
  1061. },
  1062. }
  1063. _, err = s.lc.EditPrefs(ctx, p)
  1064. return err
  1065. }
  1066. // tailscaleUp starts the daemon with the provided options.
  1067. // If reauthentication has been requested, an authURL is returned to complete device registration.
  1068. func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, opt tailscaleUpOptions) (authURL string, retErr error) {
  1069. origAuthURL := st.AuthURL
  1070. isRunning := st.BackendState == ipn.Running.String()
  1071. if !opt.Reauthenticate {
  1072. switch {
  1073. case origAuthURL != "":
  1074. return origAuthURL, nil
  1075. case isRunning:
  1076. return "", nil
  1077. case st.BackendState == ipn.Stopped.String():
  1078. // stopped and not reauthenticating, so just start running
  1079. _, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
  1080. Prefs: ipn.Prefs{
  1081. WantRunning: true,
  1082. },
  1083. WantRunningSet: true,
  1084. })
  1085. return "", err
  1086. }
  1087. }
  1088. // printAuthURL reports whether we should print out the
  1089. // provided auth URL from an IPN notify.
  1090. printAuthURL := func(url string) bool {
  1091. return url != origAuthURL
  1092. }
  1093. watchCtx, cancelWatch := context.WithCancel(ctx)
  1094. defer cancelWatch()
  1095. watcher, err := s.lc.WatchIPNBus(watchCtx, 0)
  1096. if err != nil {
  1097. return "", err
  1098. }
  1099. defer watcher.Close()
  1100. go func() {
  1101. if !isRunning {
  1102. ipnOptions := ipn.Options{AuthKey: opt.AuthKey}
  1103. if opt.ControlURL != "" {
  1104. _, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
  1105. Prefs: ipn.Prefs{
  1106. ControlURL: opt.ControlURL,
  1107. },
  1108. ControlURLSet: true,
  1109. })
  1110. if err != nil {
  1111. s.logf("edit prefs: %v", err)
  1112. }
  1113. }
  1114. if err := s.lc.Start(ctx, ipnOptions); err != nil {
  1115. s.logf("start: %v", err)
  1116. }
  1117. }
  1118. if opt.Reauthenticate {
  1119. if err := s.lc.StartLoginInteractive(ctx); err != nil {
  1120. s.logf("startLogin: %v", err)
  1121. }
  1122. }
  1123. }()
  1124. for {
  1125. n, err := watcher.Next()
  1126. if err != nil {
  1127. return "", err
  1128. }
  1129. if n.State != nil && *n.State == ipn.Running {
  1130. return "", nil
  1131. }
  1132. if n.ErrMessage != nil {
  1133. msg := *n.ErrMessage
  1134. return "", fmt.Errorf("backend error: %v", msg)
  1135. }
  1136. if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
  1137. return *url, nil
  1138. }
  1139. }
  1140. }
  1141. type tailscaleUpOptions struct {
  1142. // If true, force reauthentication of the client.
  1143. // Otherwise simply reconnect, the same as running `tailscale up`.
  1144. Reauthenticate bool
  1145. ControlURL string
  1146. AuthKey string
  1147. }
  1148. // serveTailscaleUp serves requests to /api/up.
  1149. // If the user needs to authenticate, an authURL is provided in the response.
  1150. func (s *Server) serveTailscaleUp(w http.ResponseWriter, r *http.Request) {
  1151. defer r.Body.Close()
  1152. st, err := s.lc.Status(r.Context())
  1153. if err != nil {
  1154. http.Error(w, err.Error(), http.StatusInternalServerError)
  1155. return
  1156. }
  1157. var opt tailscaleUpOptions
  1158. type mi map[string]any
  1159. if err := json.NewDecoder(r.Body).Decode(&opt); err != nil {
  1160. w.WriteHeader(400)
  1161. json.NewEncoder(w).Encode(mi{"error": err.Error()})
  1162. return
  1163. }
  1164. w.Header().Set("Content-Type", "application/json")
  1165. s.logf("tailscaleUp(reauth=%v) ...", opt.Reauthenticate)
  1166. url, err := s.tailscaleUp(r.Context(), st, opt)
  1167. s.logf("tailscaleUp = (URL %v, %v)", url != "", err)
  1168. if err != nil {
  1169. w.WriteHeader(http.StatusInternalServerError)
  1170. json.NewEncoder(w).Encode(mi{"error": err.Error()})
  1171. return
  1172. }
  1173. if url != "" {
  1174. json.NewEncoder(w).Encode(mi{"url": url})
  1175. } else {
  1176. io.WriteString(w, "{}")
  1177. }
  1178. }
  1179. // serveDeviceDetailsClick increments the web_client_device_details_click metric
  1180. // by one.
  1181. //
  1182. // Metric logging from the frontend typically is proxied to the localapi. This event
  1183. // has been special cased as access to the localapi is gated upon having a valid
  1184. // session which is not always the case when we want to be logging this metric (e.g.,
  1185. // when in readonly mode).
  1186. //
  1187. // Other metrics should not be logged in this way without a good reason.
  1188. func (s *Server) serveDeviceDetailsClick(w http.ResponseWriter, r *http.Request) {
  1189. s.lc.IncrementCounter(r.Context(), "web_client_device_details_click", 1)
  1190. io.WriteString(w, "{}")
  1191. }
  1192. // proxyRequestToLocalAPI proxies the web API request to the localapi.
  1193. //
  1194. // The web API request path is expected to exactly match a localapi path,
  1195. // with prefix /api/local/ rather than /localapi/.
  1196. func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request) {
  1197. path := strings.TrimPrefix(r.URL.Path, "/api/local")
  1198. if r.URL.Path == path { // missing prefix
  1199. http.Error(w, "invalid request", http.StatusBadRequest)
  1200. return
  1201. }
  1202. localAPIURL := "http://" + apitype.LocalAPIHost + "/localapi" + path
  1203. req, err := http.NewRequestWithContext(r.Context(), r.Method, localAPIURL, r.Body)
  1204. if err != nil {
  1205. http.Error(w, "failed to construct request", http.StatusInternalServerError)
  1206. return
  1207. }
  1208. // Make request to tailscaled localapi.
  1209. resp, err := s.lc.DoLocalRequest(req)
  1210. if err != nil {
  1211. http.Error(w, err.Error(), resp.StatusCode)
  1212. return
  1213. }
  1214. defer resp.Body.Close()
  1215. // Send response back to web frontend.
  1216. w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
  1217. w.WriteHeader(resp.StatusCode)
  1218. if _, err := io.Copy(w, resp.Body); err != nil {
  1219. http.Error(w, err.Error(), http.StatusInternalServerError)
  1220. }
  1221. }
  1222. // enforcePrefix returns a HandlerFunc that enforces a given path prefix is used in requests,
  1223. // then strips it before invoking h.
  1224. // Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.
  1225. // Instead, it returns a redirect to the prefix path.
  1226. func enforcePrefix(prefix string, h http.HandlerFunc) http.HandlerFunc {
  1227. if prefix == "" {
  1228. return h
  1229. }
  1230. // ensure that prefix always has both a leading and trailing slash so
  1231. // that relative links for JS and CSS assets work correctly.
  1232. if !strings.HasPrefix(prefix, "/") {
  1233. prefix = "/" + prefix
  1234. }
  1235. if !strings.HasSuffix(prefix, "/") {
  1236. prefix += "/"
  1237. }
  1238. return func(w http.ResponseWriter, r *http.Request) {
  1239. if !strings.HasPrefix(r.URL.Path, prefix) {
  1240. http.Redirect(w, r, prefix, http.StatusFound)
  1241. return
  1242. }
  1243. prefix = strings.TrimSuffix(prefix, "/")
  1244. http.StripPrefix(prefix, h).ServeHTTP(w, r)
  1245. }
  1246. }
  1247. func writeJSON(w http.ResponseWriter, data any) {
  1248. w.Header().Set("Content-Type", "application/json")
  1249. if err := json.NewEncoder(w).Encode(data); err != nil {
  1250. w.Header().Set("Content-Type", "text/plain")
  1251. http.Error(w, err.Error(), http.StatusInternalServerError)
  1252. return
  1253. }
  1254. }