github.com/containerd/nerdctl@v1.7.7/pkg/portutil/portutil.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package portutil
    18  
    19  import (
    20  	"encoding/json"
    21  	"fmt"
    22  	"net"
    23  	"strings"
    24  
    25  	gocni "github.com/containerd/go-cni"
    26  	"github.com/containerd/log"
    27  	"github.com/containerd/nerdctl/pkg/labels"
    28  	"github.com/containerd/nerdctl/pkg/rootlessutil"
    29  	"github.com/docker/go-connections/nat"
    30  )
    31  
    32  // return respectively ip, hostPort, containerPort
    33  func splitParts(rawport string) (string, string, string) {
    34  	parts := strings.Split(rawport, ":")
    35  	n := len(parts)
    36  	containerport := parts[n-1]
    37  
    38  	switch n {
    39  	case 1:
    40  		return "", "", containerport
    41  	case 2:
    42  		return "", parts[0], containerport
    43  	case 3:
    44  		return parts[0], parts[1], containerport
    45  	default:
    46  		return strings.Join(parts[:n-2], ":"), parts[n-2], containerport
    47  	}
    48  }
    49  
    50  // ParseFlagP parse port mapping pair, like "127.0.0.1:3000:8080/tcp",
    51  // "127.0.0.1:3000-3001:8080-8081/tcp" and "3000:8080" ...
    52  func ParseFlagP(s string) ([]gocni.PortMapping, error) {
    53  	proto := "tcp"
    54  	splitBySlash := strings.Split(s, "/")
    55  	switch len(splitBySlash) {
    56  	case 1:
    57  	// NOP
    58  	case 2:
    59  		proto = strings.ToLower(splitBySlash[1])
    60  		switch proto {
    61  		case "tcp", "udp", "sctp":
    62  		default:
    63  			return nil, fmt.Errorf("invalid protocol %q", splitBySlash[1])
    64  		}
    65  	default:
    66  		return nil, fmt.Errorf("failed to parse %q, unexpected slashes", s)
    67  	}
    68  
    69  	res := gocni.PortMapping{
    70  		Protocol: proto,
    71  	}
    72  
    73  	mr := []gocni.PortMapping{}
    74  
    75  	ip, hostPort, containerPort := splitParts(splitBySlash[0])
    76  
    77  	if containerPort == "" {
    78  		return nil, fmt.Errorf("no port specified: %s", splitBySlash[0])
    79  	}
    80  	var startHostPort uint64
    81  	var endHostPort uint64
    82  
    83  	startPort, endPort, err := nat.ParsePortRange(containerPort)
    84  	if err != nil {
    85  		return nil, fmt.Errorf("invalid containerPort: %s", containerPort)
    86  	}
    87  	if hostPort == "" {
    88  		// AutoHostPort could not be supported in rootless mode right now, because we can't get correct network from /proc/net/*
    89  		if rootlessutil.IsRootless() {
    90  			return nil, fmt.Errorf("automatic port allocation is not implemented for rootless mode (Hint: specify the port like \"12345:%s\", not just \"%s\")",
    91  				containerPort, containerPort)
    92  		}
    93  		startHostPort, endHostPort, err = portAllocate(proto, ip, endPort-startPort+1)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  		log.L.Debugf("There is no hostPort has been spec in command, the auto allocate port is from %d:%d to %d:%d", startHostPort, startPort, endHostPort, endPort)
    98  	} else {
    99  		startHostPort, endHostPort, err = nat.ParsePortRange(hostPort)
   100  		if err != nil {
   101  			return nil, fmt.Errorf("invalid hostPort: %s", hostPort)
   102  		}
   103  	}
   104  	if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
   105  		if endPort != startPort {
   106  			return nil, fmt.Errorf("invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
   107  		}
   108  	}
   109  
   110  	for i := int32(0); i <= (int32(endPort) - int32(startPort)); i++ {
   111  
   112  		res.ContainerPort = int32(startPort) + i
   113  		res.HostPort = int32(startHostPort) + i
   114  		if ip == "" {
   115  			//TODO handle ipv6
   116  			res.HostIP = "0.0.0.0"
   117  		} else {
   118  			// TODO handle ipv6
   119  			if net.ParseIP(ip) == nil {
   120  				return nil, fmt.Errorf("invalid ip address: %s", ip)
   121  			}
   122  			res.HostIP = ip
   123  		}
   124  
   125  		mr = append(mr, res)
   126  	}
   127  
   128  	return mr, nil
   129  }
   130  
   131  // ParsePortsLabel parses JSON-marshalled string from label map
   132  // (under `labels.Ports` key) and returns []gocni.PortMapping.
   133  func ParsePortsLabel(labelMap map[string]string) ([]gocni.PortMapping, error) {
   134  	portsJSON := labelMap[labels.Ports]
   135  	if portsJSON == "" {
   136  		return []gocni.PortMapping{}, nil
   137  	}
   138  	var ports []gocni.PortMapping
   139  	if err := json.Unmarshal([]byte(portsJSON), &ports); err != nil {
   140  		return nil, fmt.Errorf("failed to parse label %q=%q: %s", labels.Ports, portsJSON, err.Error())
   141  	}
   142  	return ports, nil
   143  }