/
ports.go
213 lines (182 loc) · 5.09 KB
/
ports.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
package machine
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"path/filepath"
"strconv"
"github.com/containers/storage/pkg/ioutils"
"github.com/containers/storage/pkg/lockfile"
"github.com/sirupsen/logrus"
)
const (
portAllocFileName = "port-alloc.dat"
portLockFileName = "port-alloc.lck"
)
// Reserves a unique port for a machine instance in a global (user) scope across
// all machines and backend types. On success the port is guaranteed to not be
// allocated until released with a call to ReleaseMachinePort().
//
// The purpose of this method is to prevent collisions between machine
// instances when ran at the same time. Note, that dynamic port reassignment
// on its own is insufficient to resolve conflicts, since there is a narrow
// window between port detection and actual service binding, allowing for the
// possibility of a second racing machine to fail if its check is unlucky to
// fall within that window. Additionally, there is the potential for a long
// running reassignment dance over start/stop until all machine instances
// eventually arrive at total conflict free state. By reserving ports using
// mechanism these scenarios are prevented.
func AllocateMachinePort() (int, error) {
const maxRetries = 10000
handles := []io.Closer{}
defer func() {
for _, handle := range handles {
handle.Close()
}
}()
lock, err := acquirePortLock()
if err != nil {
return 0, err
}
defer lock.Unlock()
ports, err := loadPortAllocations()
if err != nil {
return 0, err
}
var port int
for i := 0; ; i++ {
var handle io.Closer
// Ports must be held temporarily to prevent repeat search results
handle, port, err = getRandomPortHold()
if err != nil {
return 0, err
}
handles = append(handles, handle)
if _, exists := ports[port]; !exists {
break
}
if i > maxRetries {
return 0, errors.New("maximum number of retries exceeded searching for available port")
}
}
ports[port] = struct{}{}
if err := storePortAllocations(ports); err != nil {
return 0, err
}
return port, nil
}
// Releases a reserved port for a machine when no longer required. Care should
// be taken to ensure there are no conditions (e.g. failure paths) where the
// port might unintentionally remain in use after releasing
func ReleaseMachinePort(port int) error {
lock, err := acquirePortLock()
if err != nil {
return err
}
defer lock.Unlock()
ports, err := loadPortAllocations()
if err != nil {
return err
}
delete(ports, port)
return storePortAllocations(ports)
}
func IsLocalPortAvailable(port int) bool {
// Used to mark invalid / unassigned port
if port <= 0 {
return false
}
lc := getPortCheckListenConfig()
l, err := lc.Listen(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
return false
}
l.Close()
return true
}
func getRandomPortHold() (io.Closer, int, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return nil, 0, fmt.Errorf("unable to get free machine port: %w", err)
}
_, portString, err := net.SplitHostPort(l.Addr().String())
if err != nil {
l.Close()
return nil, 0, fmt.Errorf("unable to determine free machine port: %w", err)
}
port, err := strconv.Atoi(portString)
if err != nil {
l.Close()
return nil, 0, fmt.Errorf("unable to convert port to int: %w", err)
}
return l, port, err
}
func acquirePortLock() (*lockfile.LockFile, error) {
lockDir, err := GetGlobalDataDir()
if err != nil {
return nil, err
}
lock, err := lockfile.GetLockFile(filepath.Join(lockDir, portLockFileName))
if err != nil {
return nil, err
}
lock.Lock()
return lock, nil
}
func loadPortAllocations() (map[int]struct{}, error) {
portDir, err := GetGlobalDataDir()
if err != nil {
return nil, err
}
var portData []int
exists := true
file, err := os.OpenFile(filepath.Join(portDir, portAllocFileName), 0, 0)
if errors.Is(err, os.ErrNotExist) {
exists = false
} else if err != nil {
return nil, err
}
defer file.Close()
// Non-existence of the file, or a corrupt file are not treated as hard
// failures, since dynamic reassignment and continued use will eventually
// rebuild the dataset. This also makes migration cases simpler, since
// the state doesn't have to exist
if exists {
decoder := json.NewDecoder(file)
if err := decoder.Decode(&portData); err != nil {
logrus.Warnf("corrupt port allocation file, could not use state")
}
}
ports := make(map[int]struct{})
placeholder := struct{}{}
for _, port := range portData {
ports[port] = placeholder
}
return ports, nil
}
func storePortAllocations(ports map[int]struct{}) error {
portDir, err := GetGlobalDataDir()
if err != nil {
return err
}
portData := make([]int, 0, len(ports))
for port := range ports {
portData = append(portData, port)
}
opts := &ioutils.AtomicFileWriterOptions{ExplicitCommit: true}
w, err := ioutils.NewAtomicFileWriterWithOpts(filepath.Join(portDir, portAllocFileName), 0644, opts)
if err != nil {
return err
}
defer w.Close()
enc := json.NewEncoder(w)
if err := enc.Encode(portData); err != nil {
return err
}
// Commit the changes to disk if no errors
return w.Commit()
}