all: add arbitrary capability support
Updates #4217 RELNOTE=start of WhoIsResponse capability support Change-Id: I6522998a911fe49e2f003077dad6164c017eed9b Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
parent
c591c91653
commit
16f3520089
|
@ -11,6 +11,9 @@ import "tailscale.com/tailcfg"
|
||||||
type WhoIsResponse struct {
|
type WhoIsResponse struct {
|
||||||
Node *tailcfg.Node
|
Node *tailcfg.Node
|
||||||
UserProfile *tailcfg.UserProfile
|
UserProfile *tailcfg.UserProfile
|
||||||
|
|
||||||
|
// Caps are extra capabilities that the remote Node has to this node.
|
||||||
|
Caps []string `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileTarget is a node to which files can be sent, and the PeerAPI
|
// FileTarget is a node to which files can be sent, and the PeerAPI
|
||||||
|
|
|
@ -526,6 +526,30 @@ func (b *LocalBackend) WhoIs(ipp netaddr.IPPort) (n *tailcfg.Node, u tailcfg.Use
|
||||||
return n, u, true
|
return n, u, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PeerCaps returns the capabilities that remote src IP has to
|
||||||
|
// ths current node.
|
||||||
|
func (b *LocalBackend) PeerCaps(src netaddr.IP) []string {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
if b.netMap == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
filt, ok := b.filterAtomic.Load().(*filter.Filter)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, a := range b.netMap.Addresses {
|
||||||
|
if !a.IsSingleIP() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dstIP := a.IP()
|
||||||
|
if dstIP.BitLen() == src.BitLen() {
|
||||||
|
return filt.AppendCaps(nil, src, a.IP())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetDecompressor sets a decompression function, which must be a zstd
|
// SetDecompressor sets a decompression function, which must be a zstd
|
||||||
// reader.
|
// reader.
|
||||||
//
|
//
|
||||||
|
|
|
@ -223,6 +223,7 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||||
res := &apitype.WhoIsResponse{
|
res := &apitype.WhoIsResponse{
|
||||||
Node: n,
|
Node: n,
|
||||||
UserProfile: &u,
|
UserProfile: &u,
|
||||||
|
Caps: b.PeerCaps(ipp.IP()),
|
||||||
}
|
}
|
||||||
j, err := json.MarshalIndent(res, "", "\t")
|
j, err := json.MarshalIndent(res, "", "\t")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -68,7 +68,8 @@ type CapabilityVersion int
|
||||||
// 29: 2022-03-21: MapResponse.PopBrowserURL
|
// 29: 2022-03-21: MapResponse.PopBrowserURL
|
||||||
// 30: 2022-03-22: client can request id tokens.
|
// 30: 2022-03-22: client can request id tokens.
|
||||||
// 31: 2022-04-15: PingRequest & PingResponse TSMP & disco support
|
// 31: 2022-04-15: PingRequest & PingResponse TSMP & disco support
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 31
|
// 32: 2022-04-17: client knows FilterRule.CapMatch
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 32
|
||||||
|
|
||||||
type StableID string
|
type StableID string
|
||||||
|
|
||||||
|
@ -1051,6 +1052,18 @@ type NetPortRange struct {
|
||||||
Ports PortRange
|
Ports PortRange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CapGrant grants capabilities in a FilterRule.
|
||||||
|
type CapGrant struct {
|
||||||
|
// Dsts are the destination IP ranges that this capabilty
|
||||||
|
// grant matches.
|
||||||
|
Dsts []netaddr.IPPrefix
|
||||||
|
|
||||||
|
// Caps are the capabilities the source IP matched by
|
||||||
|
// FilterRule.SrcIPs are granted to the destination IP,
|
||||||
|
// matched by Dsts.
|
||||||
|
Caps []string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// FilterRule represents one rule in a packet filter.
|
// FilterRule represents one rule in a packet filter.
|
||||||
//
|
//
|
||||||
// A rule is logically a set of source CIDRs to match (described by
|
// A rule is logically a set of source CIDRs to match (described by
|
||||||
|
@ -1081,7 +1094,9 @@ 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
|
//
|
||||||
|
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
|
||||||
|
DstPorts []NetPortRange `json:",omitempty"`
|
||||||
|
|
||||||
// IPProto are the IP protocol numbers to match.
|
// IPProto are the IP protocol numbers to match.
|
||||||
//
|
//
|
||||||
|
@ -1093,6 +1108,18 @@ type FilterRule struct {
|
||||||
// Depending on the IPProto values, DstPorts may or may not be
|
// Depending on the IPProto values, DstPorts may or may not be
|
||||||
// used.
|
// used.
|
||||||
IPProto []int `json:",omitempty"`
|
IPProto []int `json:",omitempty"`
|
||||||
|
|
||||||
|
// CapGrant, if non-empty, are the capabilities to
|
||||||
|
// conditionally grant to the source IP in SrcIPs.
|
||||||
|
//
|
||||||
|
// Think of DstPorts as "capabilities for networking" and
|
||||||
|
// CapGrant as arbitrary application-defined capabilities
|
||||||
|
// defined between the admin's ACLs and the application
|
||||||
|
// doing WhoIs lookups, looking up the remote IP address's
|
||||||
|
// application-level capabilities.
|
||||||
|
//
|
||||||
|
// CapGrant and DstPorts are mutually exclusive: at most one can be non-nil.
|
||||||
|
CapGrant []CapGrant `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var FilterAllowAll = []FilterRule{
|
var FilterAllowAll = []FilterRule{
|
||||||
|
|
|
@ -27,10 +27,12 @@ type Filter struct {
|
||||||
// destination within local, regardless of the policy filter
|
// destination within local, regardless of the policy filter
|
||||||
// below.
|
// below.
|
||||||
local *netaddr.IPSet
|
local *netaddr.IPSet
|
||||||
|
|
||||||
// logIPs is the set of IPs that are allowed to appear in flow
|
// logIPs is the set of IPs that are allowed to appear in flow
|
||||||
// logs. If a packet is to or from an IP not in logIPs, it will
|
// logs. If a packet is to or from an IP not in logIPs, it will
|
||||||
// never be logged.
|
// never be logged.
|
||||||
logIPs *netaddr.IPSet
|
logIPs *netaddr.IPSet
|
||||||
|
|
||||||
// matches4 and matches6 are lists of match->action rules
|
// matches4 and matches6 are lists of match->action rules
|
||||||
// applied to all packets arriving over tailscale
|
// applied to all packets arriving over tailscale
|
||||||
// tunnels. Matches are checked in order, and processing stops
|
// tunnels. Matches are checked in order, and processing stops
|
||||||
|
@ -38,6 +40,11 @@ type Filter struct {
|
||||||
// match is to drop the packet.
|
// match is to drop the packet.
|
||||||
matches4 matches
|
matches4 matches
|
||||||
matches6 matches
|
matches6 matches
|
||||||
|
|
||||||
|
// cap4 and cap6 are the subsets of the matches that are about
|
||||||
|
// capability grants, partitioned by source IP address family.
|
||||||
|
cap4, cap6 matches
|
||||||
|
|
||||||
// state is the connection tracking state attached to this
|
// state is the connection tracking state attached to this
|
||||||
// filter. It is used to allow incoming traffic that is a response
|
// filter. It is used to allow incoming traffic that is a response
|
||||||
// to an outbound connection that this node made, even if those
|
// to an outbound connection that this node made, even if those
|
||||||
|
@ -174,6 +181,8 @@ func New(matches []Match, localNets *netaddr.IPSet, logIPs *netaddr.IPSet, share
|
||||||
logf: logf,
|
logf: logf,
|
||||||
matches4: matchesFamily(matches, netaddr.IP.Is4),
|
matches4: matchesFamily(matches, netaddr.IP.Is4),
|
||||||
matches6: matchesFamily(matches, netaddr.IP.Is6),
|
matches6: matchesFamily(matches, netaddr.IP.Is6),
|
||||||
|
cap4: capMatchesFunc(matches, netaddr.IP.Is4),
|
||||||
|
cap6: capMatchesFunc(matches, netaddr.IP.Is6),
|
||||||
local: localNets,
|
local: localNets,
|
||||||
logIPs: logIPs,
|
logIPs: logIPs,
|
||||||
state: state,
|
state: state,
|
||||||
|
@ -205,6 +214,27 @@ func matchesFamily(ms matches, keep func(netaddr.IP) bool) matches {
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// capMatchesFunc returns a copy of the subset of ms for which keep(srcNet.IP)
|
||||||
|
// and the match is a capability grant.
|
||||||
|
func capMatchesFunc(ms matches, keep func(netaddr.IP) bool) matches {
|
||||||
|
var ret matches
|
||||||
|
for _, m := range ms {
|
||||||
|
if len(m.Caps) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
retm := Match{Caps: m.Caps}
|
||||||
|
for _, src := range m.Srcs {
|
||||||
|
if keep(src.IP()) {
|
||||||
|
retm.Srcs = append(retm.Srcs, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(retm.Srcs) > 0 {
|
||||||
|
ret = append(ret, retm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
func maybeHexdump(flag RunFlags, b []byte) string {
|
func maybeHexdump(flag RunFlags, b []byte) string {
|
||||||
if flag == 0 {
|
if flag == 0 {
|
||||||
return ""
|
return ""
|
||||||
|
@ -291,6 +321,30 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
|
||||||
return f.RunIn(pkt, 0)
|
return f.RunIn(pkt, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AppendCaps appends to base the capabilities that srcIP has talking
|
||||||
|
// to dstIP.
|
||||||
|
func (f *Filter) AppendCaps(base []string, srcIP, dstIP netaddr.IP) []string {
|
||||||
|
ret := base
|
||||||
|
var mm matches
|
||||||
|
switch {
|
||||||
|
case srcIP.Is4():
|
||||||
|
mm = f.cap4
|
||||||
|
case srcIP.Is6():
|
||||||
|
mm = f.cap6
|
||||||
|
}
|
||||||
|
for _, m := range mm {
|
||||||
|
if !ipInList(srcIP, m.Srcs) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, cm := range m.Caps {
|
||||||
|
if cm.Cap != "" && cm.Dst.Contains(dstIP) {
|
||||||
|
ret = append(ret, cm.Cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
// ShieldsUp reports whether this is a "shields up" (block everything
|
// ShieldsUp reports whether this is a "shields up" (block everything
|
||||||
// incoming) filter.
|
// incoming) filter.
|
||||||
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }
|
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }
|
||||||
|
|
|
@ -872,3 +872,83 @@ func TestMatchesMatchProtoAndIPsOnlyIfAllPorts(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCaps(t *testing.T) {
|
||||||
|
mm, err := MatchesFromFilterRules([]tailcfg.FilterRule{
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"*"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{{
|
||||||
|
Dsts: []netaddr.IPPrefix{
|
||||||
|
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||||
|
},
|
||||||
|
Caps: []string{"is_ipv4"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"*"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{{
|
||||||
|
Dsts: []netaddr.IPPrefix{
|
||||||
|
netaddr.MustParseIPPrefix("::/0"),
|
||||||
|
},
|
||||||
|
Caps: []string{"is_ipv6"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SrcIPs: []string{"100.199.0.0/16"},
|
||||||
|
CapGrant: []tailcfg.CapGrant{{
|
||||||
|
Dsts: []netaddr.IPPrefix{
|
||||||
|
netaddr.MustParseIPPrefix("100.200.0.0/16"),
|
||||||
|
},
|
||||||
|
Caps: []string{"some_super_admin"},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
filt := New(mm, nil, nil, nil, t.Logf)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
src, dst string // IP
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "v4",
|
||||||
|
src: "1.2.3.4",
|
||||||
|
dst: "2.4.5.5",
|
||||||
|
want: []string{"is_ipv4"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "v6",
|
||||||
|
src: "1::1",
|
||||||
|
dst: "2::2",
|
||||||
|
want: []string{"is_ipv6"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "admin",
|
||||||
|
src: "100.199.1.2",
|
||||||
|
dst: "100.200.3.4",
|
||||||
|
want: []string{"is_ipv4", "some_super_admin"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not_admin_bad_src",
|
||||||
|
src: "100.198.1.2", // 198, not 199
|
||||||
|
dst: "100.200.3.4",
|
||||||
|
want: []string{"is_ipv4"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not_admin_bad_dst",
|
||||||
|
src: "100.199.1.2",
|
||||||
|
dst: "100.201.3.4", // 201, not 200
|
||||||
|
want: []string{"is_ipv4"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := filt.AppendCaps(nil, netaddr.MustParseIP(tt.src), netaddr.MustParseIP(tt.dst))
|
||||||
|
if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("got %q; want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -47,12 +47,24 @@ func (npr NetPortRange) String() string {
|
||||||
return fmt.Sprintf("%v:%v", npr.Net, npr.Ports)
|
return fmt.Sprintf("%v:%v", npr.Net, npr.Ports)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CapMatch is a capability grant match predicate.
|
||||||
|
type CapMatch struct {
|
||||||
|
// Dst is the IP prefix that the destination IP address matches against
|
||||||
|
// to get the capability.
|
||||||
|
Dst netaddr.IPPrefix
|
||||||
|
|
||||||
|
// Cap is the capability that's granted if the destination IP addresses
|
||||||
|
// matches Dst.
|
||||||
|
Cap 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 {
|
||||||
IPProto []ipproto.Proto // required set (no default value at this layer)
|
IPProto []ipproto.Proto // required set (no default value at this layer)
|
||||||
Dsts []NetPortRange
|
|
||||||
Srcs []netaddr.IPPrefix
|
Srcs []netaddr.IPPrefix
|
||||||
|
Dsts []NetPortRange // optional, if Srcs match
|
||||||
|
Caps []CapMatch // optional, if Srcs match
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Match) String() string {
|
func (m Match) String() string {
|
||||||
|
|
|
@ -21,14 +21,16 @@ 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.IPProto = append(src.IPProto[:0:0], src.IPProto...)
|
||||||
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...)
|
||||||
|
dst.Dsts = append(src.Dsts[:0:0], src.Dsts...)
|
||||||
|
dst.Caps = append(src.Caps[:0:0], src.Caps...)
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
var _MatchCloneNeedsRegeneration = Match(struct {
|
var _MatchCloneNeedsRegeneration = Match(struct {
|
||||||
IPProto []ipproto.Proto
|
IPProto []ipproto.Proto
|
||||||
Dsts []NetPortRange
|
|
||||||
Srcs []netaddr.IPPrefix
|
Srcs []netaddr.IPPrefix
|
||||||
|
Dsts []NetPortRange
|
||||||
|
Caps []CapMatch
|
||||||
}{})
|
}{})
|
||||||
|
|
|
@ -70,6 +70,16 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, cm := range r.CapGrant {
|
||||||
|
for _, dstNet := range cm.Dsts {
|
||||||
|
for _, cap := range cm.Caps {
|
||||||
|
m.Caps = append(m.Caps, CapMatch{
|
||||||
|
Dst: dstNet,
|
||||||
|
Cap: cap,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mm = append(mm, m)
|
mm = append(mm, m)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue