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