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