Fix bugs in PickEphemeralPort for TCP.

Netstack always picks a random start point everytime PickEphemeralPort
is called. While this is required for UDP so that DNS requests go
out through a randomized set of ports it is not required for TCP. Infact
Linux explicitly hashes the (srcip, dstip, dstport) and a one time secret
initialized at start of the application to get a random offset. But to
ensure it doesn't start from the same point on every scan it uses a static
hint that is incremented by 2 in every call to pick ephemeral ports.

The reason for 2 is Linux seems to split the port ranges where active connects
seem to use even ones while odd ones are used by listening sockets.

This CL implements a similar strategy where we use a hash + hint to generate
the offset to start the search for a free Ephemeral port.

This ensures that we cycle through the available port space in order for
repeated connects to the same destination and significantly reduces the
chance of picking a recently released port.

PiperOrigin-RevId: 272058370
This commit is contained in:
Bhasker Hariharan
2019-09-30 13:55:22 -07:00
committed by gVisor bot
parent 3ad17ff597
commit 61f6fbd0ce
6 changed files with 143 additions and 6 deletions
+45 -4
View File
@@ -19,6 +19,7 @@ import (
"math"
"math/rand"
"sync"
"sync/atomic"
"gvisor.dev/gvisor/pkg/tcpip"
)
@@ -27,6 +28,10 @@ const (
// FirstEphemeral is the first ephemeral port.
FirstEphemeral = 16000
// numEphemeralPorts it the mnumber of available ephemeral ports to
// Netstack.
numEphemeralPorts = math.MaxUint16 - FirstEphemeral + 1
anyIPAddress tcpip.Address = ""
)
@@ -40,6 +45,13 @@ type portDescriptor struct {
type PortManager struct {
mu sync.RWMutex
allocatedPorts map[portDescriptor]bindAddresses
// hint is used to pick ports ephemeral ports in a stable order for
// a given port offset.
//
// hint must be accessed using the portHint/incPortHint helpers.
// TODO(gvisor.dev/issue/940): S/R this field.
hint uint32
}
type portNode struct {
@@ -130,11 +142,40 @@ func NewPortManager() *PortManager {
// is suitable for its needs, and stopping when a port is found or an error
// occurs.
func (s *PortManager) PickEphemeralPort(testPort func(p uint16) (bool, *tcpip.Error)) (port uint16, err *tcpip.Error) {
count := uint16(math.MaxUint16 - FirstEphemeral + 1)
offset := uint16(rand.Int31n(int32(count)))
offset := uint32(rand.Int31n(numEphemeralPorts))
return s.pickEphemeralPort(offset, numEphemeralPorts, testPort)
}
for i := uint16(0); i < count; i++ {
port = FirstEphemeral + (offset+i)%count
// portHint atomically reads and returns the s.hint value.
func (s *PortManager) portHint() uint32 {
return atomic.LoadUint32(&s.hint)
}
// incPortHint atomically increments s.hint by 1.
func (s *PortManager) incPortHint() {
atomic.AddUint32(&s.hint, 1)
}
// PickEphemeralPortStable starts at the specified offset + s.portHint and
// iterates over all ephemeral ports, allowing the caller to decide whether a
// given port is suitable for its needs and stopping when a port is found or an
// error occurs.
func (s *PortManager) PickEphemeralPortStable(offset uint32, testPort func(p uint16) (bool, *tcpip.Error)) (port uint16, err *tcpip.Error) {
p, err := s.pickEphemeralPort(s.portHint()+offset, numEphemeralPorts, testPort)
if err == nil {
s.incPortHint()
}
return p, err
}
// pickEphemeralPort starts at the offset specified from the FirstEphemeral port
// and iterates over the number of ports specified by count and allows the
// caller to decide whether a given port is suitable for its needs, and stopping
// when a port is found or an error occurs.
func (s *PortManager) pickEphemeralPort(offset, count uint32, testPort func(p uint16) (bool, *tcpip.Error)) (port uint16, err *tcpip.Error) {
for i := uint32(0); i < count; i++ {
port = uint16(FirstEphemeral + (offset+i)%count)
ok, err := testPort(port)
if err != nil {
return 0, err
+55 -1
View File
@@ -15,6 +15,7 @@
package ports
import (
"math/rand"
"testing"
"gvisor.dev/gvisor/pkg/tcpip"
@@ -232,7 +233,6 @@ func TestPortReservation(t *testing.T) {
}
func TestPickEphemeralPort(t *testing.T) {
pm := NewPortManager()
customErr := &tcpip.Error{}
for _, test := range []struct {
name string
@@ -276,9 +276,63 @@ func TestPickEphemeralPort(t *testing.T) {
},
} {
t.Run(test.name, func(t *testing.T) {
pm := NewPortManager()
if port, err := pm.PickEphemeralPort(test.f); port != test.wantPort || err != test.wantErr {
t.Errorf("PickEphemeralPort(..) = (port %d, err %v); want (port %d, err %v)", port, err, test.wantPort, test.wantErr)
}
})
}
}
func TestPickEphemeralPortStable(t *testing.T) {
customErr := &tcpip.Error{}
for _, test := range []struct {
name string
f func(port uint16) (bool, *tcpip.Error)
wantErr *tcpip.Error
wantPort uint16
}{
{
name: "no-port-available",
f: func(port uint16) (bool, *tcpip.Error) {
return false, nil
},
wantErr: tcpip.ErrNoPortAvailable,
},
{
name: "port-tester-error",
f: func(port uint16) (bool, *tcpip.Error) {
return false, customErr
},
wantErr: customErr,
},
{
name: "only-port-16042-available",
f: func(port uint16) (bool, *tcpip.Error) {
if port == FirstEphemeral+42 {
return true, nil
}
return false, nil
},
wantPort: FirstEphemeral + 42,
},
{
name: "only-port-under-16000-available",
f: func(port uint16) (bool, *tcpip.Error) {
if port < FirstEphemeral {
return true, nil
}
return false, nil
},
wantErr: tcpip.ErrNoPortAvailable,
},
} {
t.Run(test.name, func(t *testing.T) {
pm := NewPortManager()
portOffset := uint32(rand.Int31n(int32(numEphemeralPorts)))
if port, err := pm.PickEphemeralPortStable(portOffset, test.f); port != test.wantPort || err != test.wantErr {
t.Errorf("PickEphemeralPort(..) = (port %d, err %v); want (port %d, err %v)", port, err, test.wantPort, test.wantErr)
}
})
}
}
+1
View File
@@ -36,6 +36,7 @@ go_library(
],
deps = [
"//pkg/ilist",
"//pkg/rand",
"//pkg/sleep",
"//pkg/tcpip",
"//pkg/tcpip/buffer",
+25
View File
@@ -20,10 +20,12 @@
package stack
import (
"encoding/binary"
"sync"
"time"
"golang.org/x/time/rate"
"gvisor.dev/gvisor/pkg/rand"
"gvisor.dev/gvisor/pkg/sleep"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
@@ -388,6 +390,12 @@ type Stack struct {
// icmpRateLimiter is a global rate limiter for all ICMP messages generated
// by the stack.
icmpRateLimiter *ICMPRateLimiter
// portSeed is a one-time random value initialized at stack startup
// and is used to seed the TCP port picking on active connections
//
// TODO(gvisor.dev/issues/940): S/R this field.
portSeed uint32
}
// Options contains optional Stack configuration.
@@ -440,6 +448,7 @@ func New(opts Options) *Stack {
stats: opts.Stats.FillIn(),
handleLocal: opts.HandleLocal,
icmpRateLimiter: NewICMPRateLimiter(),
portSeed: generateRandUint32(),
}
// Add specified network protocols.
@@ -1197,3 +1206,19 @@ func (s *Stack) SetICMPBurst(burst int) {
func (s *Stack) AllowICMPMessage() bool {
return s.icmpRateLimiter.Allow()
}
// PortSeed returns a 32 bit value that can be used as a seed value for port
// picking.
//
// NOTE: The seed is generated once during stack initialization only.
func (s *Stack) PortSeed() uint32 {
return s.portSeed
}
func generateRandUint32() uint32 {
b := make([]byte, 4)
if _, err := rand.Read(b); err != nil {
panic(err)
}
return binary.LittleEndian.Uint32(b)
}
+1
View File
@@ -49,6 +49,7 @@ go_library(
"//pkg/sleep",
"//pkg/tcpip",
"//pkg/tcpip/buffer",
"//pkg/tcpip/hash/jenkins",
"//pkg/tcpip/header",
"//pkg/tcpip/iptables",
"//pkg/tcpip/seqnum",
+16 -1
View File
@@ -15,6 +15,7 @@
package tcp
import (
"encoding/binary"
"fmt"
"math"
"strings"
@@ -26,6 +27,7 @@ import (
"gvisor.dev/gvisor/pkg/sleep"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/buffer"
"gvisor.dev/gvisor/pkg/tcpip/hash/jenkins"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/iptables"
"gvisor.dev/gvisor/pkg/tcpip/seqnum"
@@ -1504,7 +1506,20 @@ func (e *endpoint) connect(addr tcpip.FullAddress, handshake bool, run bool) (er
// address/port for both local and remote (otherwise this
// endpoint would be trying to connect to itself).
sameAddr := e.id.LocalAddress == e.id.RemoteAddress
if _, err := e.stack.PickEphemeralPort(func(p uint16) (bool, *tcpip.Error) {
// Calculate a port offset based on the destination IP/port and
// src IP to ensure that for a given tuple (srcIP, destIP,
// destPort) the offset used as a starting point is the same to
// ensure that we can cycle through the port space effectively.
h := jenkins.Sum32(e.stack.PortSeed())
h.Write([]byte(e.id.LocalAddress))
h.Write([]byte(e.id.RemoteAddress))
portBuf := make([]byte, 2)
binary.LittleEndian.PutUint16(portBuf, e.id.RemotePort)
h.Write(portBuf)
portOffset := h.Sum32()
if _, err := e.stack.PickEphemeralPortStable(portOffset, func(p uint16) (bool, *tcpip.Error) {
if sameAddr && p == e.id.RemotePort {
return false, nil
}