github.com/rootless-containers/rootlesskit/v2@v2.3.4/pkg/port/portutil/portutil.go (about)

     1  package portutil
     2  
     3  import (
     4  	"fmt"
     5  	"net"
     6  	"strconv"
     7  	"strings"
     8  	"text/scanner"
     9  
    10  	"github.com/rootless-containers/rootlesskit/v2/pkg/port"
    11  )
    12  
    13  // ParsePortSpec parses a Docker-like representation of PortSpec, but with
    14  // support for both "parent IP" and "child IP" (optional);
    15  // e.g. "127.0.0.1:8080:80/tcp", or "127.0.0.1:8080:10.0.2.100:80/tcp"
    16  //
    17  // Format is as follows:
    18  //
    19  //	<parent IP>:<parent port>[:<child IP>]:<child port>/<proto>
    20  //
    21  // Note that (child IP being optional) the format can either contain 5 or 4
    22  // components. When using IPv6 IP addresses, addresses must use square brackets
    23  // to prevent the colons being mistaken for delimiters. For example:
    24  //
    25  //	[::1]:8080:[::2]:80/udp
    26  func ParsePortSpec(portSpec string) (*port.Spec, error) {
    27  	const (
    28  		parentIP   = iota
    29  		parentPort = iota
    30  		childIP    = iota
    31  		childPort  = iota
    32  		proto      = iota
    33  	)
    34  
    35  	var (
    36  		s         scanner.Scanner
    37  		err       error
    38  		parts     = make([]string, 5)
    39  		index     = parentIP
    40  		delimiter = ':'
    41  	)
    42  
    43  	// First get the "proto" and "parent-port" at the end. These parts are
    44  	// required, whereas "ParentIP" is optional. Removing them first makes
    45  	// it easier to parse the remaining parts, as otherwise the third part
    46  	// could be _either_ an IP-address _or_ a Port.
    47  
    48  	// Get the proto
    49  	protoPos := strings.LastIndex(portSpec, "/")
    50  	if protoPos < 0 {
    51  		return nil, fmt.Errorf("missing proto in PortSpec string: %q", portSpec)
    52  	}
    53  	parts[proto] = portSpec[protoPos+1:]
    54  	err = validateProto(parts[proto])
    55  	if err != nil {
    56  		return nil, fmt.Errorf("invalid PortSpec string: %q: %w", portSpec, err)
    57  	}
    58  
    59  	// Get the parent port
    60  	portPos := strings.LastIndex(portSpec, ":")
    61  	if portPos < 0 {
    62  		return nil, fmt.Errorf("unexpected PortSpec string: %q", portSpec)
    63  	}
    64  	parts[childPort] = portSpec[portPos+1 : protoPos]
    65  
    66  	// Scan the remainder "<IP-address>:<port>[:<IP-address>]"
    67  	s.Init(strings.NewReader(portSpec[:portPos]))
    68  
    69  	for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
    70  		if index > childPort {
    71  			return nil, fmt.Errorf("unexpected PortSpec string: %q", portSpec)
    72  		}
    73  
    74  		switch tok {
    75  		case '[':
    76  			// Start of IPv6 IP-address; value ends at closing bracket (])
    77  			delimiter = ']'
    78  			continue
    79  		case delimiter:
    80  			if delimiter == ']' {
    81  				// End of IPv6 IP-address
    82  				delimiter = ':'
    83  				// Skip the next token, which should be a colon delimiter (:)
    84  				tok = s.Scan()
    85  			}
    86  			index++
    87  			continue
    88  		default:
    89  			parts[index] += s.TokenText()
    90  		}
    91  	}
    92  
    93  	if parts[parentIP] != "" && net.ParseIP(parts[parentIP]) == nil {
    94  		return nil, fmt.Errorf("unexpected ParentIP in PortSpec string: %q", portSpec)
    95  	}
    96  	if parts[childIP] != "" && net.ParseIP(parts[childIP]) == nil {
    97  		return nil, fmt.Errorf("unexpected ParentIP in PortSpec string: %q", portSpec)
    98  	}
    99  
   100  	ps := &port.Spec{
   101  		Proto:    parts[proto],
   102  		ParentIP: parts[parentIP],
   103  		ChildIP:  parts[childIP],
   104  	}
   105  
   106  	ps.ParentPort, err = strconv.Atoi(parts[parentPort])
   107  	if err != nil {
   108  		return nil, fmt.Errorf("unexpected ChildPort in PortSpec string: %q: %w", portSpec, err)
   109  	}
   110  
   111  	ps.ChildPort, err = strconv.Atoi(parts[childPort])
   112  	if err != nil {
   113  		return nil, fmt.Errorf("unexpected ParentPort in PortSpec string: %q: %w", portSpec, err)
   114  	}
   115  
   116  	return ps, nil
   117  }
   118  
   119  // ValidatePortSpec validates *port.Spec.
   120  // existingPorts can be optionally passed for detecting conflicts.
   121  func ValidatePortSpec(spec port.Spec, existingPorts map[int]*port.Status) error {
   122  	if err := validateProto(spec.Proto); err != nil {
   123  		return err
   124  	}
   125  	if spec.ParentIP != "" {
   126  		if net.ParseIP(spec.ParentIP) == nil {
   127  			return fmt.Errorf("invalid ParentIP: %q", spec.ParentIP)
   128  		}
   129  	}
   130  	if spec.ChildIP != "" {
   131  		if net.ParseIP(spec.ChildIP) == nil {
   132  			return fmt.Errorf("invalid ChildIP: %q", spec.ChildIP)
   133  		}
   134  	}
   135  	if spec.ParentPort <= 0 || spec.ParentPort > 65535 {
   136  		return fmt.Errorf("invalid ParentPort: %q", spec.ParentPort)
   137  	}
   138  	if spec.ChildPort <= 0 || spec.ChildPort > 65535 {
   139  		return fmt.Errorf("invalid ChildPort: %q", spec.ChildPort)
   140  	}
   141  	for id, p := range existingPorts {
   142  		sp := p.Spec
   143  		sameProto := sp.Proto == spec.Proto
   144  		sameParent := sp.ParentIP == spec.ParentIP && sp.ParentPort == spec.ParentPort
   145  		if sameProto && sameParent {
   146  			return fmt.Errorf("conflict with ID %d", id)
   147  		}
   148  	}
   149  	return nil
   150  }
   151  
   152  func validateProto(proto string) error {
   153  	switch proto {
   154  	case
   155  		"tcp", "tcp4", "tcp6",
   156  		"udp", "udp4", "udp6",
   157  		"sctp", "sctp4", "sctp6":
   158  		return nil
   159  	default:
   160  		return fmt.Errorf("unknown proto: %q", proto)
   161  	}
   162  }