Files
threshold/internal/libs/endpoint/preferencer.go
T
Jeff Mitchell 2a40a734aa Update preferred endpoint preference order (#1583)
Rather than pick addresses totally at random, this changes the
preference to be private IP4 > public IP4 > DNS. (Ordering is not
guaranteed, but in many cases there will only be one of each type of IP
anyways.) All three of these are preferred over IP6, which also goes
public > private.

This seems likely to match the common case where you're getting into a
machine on the private side from the public side. It's harder to know if
DNS is private or not, so IPs are preferred in this case.
2021-10-08 21:02:29 -04:00

118 lines
3.3 KiB
Go

package endpoint
import (
"context"
"net"
"github.com/hashicorp/boundary/internal/errors"
)
type preferencer struct {
matchers []matcher
}
// NewPreferencer builds up a preferencer with a set of preference options. This
// can then be used with Choose to select preferences from among a set of IP
// addresses and DNS names.
//
// Supported options: WithPreferenceOrder
func NewPreferencer(ctx context.Context, opt ...Option) (*preferencer, error) {
const op = "endpoint.NewPreferencer"
opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter))
}
pref := &preferencer{
matchers: opts.withMatchers,
}
return pref, nil
}
// Choose takes in IP addresses and/or DNS names and chooses an endpoint from
// among them, picking one at random if there are no preferences supplied. If
// preferences are specified but none match, the empty string is returned.
// However, if no IP addresses or DNS names are supplied, an error is returned.
//
// Supported options: WithIpAddrs, WithDnsNames
func (p *preferencer) Choose(ctx context.Context, opt ...Option) (string, error) {
const op = "endpoint.(preferencer).Choose"
opts, err := getOpts(opt...)
if err != nil {
return "", errors.Wrap(ctx, err, op)
}
if len(opts.withIpAddrs)+len(opts.withDnsNames) == 0 {
return "", errors.New(ctx, errors.InvalidParameter, op, "no ip addresses or dns names passed in")
}
switch len(p.matchers) {
case 0:
// We have no matchers, so pick a private address if we have one (since
// those are most likely to be what the user is trying to gain access to
// via a worker). If we don't have one, pick any IP address, as that's
// also more likely desired than a DNS name. Otherwise, pick a DNS name.
// IPv6 follows same rules as 4, but least preferenced.
nonPrivateIp4s := make([]string, 0, len(opts.withIpAddrs))
nonPrivateIp6s := make([]string, 0, len(opts.withIpAddrs))
privateIp6s := make([]string, 0, len(opts.withIpAddrs))
for _, ipStr := range opts.withIpAddrs {
ipVal := net.ParseIP(ipStr)
if ipVal != nil {
switch ipVal.To4() {
case nil: // it's v6
if ipVal.IsPrivate() {
privateIp6s = append(privateIp6s, ipStr)
} else {
nonPrivateIp6s = append(nonPrivateIp6s, ipStr)
}
default:
if ipVal.IsPrivate() {
// private IPv4 is most highly preferenced, so return it directly
return ipStr, nil
}
nonPrivateIp4s = append(nonPrivateIp4s, ipStr)
}
}
}
switch {
case len(nonPrivateIp4s) > 0:
return nonPrivateIp4s[0], nil
case len(opts.withDnsNames) > 0:
return opts.withDnsNames[0], nil
case len(privateIp6s) > 0:
return privateIp6s[0], nil
case len(nonPrivateIp6s) > 0:
return nonPrivateIp6s[0], nil
default:
// We literally have nothing if we get here, so return nothing
return "", nil
}
default:
for _, m := range p.matchers {
switch m.(type) {
case dnsMatcher:
for _, name := range opts.withDnsNames {
if m.Match(name) {
return name, nil
}
}
case cidrMatcher:
for _, addr := range opts.withIpAddrs {
if m.Match(addr) {
return addr, nil
}
}
}
}
// Nothing matched. Don't treat it as an error, let the calling function
// simply ignore the empty result.
return "", nil
}
}