1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
|
package portallocator
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"sync"
"github.com/containerd/log"
)
type ipMapping map[netip.Addr]protoMap
var (
// errAllPortsAllocated is returned when no more ports are available
errAllPortsAllocated = errors.New("all ports are allocated")
// errUnknownProtocol is returned when an unknown protocol was specified
errUnknownProtocol = errors.New("unknown protocol")
once sync.Once
instance *PortAllocator
)
// alreadyAllocatedErr is the returned error information when a requested port is already being used
type alreadyAllocatedErr struct {
ip string
port int
}
// Error is the implementation of error.Error interface
func (e alreadyAllocatedErr) Error() string {
return fmt.Sprintf("Bind for %s:%d failed: port is already allocated", e.ip, e.port)
}
type (
// PortAllocator manages the transport ports database
PortAllocator struct {
mutex sync.Mutex
defaultIP net.IP
ipMap ipMapping
begin int
end int
}
portRange struct {
begin int
end int
last int
}
portMap struct {
p map[int]struct{}
defaultRange string
portRanges map[string]*portRange
}
protoMap map[string]*portMap
)
// GetPortRange returns the PortAllocator's default port range.
//
// This function is for internal use in tests, and must not be used
// for other purposes.
func GetPortRange() (start, end uint16) {
p := Get()
return uint16(p.begin), uint16(p.end)
}
// Get returns the PortAllocator
func Get() *PortAllocator {
// Port Allocator is a singleton
once.Do(func() {
instance = newInstance()
})
return instance
}
func newInstance() *PortAllocator {
begin, end := dynamicPortRange()
return &PortAllocator{
ipMap: makeIpMapping(begin, end),
defaultIP: net.IPv4zero,
begin: begin,
end: end,
}
}
func dynamicPortRange() (start, end int) {
begin, end, err := getDynamicPortRange()
if err != nil {
log.G(context.TODO()).WithError(err).Infof("falling back to default port range %d-%d", defaultPortRangeStart, defaultPortRangeEnd)
return defaultPortRangeStart, defaultPortRangeEnd
}
return begin, end
}
func makeIpMapping(begin, end int) ipMapping {
return ipMapping{
netip.IPv4Unspecified(): makeProtoMap(begin, end),
netip.IPv6Unspecified(): makeProtoMap(begin, end),
}
}
func makeProtoMap(begin, end int) protoMap {
return protoMap{
"tcp": newPortMap(begin, end),
"udp": newPortMap(begin, end),
"sctp": newPortMap(begin, end),
}
}
// RequestPort requests new port from global ports pool for specified ip and proto.
// If port is 0 it returns first free port. Otherwise it checks port availability
// in proto's pool and returns that port or error if port is already busy.
func (p *PortAllocator) RequestPort(ip net.IP, proto string, port int) (int, error) {
if ip == nil {
ip = p.defaultIP // FIXME(thaJeztah): consider making this a required argument and producing an error instead, or set default when constructing.
}
return p.RequestPortsInRange([]net.IP{ip}, proto, port, port)
}
// RequestPortInRange is equivalent to [PortAllocator.RequestPortsInRange] with
// a single IP address. If ip is nil, a port is instead requested for the
// default IP (0.0.0.0).
func (p *PortAllocator) RequestPortInRange(ip net.IP, proto string, portStart, portEnd int) (int, error) {
if ip == nil {
ip = p.defaultIP // FIXME(thaJeztah): consider making this a required argument and producing an error instead, or set default when constructing.
}
return p.RequestPortsInRange([]net.IP{ip}, proto, portStart, portEnd)
}
// RequestPortsInRange requests new ports from the global ports pool, for proto and each of ips.
// If portStart and portEnd are 0 it returns the first free port in the default ephemeral range.
// If portStart != portEnd it returns the first free port in the requested range.
// Otherwise, (portStart == portEnd) it checks port availability in the requested proto's port-pool
// and returns that port or error if port is already busy.
func (p *PortAllocator) RequestPortsInRange(ips []net.IP, proto string, portStart, portEnd int) (int, error) {
if proto != "tcp" && proto != "udp" && proto != "sctp" {
return 0, errUnknownProtocol
}
if portStart != 0 || portEnd != 0 {
// Validate custom port-range
if portStart == 0 || portEnd == 0 || portEnd < portStart {
return 0, fmt.Errorf("invalid port range: %d-%d", portStart, portEnd)
}
}
if len(ips) == 0 {
return 0, fmt.Errorf("no IP addresses specified")
}
p.mutex.Lock()
defer p.mutex.Unlock()
// Collect the portMap for the required proto and each of the IP addresses.
// If there's a new IP address, create portMap objects for each of the protocols
// and collect the one that's needed for this request.
// Mark these portMap objects as needing port allocations.
type portMapRef struct {
portMap *portMap
allocate bool
}
ipToPortMapRef := map[netip.Addr]*portMapRef{}
var ips4, ips6 bool
for _, ip := range ips {
addr, ok := netip.AddrFromSlice(ip)
if !ok {
return 0, fmt.Errorf("invalid IP address: %s", ip)
}
addr = addr.Unmap()
if addr.Is4() {
ips4 = true
} else {
ips6 = true
}
// Make sure addr -> protoMap[proto] -> portMap exists.
if _, ok := p.ipMap[addr]; !ok {
p.ipMap[addr] = makeProtoMap(p.begin, p.end)
}
// Remember the protoMap[proto] portMap, it needs the port allocation.
ipToPortMapRef[addr] = &portMapRef{
portMap: p.ipMap[addr][proto],
allocate: true,
}
}
// If ips includes an unspecified address, the port needs to be free in all ipMaps
// for that address family. Otherwise, the port needs only needs to be free in the
// per-address maps for ips, and the map for 0.0.0.0/::.
//
// Collect the additional portMaps where the port needs to be free, but
// don't mark them as needing port allocation.
for _, unspecAddr := range []netip.Addr{netip.IPv4Unspecified(), netip.IPv6Unspecified()} {
if _, ok := ipToPortMapRef[unspecAddr]; ok {
for addr, ipm := range p.ipMap {
if unspecAddr.Is4() == addr.Is4() {
if _, ok := ipToPortMapRef[addr]; !ok {
ipToPortMapRef[addr] = &portMapRef{portMap: ipm[proto]}
}
}
}
} else if (unspecAddr.Is4() && ips4) || (unspecAddr.Is6() && ips6) {
ipToPortMapRef[unspecAddr] = &portMapRef{portMap: p.ipMap[unspecAddr][proto]}
}
}
// Handle a request for a specific port.
if portStart > 0 && portStart == portEnd {
for addr, pMap := range ipToPortMapRef {
if _, allocated := pMap.portMap.p[portStart]; allocated {
return 0, alreadyAllocatedErr{ip: addr.String(), port: portStart}
}
}
for _, pMap := range ipToPortMapRef {
if pMap.allocate {
pMap.portMap.p[portStart] = struct{}{}
}
}
return portStart, nil
}
// Handle a request for a port range.
// Create/fetch ranges for each portMap.
pRanges := map[netip.Addr]*portRange{}
for addr, pMap := range ipToPortMapRef {
pRanges[addr] = pMap.portMap.getPortRange(portStart, portEnd)
}
// Arbitrarily starting after the last port allocated for the first address, search
// for a port that's available in all ranges.
firstAddr, _ := netip.AddrFromSlice(ips[0])
firstRange := pRanges[firstAddr.Unmap()]
port := firstRange.last
for i := firstRange.begin; i <= firstRange.end; i++ {
port++
if port > firstRange.end {
port = firstRange.begin
}
portAlreadyAllocated := func() bool {
for _, pMap := range ipToPortMapRef {
if _, ok := pMap.portMap.p[port]; ok {
return true
}
}
return false
}
if !portAlreadyAllocated() {
for addr, pMap := range ipToPortMapRef {
if pMap.allocate {
pMap.portMap.p[port] = struct{}{}
pRanges[addr].last = port
}
}
return port, nil
}
}
return 0, errAllPortsAllocated
}
// ReleasePort releases port from global ports pool for specified ip and proto.
func (p *PortAllocator) ReleasePort(ip net.IP, proto string, port int) {
p.mutex.Lock()
defer p.mutex.Unlock()
if ip == nil {
ip = p.defaultIP // FIXME(thaJeztah): consider making this a required argument and producing an error instead, or set default when constructing.
}
addr, ok := netip.AddrFromSlice(ip)
if !ok {
return
}
protomap, ok := p.ipMap[addr.Unmap()]
if !ok {
return
}
delete(protomap[proto].p, port)
}
// ReleaseAll releases all ports for all ips.
func (p *PortAllocator) ReleaseAll() {
begin, end := dynamicPortRange()
p.mutex.Lock()
p.ipMap = makeIpMapping(begin, end)
p.mutex.Unlock()
}
func getRangeKey(portStart, portEnd int) string {
return fmt.Sprintf("%d-%d", portStart, portEnd)
}
func newPortRange(portStart, portEnd int) *portRange {
return &portRange{
begin: portStart,
end: portEnd,
last: portEnd,
}
}
func newPortMap(portStart, portEnd int) *portMap {
defaultKey := getRangeKey(portStart, portEnd)
return &portMap{
p: map[int]struct{}{},
defaultRange: defaultKey,
portRanges: map[string]*portRange{
defaultKey: newPortRange(portStart, portEnd),
},
}
}
func (pm *portMap) getPortRange(portStart, portEnd int) *portRange {
var key string
if portStart == 0 && portEnd == 0 {
key = pm.defaultRange
} else {
key = getRangeKey(portStart, portEnd)
}
// Return existing port range, if already known.
if pr, exists := pm.portRanges[key]; exists {
return pr
}
// Otherwise create a new port range.
pr := newPortRange(portStart, portEnd)
pm.portRanges[key] = pr
return pr
}
|