1package mox
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "os"
8 "runtime"
9 "runtime/debug"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/prometheus/client_golang/prometheus"
15 "github.com/prometheus/client_golang/prometheus/promauto"
16)
17
18// We start up as root, bind to sockets, open private key/cert files and fork and
19// exec as unprivileged user. During startup as root, we gather the fd's for the
20// listen addresses in passedListeners and files in passedFiles, and pass their
21// addresses and paths in environment variables to the new process.
22var passedListeners = map[string]*os.File{} // Listen address to file descriptor.
23var passedFiles = map[string][]*os.File{} // Path to file descriptors.
24
25// RestorePassedFiles reads addresses from $MOX_SOCKETS and paths from $MOX_FILES
26// and prepares an os.File for each file descriptor, which are used by later calls
27// of Listen or opening files.
28func RestorePassedFiles() {
29 s := os.Getenv("MOX_SOCKETS")
30 if s == "" {
31 var linuxhint string
32 if runtime.GOOS == "linux" {
33 linuxhint = " If you updated from v0.0.1, update the mox.service file to start as root (privileges are dropped): ./mox config printservice >mox.service && sudo systemctl daemon-reload && sudo systemctl restart mox."
34 }
35 pkglog.Fatal("mox must be started as root, and will drop privileges after binding required sockets (missing environment variable MOX_SOCKETS)." + linuxhint)
36 }
37
38 // 0,1,2 are stdin,stdout,stderr, 3 is the first passed fd (first listeners, then files).
39 var o uintptr = 3
40 for addr := range strings.SplitSeq(s, ",") {
41 passedListeners[addr] = os.NewFile(o, addr)
42 o++
43 }
44
45 files := os.Getenv("MOX_FILES")
46 if files == "" {
47 return
48 }
49 for path := range strings.SplitSeq(files, ",") {
50 passedFiles[path] = append(passedFiles[path], os.NewFile(o, path))
51 o++
52 }
53}
54
55// CleanupPassedFiles closes the listening socket file descriptors and files passed
56// in by the parent process. To be called by the unprivileged child after listeners
57// have been recreated (they dup the file descriptor), and by the privileged
58// process after starting its child.
59func CleanupPassedFiles() {
60 for _, f := range passedListeners {
61 err := f.Close()
62 pkglog.Check(err, "closing listener socket file descriptor")
63 }
64 for _, fl := range passedFiles {
65 for _, f := range fl {
66 err := f.Close()
67 pkglog.Check(err, "closing path file descriptor")
68 }
69 }
70}
71
72// For privileged file descriptor operations (listen and opening privileged files),
73// perform them immediately, regardless of running as root or other user, in case
74// ForkExecUnprivileged is not used.
75var FilesImmediate bool
76
77// Listen returns a newly created network listener when starting as root, and
78// otherwise (not root) returns a network listener from a file descriptor that was
79// passed by the parent root process.
80func Listen(network, addr string) (net.Listener, error) {
81 if os.Getuid() != 0 && !FilesImmediate {
82 f, ok := passedListeners[addr]
83 if !ok {
84 return nil, fmt.Errorf("no file descriptor for listener %s", addr)
85 }
86 ln, err := net.FileListener(f)
87 if err != nil {
88 return nil, fmt.Errorf("making network listener from file descriptor for address %s: %v", addr, err)
89 }
90 return ln, nil
91 }
92
93 if _, ok := passedListeners[addr]; ok {
94 return nil, fmt.Errorf("duplicate listener: %s", addr)
95 }
96
97 ln, err := net.Listen(network, addr)
98 if err != nil {
99 return nil, err
100 }
101 // On windows, we cannot duplicate a socket. We don't need to for mox localserve
102 // with FilesImmediate.
103 if !FilesImmediate {
104 tcpln, ok := ln.(*net.TCPListener)
105 if !ok {
106 return nil, fmt.Errorf("listener not a tcp listener, but %T, for network %s, address %s", ln, network, addr)
107 }
108 f, err := tcpln.File()
109 if err != nil {
110 return nil, fmt.Errorf("dup listener: %v", err)
111 }
112 passedListeners[addr] = f
113 }
114 return ln, err
115}
116
117// Open a privileged file, such as a TLS private key. When running as root
118// (during startup), the file is opened and the file descriptor is stored.
119// These file descriptors are passed to the unprivileged process. When in the
120// unprivileged processed, we lookup a passed file descriptor.
121// The same calls should be made in the privileged and unprivileged process.
122func OpenPrivileged(path string) (*os.File, error) {
123 if os.Getuid() != 0 && !FilesImmediate {
124 fl := passedFiles[path]
125 if len(fl) == 0 {
126 return nil, fmt.Errorf("no file descriptor for file %s", path)
127 }
128 f := fl[0]
129 passedFiles[path] = fl[1:]
130 return f, nil
131 }
132
133 f, err := os.Open(path)
134 if err != nil {
135 return nil, err
136 }
137 passedFiles[path] = append(passedFiles[path], f)
138
139 // Open again, the caller will be closing this file.
140 return os.Open(path)
141}
142
143// Shutdown is canceled when a graceful shutdown is initiated. SMTP, IMAP, periodic
144// processes should check this before starting a new operation. If this context is
145// canceled, the operation should not be started, and new connections/commands should
146// receive a message that the service is currently not available.
147var Shutdown context.Context
148var ShutdownCancel func()
149
150// This context should be used as parent by most operations. It is canceled 1
151// second after graceful shutdown was initiated with the cancelation of the
152// Shutdown context. This should abort active operations.
153//
154// Operations typically have context timeouts, 30s for single i/o like DNS queries,
155// and 1 minute for operations with more back and forth. These are set through a
156// context.WithTimeout based on this context, so those contexts are still canceled
157// when shutting down.
158//
159// HTTP servers don't get graceful shutdown, their connections are just aborted.
160// todo: should shut down http connections as well, and shut down the listener and/or return 503 for new requests.
161var Context context.Context
162var ContextCancel func()
163
164// Connections holds all active protocol sockets (smtp, imap). They will be given
165// an immediate read/write deadline shortly after initiating mox shutdown, after
166// which the connections get 1 more second for error handling before actual
167// shutdown.
168var Connections = &connections{
169 conns: map[net.Conn]connKind{},
170 gauges: map[connKind]prometheus.GaugeFunc{},
171 active: map[connKind]int64{},
172}
173
174type connKind struct {
175 protocol string
176 listener string
177}
178
179type connections struct {
180 sync.Mutex
181 conns map[net.Conn]connKind
182 dones []chan struct{}
183 gauges map[connKind]prometheus.GaugeFunc
184
185 activeMutex sync.Mutex
186 active map[connKind]int64
187}
188
189// Register adds a connection for receiving an immediate i/o deadline on shutdown.
190// When the connection is closed, Unregister must be called to cancel the
191// registration.
192func (c *connections) Register(nc net.Conn, protocol, listener string) {
193 // This can happen, when a connection was initiated before a shutdown, but it
194 // doesn't hurt to log it.
195 select {
196 case <-Shutdown.Done():
197 pkglog.Error("new connection added while shutting down")
198 debug.PrintStack()
199 default:
200 }
201
202 ck := connKind{protocol, listener}
203
204 c.activeMutex.Lock()
205 c.active[ck]++
206 c.activeMutex.Unlock()
207
208 c.Lock()
209 defer c.Unlock()
210 c.conns[nc] = ck
211 if _, ok := c.gauges[ck]; !ok {
212 c.gauges[ck] = promauto.NewGaugeFunc(
213 prometheus.GaugeOpts{
214 Name: "mox_connections_count",
215 Help: "Open connections, per protocol/listener.",
216 ConstLabels: prometheus.Labels{
217 "protocol": protocol,
218 "listener": listener,
219 },
220 },
221 func() float64 {
222 c.activeMutex.Lock()
223 defer c.activeMutex.Unlock()
224 return float64(c.active[ck])
225 },
226 )
227 }
228}
229
230// Unregister removes a connection for shutdown.
231func (c *connections) Unregister(nc net.Conn) {
232 c.Lock()
233 defer c.Unlock()
234 ck := c.conns[nc]
235
236 defer func() {
237 c.activeMutex.Lock()
238 c.active[ck]--
239 c.activeMutex.Unlock()
240 }()
241
242 delete(c.conns, nc)
243 if len(c.conns) > 0 {
244 return
245 }
246 for _, done := range c.dones {
247 done <- struct{}{}
248 }
249 c.dones = nil
250}
251
252// Shutdown sets an immediate i/o deadline on all open registered sockets. Called
253// some time after mox shutdown is initiated.
254// The deadline will cause i/o's to be aborted, which should result in the
255// connection being unregistered.
256func (c *connections) Shutdown() {
257 now := time.Now()
258 c.Lock()
259 defer c.Unlock()
260 for nc := range c.conns {
261 if err := nc.SetDeadline(now); err != nil {
262 pkglog.Errorx("setting immediate read/write deadline for shutdown", err)
263 }
264 }
265}
266
267// Done returns a new channel on which a value is sent when no more sockets are
268// open, which could be immediate.
269func (c *connections) Done() chan struct{} {
270 c.Lock()
271 defer c.Unlock()
272 done := make(chan struct{}, 1)
273 if len(c.conns) == 0 {
274 done <- struct{}{}
275 return done
276 }
277 c.dones = append(c.dones, done)
278 return done
279}
280