web.go 28 KB

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