mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-18 12:32:13 +00:00
tstest/natlab: support different firewall selectivities.
Signed-off-by: David Anderson <danderson@tailscale.com>
This commit is contained in:
parent
c1d9e41bef
commit
39ecb37fd6
@ -5,35 +5,93 @@
|
|||||||
package natlab
|
package natlab
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
)
|
)
|
||||||
|
|
||||||
type session struct {
|
// 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 netaddr.IPPort
|
src netaddr.IPPort
|
||||||
dst netaddr.IPPort
|
dst netaddr.IPPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 netaddr.IPPort) fwKey {
|
||||||
|
k := fwKey{src: src}
|
||||||
|
switch s {
|
||||||
|
case EndpointIndependentFirewall:
|
||||||
|
case AddressDependentFirewall:
|
||||||
|
k.dst.IP = dst.IP
|
||||||
|
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 {
|
type Firewall struct {
|
||||||
// TrustedInterface is the interface that's allowed to send
|
|
||||||
// anywhere. All other interfaces can only respond to traffic from
|
|
||||||
// TrustedInterface.
|
|
||||||
TrustedInterface *Interface
|
|
||||||
// SessionTimeout is the lifetime of idle sessions in the firewall
|
// SessionTimeout is the lifetime of idle sessions in the firewall
|
||||||
// state. Packets transiting from the TrustedInterface reset the
|
// state. Packets transiting from the TrustedInterface reset the
|
||||||
// session lifetime to SessionTimeout.
|
// session lifetime to SessionTimeout. If zero,
|
||||||
|
// DefaultSessionTimeout is used.
|
||||||
SessionTimeout time.Duration
|
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,
|
// TimeNow is a function returning the current time. If nil,
|
||||||
// time.Now is used.
|
// time.Now is used.
|
||||||
TimeNow func() time.Time
|
TimeNow func() time.Time
|
||||||
|
|
||||||
// TODO: tuple-ness pickiness: EIF, ADF, APDF
|
|
||||||
// TODO: refresh directionality: outbound-only, both
|
// TODO: refresh directionality: outbound-only, both
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
seen map[session]time.Time // session -> deadline
|
seen map[fwKey]time.Time // session -> deadline
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Firewall) timeNow() time.Time {
|
func (f *Firewall) timeNow() time.Time {
|
||||||
@ -43,33 +101,28 @@ func (f *Firewall) timeNow() time.Time {
|
|||||||
return time.Now()
|
return time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandlePacket implements the PacketHandler type.
|
||||||
func (f *Firewall) HandlePacket(p *Packet, inIf *Interface) PacketVerdict {
|
func (f *Firewall) HandlePacket(p *Packet, inIf *Interface) PacketVerdict {
|
||||||
f.mu.Lock()
|
f.mu.Lock()
|
||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
if f.seen == nil {
|
if f.seen == nil {
|
||||||
f.seen = map[session]time.Time{}
|
f.seen = map[fwKey]time.Time{}
|
||||||
}
|
}
|
||||||
if f.SessionTimeout == 0 {
|
if f.SessionTimeout == 0 {
|
||||||
f.SessionTimeout = 30 * time.Second
|
f.SessionTimeout = 30 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
if inIf == f.TrustedInterface || inIf == nil {
|
if inIf == f.TrustedInterface || inIf == nil {
|
||||||
sess := session{
|
k := f.Type.key(p.Src, p.Dst)
|
||||||
src: p.Src,
|
f.seen[k] = f.timeNow().Add(f.SessionTimeout)
|
||||||
dst: p.Dst,
|
|
||||||
}
|
|
||||||
f.seen[sess] = f.timeNow().Add(f.SessionTimeout)
|
|
||||||
p.Trace("firewall out ok")
|
p.Trace("firewall out ok")
|
||||||
return Continue
|
return Continue
|
||||||
} else {
|
} else {
|
||||||
// reverse src and dst because the session table is from the
|
// reverse src and dst because the session table is from the
|
||||||
// POV of outbound packets.
|
// POV of outbound packets.
|
||||||
sess := session{
|
k := f.Type.key(p.Dst, p.Src)
|
||||||
src: p.Dst,
|
|
||||||
dst: p.Src,
|
|
||||||
}
|
|
||||||
now := f.timeNow()
|
now := f.timeNow()
|
||||||
if now.After(f.seen[sess]) {
|
if now.After(f.seen[k]) {
|
||||||
p.Trace("firewall drop")
|
p.Trace("firewall drop")
|
||||||
return Drop
|
return Drop
|
||||||
}
|
}
|
||||||
|
@ -224,8 +224,6 @@ func TestPacketHandler(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestFirewall(t *testing.T) {
|
func TestFirewall(t *testing.T) {
|
||||||
clock := &tstest.Clock{}
|
|
||||||
|
|
||||||
wan := NewInternet()
|
wan := NewInternet()
|
||||||
lan := &Network{
|
lan := &Network{
|
||||||
Name: "lan",
|
Name: "lan",
|
||||||
@ -235,28 +233,84 @@ func TestFirewall(t *testing.T) {
|
|||||||
trust := m.Attach("trust", lan)
|
trust := m.Attach("trust", lan)
|
||||||
untrust := m.Attach("untrust", wan)
|
untrust := m.Attach("untrust", wan)
|
||||||
|
|
||||||
f := &Firewall{
|
|
||||||
TrustedInterface: trust,
|
|
||||||
SessionTimeout: 30 * time.Second,
|
|
||||||
TimeNow: clock.Now,
|
|
||||||
}
|
|
||||||
|
|
||||||
client := ipp("192.168.0.2:1234")
|
client := ipp("192.168.0.2:1234")
|
||||||
serverA := ipp("2.2.2.2:5678")
|
serverA := ipp("2.2.2.2:5678")
|
||||||
serverB := ipp("7.7.7.7:9012")
|
serverB1 := ipp("7.7.7.7:9012")
|
||||||
tests := []struct {
|
serverB2 := ipp("7.7.7.7:3456")
|
||||||
iface *Interface
|
|
||||||
src, dst netaddr.IPPort
|
|
||||||
want PacketVerdict
|
|
||||||
}{
|
|
||||||
{trust, client, serverA, Continue},
|
|
||||||
{untrust, serverA, client, Continue},
|
|
||||||
{untrust, serverA, client, Continue},
|
|
||||||
{untrust, serverB, client, Drop},
|
|
||||||
{trust, client, serverB, Continue},
|
|
||||||
{untrust, serverB, client, Continue},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
t.Run("ip_port_dependent", func(t *testing.T) {
|
||||||
|
f := &Firewall{
|
||||||
|
TrustedInterface: trust,
|
||||||
|
SessionTimeout: 30 * time.Second,
|
||||||
|
Type: AddressAndPortDependentFirewall,
|
||||||
|
}
|
||||||
|
testFirewall(t, f, []fwTest{
|
||||||
|
// client -> A authorizes A -> client
|
||||||
|
{trust, client, serverA, Continue},
|
||||||
|
{untrust, serverA, client, Continue},
|
||||||
|
{untrust, serverA, client, Continue},
|
||||||
|
|
||||||
|
// B1 -> client fails until client -> B1
|
||||||
|
{untrust, serverB1, client, Drop},
|
||||||
|
{trust, client, serverB1, Continue},
|
||||||
|
{untrust, serverB1, client, Continue},
|
||||||
|
|
||||||
|
// B2 -> client still fails
|
||||||
|
{untrust, serverB2, client, Drop},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("ip_dependent", func(t *testing.T) {
|
||||||
|
f := &Firewall{
|
||||||
|
TrustedInterface: trust,
|
||||||
|
SessionTimeout: 30 * time.Second,
|
||||||
|
Type: AddressDependentFirewall,
|
||||||
|
}
|
||||||
|
testFirewall(t, f, []fwTest{
|
||||||
|
// client -> A authorizes A -> client
|
||||||
|
{trust, client, serverA, Continue},
|
||||||
|
{untrust, serverA, client, Continue},
|
||||||
|
{untrust, serverA, client, Continue},
|
||||||
|
|
||||||
|
// B1 -> client fails until client -> B1
|
||||||
|
{untrust, serverB1, client, Drop},
|
||||||
|
{trust, client, serverB1, Continue},
|
||||||
|
{untrust, serverB1, client, Continue},
|
||||||
|
|
||||||
|
// B2 -> client also works now
|
||||||
|
{untrust, serverB2, client, Continue},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("endpoint_independent", func(t *testing.T) {
|
||||||
|
f := &Firewall{
|
||||||
|
TrustedInterface: trust,
|
||||||
|
SessionTimeout: 30 * time.Second,
|
||||||
|
Type: EndpointIndependentFirewall,
|
||||||
|
}
|
||||||
|
testFirewall(t, f, []fwTest{
|
||||||
|
// client -> A authorizes A -> client
|
||||||
|
{trust, client, serverA, Continue},
|
||||||
|
{untrust, serverA, client, Continue},
|
||||||
|
{untrust, serverA, client, Continue},
|
||||||
|
|
||||||
|
// B1 -> client also works
|
||||||
|
{untrust, serverB1, client, Continue},
|
||||||
|
|
||||||
|
// B2 -> client also works
|
||||||
|
{untrust, serverB2, client, Continue},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type fwTest struct {
|
||||||
|
iface *Interface
|
||||||
|
src, dst netaddr.IPPort
|
||||||
|
want PacketVerdict
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFirewall(t *testing.T, f *Firewall, tests []fwTest) {
|
||||||
|
t.Helper()
|
||||||
|
clock := &tstest.Clock{}
|
||||||
|
f.TimeNow = clock.Now
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
clock.Advance(time.Second)
|
clock.Advance(time.Second)
|
||||||
p := &Packet{
|
p := &Packet{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user