replay_filter.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. /*
  2. * Copyright (c) 2014, Yawning Angel <yawning at schwanenlied dot me>
  3. * All rights reserved.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. *
  8. * * Redistributions of source code must retain the above copyright notice,
  9. * this list of conditions and the following disclaimer.
  10. *
  11. * * Redistributions in binary form must reproduce the above copyright notice,
  12. * this list of conditions and the following disclaimer in the documentation
  13. * and/or other materials provided with the distribution.
  14. *
  15. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  16. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
  19. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  20. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  21. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  22. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  23. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  24. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  25. * POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. // Package replayfilter implements a generic replay detection filter with a
  28. // caller specifiable time-to-live. It only detects if a given byte sequence
  29. // has been seen before based on the SipHash-2-4 digest of the sequence.
  30. // Collisions are treated as positive matches, though the probability of this
  31. // happening is negligible.
  32. package replayfilter // import "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird/common/replayfilter"
  33. import (
  34. "container/list"
  35. "encoding/binary"
  36. "sync"
  37. "time"
  38. "github.com/dchest/siphash"
  39. "gitlab.torproject.org/tpo/anti-censorship/pluggable-transports/lyrebird/common/csrand"
  40. )
  41. // maxFilterSize is the maximum capacity of a replay filter. This value is
  42. // more as a safeguard to prevent runaway filter growth, and is sized to be
  43. // serveral orders of magnitude greater than the number of connections a busy
  44. // bridge sees in one day, so in practice should never be reached.
  45. const maxFilterSize = 100 * 1024
  46. type entry struct {
  47. digest uint64
  48. firstSeen time.Time
  49. element *list.Element
  50. }
  51. // ReplayFilter is a simple filter designed only to detect if a given byte
  52. // sequence has been seen before.
  53. type ReplayFilter struct {
  54. sync.Mutex
  55. filter map[uint64]*entry
  56. fifo *list.List
  57. key [2]uint64
  58. ttl time.Duration
  59. }
  60. // New creates a new ReplayFilter instance.
  61. func New(ttl time.Duration) (filter *ReplayFilter, err error) {
  62. // Initialize the SipHash-2-4 instance with a random key.
  63. var key [16]byte
  64. if err = csrand.Bytes(key[:]); err != nil {
  65. return
  66. }
  67. filter = new(ReplayFilter)
  68. filter.filter = make(map[uint64]*entry)
  69. filter.fifo = list.New()
  70. filter.key[0] = binary.BigEndian.Uint64(key[0:8])
  71. filter.key[1] = binary.BigEndian.Uint64(key[8:16])
  72. filter.ttl = ttl
  73. return
  74. }
  75. // TestAndSet queries the filter for a given byte sequence, inserts the
  76. // sequence, and returns if it was present before the insertion operation.
  77. func (f *ReplayFilter) TestAndSet(now time.Time, buf []byte) bool {
  78. digest := siphash.Hash(f.key[0], f.key[1], buf)
  79. f.Lock()
  80. defer f.Unlock()
  81. f.compactFilter(now)
  82. if e := f.filter[digest]; e != nil {
  83. // Hit. Just return.
  84. return true
  85. }
  86. // Miss. Add a new entry.
  87. e := new(entry)
  88. e.digest = digest
  89. e.firstSeen = now
  90. e.element = f.fifo.PushBack(e)
  91. f.filter[digest] = e
  92. return false
  93. }
  94. func (f *ReplayFilter) compactFilter(now time.Time) {
  95. e := f.fifo.Front()
  96. for e != nil {
  97. ent, _ := e.Value.(*entry)
  98. // If the filter is not full, only purge entries that exceed the TTL,
  99. // otherwise purge at least one entry, then revert to TTL based
  100. // compaction.
  101. if f.fifo.Len() < maxFilterSize && f.ttl > 0 {
  102. deltaT := now.Sub(ent.firstSeen)
  103. if deltaT < 0 {
  104. // Aeeeeeee, the system time jumped backwards, potentially by
  105. // a lot. This will eventually self-correct, but "eventually"
  106. // could be a long time. As much as this sucks, jettison the
  107. // entire filter.
  108. f.reset()
  109. return
  110. } else if deltaT < f.ttl {
  111. return
  112. }
  113. }
  114. // Remove the eldest entry.
  115. eNext := e.Next()
  116. delete(f.filter, ent.digest)
  117. f.fifo.Remove(ent.element)
  118. ent.element = nil
  119. e = eNext
  120. }
  121. }
  122. func (f *ReplayFilter) reset() {
  123. f.filter = make(map[uint64]*entry)
  124. f.fifo = list.New()
  125. }