blob: 4d5f5ae63afdf2ae53b3521071cbb14192b897dc [file] [log] [blame]
David K. Bainbridge215e0242017-09-05 23:18:24 -07001// Package nat is a convenience package for manipulation of strings describing network ports.
2package nat
3
4import (
5 "fmt"
6 "net"
7 "strconv"
8 "strings"
9)
10
11const (
12 // portSpecTemplate is the expected format for port specifications
13 portSpecTemplate = "ip:hostPort:containerPort"
14)
15
16// PortBinding represents a binding between a Host IP address and a Host Port
17type PortBinding struct {
18 // HostIP is the host IP Address
19 HostIP string `json:"HostIp"`
20 // HostPort is the host port number
21 HostPort string
22}
23
24// PortMap is a collection of PortBinding indexed by Port
25type PortMap map[Port][]PortBinding
26
27// PortSet is a collection of structs indexed by Port
28type PortSet map[Port]struct{}
29
30// Port is a string containing port number and protocol in the format "80/tcp"
31type Port string
32
33// NewPort creates a new instance of a Port given a protocol and port number or port range
34func NewPort(proto, port string) (Port, error) {
35 // Check for parsing issues on "port" now so we can avoid having
36 // to check it later on.
37
38 portStartInt, portEndInt, err := ParsePortRangeToInt(port)
39 if err != nil {
40 return "", err
41 }
42
43 if portStartInt == portEndInt {
44 return Port(fmt.Sprintf("%d/%s", portStartInt, proto)), nil
45 }
46 return Port(fmt.Sprintf("%d-%d/%s", portStartInt, portEndInt, proto)), nil
47}
48
49// ParsePort parses the port number string and returns an int
50func ParsePort(rawPort string) (int, error) {
51 if len(rawPort) == 0 {
52 return 0, nil
53 }
54 port, err := strconv.ParseUint(rawPort, 10, 16)
55 if err != nil {
56 return 0, err
57 }
58 return int(port), nil
59}
60
61// ParsePortRangeToInt parses the port range string and returns start/end ints
62func ParsePortRangeToInt(rawPort string) (int, int, error) {
63 if len(rawPort) == 0 {
64 return 0, 0, nil
65 }
66 start, end, err := ParsePortRange(rawPort)
67 if err != nil {
68 return 0, 0, err
69 }
70 return int(start), int(end), nil
71}
72
73// Proto returns the protocol of a Port
74func (p Port) Proto() string {
75 proto, _ := SplitProtoPort(string(p))
76 return proto
77}
78
79// Port returns the port number of a Port
80func (p Port) Port() string {
81 _, port := SplitProtoPort(string(p))
82 return port
83}
84
85// Int returns the port number of a Port as an int
86func (p Port) Int() int {
87 portStr := p.Port()
88 // We don't need to check for an error because we're going to
89 // assume that any error would have been found, and reported, in NewPort()
90 port, _ := ParsePort(portStr)
91 return port
92}
93
94// Range returns the start/end port numbers of a Port range as ints
95func (p Port) Range() (int, int, error) {
96 return ParsePortRangeToInt(p.Port())
97}
98
99// SplitProtoPort splits a port in the format of proto/port
100func SplitProtoPort(rawPort string) (string, string) {
101 parts := strings.Split(rawPort, "/")
102 l := len(parts)
103 if len(rawPort) == 0 || l == 0 || len(parts[0]) == 0 {
104 return "", ""
105 }
106 if l == 1 {
107 return "tcp", rawPort
108 }
109 if len(parts[1]) == 0 {
110 return "tcp", parts[0]
111 }
112 return parts[1], parts[0]
113}
114
115func validateProto(proto string) bool {
116 for _, availableProto := range []string{"tcp", "udp"} {
117 if availableProto == proto {
118 return true
119 }
120 }
121 return false
122}
123
124// ParsePortSpecs receives port specs in the format of ip:public:private/proto and parses
125// these in to the internal types
126func ParsePortSpecs(ports []string) (map[Port]struct{}, map[Port][]PortBinding, error) {
127 var (
128 exposedPorts = make(map[Port]struct{}, len(ports))
129 bindings = make(map[Port][]PortBinding)
130 )
131 for _, rawPort := range ports {
132 portMappings, err := ParsePortSpec(rawPort)
133 if err != nil {
134 return nil, nil, err
135 }
136
137 for _, portMapping := range portMappings {
138 port := portMapping.Port
139 if _, exists := exposedPorts[port]; !exists {
140 exposedPorts[port] = struct{}{}
141 }
142 bslice, exists := bindings[port]
143 if !exists {
144 bslice = []PortBinding{}
145 }
146 bindings[port] = append(bslice, portMapping.Binding)
147 }
148 }
149 return exposedPorts, bindings, nil
150}
151
152// PortMapping is a data object mapping a Port to a PortBinding
153type PortMapping struct {
154 Port Port
155 Binding PortBinding
156}
157
158func splitParts(rawport string) (string, string, string) {
159 parts := strings.Split(rawport, ":")
160 n := len(parts)
161 containerport := parts[n-1]
162
163 switch n {
164 case 1:
165 return "", "", containerport
166 case 2:
167 return "", parts[0], containerport
168 case 3:
169 return parts[0], parts[1], containerport
170 default:
171 return strings.Join(parts[:n-2], ":"), parts[n-2], containerport
172 }
173}
174
175// ParsePortSpec parses a port specification string into a slice of PortMappings
176func ParsePortSpec(rawPort string) ([]PortMapping, error) {
177 var proto string
178 rawIP, hostPort, containerPort := splitParts(rawPort)
179 proto, containerPort = SplitProtoPort(containerPort)
180
181 // Strip [] from IPV6 addresses
182 ip, _, err := net.SplitHostPort(rawIP + ":")
183 if err != nil {
184 return nil, fmt.Errorf("Invalid ip address %v: %s", rawIP, err)
185 }
186 if ip != "" && net.ParseIP(ip) == nil {
187 return nil, fmt.Errorf("Invalid ip address: %s", ip)
188 }
189 if containerPort == "" {
190 return nil, fmt.Errorf("No port specified: %s<empty>", rawPort)
191 }
192
193 startPort, endPort, err := ParsePortRange(containerPort)
194 if err != nil {
195 return nil, fmt.Errorf("Invalid containerPort: %s", containerPort)
196 }
197
198 var startHostPort, endHostPort uint64 = 0, 0
199 if len(hostPort) > 0 {
200 startHostPort, endHostPort, err = ParsePortRange(hostPort)
201 if err != nil {
202 return nil, fmt.Errorf("Invalid hostPort: %s", hostPort)
203 }
204 }
205
206 if hostPort != "" && (endPort-startPort) != (endHostPort-startHostPort) {
207 // Allow host port range iff containerPort is not a range.
208 // In this case, use the host port range as the dynamic
209 // host port range to allocate into.
210 if endPort != startPort {
211 return nil, fmt.Errorf("Invalid ranges specified for container and host Ports: %s and %s", containerPort, hostPort)
212 }
213 }
214
215 if !validateProto(strings.ToLower(proto)) {
216 return nil, fmt.Errorf("Invalid proto: %s", proto)
217 }
218
219 ports := []PortMapping{}
220 for i := uint64(0); i <= (endPort - startPort); i++ {
221 containerPort = strconv.FormatUint(startPort+i, 10)
222 if len(hostPort) > 0 {
223 hostPort = strconv.FormatUint(startHostPort+i, 10)
224 }
225 // Set hostPort to a range only if there is a single container port
226 // and a dynamic host port.
227 if startPort == endPort && startHostPort != endHostPort {
228 hostPort = fmt.Sprintf("%s-%s", hostPort, strconv.FormatUint(endHostPort, 10))
229 }
230 port, err := NewPort(strings.ToLower(proto), containerPort)
231 if err != nil {
232 return nil, err
233 }
234
235 binding := PortBinding{
236 HostIP: ip,
237 HostPort: hostPort,
238 }
239 ports = append(ports, PortMapping{Port: port, Binding: binding})
240 }
241 return ports, nil
242}