github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/pkg/specgen/generate/ports.go (about) 1 package generate 2 3 import ( 4 "context" 5 "net" 6 "strconv" 7 "strings" 8 9 "github.com/containers/podman/v2/libpod/image" 10 "github.com/containers/podman/v2/pkg/specgen" 11 "github.com/cri-o/ocicni/pkg/ocicni" 12 "github.com/pkg/errors" 13 "github.com/sirupsen/logrus" 14 ) 15 16 const ( 17 protoTCP = "tcp" 18 protoUDP = "udp" 19 protoSCTP = "sctp" 20 ) 21 22 // Parse port maps to OCICNI port mappings. 23 // Returns a set of OCICNI port mappings, and maps of utilized container and 24 // host ports. 25 func parsePortMapping(portMappings []specgen.PortMapping) ([]ocicni.PortMapping, map[string]map[string]map[uint16]uint16, map[string]map[string]map[uint16]uint16, error) { 26 // First, we need to validate the ports passed in the specgen, and then 27 // convert them into CNI port mappings. 28 type tempMapping struct { 29 mapping ocicni.PortMapping 30 startOfRange bool 31 isInRange bool 32 } 33 tempMappings := []tempMapping{} 34 35 // To validate, we need two maps: one for host ports, one for container 36 // ports. 37 // Each is a map of protocol to map of IP address to map of port to 38 // port (for hostPortValidate, it's host port to container port; 39 // for containerPortValidate, container port to host port. 40 // These will ensure no collisions. 41 hostPortValidate := make(map[string]map[string]map[uint16]uint16) 42 containerPortValidate := make(map[string]map[string]map[uint16]uint16) 43 44 // Initialize the first level of maps (we can't really guess keys for 45 // the rest). 46 for _, proto := range []string{protoTCP, protoUDP, protoSCTP} { 47 hostPortValidate[proto] = make(map[string]map[uint16]uint16) 48 containerPortValidate[proto] = make(map[string]map[uint16]uint16) 49 } 50 51 postAssignHostPort := false 52 53 // Iterate through all port mappings, generating OCICNI PortMapping 54 // structs and validating there is no overlap. 55 for _, port := range portMappings { 56 // First, check proto 57 protocols, err := checkProtocol(port.Protocol, true) 58 if err != nil { 59 return nil, nil, nil, err 60 } 61 62 // Validate host IP 63 hostIP := port.HostIP 64 if hostIP == "" { 65 hostIP = "0.0.0.0" 66 } 67 if ip := net.ParseIP(hostIP); ip == nil { 68 return nil, nil, nil, errors.Errorf("invalid IP address %s in port mapping", port.HostIP) 69 } 70 71 // Validate port numbers and range. 72 len := port.Range 73 if len == 0 { 74 len = 1 75 } 76 containerPort := port.ContainerPort 77 if containerPort == 0 { 78 return nil, nil, nil, errors.Errorf("container port number must be non-0") 79 } 80 hostPort := port.HostPort 81 if uint32(len-1)+uint32(containerPort) > 65535 { 82 return nil, nil, nil, errors.Errorf("container port range exceeds maximum allowable port number") 83 } 84 if uint32(len-1)+uint32(hostPort) > 65536 { 85 return nil, nil, nil, errors.Errorf("host port range exceeds maximum allowable port number") 86 } 87 88 // Iterate through ports, populating maps to check for conflicts 89 // and generating CNI port mappings. 90 for _, p := range protocols { 91 hostIPMap := hostPortValidate[p] 92 ctrIPMap := containerPortValidate[p] 93 94 hostPortMap, ok := hostIPMap[hostIP] 95 if !ok { 96 hostPortMap = make(map[uint16]uint16) 97 hostIPMap[hostIP] = hostPortMap 98 } 99 ctrPortMap, ok := ctrIPMap[hostIP] 100 if !ok { 101 ctrPortMap = make(map[uint16]uint16) 102 ctrIPMap[hostIP] = ctrPortMap 103 } 104 105 // Iterate through all port numbers in the requested 106 // range. 107 var index uint16 108 for index = 0; index < len; index++ { 109 cPort := containerPort + index 110 hPort := hostPort 111 // Only increment host port if it's not 0. 112 if hostPort != 0 { 113 hPort += index 114 } 115 116 if cPort == 0 { 117 return nil, nil, nil, errors.Errorf("container port cannot be 0") 118 } 119 120 // Host port is allowed to be 0. If it is, we 121 // select a random port on the host. 122 // This will happen *after* all other ports are 123 // placed, to ensure we don't accidentally 124 // select a port that a later mapping wanted. 125 if hPort == 0 { 126 // If we already have a host port 127 // assigned to their container port - 128 // just use that. 129 if ctrPortMap[cPort] != 0 { 130 hPort = ctrPortMap[cPort] 131 } else { 132 postAssignHostPort = true 133 } 134 } else { 135 testHPort := hostPortMap[hPort] 136 if testHPort != 0 && testHPort != cPort { 137 return nil, nil, nil, errors.Errorf("conflicting port mappings for host port %d (protocol %s)", hPort, p) 138 } 139 hostPortMap[hPort] = cPort 140 141 // Mapping a container port to multiple 142 // host ports is allowed. 143 // We only store the latest of these in 144 // the container port map - we don't 145 // need to know all of them, just one. 146 testCPort := ctrPortMap[cPort] 147 ctrPortMap[cPort] = hPort 148 149 // If we have an exact duplicate, just continue 150 if testCPort == hPort && testHPort == cPort { 151 continue 152 } 153 } 154 155 // We appear to be clear. Make an OCICNI port 156 // struct. 157 // Don't use hostIP - we want to preserve the 158 // empty string hostIP by default for compat. 159 cniPort := ocicni.PortMapping{ 160 HostPort: int32(hPort), 161 ContainerPort: int32(cPort), 162 Protocol: p, 163 HostIP: port.HostIP, 164 } 165 tempMappings = append( 166 tempMappings, 167 tempMapping{ 168 mapping: cniPort, 169 startOfRange: port.Range > 1 && index == 0, 170 isInRange: port.Range > 1, 171 }, 172 ) 173 } 174 } 175 } 176 177 // Handle any 0 host ports now by setting random container ports. 178 if postAssignHostPort { 179 remadeMappings := make([]ocicni.PortMapping, 0, len(tempMappings)) 180 181 var ( 182 candidate int 183 err error 184 ) 185 186 // Iterate over all 187 for _, tmp := range tempMappings { 188 p := tmp.mapping 189 190 if p.HostPort != 0 { 191 remadeMappings = append(remadeMappings, p) 192 continue 193 } 194 195 hostIPMap := hostPortValidate[p.Protocol] 196 ctrIPMap := containerPortValidate[p.Protocol] 197 198 hostPortMap, ok := hostIPMap[p.HostIP] 199 if !ok { 200 hostPortMap = make(map[uint16]uint16) 201 hostIPMap[p.HostIP] = hostPortMap 202 } 203 ctrPortMap, ok := ctrIPMap[p.HostIP] 204 if !ok { 205 ctrPortMap = make(map[uint16]uint16) 206 ctrIPMap[p.HostIP] = ctrPortMap 207 } 208 209 // See if container port has been used elsewhere 210 if ctrPortMap[uint16(p.ContainerPort)] != 0 { 211 // Duplicate definition. Let's not bother 212 // including it. 213 continue 214 } 215 216 // Max retries to ensure we don't loop forever. 217 for i := 0; i < 15; i++ { 218 // Only get a random candidate for single entries or the start 219 // of a range. Otherwise we just increment the candidate. 220 if !tmp.isInRange || tmp.startOfRange { 221 candidate, err = getRandomPort() 222 if err != nil { 223 return nil, nil, nil, errors.Wrapf(err, "error getting candidate host port for container port %d", p.ContainerPort) 224 } 225 } else { 226 candidate++ 227 } 228 229 if hostPortMap[uint16(candidate)] == 0 { 230 logrus.Debugf("Successfully assigned container port %d to host port %d (IP %s Protocol %s)", p.ContainerPort, candidate, p.HostIP, p.Protocol) 231 hostPortMap[uint16(candidate)] = uint16(p.ContainerPort) 232 ctrPortMap[uint16(p.ContainerPort)] = uint16(candidate) 233 p.HostPort = int32(candidate) 234 break 235 } 236 } 237 if p.HostPort == 0 { 238 return nil, nil, nil, errors.Errorf("could not find open host port to map container port %d to", p.ContainerPort) 239 } 240 remadeMappings = append(remadeMappings, p) 241 } 242 return remadeMappings, containerPortValidate, hostPortValidate, nil 243 } 244 245 finalMappings := []ocicni.PortMapping{} 246 for _, m := range tempMappings { 247 finalMappings = append(finalMappings, m.mapping) 248 } 249 250 return finalMappings, containerPortValidate, hostPortValidate, nil 251 } 252 253 // Make final port mappings for the container 254 func createPortMappings(ctx context.Context, s *specgen.SpecGenerator, img *image.Image) ([]ocicni.PortMapping, error) { 255 finalMappings, containerPortValidate, hostPortValidate, err := parsePortMapping(s.PortMappings) 256 if err != nil { 257 return nil, err 258 } 259 260 // If not publishing exposed ports, or if we are publishing and there is 261 // nothing to publish - then just return the port mappings we've made so 262 // far. 263 if !s.PublishExposedPorts || (len(s.Expose) == 0 && img == nil) { 264 return finalMappings, nil 265 } 266 267 logrus.Debugf("Adding exposed ports") 268 269 // We need to merge s.Expose into image exposed ports 270 expose := make(map[uint16]string) 271 for k, v := range s.Expose { 272 expose[k] = v 273 } 274 if img != nil { 275 inspect, err := img.InspectNoSize(ctx) 276 if err != nil { 277 return nil, errors.Wrapf(err, "error inspecting image to get exposed ports") 278 } 279 for imgExpose := range inspect.Config.ExposedPorts { 280 // Expose format is portNumber[/protocol] 281 splitExpose := strings.SplitN(imgExpose, "/", 2) 282 num, err := strconv.Atoi(splitExpose[0]) 283 if err != nil { 284 return nil, errors.Wrapf(err, "unable to convert image EXPOSE statement %q to port number", imgExpose) 285 } 286 if num > 65535 || num < 1 { 287 return nil, errors.Errorf("%d from image EXPOSE statement %q is not a valid port number", num, imgExpose) 288 } 289 // No need to validate protocol, we'll do it below. 290 if len(splitExpose) == 1 { 291 expose[uint16(num)] = "tcp" 292 } else { 293 expose[uint16(num)] = splitExpose[1] 294 } 295 } 296 } 297 298 // There's been a request to expose some ports. Let's do that. 299 // Start by figuring out what needs to be exposed. 300 // This is a map of container port number to protocols to expose. 301 toExpose := make(map[uint16][]string) 302 for port, proto := range expose { 303 // Validate protocol first 304 protocols, err := checkProtocol(proto, false) 305 if err != nil { 306 return nil, errors.Wrapf(err, "error validating protocols for exposed port %d", port) 307 } 308 309 if port == 0 { 310 return nil, errors.Errorf("cannot expose 0 as it is not a valid port number") 311 } 312 313 // Check to see if the port is already present in existing 314 // mappings. 315 for _, p := range protocols { 316 ctrPortMap, ok := containerPortValidate[p]["0.0.0.0"] 317 if !ok { 318 ctrPortMap = make(map[uint16]uint16) 319 containerPortValidate[p]["0.0.0.0"] = ctrPortMap 320 } 321 322 if portNum := ctrPortMap[port]; portNum == 0 { 323 // We want to expose this port for this protocol 324 exposeProto, ok := toExpose[port] 325 if !ok { 326 exposeProto = []string{} 327 } 328 exposeProto = append(exposeProto, p) 329 toExpose[port] = exposeProto 330 } 331 } 332 } 333 334 // We now have a final list of ports that we want exposed. 335 // Let's find empty, unallocated host ports for them. 336 for port, protocols := range toExpose { 337 for _, p := range protocols { 338 // Find an open port on the host. 339 // I see a faint possibility that this will infinite 340 // loop trying to find a valid open port, so I've 341 // included a max-tries counter. 342 hostPort := 0 343 tries := 15 344 for hostPort == 0 && tries > 0 { 345 // We can't select a specific protocol, which is 346 // unfortunate for the UDP case. 347 candidate, err := getRandomPort() 348 if err != nil { 349 return nil, err 350 } 351 352 // Check if the host port is already bound 353 hostPortMap, ok := hostPortValidate[p]["0.0.0.0"] 354 if !ok { 355 hostPortMap = make(map[uint16]uint16) 356 hostPortValidate[p]["0.0.0.0"] = hostPortMap 357 } 358 359 if checkPort := hostPortMap[uint16(candidate)]; checkPort != 0 { 360 // Host port is already allocated, try again 361 tries-- 362 continue 363 } 364 365 hostPortMap[uint16(candidate)] = port 366 hostPort = candidate 367 logrus.Debugf("Mapping exposed port %d/%s to host port %d", port, p, hostPort) 368 369 // Make a CNI port mapping 370 cniPort := ocicni.PortMapping{ 371 HostPort: int32(candidate), 372 ContainerPort: int32(port), 373 Protocol: p, 374 HostIP: "", 375 } 376 finalMappings = append(finalMappings, cniPort) 377 } 378 if tries == 0 && hostPort == 0 { 379 // We failed to find an open port. 380 return nil, errors.Errorf("failed to find an open port to expose container port %d on the host", port) 381 } 382 } 383 } 384 385 return finalMappings, nil 386 } 387 388 // Check a string to ensure it is a comma-separated set of valid protocols 389 func checkProtocol(protocol string, allowSCTP bool) ([]string, error) { 390 protocols := make(map[string]struct{}) 391 splitProto := strings.Split(protocol, ",") 392 // Don't error on duplicates - just deduplicate 393 for _, p := range splitProto { 394 p = strings.ToLower(p) 395 switch p { 396 case protoTCP, "": 397 protocols[protoTCP] = struct{}{} 398 case protoUDP: 399 protocols[protoUDP] = struct{}{} 400 case protoSCTP: 401 if !allowSCTP { 402 return nil, errors.Errorf("protocol SCTP is not allowed for exposed ports") 403 } 404 protocols[protoSCTP] = struct{}{} 405 default: 406 return nil, errors.Errorf("unrecognized protocol %q in port mapping", p) 407 } 408 } 409 410 finalProto := []string{} 411 for p := range protocols { 412 finalProto = append(finalProto, p) 413 } 414 415 // This shouldn't be possible, but check anyways 416 if len(finalProto) == 0 { 417 return nil, errors.Errorf("no valid protocols specified for port mapping") 418 } 419 420 return finalProto, nil 421 } 422 423 // Find a random, open port on the host 424 func getRandomPort() (int, error) { 425 l, err := net.Listen("tcp", ":0") 426 if err != nil { 427 return 0, errors.Wrapf(err, "unable to get free TCP port") 428 } 429 defer l.Close() 430 _, randomPort, err := net.SplitHostPort(l.Addr().String()) 431 if err != nil { 432 return 0, errors.Wrapf(err, "unable to determine free port") 433 } 434 rp, err := strconv.Atoi(randomPort) 435 if err != nil { 436 return 0, errors.Wrapf(err, "unable to convert random port to int") 437 } 438 return rp, nil 439 }