Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate NodeNames, Address and Tags fields #612

Merged
merged 12 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/serf/command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io/ioutil"
"log"
"os"
"regexp"
"strings"
"sync"

Expand Down Expand Up @@ -85,6 +86,18 @@ func Create(agentConf *Config, conf *serf.Config, logOutput io.Writer) (*Agent,
}
}

if agentConf.ValidateNodeNames {
var InvalidNameRe = regexp.MustCompile(`[^A-Za-z0-9\\-]+`)
if InvalidNameRe.MatchString(agentConf.NodeName) {
return nil, fmt.Errorf("NodeName contains invalid characters %v , Valid characters include "+
"all alpha-numerics and dashes.", agentConf.NodeName)
}
if len(agentConf.NodeName) > serf.MaxNodeNameLength {
return nil, fmt.Errorf("NodeName is %v characters. "+
"Valid length is between 1 and 128 characters", len(agentConf.NodeName))
}
}

return agent, nil
}

Expand Down
27 changes: 27 additions & 0 deletions cmd/serf/command/agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,3 +318,30 @@ func TestAgentKeyringFile_NoKeys(t *testing.T) {
t.Fatalf("bad: %s", err)
}
}
func TestAgentCreate_ValidateNodeName(t *testing.T) {
type test struct {
nodename string
want string
}

tests := []test{
{
nodename: "!BadChars-*",
want: "invalid characters",
},
{
nodename: "thisisonehundredandtwentyeightcharacterslongnodenametestaswehavetotesteachcaseeventheoonesonehundredandtwentyeightcharacterslong1",
want: "Valid length",
},
}
for _, tc := range tests {
agentConfig := DefaultConfig()
agentConfig.NodeName = tc.nodename
agentConfig.ValidateNodeNames = true
_, err := Create(agentConfig, serf.DefaultConfig(), nil)
if !strings.Contains(err.Error(), tc.want) {
t.Fatalf("expected: %v, got: %v", tc.want, err)
}
}

}
24 changes: 23 additions & 1 deletion cmd/serf/command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -227,6 +228,10 @@ type Config struct {
// 5 seconds.
BroadcastTimeoutRaw string `mapstructure:"broadcast_timeout"`
BroadcastTimeout time.Duration `mapstructure:"-"`

//ValidateNodeNames specifies whether or not nodenames should
// be alphanumeric and within 128 characters
ValidateNodeNames bool `mapstructure:"validate_node_names"`
}

// BindAddrParts returns the parts of the BindAddr that should be
Expand Down Expand Up @@ -344,6 +349,21 @@ func DecodeConfig(r io.Reader) (*Config, error) {
result.BroadcastTimeout = dur
}

if result.NodeName != "" && result.ValidateNodeNames {
var InvalidNameRe = regexp.MustCompile(`[^A-Za-z0-9\\-]+`)
if InvalidNameRe.MatchString(result.NodeName) {
err = fmt.Errorf("NodeName contains invalid characters %v , Valid characters include "+
"all alpha-numerics and dashes.", result.NodeName)
return nil, err
}

if len(result.NodeName) > serf.MaxNodeNameLength {
err = fmt.Errorf("NodeName is %v characters. "+
"Valid length is between 1 and 128 characters", len(result.NodeName))
return nil, err
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic for validating the name is getting repeated 3 times - we should pull it into a function for reuse. We can also avoid doing it in this serf/command/agent code at all and do it here instead: https://github.com/hashicorp/serf/blob/master/serf/serf.go#L235

serf.Create will get called on startup for Consul as well, where the code in this package is only used in the Serf binary.

}

return &result, nil
}

Expand All @@ -363,7 +383,9 @@ func containsKey(keys []string, key string) bool {
func MergeConfig(a, b *Config) *Config {
var result Config = *a

// Copy the strings if they're set
//TODO(schristoff): do i need to check nodename
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this todo (since we're verifying the final config that gets passed to Create() anyway)

//validatity here?

if b.NodeName != "" {
result.NodeName = b.NodeName
}
Expand Down
13 changes: 13 additions & 0 deletions cmd/serf/command/agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -585,3 +585,16 @@ func TestReadConfigPaths_dir(t *testing.T) {
t.Fatalf("bad: %#v", config)
}
}

func TestBadNodeName(t *testing.T) {
input := `{"node_name": "9b07b1d05b25d78a3f55d0ccde714f02
96230786636dd72ded4c731746517331
394b395a0524b495d40fdd20ad9e536c
d15c8a7872ca8617226f02c175dfc0ff
dff8012a7c25e608e4775107fd2a3aeb"}`
_, err := DecodeConfig(bytes.NewReader([]byte(input)))
if err == nil {
t.Fatalf("should have err")
}

}
7 changes: 7 additions & 0 deletions serf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"github.com/hashicorp/memberlist"
)

const MaxNodeNameLength int = 128

// ProtocolVersionMap is the mapping of Serf delegate protocol versions
// to memberlist protocol versions. We mask the memberlist protocols using
// our own protocol version.
Expand Down Expand Up @@ -253,6 +255,10 @@ type Config struct {
//
// WARNING: this should ONLY be used in tests
messageDropper func(typ messageType) bool

//ValidateNodeNames specifies whether or not nodenames should
// be alphanumeric and within 128 characters
ValidateNodeNames bool
}

// Init allocates the subdata structures
Expand Down Expand Up @@ -298,6 +304,7 @@ func DefaultConfig() *Config {
QuerySizeLimit: 1024,
EnableNameConflictResolution: true,
DisableCoordinates: false,
ValidateNodeNames: true,
UserEventSizeLimit: 512,
}
}
45 changes: 41 additions & 4 deletions serf/merge_delegate.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package serf

import (
"fmt"
"net"
"regexp"

"github.com/hashicorp/memberlist"
)
Expand All @@ -17,22 +19,31 @@ type mergeDelegate struct {
func (m *mergeDelegate) NotifyMerge(nodes []*memberlist.Node) error {
members := make([]*Member, len(nodes))
for idx, n := range nodes {
members[idx] = m.nodeToMember(n)
var err error
members[idx], err = m.nodeToMember(n)
if err != nil {
return err
}
}
return m.serf.config.Merge.NotifyMerge(members)
}

func (m *mergeDelegate) NotifyAlive(peer *memberlist.Node) error {
member := m.nodeToMember(peer)
member, err := m.nodeToMember(peer)
if err != nil {
return err
}
return m.serf.config.Merge.NotifyMerge([]*Member{member})
}

func (m *mergeDelegate) nodeToMember(n *memberlist.Node) *Member {
func (m *mergeDelegate) nodeToMember(n *memberlist.Node) (*Member, error) {
status := StatusNone
if n.State == memberlist.StateLeft {
status = StatusLeft
}

if err := m.validiateMemberInfo(n); err != nil {
return nil, err
}
return &Member{
Name: n.Name,
Addr: net.IP(n.Addr),
Expand All @@ -45,5 +56,31 @@ func (m *mergeDelegate) nodeToMember(n *memberlist.Node) *Member {
DelegateMin: n.DMin,
DelegateMax: n.DMax,
DelegateCur: n.DCur,
}, nil
}

// validateMemberInfo checks that the data we are sending is valid
func (m *mergeDelegate) validiateMemberInfo(n *memberlist.Node) error {
var InvalidNameRe = regexp.MustCompile(`[^A-Za-z0-9\\-]+`)
var InvalidIPv4Re = regexp.MustCompile(`([0-9]{1,3}\.){3}[0-9]{1,3}:[0-9]+`)
var InvalidIPv6Re = regexp.MustCompile(`([0-9a-f]{1,4}:){7}[0-9a-f]{1,4}`)
schristoff marked this conversation as resolved.
Show resolved Hide resolved
if m.serf.config.ValidateNodeNames {
if len(n.Name) > 128 {
return fmt.Errorf("NodeName length is %v characters. Valid length is between "+
"1 and 128 characters.", len(n.Name))
}
if InvalidNameRe.MatchString(n.Name) {
return fmt.Errorf("Nodename contains invalid characters %v , Valid characters include "+
"all alpha-numerics and dashes", n.Name)
}
}

if InvalidIPv4Re.MatchString(string(n.Addr)) || InvalidIPv6Re.MatchString(string(n.Addr)) {
return fmt.Errorf("Address is %v . Must be a valid representation of an IP address. ", n.Addr)
schristoff marked this conversation as resolved.
Show resolved Hide resolved
}
if len(n.Meta) > memberlist.MetaMaxSize {
return fmt.Errorf("Encoded length of tags exceeds limit of %d bytes",
memberlist.MetaMaxSize)
}
return nil
}