tailssh_test.go 34 KB

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