Documentation
¶
Overview ¶
Package boxapi serves the box-facing port-publishing API: a small HTTP/JSON API on a per-box unix socket that lets the workload inside a box open, close, and list public URLs for ports of ITS OWN box only. The handler is bound to one box's identity at construction — the request body carries only a port and description, never a box or spoke identity — so whichever channel a request arrives on decides which box it acts on. The socket is deliberately a unix socket (not TCP) so the box's own proxy data path, which only dials TCP ports, can never publish the control API itself.
The spoke serves the socket host-side for Docker boxes (in the per-box bind-mount dir, so it appears in-box at /run/llmbox/boxapi.sock); for Firecracker the same handler listens on the per-VM host UDS that the guest guest bridges the in-guest socket to. Either way the listener — and thus the enforcement — lives outside the sandbox.
Index ¶
Constants ¶
const SocketName = "boxapi.sock"
SocketName is the file name of the box-port API socket: the in-box path is /run/llmbox/boxapi.sock (next to the guest's control.sock), and the Docker host side is the same name inside the box's private socket dir.
Variables ¶
This section is empty.
Functions ¶
func NewHandler ¶
func NewHandler(boxID string, svc PortService) http.Handler
NewHandler returns the box-port HTTP API for one box. boxID is the identity every forwarded request is stamped with; it is fixed at construction so nothing the box sends can change it. A box created without a box ID gets a handler whose every call fails with a clear explanation.
Routes (all POST, JSON in/out; errors are {"error":"..."} with status 400 on a bad request and 502 when the hub rejects or is unreachable):
POST /v1/open_port {"port":3000,"description":"..."} → {"port":{"port":3000,"url":"https://...","description":"..."}}
POST /v1/close_port {"port":3000} → {}
POST /v1/list_ports {} → {"ports":[...]}
@arg boxID The identity of the box this handler serves; "" yields a handler that only explains the box cannot publish ports. @arg svc The service forwarding requests to the hub. @return http.Handler The box-port API for that one box.
@testcase TestBoxAPIOpenPort opens a port and sees the bound box ID stamped. @testcase TestBoxAPIClosePort closes a port through the handler. @testcase TestBoxAPIListPorts lists ports through the handler. @testcase TestBoxAPIRejectsBadRequest rejects malformed JSON and out-of-range ports. @testcase TestBoxAPIServiceError maps a service failure to a 502 with the error body. @testcase TestBoxAPINoBoxID explains that a box without a box ID cannot publish ports.
Types ¶
type PortService ¶
type PortService interface {
// OpenBoxPort publishes a box port and returns its public view.
OpenBoxPort(ctx context.Context, boxID string, port int, description string) (cluster.BoxPortInfo, error)
// CloseBoxPort unpublishes a box port.
CloseBoxPort(ctx context.Context, boxID string, port int) error
// ListBoxPorts returns the box's published ports.
ListBoxPorts(ctx context.Context, boxID string) ([]cluster.BoxPortInfo, error)
}
PortService is what the box-port API needs from the spoke: forward a request, stamped with the originating box's identity, to the hub. *cluster.HubCaller satisfies it.
type Server ¶
type Server struct {
// contains filtered or unexported fields
}
Server serves the box-port API for one box on a unix socket.
func ServeUnix ¶
ServeUnix creates (replacing any stale socket file) and serves the box-port API for one box at path, returning immediately; the caller stops it with Close. The socket is made group/other-accessible for the same reason as the guest control socket travelling the other way: across the container boundary the connecting peer runs as a different uid, and connect() needs write permission on the socket — the 0700 parent directory (the box's private socket dir) remains the access gate on the host side.
@arg path The filesystem path of the socket to create. @arg boxID The identity of the box this socket serves; every request is stamped with it. @arg svc The service forwarding requests to the hub. @arg log Logger for serve-loop failures; nil uses slog.Default. @return *Server The serving listener; the caller must Close it. @error error if the socket cannot be created or its mode cannot be set.
@testcase TestBoxAPIOpenPort serves requests over a socket created by ServeUnix. @testcase TestServeUnixReplacesStaleSocket replaces a stale socket file at the same path.
func (*Server) Close ¶
Close stops the server, closing the listener and any in-flight connections. The listener is also closed directly: http.Server.Close only closes listeners Serve has already registered, and the serving goroutine may not have been scheduled yet when Close runs — without the direct close, the socket would keep accepting until that goroutine finally starts. Closing more than once is harmless.
@error error if closing the underlying server fails.
@testcase TestServeUnixCloseStopsServing refuses connections after Close, even when the serve goroutine had not started.