net/tstun: accept peerapi connections through the filter

Fixes tailscale/corp#1545

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-04-07 11:32:53 -07:00 committed by Brad Fitzpatrick
parent 950fc28887
commit 939861773d
3 changed files with 115 additions and 3 deletions

View File

@ -1366,7 +1366,7 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
for _, pln := range b.peerAPIListeners { for _, pln := range b.peerAPIListeners {
if pln.ip.BitLen() == ip.BitLen() { if pln.ip == ip {
return uint16(pln.port), true return uint16(pln.port), true
} }
} }

View File

@ -115,6 +115,9 @@ type Wrapper struct {
// disableFilter disables all filtering when set. This should only be used in tests. // disableFilter disables all filtering when set. This should only be used in tests.
disableFilter bool disableFilter bool
// disableTSMPRejected disables TSMP rejected responses. For tests.
disableTSMPRejected bool
} }
func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper { func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
@ -351,13 +354,26 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
return filter.Drop return filter.Drop
} }
if filt.RunIn(p, t.filterFlags) != filter.Accept { outcome := filt.RunIn(p, t.filterFlags)
// Let peerapi through the filter; its ACLs are handled at L7,
// not at the packet level.
if outcome != filter.Accept &&
p.IPProto == ipproto.TCP &&
p.TCPFlags&packet.TCPSyn != 0 &&
t.PeerAPIPort != nil {
if port, ok := t.PeerAPIPort(p.Dst.IP); ok && port == p.Dst.Port {
outcome = filter.Accept
}
}
if outcome != filter.Accept {
// Tell them, via TSMP, we're dropping them due to the ACL. // Tell them, via TSMP, we're dropping them due to the ACL.
// Their host networking stack can translate this into ICMP // Their host networking stack can translate this into ICMP
// or whatnot as required. But notably, their GUI or tailscale CLI // or whatnot as required. But notably, their GUI or tailscale CLI
// can show them a rejection history with reasons. // can show them a rejection history with reasons.
if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 { if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 && !t.disableTSMPRejected {
rj := packet.TailscaleRejectedHeader{ rj := packet.TailscaleRejectedHeader{
IPSrc: p.Dst.IP, IPSrc: p.Dst.IP,
IPDst: p.Src.IP, IPDst: p.Src.IP,

View File

@ -6,6 +6,7 @@
import ( import (
"bytes" "bytes"
"encoding/binary"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
@ -42,6 +43,34 @@ func udp4(src, dst string, sport, dport uint16) []byte {
return packet.Generate(header, []byte("udp_payload")) return packet.Generate(header, []byte("udp_payload"))
} }
func tcp4syn(src, dst string, sport, dport uint16) []byte {
sip, err := netaddr.ParseIP(src)
if err != nil {
panic(err)
}
dip, err := netaddr.ParseIP(dst)
if err != nil {
panic(err)
}
ipHeader := packet.IP4Header{
IPProto: ipproto.TCP,
Src: sip,
Dst: dip,
IPID: 0,
}
tcpHeader := make([]byte, 20)
binary.BigEndian.PutUint16(tcpHeader[0:], sport)
binary.BigEndian.PutUint16(tcpHeader[2:], dport)
tcpHeader[13] |= 2 // SYN
both := packet.Generate(ipHeader, tcpHeader)
// 20 byte IP4 + 20 byte TCP
binary.BigEndian.PutUint16(both[2:4], 40)
return both
}
func nets(nets ...string) (ret []netaddr.IPPrefix) { func nets(nets ...string) (ret []netaddr.IPPrefix) {
for _, s := range nets { for _, s := range nets {
if i := strings.IndexByte(s, '/'); i == -1 { if i := strings.IndexByte(s, '/'); i == -1 {
@ -385,3 +414,70 @@ func TestAtomic64Alignment(t *testing.T) {
c := new(Wrapper) c := new(Wrapper)
atomic.StoreInt64(&c.lastActivityAtomic, 123) atomic.StoreInt64(&c.lastActivityAtomic, 123)
} }
func TestPeerAPIBypass(t *testing.T) {
wrapperWithPeerAPI := &Wrapper{
PeerAPIPort: func(ip netaddr.IP) (port uint16, ok bool) {
if ip == netaddr.MustParseIP("100.64.1.2") {
return 60000, true
}
return
},
}
tests := []struct {
name string
w *Wrapper
filter *filter.Filter
pkt []byte
want filter.Response
}{
{
name: "reject_nil_filter",
w: &Wrapper{
PeerAPIPort: func(netaddr.IP) (port uint16, ok bool) {
return 60000, true
},
},
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop,
},
{
name: "reject_with_filter",
w: &Wrapper{},
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop,
},
{
name: "peerapi_bypass_filter",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Accept,
},
{
name: "peerapi_dont_bypass_filter_wrong_port",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60001),
want: filter.Drop,
},
{
name: "peerapi_dont_bypass_filter_wrong_dst_ip",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.3", 1234, 60000),
want: filter.Drop,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.w.SetFilter(tt.filter)
tt.w.disableTSMPRejected = true
if got := tt.w.filterIn(tt.pkt); got != tt.want {
t.Errorf("got = %v; want %v", got, tt.want)
}
})
}
}