You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
315 lines
11 KiB
315 lines
11 KiB
package serial
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
streamProtocolVersion = 1
|
|
maxStreamHandshakeBytes = 4 * 1024
|
|
defaultStreamSocketTimeout = 10 * time.Second
|
|
)
|
|
|
|
type streamLeaseRequest struct {
|
|
Version int `json:"version"`
|
|
Action string `json:"action"`
|
|
Channel string `json:"channel"`
|
|
ClientID string `json:"client_id"`
|
|
LeaseID string `json:"lease_id,omitempty"`
|
|
}
|
|
|
|
type streamLeaseResponse struct {
|
|
Version int `json:"version"`
|
|
OK bool `json:"ok"`
|
|
Channel string `json:"channel,omitempty"`
|
|
LeaseID string `json:"lease_id,omitempty"`
|
|
Stats *StreamSnapshot `json:"stats,omitempty"`
|
|
Code string `json:"code,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// StreamSocketServer exposes a separate data-plane socket. Unix socket mode
|
|
// and service-group ownership authenticate tera-sessiond; this protocol never
|
|
// accepts a raw /dev path.
|
|
type StreamSocketServer struct {
|
|
Manager *StreamLeaseManager
|
|
HandshakeTimeout time.Duration
|
|
}
|
|
|
|
func (s StreamSocketServer) Serve(listener net.Listener) error {
|
|
if s.Manager == nil {
|
|
return errors.New("serial stream lease manager 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 StreamSocketServer) handleConnection(connection net.Conn) {
|
|
defer connection.Close()
|
|
timeout := s.HandshakeTimeout
|
|
if timeout <= 0 {
|
|
timeout = defaultStreamSocketTimeout
|
|
}
|
|
_ = connection.SetDeadline(time.Now().Add(timeout))
|
|
reader := bufio.NewReader(io.LimitReader(connection, maxStreamHandshakeBytes+1))
|
|
payload, err := reader.ReadBytes('\n')
|
|
if err != nil || len(payload) > maxStreamHandshakeBytes || !bytes.HasSuffix(payload, []byte{'\n'}) {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "invalid serial stream handshake"})
|
|
return
|
|
}
|
|
if reader.Buffered() != 0 {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "serial bytes cannot precede lease acceptance"})
|
|
return
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
var request streamLeaseRequest
|
|
if err := decoder.Decode(&request); err != nil {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "invalid serial stream handshake"})
|
|
return
|
|
}
|
|
var extra any
|
|
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "serial stream handshake must contain one JSON value"})
|
|
return
|
|
}
|
|
if request.Version != streamProtocolVersion {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "serial stream protocol is not supported"})
|
|
return
|
|
}
|
|
if request.Action == "status" {
|
|
if request.Channel != "" || request.ClientID != "" || request.LeaseID != "" {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "serial stream status must not include a lease target"})
|
|
return
|
|
}
|
|
status := s.Manager.Snapshot()
|
|
s.writeResponse(connection, streamLeaseResponse{OK: true, Stats: &status})
|
|
return
|
|
}
|
|
if request.Action == "break" {
|
|
if request.Channel == "" || request.ClientID == "" || request.LeaseID == "" {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "serial stream break requires an active lease"})
|
|
return
|
|
}
|
|
if err := s.Manager.SendBreak(context.Background(), request.Channel, request.LeaseID, request.ClientID); err != nil {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "hardware_failed", Error: err.Error()})
|
|
return
|
|
}
|
|
s.writeResponse(connection, streamLeaseResponse{OK: true, Channel: request.Channel, LeaseID: request.LeaseID})
|
|
return
|
|
}
|
|
if request.Action != "lease" {
|
|
s.writeResponse(connection, streamLeaseResponse{Code: "validation_failed", Error: "serial stream action is not supported"})
|
|
return
|
|
}
|
|
lease, err := s.Manager.Acquire(context.Background(), request.Channel, request.ClientID)
|
|
if err != nil {
|
|
s.writeLeaseError(connection, err)
|
|
return
|
|
}
|
|
defer lease.Close()
|
|
if !s.writeResponse(connection, streamLeaseResponse{
|
|
Version: streamProtocolVersion, OK: true,
|
|
Channel: request.Channel, LeaseID: lease.LeaseID(),
|
|
}) {
|
|
return
|
|
}
|
|
_ = connection.SetDeadline(time.Time{})
|
|
|
|
done := make(chan struct{}, 2)
|
|
go func() {
|
|
_, _ = io.Copy(lease, connection)
|
|
done <- struct{}{}
|
|
}()
|
|
go func() {
|
|
_, _ = io.Copy(connection, lease)
|
|
done <- struct{}{}
|
|
}()
|
|
<-done
|
|
_ = lease.Close()
|
|
_ = connection.Close()
|
|
<-done
|
|
}
|
|
|
|
func (s StreamSocketServer) writeLeaseError(connection net.Conn, err error) {
|
|
response := streamLeaseResponse{Version: streamProtocolVersion, Code: "hardware_failed", Error: err.Error()}
|
|
switch {
|
|
case errors.Is(err, ErrStreamLeaseBusy):
|
|
response.Code = "lease_busy"
|
|
case IsUnavailable(err):
|
|
response.Code = "hardware_unavailable"
|
|
default:
|
|
response.Code = "validation_failed"
|
|
}
|
|
s.writeResponse(connection, response)
|
|
}
|
|
|
|
func (StreamSocketServer) writeResponse(connection net.Conn, response streamLeaseResponse) bool {
|
|
if response.Version == 0 {
|
|
response.Version = streamProtocolVersion
|
|
}
|
|
return json.NewEncoder(connection).Encode(response) == nil
|
|
}
|
|
|
|
// StreamSocketClient implements directstream.TransportFactory without
|
|
// exposing a device path outside tera-seriald.
|
|
type StreamSocketClient struct {
|
|
path string
|
|
clientID string
|
|
timeout time.Duration
|
|
}
|
|
|
|
func NewStreamSocketClient(path, clientID string) *StreamSocketClient {
|
|
if path == "" {
|
|
path = DefaultStreamSocketPath
|
|
}
|
|
if clientID == "" {
|
|
clientID = "tera-sessiond"
|
|
}
|
|
return &StreamSocketClient{path: path, clientID: clientID, timeout: defaultStreamSocketTimeout}
|
|
}
|
|
|
|
func (c *StreamSocketClient) Open(ctx context.Context, channel string) (io.ReadWriteCloser, error) {
|
|
dialer := net.Dialer{Timeout: c.timeout}
|
|
connection, err := dialer.DialContext(ctx, "unix", c.path)
|
|
if err != nil {
|
|
return nil, &UnavailableError{Reason: "tera-seriald stream socket is not reachable"}
|
|
}
|
|
fail := true
|
|
defer func() {
|
|
if fail {
|
|
_ = connection.Close()
|
|
}
|
|
}()
|
|
deadline := time.Now().Add(c.timeout)
|
|
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
|
|
deadline = contextDeadline
|
|
}
|
|
_ = connection.SetDeadline(deadline)
|
|
request := streamLeaseRequest{
|
|
Version: streamProtocolVersion, Action: "lease", Channel: channel, ClientID: c.clientID,
|
|
}
|
|
if err := json.NewEncoder(connection).Encode(request); err != nil {
|
|
return nil, errors.New("serial stream handshake failed")
|
|
}
|
|
reader := bufio.NewReader(connection)
|
|
payload, err := reader.ReadBytes('\n')
|
|
if err != nil || len(payload) > maxStreamHandshakeBytes {
|
|
return nil, &UnavailableError{Reason: "tera-seriald returned no stream lease"}
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(payload))
|
|
decoder.DisallowUnknownFields()
|
|
var response streamLeaseResponse
|
|
if err := decoder.Decode(&response); err != nil {
|
|
return nil, &UnavailableError{Reason: "tera-seriald returned an invalid stream lease"}
|
|
}
|
|
if response.Version != streamProtocolVersion {
|
|
return nil, errors.New("tera-seriald stream protocol version mismatch")
|
|
}
|
|
if !response.OK {
|
|
switch response.Code {
|
|
case "lease_busy":
|
|
return nil, fmt.Errorf("%w: %s", ErrStreamLeaseBusy, response.Error)
|
|
case "hardware_unavailable":
|
|
return nil, &UnavailableError{Reason: response.Error}
|
|
default:
|
|
return nil, errors.New(response.Error)
|
|
}
|
|
}
|
|
if response.Channel != channel || response.LeaseID == "" {
|
|
return nil, errors.New("tera-seriald returned a mismatched stream lease")
|
|
}
|
|
_ = connection.SetDeadline(time.Time{})
|
|
fail = false
|
|
return &streamSocketTransport{
|
|
Conn: connection, reader: reader, path: c.path, channel: channel,
|
|
clientID: c.clientID, leaseID: response.LeaseID, timeout: c.timeout,
|
|
}, nil
|
|
}
|
|
|
|
func (c *StreamSocketClient) Status(ctx context.Context) (StreamSnapshot, error) {
|
|
dialer := net.Dialer{Timeout: c.timeout}
|
|
connection, err := dialer.DialContext(ctx, "unix", c.path)
|
|
if err != nil {
|
|
return StreamSnapshot{}, &UnavailableError{Reason: "tera-seriald stream socket 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(streamLeaseRequest{Version: streamProtocolVersion, Action: "status"}); err != nil {
|
|
return StreamSnapshot{}, errors.New("serial stream status request failed")
|
|
}
|
|
var response streamLeaseResponse
|
|
if err := json.NewDecoder(io.LimitReader(connection, maxStreamHandshakeBytes)).Decode(&response); err != nil {
|
|
return StreamSnapshot{}, &UnavailableError{Reason: "tera-seriald returned no stream status"}
|
|
}
|
|
if response.Version != streamProtocolVersion || !response.OK || response.Stats == nil {
|
|
return StreamSnapshot{}, errors.New("tera-seriald returned an invalid stream status")
|
|
}
|
|
return *response.Stats, nil
|
|
}
|
|
|
|
type streamSocketTransport struct {
|
|
net.Conn
|
|
reader *bufio.Reader
|
|
path string
|
|
channel string
|
|
clientID string
|
|
leaseID string
|
|
timeout time.Duration
|
|
}
|
|
|
|
func (t *streamSocketTransport) Read(buffer []byte) (int, error) {
|
|
return t.reader.Read(buffer)
|
|
}
|
|
|
|
func (t *streamSocketTransport) SendBreak(ctx context.Context) error {
|
|
dialer := net.Dialer{Timeout: t.timeout}
|
|
connection, err := dialer.DialContext(ctx, "unix", t.path)
|
|
if err != nil {
|
|
return &UnavailableError{Reason: "tera-seriald stream socket is not reachable"}
|
|
}
|
|
defer connection.Close()
|
|
deadline := time.Now().Add(t.timeout)
|
|
if contextDeadline, ok := ctx.Deadline(); ok && contextDeadline.Before(deadline) {
|
|
deadline = contextDeadline
|
|
}
|
|
_ = connection.SetDeadline(deadline)
|
|
request := streamLeaseRequest{Version: streamProtocolVersion, Action: "break", Channel: t.channel, ClientID: t.clientID, LeaseID: t.leaseID}
|
|
if err := json.NewEncoder(connection).Encode(request); err != nil {
|
|
return errors.New("serial stream break request failed")
|
|
}
|
|
var response streamLeaseResponse
|
|
if err := json.NewDecoder(io.LimitReader(connection, maxStreamHandshakeBytes)).Decode(&response); err != nil {
|
|
return &UnavailableError{Reason: "tera-seriald returned no break response"}
|
|
}
|
|
if response.Version != streamProtocolVersion || !response.OK || response.Channel != t.channel || response.LeaseID != t.leaseID {
|
|
if response.Error != "" {
|
|
return errors.New(response.Error)
|
|
}
|
|
return errors.New("tera-seriald returned an invalid break response")
|
|
}
|
|
return nil
|
|
}
|
|
|