tailcfg: add FilterRule.IPProto
Updates #1516 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
32562a82a9
commit
90a6fb7ffe
|
@ -35,7 +35,8 @@ import (
|
||||||
// 10: 2021-01-17: client understands MapResponse.PeerSeenChange
|
// 10: 2021-01-17: client understands MapResponse.PeerSeenChange
|
||||||
// 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping
|
// 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping
|
||||||
// 12: 2021-03-04: client understands PingRequest
|
// 12: 2021-03-04: client understands PingRequest
|
||||||
const CurrentMapRequestVersion = 12
|
// 13: 2021-03-19: client understands FilterRule.IPProto
|
||||||
|
const CurrentMapRequestVersion = 13
|
||||||
|
|
||||||
type StableID string
|
type StableID string
|
||||||
|
|
||||||
|
@ -693,6 +694,17 @@ type FilterRule struct {
|
||||||
// DstPorts are the port ranges to allow once a source IP
|
// DstPorts are the port ranges to allow once a source IP
|
||||||
// matches (is in the CIDR described by SrcIPs & SrcBits).
|
// matches (is in the CIDR described by SrcIPs & SrcBits).
|
||||||
DstPorts []NetPortRange
|
DstPorts []NetPortRange
|
||||||
|
|
||||||
|
// IPProto are the IP protocol numbers to match.
|
||||||
|
//
|
||||||
|
// As a special case, nil or empty means TCP, UDP, and ICMP.
|
||||||
|
//
|
||||||
|
// Numbers outside the uint8 range (below 0 or above 255) are
|
||||||
|
// reserved for Tailscale's use. Unknown ones are ignored.
|
||||||
|
//
|
||||||
|
// Depending on the IPProto values, DstPorts may or may not be
|
||||||
|
// used.
|
||||||
|
IPProto []int `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var FilterAllowAll = []FilterRule{
|
var FilterAllowAll = []FilterRule{
|
||||||
|
|
|
@ -182,6 +182,7 @@ func matchesFamily(ms matches, keep func(netaddr.IP) bool) matches {
|
||||||
var ret matches
|
var ret matches
|
||||||
for _, m := range ms {
|
for _, m := range ms {
|
||||||
var retm Match
|
var retm Match
|
||||||
|
retm.IPProto = m.IPProto
|
||||||
for _, src := range m.Srcs {
|
for _, src := range m.Srcs {
|
||||||
if keep(src.IP) {
|
if keep(src.IP) {
|
||||||
retm.Srcs = append(retm.Srcs, src)
|
retm.Srcs = append(retm.Srcs, src)
|
||||||
|
|
|
@ -7,6 +7,7 @@ package filter
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -16,19 +17,27 @@ import (
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/net/packet"
|
"tailscale.com/net/packet"
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newFilter(logf logger.Logf) *Filter {
|
func newFilter(logf logger.Logf) *Filter {
|
||||||
|
m := func(srcs []netaddr.IPPrefix, dsts []NetPortRange) Match {
|
||||||
|
return Match{
|
||||||
|
IPProto: defaultProtos,
|
||||||
|
Srcs: srcs,
|
||||||
|
Dsts: dsts,
|
||||||
|
}
|
||||||
|
}
|
||||||
matches := []Match{
|
matches := []Match{
|
||||||
{Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("1.2.3.4:22", "5.6.7.8:23-24")},
|
m(nets("8.1.1.1", "8.2.2.2"), netports("1.2.3.4:22", "5.6.7.8:23-24")),
|
||||||
{Srcs: nets("8.1.1.1", "8.2.2.2"), Dsts: netports("5.6.7.8:27-28")},
|
m(nets("8.1.1.1", "8.2.2.2"), netports("5.6.7.8:27-28")),
|
||||||
{Srcs: nets("2.2.2.2"), Dsts: netports("8.1.1.1:22")},
|
m(nets("2.2.2.2"), netports("8.1.1.1:22")),
|
||||||
{Srcs: nets("0.0.0.0/0"), Dsts: netports("100.122.98.50:*")},
|
m(nets("0.0.0.0/0"), netports("100.122.98.50:*")),
|
||||||
{Srcs: nets("0.0.0.0/0"), Dsts: netports("0.0.0.0/0:443")},
|
m(nets("0.0.0.0/0"), netports("0.0.0.0/0:443")),
|
||||||
{Srcs: nets("153.1.1.1", "153.1.1.2", "153.3.3.3"), Dsts: netports("1.2.3.4:999")},
|
m(nets("153.1.1.1", "153.1.1.2", "153.3.3.3"), netports("1.2.3.4:999")),
|
||||||
{Srcs: nets("::1", "::2"), Dsts: netports("2001::1:22", "2001::2:22")},
|
m(nets("::1", "::2"), netports("2001::1:22", "2001::2:22")),
|
||||||
{Srcs: nets("::/0"), Dsts: netports("::/0:443")},
|
m(nets("::/0"), netports("::/0:443")),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8,
|
// Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8,
|
||||||
|
@ -89,6 +98,9 @@ func TestFilter(t *testing.T) {
|
||||||
// unexpected dst IP.
|
// unexpected dst IP.
|
||||||
{Drop, parsed(packet.TCP, "8.1.1.1", "16.32.48.64", 0, 443)},
|
{Drop, parsed(packet.TCP, "8.1.1.1", "16.32.48.64", 0, 443)},
|
||||||
{Drop, parsed(packet.TCP, "1::", "2602::1", 0, 443)},
|
{Drop, parsed(packet.TCP, "1::", "2602::1", 0, 443)},
|
||||||
|
|
||||||
|
// Don't allow protocols not specified by filter
|
||||||
|
{Drop, parsed(132 /* SCTP */, "8.1.1.1", "1.2.3.4", 999, 22)},
|
||||||
}
|
}
|
||||||
for i, test := range tests {
|
for i, test := range tests {
|
||||||
aclFunc := acl.runIn4
|
aclFunc := acl.runIn4
|
||||||
|
@ -707,3 +719,91 @@ func netports(netPorts ...string) (ret []NetPortRange) {
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMatchesFromFilterRules(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in []tailcfg.FilterRule
|
||||||
|
want []Match
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
want: []Match{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "implicit_protos",
|
||||||
|
in: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.64.1.1"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{{
|
||||||
|
IP: "*",
|
||||||
|
Ports: tailcfg.PortRange{First: 22, Last: 22},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []Match{
|
||||||
|
{
|
||||||
|
IPProto: []packet.IPProto{
|
||||||
|
packet.TCP,
|
||||||
|
packet.UDP,
|
||||||
|
packet.ICMPv4,
|
||||||
|
packet.ICMPv6,
|
||||||
|
},
|
||||||
|
Dsts: []NetPortRange{
|
||||||
|
{
|
||||||
|
Net: netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||||
|
Ports: PortRange{22, 22},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Net: netaddr.MustParseIPPrefix("::0/0"),
|
||||||
|
Ports: PortRange{22, 22},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Srcs: []netaddr.IPPrefix{
|
||||||
|
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "explicit_protos",
|
||||||
|
in: []tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
IPProto: []int{int(packet.TCP)},
|
||||||
|
SrcIPs: []string{"100.64.1.1"},
|
||||||
|
DstPorts: []tailcfg.NetPortRange{{
|
||||||
|
IP: "1.2.0.0/16",
|
||||||
|
Ports: tailcfg.PortRange{First: 22, Last: 22},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []Match{
|
||||||
|
{
|
||||||
|
IPProto: []packet.IPProto{
|
||||||
|
packet.TCP,
|
||||||
|
},
|
||||||
|
Dsts: []NetPortRange{
|
||||||
|
{
|
||||||
|
Net: netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||||
|
Ports: PortRange{22, 22},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Srcs: []netaddr.IPPrefix{
|
||||||
|
netaddr.MustParseIPPrefix("100.64.1.1/32"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := MatchesFromFilterRules(tt.in)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("wrong\n got: %v\nwant: %v\n", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -47,11 +47,13 @@ func (npr NetPortRange) String() string {
|
||||||
// Match matches packets from any IP address in Srcs to any ip:port in
|
// Match matches packets from any IP address in Srcs to any ip:port in
|
||||||
// Dsts.
|
// Dsts.
|
||||||
type Match struct {
|
type Match struct {
|
||||||
Dsts []NetPortRange
|
IPProto []packet.IPProto // required set (no default value at this layer)
|
||||||
Srcs []netaddr.IPPrefix
|
Dsts []NetPortRange
|
||||||
|
Srcs []netaddr.IPPrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Match) String() string {
|
func (m Match) String() string {
|
||||||
|
// TODO(bradfitz): use strings.Builder, add String tests
|
||||||
srcs := []string{}
|
srcs := []string{}
|
||||||
for _, src := range m.Srcs {
|
for _, src := range m.Srcs {
|
||||||
srcs = append(srcs, src.String())
|
srcs = append(srcs, src.String())
|
||||||
|
@ -72,13 +74,16 @@ func (m Match) String() string {
|
||||||
} else {
|
} else {
|
||||||
ds = "[" + strings.Join(dsts, ",") + "]"
|
ds = "[" + strings.Join(dsts, ",") + "]"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%v=>%v", ss, ds)
|
return fmt.Sprintf("%v%v=>%v", m.IPProto, ss, ds)
|
||||||
}
|
}
|
||||||
|
|
||||||
type matches []Match
|
type matches []Match
|
||||||
|
|
||||||
func (ms matches) match(q *packet.Parsed) bool {
|
func (ms matches) match(q *packet.Parsed) bool {
|
||||||
for _, m := range ms {
|
for _, m := range ms {
|
||||||
|
if !protoInList(q.IPProto, m.IPProto) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if !ipInList(q.Src.IP, m.Srcs) {
|
if !ipInList(q.Src.IP, m.Srcs) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -117,3 +122,12 @@ func ipInList(ip netaddr.IP, netlist []netaddr.IPPrefix) bool {
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func protoInList(proto packet.IPProto, valid []packet.IPProto) bool {
|
||||||
|
for _, v := range valid {
|
||||||
|
if proto == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ package filter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/net/packet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Clone makes a deep copy of Match.
|
// Clone makes a deep copy of Match.
|
||||||
|
@ -18,6 +19,7 @@ func (src *Match) Clone() *Match {
|
||||||
}
|
}
|
||||||
dst := new(Match)
|
dst := new(Match)
|
||||||
*dst = *src
|
*dst = *src
|
||||||
|
dst.IPProto = append(src.IPProto[:0:0], src.IPProto...)
|
||||||
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
|
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
|
||||||
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
|
dst.Srcs = append(src.Srcs[:0:0], src.Srcs...)
|
||||||
return dst
|
return dst
|
||||||
|
@ -26,6 +28,7 @@ func (src *Match) Clone() *Match {
|
||||||
// A compilation failure here means this code must be regenerated, with command:
|
// A compilation failure here means this code must be regenerated, with command:
|
||||||
// tailscale.com/cmd/cloner -type Match
|
// tailscale.com/cmd/cloner -type Match
|
||||||
var _MatchNeedsRegeneration = Match(struct {
|
var _MatchNeedsRegeneration = Match(struct {
|
||||||
Dsts []NetPortRange
|
IPProto []packet.IPProto
|
||||||
Srcs []netaddr.IPPrefix
|
Dsts []NetPortRange
|
||||||
|
Srcs []netaddr.IPPrefix
|
||||||
}{})
|
}{})
|
||||||
|
|
|
@ -9,9 +9,17 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
|
"tailscale.com/net/packet"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var defaultProtos = []packet.IPProto{
|
||||||
|
packet.TCP,
|
||||||
|
packet.UDP,
|
||||||
|
packet.ICMPv4,
|
||||||
|
packet.ICMPv6,
|
||||||
|
}
|
||||||
|
|
||||||
// MatchesFromFilterRules converts tailcfg FilterRules into Matches.
|
// MatchesFromFilterRules converts tailcfg FilterRules into Matches.
|
||||||
// If an error is returned, the Matches result is still valid,
|
// If an error is returned, the Matches result is still valid,
|
||||||
// containing the rules that were successfully converted.
|
// containing the rules that were successfully converted.
|
||||||
|
@ -22,6 +30,17 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
|
||||||
for _, r := range pf {
|
for _, r := range pf {
|
||||||
m := Match{}
|
m := Match{}
|
||||||
|
|
||||||
|
if len(r.IPProto) == 0 {
|
||||||
|
m.IPProto = append([]packet.IPProto(nil), defaultProtos...)
|
||||||
|
} else {
|
||||||
|
m.IPProto = make([]packet.IPProto, 0, len(r.IPProto))
|
||||||
|
for _, n := range r.IPProto {
|
||||||
|
if n >= 0 && n <= 0xff {
|
||||||
|
m.IPProto = append(m.IPProto, packet.IPProto(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i, s := range r.SrcIPs {
|
for i, s := range r.SrcIPs {
|
||||||
var bits *int
|
var bits *int
|
||||||
if len(r.SrcBits) > i {
|
if len(r.SrcBits) > i {
|
||||||
|
|
|
@ -106,9 +106,13 @@ func netports(netPorts ...string) (ret []filter.NetPortRange) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setfilter(logf logger.Logf, tun *TUN) {
|
func setfilter(logf logger.Logf, tun *TUN) {
|
||||||
|
protos := []packet.IPProto{
|
||||||
|
packet.TCP,
|
||||||
|
packet.UDP,
|
||||||
|
}
|
||||||
matches := []filter.Match{
|
matches := []filter.Match{
|
||||||
{Srcs: nets("5.6.7.8"), Dsts: netports("1.2.3.4:89-90")},
|
{IPProto: protos, Srcs: nets("5.6.7.8"), Dsts: netports("1.2.3.4:89-90")},
|
||||||
{Srcs: nets("1.2.3.4"), Dsts: netports("5.6.7.8:98")},
|
{IPProto: protos, Srcs: nets("1.2.3.4"), Dsts: netports("5.6.7.8:98")},
|
||||||
}
|
}
|
||||||
var sb netaddr.IPSetBuilder
|
var sb netaddr.IPSetBuilder
|
||||||
sb.AddPrefix(netaddr.MustParseIPPrefix("1.2.0.0/16"))
|
sb.AddPrefix(netaddr.MustParseIPPrefix("1.2.0.0/16"))
|
||||||
|
|
Loading…
Reference in New Issue