auditlog_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package auditlog
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "sync"
  9. "testing"
  10. "time"
  11. qt "github.com/frankban/quicktest"
  12. "tailscale.com/ipn/store/mem"
  13. "tailscale.com/tailcfg"
  14. "tailscale.com/tstest"
  15. )
  16. // loggerForTest creates an auditLogger for you and cleans it up
  17. // (and ensures no goroutines are leaked) when the test is done.
  18. func loggerForTest(t *testing.T, opts Opts) *Logger {
  19. t.Helper()
  20. tstest.ResourceCheck(t)
  21. if opts.Logf == nil {
  22. opts.Logf = t.Logf
  23. }
  24. if opts.Store == nil {
  25. t.Fatalf("opts.Store must be set")
  26. }
  27. a := NewLogger(opts)
  28. t.Cleanup(func() {
  29. a.FlushAndStop(context.Background())
  30. })
  31. return a
  32. }
  33. func TestNonRetryableErrors(t *testing.T) {
  34. errorTests := []struct {
  35. desc string
  36. err error
  37. want bool
  38. }{
  39. {"DeadlineExceeded", context.DeadlineExceeded, false},
  40. {"Canceled", context.Canceled, false},
  41. {"Canceled wrapped", fmt.Errorf("%w: %w", context.Canceled, errors.New("ctx cancelled")), false},
  42. {"Random error", errors.New("random error"), false},
  43. }
  44. for _, tt := range errorTests {
  45. t.Run(tt.desc, func(t *testing.T) {
  46. if IsRetryableError(tt.err) != tt.want {
  47. t.Fatalf("retriable: got %v, want %v", !tt.want, tt.want)
  48. }
  49. })
  50. }
  51. }
  52. // TestEnqueueAndFlush enqueues n logs and flushes them.
  53. // We expect all logs to be flushed and for no
  54. // logs to remain in the store once FlushAndStop returns.
  55. func TestEnqueueAndFlush(t *testing.T) {
  56. c := qt.New(t)
  57. mockTransport := newMockTransport(nil)
  58. al := loggerForTest(t, Opts{
  59. RetryLimit: 200,
  60. Logf: t.Logf,
  61. Store: NewLogStore(&mem.Store{}),
  62. })
  63. c.Assert(al.SetProfileID("test"), qt.IsNil)
  64. c.Assert(al.Start(mockTransport), qt.IsNil)
  65. wantSent := 10
  66. for i := range wantSent {
  67. err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log %d", i))
  68. c.Assert(err, qt.IsNil)
  69. }
  70. al.FlushAndStop(context.Background())
  71. al.mu.Lock()
  72. defer al.mu.Unlock()
  73. gotStored, err := al.storedCountLocked()
  74. c.Assert(err, qt.IsNil)
  75. if wantStored := 0; gotStored != wantStored {
  76. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  77. }
  78. if gotSent := mockTransport.sentCount(); gotSent != wantSent {
  79. t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
  80. }
  81. }
  82. // TestEnqueueAndFlushWithFlushCancel calls FlushAndCancel with a cancelled
  83. // context. We expect nothing to be sent and all logs to be stored.
  84. func TestEnqueueAndFlushWithFlushCancel(t *testing.T) {
  85. c := qt.New(t)
  86. mockTransport := newMockTransport(&retriableError)
  87. al := loggerForTest(t, Opts{
  88. RetryLimit: 200,
  89. Logf: t.Logf,
  90. Store: NewLogStore(&mem.Store{}),
  91. })
  92. c.Assert(al.SetProfileID("test"), qt.IsNil)
  93. c.Assert(al.Start(mockTransport), qt.IsNil)
  94. for i := range 10 {
  95. err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log %d", i))
  96. c.Assert(err, qt.IsNil)
  97. }
  98. // Cancel the context before calling FlushAndStop - nothing should get sent.
  99. // This mimics a timeout before flush() has a chance to execute.
  100. ctx, cancel := context.WithCancel(context.Background())
  101. cancel()
  102. al.FlushAndStop(ctx)
  103. al.mu.Lock()
  104. defer al.mu.Unlock()
  105. gotStored, err := al.storedCountLocked()
  106. c.Assert(err, qt.IsNil)
  107. if wantStored := 10; gotStored != wantStored {
  108. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  109. }
  110. if gotSent, wantSent := mockTransport.sentCount(), 0; gotSent != wantSent {
  111. t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
  112. }
  113. }
  114. // TestDeduplicateAndSort tests that the most recent log is kept when deduplicating logs
  115. func TestDeduplicateAndSort(t *testing.T) {
  116. c := qt.New(t)
  117. al := loggerForTest(t, Opts{
  118. RetryLimit: 100,
  119. Logf: t.Logf,
  120. Store: NewLogStore(&mem.Store{}),
  121. })
  122. c.Assert(al.SetProfileID("test"), qt.IsNil)
  123. logs := []*transaction{
  124. {EventID: "1", Details: "log 1", TimeStamp: time.Now().Add(-time.Minute * 1), Retries: 1},
  125. }
  126. al.mu.Lock()
  127. defer al.mu.Unlock()
  128. al.appendToStoreLocked(logs)
  129. // Update the transaction and re-append it
  130. logs[0].Retries = 2
  131. al.appendToStoreLocked(logs)
  132. fromStore, err := al.store.load("test")
  133. c.Assert(err, qt.IsNil)
  134. // We should see only one transaction
  135. if wantStored, gotStored := len(logs), len(fromStore); gotStored != wantStored {
  136. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  137. }
  138. // We should see the latest transaction
  139. if wantRetryCount, gotRetryCount := 2, fromStore[0].Retries; gotRetryCount != wantRetryCount {
  140. t.Fatalf("reties: got %d, want %d", gotRetryCount, wantRetryCount)
  141. }
  142. }
  143. func TestChangeProfileId(t *testing.T) {
  144. c := qt.New(t)
  145. al := loggerForTest(t, Opts{
  146. RetryLimit: 100,
  147. Logf: t.Logf,
  148. Store: NewLogStore(&mem.Store{}),
  149. })
  150. c.Assert(al.SetProfileID("test"), qt.IsNil)
  151. // Calling SetProfileID with the same profile ID must not fail.
  152. c.Assert(al.SetProfileID("test"), qt.IsNil)
  153. // Changing a profile ID must fail.
  154. c.Assert(al.SetProfileID("test2"), qt.IsNotNil)
  155. }
  156. // TestSendOnRestore pushes a n logs to the persistent store, and ensures they
  157. // are sent as soon as Start is called then checks to ensure the sent logs no
  158. // longer exist in the store.
  159. func TestSendOnRestore(t *testing.T) {
  160. c := qt.New(t)
  161. mockTransport := newMockTransport(nil)
  162. al := loggerForTest(t, Opts{
  163. RetryLimit: 100,
  164. Logf: t.Logf,
  165. Store: NewLogStore(&mem.Store{}),
  166. })
  167. al.SetProfileID("test")
  168. wantTotal := 10
  169. for range 10 {
  170. al.Enqueue(tailcfg.AuditNodeDisconnect, "log")
  171. }
  172. c.Assert(al.Start(mockTransport), qt.IsNil)
  173. al.FlushAndStop(context.Background())
  174. al.mu.Lock()
  175. defer al.mu.Unlock()
  176. gotStored, err := al.storedCountLocked()
  177. c.Assert(err, qt.IsNil)
  178. if wantStored := 0; gotStored != wantStored {
  179. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  180. }
  181. if gotSent, wantSent := mockTransport.sentCount(), wantTotal; gotSent != wantSent {
  182. t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
  183. }
  184. }
  185. // TestFailureExhaustion enqueues n logs, with the transport in a failable state.
  186. // We then set it to a non-failing state, call FlushAndStop and expect all logs to be sent.
  187. func TestFailureExhaustion(t *testing.T) {
  188. c := qt.New(t)
  189. mockTransport := newMockTransport(&retriableError)
  190. al := loggerForTest(t, Opts{
  191. RetryLimit: 1,
  192. Logf: t.Logf,
  193. Store: NewLogStore(&mem.Store{}),
  194. })
  195. c.Assert(al.SetProfileID("test"), qt.IsNil)
  196. c.Assert(al.Start(mockTransport), qt.IsNil)
  197. for range 10 {
  198. err := al.Enqueue(tailcfg.AuditNodeDisconnect, "log")
  199. c.Assert(err, qt.IsNil)
  200. }
  201. al.FlushAndStop(context.Background())
  202. al.mu.Lock()
  203. defer al.mu.Unlock()
  204. gotStored, err := al.storedCountLocked()
  205. c.Assert(err, qt.IsNil)
  206. if wantStored := 0; gotStored != wantStored {
  207. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  208. }
  209. if gotSent, wantSent := mockTransport.sentCount(), 0; gotSent != wantSent {
  210. t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
  211. }
  212. }
  213. // TestEnqueueAndFailNoRetry enqueues a set of logs, all of which will fail and are not
  214. // retriable. We then call FlushAndStop and expect all to be unsent.
  215. func TestEnqueueAndFailNoRetry(t *testing.T) {
  216. c := qt.New(t)
  217. mockTransport := newMockTransport(&nonRetriableError)
  218. al := loggerForTest(t, Opts{
  219. RetryLimit: 100,
  220. Logf: t.Logf,
  221. Store: NewLogStore(&mem.Store{}),
  222. })
  223. c.Assert(al.SetProfileID("test"), qt.IsNil)
  224. c.Assert(al.Start(mockTransport), qt.IsNil)
  225. for i := range 10 {
  226. err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log %d", i))
  227. c.Assert(err, qt.IsNil)
  228. }
  229. al.FlushAndStop(context.Background())
  230. al.mu.Lock()
  231. defer al.mu.Unlock()
  232. gotStored, err := al.storedCountLocked()
  233. c.Assert(err, qt.IsNil)
  234. if wantStored := 0; gotStored != wantStored {
  235. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  236. }
  237. if gotSent, wantSent := mockTransport.sentCount(), 0; gotSent != wantSent {
  238. t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
  239. }
  240. }
  241. // TestEnqueueAndRetry enqueues a set of logs, all of which will fail and are retriable.
  242. // Mid-test, we set the transport to not-fail and expect the queue to flush properly
  243. // We set the backoff parameters to 0 seconds so retries are immediate.
  244. func TestEnqueueAndRetry(t *testing.T) {
  245. c := qt.New(t)
  246. mockTransport := newMockTransport(&retriableError)
  247. al := loggerForTest(t, Opts{
  248. RetryLimit: 100,
  249. Logf: t.Logf,
  250. Store: NewLogStore(&mem.Store{}),
  251. })
  252. al.backoffOpts = backoffOpts{
  253. min: 1 * time.Millisecond,
  254. max: 4 * time.Millisecond,
  255. multiplier: 2.0,
  256. }
  257. c.Assert(al.SetProfileID("test"), qt.IsNil)
  258. c.Assert(al.Start(mockTransport), qt.IsNil)
  259. err := al.Enqueue(tailcfg.AuditNodeDisconnect, fmt.Sprintf("log 1"))
  260. c.Assert(err, qt.IsNil)
  261. // This will wait for at least 2 retries
  262. gotRetried, wantRetried := mockTransport.waitForSendAttemptsToReach(3), true
  263. if gotRetried != wantRetried {
  264. t.Fatalf("retried: got %v, want %v", gotRetried, wantRetried)
  265. }
  266. mockTransport.setErrorCondition(nil)
  267. al.FlushAndStop(context.Background())
  268. al.mu.Lock()
  269. defer al.mu.Unlock()
  270. gotStored, err := al.storedCountLocked()
  271. c.Assert(err, qt.IsNil)
  272. if wantStored := 0; gotStored != wantStored {
  273. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  274. }
  275. if gotSent, wantSent := mockTransport.sentCount(), 1; gotSent != wantSent {
  276. t.Fatalf("sent: got %d, want %d", gotSent, wantSent)
  277. }
  278. }
  279. // TestEnqueueBeforeSetProfileID tests that logs enqueued before SetProfileId are not sent
  280. func TestEnqueueBeforeSetProfileID(t *testing.T) {
  281. c := qt.New(t)
  282. al := loggerForTest(t, Opts{
  283. RetryLimit: 100,
  284. Logf: t.Logf,
  285. Store: NewLogStore(&mem.Store{}),
  286. })
  287. err := al.Enqueue(tailcfg.AuditNodeDisconnect, "log")
  288. c.Assert(err, qt.IsNotNil)
  289. al.FlushAndStop(context.Background())
  290. al.mu.Lock()
  291. defer al.mu.Unlock()
  292. gotStored, err := al.storedCountLocked()
  293. c.Assert(err, qt.IsNotNil)
  294. if wantStored := 0; gotStored != wantStored {
  295. t.Fatalf("stored: got %d, want %d", gotStored, wantStored)
  296. }
  297. }
  298. // TestLogStoring tests that audit logs are persisted sorted by timestamp, oldest to newest
  299. func TestLogSorting(t *testing.T) {
  300. c := qt.New(t)
  301. mockStore := NewLogStore(&mem.Store{})
  302. logs := []*transaction{
  303. {EventID: "1", Details: "log 3", TimeStamp: time.Now().Add(-time.Minute * 1)},
  304. {EventID: "1", Details: "log 3", TimeStamp: time.Now().Add(-time.Minute * 2)},
  305. {EventID: "2", Details: "log 2", TimeStamp: time.Now().Add(-time.Minute * 3)},
  306. {EventID: "3", Details: "log 1", TimeStamp: time.Now().Add(-time.Minute * 4)},
  307. }
  308. wantLogs := []transaction{
  309. {Details: "log 1"},
  310. {Details: "log 2"},
  311. {Details: "log 3"},
  312. }
  313. mockStore.save("test", logs)
  314. gotLogs, err := mockStore.load("test")
  315. c.Assert(err, qt.IsNil)
  316. gotLogs = deduplicateAndSort(gotLogs)
  317. for i := range gotLogs {
  318. if want, got := wantLogs[i].Details, gotLogs[i].Details; want != got {
  319. t.Fatalf("Details: got %v, want %v", got, want)
  320. }
  321. }
  322. }
  323. // mock implementations for testing
  324. // newMockTransport returns a mock transport for testing
  325. // If err is no nil, SendAuditLog will return this error if the send is attempted
  326. // before the context is cancelled.
  327. func newMockTransport(err error) *mockAuditLogTransport {
  328. return &mockAuditLogTransport{
  329. err: err,
  330. attempts: make(chan int, 1),
  331. }
  332. }
  333. type mockAuditLogTransport struct {
  334. attempts chan int // channel to notify of send attempts
  335. mu sync.Mutex
  336. sendAttmpts int // number of attempts to send logs
  337. sendCount int // number of logs sent by the transport
  338. err error // error to return when sending logs
  339. }
  340. // waitForSendAttemptsToReach blocks until the number of send attempts reaches n
  341. // This should be use only in tests where the transport is expected to retry sending logs
  342. func (t *mockAuditLogTransport) waitForSendAttemptsToReach(n int) bool {
  343. for attempts := range t.attempts {
  344. if attempts >= n {
  345. return true
  346. }
  347. }
  348. return false
  349. }
  350. func (t *mockAuditLogTransport) setErrorCondition(err error) {
  351. t.mu.Lock()
  352. defer t.mu.Unlock()
  353. t.err = err
  354. }
  355. func (t *mockAuditLogTransport) sentCount() int {
  356. t.mu.Lock()
  357. defer t.mu.Unlock()
  358. return t.sendCount
  359. }
  360. func (t *mockAuditLogTransport) SendAuditLog(ctx context.Context, _ tailcfg.AuditLogRequest) (err error) {
  361. t.mu.Lock()
  362. t.sendAttmpts += 1
  363. defer func() {
  364. a := t.sendAttmpts
  365. t.mu.Unlock()
  366. select {
  367. case t.attempts <- a:
  368. default:
  369. }
  370. }()
  371. select {
  372. case <-ctx.Done():
  373. return ctx.Err()
  374. default:
  375. }
  376. if t.err != nil {
  377. return t.err
  378. }
  379. t.sendCount += 1
  380. return nil
  381. }
  382. var (
  383. retriableError = mockError{errors.New("retriable error")}
  384. nonRetriableError = mockError{errors.New("permanent failure error")}
  385. )
  386. type mockError struct {
  387. error
  388. }
  389. func (e mockError) Retryable() bool {
  390. return e == retriableError
  391. }