157 lines
4.7 KiB
Go
157 lines
4.7 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package natlab
|
|
|
|
import (
|
|
"fmt"
|
|
"net/netip"
|
|
"sync"
|
|
"time"
|
|
|
|
"tailscale.com/util/mak"
|
|
)
|
|
|
|
// FirewallType is the type of filtering a stateful firewall
|
|
// does. Values express different modes defined by RFC 4787.
|
|
type FirewallType int
|
|
|
|
const (
|
|
// AddressAndPortDependentFirewall specifies a destination
|
|
// address-and-port dependent firewall. Outbound traffic to an
|
|
// ip:port authorizes traffic from that ip:port exactly, and
|
|
// nothing else.
|
|
AddressAndPortDependentFirewall FirewallType = iota
|
|
// AddressDependentFirewall specifies a destination address
|
|
// dependent firewall. Once outbound traffic has been seen to an
|
|
// IP address, that IP address can talk back from any port.
|
|
AddressDependentFirewall
|
|
// EndpointIndependentFirewall specifies a destination endpoint
|
|
// independent firewall. Once outbound traffic has been seen from
|
|
// a source, anyone can talk back to that source.
|
|
EndpointIndependentFirewall
|
|
)
|
|
|
|
// fwKey is the lookup key for a firewall session. While it contains a
|
|
// 4-tuple ({src,dst} {ip,port}), some FirewallTypes will zero out
|
|
// some fields, so in practice the key is either a 2-tuple (src only),
|
|
// 3-tuple (src ip+port and dst ip) or 4-tuple (src+dst ip+port).
|
|
type fwKey struct {
|
|
src netip.AddrPort
|
|
dst netip.AddrPort
|
|
}
|
|
|
|
// key returns an fwKey for the given src and dst, trimmed according
|
|
// to the FirewallType. fwKeys are always constructed from the
|
|
// "outbound" point of view (i.e. src is the "trusted" side of the
|
|
// world), it's the caller's responsibility to swap src and dst in the
|
|
// call to key when processing packets inbound from the "untrusted"
|
|
// world.
|
|
func (s FirewallType) key(src, dst netip.AddrPort) fwKey {
|
|
k := fwKey{src: src}
|
|
switch s {
|
|
case EndpointIndependentFirewall:
|
|
case AddressDependentFirewall:
|
|
k.dst = netip.AddrPortFrom(dst.Addr(), k.dst.Port())
|
|
case AddressAndPortDependentFirewall:
|
|
k.dst = dst
|
|
default:
|
|
panic(fmt.Sprintf("unknown firewall selectivity %v", s))
|
|
}
|
|
return k
|
|
}
|
|
|
|
// DefaultSessionTimeout is the default timeout for a firewall
|
|
// session.
|
|
const DefaultSessionTimeout = 30 * time.Second
|
|
|
|
// Firewall is a simple stateful firewall that allows all outbound
|
|
// traffic and filters inbound traffic based on recently seen outbound
|
|
// traffic. Its HandlePacket method should be attached to a Machine to
|
|
// give it a stateful firewall.
|
|
type Firewall struct {
|
|
// SessionTimeout is the lifetime of idle sessions in the firewall
|
|
// state. Packets transiting from the TrustedInterface reset the
|
|
// session lifetime to SessionTimeout. If zero,
|
|
// DefaultSessionTimeout is used.
|
|
SessionTimeout time.Duration
|
|
// Type specifies how precisely return traffic must match
|
|
// previously seen outbound traffic to be allowed. Defaults to
|
|
// AddressAndPortDependentFirewall.
|
|
Type FirewallType
|
|
// TrustedInterface is an optional interface that is considered
|
|
// trusted in addition to PacketConns local to the Machine. All
|
|
// other interfaces can only respond to traffic from
|
|
// TrustedInterface or the local host.
|
|
TrustedInterface *Interface
|
|
// TimeNow is a function returning the current time. If nil,
|
|
// time.Now is used.
|
|
TimeNow func() time.Time
|
|
|
|
// TODO: refresh directionality: outbound-only, both
|
|
|
|
mu sync.Mutex
|
|
seen map[fwKey]time.Time // session -> deadline
|
|
}
|
|
|
|
func (f *Firewall) timeNow() time.Time {
|
|
if f.TimeNow != nil {
|
|
return f.TimeNow()
|
|
}
|
|
return time.Now()
|
|
}
|
|
|
|
// Reset drops all firewall state, forgetting all flows.
|
|
func (f *Firewall) Reset() {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.seen = nil
|
|
}
|
|
|
|
func (f *Firewall) HandleOut(p *Packet, oif *Interface) *Packet {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
k := f.Type.key(p.Src, p.Dst)
|
|
mak.Set(&f.seen, k, f.timeNow().Add(f.sessionTimeoutLocked()))
|
|
p.Trace("firewall out ok")
|
|
return p
|
|
}
|
|
|
|
func (f *Firewall) HandleIn(p *Packet, iif *Interface) *Packet {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
// reverse src and dst because the session table is from the POV
|
|
// of outbound packets.
|
|
k := f.Type.key(p.Dst, p.Src)
|
|
now := f.timeNow()
|
|
if now.After(f.seen[k]) {
|
|
p.Trace("firewall drop")
|
|
return nil
|
|
}
|
|
p.Trace("firewall in ok")
|
|
return p
|
|
}
|
|
|
|
func (f *Firewall) HandleForward(p *Packet, iif *Interface, oif *Interface) *Packet {
|
|
if iif == f.TrustedInterface {
|
|
// Treat just like a locally originated packet
|
|
return f.HandleOut(p, oif)
|
|
}
|
|
if oif != f.TrustedInterface {
|
|
// Not a possible return packet from our trusted interface, drop.
|
|
p.Trace("firewall drop, unexpected oif")
|
|
return nil
|
|
}
|
|
// Otherwise, a session must exist, same as HandleIn.
|
|
return f.HandleIn(p, iif)
|
|
}
|
|
|
|
func (f *Firewall) sessionTimeoutLocked() time.Duration {
|
|
if f.SessionTimeout == 0 {
|
|
return DefaultSessionTimeout
|
|
}
|
|
return f.SessionTimeout
|
|
}
|