| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- // Package localapi contains the HTTP server handlers for tailscaled's API server.
- package localapi
- import (
- "bytes"
- "cmp"
- "context"
- "crypto/sha256"
- "encoding/hex"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "maps"
- "mime"
- "mime/multipart"
- "net"
- "net/http"
- "net/http/httputil"
- "net/netip"
- "net/url"
- "os"
- "path"
- "runtime"
- "slices"
- "strconv"
- "strings"
- "sync"
- "time"
- "golang.org/x/net/dns/dnsmessage"
- "tailscale.com/client/tailscale/apitype"
- "tailscale.com/clientupdate"
- "tailscale.com/drive"
- "tailscale.com/envknob"
- "tailscale.com/hostinfo"
- "tailscale.com/ipn"
- "tailscale.com/ipn/ipnauth"
- "tailscale.com/ipn/ipnlocal"
- "tailscale.com/ipn/ipnstate"
- "tailscale.com/logtail"
- "tailscale.com/net/netmon"
- "tailscale.com/net/netutil"
- "tailscale.com/net/portmapper"
- "tailscale.com/tailcfg"
- "tailscale.com/taildrop"
- "tailscale.com/tka"
- "tailscale.com/tstime"
- "tailscale.com/types/dnstype"
- "tailscale.com/types/key"
- "tailscale.com/types/logger"
- "tailscale.com/types/logid"
- "tailscale.com/types/ptr"
- "tailscale.com/types/tkatype"
- "tailscale.com/util/clientmetric"
- "tailscale.com/util/httphdr"
- "tailscale.com/util/httpm"
- "tailscale.com/util/mak"
- "tailscale.com/util/osdiag"
- "tailscale.com/util/progresstracking"
- "tailscale.com/util/rands"
- "tailscale.com/util/syspolicy/rsop"
- "tailscale.com/util/syspolicy/setting"
- "tailscale.com/version"
- "tailscale.com/wgengine/magicsock"
- )
- type LocalAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
- // handler is the set of LocalAPI handlers, keyed by the part of the
- // Request.URL.Path after "/localapi/v0/". If the key ends with a trailing slash
- // then it's a prefix match.
- var handler = map[string]LocalAPIHandler{
- // The prefix match handlers end with a slash:
- "cert/": (*Handler).serveCert,
- "file-put/": (*Handler).serveFilePut,
- "files/": (*Handler).serveFiles,
- "policy/": (*Handler).servePolicy,
- "profiles/": (*Handler).serveProfiles,
- // The other /localapi/v0/NAME handlers are exact matches and contain only NAME
- // without a trailing slash:
- "alpha-set-device-attrs": (*Handler).serveSetDeviceAttrs, // see tailscale/corp#24690
- "bugreport": (*Handler).serveBugReport,
- "check-ip-forwarding": (*Handler).serveCheckIPForwarding,
- "check-prefs": (*Handler).serveCheckPrefs,
- "check-udp-gro-forwarding": (*Handler).serveCheckUDPGROForwarding,
- "component-debug-logging": (*Handler).serveComponentDebugLogging,
- "debug": (*Handler).serveDebug,
- "debug-derp-region": (*Handler).serveDebugDERPRegion,
- "debug-dial-types": (*Handler).serveDebugDialTypes,
- "debug-log": (*Handler).serveDebugLog,
- "debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
- "debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
- "debug-peer-endpoint-changes": (*Handler).serveDebugPeerEndpointChanges,
- "debug-portmap": (*Handler).serveDebugPortmap,
- "derpmap": (*Handler).serveDERPMap,
- "dev-set-state-store": (*Handler).serveDevSetStateStore,
- "dial": (*Handler).serveDial,
- "disconnect-control": (*Handler).disconnectControl,
- "dns-osconfig": (*Handler).serveDNSOSConfig,
- "dns-query": (*Handler).serveDNSQuery,
- "drive/fileserver-address": (*Handler).serveDriveServerAddr,
- "drive/shares": (*Handler).serveShares,
- "file-targets": (*Handler).serveFileTargets,
- "goroutines": (*Handler).serveGoroutines,
- "handle-push-message": (*Handler).serveHandlePushMessage,
- "id-token": (*Handler).serveIDToken,
- "login-interactive": (*Handler).serveLoginInteractive,
- "logout": (*Handler).serveLogout,
- "logtap": (*Handler).serveLogTap,
- "metrics": (*Handler).serveMetrics,
- "ping": (*Handler).servePing,
- "pprof": (*Handler).servePprof,
- "prefs": (*Handler).servePrefs,
- "query-feature": (*Handler).serveQueryFeature,
- "reload-config": (*Handler).reloadConfig,
- "reset-auth": (*Handler).serveResetAuth,
- "serve-config": (*Handler).serveServeConfig,
- "set-dns": (*Handler).serveSetDNS,
- "set-expiry-sooner": (*Handler).serveSetExpirySooner,
- "set-gui-visible": (*Handler).serveSetGUIVisible,
- "set-push-device-token": (*Handler).serveSetPushDeviceToken,
- "set-udp-gro-forwarding": (*Handler).serveSetUDPGROForwarding,
- "set-use-exit-node-enabled": (*Handler).serveSetUseExitNodeEnabled,
- "start": (*Handler).serveStart,
- "status": (*Handler).serveStatus,
- "suggest-exit-node": (*Handler).serveSuggestExitNode,
- "tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
- "tka/cosign-recovery-aum": (*Handler).serveTKACosignRecoveryAUM,
- "tka/disable": (*Handler).serveTKADisable,
- "tka/force-local-disable": (*Handler).serveTKALocalDisable,
- "tka/generate-recovery-aum": (*Handler).serveTKAGenerateRecoveryAUM,
- "tka/init": (*Handler).serveTKAInit,
- "tka/log": (*Handler).serveTKALog,
- "tka/modify": (*Handler).serveTKAModify,
- "tka/sign": (*Handler).serveTKASign,
- "tka/status": (*Handler).serveTKAStatus,
- "tka/submit-recovery-aum": (*Handler).serveTKASubmitRecoveryAUM,
- "tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
- "tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
- "update/check": (*Handler).serveUpdateCheck,
- "update/install": (*Handler).serveUpdateInstall,
- "update/progress": (*Handler).serveUpdateProgress,
- "upload-client-metrics": (*Handler).serveUploadClientMetrics,
- "usermetrics": (*Handler).serveUserMetrics,
- "watch-ipn-bus": (*Handler).serveWatchIPNBus,
- "whois": (*Handler).serveWhoIs,
- }
- // Register registers a new LocalAPI handler for the given name.
- func Register(name string, fn LocalAPIHandler) {
- if _, ok := handler[name]; ok {
- panic("duplicate LocalAPI handler registration: " + name)
- }
- handler[name] = fn
- }
- var (
- // The clientmetrics package is stateful, but we want to expose a simple
- // imperative API to local clients, so we need to keep track of
- // clientmetric.Metric instances that we've created for them. These need to
- // be globals because we end up creating many Handler instances for the
- // lifetime of a client.
- metricsMu sync.Mutex
- metrics = map[string]*clientmetric.Metric{}
- )
- // NewHandler creates a new LocalAPI HTTP handler. All parameters except netMon
- // are required (if non-nil it's used to do faster interface lookups).
- func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, logID logid.PublicID) *Handler {
- return &Handler{b: b, logf: logf, backendLogID: logID, clock: tstime.StdClock{}}
- }
- type Handler struct {
- // RequiredPassword, if non-empty, forces all HTTP
- // requests to have HTTP basic auth with this password.
- // It's used by the sandboxed macOS sameuserproof GUI auth mechanism.
- RequiredPassword string
- // PermitRead is whether read-only HTTP handlers are allowed.
- PermitRead bool
- // PermitWrite is whether mutating HTTP handlers are allowed.
- // If PermitWrite is true, everything is allowed.
- // It effectively means that the user is root or the admin
- // (operator user).
- PermitWrite bool
- // PermitCert is whether the client is additionally granted
- // cert fetching access.
- PermitCert bool
- // Actor is the identity of the client connected to the Handler.
- Actor ipnauth.Actor
- b *ipnlocal.LocalBackend
- logf logger.Logf
- backendLogID logid.PublicID
- clock tstime.Clock
- }
- func (h *Handler) LocalBackend() *ipnlocal.LocalBackend {
- return h.b
- }
- func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if h.b == nil {
- http.Error(w, "server has no local backend", http.StatusInternalServerError)
- return
- }
- if r.Referer() != "" || r.Header.Get("Origin") != "" || !h.validHost(r.Host) {
- metricInvalidRequests.Add(1)
- http.Error(w, "invalid localapi request", http.StatusForbidden)
- return
- }
- w.Header().Set("Tailscale-Version", version.Long())
- w.Header().Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion)))
- w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
- w.Header().Set("X-Frame-Options", "DENY")
- w.Header().Set("X-Content-Type-Options", "nosniff")
- if h.RequiredPassword != "" {
- _, pass, ok := r.BasicAuth()
- if !ok {
- metricInvalidRequests.Add(1)
- http.Error(w, "auth required", http.StatusUnauthorized)
- return
- }
- if pass != h.RequiredPassword {
- metricInvalidRequests.Add(1)
- http.Error(w, "bad password", http.StatusForbidden)
- return
- }
- }
- if fn, ok := handlerForPath(r.URL.Path); ok {
- fn(h, w, r)
- } else {
- http.NotFound(w, r)
- }
- }
- // validLocalHostForTesting allows loopback handlers without RequiredPassword for testing.
- var validLocalHostForTesting = false
- // validHost reports whether h is a valid Host header value for a LocalAPI request.
- func (h *Handler) validHost(hostname string) bool {
- // The client code sends a hostname of "local-tailscaled.sock".
- switch hostname {
- case "", apitype.LocalAPIHost:
- return true
- }
- if !validLocalHostForTesting && h.RequiredPassword == "" {
- return false // only allow localhost with basic auth or in tests
- }
- host, _, err := net.SplitHostPort(hostname)
- if err != nil {
- return false
- }
- if host == "localhost" {
- return true
- }
- addr, err := netip.ParseAddr(host)
- if err != nil {
- return false
- }
- return addr.IsLoopback()
- }
- // handlerForPath returns the LocalAPI handler for the provided Request.URI.Path.
- // (the path doesn't include any query parameters)
- func handlerForPath(urlPath string) (h LocalAPIHandler, ok bool) {
- if urlPath == "/" {
- return (*Handler).serveLocalAPIRoot, true
- }
- suff, ok := strings.CutPrefix(urlPath, "/localapi/v0/")
- if !ok {
- // Currently all LocalAPI methods start with "/localapi/v0/" to signal
- // to people that they're not necessarily stable APIs. In practice we'll
- // probably need to keep them pretty stable anyway, but for now treat
- // them as an internal implementation detail.
- return nil, false
- }
- if fn, ok := handler[suff]; ok {
- // Here we match exact handler suffixes like "status" or ones with a
- // slash already in their name, like "tka/status".
- return fn, true
- }
- // Otherwise, it might be a prefix match like "files/*" which we look up
- // by the prefix including first trailing slash.
- if i := strings.IndexByte(suff, '/'); i != -1 {
- suff = suff[:i+1]
- if fn, ok := handler[suff]; ok {
- return fn, true
- }
- }
- return nil, false
- }
- func (*Handler) serveLocalAPIRoot(w http.ResponseWriter, r *http.Request) {
- io.WriteString(w, "tailscaled\n")
- }
- // serveIDToken handles requests to get an OIDC ID token.
- func (h *Handler) serveIDToken(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "id-token access denied", http.StatusForbidden)
- return
- }
- nm := h.b.NetMap()
- if nm == nil {
- http.Error(w, "no netmap", http.StatusServiceUnavailable)
- return
- }
- aud := strings.TrimSpace(r.FormValue("aud"))
- if len(aud) == 0 {
- http.Error(w, "no audience requested", http.StatusBadRequest)
- return
- }
- req := &tailcfg.TokenRequest{
- CapVersion: tailcfg.CurrentCapabilityVersion,
- Audience: aud,
- NodeKey: nm.NodeKey,
- }
- b, err := json.Marshal(req)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- httpReq, err := http.NewRequest("POST", "https://unused/machine/id-token", bytes.NewReader(b))
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- resp, err := h.b.DoNoiseRequest(httpReq)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- defer resp.Body.Close()
- w.WriteHeader(resp.StatusCode)
- if _, err := io.Copy(w, resp.Body); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "bugreport access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
- return
- }
- defer h.b.TryFlushLogs() // kick off upload after bugreport's done logging
- logMarker := func() string {
- return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format("20060102150405Z"), rands.HexString(16))
- }
- if envknob.NoLogsNoSupport() {
- logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
- }
- startMarker := logMarker()
- h.logf("user bugreport: %s", startMarker)
- if note := r.URL.Query().Get("note"); len(note) > 0 {
- h.logf("user bugreport note: %s", note)
- }
- hi, _ := json.Marshal(hostinfo.New())
- h.logf("user bugreport hostinfo: %s", hi)
- if err := h.b.HealthTracker().OverallError(); err != nil {
- h.logf("user bugreport health: %s", err.Error())
- } else {
- h.logf("user bugreport health: ok")
- }
- // Information about the current node from the netmap
- if nm := h.b.NetMap(); nm != nil {
- if self := nm.SelfNode; self.Valid() {
- h.logf("user bugreport node info: nodeid=%q stableid=%q expiry=%q", self.ID(), self.StableID(), self.KeyExpiry().Format(time.RFC3339))
- }
- h.logf("user bugreport public keys: machine=%q node=%q", nm.MachineKey, nm.NodeKey)
- } else {
- h.logf("user bugreport netmap: no active netmap")
- }
- // Print all envknobs; we otherwise only print these on startup, and
- // printing them here ensures we don't have to go spelunking through
- // logs for them.
- envknob.LogCurrent(logger.WithPrefix(h.logf, "user bugreport: "))
- // OS-specific details
- h.logf.JSON(1, "UserBugReportOS", osdiag.SupportInfo(osdiag.LogSupportInfoReasonBugReport))
- if defBool(r.URL.Query().Get("diagnose"), false) {
- h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
- }
- w.Header().Set("Content-Type", "text/plain")
- fmt.Fprintln(w, startMarker)
- // Nothing else to do if we're not in record mode; we wrote the marker
- // above, so we can just finish our response now.
- if !defBool(r.URL.Query().Get("record"), false) {
- return
- }
- until := h.clock.Now().Add(12 * time.Hour)
- var changed map[string]bool
- for _, component := range []string{"magicsock"} {
- if h.b.GetComponentDebugLogging(component).IsZero() {
- if err := h.b.SetComponentDebugLogging(component, until); err != nil {
- h.logf("bugreport: error setting component %q logging: %v", component, err)
- continue
- }
- mak.Set(&changed, component, true)
- }
- }
- defer func() {
- for component := range changed {
- h.b.SetComponentDebugLogging(component, time.Time{})
- }
- }()
- // NOTE(andrew): if we have anything else we want to do while recording
- // a bugreport, we can add it here.
- // Read from the client; this will also return when the client closes
- // the connection.
- var buf [1]byte
- _, err := r.Body.Read(buf[:])
- switch {
- case err == nil:
- // good
- case errors.Is(err, io.EOF):
- // good
- case errors.Is(err, io.ErrUnexpectedEOF):
- // this happens when Ctrl-C'ing the tailscale client; don't
- // bother logging an error
- default:
- // Log but continue anyway.
- h.logf("user bugreport: error reading body: %v", err)
- }
- // Generate another log marker and return it to the client.
- endMarker := logMarker()
- h.logf("user bugreport end: %s", endMarker)
- fmt.Fprintln(w, endMarker)
- }
- func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
- h.serveWhoIsWithBackend(w, r, h.b)
- }
- // serveSetDeviceAttrs is (as of 2024-12-30) an experimental LocalAPI handler to
- // set device attributes via the control plane.
- //
- // See tailscale/corp#24690.
- func (h *Handler) serveSetDeviceAttrs(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- if !h.PermitWrite {
- http.Error(w, "set-device-attrs access denied", http.StatusForbidden)
- return
- }
- if r.Method != "PATCH" {
- http.Error(w, "only PATCH allowed", http.StatusMethodNotAllowed)
- return
- }
- var req map[string]any
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- if err := h.b.SetDeviceAttrs(ctx, req); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- io.WriteString(w, "{}\n")
- }
- // localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed
- // by the localapi WhoIs method.
- type localBackendWhoIsMethods interface {
- WhoIs(string, netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
- WhoIsNodeKey(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
- PeerCaps(netip.Addr) tailcfg.PeerCapMap
- }
- func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
- if !h.PermitRead {
- http.Error(w, "whois access denied", http.StatusForbidden)
- return
- }
- var (
- n tailcfg.NodeView
- u tailcfg.UserProfile
- ok bool
- )
- var ipp netip.AddrPort
- if v := r.FormValue("addr"); v != "" {
- if strings.HasPrefix(v, "nodekey:") {
- var k key.NodePublic
- if err := k.UnmarshalText([]byte(v)); err != nil {
- http.Error(w, "invalid nodekey in 'addr' parameter", http.StatusBadRequest)
- return
- }
- n, u, ok = b.WhoIsNodeKey(k)
- } else if ip, err := netip.ParseAddr(v); err == nil {
- ipp = netip.AddrPortFrom(ip, 0)
- } else {
- var err error
- ipp, err = netip.ParseAddrPort(v)
- if err != nil {
- http.Error(w, "invalid 'addr' parameter", http.StatusBadRequest)
- return
- }
- }
- if ipp.IsValid() {
- n, u, ok = b.WhoIs(r.FormValue("proto"), ipp)
- }
- } else {
- http.Error(w, "missing 'addr' parameter", http.StatusBadRequest)
- return
- }
- if !ok {
- http.Error(w, "no match for IP:port", http.StatusNotFound)
- return
- }
- res := &apitype.WhoIsResponse{
- Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
- UserProfile: &u, // always non-nil per WhoIsResponse contract
- }
- if n.Addresses().Len() > 0 {
- res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
- }
- j, err := json.MarshalIndent(res, "", "\t")
- if err != nil {
- http.Error(w, "JSON encoding error", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
- }
- func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
- // Require write access out of paranoia that the goroutine dump
- // (at least its arguments) might contain something sensitive.
- if !h.PermitWrite {
- http.Error(w, "goroutine dump access denied", http.StatusForbidden)
- return
- }
- buf := make([]byte, 2<<20)
- buf = buf[:runtime.Stack(buf, true)]
- w.Header().Set("Content-Type", "text/plain")
- w.Write(buf)
- }
- // serveLogTap taps into the tailscaled/logtail server output and streams
- // it to the client.
- func (h *Handler) serveLogTap(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- // Require write access (~root) as the logs could contain something
- // sensitive.
- if !h.PermitWrite {
- http.Error(w, "logtap access denied", http.StatusForbidden)
- return
- }
- if r.Method != "GET" {
- http.Error(w, "GET required", http.StatusMethodNotAllowed)
- return
- }
- f, ok := w.(http.Flusher)
- if !ok {
- http.Error(w, "streaming unsupported", http.StatusInternalServerError)
- return
- }
- io.WriteString(w, `{"text":"[logtap connected]\n"}`+"\n")
- f.Flush()
- msgc := make(chan string, 16)
- unreg := logtail.RegisterLogTap(msgc)
- defer unreg()
- for {
- select {
- case <-ctx.Done():
- return
- case msg := <-msgc:
- io.WriteString(w, msg)
- f.Flush()
- }
- }
- }
- func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
- metricDebugMetricsCalls.Add(1)
- // Require write access out of paranoia that the metrics
- // might contain something sensitive.
- if !h.PermitWrite {
- http.Error(w, "metric access denied", http.StatusForbidden)
- return
- }
- w.Header().Set("Content-Type", "text/plain")
- clientmetric.WritePrometheusExpositionFormat(w)
- }
- // serveUserMetrics returns user-facing metrics in Prometheus text
- // exposition format.
- func (h *Handler) serveUserMetrics(w http.ResponseWriter, r *http.Request) {
- metricUserMetricsCalls.Add(1)
- h.b.UserMetricsRegistry().Handler(w, r)
- }
- func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "debug access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "POST required", http.StatusMethodNotAllowed)
- return
- }
- // The action is normally in a POST form parameter, but
- // some actions (like "notify") want a full JSON body, so
- // permit some to have their action in a header.
- var action string
- switch v := r.Header.Get("Debug-Action"); v {
- case "notify":
- action = v
- default:
- action = r.FormValue("action")
- }
- var err error
- switch action {
- case "derp-set-homeless":
- h.b.MagicConn().SetHomeless(true)
- case "derp-unset-homeless":
- h.b.MagicConn().SetHomeless(false)
- case "rebind":
- err = h.b.DebugRebind()
- case "restun":
- err = h.b.DebugReSTUN()
- case "notify":
- var n ipn.Notify
- err = json.NewDecoder(r.Body).Decode(&n)
- if err != nil {
- break
- }
- h.b.DebugNotify(n)
- case "notify-last-netmap":
- h.b.DebugNotifyLastNetMap()
- case "break-tcp-conns":
- err = h.b.DebugBreakTCPConns()
- case "break-derp-conns":
- err = h.b.DebugBreakDERPConns()
- case "force-netmap-update":
- h.b.DebugForceNetmapUpdate()
- case "control-knobs":
- k := h.b.ControlKnobs()
- w.Header().Set("Content-Type", "application/json")
- err = json.NewEncoder(w).Encode(k.AsDebugJSON())
- if err == nil {
- return
- }
- case "pick-new-derp":
- err = h.b.DebugPickNewDERP()
- case "force-prefer-derp":
- var n int
- err = json.NewDecoder(r.Body).Decode(&n)
- if err != nil {
- break
- }
- h.b.DebugForcePreferDERP(n)
- case "":
- err = fmt.Errorf("missing parameter 'action'")
- default:
- err = fmt.Errorf("unknown action %q", action)
- }
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- w.Header().Set("Content-Type", "text/plain")
- io.WriteString(w, "done\n")
- }
- func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "debug access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "POST required", http.StatusMethodNotAllowed)
- return
- }
- if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "text/plain")
- io.WriteString(w, "done\n")
- }
- func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "debug access denied", http.StatusForbidden)
- return
- }
- nm := h.b.NetMap()
- if nm == nil {
- http.Error(w, "no netmap", http.StatusNotFound)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- enc := json.NewEncoder(w)
- enc.SetIndent("", "\t")
- enc.Encode(nm.PacketFilterRules)
- }
- func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "debug access denied", http.StatusForbidden)
- return
- }
- nm := h.b.NetMap()
- if nm == nil {
- http.Error(w, "no netmap", http.StatusNotFound)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- enc := json.NewEncoder(w)
- enc.SetIndent("", "\t")
- enc.Encode(nm.PacketFilter)
- }
- func (h *Handler) serveDebugPortmap(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "debug access denied", http.StatusForbidden)
- return
- }
- w.Header().Set("Content-Type", "text/plain")
- dur, err := time.ParseDuration(r.FormValue("duration"))
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- gwSelf := r.FormValue("gateway_and_self")
- // Update portmapper debug flags
- debugKnobs := &portmapper.DebugKnobs{VerboseLogs: true}
- switch r.FormValue("type") {
- case "":
- case "pmp":
- debugKnobs.DisablePCP = true
- debugKnobs.DisableUPnP = true
- case "pcp":
- debugKnobs.DisablePMP = true
- debugKnobs.DisableUPnP = true
- case "upnp":
- debugKnobs.DisablePCP = true
- debugKnobs.DisablePMP = true
- default:
- http.Error(w, "unknown portmap debug type", http.StatusBadRequest)
- return
- }
- if defBool(r.FormValue("log_http"), false) {
- debugKnobs.LogHTTP = true
- }
- var (
- logLock sync.Mutex
- handlerDone bool
- )
- logf := func(format string, args ...any) {
- if !strings.HasSuffix(format, "\n") {
- format = format + "\n"
- }
- logLock.Lock()
- defer logLock.Unlock()
- // The portmapper can call this log function after the HTTP
- // handler returns, which is not allowed and can cause a panic.
- // If this happens, ignore the log lines since this typically
- // occurs due to a client disconnect.
- if handlerDone {
- return
- }
- // Write and flush each line to the client so that output is streamed
- fmt.Fprintf(w, format, args...)
- if f, ok := w.(http.Flusher); ok {
- f.Flush()
- }
- }
- defer func() {
- logLock.Lock()
- handlerDone = true
- logLock.Unlock()
- }()
- ctx, cancel := context.WithTimeout(r.Context(), dur)
- defer cancel()
- done := make(chan bool, 1)
- var c *portmapper.Client
- c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), h.b.NetMon(), debugKnobs, h.b.ControlKnobs(), func() {
- logf("portmapping changed.")
- logf("have mapping: %v", c.HaveMapping())
- if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
- logf("cb: mapping: %v", ext)
- select {
- case done <- true:
- default:
- }
- return
- }
- logf("cb: no mapping")
- })
- defer c.Close()
- netMon, err := netmon.New(logger.WithPrefix(logf, "monitor: "))
- if err != nil {
- logf("error creating monitor: %v", err)
- return
- }
- gatewayAndSelfIP := func() (gw, self netip.Addr, ok bool) {
- if a, b, ok := strings.Cut(gwSelf, "/"); ok {
- gw = netip.MustParseAddr(a)
- self = netip.MustParseAddr(b)
- return gw, self, true
- }
- return netMon.GatewayAndSelfIP()
- }
- c.SetGatewayLookupFunc(gatewayAndSelfIP)
- gw, selfIP, ok := gatewayAndSelfIP()
- if !ok {
- logf("no gateway or self IP; %v", netMon.InterfaceState())
- return
- }
- logf("gw=%v; self=%v", gw, selfIP)
- uc, err := net.ListenPacket("udp", "0.0.0.0:0")
- if err != nil {
- return
- }
- defer uc.Close()
- c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
- res, err := c.Probe(ctx)
- if err != nil {
- logf("error in Probe: %v", err)
- return
- }
- logf("Probe: %+v", res)
- if !res.PCP && !res.PMP && !res.UPnP {
- logf("no portmapping services available")
- return
- }
- if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
- logf("mapping: %v", ext)
- } else {
- logf("no mapping")
- }
- select {
- case <-done:
- case <-ctx.Done():
- if r.Context().Err() == nil {
- logf("serveDebugPortmap: context done: %v", ctx.Err())
- } else {
- h.logf("serveDebugPortmap: context done: %v", ctx.Err())
- }
- }
- }
- func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "debug access denied", http.StatusForbidden)
- return
- }
- component := r.FormValue("component")
- secs, _ := strconv.Atoi(r.FormValue("secs"))
- err := h.b.SetComponentDebugLogging(component, h.clock.Now().Add(time.Duration(secs)*time.Second))
- var res struct {
- Error string
- }
- if err != nil {
- res.Error = err.Error()
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(res)
- }
- func (h *Handler) serveDebugDialTypes(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "debug-dial-types access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
- return
- }
- ip := r.FormValue("ip")
- port := r.FormValue("port")
- network := r.FormValue("network")
- addr := ip + ":" + port
- if _, err := netip.ParseAddrPort(addr); err != nil {
- w.WriteHeader(http.StatusBadRequest)
- fmt.Fprintf(w, "invalid address %q: %v", addr, err)
- return
- }
- ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
- defer cancel()
- var bareDialer net.Dialer
- dialer := h.b.Dialer()
- var peerDialer net.Dialer
- peerDialer.Control = dialer.PeerDialControlFunc()
- // Kick off a dial with each available dialer in parallel.
- dialers := []struct {
- name string
- dial func(context.Context, string, string) (net.Conn, error)
- }{
- {"SystemDial", dialer.SystemDial},
- {"UserDial", dialer.UserDial},
- {"PeerDial", peerDialer.DialContext},
- {"BareDial", bareDialer.DialContext},
- }
- type result struct {
- name string
- conn net.Conn
- err error
- }
- results := make(chan result, len(dialers))
- var wg sync.WaitGroup
- for _, dialer := range dialers {
- dialer := dialer // loop capture
- wg.Add(1)
- go func() {
- defer wg.Done()
- conn, err := dialer.dial(ctx, network, addr)
- results <- result{dialer.name, conn, err}
- }()
- }
- wg.Wait()
- for range len(dialers) {
- res := <-results
- fmt.Fprintf(w, "[%s] connected=%v err=%v\n", res.name, res.conn != nil, res.err)
- if res.conn != nil {
- res.conn.Close()
- }
- }
- }
- // servePprofFunc is the implementation of Handler.servePprof, after auth,
- // for platforms where we want to link it in.
- var servePprofFunc func(http.ResponseWriter, *http.Request)
- func (h *Handler) servePprof(w http.ResponseWriter, r *http.Request) {
- // Require write access out of paranoia that the profile dump
- // might contain something sensitive.
- if !h.PermitWrite {
- http.Error(w, "profile access denied", http.StatusForbidden)
- return
- }
- if servePprofFunc == nil {
- http.Error(w, "not implemented on this platform", http.StatusServiceUnavailable)
- return
- }
- servePprofFunc(w, r)
- }
- // disconnectControl is the handler for local API /disconnect-control endpoint that shuts down control client, so that
- // node no longer communicates with control. Doing this makes control consider this node inactive. This can be used
- // before shutting down a replica of HA subnet router or app connector deployments to ensure that control tells the
- // peers to switch over to another replica whilst still maintaining th existing peer connections.
- func (h *Handler) disconnectControl(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- h.b.DisconnectControl()
- }
- func (h *Handler) reloadConfig(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- ok, err := h.b.ReloadConfig()
- var res apitype.ReloadConfigResponse
- res.Reloaded = ok
- if err != nil {
- res.Err = err.Error()
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(&res)
- }
- func (h *Handler) serveResetAuth(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "reset-auth modify access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- if err := h.b.ResetAuth(); err != nil {
- http.Error(w, "reset-auth failed: "+err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- }
- func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
- switch r.Method {
- case "GET":
- if !h.PermitRead {
- http.Error(w, "serve config denied", http.StatusForbidden)
- return
- }
- config := h.b.ServeConfig()
- bts, err := json.Marshal(config)
- if err != nil {
- http.Error(w, "error encoding config: "+err.Error(), http.StatusInternalServerError)
- return
- }
- sum := sha256.Sum256(bts)
- etag := hex.EncodeToString(sum[:])
- w.Header().Set("Etag", etag)
- w.Header().Set("Content-Type", "application/json")
- w.Write(bts)
- case "POST":
- if !h.PermitWrite {
- http.Error(w, "serve config denied", http.StatusForbidden)
- return
- }
- configIn := new(ipn.ServeConfig)
- if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
- writeErrorJSON(w, fmt.Errorf("decoding config: %w", err))
- return
- }
- // require a local admin when setting a path handler
- // TODO: roll-up this Windows-specific check into either PermitWrite
- // or a global admin escalation check.
- if err := authorizeServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h); err != nil {
- http.Error(w, err.Error(), http.StatusUnauthorized)
- return
- }
- etag := r.Header.Get("If-Match")
- if err := h.b.SetServeConfig(configIn, etag); err != nil {
- if errors.Is(err, ipnlocal.ErrETagMismatch) {
- http.Error(w, err.Error(), http.StatusPreconditionFailed)
- return
- }
- writeErrorJSON(w, fmt.Errorf("updating config: %w", err))
- return
- }
- w.WriteHeader(http.StatusOK)
- default:
- http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
- }
- }
- func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) error {
- switch goos {
- case "windows", "linux", "darwin", "illumos", "solaris":
- default:
- return nil
- }
- // Only check for local admin on tailscaled-on-mac (based on "sudo"
- // permissions). On sandboxed variants (MacSys and AppStore), tailscaled
- // cannot serve files outside of the sandbox and this check is not
- // relevant.
- if goos == "darwin" && version.IsSandboxedMacOS() {
- return nil
- }
- if !configIn.HasPathHandler() {
- return nil
- }
- if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
- return nil
- }
- switch goos {
- case "windows":
- return errors.New("must be a Windows local admin to serve a path")
- case "linux", "darwin", "illumos", "solaris":
- return errors.New("must be root, or be an operator and able to run 'sudo tailscale' to serve a path")
- default:
- // We filter goos at the start of the func, this default case
- // should never happen.
- panic("unreachable")
- }
- }
- func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
- return
- }
- var warning string
- if err := h.b.CheckIPForwarding(); err != nil {
- warning = err.Error()
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(struct {
- Warning string
- }{
- Warning: warning,
- })
- }
- func (h *Handler) serveCheckUDPGROForwarding(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "UDP GRO forwarding check access denied", http.StatusForbidden)
- return
- }
- var warning string
- if err := h.b.CheckUDPGROForwarding(); err != nil {
- warning = err.Error()
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(struct {
- Warning string
- }{
- Warning: warning,
- })
- }
- func (h *Handler) serveSetUDPGROForwarding(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "UDP GRO forwarding set access denied", http.StatusForbidden)
- return
- }
- var warning string
- if err := h.b.SetUDPGROForwarding(); err != nil {
- warning = err.Error()
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(struct {
- Warning string
- }{
- Warning: warning,
- })
- }
- func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "status access denied", http.StatusForbidden)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- var st *ipnstate.Status
- if defBool(r.FormValue("peers"), true) {
- st = h.b.Status()
- } else {
- st = h.b.StatusWithoutPeers()
- }
- e := json.NewEncoder(w)
- e.SetIndent("", "\t")
- e.Encode(st)
- }
- func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "status access denied", http.StatusForbidden)
- return
- }
- ipStr := r.FormValue("ip")
- if ipStr == "" {
- http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
- return
- }
- ip, err := netip.ParseAddr(ipStr)
- if err != nil {
- http.Error(w, "invalid IP", http.StatusBadRequest)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- e := json.NewEncoder(w)
- e.SetIndent("", "\t")
- e.Encode(chs)
- }
- // InUseOtherUserIPNStream reports whether r is a request for the watch-ipn-bus
- // handler. If so, it writes an ipn.Notify InUseOtherUser message to the user
- // and returns true. Otherwise it returns false, in which case it doesn't write
- // to w.
- //
- // Unlike the regular watch-ipn-bus handler, this one doesn't block. The caller
- // (in ipnserver.Server) provides the blocking until the connection is no longer
- // in use.
- func InUseOtherUserIPNStream(w http.ResponseWriter, r *http.Request, err error) (handled bool) {
- if r.Method != "GET" || r.URL.Path != "/localapi/v0/watch-ipn-bus" {
- return false
- }
- js, err := json.Marshal(&ipn.Notify{
- Version: version.Long(),
- State: ptr.To(ipn.InUseOtherUser),
- ErrMessage: ptr.To(err.Error()),
- })
- if err != nil {
- return false
- }
- js = append(js, '\n')
- w.Header().Set("Content-Type", "application/json")
- w.Write(js)
- return true
- }
- func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "watch ipn bus access denied", http.StatusForbidden)
- return
- }
- f, ok := w.(http.Flusher)
- if !ok {
- http.Error(w, "not a flusher", http.StatusInternalServerError)
- return
- }
- var mask ipn.NotifyWatchOpt
- if s := r.FormValue("mask"); s != "" {
- v, err := strconv.ParseUint(s, 10, 64)
- if err != nil {
- http.Error(w, "bad mask", http.StatusBadRequest)
- return
- }
- mask = ipn.NotifyWatchOpt(v)
- }
- // Users with only read access must request private key filtering. If they
- // don't filter out private keys, require write access.
- if (mask & ipn.NotifyNoPrivateKeys) == 0 {
- if !h.PermitWrite {
- http.Error(w, "watch IPN bus access denied, must set ipn.NotifyNoPrivateKeys when not running as admin/root or operator", http.StatusForbidden)
- return
- }
- }
- w.Header().Set("Content-Type", "application/json")
- ctx := r.Context()
- enc := json.NewEncoder(w)
- h.b.WatchNotificationsAs(ctx, h.Actor, mask, f.Flush, func(roNotify *ipn.Notify) (keepGoing bool) {
- err := enc.Encode(roNotify)
- if err != nil {
- h.logf("json.Encode: %v", err)
- return false
- }
- f.Flush()
- return true
- })
- }
- func (h *Handler) serveLoginInteractive(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "login access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "want POST", http.StatusBadRequest)
- return
- }
- h.b.StartLoginInteractiveAs(r.Context(), h.Actor)
- w.WriteHeader(http.StatusNoContent)
- return
- }
- func (h *Handler) serveStart(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "want POST", http.StatusBadRequest)
- return
- }
- var o ipn.Options
- if err := json.NewDecoder(r.Body).Decode(&o); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- err := h.b.Start(o)
- if err != nil {
- // TODO(bradfitz): map error to a good HTTP error
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- }
- func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "logout access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "want POST", http.StatusBadRequest)
- return
- }
- err := h.b.Logout(r.Context())
- if err == nil {
- w.WriteHeader(http.StatusNoContent)
- return
- }
- http.Error(w, err.Error(), http.StatusInternalServerError)
- }
- func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "prefs access denied", http.StatusForbidden)
- return
- }
- var prefs ipn.PrefsView
- switch r.Method {
- case "PATCH":
- if !h.PermitWrite {
- http.Error(w, "prefs write access denied", http.StatusForbidden)
- return
- }
- mp := new(ipn.MaskedPrefs)
- if err := json.NewDecoder(r.Body).Decode(mp); err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- if err := h.b.MaybeClearAppConnector(mp); err != nil {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusInternalServerError)
- json.NewEncoder(w).Encode(resJSON{Error: err.Error()})
- return
- }
- var err error
- prefs, err = h.b.EditPrefsAs(mp, h.Actor)
- if err != nil {
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusBadRequest)
- json.NewEncoder(w).Encode(resJSON{Error: err.Error()})
- return
- }
- case "GET", "HEAD":
- prefs = h.b.Prefs()
- default:
- http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- e := json.NewEncoder(w)
- e.SetIndent("", "\t")
- e.Encode(prefs)
- }
- func (h *Handler) servePolicy(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "policy access denied", http.StatusForbidden)
- return
- }
- suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/policy/")
- if !ok {
- http.Error(w, "misconfigured", http.StatusInternalServerError)
- return
- }
- var scope setting.PolicyScope
- if suffix == "" {
- scope = setting.DefaultScope()
- } else if err := scope.UnmarshalText([]byte(suffix)); err != nil {
- http.Error(w, fmt.Sprintf("%q is not a valid scope", suffix), http.StatusBadRequest)
- return
- }
- policy, err := rsop.PolicyFor(scope)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- var effectivePolicy *setting.Snapshot
- switch r.Method {
- case "GET":
- effectivePolicy = policy.Get()
- case "POST":
- effectivePolicy, err = policy.Reload()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- default:
- http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- e := json.NewEncoder(w)
- e.SetIndent("", "\t")
- e.Encode(effectivePolicy)
- }
- type resJSON struct {
- Error string `json:",omitempty"`
- }
- func (h *Handler) serveCheckPrefs(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "checkprefs access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
- p := new(ipn.Prefs)
- if err := json.NewDecoder(r.Body).Decode(p); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- err := h.b.CheckPrefs(p)
- var res resJSON
- if err != nil {
- res.Error = err.Error()
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(res)
- }
- func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "file access denied", http.StatusForbidden)
- return
- }
- suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/files/")
- if !ok {
- http.Error(w, "misconfigured", http.StatusInternalServerError)
- return
- }
- if suffix == "" {
- if r.Method != "GET" {
- http.Error(w, "want GET to list files", http.StatusBadRequest)
- return
- }
- ctx := r.Context()
- if s := r.FormValue("waitsec"); s != "" && s != "0" {
- d, err := strconv.Atoi(s)
- if err != nil {
- http.Error(w, "invalid waitsec", http.StatusBadRequest)
- return
- }
- deadline := time.Now().Add(time.Duration(d) * time.Second)
- var cancel context.CancelFunc
- ctx, cancel = context.WithDeadline(ctx, deadline)
- defer cancel()
- }
- wfs, err := h.b.AwaitWaitingFiles(ctx)
- if err != nil && ctx.Err() == nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(wfs)
- return
- }
- name, err := url.PathUnescape(suffix)
- if err != nil {
- http.Error(w, "bad filename", http.StatusBadRequest)
- return
- }
- if r.Method == "DELETE" {
- if err := h.b.DeleteFile(name); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- return
- }
- rc, size, err := h.b.OpenFile(name)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- defer rc.Close()
- w.Header().Set("Content-Length", fmt.Sprint(size))
- w.Header().Set("Content-Type", "application/octet-stream")
- io.Copy(w, rc)
- }
- func writeErrorJSON(w http.ResponseWriter, err error) {
- if err == nil {
- err = errors.New("unexpected nil error")
- }
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(http.StatusInternalServerError)
- type E struct {
- Error string `json:"error"`
- }
- json.NewEncoder(w).Encode(E{err.Error()})
- }
- func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != "GET" {
- http.Error(w, "want GET to list targets", http.StatusBadRequest)
- return
- }
- fts, err := h.b.FileTargets()
- if err != nil {
- writeErrorJSON(w, err)
- return
- }
- mak.NonNilSliceForJSON(&fts)
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(fts)
- }
- // serveFilePut sends a file to another node.
- //
- // It's sometimes possible for clients to do this themselves, without
- // tailscaled, except in the case of tailscaled running in
- // userspace-networking ("netstack") mode, in which case tailscaled
- // needs to a do a netstack dial out.
- //
- // Instead, the CLI also goes through tailscaled so it doesn't need to be
- // aware of the network mode in use.
- //
- // macOS/iOS have always used this localapi method to simplify the GUI
- // clients.
- //
- // The Windows client currently (2021-11-30) uses the peerapi (/v0/put/)
- // directly, as the Windows GUI always runs in tun mode anyway.
- //
- // In addition to single file PUTs, this endpoint accepts multipart file
- // POSTS encoded as multipart/form-data.The first part should be an
- // application/json file that contains a manifest consisting of a JSON array of
- // OutgoingFiles which wecan use for tracking progress even before reading the
- // file parts.
- //
- // URL format:
- //
- // - PUT /localapi/v0/file-put/:stableID/:escaped-filename
- // - POST /localapi/v0/file-put/:stableID
- func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
- metricFilePutCalls.Add(1)
- if !h.PermitWrite {
- http.Error(w, "file access denied", http.StatusForbidden)
- return
- }
- if r.Method != "PUT" && r.Method != "POST" {
- http.Error(w, "want PUT to put file", http.StatusBadRequest)
- return
- }
- fts, err := h.b.FileTargets()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- upath, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/file-put/")
- if !ok {
- http.Error(w, "misconfigured", http.StatusInternalServerError)
- return
- }
- var peerIDStr, filenameEscaped string
- if r.Method == "PUT" {
- ok := false
- peerIDStr, filenameEscaped, ok = strings.Cut(upath, "/")
- if !ok {
- http.Error(w, "bogus URL", http.StatusBadRequest)
- return
- }
- } else {
- peerIDStr = upath
- }
- peerID := tailcfg.StableNodeID(peerIDStr)
- var ft *apitype.FileTarget
- for _, x := range fts {
- if x.Node.StableID == peerID {
- ft = x
- break
- }
- }
- if ft == nil {
- http.Error(w, "node not found", http.StatusNotFound)
- return
- }
- dstURL, err := url.Parse(ft.PeerAPIURL)
- if err != nil {
- http.Error(w, "bogus peer URL", http.StatusInternalServerError)
- return
- }
- // Periodically report progress of outgoing files.
- outgoingFiles := make(map[string]*ipn.OutgoingFile)
- t := time.NewTicker(1 * time.Second)
- progressUpdates := make(chan ipn.OutgoingFile)
- defer close(progressUpdates)
- go func() {
- defer t.Stop()
- defer h.b.UpdateOutgoingFiles(outgoingFiles)
- for {
- select {
- case u, ok := <-progressUpdates:
- if !ok {
- return
- }
- outgoingFiles[u.ID] = &u
- case <-t.C:
- h.b.UpdateOutgoingFiles(outgoingFiles)
- }
- }
- }()
- switch r.Method {
- case "PUT":
- file := ipn.OutgoingFile{
- ID: rands.HexString(30),
- PeerID: peerID,
- Name: filenameEscaped,
- DeclaredSize: r.ContentLength,
- }
- h.singleFilePut(r.Context(), progressUpdates, w, r.Body, dstURL, file)
- case "POST":
- h.multiFilePost(progressUpdates, w, r, peerID, dstURL)
- default:
- http.Error(w, "want PUT to put file", http.StatusBadRequest)
- return
- }
- }
- func (h *Handler) multiFilePost(progressUpdates chan (ipn.OutgoingFile), w http.ResponseWriter, r *http.Request, peerID tailcfg.StableNodeID, dstURL *url.URL) {
- _, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
- if err != nil {
- http.Error(w, fmt.Sprintf("invalid Content-Type for multipart POST: %s", err), http.StatusBadRequest)
- return
- }
- ww := &multiFilePostResponseWriter{}
- defer func() {
- if err := ww.Flush(w); err != nil {
- h.logf("error: multiFilePostResponseWriter.Flush(): %s", err)
- }
- }()
- outgoingFilesByName := make(map[string]ipn.OutgoingFile)
- first := true
- mr := multipart.NewReader(r.Body, params["boundary"])
- for {
- part, err := mr.NextPart()
- if err == io.EOF {
- // No more parts.
- return
- } else if err != nil {
- http.Error(ww, fmt.Sprintf("failed to decode multipart/form-data: %s", err), http.StatusBadRequest)
- return
- }
- if first {
- first = false
- if part.Header.Get("Content-Type") != "application/json" {
- http.Error(ww, "first MIME part must be a JSON map of filename -> size", http.StatusBadRequest)
- return
- }
- var manifest []ipn.OutgoingFile
- err := json.NewDecoder(part).Decode(&manifest)
- if err != nil {
- http.Error(ww, fmt.Sprintf("invalid manifest: %s", err), http.StatusBadRequest)
- return
- }
- for _, file := range manifest {
- outgoingFilesByName[file.Name] = file
- progressUpdates <- file
- }
- continue
- }
- if !h.singleFilePut(r.Context(), progressUpdates, ww, part, dstURL, outgoingFilesByName[part.FileName()]) {
- return
- }
- if ww.statusCode >= 400 {
- // put failed, stop immediately
- h.logf("error: singleFilePut: failed with status %d", ww.statusCode)
- return
- }
- }
- }
- // multiFilePostResponseWriter is a buffering http.ResponseWriter that can be
- // reused across multiple singleFilePut calls and then flushed to the client
- // when all files have been PUT.
- type multiFilePostResponseWriter struct {
- header http.Header
- statusCode int
- body *bytes.Buffer
- }
- func (ww *multiFilePostResponseWriter) Header() http.Header {
- if ww.header == nil {
- ww.header = make(http.Header)
- }
- return ww.header
- }
- func (ww *multiFilePostResponseWriter) WriteHeader(statusCode int) {
- ww.statusCode = statusCode
- }
- func (ww *multiFilePostResponseWriter) Write(p []byte) (int, error) {
- if ww.body == nil {
- ww.body = bytes.NewBuffer(nil)
- }
- return ww.body.Write(p)
- }
- func (ww *multiFilePostResponseWriter) Flush(w http.ResponseWriter) error {
- if ww.header != nil {
- maps.Copy(w.Header(), ww.header)
- }
- if ww.statusCode > 0 {
- w.WriteHeader(ww.statusCode)
- }
- if ww.body != nil {
- _, err := io.Copy(w, ww.body)
- return err
- }
- return nil
- }
- func (h *Handler) singleFilePut(
- ctx context.Context,
- progressUpdates chan (ipn.OutgoingFile),
- w http.ResponseWriter,
- body io.Reader,
- dstURL *url.URL,
- outgoingFile ipn.OutgoingFile,
- ) bool {
- outgoingFile.Started = time.Now()
- body = progresstracking.NewReader(body, 1*time.Second, func(n int, err error) {
- outgoingFile.Sent = int64(n)
- progressUpdates <- outgoingFile
- })
- fail := func() {
- outgoingFile.Finished = true
- outgoingFile.Succeeded = false
- progressUpdates <- outgoingFile
- }
- // Before we PUT a file we check to see if there are any existing partial file and if so,
- // we resume the upload from where we left off by sending the remaining file instead of
- // the full file.
- var offset int64
- var resumeDuration time.Duration
- remainingBody := io.Reader(body)
- client := &http.Client{
- Transport: h.b.Dialer().PeerAPITransport(),
- Timeout: 10 * time.Second,
- }
- req, err := http.NewRequestWithContext(ctx, "GET", dstURL.String()+"/v0/put/"+outgoingFile.Name, nil)
- if err != nil {
- http.Error(w, "bogus peer URL", http.StatusInternalServerError)
- fail()
- return false
- }
- switch resp, err := client.Do(req); {
- case err != nil:
- h.logf("could not fetch remote hashes: %v", err)
- case resp.StatusCode == http.StatusMethodNotAllowed || resp.StatusCode == http.StatusNotFound:
- // noop; implies older peerapi without resume support
- case resp.StatusCode != http.StatusOK:
- h.logf("fetch remote hashes status code: %d", resp.StatusCode)
- default:
- resumeStart := time.Now()
- dec := json.NewDecoder(resp.Body)
- offset, remainingBody, err = taildrop.ResumeReader(body, func() (out taildrop.BlockChecksum, err error) {
- err = dec.Decode(&out)
- return out, err
- })
- if err != nil {
- h.logf("reader could not be fully resumed: %v", err)
- }
- resumeDuration = time.Since(resumeStart).Round(time.Millisecond)
- }
- outReq, err := http.NewRequestWithContext(ctx, "PUT", "http://peer/v0/put/"+outgoingFile.Name, remainingBody)
- if err != nil {
- http.Error(w, "bogus outreq", http.StatusInternalServerError)
- fail()
- return false
- }
- outReq.ContentLength = outgoingFile.DeclaredSize
- if offset > 0 {
- h.logf("resuming put at offset %d after %v", offset, resumeDuration)
- rangeHdr, _ := httphdr.FormatRange([]httphdr.Range{{Start: offset, Length: 0}})
- outReq.Header.Set("Range", rangeHdr)
- if outReq.ContentLength >= 0 {
- outReq.ContentLength -= offset
- }
- }
- rp := httputil.NewSingleHostReverseProxy(dstURL)
- rp.Transport = h.b.Dialer().PeerAPITransport()
- rp.ServeHTTP(w, outReq)
- outgoingFile.Finished = true
- outgoingFile.Succeeded = true
- progressUpdates <- outgoingFile
- return true
- }
- func (h *Handler) serveSetDNS(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "want POST", http.StatusBadRequest)
- return
- }
- ctx := r.Context()
- err := h.b.SetDNS(ctx, r.FormValue("name"), r.FormValue("value"))
- if err != nil {
- writeErrorJSON(w, err)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(struct{}{})
- }
- func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
- if r.Method != "GET" {
- http.Error(w, "want GET", http.StatusBadRequest)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- e := json.NewEncoder(w)
- e.SetIndent("", "\t")
- e.Encode(h.b.DERPMap())
- }
- // serveSetExpirySooner sets the expiry date on the current machine, specified
- // by an `expiry` unix timestamp as POST or query param.
- func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "POST required", http.StatusMethodNotAllowed)
- return
- }
- var expiryTime time.Time
- if v := r.FormValue("expiry"); v != "" {
- expiryInt, err := strconv.ParseInt(v, 10, 64)
- if err != nil {
- http.Error(w, "can't parse expiry time, expects a unix timestamp", http.StatusBadRequest)
- return
- }
- expiryTime = time.Unix(expiryInt, 0)
- } else {
- http.Error(w, "missing 'expiry' parameter, a unix timestamp", http.StatusBadRequest)
- return
- }
- err := h.b.SetExpirySooner(r.Context(), expiryTime)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- w.Header().Set("Content-Type", "text/plain")
- io.WriteString(w, "done\n")
- }
- func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
- ctx := r.Context()
- if r.Method != "POST" {
- http.Error(w, "want POST", http.StatusBadRequest)
- return
- }
- ipStr := r.FormValue("ip")
- if ipStr == "" {
- http.Error(w, "missing 'ip' parameter", http.StatusBadRequest)
- return
- }
- ip, err := netip.ParseAddr(ipStr)
- if err != nil {
- http.Error(w, "invalid IP", http.StatusBadRequest)
- return
- }
- pingTypeStr := r.FormValue("type")
- if pingTypeStr == "" {
- http.Error(w, "missing 'type' parameter", http.StatusBadRequest)
- return
- }
- size := 0
- sizeStr := r.FormValue("size")
- if sizeStr != "" {
- size, err = strconv.Atoi(sizeStr)
- if err != nil {
- http.Error(w, "invalid 'size' parameter", http.StatusBadRequest)
- return
- }
- if size != 0 && tailcfg.PingType(pingTypeStr) != tailcfg.PingDisco {
- http.Error(w, "'size' parameter is only supported with disco pings", http.StatusBadRequest)
- return
- }
- if size > magicsock.MaxDiscoPingSize {
- http.Error(w, fmt.Sprintf("maximum value for 'size' is %v", magicsock.MaxDiscoPingSize), http.StatusBadRequest)
- return
- }
- }
- res, err := h.b.Ping(ctx, ip, tailcfg.PingType(pingTypeStr), size)
- if err != nil {
- writeErrorJSON(w, err)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(res)
- }
- func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
- if r.Method != "POST" {
- http.Error(w, "POST required", http.StatusMethodNotAllowed)
- return
- }
- const upgradeProto = "ts-dial"
- if !strings.Contains(r.Header.Get("Connection"), "upgrade") ||
- r.Header.Get("Upgrade") != upgradeProto {
- http.Error(w, "bad ts-dial upgrade", http.StatusBadRequest)
- return
- }
- hostStr, portStr := r.Header.Get("Dial-Host"), r.Header.Get("Dial-Port")
- if hostStr == "" || portStr == "" {
- http.Error(w, "missing Dial-Host or Dial-Port header", http.StatusBadRequest)
- return
- }
- hijacker, ok := w.(http.Hijacker)
- if !ok {
- http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
- return
- }
- network := cmp.Or(r.Header.Get("Dial-Network"), "tcp")
- addr := net.JoinHostPort(hostStr, portStr)
- outConn, err := h.b.Dialer().UserDial(r.Context(), network, addr)
- if err != nil {
- http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
- return
- }
- defer outConn.Close()
- w.Header().Set("Upgrade", upgradeProto)
- w.Header().Set("Connection", "upgrade")
- w.WriteHeader(http.StatusSwitchingProtocols)
- reqConn, brw, err := hijacker.Hijack()
- if err != nil {
- h.logf("localapi dial Hijack error: %v", err)
- return
- }
- defer reqConn.Close()
- if err := brw.Flush(); err != nil {
- return
- }
- reqConn = netutil.NewDrainBufConn(reqConn, brw.Reader)
- errc := make(chan error, 1)
- go func() {
- _, err := io.Copy(reqConn, outConn)
- errc <- err
- }()
- go func() {
- _, err := io.Copy(outConn, reqConn)
- errc <- err
- }()
- <-errc
- }
- func (h *Handler) serveSetPushDeviceToken(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "set push device token access denied", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
- var params apitype.SetPushDeviceTokenRequest
- if err := json.NewDecoder(r.Body).Decode(¶ms); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- h.b.SetPushDeviceToken(params.PushDeviceToken)
- w.WriteHeader(http.StatusOK)
- }
- func (h *Handler) serveHandlePushMessage(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "handle push message not allowed", http.StatusForbidden)
- return
- }
- if r.Method != "POST" {
- http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
- var pushMessageBody map[string]any
- if err := json.NewDecoder(r.Body).Decode(&pushMessageBody); err != nil {
- http.Error(w, "failed to decode JSON body: "+err.Error(), http.StatusBadRequest)
- return
- }
- // TODO(bradfitz): do something with pushMessageBody
- h.logf("localapi: got push message: %v", logger.AsJSON(pushMessageBody))
- w.WriteHeader(http.StatusNoContent)
- }
- func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Request) {
- if r.Method != "POST" {
- http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
- return
- }
- type clientMetricJSON struct {
- Name string `json:"name"`
- Type string `json:"type"` // one of "counter" or "gauge"
- Value int `json:"value"` // amount to increment metric by
- }
- var clientMetrics []clientMetricJSON
- if err := json.NewDecoder(r.Body).Decode(&clientMetrics); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- metricsMu.Lock()
- defer metricsMu.Unlock()
- for _, m := range clientMetrics {
- if metric, ok := metrics[m.Name]; ok {
- metric.Add(int64(m.Value))
- } else {
- if clientmetric.HasPublished(m.Name) {
- http.Error(w, "Already have a metric named "+m.Name, http.StatusBadRequest)
- return
- }
- var metric *clientmetric.Metric
- switch m.Type {
- case "counter":
- metric = clientmetric.NewCounter(m.Name)
- case "gauge":
- metric = clientmetric.NewGauge(m.Name)
- default:
- http.Error(w, "Unknown metric type "+m.Type, http.StatusBadRequest)
- return
- }
- metrics[m.Name] = metric
- metric.Add(int64(m.Value))
- }
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(struct{}{})
- }
- func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "lock status access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.GET {
- http.Error(w, "use GET", http.StatusMethodNotAllowed)
- return
- }
- j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
- if err != nil {
- http.Error(w, "JSON encoding error", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
- }
- func (h *Handler) serveSetGUIVisible(w http.ResponseWriter, r *http.Request) {
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- type setGUIVisibleRequest struct {
- IsVisible bool // whether the Tailscale client UI is now presented to the user
- SessionID string // the last SessionID sent to the client in ipn.Notify.SessionID
- }
- var req setGUIVisibleRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- // TODO(bradfitz): use `req.IsVisible == true` to flush netmap
- w.WriteHeader(http.StatusOK)
- }
- func (h *Handler) serveSetUseExitNodeEnabled(w http.ResponseWriter, r *http.Request) {
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- v, err := strconv.ParseBool(r.URL.Query().Get("enabled"))
- if err != nil {
- http.Error(w, "invalid 'enabled' parameter", http.StatusBadRequest)
- return
- }
- prefs, err := h.b.SetUseExitNodeEnabled(v)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- e := json.NewEncoder(w)
- e.SetIndent("", "\t")
- e.Encode(prefs)
- }
- func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "lock sign access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- type signRequest struct {
- NodeKey key.NodePublic
- RotationPublic []byte
- }
- var req signRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- if err := h.b.NetworkLockSign(req.NodeKey, req.RotationPublic); err != nil {
- http.Error(w, "signing failed: "+err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusOK)
- }
- func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "lock init access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- type initRequest struct {
- Keys []tka.Key
- DisablementValues [][]byte
- SupportDisablement []byte
- }
- var req initRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); err != nil {
- http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
- return
- }
- j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
- if err != nil {
- http.Error(w, "JSON encoding error", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
- }
- func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "network-lock modify access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- type modifyRequest struct {
- AddKeys []tka.Key
- RemoveKeys []tka.Key
- }
- var req modifyRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
- http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(204)
- }
- func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "network-lock modify access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- type wrapRequest struct {
- TSKey string
- TKAKey string // key.NLPrivate.MarshalText
- }
- var req wrapRequest
- if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 12*1024)).Decode(&req); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- var priv key.NLPrivate
- if err := priv.UnmarshalText([]byte(req.TKAKey)); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- wrappedKey, err := h.b.NetworkLockWrapPreauthKey(req.TSKey, priv)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusOK)
- w.Write([]byte(wrappedKey))
- }
- func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "signing deeplink verification access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- type verifyRequest struct {
- URL string
- }
- var req verifyRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
- return
- }
- res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
- j, err := json.MarshalIndent(res, "", "\t")
- if err != nil {
- http.Error(w, "JSON encoding error", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
- }
- func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "network-lock modify access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- body := io.LimitReader(r.Body, 1024*1024)
- secret, err := io.ReadAll(body)
- if err != nil {
- http.Error(w, "reading secret", http.StatusBadRequest)
- return
- }
- if err := h.b.NetworkLockDisable(secret); err != nil {
- http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
- return
- }
- w.WriteHeader(http.StatusOK)
- }
- func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "network-lock modify access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- // Require a JSON stanza for the body as an additional CSRF protection.
- var req struct{}
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- if err := h.b.NetworkLockForceLocalDisable(); err != nil {
- http.Error(w, "network-lock local disable failed: "+err.Error(), http.StatusBadRequest)
- return
- }
- w.WriteHeader(http.StatusOK)
- }
- func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
- if r.Method != httpm.GET {
- http.Error(w, "use GET", http.StatusMethodNotAllowed)
- return
- }
- limit := 50
- if limitStr := r.FormValue("limit"); limitStr != "" {
- l, err := strconv.Atoi(limitStr)
- if err != nil {
- http.Error(w, "parsing 'limit' parameter: "+err.Error(), http.StatusBadRequest)
- return
- }
- limit = int(l)
- }
- updates, err := h.b.NetworkLockLog(limit)
- if err != nil {
- http.Error(w, "reading log failed: "+err.Error(), http.StatusInternalServerError)
- return
- }
- j, err := json.MarshalIndent(updates, "", "\t")
- if err != nil {
- http.Error(w, "JSON encoding error", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
- }
- func (h *Handler) serveTKAAffectedSigs(w http.ResponseWriter, r *http.Request) {
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- keyID, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 2048))
- if err != nil {
- http.Error(w, "reading body", http.StatusBadRequest)
- return
- }
- sigs, err := h.b.NetworkLockAffectedSigs(keyID)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- j, err := json.MarshalIndent(sigs, "", "\t")
- if err != nil {
- http.Error(w, "JSON encoding error", http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(j)
- }
- func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- type verifyRequest struct {
- Keys []tkatype.KeyID
- ForkFrom string
- }
- var req verifyRequest
- if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest)
- return
- }
- var forkFrom tka.AUMHash
- if req.ForkFrom != "" {
- if err := forkFrom.UnmarshalText([]byte(req.ForkFrom)); err != nil {
- http.Error(w, "decoding fork-from: "+err.Error(), http.StatusBadRequest)
- return
- }
- }
- res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/octet-stream")
- w.Write(res.Serialize())
- }
- func (h *Handler) serveTKACosignRecoveryAUM(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- body := io.LimitReader(r.Body, 1024*1024)
- aumBytes, err := io.ReadAll(body)
- if err != nil {
- http.Error(w, "reading AUM", http.StatusBadRequest)
- return
- }
- var aum tka.AUM
- if err := aum.Unserialize(aumBytes); err != nil {
- http.Error(w, "decoding AUM", http.StatusBadRequest)
- return
- }
- res, err := h.b.NetworkLockCosignRecoveryAUM(&aum)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/octet-stream")
- w.Write(res.Serialize())
- }
- func (h *Handler) serveTKASubmitRecoveryAUM(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- }
- body := io.LimitReader(r.Body, 1024*1024)
- aumBytes, err := io.ReadAll(body)
- if err != nil {
- http.Error(w, "reading AUM", http.StatusBadRequest)
- return
- }
- var aum tka.AUM
- if err := aum.Unserialize(aumBytes); err != nil {
- http.Error(w, "decoding AUM", http.StatusBadRequest)
- return
- }
- if err := h.b.NetworkLockSubmitRecoveryAUM(&aum); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusOK)
- }
- // serveProfiles serves profile switching-related endpoints. Supported methods
- // and paths are:
- // - GET /profiles/: list all profiles (JSON-encoded array of ipn.LoginProfiles)
- // - PUT /profiles/: add new profile (no response). A separate
- // StartLoginInteractive() is needed to populate and persist the new profile.
- // - GET /profiles/current: current profile (JSON-ecoded ipn.LoginProfile)
- // - GET /profiles/<id>: output profile (JSON-ecoded ipn.LoginProfile)
- // - POST /profiles/<id>: switch to profile (no response)
- // - DELETE /profiles/<id>: delete profile (no response)
- func (h *Handler) serveProfiles(w http.ResponseWriter, r *http.Request) {
- if !h.PermitWrite {
- http.Error(w, "profiles access denied", http.StatusForbidden)
- return
- }
- suffix, ok := strings.CutPrefix(r.URL.EscapedPath(), "/localapi/v0/profiles/")
- if !ok {
- http.Error(w, "misconfigured", http.StatusInternalServerError)
- return
- }
- if suffix == "" {
- switch r.Method {
- case httpm.GET:
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(h.b.ListProfiles())
- case httpm.PUT:
- err := h.b.NewProfile()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusCreated)
- default:
- http.Error(w, "use GET or PUT", http.StatusMethodNotAllowed)
- }
- return
- }
- suffix, err := url.PathUnescape(suffix)
- if err != nil {
- http.Error(w, "bad profile ID", http.StatusBadRequest)
- return
- }
- if suffix == "current" {
- switch r.Method {
- case httpm.GET:
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(h.b.CurrentProfile())
- default:
- http.Error(w, "use GET", http.StatusMethodNotAllowed)
- }
- return
- }
- profileID := ipn.ProfileID(suffix)
- switch r.Method {
- case httpm.GET:
- profiles := h.b.ListProfiles()
- profileIndex := slices.IndexFunc(profiles, func(p ipn.LoginProfileView) bool {
- return p.ID() == profileID
- })
- if profileIndex == -1 {
- http.Error(w, "Profile not found", http.StatusNotFound)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(profiles[profileIndex])
- case httpm.POST:
- err := h.b.SwitchProfile(profileID)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- case httpm.DELETE:
- err := h.b.DeleteProfile(profileID)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- default:
- http.Error(w, "use POST or DELETE", http.StatusMethodNotAllowed)
- }
- }
- // serveQueryFeature makes a request to the "/machine/feature/query"
- // Noise endpoint to get instructions on how to enable a feature, such as
- // Funnel, for the node's tailnet.
- //
- // This request itself does not directly enable the feature on behalf of
- // the node, but rather returns information that can be presented to the
- // acting user about where/how to enable the feature. If relevant, this
- // includes a control URL the user can visit to explicitly consent to
- // using the feature.
- //
- // See tailcfg.QueryFeatureResponse for full response structure.
- func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
- feature := r.FormValue("feature")
- switch {
- case !h.PermitRead:
- http.Error(w, "access denied", http.StatusForbidden)
- return
- case r.Method != httpm.POST:
- http.Error(w, "use POST", http.StatusMethodNotAllowed)
- return
- case feature == "":
- http.Error(w, "missing feature", http.StatusInternalServerError)
- return
- }
- nm := h.b.NetMap()
- if nm == nil {
- http.Error(w, "no netmap", http.StatusServiceUnavailable)
- return
- }
- b, err := json.Marshal(&tailcfg.QueryFeatureRequest{
- NodeKey: nm.NodeKey,
- Feature: feature,
- })
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- req, err := http.NewRequestWithContext(r.Context(),
- "POST", "https://unused/machine/feature/query", bytes.NewReader(b))
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- resp, err := h.b.DoNoiseRequest(req)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- defer resp.Body.Close()
- w.Header().Set("Content-Type", "application/json")
- w.WriteHeader(resp.StatusCode)
- if _, err := io.Copy(w, resp.Body); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- }
- func defBool(a string, def bool) bool {
- if a == "" {
- return def
- }
- v, err := strconv.ParseBool(a)
- if err != nil {
- return def
- }
- return v
- }
- func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
- if !h.PermitRead {
- http.Error(w, "debug-log access denied", http.StatusForbidden)
- return
- }
- if r.Method != httpm.POST {
- http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
- return
- }
- defer h.b.TryFlushLogs() // kick off upload after we're done logging
- type logRequestJSON struct {
- Lines []string
- Prefix string
- }
- var logRequest logRequestJSON
- if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil {
- http.Error(w, "invalid JSON body", http.StatusBadRequest)
- return
- }
- prefix := logRequest.Prefix
- if prefix == "" {
- prefix = "debug-log"
- }
- logf := logger.WithPrefix(h.logf, prefix+": ")
- // We can write logs too fast for logtail to handle, even when
- // opting-out of rate limits. Limit ourselves to at most one message
- // per 20ms and a burst of 60 log lines, which should be fast enough to
- // not block for too long but slow enough that we can upload all lines.
- logf = logger.SlowLoggerWithClock(r.Context(), logf, 20*time.Millisecond, 60, h.clock.Now)
- for _, line := range logRequest.Lines {
- logf("%s", line)
- }
- w.WriteHeader(http.StatusNoContent)
- }
- // serveUpdateCheck returns the ClientVersion from Status, which contains
- // information on whether an update is available, and if so, what version,
- // *if* we support auto-updates on this platform. If we don't, this endpoint
- // always returns a ClientVersion saying we're running the newest version.
- // Effectively, it tells us whether serveUpdateInstall will be able to install
- // an update for us.
- func (h *Handler) serveUpdateCheck(w http.ResponseWriter, r *http.Request) {
- if r.Method != "GET" {
- http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
- return
- }
- if !clientupdate.CanAutoUpdate() {
- // if we don't support auto-update, just say that we're up to date
- json.NewEncoder(w).Encode(tailcfg.ClientVersion{RunningLatest: true})
- return
- }
- cv := h.b.StatusWithoutPeers().ClientVersion
- // ipnstate.Status documentation notes that ClientVersion may be nil on some
- // platforms where this information is unavailable. In that case, return a
- // ClientVersion that says we're up to date, since we have no information on
- // whether an update is possible.
- if cv == nil {
- cv = &tailcfg.ClientVersion{RunningLatest: true}
- }
- json.NewEncoder(w).Encode(cv)
- }
- // serveUpdateInstall sends a request to the LocalBackend to start a Tailscale
- // self-update. A successful response does not indicate whether the update
- // succeeded, only that the request was accepted. Clients should use
- // serveUpdateProgress after pinging this endpoint to check how the update is
- // going.
- func (h *Handler) serveUpdateInstall(w http.ResponseWriter, r *http.Request) {
- if r.Method != "POST" {
- http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
- return
- }
- w.WriteHeader(http.StatusAccepted)
- go h.b.DoSelfUpdate()
- }
- // serveUpdateProgress returns the status of an in-progress Tailscale self-update.
- // This is provided as a slice of ipnstate.UpdateProgress structs with various
- // log messages in order from oldest to newest. If an update is not in progress,
- // the returned slice will be empty.
- func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
- if r.Method != "GET" {
- http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
- return
- }
- ups := h.b.GetSelfUpdateProgress()
- json.NewEncoder(w).Encode(ups)
- }
- // serveDNSOSConfig serves the current system DNS configuration as a JSON object, if
- // supported by the OS.
- func (h *Handler) serveDNSOSConfig(w http.ResponseWriter, r *http.Request) {
- if r.Method != httpm.GET {
- http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
- return
- }
- // Require write access for privacy reasons.
- if !h.PermitWrite {
- http.Error(w, "dns-osconfig dump access denied", http.StatusForbidden)
- return
- }
- bCfg, err := h.b.GetDNSOSConfig()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- nameservers := make([]string, 0, len(bCfg.Nameservers))
- for _, ns := range bCfg.Nameservers {
- nameservers = append(nameservers, ns.String())
- }
- searchDomains := make([]string, 0, len(bCfg.SearchDomains))
- for _, sd := range bCfg.SearchDomains {
- searchDomains = append(searchDomains, sd.WithoutTrailingDot())
- }
- matchDomains := make([]string, 0, len(bCfg.MatchDomains))
- for _, md := range bCfg.MatchDomains {
- matchDomains = append(matchDomains, md.WithoutTrailingDot())
- }
- response := apitype.DNSOSConfig{
- Nameservers: nameservers,
- SearchDomains: searchDomains,
- MatchDomains: matchDomains,
- }
- json.NewEncoder(w).Encode(response)
- }
- // serveDNSQuery provides the ability to perform DNS queries using the internal
- // DNS forwarder. This is useful for debugging and testing purposes.
- // URL parameters:
- // - name: the domain name to query
- // - type: the DNS record type to query as a number (default if empty: A = '1')
- //
- // The response if successful is a DNSQueryResponse JSON object.
- func (h *Handler) serveDNSQuery(w http.ResponseWriter, r *http.Request) {
- if r.Method != "GET" {
- http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
- return
- }
- // Require write access for privacy reasons.
- if !h.PermitWrite {
- http.Error(w, "dns-query access denied", http.StatusForbidden)
- return
- }
- q := r.URL.Query()
- name := q.Get("name")
- queryType := q.Get("type")
- qt := dnsmessage.TypeA
- if queryType != "" {
- t, err := dnstype.DNSMessageTypeForString(queryType)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- qt = t
- }
- res, rrs, err := h.b.QueryDNS(name, qt)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(&apitype.DNSQueryResponse{
- Bytes: res,
- Resolvers: rrs,
- })
- }
- // serveDriveServerAddr handles updates of the Taildrive file server address.
- func (h *Handler) serveDriveServerAddr(w http.ResponseWriter, r *http.Request) {
- if r.Method != "PUT" {
- http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
- return
- }
- b, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- h.b.DriveSetServerAddr(string(b))
- w.WriteHeader(http.StatusCreated)
- }
- // serveShares handles the management of Taildrive shares.
- //
- // PUT - adds or updates an existing share
- // DELETE - removes a share
- // GET - gets a list of all shares, sorted by name
- // POST - renames an existing share
- func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
- if !h.b.DriveSharingEnabled() {
- http.Error(w, `taildrive sharing not enabled, please add the attribute "drive:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusForbidden)
- return
- }
- switch r.Method {
- case "PUT":
- var share drive.Share
- err := json.NewDecoder(r.Body).Decode(&share)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- share.Path = path.Clean(share.Path)
- fi, err := os.Stat(share.Path)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- if !fi.IsDir() {
- http.Error(w, "not a directory", http.StatusBadRequest)
- return
- }
- if drive.AllowShareAs() {
- // share as the connected user
- username, err := h.Actor.Username()
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- share.As = username
- }
- err = h.b.DriveSetShare(&share)
- if err != nil {
- if errors.Is(err, drive.ErrInvalidShareName) {
- http.Error(w, "invalid share name", http.StatusBadRequest)
- return
- }
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusCreated)
- case "DELETE":
- b, err := io.ReadAll(r.Body)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- err = h.b.DriveRemoveShare(string(b))
- if err != nil {
- if os.IsNotExist(err) {
- http.Error(w, "share not found", http.StatusNotFound)
- return
- }
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- case "POST":
- var names [2]string
- err := json.NewDecoder(r.Body).Decode(&names)
- if err != nil {
- http.Error(w, err.Error(), http.StatusBadRequest)
- return
- }
- err = h.b.DriveRenameShare(names[0], names[1])
- if err != nil {
- if os.IsNotExist(err) {
- http.Error(w, "share not found", http.StatusNotFound)
- return
- }
- if os.IsExist(err) {
- http.Error(w, "share name already used", http.StatusBadRequest)
- return
- }
- if errors.Is(err, drive.ErrInvalidShareName) {
- http.Error(w, "invalid share name", http.StatusBadRequest)
- return
- }
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- w.WriteHeader(http.StatusNoContent)
- case "GET":
- shares := h.b.DriveGetShares()
- err := json.NewEncoder(w).Encode(shares)
- if err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- default:
- http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
- }
- }
- var (
- metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")
- // User-visible LocalAPI endpoints.
- metricFilePutCalls = clientmetric.NewCounter("localapi_file_put")
- metricDebugMetricsCalls = clientmetric.NewCounter("localapi_debugmetric_requests")
- metricUserMetricsCalls = clientmetric.NewCounter("localapi_usermetric_requests")
- )
- // serveSuggestExitNode serves a POST endpoint for returning a suggested exit node.
- func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) {
- if r.Method != "GET" {
- http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
- return
- }
- res, err := h.b.SuggestExitNode()
- if err != nil {
- writeErrorJSON(w, err)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(res)
- }
|