tailssh_test.go 36 KB

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