]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package plugin |
2 | ||
3 | import ( | |
15c0b25d | 4 | "crypto/tls" |
107c1cdb | 5 | "crypto/x509" |
15c0b25d | 6 | "encoding/base64" |
bae9f6d2 JC |
7 | "errors" |
8 | "fmt" | |
9 | "io/ioutil" | |
10 | "log" | |
11 | "net" | |
12 | "os" | |
13 | "os/signal" | |
14 | "runtime" | |
107c1cdb | 15 | "sort" |
bae9f6d2 | 16 | "strconv" |
107c1cdb | 17 | "strings" |
bae9f6d2 | 18 | "sync/atomic" |
15c0b25d AP |
19 | |
20 | "github.com/hashicorp/go-hclog" | |
21 | ||
22 | "google.golang.org/grpc" | |
bae9f6d2 JC |
23 | ) |
24 | ||
25 | // CoreProtocolVersion is the ProtocolVersion of the plugin system itself. | |
26 | // We will increment this whenever we change any protocol behavior. This | |
27 | // will invalidate any prior plugins but will at least allow us to iterate | |
28 | // on the core in a safe way. We will do our best to do this very | |
29 | // infrequently. | |
30 | const CoreProtocolVersion = 1 | |
31 | ||
32 | // HandshakeConfig is the configuration used by client and servers to | |
33 | // handshake before starting a plugin connection. This is embedded by | |
34 | // both ServeConfig and ClientConfig. | |
35 | // | |
36 | // In practice, the plugin host creates a HandshakeConfig that is exported | |
37 | // and plugins then can easily consume it. | |
38 | type HandshakeConfig struct { | |
39 | // ProtocolVersion is the version that clients must match on to | |
40 | // agree they can communicate. This should match the ProtocolVersion | |
41 | // set on ClientConfig when using a plugin. | |
107c1cdb ND |
42 | // This field is not required if VersionedPlugins are being used in the |
43 | // Client or Server configurations. | |
bae9f6d2 JC |
44 | ProtocolVersion uint |
45 | ||
46 | // MagicCookieKey and value are used as a very basic verification | |
47 | // that a plugin is intended to be launched. This is not a security | |
48 | // measure, just a UX feature. If the magic cookie doesn't match, | |
49 | // we show human-friendly output. | |
50 | MagicCookieKey string | |
51 | MagicCookieValue string | |
52 | } | |
53 | ||
107c1cdb ND |
54 | // PluginSet is a set of plugins provided to be registered in the plugin |
55 | // server. | |
56 | type PluginSet map[string]Plugin | |
57 | ||
bae9f6d2 JC |
58 | // ServeConfig configures what sorts of plugins are served. |
59 | type ServeConfig struct { | |
60 | // HandshakeConfig is the configuration that must match clients. | |
61 | HandshakeConfig | |
62 | ||
15c0b25d AP |
63 | // TLSProvider is a function that returns a configured tls.Config. |
64 | TLSProvider func() (*tls.Config, error) | |
65 | ||
bae9f6d2 | 66 | // Plugins are the plugins that are served. |
107c1cdb ND |
67 | // The implied version of this PluginSet is the Handshake.ProtocolVersion. |
68 | Plugins PluginSet | |
69 | ||
70 | // VersionedPlugins is a map of PluginSets for specific protocol versions. | |
71 | // These can be used to negotiate a compatible version between client and | |
72 | // server. If this is set, Handshake.ProtocolVersion is not required. | |
73 | VersionedPlugins map[int]PluginSet | |
15c0b25d AP |
74 | |
75 | // GRPCServer should be non-nil to enable serving the plugins over | |
76 | // gRPC. This is a function to create the server when needed with the | |
77 | // given server options. The server options populated by go-plugin will | |
78 | // be for TLS if set. You may modify the input slice. | |
79 | // | |
80 | // Note that the grpc.Server will automatically be registered with | |
81 | // the gRPC health checking service. This is not optional since go-plugin | |
82 | // relies on this to implement Ping(). | |
83 | GRPCServer func([]grpc.ServerOption) *grpc.Server | |
84 | ||
85 | // Logger is used to pass a logger into the server. If none is provided the | |
86 | // server will create a default logger. | |
87 | Logger hclog.Logger | |
88 | } | |
89 | ||
107c1cdb ND |
90 | // protocolVersion determines the protocol version and plugin set to be used by |
91 | // the server. In the event that there is no suitable version, the last version | |
92 | // in the config is returned leaving the client to report the incompatibility. | |
93 | func protocolVersion(opts *ServeConfig) (int, Protocol, PluginSet) { | |
94 | protoVersion := int(opts.ProtocolVersion) | |
95 | pluginSet := opts.Plugins | |
96 | protoType := ProtocolNetRPC | |
97 | // Check if the client sent a list of acceptable versions | |
98 | var clientVersions []int | |
99 | if vs := os.Getenv("PLUGIN_PROTOCOL_VERSIONS"); vs != "" { | |
100 | for _, s := range strings.Split(vs, ",") { | |
101 | v, err := strconv.Atoi(s) | |
102 | if err != nil { | |
103 | fmt.Fprintf(os.Stderr, "server sent invalid plugin version %q", s) | |
104 | continue | |
105 | } | |
106 | clientVersions = append(clientVersions, v) | |
107 | } | |
108 | } | |
109 | ||
110 | // We want to iterate in reverse order, to ensure we match the newest | |
111 | // compatible plugin version. | |
112 | sort.Sort(sort.Reverse(sort.IntSlice(clientVersions))) | |
113 | ||
114 | // set the old un-versioned fields as if they were versioned plugins | |
115 | if opts.VersionedPlugins == nil { | |
116 | opts.VersionedPlugins = make(map[int]PluginSet) | |
117 | } | |
118 | ||
119 | if pluginSet != nil { | |
120 | opts.VersionedPlugins[protoVersion] = pluginSet | |
121 | } | |
122 | ||
123 | // Sort the version to make sure we match the latest first | |
124 | var versions []int | |
125 | for v := range opts.VersionedPlugins { | |
126 | versions = append(versions, v) | |
127 | } | |
128 | ||
129 | sort.Sort(sort.Reverse(sort.IntSlice(versions))) | |
130 | ||
131 | // See if we have multiple versions of Plugins to choose from | |
132 | for _, version := range versions { | |
133 | // Record each version, since we guarantee that this returns valid | |
134 | // values even if they are not a protocol match. | |
135 | protoVersion = version | |
136 | pluginSet = opts.VersionedPlugins[version] | |
137 | ||
138 | // If we have a configured gRPC server we should select a protocol | |
139 | if opts.GRPCServer != nil { | |
140 | // All plugins in a set must use the same transport, so check the first | |
141 | // for the protocol type | |
142 | for _, p := range pluginSet { | |
143 | switch p.(type) { | |
144 | case GRPCPlugin: | |
145 | protoType = ProtocolGRPC | |
146 | default: | |
147 | protoType = ProtocolNetRPC | |
148 | } | |
149 | break | |
150 | } | |
151 | } | |
152 | ||
153 | for _, clientVersion := range clientVersions { | |
154 | if clientVersion == protoVersion { | |
155 | return protoVersion, protoType, pluginSet | |
156 | } | |
157 | } | |
15c0b25d AP |
158 | } |
159 | ||
107c1cdb ND |
160 | // Return the lowest version as the fallback. |
161 | // Since we iterated over all the versions in reverse order above, these | |
162 | // values are from the lowest version number plugins (which may be from | |
163 | // a combination of the Handshake.ProtocolVersion and ServeConfig.Plugins | |
164 | // fields). This allows serving the oldest version of our plugins to a | |
165 | // legacy client that did not send a PLUGIN_PROTOCOL_VERSIONS list. | |
166 | return protoVersion, protoType, pluginSet | |
bae9f6d2 JC |
167 | } |
168 | ||
169 | // Serve serves the plugins given by ServeConfig. | |
170 | // | |
171 | // Serve doesn't return until the plugin is done being executed. Any | |
15c0b25d | 172 | // errors will be outputted to os.Stderr. |
bae9f6d2 JC |
173 | // |
174 | // This is the method that plugins should call in their main() functions. | |
175 | func Serve(opts *ServeConfig) { | |
176 | // Validate the handshake config | |
177 | if opts.MagicCookieKey == "" || opts.MagicCookieValue == "" { | |
178 | fmt.Fprintf(os.Stderr, | |
179 | "Misconfigured ServeConfig given to serve this plugin: no magic cookie\n"+ | |
180 | "key or value was set. Please notify the plugin author and report\n"+ | |
181 | "this as a bug.\n") | |
182 | os.Exit(1) | |
183 | } | |
184 | ||
185 | // First check the cookie | |
186 | if os.Getenv(opts.MagicCookieKey) != opts.MagicCookieValue { | |
187 | fmt.Fprintf(os.Stderr, | |
188 | "This binary is a plugin. These are not meant to be executed directly.\n"+ | |
189 | "Please execute the program that consumes these plugins, which will\n"+ | |
190 | "load any plugins automatically\n") | |
191 | os.Exit(1) | |
192 | } | |
193 | ||
107c1cdb ND |
194 | // negotiate the version and plugins |
195 | // start with default version in the handshake config | |
196 | protoVersion, protoType, pluginSet := protocolVersion(opts) | |
197 | ||
bae9f6d2 JC |
198 | // Logging goes to the original stderr |
199 | log.SetOutput(os.Stderr) | |
200 | ||
15c0b25d AP |
201 | logger := opts.Logger |
202 | if logger == nil { | |
203 | // internal logger to os.Stderr | |
204 | logger = hclog.New(&hclog.LoggerOptions{ | |
205 | Level: hclog.Trace, | |
206 | Output: os.Stderr, | |
207 | JSONFormat: true, | |
208 | }) | |
209 | } | |
210 | ||
bae9f6d2 JC |
211 | // Create our new stdout, stderr files. These will override our built-in |
212 | // stdout/stderr so that it works across the stream boundary. | |
213 | stdout_r, stdout_w, err := os.Pipe() | |
214 | if err != nil { | |
215 | fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) | |
216 | os.Exit(1) | |
217 | } | |
218 | stderr_r, stderr_w, err := os.Pipe() | |
219 | if err != nil { | |
220 | fmt.Fprintf(os.Stderr, "Error preparing plugin: %s\n", err) | |
221 | os.Exit(1) | |
222 | } | |
223 | ||
224 | // Register a listener so we can accept a connection | |
225 | listener, err := serverListener() | |
226 | if err != nil { | |
15c0b25d | 227 | logger.Error("plugin init error", "error", err) |
bae9f6d2 JC |
228 | return |
229 | } | |
15c0b25d AP |
230 | |
231 | // Close the listener on return. We wrap this in a func() on purpose | |
232 | // because the "listener" reference may change to TLS. | |
233 | defer func() { | |
234 | listener.Close() | |
235 | }() | |
236 | ||
237 | var tlsConfig *tls.Config | |
238 | if opts.TLSProvider != nil { | |
239 | tlsConfig, err = opts.TLSProvider() | |
240 | if err != nil { | |
241 | logger.Error("plugin tls init", "error", err) | |
242 | return | |
243 | } | |
244 | } | |
bae9f6d2 | 245 | |
107c1cdb ND |
246 | var serverCert string |
247 | clientCert := os.Getenv("PLUGIN_CLIENT_CERT") | |
248 | // If the client is configured using AutoMTLS, the certificate will be here, | |
249 | // and we need to generate our own in response. | |
250 | if tlsConfig == nil && clientCert != "" { | |
251 | logger.Info("configuring server automatic mTLS") | |
252 | clientCertPool := x509.NewCertPool() | |
253 | if !clientCertPool.AppendCertsFromPEM([]byte(clientCert)) { | |
254 | logger.Error("client cert provided but failed to parse", "cert", clientCert) | |
255 | } | |
256 | ||
257 | certPEM, keyPEM, err := generateCert() | |
258 | if err != nil { | |
259 | logger.Error("failed to generate client certificate", "error", err) | |
260 | panic(err) | |
261 | } | |
262 | ||
263 | cert, err := tls.X509KeyPair(certPEM, keyPEM) | |
264 | if err != nil { | |
265 | logger.Error("failed to parse client certificate", "error", err) | |
266 | panic(err) | |
267 | } | |
268 | ||
269 | tlsConfig = &tls.Config{ | |
270 | Certificates: []tls.Certificate{cert}, | |
271 | ClientAuth: tls.RequireAndVerifyClientCert, | |
272 | ClientCAs: clientCertPool, | |
273 | MinVersion: tls.VersionTLS12, | |
274 | } | |
275 | ||
276 | // We send back the raw leaf cert data for the client rather than the | |
277 | // PEM, since the protocol can't handle newlines. | |
278 | serverCert = base64.RawStdEncoding.EncodeToString(cert.Certificate[0]) | |
279 | } | |
280 | ||
bae9f6d2 JC |
281 | // Create the channel to tell us when we're done |
282 | doneCh := make(chan struct{}) | |
283 | ||
15c0b25d AP |
284 | // Build the server type |
285 | var server ServerProtocol | |
107c1cdb | 286 | switch protoType { |
15c0b25d AP |
287 | case ProtocolNetRPC: |
288 | // If we have a TLS configuration then we wrap the listener | |
289 | // ourselves and do it at that level. | |
290 | if tlsConfig != nil { | |
291 | listener = tls.NewListener(listener, tlsConfig) | |
292 | } | |
293 | ||
294 | // Create the RPC server to dispense | |
295 | server = &RPCServer{ | |
107c1cdb | 296 | Plugins: pluginSet, |
15c0b25d AP |
297 | Stdout: stdout_r, |
298 | Stderr: stderr_r, | |
299 | DoneCh: doneCh, | |
300 | } | |
301 | ||
302 | case ProtocolGRPC: | |
303 | // Create the gRPC server | |
304 | server = &GRPCServer{ | |
107c1cdb | 305 | Plugins: pluginSet, |
15c0b25d AP |
306 | Server: opts.GRPCServer, |
307 | TLS: tlsConfig, | |
308 | Stdout: stdout_r, | |
309 | Stderr: stderr_r, | |
310 | DoneCh: doneCh, | |
107c1cdb | 311 | logger: logger, |
15c0b25d AP |
312 | } |
313 | ||
314 | default: | |
107c1cdb | 315 | panic("unknown server protocol: " + protoType) |
bae9f6d2 JC |
316 | } |
317 | ||
15c0b25d AP |
318 | // Initialize the servers |
319 | if err := server.Init(); err != nil { | |
320 | logger.Error("protocol init", "error", err) | |
321 | return | |
322 | } | |
323 | ||
15c0b25d AP |
324 | logger.Debug("plugin address", "network", listener.Addr().Network(), "address", listener.Addr().String()) |
325 | ||
107c1cdb ND |
326 | // Output the address and service name to stdout so that the client can bring it up. |
327 | fmt.Printf("%d|%d|%s|%s|%s|%s\n", | |
bae9f6d2 | 328 | CoreProtocolVersion, |
107c1cdb | 329 | protoVersion, |
bae9f6d2 | 330 | listener.Addr().Network(), |
15c0b25d | 331 | listener.Addr().String(), |
107c1cdb ND |
332 | protoType, |
333 | serverCert) | |
bae9f6d2 JC |
334 | os.Stdout.Sync() |
335 | ||
336 | // Eat the interrupts | |
337 | ch := make(chan os.Signal, 1) | |
338 | signal.Notify(ch, os.Interrupt) | |
339 | go func() { | |
340 | var count int32 = 0 | |
341 | for { | |
342 | <-ch | |
343 | newCount := atomic.AddInt32(&count, 1) | |
15c0b25d | 344 | logger.Debug("plugin received interrupt signal, ignoring", "count", newCount) |
bae9f6d2 JC |
345 | } |
346 | }() | |
347 | ||
348 | // Set our new out, err | |
349 | os.Stdout = stdout_w | |
350 | os.Stderr = stderr_w | |
351 | ||
15c0b25d AP |
352 | // Accept connections and wait for completion |
353 | go server.Serve(listener) | |
bae9f6d2 JC |
354 | <-doneCh |
355 | } | |
356 | ||
357 | func serverListener() (net.Listener, error) { | |
358 | if runtime.GOOS == "windows" { | |
359 | return serverListener_tcp() | |
360 | } | |
361 | ||
362 | return serverListener_unix() | |
363 | } | |
364 | ||
365 | func serverListener_tcp() (net.Listener, error) { | |
366 | minPort, err := strconv.ParseInt(os.Getenv("PLUGIN_MIN_PORT"), 10, 32) | |
367 | if err != nil { | |
368 | return nil, err | |
369 | } | |
370 | ||
371 | maxPort, err := strconv.ParseInt(os.Getenv("PLUGIN_MAX_PORT"), 10, 32) | |
372 | if err != nil { | |
373 | return nil, err | |
374 | } | |
375 | ||
376 | for port := minPort; port <= maxPort; port++ { | |
377 | address := fmt.Sprintf("127.0.0.1:%d", port) | |
378 | listener, err := net.Listen("tcp", address) | |
379 | if err == nil { | |
380 | return listener, nil | |
381 | } | |
382 | } | |
383 | ||
384 | return nil, errors.New("Couldn't bind plugin TCP listener") | |
385 | } | |
386 | ||
387 | func serverListener_unix() (net.Listener, error) { | |
388 | tf, err := ioutil.TempFile("", "plugin") | |
389 | if err != nil { | |
390 | return nil, err | |
391 | } | |
392 | path := tf.Name() | |
393 | ||
394 | // Close the file and remove it because it has to not exist for | |
395 | // the domain socket. | |
396 | if err := tf.Close(); err != nil { | |
397 | return nil, err | |
398 | } | |
399 | if err := os.Remove(path); err != nil { | |
400 | return nil, err | |
401 | } | |
402 | ||
403 | l, err := net.Listen("unix", path) | |
404 | if err != nil { | |
405 | return nil, err | |
406 | } | |
407 | ||
408 | // Wrap the listener in rmListener so that the Unix domain socket file | |
409 | // is removed on close. | |
410 | return &rmListener{ | |
411 | Listener: l, | |
412 | Path: path, | |
413 | }, nil | |
414 | } | |
415 | ||
416 | // rmListener is an implementation of net.Listener that forwards most | |
417 | // calls to the listener but also removes a file as part of the close. We | |
418 | // use this to cleanup the unix domain socket on close. | |
419 | type rmListener struct { | |
420 | net.Listener | |
421 | Path string | |
422 | } | |
423 | ||
424 | func (l *rmListener) Close() error { | |
425 | // Close the listener itself | |
426 | if err := l.Listener.Close(); err != nil { | |
427 | return err | |
428 | } | |
429 | ||
430 | // Remove the file | |
431 | return os.Remove(l.Path) | |
432 | } |