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.
 
 
 
 
 
 

199 lines
6.1 KiB

// Package netsocket exposes the narrow group-restricted tera-netd protocol.
package netsocket
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"time"
"tera-clone/services/tera-api/internal/networkconfig"
)
const DefaultSocketPath = "/run/tera-netd/control.sock"
type request struct {
Action string `json:"action"`
Config networkconfig.Config `json:"config,omitempty"`
TransactionID string `json:"transaction_id,omitempty"`
}
type response struct {
ApplyResult *networkconfig.ApplyResult `json:"apply_result,omitempty"`
Status *networkconfig.Status `json:"status,omitempty"`
Code string `json:"code,omitempty"`
Error string `json:"error,omitempty"`
}
type Server struct{ Controller networkconfig.Controller }
func (s Server) Serve(listener net.Listener) error {
if s.Controller == nil {
return errors.New("network 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 Server) handleConnection(connection net.Conn) {
defer connection.Close()
_ = connection.SetDeadline(time.Now().Add(35 * time.Second))
payload, err := bufio.NewReader(io.LimitReader(connection, 65537)).ReadBytes('\n')
if err != nil && !errors.Is(err, io.EOF) {
s.writeResponse(connection, response{Code: "validation_failed", Error: "invalid network request"})
return
}
if len(payload) == 0 || len(payload) > 65536 || payload[len(payload)-1] != '\n' {
s.writeResponse(connection, response{Code: "validation_failed", Error: "invalid network request"})
return
}
decoder := json.NewDecoder(bytes.NewReader(payload))
decoder.DisallowUnknownFields()
var command request
if err := decoder.Decode(&command); err != nil {
s.writeResponse(connection, response{Code: "validation_failed", Error: "invalid network request"})
return
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
s.writeResponse(connection, response{Code: "validation_failed", Error: "network request must contain one JSON value"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
switch command.Action {
case "apply":
result, err := s.Controller.Apply(ctx, command.Config)
if err != nil {
s.writeError(connection, err)
return
}
s.writeResponse(connection, response{ApplyResult: &result})
case "confirm":
status, err := s.Controller.Confirm(ctx, command.TransactionID)
if err != nil {
s.writeError(connection, err)
return
}
s.writeResponse(connection, response{Status: &status})
case "rollback":
status, err := s.Controller.Rollback(ctx, command.TransactionID)
if err != nil {
s.writeError(connection, err)
return
}
s.writeResponse(connection, response{Status: &status})
case "status":
status, err := s.Controller.Status(ctx)
if err != nil {
s.writeError(connection, err)
return
}
s.writeResponse(connection, response{Status: &status})
default:
s.writeResponse(connection, response{Code: "validation_failed", Error: "network action is not supported"})
}
}
func (s Server) writeError(connection net.Conn, err error) {
code := "network_failed"
if networkconfig.IsUnavailable(err) {
code = "network_unavailable"
}
s.writeResponse(connection, response{Code: code, Error: err.Error()})
}
func (Server) writeResponse(connection net.Conn, value response) {
_ = json.NewEncoder(connection).Encode(value)
}
type Client struct {
path string
timeout time.Duration
}
func NewClient(path string) *Client {
if path == "" {
path = DefaultSocketPath
}
return &Client{path: path, timeout: 35 * time.Second}
}
func (c *Client) Apply(ctx context.Context, config networkconfig.Config) (networkconfig.ApplyResult, error) {
result, err := c.execute(ctx, request{Action: "apply", Config: config})
if err != nil {
return networkconfig.ApplyResult{}, err
}
if result.ApplyResult == nil {
return networkconfig.ApplyResult{}, errors.New("tera-netd returned no apply result")
}
return *result.ApplyResult, nil
}
func (c *Client) Confirm(ctx context.Context, transactionID string) (networkconfig.Status, error) {
return c.statusAction(ctx, "confirm", transactionID)
}
func (c *Client) Rollback(ctx context.Context, transactionID string) (networkconfig.Status, error) {
return c.statusAction(ctx, "rollback", transactionID)
}
func (c *Client) Status(ctx context.Context) (networkconfig.Status, error) {
return c.statusAction(ctx, "status", "")
}
func (c *Client) statusAction(ctx context.Context, action, transactionID string) (networkconfig.Status, error) {
result, err := c.execute(ctx, request{Action: action, TransactionID: transactionID})
if err != nil {
return networkconfig.Status{}, err
}
if result.Status == nil {
return networkconfig.Status{}, errors.New("tera-netd returned no status")
}
return *result.Status, nil
}
func (c *Client) execute(ctx context.Context, command request) (response, error) {
dialer := net.Dialer{Timeout: c.timeout}
connection, err := dialer.DialContext(ctx, "unix", c.path)
if err != nil {
return response{}, &networkconfig.UnavailableError{Reason: "tera-netd 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 response{}, errors.New("network request failed")
}
var result response
if err := json.NewDecoder(io.LimitReader(connection, 65536)).Decode(&result); err != nil {
return response{}, &networkconfig.UnavailableError{Reason: "tera-netd returned no response"}
}
if result.Error != "" {
if result.Code == "network_unavailable" {
return response{}, &networkconfig.UnavailableError{Reason: result.Error}
}
return response{}, fmt.Errorf("tera-netd: %s", result.Error)
}
return result, nil
}