tailssh_test.go 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux || darwin
  4. package tailssh
  5. import (
  6. "bytes"
  7. "context"
  8. "crypto/ecdsa"
  9. "crypto/ed25519"
  10. "crypto/elliptic"
  11. "crypto/rand"
  12. "encoding/json"
  13. "errors"
  14. "fmt"
  15. "io"
  16. "log"
  17. "net"
  18. "net/http"
  19. "net/http/httptest"
  20. "net/netip"
  21. "os"
  22. "os/exec"
  23. "os/user"
  24. "reflect"
  25. "runtime"
  26. "slices"
  27. "strconv"
  28. "strings"
  29. "sync"
  30. "sync/atomic"
  31. "testing"
  32. "time"
  33. gossh "golang.org/x/crypto/ssh"
  34. "golang.org/x/net/http2"
  35. "golang.org/x/net/http2/h2c"
  36. "tailscale.com/ipn/ipnlocal"
  37. "tailscale.com/ipn/store/mem"
  38. "tailscale.com/net/memnet"
  39. "tailscale.com/net/tsdial"
  40. "tailscale.com/sessionrecording"
  41. "tailscale.com/tailcfg"
  42. "tailscale.com/tempfork/gliderlabs/ssh"
  43. testssh "tailscale.com/tempfork/sshtest/ssh"
  44. "tailscale.com/tsd"
  45. "tailscale.com/tstest"
  46. "tailscale.com/types/key"
  47. "tailscale.com/types/logger"
  48. "tailscale.com/types/logid"
  49. "tailscale.com/types/netmap"
  50. "tailscale.com/types/ptr"
  51. "tailscale.com/util/cibuild"
  52. "tailscale.com/util/lineiter"
  53. "tailscale.com/util/must"
  54. "tailscale.com/version/distro"
  55. "tailscale.com/wgengine"
  56. )
  57. func TestMatchRule(t *testing.T) {
  58. someAction := new(tailcfg.SSHAction)
  59. tests := []struct {
  60. name string
  61. rule *tailcfg.SSHRule
  62. ci *sshConnInfo
  63. wantErr error
  64. wantUser string
  65. wantAcceptEnv []string
  66. }{
  67. {
  68. name: "invalid-conn",
  69. rule: &tailcfg.SSHRule{
  70. Action: someAction,
  71. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  72. SSHUsers: map[string]string{
  73. "*": "ubuntu",
  74. },
  75. },
  76. wantErr: errInvalidConn,
  77. },
  78. {
  79. name: "nil-rule",
  80. ci: &sshConnInfo{},
  81. rule: nil,
  82. wantErr: errNilRule,
  83. },
  84. {
  85. name: "nil-action",
  86. ci: &sshConnInfo{},
  87. rule: &tailcfg.SSHRule{},
  88. wantErr: errNilAction,
  89. },
  90. {
  91. name: "expired",
  92. rule: &tailcfg.SSHRule{
  93. Action: someAction,
  94. RuleExpires: ptr.To(time.Unix(100, 0)),
  95. },
  96. ci: &sshConnInfo{},
  97. wantErr: errRuleExpired,
  98. },
  99. {
  100. name: "no-principal",
  101. rule: &tailcfg.SSHRule{
  102. Action: someAction,
  103. SSHUsers: map[string]string{
  104. "*": "ubuntu",
  105. }},
  106. ci: &sshConnInfo{},
  107. wantErr: errPrincipalMatch,
  108. },
  109. {
  110. name: "no-user-match",
  111. rule: &tailcfg.SSHRule{
  112. Action: someAction,
  113. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  114. },
  115. ci: &sshConnInfo{sshUser: "alice"},
  116. wantErr: errUserMatch,
  117. },
  118. {
  119. name: "ok-wildcard",
  120. rule: &tailcfg.SSHRule{
  121. Action: someAction,
  122. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  123. SSHUsers: map[string]string{
  124. "*": "ubuntu",
  125. },
  126. },
  127. ci: &sshConnInfo{sshUser: "alice"},
  128. wantUser: "ubuntu",
  129. },
  130. {
  131. name: "ok-wildcard-and-nil-principal",
  132. rule: &tailcfg.SSHRule{
  133. Action: someAction,
  134. Principals: []*tailcfg.SSHPrincipal{
  135. nil, // don't crash on this
  136. {Any: true},
  137. },
  138. SSHUsers: map[string]string{
  139. "*": "ubuntu",
  140. },
  141. },
  142. ci: &sshConnInfo{sshUser: "alice"},
  143. wantUser: "ubuntu",
  144. },
  145. {
  146. name: "ok-exact",
  147. rule: &tailcfg.SSHRule{
  148. Action: someAction,
  149. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  150. SSHUsers: map[string]string{
  151. "*": "ubuntu",
  152. "alice": "thealice",
  153. },
  154. },
  155. ci: &sshConnInfo{sshUser: "alice"},
  156. wantUser: "thealice",
  157. },
  158. {
  159. name: "ok-with-accept-env",
  160. rule: &tailcfg.SSHRule{
  161. Action: someAction,
  162. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  163. SSHUsers: map[string]string{
  164. "*": "ubuntu",
  165. "alice": "thealice",
  166. },
  167. AcceptEnv: []string{"EXAMPLE", "?_?", "TEST_*"},
  168. },
  169. ci: &sshConnInfo{sshUser: "alice"},
  170. wantUser: "thealice",
  171. wantAcceptEnv: []string{"EXAMPLE", "?_?", "TEST_*"},
  172. },
  173. {
  174. name: "no-users-for-reject",
  175. rule: &tailcfg.SSHRule{
  176. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  177. Action: &tailcfg.SSHAction{Reject: true},
  178. },
  179. ci: &sshConnInfo{sshUser: "alice"},
  180. },
  181. {
  182. name: "match-principal-node-ip",
  183. rule: &tailcfg.SSHRule{
  184. Action: someAction,
  185. Principals: []*tailcfg.SSHPrincipal{{NodeIP: "1.2.3.4"}},
  186. SSHUsers: map[string]string{"*": "ubuntu"},
  187. },
  188. ci: &sshConnInfo{src: netip.MustParseAddrPort("1.2.3.4:30343")},
  189. wantUser: "ubuntu",
  190. },
  191. {
  192. name: "match-principal-node-id",
  193. rule: &tailcfg.SSHRule{
  194. Action: someAction,
  195. Principals: []*tailcfg.SSHPrincipal{{Node: "some-node-ID"}},
  196. SSHUsers: map[string]string{"*": "ubuntu"},
  197. },
  198. ci: &sshConnInfo{node: (&tailcfg.Node{StableID: "some-node-ID"}).View()},
  199. wantUser: "ubuntu",
  200. },
  201. {
  202. name: "match-principal-userlogin",
  203. rule: &tailcfg.SSHRule{
  204. Action: someAction,
  205. Principals: []*tailcfg.SSHPrincipal{{UserLogin: "[email protected]"}},
  206. SSHUsers: map[string]string{"*": "ubuntu"},
  207. },
  208. ci: &sshConnInfo{uprof: tailcfg.UserProfile{LoginName: "[email protected]"}},
  209. wantUser: "ubuntu",
  210. },
  211. {
  212. name: "ssh-user-equal",
  213. rule: &tailcfg.SSHRule{
  214. Action: someAction,
  215. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  216. SSHUsers: map[string]string{
  217. "*": "=",
  218. },
  219. },
  220. ci: &sshConnInfo{sshUser: "alice"},
  221. wantUser: "alice",
  222. },
  223. }
  224. for _, tt := range tests {
  225. t.Run(tt.name, func(t *testing.T) {
  226. c := &conn{
  227. info: tt.ci,
  228. srv: &server{logf: t.Logf},
  229. }
  230. got, gotUser, gotAcceptEnv, err := c.matchRule(tt.rule)
  231. if err != tt.wantErr {
  232. t.Errorf("err = %v; want %v", err, tt.wantErr)
  233. }
  234. if gotUser != tt.wantUser {
  235. t.Errorf("user = %q; want %q", gotUser, tt.wantUser)
  236. }
  237. if err == nil && got == nil {
  238. t.Errorf("expected non-nil action on success")
  239. }
  240. if !slices.Equal(gotAcceptEnv, tt.wantAcceptEnv) {
  241. t.Errorf("acceptEnv = %v; want %v", gotAcceptEnv, tt.wantAcceptEnv)
  242. }
  243. })
  244. }
  245. }
  246. func TestEvalSSHPolicy(t *testing.T) {
  247. someAction := new(tailcfg.SSHAction)
  248. tests := []struct {
  249. name string
  250. policy *tailcfg.SSHPolicy
  251. ci *sshConnInfo
  252. wantMatch bool
  253. wantUser string
  254. wantAcceptEnv []string
  255. }{
  256. {
  257. name: "multiple-matches-picks-first-match",
  258. policy: &tailcfg.SSHPolicy{
  259. Rules: []*tailcfg.SSHRule{
  260. {
  261. Action: someAction,
  262. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  263. SSHUsers: map[string]string{
  264. "other": "other1",
  265. },
  266. },
  267. {
  268. Action: someAction,
  269. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  270. SSHUsers: map[string]string{
  271. "*": "ubuntu",
  272. "alice": "thealice",
  273. },
  274. AcceptEnv: []string{"EXAMPLE", "?_?", "TEST_*"},
  275. },
  276. {
  277. Action: someAction,
  278. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  279. SSHUsers: map[string]string{
  280. "other2": "other3",
  281. },
  282. },
  283. {
  284. Action: someAction,
  285. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  286. SSHUsers: map[string]string{
  287. "*": "ubuntu",
  288. "alice": "thealice",
  289. "mark": "markthe",
  290. },
  291. AcceptEnv: []string{"*"},
  292. },
  293. },
  294. },
  295. ci: &sshConnInfo{sshUser: "alice"},
  296. wantUser: "thealice",
  297. wantAcceptEnv: []string{"EXAMPLE", "?_?", "TEST_*"},
  298. wantMatch: true,
  299. },
  300. {
  301. name: "no-matches-returns-failure",
  302. policy: &tailcfg.SSHPolicy{
  303. Rules: []*tailcfg.SSHRule{
  304. {
  305. Action: someAction,
  306. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  307. SSHUsers: map[string]string{
  308. "other": "other1",
  309. },
  310. },
  311. {
  312. Action: someAction,
  313. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  314. SSHUsers: map[string]string{
  315. "fedora": "ubuntu",
  316. },
  317. AcceptEnv: []string{"EXAMPLE", "?_?", "TEST_*"},
  318. },
  319. {
  320. Action: someAction,
  321. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  322. SSHUsers: map[string]string{
  323. "other2": "other3",
  324. },
  325. },
  326. {
  327. Action: someAction,
  328. Principals: []*tailcfg.SSHPrincipal{{Any: true}},
  329. SSHUsers: map[string]string{
  330. "mark": "markthe",
  331. },
  332. AcceptEnv: []string{"*"},
  333. },
  334. },
  335. },
  336. ci: &sshConnInfo{sshUser: "alice"},
  337. wantUser: "",
  338. wantAcceptEnv: nil,
  339. wantMatch: false,
  340. },
  341. }
  342. for _, tt := range tests {
  343. t.Run(tt.name, func(t *testing.T) {
  344. c := &conn{
  345. info: tt.ci,
  346. srv: &server{logf: t.Logf},
  347. }
  348. got, gotUser, gotAcceptEnv, match := c.evalSSHPolicy(tt.policy)
  349. if match != tt.wantMatch {
  350. t.Errorf("match = %v; want %v", match, tt.wantMatch)
  351. }
  352. if gotUser != tt.wantUser {
  353. t.Errorf("user = %q; want %q", gotUser, tt.wantUser)
  354. }
  355. if tt.wantMatch == true && got == nil {
  356. t.Errorf("expected non-nil action on success")
  357. }
  358. if !slices.Equal(gotAcceptEnv, tt.wantAcceptEnv) {
  359. t.Errorf("acceptEnv = %v; want %v", gotAcceptEnv, tt.wantAcceptEnv)
  360. }
  361. })
  362. }
  363. }
  364. // localState implements ipnLocalBackend for testing.
  365. type localState struct {
  366. sshEnabled bool
  367. matchingRule *tailcfg.SSHRule
  368. // serverActions is a map of the action name to the action.
  369. // It is served for paths like https://unused/ssh-action/<action-name>.
  370. // The action name is the last part of the action URL.
  371. serverActions map[string]*tailcfg.SSHAction
  372. }
  373. var (
  374. currentUser = os.Getenv("USER") // Use the current user for the test.
  375. testSigner gossh.Signer
  376. testSignerOnce sync.Once
  377. )
  378. func (ts *localState) Dialer() *tsdial.Dialer {
  379. return &tsdial.Dialer{}
  380. }
  381. func (ts *localState) GetSSH_HostKeys() ([]gossh.Signer, error) {
  382. testSignerOnce.Do(func() {
  383. _, priv, err := ed25519.GenerateKey(rand.Reader)
  384. if err != nil {
  385. panic(err)
  386. }
  387. s, err := gossh.NewSignerFromSigner(priv)
  388. if err != nil {
  389. panic(err)
  390. }
  391. testSigner = s
  392. })
  393. return []gossh.Signer{testSigner}, nil
  394. }
  395. func (ts *localState) ShouldRunSSH() bool {
  396. return ts.sshEnabled
  397. }
  398. func (ts *localState) NetMap() *netmap.NetworkMap {
  399. var policy *tailcfg.SSHPolicy
  400. if ts.matchingRule != nil {
  401. policy = &tailcfg.SSHPolicy{
  402. Rules: []*tailcfg.SSHRule{
  403. ts.matchingRule,
  404. },
  405. }
  406. }
  407. return &netmap.NetworkMap{
  408. SelfNode: (&tailcfg.Node{
  409. ID: 1,
  410. }).View(),
  411. SSHPolicy: policy,
  412. }
  413. }
  414. func (ts *localState) WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  415. if proto != "tcp" {
  416. return tailcfg.NodeView{}, tailcfg.UserProfile{}, false
  417. }
  418. return (&tailcfg.Node{
  419. ID: 2,
  420. StableID: "peer-id",
  421. }).View(), tailcfg.UserProfile{
  422. LoginName: "peer",
  423. }, true
  424. }
  425. func (ts *localState) DoNoiseRequest(req *http.Request) (*http.Response, error) {
  426. rec := httptest.NewRecorder()
  427. k, ok := strings.CutPrefix(req.URL.Path, "/ssh-action/")
  428. if !ok {
  429. rec.WriteHeader(http.StatusNotFound)
  430. }
  431. a, ok := ts.serverActions[k]
  432. if !ok {
  433. rec.WriteHeader(http.StatusNotFound)
  434. return rec.Result(), nil
  435. }
  436. rec.WriteHeader(http.StatusOK)
  437. if err := json.NewEncoder(rec).Encode(a); err != nil {
  438. return nil, err
  439. }
  440. return rec.Result(), nil
  441. }
  442. func (ts *localState) TailscaleVarRoot() string {
  443. return ""
  444. }
  445. func (ts *localState) NodeKey() key.NodePublic {
  446. return key.NewNode().Public()
  447. }
  448. func newSSHRule(action *tailcfg.SSHAction) *tailcfg.SSHRule {
  449. return &tailcfg.SSHRule{
  450. SSHUsers: map[string]string{
  451. "*": currentUser,
  452. },
  453. Action: action,
  454. Principals: []*tailcfg.SSHPrincipal{
  455. {
  456. Any: true,
  457. },
  458. },
  459. }
  460. }
  461. func TestSSHRecordingCancelsSessionsOnUploadFailure(t *testing.T) {
  462. if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
  463. t.Skipf("skipping on %q; only runs on linux and darwin", runtime.GOOS)
  464. }
  465. var handler http.HandlerFunc
  466. recordingServer := mockRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
  467. handler(w, r)
  468. })
  469. s := &server{
  470. logf: t.Logf,
  471. lb: &localState{
  472. sshEnabled: true,
  473. matchingRule: newSSHRule(
  474. &tailcfg.SSHAction{
  475. Accept: true,
  476. Recorders: []netip.AddrPort{
  477. netip.MustParseAddrPort(recordingServer.Listener.Addr().String()),
  478. },
  479. OnRecordingFailure: &tailcfg.SSHRecorderFailureAction{
  480. RejectSessionWithMessage: "session rejected",
  481. TerminateSessionWithMessage: "session terminated",
  482. },
  483. },
  484. ),
  485. },
  486. }
  487. defer s.Shutdown()
  488. const sshUser = "alice"
  489. cfg := &testssh.ClientConfig{
  490. User: sshUser,
  491. HostKeyCallback: testssh.InsecureIgnoreHostKey(),
  492. }
  493. tests := []struct {
  494. name string
  495. handler func(w http.ResponseWriter, r *http.Request)
  496. sshCommand string
  497. wantClientOutput string
  498. clientOutputMustNotContain []string
  499. }{
  500. {
  501. name: "upload-denied",
  502. handler: func(w http.ResponseWriter, r *http.Request) {
  503. w.WriteHeader(http.StatusForbidden)
  504. },
  505. sshCommand: "echo hello",
  506. wantClientOutput: "session rejected\r\n",
  507. clientOutputMustNotContain: []string{"hello"},
  508. },
  509. {
  510. name: "upload-fails-after-starting",
  511. handler: func(w http.ResponseWriter, r *http.Request) {
  512. w.WriteHeader(http.StatusOK)
  513. w.(http.Flusher).Flush()
  514. r.Body.Read(make([]byte, 1))
  515. time.Sleep(100 * time.Millisecond)
  516. },
  517. sshCommand: "echo hello && sleep 1 && echo world",
  518. wantClientOutput: "\r\n\r\nsession terminated\r\n\r\n",
  519. clientOutputMustNotContain: []string{"world"},
  520. },
  521. }
  522. src, dst := must.Get(netip.ParseAddrPort("100.100.100.101:2231")), must.Get(netip.ParseAddrPort("100.100.100.102:22"))
  523. for _, tt := range tests {
  524. t.Run(tt.name, func(t *testing.T) {
  525. s.logf = t.Logf
  526. tstest.Replace(t, &handler, tt.handler)
  527. sc, dc := memnet.NewTCPConn(src, dst, 1024)
  528. var wg sync.WaitGroup
  529. wg.Add(1)
  530. go func() {
  531. defer wg.Done()
  532. c, chans, reqs, err := testssh.NewClientConn(sc, sc.RemoteAddr().String(), cfg)
  533. if err != nil {
  534. t.Errorf("client: %v", err)
  535. return
  536. }
  537. client := testssh.NewClient(c, chans, reqs)
  538. defer client.Close()
  539. session, err := client.NewSession()
  540. if err != nil {
  541. t.Errorf("client: %v", err)
  542. return
  543. }
  544. defer session.Close()
  545. t.Logf("client established session")
  546. got, err := session.CombinedOutput(tt.sshCommand)
  547. if err != nil {
  548. t.Logf("client got: %q: %v", got, err)
  549. } else {
  550. t.Errorf("client did not get kicked out: %q", got)
  551. }
  552. gotStr := string(got)
  553. if !strings.HasSuffix(gotStr, tt.wantClientOutput) {
  554. t.Errorf("client got %q, want %q", got, tt.wantClientOutput)
  555. }
  556. for _, x := range tt.clientOutputMustNotContain {
  557. if strings.Contains(gotStr, x) {
  558. t.Errorf("client output must not contain %q", x)
  559. }
  560. }
  561. }()
  562. if err := s.HandleSSHConn(dc); err != nil {
  563. t.Errorf("unexpected error: %v", err)
  564. }
  565. wg.Wait()
  566. })
  567. }
  568. }
  569. func TestMultipleRecorders(t *testing.T) {
  570. if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
  571. t.Skipf("skipping on %q; only runs on linux and darwin", runtime.GOOS)
  572. }
  573. done := make(chan struct{})
  574. recordingServer := mockRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
  575. defer close(done)
  576. w.WriteHeader(http.StatusOK)
  577. w.(http.Flusher).Flush()
  578. io.ReadAll(r.Body)
  579. })
  580. badRecorder, err := net.Listen("tcp", ":0")
  581. if err != nil {
  582. t.Fatal(err)
  583. }
  584. badRecorderAddr := badRecorder.Addr().String()
  585. badRecorder.Close()
  586. badRecordingServer500 := mockRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
  587. w.WriteHeader(http.StatusInternalServerError)
  588. })
  589. s := &server{
  590. logf: t.Logf,
  591. lb: &localState{
  592. sshEnabled: true,
  593. matchingRule: newSSHRule(
  594. &tailcfg.SSHAction{
  595. Accept: true,
  596. Recorders: []netip.AddrPort{
  597. netip.MustParseAddrPort(badRecorderAddr),
  598. netip.MustParseAddrPort(badRecordingServer500.Listener.Addr().String()),
  599. netip.MustParseAddrPort(recordingServer.Listener.Addr().String()),
  600. },
  601. OnRecordingFailure: &tailcfg.SSHRecorderFailureAction{
  602. RejectSessionWithMessage: "session rejected",
  603. TerminateSessionWithMessage: "session terminated",
  604. },
  605. },
  606. ),
  607. },
  608. }
  609. defer s.Shutdown()
  610. src, dst := must.Get(netip.ParseAddrPort("100.100.100.101:2231")), must.Get(netip.ParseAddrPort("100.100.100.102:22"))
  611. sc, dc := memnet.NewTCPConn(src, dst, 1024)
  612. const sshUser = "alice"
  613. cfg := &testssh.ClientConfig{
  614. User: sshUser,
  615. HostKeyCallback: testssh.InsecureIgnoreHostKey(),
  616. }
  617. var wg sync.WaitGroup
  618. wg.Add(1)
  619. go func() {
  620. defer wg.Done()
  621. c, chans, reqs, err := testssh.NewClientConn(sc, sc.RemoteAddr().String(), cfg)
  622. if err != nil {
  623. t.Errorf("client: %v", err)
  624. return
  625. }
  626. client := testssh.NewClient(c, chans, reqs)
  627. defer client.Close()
  628. session, err := client.NewSession()
  629. if err != nil {
  630. t.Errorf("client: %v", err)
  631. return
  632. }
  633. defer session.Close()
  634. t.Logf("client established session")
  635. out, err := session.CombinedOutput("echo Ran echo!")
  636. if err != nil {
  637. t.Errorf("client: %v", err)
  638. }
  639. if string(out) != "Ran echo!\n" {
  640. t.Errorf("client: unexpected output: %q", out)
  641. }
  642. }()
  643. if err := s.HandleSSHConn(dc); err != nil {
  644. t.Errorf("unexpected error: %v", err)
  645. }
  646. wg.Wait()
  647. select {
  648. case <-done:
  649. case <-time.After(1 * time.Second):
  650. t.Fatal("timed out waiting for recording")
  651. }
  652. }
  653. // TestSSHRecordingNonInteractive tests that the SSH server records the SSH session
  654. // when the client is not interactive (i.e. no PTY).
  655. // It starts a local SSH server and a recording server. The recording server
  656. // records the SSH session and returns it to the test.
  657. // The test then verifies that the recording has a valid CastHeader, it does not
  658. // validate the contents of the recording.
  659. func TestSSHRecordingNonInteractive(t *testing.T) {
  660. if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
  661. t.Skipf("skipping on %q; only runs on linux and darwin", runtime.GOOS)
  662. }
  663. var recording []byte
  664. ctx, cancel := context.WithTimeout(context.Background(), time.Second)
  665. recordingServer := mockRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
  666. defer cancel()
  667. w.WriteHeader(http.StatusOK)
  668. w.(http.Flusher).Flush()
  669. var err error
  670. recording, err = io.ReadAll(r.Body)
  671. if err != nil {
  672. t.Error(err)
  673. return
  674. }
  675. })
  676. s := &server{
  677. logf: t.Logf,
  678. lb: &localState{
  679. sshEnabled: true,
  680. matchingRule: newSSHRule(
  681. &tailcfg.SSHAction{
  682. Accept: true,
  683. Recorders: []netip.AddrPort{
  684. must.Get(netip.ParseAddrPort(recordingServer.Listener.Addr().String())),
  685. },
  686. OnRecordingFailure: &tailcfg.SSHRecorderFailureAction{
  687. RejectSessionWithMessage: "session rejected",
  688. TerminateSessionWithMessage: "session terminated",
  689. },
  690. },
  691. ),
  692. },
  693. }
  694. defer s.Shutdown()
  695. src, dst := must.Get(netip.ParseAddrPort("100.100.100.101:2231")), must.Get(netip.ParseAddrPort("100.100.100.102:22"))
  696. sc, dc := memnet.NewTCPConn(src, dst, 1024)
  697. const sshUser = "alice"
  698. cfg := &testssh.ClientConfig{
  699. User: sshUser,
  700. HostKeyCallback: testssh.InsecureIgnoreHostKey(),
  701. }
  702. var wg sync.WaitGroup
  703. wg.Add(1)
  704. go func() {
  705. defer wg.Done()
  706. c, chans, reqs, err := testssh.NewClientConn(sc, sc.RemoteAddr().String(), cfg)
  707. if err != nil {
  708. t.Errorf("client: %v", err)
  709. return
  710. }
  711. client := testssh.NewClient(c, chans, reqs)
  712. defer client.Close()
  713. session, err := client.NewSession()
  714. if err != nil {
  715. t.Errorf("client: %v", err)
  716. return
  717. }
  718. defer session.Close()
  719. t.Logf("client established session")
  720. _, err = session.CombinedOutput("echo Ran echo!")
  721. if err != nil {
  722. t.Errorf("client: %v", err)
  723. }
  724. }()
  725. if err := s.HandleSSHConn(dc); err != nil {
  726. t.Errorf("unexpected error: %v", err)
  727. }
  728. wg.Wait()
  729. <-ctx.Done() // wait for recording to finish
  730. var ch sessionrecording.CastHeader
  731. if err := json.NewDecoder(bytes.NewReader(recording)).Decode(&ch); err != nil {
  732. t.Fatal(err)
  733. }
  734. if ch.SSHUser != sshUser {
  735. t.Errorf("SSHUser = %q; want %q", ch.SSHUser, sshUser)
  736. }
  737. if ch.Command != "echo Ran echo!" {
  738. t.Errorf("Command = %q; want %q", ch.Command, "echo Ran echo!")
  739. }
  740. }
  741. func TestSSHAuthFlow(t *testing.T) {
  742. if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
  743. t.Skipf("skipping on %q; only runs on linux and darwin", runtime.GOOS)
  744. }
  745. acceptRule := newSSHRule(&tailcfg.SSHAction{
  746. Accept: true,
  747. Message: "Welcome to Tailscale SSH!",
  748. })
  749. rejectRule := newSSHRule(&tailcfg.SSHAction{
  750. Reject: true,
  751. Message: "Go Away!",
  752. })
  753. tests := []struct {
  754. name string
  755. sshUser string // defaults to alice
  756. state *localState
  757. wantBanners []string
  758. usesPassword bool
  759. authErr bool
  760. }{
  761. {
  762. name: "no-policy",
  763. state: &localState{
  764. sshEnabled: true,
  765. },
  766. authErr: true,
  767. wantBanners: []string{"tailscale: failed to evaluate SSH policy"},
  768. },
  769. {
  770. name: "accept",
  771. state: &localState{
  772. sshEnabled: true,
  773. matchingRule: acceptRule,
  774. },
  775. wantBanners: []string{"Welcome to Tailscale SSH!"},
  776. },
  777. {
  778. name: "reject",
  779. state: &localState{
  780. sshEnabled: true,
  781. matchingRule: rejectRule,
  782. },
  783. wantBanners: []string{"Go Away!"},
  784. authErr: true,
  785. },
  786. {
  787. name: "simple-check",
  788. state: &localState{
  789. sshEnabled: true,
  790. matchingRule: newSSHRule(&tailcfg.SSHAction{
  791. HoldAndDelegate: "https://unused/ssh-action/accept",
  792. }),
  793. serverActions: map[string]*tailcfg.SSHAction{
  794. "accept": acceptRule.Action,
  795. },
  796. },
  797. wantBanners: []string{"Welcome to Tailscale SSH!"},
  798. },
  799. {
  800. name: "multi-check",
  801. state: &localState{
  802. sshEnabled: true,
  803. matchingRule: newSSHRule(&tailcfg.SSHAction{
  804. Message: "First",
  805. HoldAndDelegate: "https://unused/ssh-action/check1",
  806. }),
  807. serverActions: map[string]*tailcfg.SSHAction{
  808. "check1": {
  809. Message: "url-here",
  810. HoldAndDelegate: "https://unused/ssh-action/check2",
  811. },
  812. "check2": acceptRule.Action,
  813. },
  814. },
  815. wantBanners: []string{"First", "url-here", "Welcome to Tailscale SSH!"},
  816. },
  817. {
  818. name: "check-reject",
  819. state: &localState{
  820. sshEnabled: true,
  821. matchingRule: newSSHRule(&tailcfg.SSHAction{
  822. Message: "First",
  823. HoldAndDelegate: "https://unused/ssh-action/reject",
  824. }),
  825. serverActions: map[string]*tailcfg.SSHAction{
  826. "reject": rejectRule.Action,
  827. },
  828. },
  829. wantBanners: []string{"First", "Go Away!"},
  830. authErr: true,
  831. },
  832. {
  833. name: "force-password-auth",
  834. sshUser: "alice+password",
  835. state: &localState{
  836. sshEnabled: true,
  837. matchingRule: acceptRule,
  838. },
  839. usesPassword: true,
  840. wantBanners: []string{"Welcome to Tailscale SSH!"},
  841. },
  842. }
  843. s := &server{
  844. logf: log.Printf,
  845. }
  846. defer s.Shutdown()
  847. src, dst := must.Get(netip.ParseAddrPort("100.100.100.101:2231")), must.Get(netip.ParseAddrPort("100.100.100.102:22"))
  848. for _, tc := range tests {
  849. for _, authMethods := range [][]string{nil, {"publickey", "password"}, {"password", "publickey"}} {
  850. t.Run(fmt.Sprintf("%s-skip-none-auth-%v", tc.name, strings.Join(authMethods, "-then-")), func(t *testing.T) {
  851. sc, dc := memnet.NewTCPConn(src, dst, 1024)
  852. s.lb = tc.state
  853. sshUser := "alice"
  854. if tc.sshUser != "" {
  855. sshUser = tc.sshUser
  856. }
  857. wantBanners := slices.Clone(tc.wantBanners)
  858. noneAuthEnabled := len(authMethods) == 0
  859. var publicKeyUsed atomic.Bool
  860. var passwordUsed atomic.Bool
  861. var methods []testssh.AuthMethod
  862. for _, authMethod := range authMethods {
  863. switch authMethod {
  864. case "publickey":
  865. methods = append(methods,
  866. testssh.PublicKeysCallback(func() (signers []testssh.Signer, err error) {
  867. publicKeyUsed.Store(true)
  868. key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
  869. if err != nil {
  870. return nil, err
  871. }
  872. sig, err := testssh.NewSignerFromKey(key)
  873. if err != nil {
  874. return nil, err
  875. }
  876. return []testssh.Signer{sig}, nil
  877. }))
  878. case "password":
  879. methods = append(methods, testssh.PasswordCallback(func() (secret string, err error) {
  880. passwordUsed.Store(true)
  881. return "any-pass", nil
  882. }))
  883. }
  884. }
  885. if noneAuthEnabled && tc.usesPassword {
  886. methods = append(methods, testssh.PasswordCallback(func() (secret string, err error) {
  887. passwordUsed.Store(true)
  888. return "any-pass", nil
  889. }))
  890. }
  891. cfg := &testssh.ClientConfig{
  892. User: sshUser,
  893. HostKeyCallback: testssh.InsecureIgnoreHostKey(),
  894. SkipNoneAuth: !noneAuthEnabled,
  895. Auth: methods,
  896. BannerCallback: func(message string) error {
  897. if len(wantBanners) == 0 {
  898. t.Errorf("unexpected banner: %q", message)
  899. } else if message != wantBanners[0] {
  900. t.Errorf("banner = %q; want %q", message, wantBanners[0])
  901. } else {
  902. t.Logf("banner = %q", message)
  903. wantBanners = wantBanners[1:]
  904. }
  905. return nil
  906. },
  907. }
  908. var wg sync.WaitGroup
  909. wg.Add(1)
  910. go func() {
  911. defer wg.Done()
  912. c, chans, reqs, err := testssh.NewClientConn(sc, sc.RemoteAddr().String(), cfg)
  913. if err != nil {
  914. if !tc.authErr {
  915. t.Errorf("client: %v", err)
  916. }
  917. return
  918. } else if tc.authErr {
  919. c.Close()
  920. t.Errorf("client: expected error, got nil")
  921. return
  922. }
  923. client := testssh.NewClient(c, chans, reqs)
  924. defer client.Close()
  925. session, err := client.NewSession()
  926. if err != nil {
  927. t.Errorf("client: %v", err)
  928. return
  929. }
  930. defer session.Close()
  931. _, err = session.CombinedOutput("echo Ran echo!")
  932. if err != nil {
  933. t.Errorf("client: %v", err)
  934. }
  935. }()
  936. if err := s.HandleSSHConn(dc); err != nil {
  937. t.Errorf("unexpected error: %v", err)
  938. }
  939. wg.Wait()
  940. if len(wantBanners) > 0 {
  941. t.Errorf("missing banners: %v", wantBanners)
  942. }
  943. // Check to see which callbacks were invoked.
  944. //
  945. // When `none` auth is enabled, the public key callback should
  946. // never fire, and the password callback should only fire if
  947. // authentication succeeded and the client was trying to force
  948. // password authentication by connecting with the '-password'
  949. // username suffix.
  950. //
  951. // When skipping `none` auth, the first callback should always
  952. // fire, and the 2nd callback should fire only if
  953. // authentication failed.
  954. wantPublicKey := false
  955. wantPassword := false
  956. if noneAuthEnabled {
  957. wantPassword = !tc.authErr && tc.usesPassword
  958. } else {
  959. for i, authMethod := range authMethods {
  960. switch authMethod {
  961. case "publickey":
  962. wantPublicKey = i == 0 || tc.authErr
  963. case "password":
  964. wantPassword = i == 0 || tc.authErr
  965. }
  966. }
  967. }
  968. if wantPublicKey && !publicKeyUsed.Load() {
  969. t.Error("public key should have been attempted")
  970. } else if !wantPublicKey && publicKeyUsed.Load() {
  971. t.Errorf("public key should not have been attempted")
  972. }
  973. if wantPassword && !passwordUsed.Load() {
  974. t.Error("password should have been attempted")
  975. } else if !wantPassword && passwordUsed.Load() {
  976. t.Error("password should not have been attempted")
  977. }
  978. })
  979. }
  980. }
  981. }
  982. func TestSSH(t *testing.T) {
  983. var logf logger.Logf = t.Logf
  984. sys := tsd.NewSystem()
  985. eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
  986. if err != nil {
  987. t.Fatal(err)
  988. }
  989. sys.Set(eng)
  990. sys.Set(new(mem.Store))
  991. lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
  992. if err != nil {
  993. t.Fatal(err)
  994. }
  995. defer lb.Shutdown()
  996. dir := t.TempDir()
  997. lb.SetVarRoot(dir)
  998. srv := &server{
  999. lb: lb,
  1000. logf: logf,
  1001. }
  1002. sc, err := srv.newConn()
  1003. if err != nil {
  1004. t.Fatal(err)
  1005. }
  1006. // Remove the auth checks for the test
  1007. sc.insecureSkipTailscaleAuth = true
  1008. u, err := user.Current()
  1009. if err != nil {
  1010. t.Fatal(err)
  1011. }
  1012. um, err := userLookup(u.Username)
  1013. if err != nil {
  1014. t.Fatal(err)
  1015. }
  1016. sc.localUser = um
  1017. sc.info = &sshConnInfo{
  1018. sshUser: "test",
  1019. src: netip.MustParseAddrPort("1.2.3.4:32342"),
  1020. dst: netip.MustParseAddrPort("1.2.3.5:22"),
  1021. node: (&tailcfg.Node{}).View(),
  1022. uprof: tailcfg.UserProfile{},
  1023. }
  1024. sc.action0 = &tailcfg.SSHAction{Accept: true}
  1025. sc.finalAction = sc.action0
  1026. sc.Handler = func(s ssh.Session) {
  1027. sc.newSSHSession(s).run()
  1028. }
  1029. ln, err := net.Listen("tcp4", "127.0.0.1:0")
  1030. if err != nil {
  1031. t.Fatal(err)
  1032. }
  1033. defer ln.Close()
  1034. port := ln.Addr().(*net.TCPAddr).Port
  1035. go func() {
  1036. for {
  1037. c, err := ln.Accept()
  1038. if err != nil {
  1039. if !errors.Is(err, net.ErrClosed) {
  1040. t.Errorf("Accept: %v", err)
  1041. }
  1042. return
  1043. }
  1044. go sc.HandleConn(c)
  1045. }
  1046. }()
  1047. execSSH := func(args ...string) *exec.Cmd {
  1048. cmd := exec.Command("ssh",
  1049. "-F",
  1050. "none",
  1051. "-v",
  1052. "-p", fmt.Sprint(port),
  1053. "-o", "StrictHostKeyChecking=no",
  1054. "[email protected]")
  1055. cmd.Args = append(cmd.Args, args...)
  1056. return cmd
  1057. }
  1058. t.Run("env", func(t *testing.T) {
  1059. if cibuild.On() {
  1060. t.Skip("Skipping for now; see https://github.com/tailscale/tailscale/issues/4051")
  1061. }
  1062. cmd := execSSH("LANG=foo env")
  1063. cmd.Env = append(os.Environ(), "LOCAL_ENV=bar")
  1064. got, err := cmd.CombinedOutput()
  1065. if err != nil {
  1066. t.Fatal(err, string(got))
  1067. }
  1068. m := parseEnv(got)
  1069. if got := m["USER"]; got == "" || got != u.Username {
  1070. t.Errorf("USER = %q; want %q", got, u.Username)
  1071. }
  1072. if got := m["HOME"]; got == "" || got != u.HomeDir {
  1073. t.Errorf("HOME = %q; want %q", got, u.HomeDir)
  1074. }
  1075. if got := m["PWD"]; got == "" || got != u.HomeDir {
  1076. t.Errorf("PWD = %q; want %q", got, u.HomeDir)
  1077. }
  1078. if got := m["SHELL"]; got == "" {
  1079. t.Errorf("no SHELL")
  1080. }
  1081. if got, want := m["LANG"], "foo"; got != want {
  1082. t.Errorf("LANG = %q; want %q", got, want)
  1083. }
  1084. if got := m["LOCAL_ENV"]; got != "" {
  1085. t.Errorf("LOCAL_ENV leaked over ssh: %v", got)
  1086. }
  1087. t.Logf("got: %+v", m)
  1088. })
  1089. t.Run("stdout_stderr", func(t *testing.T) {
  1090. cmd := execSSH("sh", "-c", "echo foo; echo bar >&2")
  1091. var outBuf, errBuf bytes.Buffer
  1092. cmd.Stdout = &outBuf
  1093. cmd.Stderr = &errBuf
  1094. if err := cmd.Run(); err != nil {
  1095. t.Fatal(err)
  1096. }
  1097. t.Logf("Got: %q and %q", outBuf.Bytes(), errBuf.Bytes())
  1098. // TODO: figure out why these aren't right. should be
  1099. // "foo\n" and "bar\n", not "\n" and "bar\n".
  1100. })
  1101. t.Run("large_file", func(t *testing.T) {
  1102. const wantSize = 1e6
  1103. var outBuf bytes.Buffer
  1104. cmd := execSSH("head", "-c", strconv.Itoa(wantSize), "/dev/zero")
  1105. cmd.Stdout = &outBuf
  1106. if err := cmd.Run(); err != nil {
  1107. t.Fatal(err)
  1108. }
  1109. if gotSize := outBuf.Len(); gotSize != wantSize {
  1110. t.Fatalf("got %d, want %d", gotSize, int(wantSize))
  1111. }
  1112. })
  1113. t.Run("stdin", func(t *testing.T) {
  1114. if cibuild.On() {
  1115. t.Skip("Skipping for now; see https://github.com/tailscale/tailscale/issues/4051")
  1116. }
  1117. cmd := execSSH("cat")
  1118. var outBuf bytes.Buffer
  1119. cmd.Stdout = &outBuf
  1120. const str = "foo\nbar\n"
  1121. cmd.Stdin = strings.NewReader(str)
  1122. if err := cmd.Run(); err != nil {
  1123. t.Fatal(err)
  1124. }
  1125. if got := outBuf.String(); got != str {
  1126. t.Errorf("got %q; want %q", got, str)
  1127. }
  1128. })
  1129. }
  1130. func parseEnv(out []byte) map[string]string {
  1131. e := map[string]string{}
  1132. for line := range lineiter.Bytes(out) {
  1133. if i := bytes.IndexByte(line, '='); i != -1 {
  1134. e[string(line[:i])] = string(line[i+1:])
  1135. }
  1136. }
  1137. return e
  1138. }
  1139. func TestAcceptEnvPair(t *testing.T) {
  1140. tests := []struct {
  1141. in string
  1142. want bool
  1143. }{
  1144. {"TERM=x", true},
  1145. {"term=x", false},
  1146. {"TERM", false},
  1147. {"LC_FOO=x", true},
  1148. {"LD_PRELOAD=naah", false},
  1149. {"TERM=screen-256color", true},
  1150. }
  1151. for _, tt := range tests {
  1152. if got := acceptEnvPair(tt.in); got != tt.want {
  1153. t.Errorf("for %q, got %v; want %v", tt.in, got, tt.want)
  1154. }
  1155. }
  1156. }
  1157. func TestPathFromPAMEnvLine(t *testing.T) {
  1158. u := &user.User{Username: "foo", HomeDir: "/Homes/Foo"}
  1159. tests := []struct {
  1160. line string
  1161. u *user.User
  1162. want string
  1163. }{
  1164. {"", u, ""},
  1165. {`PATH DEFAULT="/run/wrappers/bin:@{HOME}/.nix-profile/bin:/etc/profiles/per-user/@{PAM_USER}/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin"`,
  1166. u, "/run/wrappers/bin:/Homes/Foo/.nix-profile/bin:/etc/profiles/per-user/foo/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin"},
  1167. {`PATH DEFAULT="@{SOMETHING_ELSE}:nope:@{HOME}"`,
  1168. u, ""},
  1169. }
  1170. for i, tt := range tests {
  1171. got := pathFromPAMEnvLine([]byte(tt.line), tt.u)
  1172. if got != tt.want {
  1173. t.Errorf("%d. got %q; want %q", i, got, tt.want)
  1174. }
  1175. }
  1176. }
  1177. func TestExpandDefaultPathTmpl(t *testing.T) {
  1178. u := &user.User{Username: "foo", HomeDir: "/Homes/Foo"}
  1179. tests := []struct {
  1180. t string
  1181. u *user.User
  1182. want string
  1183. }{
  1184. {"", u, ""},
  1185. {`/run/wrappers/bin:@{HOME}/.nix-profile/bin:/etc/profiles/per-user/@{PAM_USER}/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin`,
  1186. u, "/run/wrappers/bin:/Homes/Foo/.nix-profile/bin:/etc/profiles/per-user/foo/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin"},
  1187. {`@{SOMETHING_ELSE}:nope:@{HOME}`, u, ""},
  1188. }
  1189. for i, tt := range tests {
  1190. got := expandDefaultPathTmpl(tt.t, tt.u)
  1191. if got != tt.want {
  1192. t.Errorf("%d. got %q; want %q", i, got, tt.want)
  1193. }
  1194. }
  1195. }
  1196. func TestPathFromPAMEnvLineOnNixOS(t *testing.T) {
  1197. if runtime.GOOS != "linux" {
  1198. t.Skip("skipping on non-linux")
  1199. }
  1200. if distro.Get() != distro.NixOS {
  1201. t.Skip("skipping on non-NixOS")
  1202. }
  1203. u, err := user.Current()
  1204. if err != nil {
  1205. t.Fatal(err)
  1206. }
  1207. got := defaultPathForUserOnNixOS(u)
  1208. if got == "" {
  1209. x, err := os.ReadFile("/etc/pam/environment")
  1210. t.Fatalf("no result. file was: err=%v, contents=%s", err, x)
  1211. }
  1212. t.Logf("success; got=%q", got)
  1213. }
  1214. func TestStdOsUserUserAssumptions(t *testing.T) {
  1215. v := reflect.TypeFor[user.User]()
  1216. if got, want := v.NumField(), 5; got != want {
  1217. t.Errorf("os/user.User has %v fields; this package assumes %v", got, want)
  1218. }
  1219. }
  1220. func mockRecordingServer(t *testing.T, handleRecord http.HandlerFunc) *httptest.Server {
  1221. t.Helper()
  1222. mux := http.NewServeMux()
  1223. mux.HandleFunc("POST /record", func(http.ResponseWriter, *http.Request) {
  1224. t.Errorf("v1 recording endpoint called")
  1225. })
  1226. mux.HandleFunc("HEAD /v2/record", func(http.ResponseWriter, *http.Request) {})
  1227. mux.HandleFunc("POST /v2/record", handleRecord)
  1228. h2s := &http2.Server{}
  1229. srv := httptest.NewUnstartedServer(h2c.NewHandler(mux, h2s))
  1230. if err := http2.ConfigureServer(srv.Config, h2s); err != nil {
  1231. t.Errorf("configuring HTTP/2 support in recording server: %v", err)
  1232. }
  1233. srv.Start()
  1234. t.Cleanup(srv.Close)
  1235. return srv
  1236. }