firewall.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. package nebula
  2. import (
  3. "context"
  4. "errors"
  5. "log/slog"
  6. "net/netip"
  7. "strconv"
  8. "sync"
  9. "time"
  10. "github.com/gaissmai/bart"
  11. "github.com/rcrowley/go-metrics"
  12. "github.com/slackhq/nebula/cert"
  13. "github.com/slackhq/nebula/config"
  14. "github.com/slackhq/nebula/firewall"
  15. "github.com/slackhq/nebula/iputil"
  16. )
  17. type conn struct {
  18. Expires time.Time // Time when this conntrack entry will expire
  19. // record why the original connection passed the firewall, so we can re-validate
  20. // after ruleset changes. Note, rulesVersion is a uint16 so that these two
  21. // fields pack for free after the uint32 above
  22. incoming bool
  23. rulesVersion uint16
  24. }
  25. // TODO: need conntrack max tracked connections handling
  26. type Firewall struct {
  27. Conntrack *FirewallConntrack
  28. rules firewall.Rules
  29. InboundSendReject bool
  30. OutboundSendReject bool
  31. //TODO: we should have many more options for TCP, an option for ICMP, and mimic the kernel a bit better
  32. // https://www.kernel.org/doc/Documentation/networking/nf_conntrack-sysctl.txt
  33. TCPTimeout time.Duration //linux: 5 days max
  34. UDPTimeout time.Duration //linux: 180s max
  35. DefaultTimeout time.Duration //linux: 600s
  36. // routableNetworks describes the vpn addresses as well as any unsafe networks issued to us in the certificate.
  37. // The vpn addresses are a full bit match while the unsafe networks only match the prefix
  38. routableNetworks *bart.Lite
  39. // assignedNetworks is a list of vpn networks assigned to us in the certificate.
  40. assignedNetworks []netip.Prefix
  41. // unsafeNetworks is the list of unsafe networks issued to us in the certificate
  42. unsafeNetworks []netip.Prefix
  43. rulesVersion uint16
  44. incomingMetrics firewallMetrics
  45. outgoingMetrics firewallMetrics
  46. l *slog.Logger
  47. }
  48. type firewallMetrics struct {
  49. droppedLocalAddr metrics.Counter
  50. droppedRemoteAddr metrics.Counter
  51. droppedNoRule metrics.Counter
  52. }
  53. type FirewallConntrack struct {
  54. sync.Mutex
  55. Conns map[firewall.Packet]*conn
  56. TimerWheel *TimerWheel[firewall.Packet]
  57. }
  58. // NewFirewall creates a new Firewall object. A TimerWheel is created for you from the provided timeouts.
  59. // The certificate provided should be the highest version loaded in memory.
  60. func NewFirewall(l *slog.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.Duration, c cert.Certificate) *Firewall {
  61. //TODO: error on 0 duration
  62. var tmin, tmax time.Duration
  63. if tcpTimeout < UDPTimeout {
  64. tmin = tcpTimeout
  65. tmax = UDPTimeout
  66. } else {
  67. tmin = UDPTimeout
  68. tmax = tcpTimeout
  69. }
  70. if defaultTimeout < tmin {
  71. tmin = defaultTimeout
  72. } else if defaultTimeout > tmax {
  73. tmax = defaultTimeout
  74. }
  75. routableNetworks := new(bart.Lite)
  76. var assignedNetworks []netip.Prefix
  77. for _, network := range c.Networks() {
  78. nprefix := netip.PrefixFrom(network.Addr(), network.Addr().BitLen())
  79. routableNetworks.Insert(nprefix)
  80. assignedNetworks = append(assignedNetworks, network)
  81. }
  82. unsafeNetworks := c.UnsafeNetworks()
  83. for _, n := range unsafeNetworks {
  84. routableNetworks.Insert(n)
  85. }
  86. return &Firewall{
  87. Conntrack: &FirewallConntrack{
  88. Conns: make(map[firewall.Packet]*conn),
  89. TimerWheel: NewTimerWheel[firewall.Packet](tmin, tmax),
  90. },
  91. // Nothing is allowed until there are rules
  92. rules: firewall.NewRulesBuilder(l).Build(assignedNetworks, unsafeNetworks),
  93. TCPTimeout: tcpTimeout,
  94. UDPTimeout: UDPTimeout,
  95. DefaultTimeout: defaultTimeout,
  96. routableNetworks: routableNetworks,
  97. assignedNetworks: assignedNetworks,
  98. unsafeNetworks: unsafeNetworks,
  99. l: l,
  100. incomingMetrics: firewallMetrics{
  101. droppedLocalAddr: metrics.GetOrRegisterCounter("firewall.incoming.dropped.local_addr", nil),
  102. droppedRemoteAddr: metrics.GetOrRegisterCounter("firewall.incoming.dropped.remote_addr", nil),
  103. droppedNoRule: metrics.GetOrRegisterCounter("firewall.incoming.dropped.no_rule", nil),
  104. },
  105. outgoingMetrics: firewallMetrics{
  106. droppedLocalAddr: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.local_addr", nil),
  107. droppedRemoteAddr: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.remote_addr", nil),
  108. droppedNoRule: metrics.GetOrRegisterCounter("firewall.outgoing.dropped.no_rule", nil),
  109. },
  110. }
  111. }
  112. func NewFirewallFromConfig(l *slog.Logger, cs *CertState, c *config.C) (*Firewall, error) {
  113. certificate := cs.getCertificate(cert.Version2)
  114. if certificate == nil {
  115. certificate = cs.getCertificate(cert.Version1)
  116. }
  117. if certificate == nil {
  118. panic("No certificate available to reconfigure the firewall")
  119. }
  120. fw := NewFirewall(
  121. l,
  122. c.GetDuration("firewall.conntrack.tcp_timeout", time.Minute*12),
  123. c.GetDuration("firewall.conntrack.udp_timeout", time.Minute*3),
  124. c.GetDuration("firewall.conntrack.default_timeout", time.Minute*10),
  125. certificate,
  126. //TODO: max_connections
  127. )
  128. inboundAction := c.GetString("firewall.inbound_action", "drop")
  129. switch inboundAction {
  130. case "reject":
  131. fw.InboundSendReject = true
  132. case "drop":
  133. fw.InboundSendReject = false
  134. default:
  135. l.Warn("invalid firewall.inbound_action, defaulting to `drop`", "action", inboundAction)
  136. fw.InboundSendReject = false
  137. }
  138. outboundAction := c.GetString("firewall.outbound_action", "drop")
  139. switch outboundAction {
  140. case "reject":
  141. fw.OutboundSendReject = true
  142. case "drop":
  143. fw.OutboundSendReject = false
  144. default:
  145. l.Warn("invalid firewall.outbound_action, defaulting to `drop`", "action", outboundAction)
  146. fw.OutboundSendReject = false
  147. }
  148. rules, err := firewall.RulesFromConfig(l, c, fw.assignedNetworks, fw.unsafeNetworks)
  149. if err != nil {
  150. return nil, err
  151. }
  152. fw.rules = rules
  153. return fw, nil
  154. }
  155. // GetRuleHash returns a hash representation of all inbound and outbound rules
  156. func (f *Firewall) GetRuleHash() string {
  157. return f.rules.Hash
  158. }
  159. // GetRuleHashFNV returns a uint32 FNV-1 hash representation the rules, for use as a metric value
  160. func (f *Firewall) GetRuleHashFNV() uint32 {
  161. return f.rules.HashFNV
  162. }
  163. // GetRuleHashes returns both the sha256 and FNV-1 hashes, suitable for logging
  164. func (f *Firewall) GetRuleHashes() string {
  165. return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10)
  166. }
  167. var ErrUnknownNetworkType = errors.New("unknown network type")
  168. var ErrPeerRejected = errors.New("remote address is not within a network that we handle")
  169. var ErrInvalidRemoteIP = errors.New("remote address is not in remote certificate networks")
  170. var ErrInvalidLocalIP = errors.New("local address is not in list of handled local addresses")
  171. var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
  172. // Drop returns an error if the packet should be dropped, explaining why. It
  173. // returns nil if the packet should not be dropped.
  174. func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error {
  175. // Make sure remote address matches nebula certificate, and determine how to treat it
  176. if h.networks == nil {
  177. // Simple case: Certificate has one address and no unsafe networks
  178. if h.vpnAddrs[0] != fp.RemoteAddr {
  179. f.metrics(incoming).droppedRemoteAddr.Inc(1)
  180. return ErrInvalidRemoteIP
  181. }
  182. } else {
  183. nwType, ok := h.networks.Lookup(fp.RemoteAddr)
  184. if !ok {
  185. f.metrics(incoming).droppedRemoteAddr.Inc(1)
  186. return ErrInvalidRemoteIP
  187. }
  188. switch nwType {
  189. case NetworkTypeVPN:
  190. break // nothing special
  191. case NetworkTypeVPNPeer:
  192. f.metrics(incoming).droppedRemoteAddr.Inc(1)
  193. return ErrPeerRejected // reject for now, one day this may have different FW rules
  194. case NetworkTypeUnsafe:
  195. break // nothing special, one day this may have different FW rules
  196. default:
  197. f.metrics(incoming).droppedRemoteAddr.Inc(1)
  198. return ErrUnknownNetworkType //should never happen
  199. }
  200. }
  201. // Make sure we are supposed to be handling this local ip address
  202. if !f.routableNetworks.Contains(fp.LocalAddr) {
  203. f.metrics(incoming).droppedLocalAddr.Inc(1)
  204. return ErrInvalidLocalIP
  205. }
  206. // Check if we spoke to this tuple, if we did then allow this packet
  207. if f.inConns(&fp, h, caPool, localCache) {
  208. return nil
  209. }
  210. if !f.rules.Match(&fp, incoming, h.ConnectionState.peerCert, caPool) {
  211. f.metrics(incoming).droppedNoRule.Inc(1)
  212. return ErrNoMatchingRule
  213. }
  214. // We always want to conntrack since it is a faster operation
  215. f.addConn(&fp, incoming)
  216. return nil
  217. }
  218. func (f *Firewall) metrics(incoming bool) firewallMetrics {
  219. if incoming {
  220. return f.incomingMetrics
  221. } else {
  222. return f.outgoingMetrics
  223. }
  224. }
  225. // Destroy cleans up any known cyclical references so the object can be freed by GC. This should be called if a new
  226. // firewall object is created
  227. func (f *Firewall) Destroy() {
  228. //TODO: clean references if/when needed
  229. }
  230. func (f *Firewall) EmitStats() {
  231. conntrack := f.Conntrack
  232. conntrack.Lock()
  233. conntrackCount := len(conntrack.Conns)
  234. conntrack.Unlock()
  235. metrics.GetOrRegisterGauge("firewall.conntrack.count", nil).Update(int64(conntrackCount))
  236. metrics.GetOrRegisterGauge("firewall.rules.version", nil).Update(int64(f.rulesVersion))
  237. metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV()))
  238. }
  239. func (f *Firewall) inConns(fp *firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) bool {
  240. if localCache != nil {
  241. if _, ok := localCache[*fp]; ok {
  242. return true
  243. }
  244. }
  245. conntrack := f.Conntrack
  246. conntrack.Lock()
  247. // Purge every time we test
  248. ep, has := conntrack.TimerWheel.Purge()
  249. if has {
  250. f.evict(ep)
  251. }
  252. c, ok := conntrack.Conns[*fp]
  253. if !ok {
  254. conntrack.Unlock()
  255. return false
  256. }
  257. if c.rulesVersion != f.rulesVersion {
  258. // This conntrack entry was for an older rule set, validate
  259. // it still passes with the current rule set
  260. if !f.rules.Match(fp, c.incoming, h.ConnectionState.peerCert, caPool) {
  261. if f.l.Enabled(context.Background(), slog.LevelDebug) {
  262. h.logger(f.l).Debug("dropping old conntrack entry, does not match new ruleset",
  263. "fwPacket", *fp,
  264. "incoming", c.incoming,
  265. "rulesVersion", f.rulesVersion,
  266. "oldRulesVersion", c.rulesVersion,
  267. )
  268. }
  269. delete(conntrack.Conns, *fp)
  270. conntrack.Unlock()
  271. return false
  272. }
  273. if f.l.Enabled(context.Background(), slog.LevelDebug) {
  274. h.logger(f.l).Debug("keeping old conntrack entry, does match new ruleset",
  275. "fwPacket", *fp,
  276. "incoming", c.incoming,
  277. "rulesVersion", f.rulesVersion,
  278. "oldRulesVersion", c.rulesVersion,
  279. )
  280. }
  281. c.rulesVersion = f.rulesVersion
  282. }
  283. c.Expires = time.Now().Add(f.conntrackTimeout(fp.Protocol))
  284. conntrack.Unlock()
  285. if localCache != nil {
  286. localCache[*fp] = struct{}{}
  287. }
  288. return true
  289. }
  290. // conntrackTimeout returns how long a conntrack entry for proto lives without traffic.
  291. // Connection oriented protocols share the tcp timeout, datagram protocols share the udp timeout.
  292. func (f *Firewall) conntrackTimeout(proto uint8) time.Duration {
  293. switch proto {
  294. case iputil.IPProtocolTCP, iputil.IPProtocolSCTP, iputil.IPProtocolDCCP:
  295. return f.TCPTimeout
  296. case iputil.IPProtocolUDP, iputil.IPProtocolUDPLite:
  297. return f.UDPTimeout
  298. default:
  299. return f.DefaultTimeout
  300. }
  301. }
  302. func (f *Firewall) addConn(fp *firewall.Packet, incoming bool) {
  303. timeout := f.conntrackTimeout(fp.Protocol)
  304. c := &conn{}
  305. conntrack := f.Conntrack
  306. conntrack.Lock()
  307. if _, ok := conntrack.Conns[*fp]; !ok {
  308. conntrack.TimerWheel.Advance(time.Now())
  309. conntrack.TimerWheel.Add(*fp, timeout)
  310. }
  311. // Record which rulesVersion allowed this connection, so we can retest after
  312. // firewall reload
  313. c.incoming = incoming
  314. c.rulesVersion = f.rulesVersion
  315. c.Expires = time.Now().Add(timeout)
  316. conntrack.Conns[*fp] = c
  317. conntrack.Unlock()
  318. }
  319. // Evict checks if a conntrack entry has expired, if so it is removed, if not it is re-added to the wheel
  320. // Caller must own the connMutex lock!
  321. func (f *Firewall) evict(p firewall.Packet) {
  322. // Are we still tracking this conn?
  323. conntrack := f.Conntrack
  324. t, ok := conntrack.Conns[p]
  325. if !ok {
  326. return
  327. }
  328. newT := t.Expires.Sub(time.Now())
  329. // Timeout is in the future, re-add the timer
  330. if newT > 0 {
  331. conntrack.TimerWheel.Advance(time.Now())
  332. conntrack.TimerWheel.Add(p, newT)
  333. return
  334. }
  335. // This conn is done
  336. delete(conntrack.Conns, p)
  337. }