web.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  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. "context"
  7. "crypto/rand"
  8. "encoding/json"
  9. "errors"
  10. "fmt"
  11. "io"
  12. "log"
  13. "net/http"
  14. "net/netip"
  15. "os"
  16. "path/filepath"
  17. "slices"
  18. "strings"
  19. "sync"
  20. "time"
  21. "github.com/gorilla/csrf"
  22. "tailscale.com/client/tailscale"
  23. "tailscale.com/client/tailscale/apitype"
  24. "tailscale.com/envknob"
  25. "tailscale.com/ipn"
  26. "tailscale.com/ipn/ipnstate"
  27. "tailscale.com/licenses"
  28. "tailscale.com/net/netutil"
  29. "tailscale.com/net/tsaddr"
  30. "tailscale.com/tailcfg"
  31. "tailscale.com/types/logger"
  32. "tailscale.com/util/httpm"
  33. "tailscale.com/version/distro"
  34. )
  35. // ListenPort is the static port used for the web client when run inside tailscaled.
  36. // (5252 are the numbers above the letters "TSTS" on a qwerty keyboard.)
  37. const ListenPort = 5252
  38. // Server is the backend server for a Tailscale web client.
  39. type Server struct {
  40. mode ServerMode
  41. logf logger.Logf
  42. lc *tailscale.LocalClient
  43. timeNow func() time.Time
  44. // devMode indicates that the server run with frontend assets
  45. // served by a Vite dev server, allowing for local development
  46. // on the web client frontend.
  47. devMode bool
  48. cgiMode bool
  49. pathPrefix string
  50. apiHandler http.Handler // serves api endpoints; csrf-protected
  51. assetsHandler http.Handler // serves frontend assets
  52. assetsCleanup func() // called from Server.Shutdown
  53. // browserSessions is an in-memory cache of browser sessions for the
  54. // full management web client, which is only accessible over Tailscale.
  55. //
  56. // Users obtain a valid browser session by connecting to the web client
  57. // over Tailscale and verifying their identity by authenticating on the
  58. // control server.
  59. //
  60. // browserSessions get reset on every Server restart.
  61. //
  62. // The map provides a lookup of the session by cookie value
  63. // (browserSession.ID => browserSession).
  64. browserSessions sync.Map
  65. }
  66. // ServerMode specifies the mode of a running web.Server.
  67. type ServerMode string
  68. const (
  69. // LoginServerMode serves a readonly login client for logging a
  70. // node into a tailnet, and viewing a readonly interface of the
  71. // node's current Tailscale settings.
  72. //
  73. // In this mode, API calls are authenticated via platform auth.
  74. LoginServerMode ServerMode = "login"
  75. // ManageServerMode serves a management client for editing tailscale
  76. // settings of a node.
  77. //
  78. // This mode restricts the app to only being assessible over Tailscale,
  79. // and API calls are authenticated via browser sessions associated with
  80. // the source's Tailscale identity. If the source browser does not have
  81. // a valid session, a readonly version of the app is displayed.
  82. ManageServerMode ServerMode = "manage"
  83. )
  84. var (
  85. exitNodeRouteV4 = netip.MustParsePrefix("0.0.0.0/0")
  86. exitNodeRouteV6 = netip.MustParsePrefix("::/0")
  87. )
  88. // ServerOpts contains options for constructing a new Server.
  89. type ServerOpts struct {
  90. // Mode specifies the mode of web client being constructed.
  91. Mode ServerMode
  92. // CGIMode indicates if the server is running as a CGI script.
  93. CGIMode bool
  94. // PathPrefix is the URL prefix added to requests by CGI or reverse proxy.
  95. PathPrefix string
  96. // LocalClient is the tailscale.LocalClient to use for this web server.
  97. // If nil, a new one will be created.
  98. LocalClient *tailscale.LocalClient
  99. // TimeNow optionally provides a time function.
  100. // time.Now is used as default.
  101. TimeNow func() time.Time
  102. // Logf optionally provides a logger function.
  103. // log.Printf is used as default.
  104. Logf logger.Logf
  105. }
  106. // NewServer constructs a new Tailscale web client server.
  107. // If err is empty, s is always non-nil.
  108. // ctx is only required to live the duration of the NewServer call,
  109. // and not the lifespan of the web server.
  110. func NewServer(opts ServerOpts) (s *Server, err error) {
  111. switch opts.Mode {
  112. case LoginServerMode, ManageServerMode:
  113. // valid types
  114. case "":
  115. return nil, fmt.Errorf("must specify a Mode")
  116. default:
  117. return nil, fmt.Errorf("invalid Mode provided")
  118. }
  119. if opts.LocalClient == nil {
  120. opts.LocalClient = &tailscale.LocalClient{}
  121. }
  122. s = &Server{
  123. mode: opts.Mode,
  124. logf: opts.Logf,
  125. devMode: envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
  126. lc: opts.LocalClient,
  127. cgiMode: opts.CGIMode,
  128. pathPrefix: opts.PathPrefix,
  129. timeNow: opts.TimeNow,
  130. }
  131. if s.timeNow == nil {
  132. s.timeNow = time.Now
  133. }
  134. if s.logf == nil {
  135. s.logf = log.Printf
  136. }
  137. s.assetsHandler, s.assetsCleanup = assetsHandler(s.devMode)
  138. var metric string // clientmetric to report on startup
  139. // Create handler for "/api" requests with CSRF protection.
  140. // We don't require secure cookies, since the web client is regularly used
  141. // on network appliances that are served on local non-https URLs.
  142. // The client is secured by limiting the interface it listens on,
  143. // or by authenticating requests before they reach the web client.
  144. csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
  145. if s.mode == LoginServerMode {
  146. s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
  147. metric = "web_login_client_initialization"
  148. } else {
  149. s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
  150. metric = "web_client_initialization"
  151. }
  152. // Don't block startup on reporting metric.
  153. // Report in separate go routine with 5 second timeout.
  154. go func() {
  155. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  156. defer cancel()
  157. s.lc.IncrementCounter(ctx, metric, 1)
  158. }()
  159. return s, nil
  160. }
  161. func (s *Server) Shutdown() {
  162. s.logf("web.Server: shutting down")
  163. if s.assetsCleanup != nil {
  164. s.assetsCleanup()
  165. }
  166. }
  167. // ServeHTTP processes all requests for the Tailscale web client.
  168. func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  169. handler := s.serve
  170. // if path prefix is defined, strip it from requests.
  171. if s.pathPrefix != "" {
  172. handler = enforcePrefix(s.pathPrefix, handler)
  173. }
  174. handler(w, r)
  175. }
  176. func (s *Server) serve(w http.ResponseWriter, r *http.Request) {
  177. if s.mode == ManageServerMode {
  178. // In manage mode, requests must be sent directly to the bare Tailscale IP address.
  179. // If a request comes in on any other hostname, redirect.
  180. if s.requireTailscaleIP(w, r) {
  181. return // user was redirected
  182. }
  183. // serve HTTP 204 on /ok requests as connectivity check
  184. if r.Method == httpm.GET && r.URL.Path == "/ok" {
  185. w.WriteHeader(http.StatusNoContent)
  186. return
  187. }
  188. if !s.devMode {
  189. w.Header().Set("X-Frame-Options", "DENY")
  190. // TODO: use CSP nonce or hash to eliminate need for unsafe-inline
  191. w.Header().Set("Content-Security-Policy", "default-src 'self' 'unsafe-inline'; img-src * data:")
  192. w.Header().Set("Cross-Origin-Resource-Policy", "same-origin")
  193. }
  194. }
  195. if strings.HasPrefix(r.URL.Path, "/api/") {
  196. switch {
  197. case r.URL.Path == "/api/auth" && r.Method == httpm.GET:
  198. s.serveAPIAuth(w, r) // serve auth status
  199. return
  200. case r.URL.Path == "/api/auth/session/new" && r.Method == httpm.GET:
  201. s.serveAPIAuthSessionNew(w, r) // create new session
  202. return
  203. case r.URL.Path == "/api/auth/session/wait" && r.Method == httpm.GET:
  204. s.serveAPIAuthSessionWait(w, r) // wait for session to be authorized
  205. return
  206. }
  207. if ok := s.authorizeRequest(w, r); !ok {
  208. http.Error(w, "not authorized", http.StatusUnauthorized)
  209. return
  210. }
  211. // Pass API requests through to the API handler.
  212. s.apiHandler.ServeHTTP(w, r)
  213. return
  214. }
  215. if !s.devMode {
  216. s.lc.IncrementCounter(r.Context(), "web_client_page_load", 1)
  217. }
  218. s.assetsHandler.ServeHTTP(w, r)
  219. }
  220. // requireTailscaleIP redirects an incoming request if the HTTP request was not made to a bare Tailscale IP address.
  221. // The request will be redirected to the Tailscale IP, port 5252, with the original request path.
  222. // This allows any custom hostname to be used to access the device, but protects against DNS rebinding attacks.
  223. // Returns true if the request has been fully handled, either be returning a redirect or an HTTP error.
  224. func (s *Server) requireTailscaleIP(w http.ResponseWriter, r *http.Request) (handled bool) {
  225. const (
  226. ipv4ServiceHost = tsaddr.TailscaleServiceIPString
  227. ipv6ServiceHost = "[" + tsaddr.TailscaleServiceIPv6String + "]"
  228. )
  229. // allow requests on quad-100 (or ipv6 equivalent)
  230. if r.Host == ipv4ServiceHost || r.Host == ipv6ServiceHost {
  231. return false
  232. }
  233. st, err := s.lc.StatusWithoutPeers(r.Context())
  234. if err != nil {
  235. s.logf("error getting status: %v", err)
  236. http.Error(w, "internal error", http.StatusInternalServerError)
  237. return true
  238. }
  239. var ipv4 string // store the first IPv4 address we see for redirect later
  240. for _, ip := range st.Self.TailscaleIPs {
  241. if ip.Is4() {
  242. if r.Host == fmt.Sprintf("%s:%d", ip, ListenPort) {
  243. return false
  244. }
  245. ipv4 = ip.String()
  246. }
  247. if ip.Is6() && r.Host == fmt.Sprintf("[%s]:%d", ip, ListenPort) {
  248. return false
  249. }
  250. }
  251. newURL := *r.URL
  252. newURL.Host = fmt.Sprintf("%s:%d", ipv4, ListenPort)
  253. http.Redirect(w, r, newURL.String(), http.StatusMovedPermanently)
  254. return true
  255. }
  256. // authorizeRequest reports whether the request from the web client
  257. // is authorized to be completed.
  258. // It reports true if the request is authorized, and false otherwise.
  259. // authorizeRequest manages writing out any relevant authorization
  260. // errors to the ResponseWriter itself.
  261. func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bool) {
  262. if s.mode == ManageServerMode { // client using tailscale auth
  263. _, err := s.lc.WhoIs(r.Context(), r.RemoteAddr)
  264. switch {
  265. case err != nil:
  266. // All requests must be made over tailscale.
  267. http.Error(w, "must access over tailscale", http.StatusUnauthorized)
  268. return false
  269. case r.URL.Path == "/api/data" && r.Method == httpm.GET:
  270. // Readonly endpoint allowed without browser session.
  271. return true
  272. case strings.HasPrefix(r.URL.Path, "/api/"):
  273. // All other /api/ endpoints require a valid browser session.
  274. //
  275. // TODO(sonia): s.getSession calls whois again,
  276. // should try and use the above call instead of running another
  277. // localapi request.
  278. session, _, err := s.getSession(r)
  279. if err != nil || !session.isAuthorized(s.timeNow()) {
  280. http.Error(w, "no valid session", http.StatusUnauthorized)
  281. return false
  282. }
  283. return true
  284. default:
  285. // No additional auth on non-api (assets, index.html, etc).
  286. return true
  287. }
  288. }
  289. // Client using system-specific auth.
  290. switch distro.Get() {
  291. case distro.Synology:
  292. authorized, _ := authorizeSynology(r)
  293. return authorized
  294. case distro.QNAP:
  295. authorized, _ := authorizeQNAP(r)
  296. return authorized
  297. default:
  298. return true // no additional auth for this distro
  299. }
  300. }
  301. // serveLoginAPI serves requests for the web login client.
  302. // It should only be called by Server.ServeHTTP, via Server.apiHandler,
  303. // which protects the handler using gorilla csrf.
  304. func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
  305. w.Header().Set("X-CSRF-Token", csrf.Token(r))
  306. if r.URL.Path != "/api/data" { // only endpoint allowed for login client
  307. http.Error(w, "invalid endpoint", http.StatusNotFound)
  308. return
  309. }
  310. switch r.Method {
  311. case httpm.GET:
  312. // TODO(soniaappasamy): we may want a minimal node data response here
  313. s.serveGetNodeData(w, r)
  314. return
  315. }
  316. http.Error(w, "invalid endpoint", http.StatusNotFound)
  317. return
  318. }
  319. type authType string
  320. var (
  321. synoAuth authType = "synology" // user needs a SynoToken for subsequent API calls
  322. tailscaleAuth authType = "tailscale" // user needs to complete Tailscale check mode
  323. )
  324. type authResponse struct {
  325. AuthNeeded authType `json:"authNeeded,omitempty"` // filled when user needs to complete a specific type of auth
  326. CanManageNode bool `json:"canManageNode"`
  327. ViewerIdentity *viewerIdentity `json:"viewerIdentity,omitempty"`
  328. }
  329. // viewerIdentity is the Tailscale identity of the source node
  330. // connected to this web client.
  331. type viewerIdentity struct {
  332. LoginName string `json:"loginName"`
  333. NodeName string `json:"nodeName"`
  334. NodeIP string `json:"nodeIP"`
  335. ProfilePicURL string `json:"profilePicUrl,omitempty"`
  336. }
  337. // serverAPIAuth handles requests to the /api/auth endpoint
  338. // and returns an authResponse indicating the current auth state and any steps the user needs to take.
  339. func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
  340. var resp authResponse
  341. session, whois, err := s.getSession(r)
  342. switch {
  343. case err != nil && errors.Is(err, errNotUsingTailscale):
  344. // not using tailscale, so perform platform auth
  345. switch distro.Get() {
  346. case distro.Synology:
  347. authorized, err := authorizeSynology(r)
  348. if err != nil {
  349. http.Error(w, err.Error(), http.StatusUnauthorized)
  350. return
  351. }
  352. if !authorized {
  353. resp.AuthNeeded = synoAuth
  354. }
  355. case distro.QNAP:
  356. if _, err := authorizeQNAP(r); err != nil {
  357. http.Error(w, err.Error(), http.StatusUnauthorized)
  358. return
  359. }
  360. default:
  361. // no additional auth for this distro
  362. }
  363. case err != nil && (errors.Is(err, errNotOwner) ||
  364. errors.Is(err, errNotUsingTailscale) ||
  365. errors.Is(err, errTaggedLocalSource) ||
  366. errors.Is(err, errTaggedRemoteSource)):
  367. // These cases are all restricted to the readonly view.
  368. // No auth action to take.
  369. resp.AuthNeeded = ""
  370. case err != nil && !errors.Is(err, errNoSession):
  371. // Any other error.
  372. http.Error(w, err.Error(), http.StatusInternalServerError)
  373. return
  374. case session.isAuthorized(s.timeNow()):
  375. resp.CanManageNode = true
  376. resp.AuthNeeded = ""
  377. default:
  378. resp.AuthNeeded = tailscaleAuth
  379. }
  380. if whois != nil {
  381. resp.ViewerIdentity = &viewerIdentity{
  382. LoginName: whois.UserProfile.LoginName,
  383. NodeName: whois.Node.Name,
  384. ProfilePicURL: whois.UserProfile.ProfilePicURL,
  385. }
  386. if addrs := whois.Node.Addresses; len(addrs) > 0 {
  387. resp.ViewerIdentity.NodeIP = addrs[0].Addr().String()
  388. }
  389. }
  390. writeJSON(w, resp)
  391. }
  392. type newSessionAuthResponse struct {
  393. AuthURL string `json:"authUrl,omitempty"`
  394. }
  395. // serveAPIAuthSessionNew handles requests to the /api/auth/session/new endpoint.
  396. func (s *Server) serveAPIAuthSessionNew(w http.ResponseWriter, r *http.Request) {
  397. session, whois, err := s.getSession(r)
  398. if err != nil && !errors.Is(err, errNoSession) {
  399. // Source associated with request not allowed to create
  400. // a session for this web client.
  401. http.Error(w, err.Error(), http.StatusUnauthorized)
  402. return
  403. }
  404. if session == nil {
  405. // Create a new session.
  406. // If one already existed, we return that authURL rather than creating a new one.
  407. session, err = s.newSession(r.Context(), whois)
  408. if err != nil {
  409. http.Error(w, err.Error(), http.StatusInternalServerError)
  410. return
  411. }
  412. // Set the cookie on browser.
  413. http.SetCookie(w, &http.Cookie{
  414. Name: sessionCookieName,
  415. Value: session.ID,
  416. Raw: session.ID,
  417. Path: "/",
  418. Expires: session.expires(),
  419. })
  420. }
  421. writeJSON(w, newSessionAuthResponse{AuthURL: session.AuthURL})
  422. }
  423. // serveAPIAuthSessionWait handles requests to the /api/auth/session/wait endpoint.
  424. func (s *Server) serveAPIAuthSessionWait(w http.ResponseWriter, r *http.Request) {
  425. session, _, err := s.getSession(r)
  426. if err != nil {
  427. http.Error(w, err.Error(), http.StatusUnauthorized)
  428. return
  429. }
  430. if session.isAuthorized(s.timeNow()) {
  431. return // already authorized
  432. }
  433. if err := s.awaitUserAuth(r.Context(), session); err != nil {
  434. http.Error(w, err.Error(), http.StatusUnauthorized)
  435. return
  436. }
  437. }
  438. // serveAPI serves requests for the web client api.
  439. // It should only be called by Server.ServeHTTP, via Server.apiHandler,
  440. // which protects the handler using gorilla csrf.
  441. func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
  442. w.Header().Set("X-CSRF-Token", csrf.Token(r))
  443. path := strings.TrimPrefix(r.URL.Path, "/api")
  444. switch {
  445. case path == "/data":
  446. switch r.Method {
  447. case httpm.GET:
  448. s.serveGetNodeData(w, r)
  449. case httpm.POST:
  450. s.servePostNodeUpdate(w, r)
  451. default:
  452. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  453. }
  454. return
  455. case strings.HasPrefix(path, "/local/"):
  456. s.proxyRequestToLocalAPI(w, r)
  457. return
  458. }
  459. http.Error(w, "invalid endpoint", http.StatusNotFound)
  460. }
  461. type nodeData struct {
  462. ID tailcfg.StableNodeID
  463. Status string
  464. DeviceName string
  465. TailnetName string // TLS cert name
  466. DomainName string
  467. IP string // IPv4
  468. IPv6 string
  469. OS string
  470. IPNVersion string
  471. Profile tailcfg.UserProfile
  472. IsTagged bool
  473. Tags []string
  474. KeyExpiry string // time.RFC3339
  475. KeyExpired bool
  476. TUNMode bool
  477. IsSynology bool
  478. DSMVersion int // 6 or 7, if IsSynology=true
  479. IsUnraid bool
  480. UnraidToken string
  481. URLPrefix string // if set, the URL prefix the client is served behind
  482. AdvertiseExitNode bool
  483. AdvertiseRoutes string
  484. RunningSSHServer bool
  485. ClientVersion *tailcfg.ClientVersion
  486. LicensesURL string
  487. }
  488. func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
  489. st, err := s.lc.Status(r.Context())
  490. if err != nil {
  491. http.Error(w, err.Error(), http.StatusInternalServerError)
  492. return
  493. }
  494. prefs, err := s.lc.GetPrefs(r.Context())
  495. if err != nil {
  496. http.Error(w, err.Error(), http.StatusInternalServerError)
  497. return
  498. }
  499. data := &nodeData{
  500. ID: st.Self.ID,
  501. Status: st.BackendState,
  502. DeviceName: strings.Split(st.Self.DNSName, ".")[0],
  503. OS: st.Self.OS,
  504. IPNVersion: strings.Split(st.Version, "-")[0],
  505. Profile: st.User[st.Self.UserID],
  506. IsTagged: st.Self.IsTagged(),
  507. KeyExpired: st.Self.Expired,
  508. TUNMode: st.TUN,
  509. IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
  510. DSMVersion: distro.DSMVersion(),
  511. IsUnraid: distro.Get() == distro.Unraid,
  512. UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
  513. RunningSSHServer: prefs.RunSSH,
  514. URLPrefix: strings.TrimSuffix(s.pathPrefix, "/"),
  515. LicensesURL: licenses.LicensesURL(),
  516. }
  517. cv, err := s.lc.CheckUpdate(r.Context())
  518. if err != nil {
  519. s.logf("could not check for updates: %v", err)
  520. } else {
  521. data.ClientVersion = cv
  522. }
  523. for _, ip := range st.TailscaleIPs {
  524. if ip.Is4() {
  525. data.IP = ip.String()
  526. } else if ip.Is6() {
  527. data.IPv6 = ip.String()
  528. }
  529. if data.IP != "" && data.IPv6 != "" {
  530. break
  531. }
  532. }
  533. if st.CurrentTailnet != nil {
  534. data.TailnetName = st.CurrentTailnet.MagicDNSSuffix
  535. data.DomainName = st.CurrentTailnet.Name
  536. }
  537. if st.Self.Tags != nil {
  538. data.Tags = st.Self.Tags.AsSlice()
  539. }
  540. if st.Self.KeyExpiry != nil {
  541. data.KeyExpiry = st.Self.KeyExpiry.Format(time.RFC3339)
  542. }
  543. for _, r := range prefs.AdvertiseRoutes {
  544. if r == exitNodeRouteV4 || r == exitNodeRouteV6 {
  545. data.AdvertiseExitNode = true
  546. } else {
  547. if data.AdvertiseRoutes != "" {
  548. data.AdvertiseRoutes += ","
  549. }
  550. data.AdvertiseRoutes += r.String()
  551. }
  552. }
  553. writeJSON(w, *data)
  554. }
  555. type nodeUpdate struct {
  556. AdvertiseRoutes string
  557. AdvertiseExitNode bool
  558. Reauthenticate bool
  559. ForceLogout bool
  560. }
  561. func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) {
  562. defer r.Body.Close()
  563. st, err := s.lc.Status(r.Context())
  564. if err != nil {
  565. http.Error(w, err.Error(), http.StatusInternalServerError)
  566. return
  567. }
  568. var postData nodeUpdate
  569. type mi map[string]any
  570. if err := json.NewDecoder(r.Body).Decode(&postData); err != nil {
  571. w.WriteHeader(400)
  572. json.NewEncoder(w).Encode(mi{"error": err.Error()})
  573. return
  574. }
  575. prefs, err := s.lc.GetPrefs(r.Context())
  576. if err != nil {
  577. http.Error(w, err.Error(), http.StatusInternalServerError)
  578. return
  579. }
  580. isCurrentlyExitNode := slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV4) || slices.Contains(prefs.AdvertiseRoutes, exitNodeRouteV6)
  581. if postData.AdvertiseExitNode != isCurrentlyExitNode {
  582. if postData.AdvertiseExitNode {
  583. s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_enable", 1)
  584. } else {
  585. s.lc.IncrementCounter(r.Context(), "web_client_advertise_exitnode_disable", 1)
  586. }
  587. }
  588. routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode)
  589. if err != nil {
  590. w.WriteHeader(http.StatusInternalServerError)
  591. json.NewEncoder(w).Encode(mi{"error": err.Error()})
  592. return
  593. }
  594. mp := &ipn.MaskedPrefs{
  595. AdvertiseRoutesSet: true,
  596. WantRunningSet: true,
  597. }
  598. mp.Prefs.WantRunning = true
  599. mp.Prefs.AdvertiseRoutes = routes
  600. s.logf("Doing edit: %v", mp.Pretty())
  601. if _, err := s.lc.EditPrefs(r.Context(), mp); err != nil {
  602. w.WriteHeader(http.StatusInternalServerError)
  603. json.NewEncoder(w).Encode(mi{"error": err.Error()})
  604. return
  605. }
  606. w.Header().Set("Content-Type", "application/json")
  607. var reauth, logout bool
  608. if postData.Reauthenticate {
  609. reauth = true
  610. }
  611. if postData.ForceLogout {
  612. logout = true
  613. }
  614. s.logf("tailscaleUp(reauth=%v, logout=%v) ...", reauth, logout)
  615. url, err := s.tailscaleUp(r.Context(), st, postData)
  616. s.logf("tailscaleUp = (URL %v, %v)", url != "", err)
  617. if err != nil {
  618. w.WriteHeader(http.StatusInternalServerError)
  619. json.NewEncoder(w).Encode(mi{"error": err.Error()})
  620. return
  621. }
  622. if url != "" {
  623. json.NewEncoder(w).Encode(mi{"url": url})
  624. } else {
  625. io.WriteString(w, "{}")
  626. }
  627. }
  628. func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, postData nodeUpdate) (authURL string, retErr error) {
  629. if postData.ForceLogout {
  630. if err := s.lc.Logout(ctx); err != nil {
  631. return "", fmt.Errorf("Logout error: %w", err)
  632. }
  633. return "", nil
  634. }
  635. origAuthURL := st.AuthURL
  636. isRunning := st.BackendState == ipn.Running.String()
  637. forceReauth := postData.Reauthenticate
  638. if !forceReauth {
  639. if origAuthURL != "" {
  640. return origAuthURL, nil
  641. }
  642. if isRunning {
  643. return "", nil
  644. }
  645. }
  646. // printAuthURL reports whether we should print out the
  647. // provided auth URL from an IPN notify.
  648. printAuthURL := func(url string) bool {
  649. return url != origAuthURL
  650. }
  651. watchCtx, cancelWatch := context.WithCancel(ctx)
  652. defer cancelWatch()
  653. watcher, err := s.lc.WatchIPNBus(watchCtx, 0)
  654. if err != nil {
  655. return "", err
  656. }
  657. defer watcher.Close()
  658. go func() {
  659. if !isRunning {
  660. s.lc.Start(ctx, ipn.Options{})
  661. }
  662. if forceReauth {
  663. s.lc.StartLoginInteractive(ctx)
  664. }
  665. }()
  666. for {
  667. n, err := watcher.Next()
  668. if err != nil {
  669. return "", err
  670. }
  671. if n.ErrMessage != nil {
  672. msg := *n.ErrMessage
  673. return "", fmt.Errorf("backend error: %v", msg)
  674. }
  675. if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
  676. return *url, nil
  677. }
  678. }
  679. }
  680. // proxyRequestToLocalAPI proxies the web API request to the localapi.
  681. //
  682. // The web API request path is expected to exactly match a localapi path,
  683. // with prefix /api/local/ rather than /localapi/.
  684. //
  685. // If the localapi path is not included in localapiAllowlist,
  686. // the request is rejected.
  687. func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request) {
  688. path := strings.TrimPrefix(r.URL.Path, "/api/local")
  689. if r.URL.Path == path { // missing prefix
  690. http.Error(w, "invalid request", http.StatusBadRequest)
  691. return
  692. }
  693. if !slices.Contains(localapiAllowlist, path) {
  694. http.Error(w, fmt.Sprintf("%s not allowed from localapi proxy", path), http.StatusForbidden)
  695. return
  696. }
  697. localAPIURL := "http://" + apitype.LocalAPIHost + "/localapi" + path
  698. req, err := http.NewRequestWithContext(r.Context(), r.Method, localAPIURL, r.Body)
  699. if err != nil {
  700. http.Error(w, "failed to construct request", http.StatusInternalServerError)
  701. return
  702. }
  703. // Make request to tailscaled localapi.
  704. resp, err := s.lc.DoLocalRequest(req)
  705. if err != nil {
  706. http.Error(w, err.Error(), resp.StatusCode)
  707. return
  708. }
  709. defer resp.Body.Close()
  710. // Send response back to web frontend.
  711. w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
  712. w.WriteHeader(resp.StatusCode)
  713. if _, err := io.Copy(w, resp.Body); err != nil {
  714. http.Error(w, err.Error(), http.StatusInternalServerError)
  715. }
  716. }
  717. // localapiAllowlist is an allowlist of localapi endpoints the
  718. // web client is allowed to proxy to the client's localapi.
  719. //
  720. // Rather than exposing all localapi endpoints over the proxy,
  721. // this limits to just the ones actually used from the web
  722. // client frontend.
  723. var localapiAllowlist = []string{
  724. "/v0/logout",
  725. "/v0/prefs",
  726. "/v0/update/check",
  727. "/v0/update/install",
  728. "/v0/update/progress",
  729. }
  730. // csrfKey returns a key that can be used for CSRF protection.
  731. // If an error occurs during key creation, the error is logged and the active process terminated.
  732. // If the server is running in CGI mode, the key is cached to disk and reused between requests.
  733. // If an error occurs during key storage, the error is logged and the active process terminated.
  734. func (s *Server) csrfKey() []byte {
  735. csrfFile := filepath.Join(os.TempDir(), "tailscale-web-csrf.key")
  736. // if running in CGI mode, try to read from disk, but ignore errors
  737. if s.cgiMode {
  738. key, _ := os.ReadFile(csrfFile)
  739. if len(key) == 32 {
  740. return key
  741. }
  742. }
  743. // create a new key
  744. key := make([]byte, 32)
  745. if _, err := rand.Read(key); err != nil {
  746. log.Fatalf("error generating CSRF key: %v", err)
  747. }
  748. // if running in CGI mode, try to write the newly created key to disk, and exit if it fails.
  749. if s.cgiMode {
  750. if err := os.WriteFile(csrfFile, key, 0600); err != nil {
  751. log.Fatalf("unable to store CSRF key: %v", err)
  752. }
  753. }
  754. return key
  755. }
  756. // enforcePrefix returns a HandlerFunc that enforces a given path prefix is used in requests,
  757. // then strips it before invoking h.
  758. // Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.
  759. // Instead, it returns a redirect to the prefix path.
  760. func enforcePrefix(prefix string, h http.HandlerFunc) http.HandlerFunc {
  761. if prefix == "" {
  762. return h
  763. }
  764. // ensure that prefix always has both a leading and trailing slash so
  765. // that relative links for JS and CSS assets work correctly.
  766. if !strings.HasPrefix(prefix, "/") {
  767. prefix = "/" + prefix
  768. }
  769. if !strings.HasSuffix(prefix, "/") {
  770. prefix += "/"
  771. }
  772. return func(w http.ResponseWriter, r *http.Request) {
  773. if !strings.HasPrefix(r.URL.Path, prefix) {
  774. http.Redirect(w, r, prefix, http.StatusFound)
  775. return
  776. }
  777. prefix = strings.TrimSuffix(prefix, "/")
  778. http.StripPrefix(prefix, h).ServeHTTP(w, r)
  779. }
  780. }
  781. func writeJSON(w http.ResponseWriter, data any) {
  782. w.Header().Set("Content-Type", "application/json")
  783. if err := json.NewEncoder(w).Encode(data); err != nil {
  784. w.Header().Set("Content-Type", "text/plain")
  785. http.Error(w, err.Error(), http.StatusInternalServerError)
  786. return
  787. }
  788. }