| // Copyright 2024 Google LLC | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| /* | |
| * This file contains the details of command's internal communication protocol | |
| * listener. Most callers should not need to call anything in this file. The | |
| * command handler and caller API is contained in command.go. | |
| */ | |
| package command | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "net" | |
| "os" | |
| "reflect" | |
| "regexp" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| "time" | |
| "github.com/GoogleCloudPlatform/galog" | |
| "github.com/GoogleCloudPlatform/google-guest-agent/internal/cfg" | |
| ) | |
| const ( | |
| // Fallback constants should be the same as the default values in the cfg | |
| // package. | |
| fallbackTimeout = time.Duration(10) * time.Second | |
| fallbackPipeMode = 0770 | |
| ) | |
| // cmdMonitor holds the registered command handlers. It does not hold the | |
| // server listener. | |
| var cmdMonitor *Monitor = &Monitor{ | |
| handlers: make(map[string]Handler), | |
| } | |
| func parseTimeoutFromCfg() time.Duration { | |
| timeout, err := time.ParseDuration(cfg.Retrieve().Unstable.CommandRequestTimeout) | |
| if err != nil { | |
| galog.Errorf("commmand request timeout configuration is not a valid duration string, falling back to default 10s timeout") | |
| return fallbackTimeout | |
| } | |
| return timeout | |
| } | |
| func parsePipemodeFromCfg() int { | |
| pipemode, err := strconv.ParseInt(cfg.Retrieve().Unstable.CommandPipeMode, 8, 32) | |
| if err != nil { | |
| galog.Errorf("could not parse command_pipe_mode as octal integer: %v falling back to default mode 0770", err) | |
| return fallbackPipeMode | |
| } | |
| return int(pipemode) | |
| } | |
| // Setup starts an internally managed command server. The agent configuration | |
| // will decide the server options. | |
| func Setup(ctx context.Context, listener KnownListeners) error { | |
| // Only setup command monitor if it is enabled in the configuration file. | |
| if !cfg.Retrieve().Unstable.CommandMonitorEnabled { | |
| galog.Debug("Command monitor is disabled in the configuration file, skipping setup") | |
| return nil | |
| } | |
| galog.Debugf("Setting up command monitor for %v", listener) | |
| if cmdMonitor.srv != nil { | |
| return fmt.Errorf("command monitor is already running") | |
| } | |
| if err := cmdMonitor.RegisterHandler(getOptionCommand, getOption); err != nil { | |
| galog.Errorf("Could not register command handler for %s: %v", getOptionCommand, err) | |
| } | |
| timeout := parseTimeoutFromCfg() | |
| pipemode := parsePipemodeFromCfg() | |
| cmdMonitor.srv = &Server{ | |
| pipe: PipeName(listener), | |
| pipeMode: int(pipemode), | |
| pipeGroup: cfg.Retrieve().Unstable.CommandPipeGroup, | |
| timeout: timeout, | |
| monitor: cmdMonitor, | |
| } | |
| err := cmdMonitor.srv.start(ctx) | |
| if err == nil { | |
| galog.Debugf("Successfully started command monitor for %v", listener) | |
| } | |
| return err | |
| } | |
| // Close will close the internally managed command server, if it was | |
| // initialized. | |
| func Close(_ context.Context) { | |
| if err := cmdMonitor.UnregisterHandler(getOptionCommand); err != nil { | |
| galog.Errorf("Could not unregister command handler for %s: %v", getOptionCommand, err) | |
| } | |
| if cmdMonitor.srv != nil { | |
| if err := cmdMonitor.srv.Close(); err != nil { | |
| galog.Errorf("error closing command-monitor: %v", err) | |
| } | |
| cmdMonitor.srv = nil | |
| } | |
| galog.Debug("Command monitor closed") | |
| } | |
| // Monitor is the structure which handles command registration and | |
| // deregistration. | |
| type Monitor struct { | |
| srv *Server | |
| handlersMu sync.RWMutex | |
| handlers map[string]Handler | |
| } | |
| // Close stops the server from listening to commands. | |
| func (m *Monitor) Close() error { return m.srv.Close() } | |
| // Start begins listening for commands. | |
| func (m *Monitor) Start(ctx context.Context) error { return m.srv.start(ctx) } | |
| // Server is the server structure which will listen for command requests and | |
| // route them to handlers. Most callers should not interact with this directly. | |
| type Server struct { | |
| pipe string | |
| pipeMode int | |
| pipeGroup string | |
| timeout time.Duration | |
| srv net.Listener | |
| monitor *Monitor | |
| } | |
| // Close signals the server to stop listening for commands and stop waiting to | |
| // listen. | |
| func (c *Server) Close() error { | |
| if c.srv != nil { | |
| return c.srv.Close() | |
| } | |
| return nil | |
| } | |
| func readOrError(c net.Conn) ([]byte, bool) { | |
| b := make([]byte, 1024) | |
| n, err := c.Read(b) | |
| if err == nil { | |
| return b[:n], true | |
| } | |
| if errors.Is(err, os.ErrDeadlineExceeded) { | |
| if e, err := json.Marshal(TimeoutError); err == nil { | |
| c.Write(e) | |
| return nil, false | |
| } | |
| } else { | |
| if e, err := json.Marshal(ConnError); err == nil { | |
| c.Write(e) | |
| return nil, false | |
| } | |
| } | |
| c.Write(internalError) | |
| return nil, false | |
| } | |
| func (c *Server) start(ctx context.Context) error { | |
| if c.srv != nil { | |
| return errors.New("server already listening") | |
| } | |
| galog.Debugf("Starting command server at %q", c.pipe) | |
| srv, err := listen(ctx, c.pipe, c.pipeMode, c.pipeGroup) | |
| if err != nil { | |
| return err | |
| } | |
| go func() { | |
| defer srv.Close() | |
| for { | |
| if ctx.Err() != nil { | |
| return | |
| } | |
| conn, err := srv.Accept() | |
| if err != nil { | |
| if errors.Is(err, net.ErrClosed) { | |
| break | |
| } | |
| galog.Errorf("error on connection to pipe %s: %v", c.pipe, err) | |
| continue | |
| } | |
| go func(conn net.Conn) { | |
| defer conn.Close() | |
| // Go has lots of helpers to read json from an io.Reader but none of | |
| // them return the byte slice afterwards and we need it for the handler. | |
| deadline := time.Now().Add(c.timeout) | |
| if err := conn.SetDeadline(deadline); err != nil { | |
| galog.Errorf("could not set deadline on command request: %v", err) | |
| return | |
| } | |
| message, ok := readOrError(conn) | |
| if !ok { | |
| return | |
| } | |
| var req Request | |
| for err := json.Unmarshal(message, &req); err != nil; err = json.Unmarshal(message, &req) { | |
| b, ok := readOrError(conn) | |
| if !ok { | |
| return | |
| } | |
| message = append(message, b...) | |
| } | |
| galog.V(1).Debugf("Received command request: %s", req.Command) | |
| c.monitor.handlersMu.RLock() | |
| defer c.monitor.handlersMu.RUnlock() | |
| handler, ok := c.monitor.handlers[req.Command] | |
| if !ok { | |
| if b, err := json.Marshal(CmdNotFoundError); err != nil { | |
| conn.Write(internalError) | |
| } else { | |
| conn.Write(b) | |
| } | |
| return | |
| } | |
| resp, err := handler(ctx, message) | |
| if err != nil { | |
| re := Response{Status: HandlerError.Status, StatusMessage: err.Error()} | |
| if b, err := json.Marshal(re); err != nil { | |
| resp = internalError | |
| } else { | |
| resp = b | |
| } | |
| } | |
| galog.V(1).Debugf("Sending response for request %s: %s", req.Command, string(resp)) | |
| conn.Write(resp) | |
| }(conn) | |
| } | |
| }() | |
| c.srv = srv | |
| return nil | |
| } | |
| // getOptionRequest is a request to get a config value by name. Guest agent | |
| // code should use the cfg package, this is intended for use outside of the | |
| // agent by the guest environment. | |
| type getOptionRequest struct { | |
| Request | |
| Option string | |
| } | |
| // getOptionResponse is a response to a getOptionRequest with the config value. | |
| type getOptionResponse struct { | |
| Response | |
| Option string | |
| Value string | |
| } | |
| // getOptionCommand is the command name for getOption. | |
| const getOptionCommand = "agent.config.getoption" | |
| var validKey = regexp.MustCompile("^[A-Z][a-zA-Z]+$") | |
| // getOption processes getOptionRequests encoded as json coming from the | |
| // command monitor. | |
| func getOption(_ context.Context, b []byte) ([]byte, error) { | |
| var req getOptionRequest | |
| if err := json.Unmarshal(b, &req); err != nil { | |
| return nil, err | |
| } | |
| var resp getOptionResponse | |
| resp.Option = req.Option | |
| if req.Option == "" { | |
| resp.Status = 2 | |
| resp.StatusMessage = "No option specified" | |
| return json.Marshal(resp) | |
| } | |
| var opt any | |
| opt = cfg.Retrieve() | |
| for _, k := range strings.Split(resp.Option, ".") { | |
| if !validKey.MatchString(k) { | |
| resp.Status = 3 | |
| resp.StatusMessage = "Invalid option, key names must start with uppercase" | |
| return json.Marshal(resp) | |
| } | |
| field := reflect.Indirect(reflect.ValueOf(opt)).FieldByName(k) | |
| if !field.IsValid() { | |
| resp.Status = 1 | |
| resp.StatusMessage = "Option does not exist" | |
| return json.Marshal(resp) | |
| } | |
| opt = field.Interface() | |
| } | |
| resp.Value = fmt.Sprintf("%v", opt) | |
| return json.Marshal(resp) | |
| } |