From c6d3238b15a9d9665e24b204db67c64a64816ff3 Mon Sep 17 00:00:00 2001 From: ginuerzh Date: Thu, 4 Jun 2026 23:38:04 +0800 Subject: [PATCH] docs(handler/serial): add comprehensive English comments to all files Add package-level architecture documentation describing two modes (hop-based forwarding and direct proxy), two-tier dialing strategy in forwardSerial, recorderConn per-packet logging with direction convention, and metadata parsing details. No code changes. --- handler/serial/conn.go | 21 +++++++ handler/serial/handler.go | 124 ++++++++++++++++++++++++++++++++++--- handler/serial/metadata.go | 9 +++ 3 files changed, 147 insertions(+), 7 deletions(-) diff --git a/handler/serial/conn.go b/handler/serial/conn.go index ddc4c45f..156b2657 100644 --- a/handler/serial/conn.go +++ b/handler/serial/conn.go @@ -10,11 +10,28 @@ import ( "github.com/go-gost/core/recorder" ) +// recorderConn wraps a net.Conn to record raw traffic flowing through it. +// Each successful Read or Write that transfers data is logged via the +// configured Recorder, optionally annotated with direction markers, +// timestamps, and hex dumps. +// +// The direction convention is: +// - Read: '>' (data entering GOST from the serial port / client side) +// - Write: '<' (data leaving GOST toward the serial port / client side) +// +// This provides a per-packet-level traffic log, as opposed to the +// aggregate stats approach used by handlers like redirect/tcp (which +// accumulate total byte counts and record once at the end). type recorderConn struct { net.Conn recorder recorder.RecorderObject } +// Read reads from the underlying connection and records the received data. +// Recording happens only when bytes were actually read (n > 0) AND a +// Recorder is configured. If Read returns an error alongside data +// (e.g. io.EOF after the last chunk), the data is still recorded — +// partial reads before an error are meaningful traffic. func (c *recorderConn) Read(b []byte) (n int, err error) { n, err = c.Conn.Read(b) @@ -40,6 +57,10 @@ func (c *recorderConn) Read(b []byte) (n int, err error) { return } +// Write writes to the underlying connection and records the sent data. +// Recording follows the same rules as Read: only when n > 0 and a +// Recorder is configured. The direction marker is '<' to indicate +// outbound traffic (data leaving GOST toward the target). func (c *recorderConn) Write(b []byte) (n int, err error) { n, err = c.Conn.Write(b) diff --git a/handler/serial/handler.go b/handler/serial/handler.go index 40cd1e4f..e0643601 100644 --- a/handler/serial/handler.go +++ b/handler/serial/handler.go @@ -1,3 +1,37 @@ +// Package serial implements a handler for serial port connections in GOST. +// +// The serial handler operates in two distinct modes: +// +// # Mode 1 — Hop-based forwarding (h.hop != nil) +// +// When a hop is configured (typically via the -F CLI flag for forward +// proxies), the handler selects a target node from the hop and forwards +// traffic through that target's serial port. The forwarding path is: +// +// 1. Parse serial port parameters (baud rate, parity, etc.) from the local +// connection address (e.g. "COM1,9600,even"). +// 2. Override the port NAME with the target node's address — the target +// specifies WHICH serial device to use, while the local address +// specifies HOW to configure it. +// 3. Attempt to dial through the router's chain using network "serial". +// If a chain IS configured, it handles the actual transport (e.g. +// TCP/TLS/WS to a remote host, then a serial connector at the far end). +// 4. If the router has NO chain (or no router at all), fall back to +// opening the serial port directly on the local machine. +// 5. Bidirectional data pipe between the client connection and the serial +// port. +// +// # Mode 2 — Direct proxy (h.hop == nil) +// +// Without a hop, the handler acts as a simple pass-through: it dials the +// router with the sentinel address "@" (meaning "local"), then pipes data +// between the client connection and the router connection. +// +// # Traffic recording +// +// Raw traffic is recorded per-packet via recorderConn. Each successful +// Read/Write is logged with optional direction markers, timestamps, and +// hex dumps (controlled by RecorderOptions). package serial import ( @@ -24,13 +58,22 @@ func init() { registry.HandlerRegistry().Register("serial", NewHandler) } +// serialHandler handles serial port connections. It can operate as a +// forwarder (with a hop) or as a simple pass-through proxy (without a hop). type serialHandler struct { - hop hop.Hop - md metadata - options handler.Options + // hop selects a target node for forwarding mode. Set via Forward(). + hop hop.Hop + // md holds parsed metadata (timeout). + md metadata + // options carries the standard handler options (router, logger, bypass, etc.). + options handler.Options + // recorder is the traffic recorder for serial handler traffic. It is selected + // during Init() by matching the RecorderServiceHandlerSerial record name. recorder recorder.RecorderObject } +// NewHandler creates a new serial handler with the given options. +// The handler must be initialized via Init() before use. func NewHandler(opts ...handler.Option) handler.Handler { options := handler.Options{} for _, opt := range opts { @@ -42,11 +85,15 @@ func NewHandler(opts ...handler.Option) handler.Handler { } } +// Init initializes the handler by parsing metadata and locating the +// traffic recorder for serial handler events. It implements handler.Initable. func (h *serialHandler) Init(md md.Metadata) (err error) { if err = h.parseMetadata(md); err != nil { return } + // Find the recorder whose Record name matches the serial handler + // constant. Only one recorder per handler type is supported. for _, ro := range h.options.Recorders { if ro.Record == xrecorder.RecorderServiceHandlerSerial { h.recorder = ro @@ -57,11 +104,25 @@ func (h *serialHandler) Init(md md.Metadata) (err error) { return } -// Forward implements handler.Forwarder. +// Forward sets the hop used for forwarding mode. When a hop is configured, +// Handle() will select a target node and forward traffic through it. +// Implements handler.Forwarder. func (h *serialHandler) Forward(hop hop.Hop) { h.hop = hop } +// Handle processes an incoming serial port connection. +// +// The method always closes the connection on return (via defer). It follows +// one of two code paths depending on whether a hop has been configured: +// +// Hop configured → forwardSerial() — route through chain or open +// serial port directly. +// No hop → Router.Dial("tcp", "@") — dial through router +// chain to a local handler, then pipe data. +// +// The connection is wrapped in a recorderConn before any I/O, so all +// traffic through the handler is logged (subject to recorder config). func (h *serialHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) error { defer conn.Close() @@ -69,16 +130,22 @@ func (h *serialHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl log = log.WithFields(map[string]any{ "network": "serial", - "remote": conn.RemoteAddr().String(), - "local": conn.LocalAddr().String(), - "sid": xctx.SidFromContext(ctx).String(), + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "sid": xctx.SidFromContext(ctx).String(), }) + // Wrap the connection for per-packet traffic recording. The recorderConn + // delegates Read/Write to the underlying connection, recording each + // successful transfer. When no recorder is configured, the wrapper is + // effectively a no-op (the nil checks on each call are cheap). conn = &recorderConn{ Conn: conn, recorder: h.recorder, } + // --- Mode 1: Hop-based forwarding --- + // Select a target node from the hop and forward serial traffic to it. if h.hop != nil { target := h.hop.Select(ctx) if target == nil { @@ -93,6 +160,10 @@ func (h *serialHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl return h.forwardSerial(ctx, conn, target, log) } + // --- Mode 2: Direct proxy (no hop) --- + // Dial through the router chain with the sentinel address "@" (meaning + // "local" or "loopback"). The router's chain is expected to route "@" + // to an appropriate handler (e.g. a local relay listener). if h.options.Router == nil { err := errors.New("router not available") log.Error(err) @@ -116,18 +187,54 @@ func (h *serialHandler) Handle(ctx context.Context, conn net.Conn, opts ...handl return nil } +// forwardSerial forwards traffic from conn to the target node's serial port. +// +// The method implements a two-tier dialing strategy: +// +// 1. Router chain (preferred): If a router with a chain is configured, +// dial through the chain with network "serial". The chain handles +// transport (TCP/TLS/WS/etc.) and uses a serial connector at the +// far end to reach the remote serial device. +// +// 2. Direct open (fallback): If no router chain exists or the chain +// returned no connection without error, open the serial port +// directly on the local machine. This is the typical case when +// GOST runs on the same host as the serial device. +// +// Serial port parameters (baud rate, parity, stop bits) are parsed from +// the local connection address, while the port NAME comes from the +// target node's address. This separation allows the hop target to +// specify WHICH device to use while the local port configuration +// specifies HOW to talk to it. func (h *serialHandler) forwardSerial(ctx context.Context, conn net.Conn, target *chain.Node, log logger.Logger) (err error) { log.Debugf("%s >> %s", conn.LocalAddr(), target.Addr) var port io.ReadWriteCloser + // Parse serial configuration from the local connection address. + // The address format is: "NAME,BAUD,PARITY" (e.g. "COM1,9600,even"). + // Baud and parity default to 9600 and None if not specified. cfg := serial.ParseConfigFromAddr(conn.LocalAddr().String()) + // Override the port name with the target node's address — the hop + // target determines WHICH serial device to open. cfg.Name = target.Addr + // Tier 1: Try routing through the chain (if configured). + // The chain dials with network="serial", which selects a transport + // whose connector understands serial addresses. This path supports + // remote serial port access through GOST's proxy chain. if h.options.Router != nil { if opts := h.options.Router.Options(); opts != nil && opts.Chain != nil { port, err = h.options.Router.Dial(ctx, "serial", serial.AddrFromConfig(cfg)) } } + // Tier 2: Fall back to directly opening the local serial port. + // This runs when: + // - No router is configured, OR + // - The router has no chain, OR + // - The router chain returned (nil, nil) — meaning no route was + // available but no error occurred either. + // If the router chain returned an actual error, we skip the fallback + // and propagate the error — a configured chain is authoritative. if port == nil && err == nil { cfg.ReadTimeout = h.md.timeout port, err = serial.OpenPort(cfg) @@ -141,6 +248,9 @@ func (h *serialHandler) forwardSerial(ctx context.Context, conn net.Conn, target t := time.Now() log.Infof("%s <-> %s", conn.LocalAddr(), target.Addr) // xnet.Transport(conn, port) + // Pipe bidirectionally between the client connection and the serial port. + // xnet.Pipe spawns two goroutines and returns on first error or when + // both directions complete. xnet.Pipe(ctx, conn, port) log.WithFields(map[string]any{ "duration": time.Since(t), diff --git a/handler/serial/metadata.go b/handler/serial/metadata.go index 65bc8e09..94b59ed8 100644 --- a/handler/serial/metadata.go +++ b/handler/serial/metadata.go @@ -7,10 +7,19 @@ import ( mdutil "github.com/go-gost/x/metadata/util" ) +// metadata holds the parsed configuration for the serial handler. +// It is populated from the handler's metadata map during Init(). type metadata struct { + // timeout is the per-read timeout applied when opening a serial port + // directly (not through the router chain). It controls how long the + // serial port blocks waiting for data before returning to the caller. + // A value of 0 means blocking indefinitely (no timeout). timeout time.Duration } +// parseMetadata extracts typed configuration from the generic metadata map. +// Supported keys: "timeout", "serial.timeout" (both accept duration strings +// like "5s" or integer seconds). func (h *serialHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.timeout = mdutil.GetDuration(md, "timeout", "serial.timeout") return