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  }