-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.go
183 lines (157 loc) · 6.67 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
// Copyright © 2016,2019,2020 Pennock Tech, LLC.
// All rights reserved, except as granted under license.
// Licensed per file LICENSE.txt
// We are canonically imported from go.pennock.tech/fingerd but because we are
// not a library, we do not apply this as an import constraint on the package
// declarations. You can fork and build elsewhere more easily this way, while
// still getting dependencies without a dependency manager in play.
//
// This comment is just to let you know that the canonical import path is
// go.pennock.tech/fingerd and not now, nor ever, using DNS pointing to a
// code-hosting site not under our direct control. We keep our options open,
// for moving where we keep the code publicly available.
package main
import (
"flag"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/sirupsen/logrus"
)
// I initially set 64 and in my first tests hit this because of an 84K .pubkey
// Let's be a _little_ more generous; 256KB by default let's one use send up to
// very slightly more than three-quarters of a MB.
const defaultFileSizeLimit = 256 * 1024
var opts struct {
aliasfile string
listen string
listenEnv string
homesDir string
runAsUser string
pidFile string
fileSizeLimit int64
requestReadTimeout time.Duration
requestWriteTimeout time.Duration
minPasswdUID uint64
showVersion bool
}
func init() {
flag.StringVar(&opts.aliasfile, "alias-file", "/etc/finger.conf", "file to read aliases from (if it exists)")
flag.StringVar(&opts.homesDir, "homes-dir", "/home", "where end-user home-dirs live")
flag.StringVar(&opts.listen, "listen", ":79", "address-spec to listen for finger requests on")
flag.StringVar(&opts.listenEnv, "listen-env", "", "environment variable to use as -listen (takes precedence)")
flag.StringVar(&opts.runAsUser, "run-as-user", "", "if starting as root, setuid to this user")
flag.StringVar(&opts.pidFile, "pidfile", "", "write pid to this file after bind but before listening")
flag.DurationVar(&opts.requestReadTimeout, "request.timeout.read", 10*time.Second, "timeout for receiving the finger request")
flag.DurationVar(&opts.requestWriteTimeout, "request.timeout.write", 30*time.Second, "timeout for each write of the response")
flag.Int64Var(&opts.fileSizeLimit, "file.size-limit", defaultFileSizeLimit, "how large a file we will serve")
flag.Uint64Var(&opts.minPasswdUID, "passwd.min-uid", 0, "set non-zero to enable passwd lookups")
flag.BoolVar(&opts.showVersion, "version", false, "show version and exit")
// TODO: remove this in a future release
var listenTime time.Duration
flag.DurationVar(&listenTime, "listen.at-a-time", 0, "defunct and does nothing (will be removed in a future release)")
}
func main() {
flag.Parse()
if opts.showVersion {
version()
return
}
// q: should this really be one global waitgroup instead of per-AF and entirely encapsulate in the TCPFingerListener?
running := &sync.WaitGroup{}
running.Add(1)
shutdown := make(chan struct{})
logger := setupLogging()
masterThreadLogger := logrus.NewEntry(logger).WithFields(logrus.Fields{
"uid": os.Getuid(),
"gid": os.Getgid(),
"pid": os.Getpid(),
})
fullStatusLogger := masterThreadLogger.WithFields(logrus.Fields{
"argv": os.Args,
"version": currentVersion(),
"go": goVersion(),
})
haveListeners := make([]*TCPFingerListener, 0, 3)
if tmp, ok := inheritedListeners(running, shutdown, logger); ok {
masterThreadLogger.Infof("recovered %d listeners", len(tmp))
haveListeners = tmp
} else {
for _, netFamily := range []string{"tcp4", "tcp6"} {
fl, err := NewTCPFingerListener(netFamily, running, shutdown, logger)
if err != nil {
// It's not an error to fail to listen on just one family (eg,
// system which is missing IPv4) so only Warn level. If we got
// none at all, then we'll fatal out below, which will cover us.
masterThreadLogger.WithError(err).Warnf("failed to listen/%s", netFamily)
} else {
haveListeners = append(haveListeners, fl)
// start below, after dropping privs and loading aliases
}
}
}
running.Done()
if len(haveListeners) == 0 {
// avoid chewing CPU in a tight loop if we're being constantly respawned
time.Sleep(time.Second)
fullStatusLogger.Fatal("no listeners accepted; slept 1s before exiting")
}
if os.Getuid() == 0 {
masterThreadLogger.Info("running as root, need to drop privileges")
dropPrivileges(haveListeners, logger)
// only reach here if something has gone wrong; dropPrivileges _should_ re-exec us
time.Sleep(time.Second)
fullStatusLogger.Fatal("we must drop privileges when running as root")
}
// Set up signal handling as soon as we've dropped privs, even though we'll
// not act on it until late. NB: package Signal DOES NOT BLOCK writing
// to the channel, so it MUST be buffered. [caught by staticcheck]
signalShutdownCh := make(chan os.Signal, 1)
signal.Notify(signalShutdownCh, syscall.SIGTERM, syscall.SIGINT)
// We parse these _after_ dropping privileges, so the listening socket is open, but
// before we start the listening, so that the aliases are available without race.
if opts.aliasfile != "" {
// It's okay for the file to not exist. Also, if it doesn't exist but later comes into existence,
// we accept it at that point. A _missing_ file should not immediately blank data (might be a race
// between updates in a bad editor) so write an empty file first, before deleting it, if you want that.
//
// Because it's okay to not exist, we never actually fail setup and abort service. If we lose
// the ability to dynamically reload then that will be logged. It's thus in the audit trail and
// an acceptable degradation of service.
loadMappingData(logger)
scheduleAutoMappingDataReload(logger)
}
// Pidfile must be after bind, but before listening.
var weCreatedPidfile bool
if opts.pidFile != "" {
pf, err := os.Create(opts.pidFile)
if err != nil {
masterThreadLogger.WithError(err).WithField("pidfile", opts.pidFile).Info("unable to create pidfile")
} else {
fmt.Fprintf(pf, "%d\n", os.Getpid())
_ = pf.Close()
weCreatedPidfile = true
}
}
// From this point on, we're sufficiently init-like to pass muster.
go childReaper(logger)
// From this point on, we're accepting connection.
for _, fl := range haveListeners {
fl.GoServeThenClose()
}
fullStatusLogger.WithFields(logrus.Fields{
"listeners": len(haveListeners),
}).Info("running")
// Hang around forever, or until signalled
masterThreadLogger.WithField("signal", <-signalShutdownCh).Warn("shutdown signal received")
close(shutdown)
running.Wait()
if weCreatedPidfile {
_ = os.Remove(opts.pidFile)
}
masterThreadLogger.Info("exiting cleanly")
logrus.Exit(0)
}