diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3e05c155f..cc6122132 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -1366,7 +1366,7 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok b.mu.Lock() defer b.mu.Unlock() for _, pln := range b.peerAPIListeners { - if pln.ip.BitLen() == ip.BitLen() { + if pln.ip == ip { return uint16(pln.port), true } } diff --git a/net/tstun/wrap.go b/net/tstun/wrap.go index 503438042..c8003659c 100644 --- a/net/tstun/wrap.go +++ b/net/tstun/wrap.go @@ -115,6 +115,9 @@ type Wrapper struct { // disableFilter disables all filtering when set. This should only be used in tests. disableFilter bool + + // disableTSMPRejected disables TSMP rejected responses. For tests. + disableTSMPRejected bool } func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper { @@ -351,13 +354,26 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response { 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. // Their host networking stack can translate this into ICMP // or whatnot as required. But notably, their GUI or tailscale CLI // 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{ IPSrc: p.Dst.IP, IPDst: p.Src.IP, diff --git a/net/tstun/wrap_test.go b/net/tstun/wrap_test.go index 4032b168b..66330bb55 100644 --- a/net/tstun/wrap_test.go +++ b/net/tstun/wrap_test.go @@ -6,6 +6,7 @@ package tstun import ( "bytes" + "encoding/binary" "fmt" "strconv" "strings" @@ -42,6 +43,34 @@ func udp4(src, dst string, sport, dport uint16) []byte { 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) { for _, s := range nets { if i := strings.IndexByte(s, '/'); i == -1 { @@ -385,3 +414,70 @@ func TestAtomic64Alignment(t *testing.T) { c := new(Wrapper) 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) + } + }) + } +}