Files
Zeling FengandgVisor bot fb2fd2ee88 Randomize TCP source port selection
Drop PickEphemeralPortStable which uses a hash-based algorithm from RFC 6056,
instead just use PickEphemeralPort that uses a randomized algorithm from RFC
6056 to avoid potential security concerns.

Discovered by Inon Kaplan (PhD candidate in the Hebrew University School of
Computer Science and Engineering), Ron Even (BSc graduate of Bar Ilan
University) and Amit Klein (faculty member in the Hebrew University School of
Computer Science and Engineering). Details will be provided in their paper, to
be presented in a forthcoming academic conference.

PiperOrigin-RevId: 581334218
2023-11-10 12:22:32 -08:00

461 lines
19 KiB
Go

// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ports
import (
"math"
"testing"
"github.com/google/go-cmp/cmp"
cryptorand "gvisor.dev/gvisor/pkg/rand"
"gvisor.dev/gvisor/pkg/tcpip"
"gvisor.dev/gvisor/pkg/tcpip/testutil"
)
const (
fakeTransNumber tcpip.TransportProtocolNumber = 1
fakeNetworkNumber tcpip.NetworkProtocolNumber = 2
)
var (
fakeIPAddress = testutil.MustParse4("8.8.8.8")
fakeIPAddress1 = testutil.MustParse4("8.8.8.9")
)
type portReserveTestAction struct {
port uint16
ip tcpip.Address
want tcpip.Error
flags Flags
release bool
device tcpip.NICID
dest tcpip.FullAddress
}
func TestPortReservation(t *testing.T) {
for _, test := range []struct {
tname string
actions []portReserveTestAction
}{
{
tname: "bind to ip",
actions: []portReserveTestAction{
{port: 80, ip: fakeIPAddress, want: nil},
{port: 80, ip: fakeIPAddress1, want: nil},
/* N.B. Order of tests matters! */
{port: 80, ip: anyIPAddress, want: &tcpip.ErrPortInUse{}},
{port: 80, ip: fakeIPAddress, want: &tcpip.ErrPortInUse{}, flags: Flags{LoadBalanced: true}},
},
},
{
tname: "bind to inaddr any",
actions: []portReserveTestAction{
{port: 22, ip: anyIPAddress, want: nil},
{port: 22, ip: fakeIPAddress, want: &tcpip.ErrPortInUse{}},
/* release fakeIPAddress, but anyIPAddress is still inuse */
{port: 22, ip: fakeIPAddress, release: true},
{port: 22, ip: fakeIPAddress, want: &tcpip.ErrPortInUse{}},
{port: 22, ip: fakeIPAddress, want: &tcpip.ErrPortInUse{}, flags: Flags{LoadBalanced: true}},
/* Release port 22 from any IP address, then try to reserve fake IP address on 22 */
{port: 22, ip: anyIPAddress, want: nil, release: true},
{port: 22, ip: fakeIPAddress, want: nil},
},
}, {
tname: "bind to zero port",
actions: []portReserveTestAction{
{port: 00, ip: fakeIPAddress, want: nil},
{port: 00, ip: fakeIPAddress, want: nil},
{port: 00, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
},
}, {
tname: "bind to ip with reuseport",
actions: []portReserveTestAction{
{port: 25, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 25, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 25, ip: fakeIPAddress, flags: Flags{}, want: &tcpip.ErrPortInUse{}},
{port: 25, ip: anyIPAddress, flags: Flags{}, want: &tcpip.ErrPortInUse{}},
{port: 25, ip: anyIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
},
}, {
tname: "bind to inaddr any with reuseport",
actions: []portReserveTestAction{
{port: 24, ip: anyIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: anyIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: anyIPAddress, flags: Flags{}, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, flags: Flags{}, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, release: true, want: nil},
{port: 24, ip: anyIPAddress, flags: Flags{LoadBalanced: true}, release: true},
{port: 24, ip: anyIPAddress, flags: Flags{}, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: anyIPAddress, flags: Flags{LoadBalanced: true}, release: true},
{port: 24, ip: anyIPAddress, flags: Flags{}, want: nil},
},
}, {
tname: "bind twice with device fails",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 3, want: nil},
{port: 24, ip: fakeIPAddress, device: 3, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind to device",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 1, want: nil},
{port: 24, ip: fakeIPAddress, device: 2, want: nil},
},
}, {
tname: "bind to device and then without device",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind without device",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, want: nil},
{port: 24, ip: fakeIPAddress, device: 123, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind with device",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, want: nil},
{port: 24, ip: fakeIPAddress, device: 123, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 0, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 456, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 789, want: nil},
{port: 24, ip: fakeIPAddress, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind with reuseport",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 123, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{LoadBalanced: true}, want: nil},
},
}, {
tname: "binding with reuseport and device",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 123, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 456, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 789, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 999, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "mixing reuseport and not reuseport by binding to device",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 456, want: nil},
{port: 24, ip: fakeIPAddress, device: 789, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 999, want: nil},
},
}, {
tname: "can't bind to 0 after mixing reuseport and not reuseport",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 456, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind and release",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 345, flags: Flags{}, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 789, flags: Flags{LoadBalanced: true}, want: nil},
// Release the bind to device 0 and try again.
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{LoadBalanced: true}, want: nil, release: true},
{port: 24, ip: fakeIPAddress, device: 345, flags: Flags{}, want: nil},
},
}, {
tname: "bind twice with reuseport once",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{}, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "release an unreserved device",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{}, want: nil},
{port: 24, ip: fakeIPAddress, device: 456, flags: Flags{}, want: nil},
// The below don't exist.
{port: 24, ip: fakeIPAddress, device: 345, flags: Flags{}, want: nil, release: true},
{port: 9999, ip: fakeIPAddress, device: 123, flags: Flags{}, want: nil, release: true},
// Release all.
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{}, want: nil, release: true},
{port: 24, ip: fakeIPAddress, device: 456, flags: Flags{}, want: nil, release: true},
},
}, {
tname: "bind with reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 123, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{MostRecent: true}, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, want: &tcpip.ErrPortInUse{}},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{MostRecent: true}, want: nil},
},
}, {
tname: "bind twice with reuseaddr once",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, device: 123, flags: Flags{}, want: nil},
{port: 24, ip: fakeIPAddress, device: 0, flags: Flags{MostRecent: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind with reuseaddr and reuseport",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
},
}, {
tname: "bind with reuseaddr and reuseport, and then reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind with reuseaddr and reuseport, and then reuseport",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind with reuseaddr and reuseport twice, and then reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true}, want: nil},
},
}, {
tname: "bind with reuseaddr and reuseport twice, and then reuseport",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
},
}, {
tname: "bind with reuseaddr, and then reuseaddr and reuseport",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind with reuseport, and then reuseaddr and reuseport",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true, LoadBalanced: true}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{MostRecent: true}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind tuple with reuseaddr, and then wildcard with reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 24}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{}, want: nil},
},
}, {
tname: "bind tuple with reuseaddr, and then wildcard",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 24}, want: nil},
{port: 24, ip: fakeIPAddress, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind wildcard with reuseaddr, and then tuple with reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 24}, want: nil},
},
}, {
tname: "bind tuple with reuseaddr, and then wildcard",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 24}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind two tuples with reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 24}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 25}, want: nil},
},
}, {
tname: "bind two tuples",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 24}, want: nil},
{port: 24, ip: fakeIPAddress, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 25}, want: nil},
},
}, {
tname: "bind wildcard, and then tuple with reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: fakeIPAddress, dest: tcpip.FullAddress{}, want: nil},
{port: 24, ip: fakeIPAddress, flags: Flags{TupleOnly: true}, dest: tcpip.FullAddress{Addr: fakeIPAddress, Port: 24}, want: &tcpip.ErrPortInUse{}},
},
}, {
tname: "bind wildcard twice with reuseaddr",
actions: []portReserveTestAction{
{port: 24, ip: anyIPAddress, flags: Flags{TupleOnly: true}, want: nil},
{port: 24, ip: anyIPAddress, flags: Flags{TupleOnly: true}, want: nil},
},
},
} {
t.Run(test.tname, func(t *testing.T) {
pm := NewPortManager()
net := []tcpip.NetworkProtocolNumber{fakeNetworkNumber}
rng := cryptorand.RNGFrom(cryptorand.Reader)
for _, test := range test.actions {
first, _ := pm.PortRange()
if test.release {
portRes := Reservation{
Networks: net,
Transport: fakeTransNumber,
Addr: test.ip,
Port: test.port,
Flags: test.flags,
BindToDevice: test.device,
Dest: test.dest,
}
pm.ReleasePort(portRes)
continue
}
portRes := Reservation{
Networks: net,
Transport: fakeTransNumber,
Addr: test.ip,
Port: test.port,
Flags: test.flags,
BindToDevice: test.device,
Dest: test.dest,
}
gotPort, err := pm.ReservePort(rng, portRes, nil /* testPort */)
if diff := cmp.Diff(test.want, err); diff != "" {
t.Fatalf("unexpected error from ReservePort(%+v, _), (-want, +got):\n%s", portRes, diff)
}
if test.port == 0 && (gotPort == 0 || gotPort < first) {
t.Fatalf("ReservePort(%+v, _) = %d, want port number >= %d to be picked", portRes, gotPort, first)
}
}
})
}
}
func TestPickEphemeralPort(t *testing.T) {
const (
firstEphemeral = 32000
numEphemeralPorts = 1000
)
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, &tcpip.ErrBadBuffer{}
},
wantErr: &tcpip.ErrBadBuffer{},
},
{
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()
rng := cryptorand.RNGFrom(cryptorand.Reader)
if err := pm.SetPortRange(firstEphemeral, firstEphemeral+numEphemeralPorts); err != nil {
t.Fatalf("failed to set ephemeral port range: %s", err)
}
port, err := pm.PickEphemeralPort(rng, test.f)
if diff := cmp.Diff(test.wantErr, err); diff != "" {
t.Fatalf("unexpected error from PickEphemeralPort(..), (-want, +got):\n%s", diff)
}
if port != test.wantPort {
t.Errorf("got PickEphemeralPort(..) = (%d, nil); want (%d, nil)", port, test.wantPort)
}
})
}
}
// TestOverflow addresses b/183593432, wherein an overflowing uint16 causes a
// port allocation failure.
func TestOverflow(t *testing.T) {
// Use a small range and start at offsets that will cause an overflow.
count := uint16(50)
for offset := uint32(math.MaxUint16 - count); offset < math.MaxUint16; offset++ {
reservedPorts := make(map[uint16]struct{})
// Ensure we can reserve everything in the allowed range.
for i := uint16(0); i < count; i++ {
port, err := pickEphemeralPort(offset, firstEphemeral, count, func(port uint16) (bool, tcpip.Error) {
if _, ok := reservedPorts[port]; !ok {
reservedPorts[port] = struct{}{}
return true, nil
}
return false, nil
})
if err != nil {
t.Fatalf("port picking failed at iteration %d, for offset %d, len(reserved): %+v", i, offset, len(reservedPorts))
}
if port < firstEphemeral || port > firstEphemeral+count {
t.Fatalf("reserved port %d, which is not in range [%d, %d]", port, firstEphemeral, firstEphemeral+count-1)
}
}
}
}