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