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  }