tailssh_test.go 34 KB

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