storagenode/trust: rule and excluders

Change-Id: I84ed542e1ef3cfaa5cc3d3f631cdc295393bf978
This commit is contained in:
Andrew Harding 2019-11-15 17:20:44 -07:00
parent 5d8d9cd89f
commit 715d97e3d8
4 changed files with 392 additions and 0 deletions

View File

@ -0,0 +1,173 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package trust
import (
"net"
"strconv"
"strings"
"github.com/zeebo/errs"
"storj.io/storj/pkg/storj"
)
var (
// ErrExclusion is an error class for exclusion related errors
ErrExclusion = errs.Class("exclusion")
)
// NewExcluder takes a configuration string and returns an excluding Rule.
// Accepted forms are 1) a Satellite ID followed by '@', 2) a hostname or IP
// address, 3) a full Satellite URL.
func NewExcluder(config string) (Rule, error) {
url, err := parseExcluderConfig(config)
if err != nil {
return nil, err
}
switch {
case url.Host == "":
return NewIDExcluder(url.ID), nil
case url.ID.IsZero():
return NewHostExcluder(url.Host), nil
default:
return NewURLExcluder(url), nil
}
}
// URLExcluder excludes matching URLs
type URLExcluder struct {
url SatelliteURL
}
// NewURLExcluder returns a new URLExcluder
func NewURLExcluder(url SatelliteURL) *URLExcluder {
url.Host = normalizeHost(url.Host)
return &URLExcluder{
url: url,
}
}
// IsTrusted returns true if the given Satellite is trusted and false otherwise
func (excluder *URLExcluder) IsTrusted(url SatelliteURL) bool {
url.Host = normalizeHost(url.Host)
return excluder.url != url
}
// String returns a string representation of the excluder
func (excluder *URLExcluder) String() string {
return excluder.url.String()
}
// IDExcluder excludes URLs matching a given URL
type IDExcluder struct {
id storj.NodeID
}
// NewIDExcluder returns a new IDExcluder
func NewIDExcluder(id storj.NodeID) *IDExcluder {
return &IDExcluder{
id: id,
}
}
// IsTrusted returns true if the given Satellite is trusted and false otherwise
func (excluder *IDExcluder) IsTrusted(url SatelliteURL) bool {
return excluder.id != url.ID
}
// String returns a string representation of the excluder
func (excluder *IDExcluder) String() string {
return excluder.id.String() + "@"
}
// HostExcluder excludes URLs that match a given host. If the host is a domain
// name then URLs in a subdomain of that domain are excluded as well.
type HostExcluder struct {
host string
suffix string
}
// NewHostExcluder returns a new HostExcluder
func NewHostExcluder(host string) *HostExcluder {
host = normalizeHost(host)
// If it appears to be a domain name (i.e. has a dot) then configure the
// suffix as well
var suffix string
if strings.ContainsRune(host, '.') {
suffix = "." + host
}
return &HostExcluder{
host: host,
suffix: suffix,
}
}
// IsTrusted returns true if the given Satellite is trusted and false otherwise
func (excluder *HostExcluder) IsTrusted(url SatelliteURL) bool {
host := normalizeHost(url.Host)
if excluder.host == host {
return false
}
if excluder.suffix != "" && strings.HasSuffix(host, excluder.suffix) {
return false
}
return true
}
// String returns a string representation of the excluder
func (excluder *HostExcluder) String() string {
return excluder.host
}
// parseExcluderConfig parses a excluder configuration. The following forms are accepted:
// - Satellite ID followed by @
// - Satellite host
// - Full Satellite URL (i.e. id@host:port)
func parseExcluderConfig(s string) (SatelliteURL, error) {
url, err := storj.ParseNodeURL(s)
if err != nil {
return SatelliteURL{}, ErrExclusion.Wrap(err)
}
switch {
case url.ID.IsZero() && url.Address != "":
// Just the address was specified. Ensure it does not have a port.
_, _, err := net.SplitHostPort(url.Address)
if err == nil {
return SatelliteURL{}, ErrExclusion.New("host exclusion must not include a port")
}
return SatelliteURL{
Host: url.Address,
}, nil
case !url.ID.IsZero() && url.Address == "":
// Just the ID was specified.
return SatelliteURL{
ID: url.ID,
}, nil
}
// storj.ParseNodeURL will have already verified that the address is
// well-formed, so if SplitHostPort fails it should be due to the address
// not having a port.
host, portStr, err := net.SplitHostPort(url.Address)
if err != nil {
return SatelliteURL{}, ErrExclusion.New("satellite URL exclusion must specify a port")
}
// Port should already be numeric so this shouldn't fail, but just in case.
port, err := strconv.Atoi(portStr)
if err != nil {
return SatelliteURL{}, ErrExclusion.New("satellite URL exclusion port is not numeric")
}
return SatelliteURL{
ID: url.ID,
Host: host,
Port: port,
}, nil
}

View File

@ -0,0 +1,153 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package trust_test
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"storj.io/storj/storagenode/trust"
)
func TestNewExcluderFailure(t *testing.T) {
for _, tt := range []struct {
name string
config string
err string
}{
{
name: "not a valid URL",
config: "://",
err: "exclusion: node URL error: parse ://: missing protocol scheme",
},
{
name: "host exclusion must not include a port",
config: "bar.test:7777",
err: "exclusion: host exclusion must not include a port",
},
{
name: "satellite URL exclusion must specify a port",
config: "121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6@bar.test",
err: "exclusion: satellite URL exclusion must specify a port",
},
} {
tt := tt // quiet linting
t.Run(tt.name, func(t *testing.T) {
_, err := trust.NewExcluder(tt.config)
require.EqualError(t, err, tt.err)
})
}
}
func TestNewExcluder(t *testing.T) {
goodURL := makeSatelliteURL("foo.test")
badURL := makeSatelliteURL("b.bar.test")
for _, tt := range []struct {
name string
config string
}{
{
name: "filtered by id",
config: badURL.ID.String() + "@",
},
{
name: "filtered by root domain",
config: "bar.test",
},
{
name: "filtered by exact domain",
config: "b.bar.test",
},
{
name: "filtered by full url",
config: badURL.String(),
},
} {
tt := tt // quiet linting
t.Run(tt.name, func(t *testing.T) {
excluder, err := trust.NewExcluder(tt.config)
require.NoError(t, err)
assert.True(t, excluder.IsTrusted(goodURL), "good URL should not be excluded")
assert.False(t, excluder.IsTrusted(badURL), "bad URL should be excluded")
})
}
}
func TestHostExcluder(t *testing.T) {
for _, tt := range []struct {
exclusion string
host string
isTrusted bool
}{
{
exclusion: "foo.test",
host: "foo.test",
isTrusted: false,
},
{
exclusion: "foo.test",
host: "x.foo.test",
isTrusted: false,
},
{
exclusion: "foo.test",
host: ".foo.test",
isTrusted: false,
},
{
exclusion: "foo.test",
host: "foo.test.",
isTrusted: false,
},
{
exclusion: "x.bar.test",
host: "bar.test",
isTrusted: true,
},
{
exclusion: "x.bar.test",
host: "x.bar.test",
isTrusted: false,
},
{
exclusion: "x.bar.test",
host: "y.x.bar.test",
isTrusted: false,
},
{
exclusion: ".baz.test",
host: "baz.test",
isTrusted: false,
},
{
exclusion: "baz.test.",
host: "baz.test",
isTrusted: false,
},
{
exclusion: "satellite",
host: "satellite",
isTrusted: false,
},
{
exclusion: "satellite",
host: "x.satellite",
isTrusted: true,
},
} {
tt := tt // quiet linting
name := fmt.Sprintf("%s-%s-%t", tt.exclusion, tt.host, tt.isTrusted)
t.Run(name, func(t *testing.T) {
excluder := trust.NewHostExcluder(tt.exclusion)
isTrusted := excluder.IsTrusted(trust.SatelliteURL{Host: tt.host})
assert.Equal(t, tt.isTrusted, isTrusted)
})
}
}

26
storagenode/trust/rule.go Normal file
View File

@ -0,0 +1,26 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package trust
// Rule indicates whether or not a Satellite URL is trusted
type Rule interface {
// IsTrusted returns true if the given Satellite is trusted and false otherwise
IsTrusted(url SatelliteURL) bool
// String returns a string representation of the rule
String() string
}
// Rules is a collection of rules
type Rules []Rule
// IsTrusted returns true if the given Satellite is trusted and false otherwise
func (rules Rules) IsTrusted(url SatelliteURL) bool {
for _, rule := range rules {
if !rule.IsTrusted(url) {
return false
}
}
return true
}

View File

@ -0,0 +1,40 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package trust_test
import (
"testing"
"github.com/stretchr/testify/assert"
"storj.io/storj/storagenode/trust"
)
func TestRulesIsTrusted(t *testing.T) {
url := makeSatelliteURL("domain.test")
// default is trusted when there are no rules
var rules trust.Rules
assert.True(t, rules.IsTrusted(url))
rules = trust.Rules{fakeRule(true)}
assert.True(t, rules.IsTrusted(url))
rules = trust.Rules{fakeRule(false)}
assert.False(t, rules.IsTrusted(url))
rules = trust.Rules{fakeRule(true), fakeRule(false), fakeRule(true)}
assert.False(t, rules.IsTrusted(url))
}
type fakeRule bool
func (rule fakeRule) IsTrusted(url trust.SatelliteURL) bool {
return bool(rule)
}
func (rule fakeRule) String() string {
return "fake"
}