package serial import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "net" "time" ) const DefaultSocketPath = "/run/tera-seriald/serial-ports.sock" type socketRequest struct { Action string `json:"action"` Configs []ChannelConfig `json:"configs,omitempty"` Config *ChannelConfig `json:"config,omitempty"` Captures []CaptureConfig `json:"captures,omitempty"` } type socketResponse struct { States []State `json:"states,omitempty"` Links []LinkState `json:"links,omitempty"` CRProbes []CRProbeState `json:"cr_probes,omitempty"` Probe *ProbeResult `json:"probe,omitempty"` Code string `json:"code,omitempty"` Error string `json:"error,omitempty"` } const maxSocketRequestBytes = 16 * 1024 // SocketServer exposes the seriald-only controller on a local Unix socket. // It accepts only bounded state and line-configuration requests, never data. type SocketServer struct { Controller Controller Capture CaptureController CRProbe CRProbeStateController Links LinkStateController } func (s SocketServer) Serve(listener net.Listener) error { if s.Controller == nil { return errors.New("serial controller is required") } for { connection, err := listener.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return nil } if temporary, ok := err.(interface{ Temporary() bool }); ok && temporary.Temporary() { continue } return err } go s.handleConnection(connection) } } func (s SocketServer) handleConnection(connection net.Conn) { defer connection.Close() _ = connection.SetDeadline(time.Now().Add(10 * time.Second)) payload, err := bufio.NewReader(io.LimitReader(connection, maxSocketRequestBytes+1)).ReadBytes('\n') if err != nil || len(payload) > maxSocketRequestBytes || !bytes.HasSuffix(payload, []byte{'\n'}) { s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "invalid serial control request"}) return } decoder := json.NewDecoder(bytes.NewReader(payload)) decoder.DisallowUnknownFields() var command socketRequest if err := decoder.Decode(&command); err != nil { s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "invalid serial control request"}) return } var extra any if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) { s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "serial control request must contain a single JSON value"}) return } ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() switch command.Action { case "states": states, err := s.Controller.States(ctx) if err != nil { s.writeError(connection, err) return } s.writeResponse(connection, socketResponse{States: states}) case "link_states": controller := s.Links if controller == nil { controller, _ = s.Controller.(LinkStateController) } if controller == nil { s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial modem status is unavailable"}) return } links, err := controller.LinkStates(ctx) if err != nil { s.writeError(connection, err) return } s.writeResponse(connection, socketResponse{Links: links}) case "cr_probe_states": if s.CRProbe == nil { s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial CR response probe is unavailable"}) return } states, err := s.CRProbe.CRProbeStates(ctx) if err != nil { s.writeError(connection, err) return } s.writeResponse(connection, socketResponse{CRProbes: states}) case "apply": states, err := s.Controller.Apply(ctx, command.Configs) if err != nil { s.writeError(connection, err) return } s.writeResponse(connection, socketResponse{States: states}) case "apply_capture": if s.Capture == nil { s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial payload capture control is unavailable"}) return } if err := s.Capture.ApplyCapture(ctx, command.Captures); err != nil { s.writeError(connection, err) return } s.writeResponse(connection, socketResponse{}) case "probe": if command.Config == nil { s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "serial line probe requires one config"}) return } prober, ok := s.Controller.(interface { Probe(context.Context, ChannelConfig) (ProbeResult, error) }) if !ok { s.writeResponse(connection, socketResponse{Code: "hardware_unavailable", Error: "serial line probe is unavailable"}) return } result, err := prober.Probe(ctx, *command.Config) if err != nil { s.writeError(connection, err) return } s.writeResponse(connection, socketResponse{Probe: &result}) default: s.writeResponse(connection, socketResponse{Code: "validation_failed", Error: "serial control action is not supported"}) } } func (s SocketServer) writeError(connection net.Conn, err error) { code := "hardware_failed" if errors.Is(err, ErrStreamLeaseBusy) { code = "stream_lease_busy" } else if IsUnavailable(err) { code = "hardware_unavailable" } s.writeResponse(connection, socketResponse{Code: code, Error: err.Error()}) } func (s SocketServer) writeResponse(connection net.Conn, value socketResponse) { _ = json.NewEncoder(connection).Encode(value) } // SocketClient implements Controller for the unprivileged tera-api process. // Unix-socket permissions are the boundary between the API and seriald. type SocketClient struct { path string timeout time.Duration } func NewSocketClient(path string) *SocketClient { if path == "" { path = DefaultSocketPath } return &SocketClient{path: path, timeout: 10 * time.Second} } func (c *SocketClient) States(ctx context.Context) ([]State, error) { response, err := c.exchange(ctx, socketRequest{Action: "states"}) if err != nil { return nil, err } return response.States, nil } func (c *SocketClient) LinkStates(ctx context.Context) ([]LinkState, error) { response, err := c.exchange(ctx, socketRequest{Action: "link_states"}) if err != nil { return nil, err } return response.Links, nil } func (c *SocketClient) CRProbeStates(ctx context.Context) ([]CRProbeState, error) { response, err := c.exchange(ctx, socketRequest{Action: "cr_probe_states"}) if err != nil { return nil, err } return response.CRProbes, nil } func (c *SocketClient) Apply(ctx context.Context, configs []ChannelConfig) ([]State, error) { response, err := c.exchange(ctx, socketRequest{Action: "apply", Configs: configs}) if err != nil { return nil, err } return response.States, nil } func (c *SocketClient) ApplyCapture(ctx context.Context, configs []CaptureConfig) error { _, err := c.exchange(ctx, socketRequest{Action: "apply_capture", Captures: configs}) return err } func (c *SocketClient) Probe(ctx context.Context, config ChannelConfig) (ProbeResult, error) { response, err := c.exchange(ctx, socketRequest{Action: "probe", Config: &config}) if err != nil { return ProbeResult{}, err } if response.Probe == nil { return ProbeResult{}, errors.New("tera-seriald returned no probe result") } return *response.Probe, nil } func (c *SocketClient) exchange(ctx context.Context, command socketRequest) (socketResponse, error) { dialer := net.Dialer{Timeout: c.timeout} connection, err := dialer.DialContext(ctx, "unix", c.path) if err != nil { return socketResponse{}, &UnavailableError{Reason: "tera-seriald is not reachable"} } defer connection.Close() deadline := time.Now().Add(c.timeout) if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) { deadline = contextDeadline } _ = connection.SetDeadline(deadline) if err := json.NewEncoder(connection).Encode(command); err != nil { return socketResponse{}, fmt.Errorf("serial control request failed") } var response socketResponse if err := json.NewDecoder(io.LimitReader(connection, 65536)).Decode(&response); err != nil { return socketResponse{}, &UnavailableError{Reason: "tera-seriald returned no response"} } if response.Error != "" { if response.Code == "hardware_unavailable" { return socketResponse{}, &UnavailableError{Reason: response.Error} } // The exact-message fallback keeps a newer API compatible with a // seriald that predates the stream_lease_busy protocol code. if response.Code == "stream_lease_busy" || response.Error == ErrStreamLeaseBusy.Error() { return socketResponse{}, ErrStreamLeaseBusy } return socketResponse{}, errors.New(response.Error) } return response, nil }