github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/socket_nix.go (about)

     1  // Copyright 2015 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  //go:build !windows
     5  // +build !windows
     6  
     7  // socket_nix.go
     8  
     9  package libkb
    10  
    11  import (
    12  	"fmt"
    13  	"net"
    14  	"os"
    15  	"path/filepath"
    16  	"strings"
    17  	"sync"
    18  
    19  	"github.com/keybase/client/go/logger"
    20  )
    21  
    22  // Though I've never seen this come up in production, it definitely comes up
    23  // in systests that multiple go-routines might race over the current
    24  // working directory as they do the (chdir && dial) dance below. Make sure
    25  // a lock is held whenever operating on sockets, so that two racing goroutines
    26  // can't conflict here.
    27  var bindLock sync.Mutex
    28  
    29  func (s SocketInfo) BindToSocket() (ret net.Listener, err error) {
    30  
    31  	// Lock so that multiple goroutines can't race over current working dir.
    32  	// See note above.
    33  	bindLock.Lock()
    34  	defer bindLock.Unlock()
    35  
    36  	bindFile := s.bindFile
    37  	what := fmt.Sprintf("SocketInfo#BindToSocket(unix:%s)", bindFile)
    38  	defer Trace(s.log, what, &err)()
    39  
    40  	if err := MakeParentDirs(s.log, bindFile); err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	// Path can't be longer than N characters.
    45  	// In this case Chdir to the file directory first and use a local path.
    46  	// On many Linuxes, N=108, on some N=106, and on macOS N=104.
    47  	// N=104 is the lowest I know of.
    48  	// It's the length of the path buffer in sockaddr_un.
    49  	// And there may be a null-terminator in there, not sure, so make it 103 for good luck.
    50  	// https://github.com/golang/go/issues/6895#issuecomment-98006662
    51  	// https://gist.github.com/mlsteele/16dc5b6eb3d112b914183928c9af71b8#file-un-h-L79
    52  	// We could always Chdir, but then this function would be non-threadsafe more of the time.
    53  	// Pick your poison.
    54  	if len(bindFile) >= 103 {
    55  		prevWd, err := os.Getwd()
    56  		if err != nil {
    57  			return nil, fmt.Errorf("Error getting working directory: %s", err)
    58  		}
    59  		s.log.Debug("| Changing current working directory because path for binding is too long")
    60  		dir := filepath.Dir(bindFile)
    61  		s.log.Debug("| Chdir(%s)", dir)
    62  		if err := os.Chdir(dir); err != nil {
    63  			return nil, fmt.Errorf("Path can't be longer than 108 characters (failed to chdir): %s", err)
    64  		}
    65  
    66  		defer func() {
    67  			s.log.Debug("| Chdir(%s)", prevWd)
    68  			err := os.Chdir(prevWd)
    69  			if err != nil {
    70  				s.log.Debug("| Chdir(%s) err=%v", prevWd, err)
    71  			}
    72  		}()
    73  
    74  		bindFile = filepath.Base(bindFile)
    75  	}
    76  
    77  	s.log.Info("| net.Listen on unix:%s", bindFile)
    78  	ret, err = net.Listen("unix", bindFile)
    79  	if err != nil {
    80  		s.log.Warning("net.Listen failed with: %s", err.Error())
    81  	}
    82  	return ret, err
    83  }
    84  
    85  func (s SocketInfo) DialSocket() (net.Conn, error) {
    86  	errs := []error{}
    87  	for _, file := range s.dialFiles {
    88  		ret, err := s.dialSocket(file)
    89  		if err == nil {
    90  			return ret, nil
    91  		}
    92  		errs = append(errs, err)
    93  	}
    94  	return nil, CombineErrors(errs...)
    95  }
    96  
    97  func (s SocketInfo) dialSocket(dialFile string) (ret net.Conn, err error) {
    98  
    99  	// Lock so that multiple goroutines can't race over current working dir.
   100  	// See note above.
   101  	bindLock.Lock()
   102  	defer bindLock.Unlock()
   103  
   104  	what := fmt.Sprintf("SocketInfo#dialSocket(unix:%s)", dialFile)
   105  	defer Trace(s.log, what, &err)()
   106  
   107  	if dialFile == "" {
   108  		return nil, fmt.Errorf("Can't dial empty path")
   109  	}
   110  
   111  	// Path can't be longer than 103 characters.
   112  	// In this case Chdir to the file directory first.
   113  	// https://github.com/golang/go/issues/6895#issuecomment-98006662
   114  	if len(dialFile) >= 103 {
   115  		prevWd, err := os.Getwd()
   116  		if err != nil {
   117  			return nil, fmt.Errorf("Error getting working directory: %s", err)
   118  		}
   119  		s.log.Debug("| Changing current working directory because path for dialing is too long")
   120  		dir := filepath.Dir(dialFile)
   121  		s.log.Debug("| os.Chdir(%s)", dir)
   122  		if err := os.Chdir(dir); err != nil {
   123  			return nil, fmt.Errorf("Path can't be longer than 108 characters (failed to chdir): %s", err)
   124  		}
   125  		defer func() {
   126  			err := os.Chdir(prevWd)
   127  			if err != nil {
   128  				s.log.Debug("| Chdir(%s) err=%v", prevWd, err)
   129  			}
   130  		}()
   131  		dialFile = filepath.Base(dialFile)
   132  	}
   133  
   134  	s.log.Debug("| net.Dial(unix:%s)", dialFile)
   135  	return net.Dial("unix", dialFile)
   136  }
   137  
   138  func NewSocket(g *GlobalContext) (ret Socket, err error) {
   139  	var dialFiles []string
   140  	dialFiles, err = g.Env.GetSocketDialFiles()
   141  	if err != nil {
   142  		return
   143  	}
   144  	var bindFile string
   145  	bindFile, err = g.Env.GetSocketBindFile()
   146  	if err != nil {
   147  		return
   148  	}
   149  	g.Log.Debug("Connecting to socket with dialFiles=%s, bindFiles=%s", dialFiles, bindFile)
   150  	log := g.Log
   151  	if log == nil {
   152  		log = logger.NewNull()
   153  	}
   154  	ret = SocketInfo{
   155  		log:       log,
   156  		dialFiles: dialFiles,
   157  		bindFile:  bindFile,
   158  	}
   159  	return
   160  }
   161  
   162  func NewSocketWithFiles(
   163  	log logger.Logger, bindFile string, dialFiles []string) Socket {
   164  	return SocketInfo{
   165  		log:       log,
   166  		bindFile:  bindFile,
   167  		dialFiles: dialFiles,
   168  	}
   169  }
   170  
   171  // net.errClosing isn't exported, so do this.. UGLY!
   172  func IsSocketClosedError(e error) bool {
   173  	return strings.HasSuffix(e.Error(), "use of closed network connection")
   174  }