mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 16:22:03 +01:00 
			
		
		
		
	This updates all source files to use a new standard header for copyright and license declaration. Notably, copyright no longer includes a date, and we now use the standard SPDX-License-Identifier header. This commit was done almost entirely mechanically with perl, and then some minimal manual fixes. Updates #6865 Signed-off-by: Will Norris <will@tailscale.com>
		
			
				
	
	
		
			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
 | |
| }
 |