tailssh.go 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251
  1. // Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. //go:build linux || (darwin && !ios)
  5. // +build linux darwin,!ios
  6. // Package tailssh is an SSH server integrated into Tailscale.
  7. package tailssh
  8. import (
  9. "bytes"
  10. "context"
  11. "crypto/rand"
  12. "encoding/base64"
  13. "encoding/json"
  14. "errors"
  15. "fmt"
  16. "io"
  17. "io/ioutil"
  18. "net"
  19. "net/http"
  20. "net/url"
  21. "os"
  22. "os/exec"
  23. "os/user"
  24. "path/filepath"
  25. "runtime"
  26. "strconv"
  27. "strings"
  28. "sync"
  29. "time"
  30. gossh "github.com/tailscale/golang-x-crypto/ssh"
  31. "inet.af/netaddr"
  32. "tailscale.com/envknob"
  33. "tailscale.com/ipn/ipnlocal"
  34. "tailscale.com/logtail/backoff"
  35. "tailscale.com/net/tsaddr"
  36. "tailscale.com/tailcfg"
  37. "tailscale.com/tempfork/gliderlabs/ssh"
  38. "tailscale.com/types/logger"
  39. "tailscale.com/util/mak"
  40. )
  41. var (
  42. debugPolicyFile = envknob.SSHPolicyFile()
  43. debugIgnoreTailnetSSHPolicy = envknob.SSHIgnoreTailnetPolicy()
  44. sshVerboseLogging = envknob.Bool("TS_DEBUG_SSH_VLOG")
  45. )
  46. type server struct {
  47. lb *ipnlocal.LocalBackend
  48. logf logger.Logf
  49. tailscaledPath string
  50. pubKeyHTTPClient *http.Client // or nil for http.DefaultClient
  51. timeNow func() time.Time // or nil for time.Now
  52. // mu protects the following
  53. mu sync.Mutex
  54. activeSessionByH map[string]*sshSession // ssh.SessionID (DH H) => session
  55. activeSessionBySharedID map[string]*sshSession // yyymmddThhmmss-XXXXX => session
  56. fetchPublicKeysCache map[string]pubKeyCacheEntry // by https URL
  57. }
  58. func (srv *server) now() time.Time {
  59. if srv.timeNow != nil {
  60. return srv.timeNow()
  61. }
  62. return time.Now()
  63. }
  64. func init() {
  65. ipnlocal.RegisterNewSSHServer(func(logf logger.Logf, lb *ipnlocal.LocalBackend) (ipnlocal.SSHServer, error) {
  66. tsd, err := os.Executable()
  67. if err != nil {
  68. return nil, err
  69. }
  70. srv := &server{
  71. lb: lb,
  72. logf: logf,
  73. tailscaledPath: tsd,
  74. }
  75. return srv, nil
  76. })
  77. }
  78. // HandleSSHConn handles a Tailscale SSH connection from c.
  79. func (srv *server) HandleSSHConn(c net.Conn) error {
  80. ss, err := srv.newConn()
  81. if err != nil {
  82. return err
  83. }
  84. ss.HandleConn(c)
  85. // Return nil to signal to netstack's interception that it doesn't need to
  86. // log. If ss.HandleConn had problems, it can log itself (ideally on an
  87. // sshSession.logf).
  88. return nil
  89. }
  90. // OnPolicyChange terminates any active sessions that no longer match
  91. // the SSH access policy.
  92. func (srv *server) OnPolicyChange() {
  93. srv.mu.Lock()
  94. defer srv.mu.Unlock()
  95. for _, s := range srv.activeSessionByH {
  96. go s.checkStillValid()
  97. }
  98. }
  99. // conn represents a single SSH connection and its associated
  100. // ssh.Server.
  101. type conn struct {
  102. *ssh.Server
  103. // now is the time to consider the present moment for the
  104. // purposes of rule evaluation.
  105. now time.Time
  106. action0 *tailcfg.SSHAction // first matching action
  107. srv *server
  108. info *sshConnInfo // set by setInfo
  109. localUser *user.User // set by checkAuth
  110. userGroupIDs []string // set by checkAuth
  111. insecureSkipTailscaleAuth bool // used by tests.
  112. }
  113. func (c *conn) logf(format string, args ...any) {
  114. if c.info == nil {
  115. c.srv.logf(format, args...)
  116. return
  117. }
  118. format = fmt.Sprintf("%v: %v", c.info.String(), format)
  119. c.srv.logf(format, args...)
  120. }
  121. // PublicKeyHandler implements ssh.PublicKeyHandler is called by the the
  122. // ssh.Server when the client presents a public key.
  123. func (c *conn) PublicKeyHandler(ctx ssh.Context, pubKey ssh.PublicKey) error {
  124. if c.info == nil {
  125. return gossh.ErrDenied
  126. }
  127. if err := c.checkAuth(pubKey); err != nil {
  128. // TODO(maisem/bradfitz): surface the error here.
  129. c.logf("rejecting SSH public key %s: %v", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)), err)
  130. return err
  131. }
  132. c.logf("accepting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)))
  133. return nil
  134. }
  135. // errPubKeyRequired is returned by NoClientAuthCallback to make the client
  136. // resort to public-key auth; not user visible.
  137. var errPubKeyRequired = errors.New("ssh publickey required")
  138. // NoClientAuthCallback implements gossh.NoClientAuthCallback and is called by
  139. // the the ssh.Server when the client first connects with the "none"
  140. // authentication method.
  141. func (c *conn) NoClientAuthCallback(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
  142. if c.insecureSkipTailscaleAuth {
  143. return nil, nil
  144. }
  145. if err := c.setInfo(cm); err != nil {
  146. c.logf("failed to get conninfo: %v", err)
  147. return nil, gossh.ErrDenied
  148. }
  149. return nil, c.checkAuth(nil /* no pub key */)
  150. }
  151. // checkAuth verifies that conn can proceed with the specified (optional)
  152. // pubKey. It returns nil if the matching policy action is Accept or
  153. // HoldAndDelegate. If pubKey is nil, there was no policy match but there is a
  154. // policy that might match a public key it returns errPubKeyRequired. Otherwise,
  155. // it returns gossh.ErrDenied possibly wrapped in gossh.WithBannerError.
  156. func (c *conn) checkAuth(pubKey ssh.PublicKey) error {
  157. a, localUser, err := c.evaluatePolicy(pubKey)
  158. if err != nil {
  159. if pubKey == nil && c.havePubKeyPolicy(c.info) {
  160. return errPubKeyRequired
  161. }
  162. return fmt.Errorf("%w: %v", gossh.ErrDenied, err)
  163. }
  164. c.action0 = a
  165. if a.Accept || a.HoldAndDelegate != "" {
  166. lu, err := user.Lookup(localUser)
  167. if err != nil {
  168. c.logf("failed to lookup %v: %v", localUser, err)
  169. return gossh.WithBannerError{
  170. Err: gossh.ErrDenied,
  171. Message: fmt.Sprintf("failed to lookup %v\r\n", localUser),
  172. }
  173. }
  174. gids, err := lu.GroupIds()
  175. if err != nil {
  176. return err
  177. }
  178. c.userGroupIDs = gids
  179. c.localUser = lu
  180. return nil
  181. }
  182. if a.Reject {
  183. err := gossh.ErrDenied
  184. if a.Message != "" {
  185. err = gossh.WithBannerError{
  186. Err: err,
  187. Message: a.Message,
  188. }
  189. }
  190. return err
  191. }
  192. // Shouldn't get here, but:
  193. return gossh.ErrDenied
  194. }
  195. // ServerConfig implements ssh.ServerConfigCallback.
  196. func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
  197. return &gossh.ServerConfig{
  198. // OpenSSH presents this on failure as `Permission denied (tailscale).`
  199. ImplictAuthMethod: "tailscale",
  200. NoClientAuth: true, // required for the NoClientAuthCallback to run
  201. NoClientAuthCallback: c.NoClientAuthCallback,
  202. }
  203. }
  204. func (srv *server) newConn() (*conn, error) {
  205. c := &conn{srv: srv, now: srv.now()}
  206. c.Server = &ssh.Server{
  207. Version: "Tailscale",
  208. Handler: c.handleConnPostSSHAuth,
  209. RequestHandlers: map[string]ssh.RequestHandler{},
  210. SubsystemHandlers: map[string]ssh.SubsystemHandler{
  211. "sftp": c.handleConnPostSSHAuth,
  212. },
  213. // Note: the direct-tcpip channel handler and LocalPortForwardingCallback
  214. // only adds support for forwarding ports from the local machine.
  215. // TODO(maisem/bradfitz): add remote port forwarding support.
  216. ChannelHandlers: map[string]ssh.ChannelHandler{
  217. "direct-tcpip": ssh.DirectTCPIPHandler,
  218. },
  219. LocalPortForwardingCallback: srv.mayForwardLocalPortTo,
  220. PublicKeyHandler: c.PublicKeyHandler,
  221. ServerConfigCallback: c.ServerConfig,
  222. }
  223. ss := c.Server
  224. for k, v := range ssh.DefaultRequestHandlers {
  225. ss.RequestHandlers[k] = v
  226. }
  227. for k, v := range ssh.DefaultChannelHandlers {
  228. ss.ChannelHandlers[k] = v
  229. }
  230. for k, v := range ssh.DefaultSubsystemHandlers {
  231. ss.SubsystemHandlers[k] = v
  232. }
  233. keys, err := srv.lb.GetSSH_HostKeys()
  234. if err != nil {
  235. return nil, err
  236. }
  237. for _, signer := range keys {
  238. ss.AddHostKey(signer)
  239. }
  240. return c, nil
  241. }
  242. // mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
  243. // to the specified host and port.
  244. // TODO(bradfitz/maisem): should we have more checks on host/port?
  245. func (srv *server) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
  246. ss, ok := srv.getSessionForContext(ctx)
  247. if !ok {
  248. return false
  249. }
  250. return ss.action.AllowLocalPortForwarding
  251. }
  252. // havePubKeyPolicy reports whether any policy rule may provide access by means
  253. // of a ssh.PublicKey.
  254. func (c *conn) havePubKeyPolicy(ci *sshConnInfo) bool {
  255. // Is there any rule that looks like it'd require a public key for this
  256. // sshUser?
  257. pol, ok := c.sshPolicy()
  258. if !ok {
  259. return false
  260. }
  261. for _, r := range pol.Rules {
  262. if c.ruleExpired(r) {
  263. continue
  264. }
  265. if mapLocalUser(r.SSHUsers, ci.sshUser) == "" {
  266. continue
  267. }
  268. for _, p := range r.Principals {
  269. if len(p.PubKeys) > 0 && c.principalMatchesTailscaleIdentity(p) {
  270. return true
  271. }
  272. }
  273. }
  274. return false
  275. }
  276. // sshPolicy returns the SSHPolicy for current node.
  277. // If there is no SSHPolicy in the netmap, it returns a debugPolicy
  278. // if one is defined.
  279. func (c *conn) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
  280. lb := c.srv.lb
  281. nm := lb.NetMap()
  282. if nm == nil {
  283. return nil, false
  284. }
  285. if pol := nm.SSHPolicy; pol != nil && !debugIgnoreTailnetSSHPolicy {
  286. return pol, true
  287. }
  288. if debugPolicyFile != "" {
  289. c.logf("reading debug SSH policy file: %v", debugPolicyFile)
  290. f, err := os.ReadFile(debugPolicyFile)
  291. if err != nil {
  292. c.logf("error reading debug SSH policy file: %v", err)
  293. return nil, false
  294. }
  295. p := new(tailcfg.SSHPolicy)
  296. if err := json.Unmarshal(f, p); err != nil {
  297. c.logf("invalid JSON in %v: %v", debugPolicyFile, err)
  298. return nil, false
  299. }
  300. return p, true
  301. }
  302. return nil, false
  303. }
  304. func toIPPort(a net.Addr) (ipp netaddr.IPPort) {
  305. ta, ok := a.(*net.TCPAddr)
  306. if !ok {
  307. return
  308. }
  309. tanetaddr, ok := netaddr.FromStdIP(ta.IP)
  310. if !ok {
  311. return
  312. }
  313. return netaddr.IPPortFrom(tanetaddr, uint16(ta.Port))
  314. }
  315. // connInfo returns a populated sshConnInfo from the provided arguments,
  316. // validating only that they represent a known Tailscale identity.
  317. func (c *conn) setInfo(cm gossh.ConnMetadata) error {
  318. ci := &sshConnInfo{
  319. sshUser: cm.User(),
  320. src: toIPPort(cm.RemoteAddr()),
  321. dst: toIPPort(cm.LocalAddr()),
  322. }
  323. if !tsaddr.IsTailscaleIP(ci.dst.IP()) {
  324. return fmt.Errorf("tailssh: rejecting non-Tailscale local address %v", ci.dst)
  325. }
  326. if !tsaddr.IsTailscaleIP(ci.src.IP()) {
  327. return fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", ci.src)
  328. }
  329. node, uprof, ok := c.srv.lb.WhoIs(ci.src)
  330. if !ok {
  331. return fmt.Errorf("unknown Tailscale identity from src %v", ci.src)
  332. }
  333. ci.node = node
  334. ci.uprof = &uprof
  335. c.info = ci
  336. return nil
  337. }
  338. // evaluatePolicy returns the SSHAction and localUser after evaluating
  339. // the SSHPolicy for this conn. The pubKey may be nil for "none" auth.
  340. func (c *conn) evaluatePolicy(pubKey gossh.PublicKey) (_ *tailcfg.SSHAction, localUser string, _ error) {
  341. pol, ok := c.sshPolicy()
  342. if !ok {
  343. return nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
  344. }
  345. a, localUser, ok := c.evalSSHPolicy(pol, pubKey)
  346. if !ok {
  347. return nil, "", fmt.Errorf("tailssh: rejecting connection; no matching policy")
  348. }
  349. return a, localUser, nil
  350. }
  351. // pubKeyCacheEntry is the cache value for an HTTPS URL of public keys (like
  352. // "https://github.com/foo.keys")
  353. type pubKeyCacheEntry struct {
  354. lines []string
  355. etag string // if sent by server
  356. at time.Time
  357. }
  358. const (
  359. pubKeyCacheDuration = time.Minute // how long to cache non-empty public keys
  360. pubKeyCacheEmptyDuration = 15 * time.Second // how long to cache empty responses
  361. )
  362. func (srv *server) fetchPublicKeysURLCached(url string) (ce pubKeyCacheEntry, ok bool) {
  363. srv.mu.Lock()
  364. defer srv.mu.Unlock()
  365. // Mostly don't care about the size of this cache. Clean rarely.
  366. if m := srv.fetchPublicKeysCache; len(m) > 50 {
  367. tooOld := srv.now().Add(pubKeyCacheDuration * 10)
  368. for k, ce := range m {
  369. if ce.at.Before(tooOld) {
  370. delete(m, k)
  371. }
  372. }
  373. }
  374. ce, ok = srv.fetchPublicKeysCache[url]
  375. if !ok {
  376. return ce, false
  377. }
  378. maxAge := pubKeyCacheDuration
  379. if len(ce.lines) == 0 {
  380. maxAge = pubKeyCacheEmptyDuration
  381. }
  382. return ce, srv.now().Sub(ce.at) < maxAge
  383. }
  384. func (srv *server) pubKeyClient() *http.Client {
  385. if srv.pubKeyHTTPClient != nil {
  386. return srv.pubKeyHTTPClient
  387. }
  388. return http.DefaultClient
  389. }
  390. // fetchPublicKeysURL fetches the public keys from a URL. The strings are in the
  391. // the typical public key "type base64-string [comment]" format seen at e.g.
  392. // https://github.com/USER.keys
  393. func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
  394. if !strings.HasPrefix(url, "https://") {
  395. return nil, errors.New("invalid URL scheme")
  396. }
  397. ce, ok := srv.fetchPublicKeysURLCached(url)
  398. if ok {
  399. return ce.lines, nil
  400. }
  401. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  402. defer cancel()
  403. req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
  404. if err != nil {
  405. return nil, err
  406. }
  407. if ce.etag != "" {
  408. req.Header.Add("If-None-Match", ce.etag)
  409. }
  410. res, err := srv.pubKeyClient().Do(req)
  411. if err != nil {
  412. return nil, err
  413. }
  414. defer res.Body.Close()
  415. var lines []string
  416. var etag string
  417. switch res.StatusCode {
  418. default:
  419. err = fmt.Errorf("unexpected status %v", res.Status)
  420. srv.logf("fetching public keys from %s: %v", url, err)
  421. case http.StatusNotModified:
  422. lines = ce.lines
  423. etag = ce.etag
  424. case http.StatusOK:
  425. var all []byte
  426. all, err = io.ReadAll(io.LimitReader(res.Body, 4<<10))
  427. if s := strings.TrimSpace(string(all)); s != "" {
  428. lines = strings.Split(s, "\n")
  429. }
  430. etag = res.Header.Get("Etag")
  431. }
  432. srv.mu.Lock()
  433. defer srv.mu.Unlock()
  434. mak.Set(&srv.fetchPublicKeysCache, url, pubKeyCacheEntry{
  435. at: srv.now(),
  436. lines: lines,
  437. etag: etag,
  438. })
  439. return lines, err
  440. }
  441. // handleConnPostSSHAuth runs an SSH session after the SSH-level authentication,
  442. // but not necessarily before all the Tailscale-level extra verification has
  443. // completed. It also handles SFTP requests.
  444. func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
  445. sshUser := s.User()
  446. action, err := c.resolveTerminalAction(s)
  447. if err != nil {
  448. c.logf("resolveTerminalAction: %v", err)
  449. io.WriteString(s.Stderr(), "Access Denied: failed during authorization check.\r\n")
  450. s.Exit(1)
  451. return
  452. }
  453. if action.Reject || !action.Accept {
  454. c.logf("access denied for %v", c.info.uprof.LoginName)
  455. s.Exit(1)
  456. return
  457. }
  458. // Do this check after auth, but before starting the session.
  459. switch s.Subsystem() {
  460. case "sftp", "":
  461. default:
  462. fmt.Fprintf(s.Stderr(), "Unsupported subsystem %q \r\n", s.Subsystem())
  463. s.Exit(1)
  464. return
  465. }
  466. ss := c.newSSHSession(s, action)
  467. ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", c.info.uprof.LoginName, c.info.src.IP(), sshUser)
  468. ss.logf("access granted to %v as ssh-user %q", c.info.uprof.LoginName, sshUser)
  469. ss.run()
  470. }
  471. // resolveTerminalAction either returns action0 (if it's Accept or Reject) or
  472. // else loops, fetching new SSHActions from the control plane.
  473. //
  474. // Any action with a Message in the chain will be printed to s.
  475. //
  476. // The returned SSHAction will be either Reject or Accept.
  477. func (c *conn) resolveTerminalAction(s ssh.Session) (*tailcfg.SSHAction, error) {
  478. action := c.action0
  479. // Loop processing/fetching Actions until one reaches a
  480. // terminal state (Accept, Reject, or invalid Action), or
  481. // until fetchSSHAction times out due to the context being
  482. // done (client disconnect) or its 30 minute timeout passes.
  483. // (Which is a long time for somebody to see login
  484. // instructions and go to a URL to do something.)
  485. for {
  486. if action.Message != "" {
  487. io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
  488. }
  489. if action.Accept || action.Reject {
  490. return action, nil
  491. }
  492. url := action.HoldAndDelegate
  493. if url == "" {
  494. return nil, errors.New("reached Action that lacked Accept, Reject, and HoldAndDelegate")
  495. }
  496. url = c.expandDelegateURL(url)
  497. var err error
  498. action, err = c.fetchSSHAction(s.Context(), url)
  499. if err != nil {
  500. return nil, fmt.Errorf("fetching SSHAction from %s: %w", url, err)
  501. }
  502. }
  503. }
  504. func (c *conn) expandDelegateURL(actionURL string) string {
  505. nm := c.srv.lb.NetMap()
  506. ci := c.info
  507. var dstNodeID string
  508. if nm != nil {
  509. dstNodeID = fmt.Sprint(int64(nm.SelfNode.ID))
  510. }
  511. return strings.NewReplacer(
  512. "$SRC_NODE_IP", url.QueryEscape(ci.src.IP().String()),
  513. "$SRC_NODE_ID", fmt.Sprint(int64(ci.node.ID)),
  514. "$DST_NODE_IP", url.QueryEscape(ci.dst.IP().String()),
  515. "$DST_NODE_ID", dstNodeID,
  516. "$SSH_USER", url.QueryEscape(ci.sshUser),
  517. "$LOCAL_USER", url.QueryEscape(c.localUser.Username),
  518. ).Replace(actionURL)
  519. }
  520. func (c *conn) expandPublicKeyURL(pubKeyURL string) string {
  521. if !strings.Contains(pubKeyURL, "$") {
  522. return pubKeyURL
  523. }
  524. var localPart string
  525. var loginName string
  526. if c.info.uprof != nil {
  527. loginName = c.info.uprof.LoginName
  528. localPart, _, _ = strings.Cut(loginName, "@")
  529. }
  530. return strings.NewReplacer(
  531. "$LOGINNAME_EMAIL", loginName,
  532. "$LOGINNAME_LOCALPART", localPart,
  533. ).Replace(pubKeyURL)
  534. }
  535. // sshSession is an accepted Tailscale SSH session.
  536. type sshSession struct {
  537. ssh.Session
  538. idH string // the RFC4253 sec8 hash H; don't share outside process
  539. sharedID string // ID that's shared with control
  540. logf logger.Logf
  541. ctx *sshContext // implements context.Context
  542. conn *conn
  543. action *tailcfg.SSHAction
  544. agentListener net.Listener // non-nil if agent-forwarding requested+allowed
  545. // initialized by launchProcess:
  546. cmd *exec.Cmd
  547. stdin io.WriteCloser
  548. stdout io.Reader
  549. stderr io.Reader // nil for pty sessions
  550. ptyReq *ssh.Pty // non-nil for pty sessions
  551. // We use this sync.Once to ensure that we only terminate the process once,
  552. // either it exits itself or is terminated
  553. exitOnce sync.Once
  554. }
  555. func (ss *sshSession) vlogf(format string, args ...interface{}) {
  556. if sshVerboseLogging {
  557. ss.logf(format, args...)
  558. }
  559. }
  560. func (c *conn) newSSHSession(s ssh.Session, action *tailcfg.SSHAction) *sshSession {
  561. sharedID := fmt.Sprintf("%s-%02x", c.now.UTC().Format("20060102T150405"), randBytes(5))
  562. c.logf("starting session: %v", sharedID)
  563. return &sshSession{
  564. Session: s,
  565. idH: s.Context().(ssh.Context).SessionID(),
  566. sharedID: sharedID,
  567. ctx: newSSHContext(),
  568. conn: c,
  569. logf: logger.WithPrefix(c.srv.logf, "ssh-session("+sharedID+"): "),
  570. action: action,
  571. }
  572. }
  573. func (c *conn) isStillValid(pubKey ssh.PublicKey) bool {
  574. a, localUser, err := c.evaluatePolicy(pubKey)
  575. if err != nil {
  576. return false
  577. }
  578. if !a.Accept && a.HoldAndDelegate == "" {
  579. return false
  580. }
  581. return c.localUser.Username == localUser
  582. }
  583. // checkStillValid checks that the session is still valid per the latest SSHPolicy.
  584. // If not, it terminates the session.
  585. func (ss *sshSession) checkStillValid() {
  586. if ss.conn.isStillValid(ss.PublicKey()) {
  587. return
  588. }
  589. ss.logf("session no longer valid per new SSH policy; closing")
  590. ss.ctx.CloseWithError(userVisibleError{
  591. fmt.Sprintf("Access revoked.\r\n"),
  592. context.Canceled,
  593. })
  594. }
  595. func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
  596. ctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
  597. defer cancel()
  598. bo := backoff.NewBackoff("fetch-ssh-action", c.logf, 10*time.Second)
  599. for {
  600. if err := ctx.Err(); err != nil {
  601. return nil, err
  602. }
  603. req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
  604. if err != nil {
  605. return nil, err
  606. }
  607. res, err := c.srv.lb.DoNoiseRequest(req)
  608. if err != nil {
  609. bo.BackOff(ctx, err)
  610. continue
  611. }
  612. if res.StatusCode != 200 {
  613. body, _ := io.ReadAll(res.Body)
  614. res.Body.Close()
  615. if len(body) > 1<<10 {
  616. body = body[:1<<10]
  617. }
  618. c.logf("fetch of %v: %s, %s", url, res.Status, body)
  619. bo.BackOff(ctx, fmt.Errorf("unexpected status: %v", res.Status))
  620. continue
  621. }
  622. a := new(tailcfg.SSHAction)
  623. err = json.NewDecoder(res.Body).Decode(a)
  624. res.Body.Close()
  625. if err != nil {
  626. c.logf("invalid next SSHAction JSON from %v: %v", url, err)
  627. bo.BackOff(ctx, err)
  628. continue
  629. }
  630. return a, nil
  631. }
  632. }
  633. // killProcessOnContextDone waits for ss.ctx to be done and kills the process,
  634. // unless the process has already exited.
  635. func (ss *sshSession) killProcessOnContextDone() {
  636. <-ss.ctx.Done()
  637. // Either the process has already exited, in which case this does nothing.
  638. // Or, the process is still running in which case this will kill it.
  639. ss.exitOnce.Do(func() {
  640. err := ss.ctx.Err()
  641. if serr, ok := err.(SSHTerminationError); ok {
  642. msg := serr.SSHTerminationMessage()
  643. if msg != "" {
  644. io.WriteString(ss.Stderr(), "\r\n\r\n"+msg+"\r\n\r\n")
  645. }
  646. }
  647. ss.logf("terminating SSH session from %v: %v", ss.conn.info.src.IP(), err)
  648. // We don't need to Process.Wait here, sshSession.run() does
  649. // the waiting regardless of termination reason.
  650. ss.cmd.Process.Kill()
  651. })
  652. }
  653. // sessionAction returns the SSHAction associated with the session.
  654. func (srv *server) getSessionForContext(sctx ssh.Context) (ss *sshSession, ok bool) {
  655. srv.mu.Lock()
  656. defer srv.mu.Unlock()
  657. ss, ok = srv.activeSessionByH[sctx.SessionID()]
  658. return
  659. }
  660. // startSession registers ss as an active session.
  661. func (srv *server) startSession(ss *sshSession) {
  662. srv.mu.Lock()
  663. defer srv.mu.Unlock()
  664. if ss.idH == "" {
  665. panic("empty idH")
  666. }
  667. if ss.sharedID == "" {
  668. panic("empty sharedID")
  669. }
  670. if _, dup := srv.activeSessionByH[ss.idH]; dup {
  671. panic("dup idH")
  672. }
  673. if _, dup := srv.activeSessionBySharedID[ss.sharedID]; dup {
  674. panic("dup sharedID")
  675. }
  676. mak.Set(&srv.activeSessionByH, ss.idH, ss)
  677. mak.Set(&srv.activeSessionBySharedID, ss.sharedID, ss)
  678. }
  679. // endSession unregisters s from the list of active sessions.
  680. func (srv *server) endSession(ss *sshSession) {
  681. srv.mu.Lock()
  682. defer srv.mu.Unlock()
  683. delete(srv.activeSessionByH, ss.idH)
  684. delete(srv.activeSessionBySharedID, ss.sharedID)
  685. }
  686. var errSessionDone = errors.New("session is done")
  687. // handleSSHAgentForwarding starts a Unix socket listener and in the background
  688. // forwards agent connections between the listener and the ssh.Session.
  689. // On success, it assigns ss.agentListener.
  690. func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *user.User) error {
  691. if !ssh.AgentRequested(ss) || !ss.action.AllowAgentForwarding {
  692. return nil
  693. }
  694. ss.logf("ssh: agent forwarding requested")
  695. ln, err := ssh.NewAgentListener()
  696. if err != nil {
  697. return err
  698. }
  699. defer func() {
  700. if err != nil && ln != nil {
  701. ln.Close()
  702. }
  703. }()
  704. uid, err := strconv.ParseUint(lu.Uid, 10, 32)
  705. if err != nil {
  706. return err
  707. }
  708. gid, err := strconv.ParseUint(lu.Gid, 10, 32)
  709. if err != nil {
  710. return err
  711. }
  712. socket := ln.Addr().String()
  713. dir := filepath.Dir(socket)
  714. // Make sure the socket is accessible only by the user.
  715. if err := os.Chmod(socket, 0600); err != nil {
  716. return err
  717. }
  718. if err := os.Chown(socket, int(uid), int(gid)); err != nil {
  719. return err
  720. }
  721. // Make sure the dir is also accessible.
  722. if err := os.Chmod(dir, 0755); err != nil {
  723. return err
  724. }
  725. go ssh.ForwardAgentConnections(ln, s)
  726. ss.agentListener = ln
  727. return nil
  728. }
  729. // recordSSH is a temporary dev knob to test the SSH recording
  730. // functionality and support off-node streaming.
  731. //
  732. // TODO(bradfitz,maisem): move this to SSHPolicy.
  733. var recordSSH = envknob.Bool("TS_DEBUG_LOG_SSH")
  734. // run is the entrypoint for a newly accepted SSH session.
  735. //
  736. // It handles ss once it's been accepted and determined
  737. // that it should run.
  738. func (ss *sshSession) run() {
  739. srv := ss.conn.srv
  740. srv.startSession(ss)
  741. defer srv.endSession(ss)
  742. defer ss.ctx.CloseWithError(errSessionDone)
  743. if ss.action.SessionDuration != 0 {
  744. t := time.AfterFunc(ss.action.SessionDuration, func() {
  745. ss.ctx.CloseWithError(userVisibleError{
  746. fmt.Sprintf("Session timeout of %v elapsed.", ss.action.SessionDuration),
  747. context.DeadlineExceeded,
  748. })
  749. })
  750. defer t.Stop()
  751. }
  752. logf := srv.logf
  753. lu := ss.conn.localUser
  754. localUser := lu.Username
  755. if euid := os.Geteuid(); euid != 0 {
  756. if lu.Uid != fmt.Sprint(euid) {
  757. ss.logf("can't switch to user %q from process euid %v", localUser, euid)
  758. fmt.Fprintf(ss, "can't switch user\r\n")
  759. ss.Exit(1)
  760. return
  761. }
  762. }
  763. // Take control of the PTY so that we can configure it below.
  764. // See https://github.com/tailscale/tailscale/issues/4146
  765. ss.DisablePTYEmulation()
  766. var rec *recording // or nil if disabled
  767. if ss.Subsystem() != "sftp" {
  768. if err := ss.handleSSHAgentForwarding(ss, lu); err != nil {
  769. ss.logf("agent forwarding failed: %v", err)
  770. } else if ss.agentListener != nil {
  771. // TODO(maisem/bradfitz): add a way to close all session resources
  772. defer ss.agentListener.Close()
  773. }
  774. if ss.shouldRecord() {
  775. var err error
  776. rec, err = ss.startNewRecording()
  777. if err != nil {
  778. fmt.Fprintf(ss, "can't start new recording\r\n")
  779. ss.logf("startNewRecording: %v", err)
  780. ss.Exit(1)
  781. return
  782. }
  783. defer rec.Close()
  784. }
  785. }
  786. err := ss.launchProcess()
  787. if err != nil {
  788. logf("start failed: %v", err.Error())
  789. ss.Exit(1)
  790. return
  791. }
  792. go ss.killProcessOnContextDone()
  793. go func() {
  794. _, err := io.Copy(rec.writer("i", ss.stdin), ss)
  795. if err != nil {
  796. // TODO: don't log in the success case.
  797. logf("ssh: stdin copy: %v", err)
  798. }
  799. ss.stdin.Close()
  800. }()
  801. go func() {
  802. _, err := io.Copy(rec.writer("o", ss), ss.stdout)
  803. if err != nil {
  804. // TODO: don't log in the success case.
  805. logf("ssh: stdout copy: %v", err)
  806. }
  807. }()
  808. // stderr is nil for ptys.
  809. if ss.stderr != nil {
  810. go func() {
  811. _, err := io.Copy(ss.Stderr(), ss.stderr)
  812. if err != nil {
  813. // TODO: don't log in the success case.
  814. logf("ssh: stderr copy: %v", err)
  815. }
  816. }()
  817. }
  818. err = ss.cmd.Wait()
  819. // This will either make the SSH Termination goroutine be a no-op,
  820. // or itself will be a no-op because the process was killed by the
  821. // aforementioned goroutine.
  822. ss.exitOnce.Do(func() {})
  823. if err == nil {
  824. ss.Exit(0)
  825. return
  826. }
  827. if ee, ok := err.(*exec.ExitError); ok {
  828. code := ee.ProcessState.ExitCode()
  829. ss.logf("Wait: code=%v", code)
  830. ss.Exit(code)
  831. return
  832. }
  833. ss.logf("Wait: %v", err)
  834. ss.Exit(1)
  835. return
  836. }
  837. func (ss *sshSession) shouldRecord() bool {
  838. // for now only record pty sessions
  839. // TODO(bradfitz,maisem): make configurable on SSHPolicy and
  840. // support recording non-pty stuff too.
  841. _, _, isPtyReq := ss.Pty()
  842. return recordSSH && isPtyReq
  843. }
  844. type sshConnInfo struct {
  845. // sshUser is the requested local SSH username ("root", "alice", etc).
  846. sshUser string
  847. // src is the Tailscale IP and port that the connection came from.
  848. src netaddr.IPPort
  849. // dst is the Tailscale IP and port that the connection came for.
  850. dst netaddr.IPPort
  851. // node is srcIP's node.
  852. node *tailcfg.Node
  853. // uprof is node's UserProfile.
  854. uprof *tailcfg.UserProfile
  855. }
  856. func (ci *sshConnInfo) String() string {
  857. return fmt.Sprintf("%v->%v@%v", ci.src, ci.sshUser, ci.dst)
  858. }
  859. func (c *conn) ruleExpired(r *tailcfg.SSHRule) bool {
  860. if r.RuleExpires == nil {
  861. return false
  862. }
  863. return r.RuleExpires.Before(c.now)
  864. }
  865. func (c *conn) evalSSHPolicy(pol *tailcfg.SSHPolicy, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, ok bool) {
  866. for _, r := range pol.Rules {
  867. if a, localUser, err := c.matchRule(r, pubKey); err == nil {
  868. return a, localUser, true
  869. }
  870. }
  871. return nil, "", false
  872. }
  873. // internal errors for testing; they don't escape to callers or logs.
  874. var (
  875. errNilRule = errors.New("nil rule")
  876. errNilAction = errors.New("nil action")
  877. errRuleExpired = errors.New("rule expired")
  878. errPrincipalMatch = errors.New("principal didn't match")
  879. errUserMatch = errors.New("user didn't match")
  880. )
  881. func (c *conn) matchRule(r *tailcfg.SSHRule, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, err error) {
  882. if r == nil {
  883. return nil, "", errNilRule
  884. }
  885. if r.Action == nil {
  886. return nil, "", errNilAction
  887. }
  888. if c.ruleExpired(r) {
  889. return nil, "", errRuleExpired
  890. }
  891. if !r.Action.Reject {
  892. // For all but Reject rules, SSHUsers is required.
  893. // If SSHUsers is nil or empty, mapLocalUser will return an
  894. // empty string anyway.
  895. localUser = mapLocalUser(r.SSHUsers, c.info.sshUser)
  896. if localUser == "" {
  897. return nil, "", errUserMatch
  898. }
  899. }
  900. if ok, err := c.anyPrincipalMatches(r.Principals, pubKey); err != nil {
  901. return nil, "", err
  902. } else if !ok {
  903. return nil, "", errPrincipalMatch
  904. }
  905. return r.Action, localUser, nil
  906. }
  907. func mapLocalUser(ruleSSHUsers map[string]string, reqSSHUser string) (localUser string) {
  908. v, ok := ruleSSHUsers[reqSSHUser]
  909. if !ok {
  910. v = ruleSSHUsers["*"]
  911. }
  912. if v == "=" {
  913. return reqSSHUser
  914. }
  915. return v
  916. }
  917. func (c *conn) anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
  918. for _, p := range ps {
  919. if p == nil {
  920. continue
  921. }
  922. if ok, err := c.principalMatches(p, pubKey); err != nil {
  923. return false, err
  924. } else if ok {
  925. return true, nil
  926. }
  927. }
  928. return false, nil
  929. }
  930. func (c *conn) principalMatches(p *tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
  931. if !c.principalMatchesTailscaleIdentity(p) {
  932. return false, nil
  933. }
  934. return c.principalMatchesPubKey(p, pubKey)
  935. }
  936. // principalMatchesTailscaleIdentity reports whether one of p's four fields
  937. // that match the Tailscale identity match (Node, NodeIP, UserLogin, Any).
  938. // This function does not consider PubKeys.
  939. func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
  940. ci := c.info
  941. if p.Any {
  942. return true
  943. }
  944. if !p.Node.IsZero() && ci.node != nil && p.Node == ci.node.StableID {
  945. return true
  946. }
  947. if p.NodeIP != "" {
  948. if ip, _ := netaddr.ParseIP(p.NodeIP); ip == ci.src.IP() {
  949. return true
  950. }
  951. }
  952. if p.UserLogin != "" && ci.uprof != nil && ci.uprof.LoginName == p.UserLogin {
  953. return true
  954. }
  955. return false
  956. }
  957. func (c *conn) principalMatchesPubKey(p *tailcfg.SSHPrincipal, clientPubKey gossh.PublicKey) (bool, error) {
  958. if len(p.PubKeys) == 0 {
  959. return true, nil
  960. }
  961. if clientPubKey == nil {
  962. return false, nil
  963. }
  964. knownKeys := p.PubKeys
  965. if len(knownKeys) == 1 && strings.HasPrefix(knownKeys[0], "https://") {
  966. var err error
  967. knownKeys, err = c.srv.fetchPublicKeysURL(c.expandPublicKeyURL(knownKeys[0]))
  968. if err != nil {
  969. return false, err
  970. }
  971. }
  972. for _, knownKey := range knownKeys {
  973. if pubKeyMatchesAuthorizedKey(clientPubKey, knownKey) {
  974. return true, nil
  975. }
  976. }
  977. return false, nil
  978. }
  979. func pubKeyMatchesAuthorizedKey(pubKey ssh.PublicKey, wantKey string) bool {
  980. wantKeyType, rest, ok := strings.Cut(wantKey, " ")
  981. if !ok {
  982. return false
  983. }
  984. if pubKey.Type() != wantKeyType {
  985. return false
  986. }
  987. wantKeyB64, _, _ := strings.Cut(rest, " ")
  988. wantKeyData, _ := base64.StdEncoding.DecodeString(wantKeyB64)
  989. return len(wantKeyData) > 0 && bytes.Equal(pubKey.Marshal(), wantKeyData)
  990. }
  991. func randBytes(n int) []byte {
  992. b := make([]byte, n)
  993. if _, err := rand.Read(b); err != nil {
  994. panic(err)
  995. }
  996. return b
  997. }
  998. // startNewRecording starts a new SSH session recording.
  999. //
  1000. // It writes an asciinema file to
  1001. // $TAILSCALE_VAR_ROOT/ssh-sessions/ssh-session-<unixtime>-*.cast.
  1002. func (ss *sshSession) startNewRecording() (*recording, error) {
  1003. var w ssh.Window
  1004. if ptyReq, _, isPtyReq := ss.Pty(); isPtyReq {
  1005. w = ptyReq.Window
  1006. }
  1007. term := envValFromList(ss.Environ(), "TERM")
  1008. if term == "" {
  1009. term = "xterm-256color" // something non-empty
  1010. }
  1011. now := time.Now()
  1012. rec := &recording{
  1013. ss: ss,
  1014. start: now,
  1015. }
  1016. varRoot := ss.conn.srv.lb.TailscaleVarRoot()
  1017. if varRoot == "" {
  1018. return nil, errors.New("no var root for recording storage")
  1019. }
  1020. dir := filepath.Join(varRoot, "ssh-sessions")
  1021. if err := os.MkdirAll(dir, 0700); err != nil {
  1022. return nil, err
  1023. }
  1024. f, err := ioutil.TempFile(dir, fmt.Sprintf("ssh-session-%v-*.cast", now.UnixNano()))
  1025. if err != nil {
  1026. return nil, err
  1027. }
  1028. rec.out = f
  1029. // {"version": 2, "width": 221, "height": 84, "timestamp": 1647146075, "env": {"SHELL": "/bin/bash", "TERM": "screen"}}
  1030. type CastHeader struct {
  1031. Version int `json:"version"`
  1032. Width int `json:"width"`
  1033. Height int `json:"height"`
  1034. Timestamp int64 `json:"timestamp"`
  1035. Env map[string]string `json:"env"`
  1036. }
  1037. j, err := json.Marshal(CastHeader{
  1038. Version: 2,
  1039. Width: w.Width,
  1040. Height: w.Height,
  1041. Timestamp: now.Unix(),
  1042. Env: map[string]string{
  1043. "TERM": term,
  1044. // TODO(bradiftz): anything else important?
  1045. // including all seems noisey, but maybe we should
  1046. // for auditing. But first need to break
  1047. // launchProcess's startWithStdPipes and
  1048. // startWithPTY up so that they first return the cmd
  1049. // without starting it, and then a step that starts
  1050. // it. Then we can (1) make the cmd, (2) start the
  1051. // recording, (3) start the process.
  1052. },
  1053. })
  1054. if err != nil {
  1055. f.Close()
  1056. return nil, err
  1057. }
  1058. ss.logf("starting asciinema recording to %s", f.Name())
  1059. j = append(j, '\n')
  1060. if _, err := f.Write(j); err != nil {
  1061. f.Close()
  1062. return nil, err
  1063. }
  1064. return rec, nil
  1065. }
  1066. // recording is the state for an SSH session recording.
  1067. type recording struct {
  1068. ss *sshSession
  1069. start time.Time
  1070. mu sync.Mutex // guards writes to, close of out
  1071. out *os.File // nil if closed
  1072. }
  1073. func (r *recording) Close() error {
  1074. r.mu.Lock()
  1075. defer r.mu.Unlock()
  1076. if r.out == nil {
  1077. return nil
  1078. }
  1079. err := r.out.Close()
  1080. r.out = nil
  1081. return err
  1082. }
  1083. // writer returns an io.Writer around w that first records the write.
  1084. //
  1085. // The dir should be "i" for input or "o" for output.
  1086. //
  1087. // If r is nil, it returns w unchanged.
  1088. func (r *recording) writer(dir string, w io.Writer) io.Writer {
  1089. if r == nil {
  1090. return w
  1091. }
  1092. return &loggingWriter{r, dir, w}
  1093. }
  1094. // loggingWriter is an io.Writer wrapper that writes first an
  1095. // asciinema JSON cast format recording line, and then writes to w.
  1096. type loggingWriter struct {
  1097. r *recording
  1098. dir string // "i" or "o" (input or output)
  1099. w io.Writer // underlying Writer, after writing to r.out
  1100. }
  1101. func (w loggingWriter) Write(p []byte) (n int, err error) {
  1102. j, err := json.Marshal([]interface{}{
  1103. time.Since(w.r.start).Seconds(),
  1104. w.dir,
  1105. string(p),
  1106. })
  1107. if err != nil {
  1108. return 0, err
  1109. }
  1110. j = append(j, '\n')
  1111. if err := w.writeCastLine(j); err != nil {
  1112. return 0, err
  1113. }
  1114. return w.w.Write(p)
  1115. }
  1116. func (w loggingWriter) writeCastLine(j []byte) error {
  1117. w.r.mu.Lock()
  1118. defer w.r.mu.Unlock()
  1119. if w.r.out == nil {
  1120. return errors.New("logger closed")
  1121. }
  1122. _, err := w.r.out.Write(j)
  1123. if err != nil {
  1124. return fmt.Errorf("logger Write: %w", err)
  1125. }
  1126. return nil
  1127. }
  1128. func envValFromList(env []string, wantKey string) (v string) {
  1129. for _, kv := range env {
  1130. if thisKey, v, ok := strings.Cut(kv, "="); ok && envEq(thisKey, wantKey) {
  1131. return v
  1132. }
  1133. }
  1134. return ""
  1135. }
  1136. // envEq reports whether environment variable a == b for the current
  1137. // operating system.
  1138. func envEq(a, b string) bool {
  1139. if runtime.GOOS == "windows" {
  1140. return strings.EqualFold(a, b)
  1141. }
  1142. return a == b
  1143. }