satellite/contact: reject privateIPs in PingMe and CheckIn endpoints

prevent network enumeration by rejecting privateIPs in PingMe and
Checkin endpoints

Closes storj/storj-private#32

Change-Id: I63f00483ff4128ebd5fa9b7b8da826a5706748c9
This commit is contained in:
Paul Willoughby 2022-05-26 21:44:48 -06:00 committed by Storj Robot
parent f0b28d6326
commit 911cc1e163
5 changed files with 42 additions and 21 deletions

View File

@ -5,6 +5,7 @@ package contact
import ( import (
"context" "context"
"net"
"time" "time"
"github.com/zeebo/errs" "github.com/zeebo/errs"
@ -68,11 +69,15 @@ func (endpoint *Endpoint) CheckIn(ctx context.Context, req *pb.CheckInRequest) (
return nil, rpcstatus.Error(rpcstatus.FailedPrecondition, errCheckInIdentity.New("failed to add peer identity entry for ID: %v", err).Error()) return nil, rpcstatus.Error(rpcstatus.FailedPrecondition, errCheckInIdentity.New("failed to add peer identity entry for ID: %v", err).Error())
} }
resolvedIPPort, resolvedNetwork, err := overlay.ResolveIPAndNetwork(ctx, req.Address) resolvedIP, port, resolvedNetwork, err := overlay.ResolveIPAndNetwork(ctx, req.Address)
if err != nil { if err != nil {
endpoint.log.Info("failed to resolve IP from address", zap.String("node address", req.Address), zap.Stringer("Node ID", nodeID), zap.Error(err)) endpoint.log.Info("failed to resolve IP from address", zap.String("node address", req.Address), zap.Stringer("Node ID", nodeID), zap.Error(err))
return nil, rpcstatus.Error(rpcstatus.InvalidArgument, errCheckInNetwork.New("failed to resolve IP from address: %s, err: %v", req.Address, err).Error()) return nil, rpcstatus.Error(rpcstatus.InvalidArgument, errCheckInNetwork.New("failed to resolve IP from address: %s, err: %v", req.Address, err).Error())
} }
if !endpoint.service.allowPrivateIP && (!resolvedIP.IsGlobalUnicast() || resolvedIP.IsPrivate()) {
endpoint.log.Info("IP address not allowed", zap.String("node address", req.Address), zap.Stringer("Node ID", nodeID))
return nil, rpcstatus.Error(rpcstatus.InvalidArgument, errCheckInNetwork.New("IP address not allowed: %s", req.Address).Error())
}
nodeurl := storj.NodeURL{ nodeurl := storj.NodeURL{
ID: nodeID, ID: nodeID,
@ -102,7 +107,7 @@ func (endpoint *Endpoint) CheckIn(ctx context.Context, req *pb.CheckInRequest) (
Transport: pb.NodeTransport_TCP_TLS_GRPC, Transport: pb.NodeTransport_TCP_TLS_GRPC,
}, },
LastNet: resolvedNetwork, LastNet: resolvedNetwork,
LastIPPort: resolvedIPPort, LastIPPort: net.JoinHostPort(resolvedIP.String(), port),
IsUp: pingNodeSuccess, IsUp: pingNodeSuccess,
Capacity: req.Capacity, Capacity: req.Capacity,
Operator: req.Operator, Operator: req.Operator,
@ -157,6 +162,16 @@ func (endpoint *Endpoint) PingMe(ctx context.Context, req *pb.PingMeRequest) (_
Address: req.Address, Address: req.Address,
} }
resolvedIP, _, _, err := overlay.ResolveIPAndNetwork(ctx, req.Address)
if err != nil {
endpoint.log.Info("failed to resolve IP from address", zap.String("node address", req.Address), zap.Stringer("Node ID", nodeID), zap.Error(err))
return nil, rpcstatus.Error(rpcstatus.InvalidArgument, errCheckInNetwork.New("failed to resolve IP from address: %s, err: %v", req.Address, err).Error())
}
if !endpoint.service.allowPrivateIP && (!resolvedIP.IsGlobalUnicast() || resolvedIP.IsPrivate()) {
endpoint.log.Info("IP address not allowed", zap.String("node address", req.Address), zap.Stringer("Node ID", nodeID))
return nil, rpcstatus.Error(rpcstatus.InvalidArgument, errCheckInNetwork.New("IP address not allowed: %s", req.Address).Error())
}
if endpoint.service.timeout > 0 { if endpoint.service.timeout > 0 {
var cancel func() var cancel func()
ctx, cancel = context.WithTimeout(ctx, endpoint.service.timeout) ctx, cancel = context.WithTimeout(ctx, endpoint.service.timeout)

View File

@ -24,6 +24,7 @@ import (
type Config struct { type Config struct {
ExternalAddress string `user:"true" help:"the public address of the node, useful for nodes behind NAT" default:""` ExternalAddress string `user:"true" help:"the public address of the node, useful for nodes behind NAT" default:""`
Timeout time.Duration `help:"timeout for pinging storage nodes" default:"10m0s" testDefault:"1m"` Timeout time.Duration `help:"timeout for pinging storage nodes" default:"10m0s" testDefault:"1m"`
AllowPrivateIP bool `help:"allow private IPs in CheckIn and PingMe" testDefault:"true" devDefault:"true" default:"false"`
RateLimitInterval time.Duration `help:"the amount of time that should happen between contact attempts usually" releaseDefault:"10m0s" devDefault:"1ns"` RateLimitInterval time.Duration `help:"the amount of time that should happen between contact attempts usually" releaseDefault:"10m0s" devDefault:"1ns"`
RateLimitBurst int `help:"the maximum burst size for the contact rate limit token bucket" releaseDefault:"2" devDefault:"1000"` RateLimitBurst int `help:"the maximum burst size for the contact rate limit token bucket" releaseDefault:"2" devDefault:"1000"`
@ -45,20 +46,22 @@ type Service struct {
peerIDs overlay.PeerIdentities peerIDs overlay.PeerIdentities
dialer rpc.Dialer dialer rpc.Dialer
timeout time.Duration timeout time.Duration
idLimiter *RateLimiter idLimiter *RateLimiter
allowPrivateIP bool
} }
// NewService creates a new contact service. // NewService creates a new contact service.
func NewService(log *zap.Logger, self *overlay.NodeDossier, overlay *overlay.Service, peerIDs overlay.PeerIdentities, dialer rpc.Dialer, config Config) *Service { func NewService(log *zap.Logger, self *overlay.NodeDossier, overlay *overlay.Service, peerIDs overlay.PeerIdentities, dialer rpc.Dialer, config Config) *Service {
return &Service{ return &Service{
log: log, log: log,
self: self, self: self,
overlay: overlay, overlay: overlay,
peerIDs: peerIDs, peerIDs: peerIDs,
dialer: dialer, dialer: dialer,
timeout: config.Timeout, timeout: config.Timeout,
idLimiter: NewRateLimiter(config.RateLimitInterval, config.RateLimitBurst, config.RateLimitCacheSize), idLimiter: NewRateLimiter(config.RateLimitInterval, config.RateLimitBurst, config.RateLimitCacheSize),
allowPrivateIP: config.AllowPrivateIP,
} }
} }

View File

@ -721,15 +721,15 @@ func TestAddrtoNetwork_Conversion(t *testing.T) {
defer ctx.Cleanup() defer ctx.Cleanup()
ip := "8.8.8.8:28967" ip := "8.8.8.8:28967"
resolvedIPPort, network, err := overlay.ResolveIPAndNetwork(ctx, ip) resolvedIP, port, network, err := overlay.ResolveIPAndNetwork(ctx, ip)
require.Equal(t, "8.8.8.0", network) require.Equal(t, "8.8.8.0", network)
require.Equal(t, ip, resolvedIPPort) require.Equal(t, ip, net.JoinHostPort(resolvedIP.String(), port))
require.NoError(t, err) require.NoError(t, err)
ipv6 := "[fc00::1:200]:28967" ipv6 := "[fc00::1:200]:28967"
resolvedIPPort, network, err = overlay.ResolveIPAndNetwork(ctx, ipv6) resolvedIP, port, network, err = overlay.ResolveIPAndNetwork(ctx, ipv6)
require.Equal(t, "fc00::", network) require.Equal(t, "fc00::", network)
require.Equal(t, ipv6, resolvedIPPort) require.Equal(t, ipv6, net.JoinHostPort(resolvedIP.String(), port))
require.NoError(t, err) require.NoError(t, err)
} }

View File

@ -648,31 +648,31 @@ func (service *Service) DisqualifyNode(ctx context.Context, nodeID storj.NodeID,
} }
// ResolveIPAndNetwork resolves the target address and determines its IP and /24 subnet IPv4 or /64 subnet IPv6. // ResolveIPAndNetwork resolves the target address and determines its IP and /24 subnet IPv4 or /64 subnet IPv6.
func ResolveIPAndNetwork(ctx context.Context, target string) (ipPort, network string, err error) { func ResolveIPAndNetwork(ctx context.Context, target string) (ip net.IP, port, network string, err error) {
defer mon.Task()(&ctx)(&err) defer mon.Task()(&ctx)(&err)
host, port, err := net.SplitHostPort(target) host, port, err := net.SplitHostPort(target)
if err != nil { if err != nil {
return "", "", err return nil, "", "", err
} }
ipAddr, err := net.ResolveIPAddr("ip", host) ipAddr, err := net.ResolveIPAddr("ip", host)
if err != nil { if err != nil {
return "", "", err return nil, "", "", err
} }
// If addr can be converted to 4byte notation, it is an IPv4 address, else its an IPv6 address // If addr can be converted to 4byte notation, it is an IPv4 address, else its an IPv6 address
if ipv4 := ipAddr.IP.To4(); ipv4 != nil { if ipv4 := ipAddr.IP.To4(); ipv4 != nil {
// Filter all IPv4 Addresses into /24 Subnet's // Filter all IPv4 Addresses into /24 Subnet's
mask := net.CIDRMask(24, 32) mask := net.CIDRMask(24, 32)
return net.JoinHostPort(ipAddr.String(), port), ipv4.Mask(mask).String(), nil return ipAddr.IP, port, ipv4.Mask(mask).String(), nil
} }
if ipv6 := ipAddr.IP.To16(); ipv6 != nil { if ipv6 := ipAddr.IP.To16(); ipv6 != nil {
// Filter all IPv6 Addresses into /64 Subnet's // Filter all IPv6 Addresses into /64 Subnet's
mask := net.CIDRMask(64, 128) mask := net.CIDRMask(64, 128)
return net.JoinHostPort(ipAddr.String(), port), ipv6.Mask(mask).String(), nil return ipAddr.IP, port, ipv6.Mask(mask).String(), nil
} }
return "", "", errors.New("unable to get network for address " + ipAddr.String()) return nil, "", "", errors.New("unable to get network for address " + ipAddr.String())
} }
// TestVetNode directly sets a node's vetted_at timestamp to make testing easier. // TestVetNode directly sets a node's vetted_at timestamp to make testing easier.

View File

@ -277,6 +277,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# whether to load templates on each request # whether to load templates on each request
# console.watch: false # console.watch: false
# allow private IPs in CheckIn and PingMe
# contact.allow-private-ip: false
# the public address of the node, useful for nodes behind NAT # the public address of the node, useful for nodes behind NAT
contact.external-address: "" contact.external-address: ""