// Package forward manages TCP/UDP/COM forwarding targets for serial data. package forward import ( "fmt" "net" "sort" "strings" "sync" "sync/atomic" "time" "go.bug.st/serial" ) // Mode is the forwarding protocol mode. type Mode int const ( None Mode = 0 TCP Mode = 1 UDP Mode = 2 TCPServer Mode = 3 UDPServer Mode = 4 COMPort Mode = 5 ) // ParseMode parses a mode string. func ParseMode(v string) (Mode, bool) { switch strings.ToLower(strings.TrimSpace(v)) { case "tcp", "tcp-c", "tcpc", "1": return TCP, true case "udp", "udp-c", "udpc", "2": return UDP, true case "tcp-s", "tcps", "tcp-server", "3": return TCPServer, true case "udp-s", "udps", "udp-server", "4": return UDPServer, true case "com", "serial", "5": return COMPort, true default: return None, false } } func (m Mode) Network() string { switch m { case TCP, TCPServer: return "tcp" case UDP, UDPServer: return "udp" case COMPort: return "serial" default: return "" } } func (m Mode) String() string { switch m { case TCP: return "tcp" case UDP: return "udp" case TCPServer: return "tcp-s" case UDPServer: return "udp-s" case COMPort: return "com" default: return "none" } } // Stats holds I/O statistics for a forward target. type Stats struct { ReadBytes uint64 WrittenBytes uint64 LastError string } // Target represents a single forwarding connection. type Target struct { ID int Mode Mode Address string Enabled bool Connected bool CreatedAt time.Time // Client-mode connection (TCP/UDP client) conn net.Conn // Server-mode fields listener net.Listener // TCP server listener conns map[net.Conn]struct{} // TCP server accepted connections connsMu sync.Mutex // UDP server packetConn net.PacketConn // UDP server listener remoteAddrs map[string]net.Addr // known UDP remotes // COM port serialPort serial.Port stats Stats mu sync.Mutex closeCh chan struct{} closed bool } // AcceptedConns returns the number of accepted connections (TCP server only). func (t *Target) acceptedConns() int { t.connsMu.Lock() defer t.connsMu.Unlock() return len(t.conns) } // Snapshot is a read-only view of a forward target for display. type Snapshot struct { ID int Mode string Address string Enabled bool Connected bool ReadBytes uint64 WriteByte uint64 LastError string Conns int // accepted connection count (TCP server) } // Manager coordinates forwarding targets. type Manager struct { mu sync.RWMutex targets map[int]*Target nextID int writeToSerial func([]byte) error notify func(string, ...any) onInbound func(int, []byte) } // NewManager creates a forwarding manager. func NewManager(writeToSerial func([]byte) error, notify func(string, ...any)) *Manager { return &Manager{ targets: make(map[int]*Target), nextID: 1, writeToSerial: writeToSerial, notify: notify, } } // SetInboundReporter sets a callback invoked when inbound data arrives from a target. func (m *Manager) SetInboundReporter(fn func(int, []byte)) { m.mu.Lock() defer m.mu.Unlock() m.onInbound = fn } // Add creates and connects a new forward target. func (m *Manager) Add(mode Mode, address string) (int, error) { if mode == None { return 0, fmt.Errorf("forward mode cannot be none") } t := &Target{ Mode: mode, Address: address, Enabled: true, CreatedAt: time.Now(), closeCh: make(chan struct{}), } switch mode { case TCP, UDP: conn, err := net.Dial(mode.Network(), address) if err != nil { t.stats.LastError = err.Error() return 0, err } t.conn = conn t.Connected = true m.mu.Lock() t.ID = m.nextID m.nextID++ m.targets[t.ID] = t m.mu.Unlock() go m.readLoop(t, conn, t.closeCh) case TCPServer: listener, err := net.Listen("tcp", address) if err != nil { t.stats.LastError = err.Error() return 0, err } t.listener = listener t.conns = make(map[net.Conn]struct{}) t.Connected = true m.mu.Lock() t.ID = m.nextID m.nextID++ m.targets[t.ID] = t m.mu.Unlock() go m.acceptLoop(t) case UDPServer: pc, err := net.ListenPacket("udp", address) if err != nil { t.stats.LastError = err.Error() return 0, err } t.packetConn = pc t.remoteAddrs = make(map[string]net.Addr) t.Connected = true m.mu.Lock() t.ID = m.nextID m.nextID++ m.targets[t.ID] = t m.mu.Unlock() go m.readLoopPacket(t) case COMPort: sp, err := serial.Open(address, &serial.Mode{BaudRate: 115200, DataBits: 8, StopBits: 0, Parity: 0}) if err != nil { t.stats.LastError = err.Error() return 0, err } t.serialPort = sp t.Connected = true m.mu.Lock() t.ID = m.nextID m.nextID++ m.targets[t.ID] = t m.mu.Unlock() go m.readLoopSerial(t) } m.notify("[forward] #%d %s %s connected", t.ID, t.Mode.String(), t.Address) return t.ID, nil } func (m *Manager) acceptLoop(t *Target) { for { conn, err := t.listener.Accept() if err != nil { select { case <-t.closeCh: return default: } t.stats.LastError = err.Error() m.notify("[forward] #%d accept error: %v", t.ID, err) return } t.connsMu.Lock() t.conns[conn] = struct{}{} t.connsMu.Unlock() m.notify("[forward] #%d accepted %s", t.ID, conn.RemoteAddr()) go m.readLoop(t, conn, t.closeCh) } } func (m *Manager) processChunk(t *Target, data []byte) { if len(data) == 0 { return } n := len(data) atomic.AddUint64(&t.stats.ReadBytes, uint64(n)) chunk := make([]byte, n) copy(chunk, data) if wErr := m.writeToSerial(chunk); wErr != nil { t.stats.LastError = wErr.Error() m.notify("[forward] #%d write serial error: %v", t.ID, wErr) } else if m.onInbound != nil { m.onInbound(t.ID, chunk) } } func (m *Manager) readLoopError(t *Target, err error) { select { case <-t.closeCh: return default: } t.Connected = false t.stats.LastError = err.Error() m.notify("[forward] #%d disconnected: %v", t.ID, err) } func (m *Manager) readLoopPacket(t *Target) { buf := make([]byte, 4096) for { n, addr, err := t.packetConn.ReadFrom(buf) if n > 0 { m.processChunk(t, buf[:n]) t.mu.Lock() t.remoteAddrs[addr.String()] = addr t.mu.Unlock() } if err != nil { m.readLoopError(t, err) return } select { case <-t.closeCh: return default: } } } func (m *Manager) readLoopSerial(t *Target) { buf := make([]byte, 4096) for { n, err := t.serialPort.Read(buf) if n > 0 { m.processChunk(t, buf[:n]) } if err != nil { m.readLoopError(t, err) return } select { case <-t.closeCh: return default: } } } func (m *Manager) readLoop(t *Target, conn net.Conn, stop <-chan struct{}) { buf := make([]byte, 4096) for { n, err := conn.Read(buf) if n > 0 { m.processChunk(t, buf[:n]) } if err != nil { t.Connected = false t.stats.LastError = err.Error() if t.Mode == TCPServer { t.connsMu.Lock() delete(t.conns, conn) t.connsMu.Unlock() } m.notify("[forward] #%d disconnected: %v", t.ID, err) _ = conn.Close() return } select { case <-stop: _ = conn.Close() if t.Mode == TCPServer { t.connsMu.Lock() delete(t.conns, conn) t.connsMu.Unlock() } return default: } } } // Remove disconnects and removes a target. func (m *Manager) Remove(id int) error { m.mu.Lock() t, ok := m.targets[id] if !ok { m.mu.Unlock() return fmt.Errorf("forward #%d not found", id) } delete(m.targets, id) m.mu.Unlock() t.close() m.notify("[forward] #%d removed", id) return nil } // Enable (re)connects a target. func (m *Manager) Enable(id int) error { m.mu.RLock() t, ok := m.targets[id] m.mu.RUnlock() if !ok { return fmt.Errorf("forward #%d not found", id) } t.mu.Lock() defer t.mu.Unlock() if t.Enabled && t.Connected { return nil } switch t.Mode { case TCP, UDP: conn, err := net.Dial(t.Mode.Network(), t.Address) if err != nil { t.stats.LastError = err.Error() return err } t.conn = conn t.Connected = true t.closeCh = make(chan struct{}) t.closed = false go m.readLoop(t, conn, t.closeCh) case TCPServer: listener, err := net.Listen("tcp", t.Address) if err != nil { t.stats.LastError = err.Error() return err } t.listener = listener t.conns = make(map[net.Conn]struct{}) t.Connected = true t.closeCh = make(chan struct{}) t.closed = false go m.acceptLoop(t) case UDPServer: pc, err := net.ListenPacket("udp", t.Address) if err != nil { t.stats.LastError = err.Error() return err } t.packetConn = pc t.remoteAddrs = make(map[string]net.Addr) t.Connected = true t.closeCh = make(chan struct{}) t.closed = false go m.readLoopPacket(t) case COMPort: sp, err := serial.Open(t.Address, &serial.Mode{BaudRate: 115200, DataBits: 8, StopBits: 0, Parity: 0}) if err != nil { t.stats.LastError = err.Error() return err } t.serialPort = sp t.Connected = true t.closeCh = make(chan struct{}) t.closed = false go m.readLoopSerial(t) } t.Enabled = true m.notify("[forward] #%d enabled", id) return nil } // Update changes a target's mode and address, reconnecting if enabled. func (m *Manager) Update(id int, mode Mode, address string) error { if mode == None { return fmt.Errorf("forward mode cannot be none") } m.mu.RLock() t, ok := m.targets[id] m.mu.RUnlock() if !ok { return fmt.Errorf("forward #%d not found", id) } t.mu.Lock() wasEnabled := t.Enabled t.Mode = mode t.Address = address t.mu.Unlock() t.close() if !wasEnabled { m.notify("[forward] #%d updated (disabled)", id) return nil } return m.Enable(id) } // Disable disconnects a target without removing it. func (m *Manager) Disable(id int) error { m.mu.RLock() t, ok := m.targets[id] m.mu.RUnlock() if !ok { return fmt.Errorf("forward #%d not found", id) } t.mu.Lock() t.Enabled = false t.mu.Unlock() t.close() m.notify("[forward] #%d disabled", id) return nil } // Broadcast sends data to all enabled, connected targets. func (m *Manager) Broadcast(data []byte) { if len(data) == 0 { return } m.mu.RLock() items := make([]*Target, 0, len(m.targets)) for _, t := range m.targets { items = append(items, t) } m.mu.RUnlock() for _, t := range items { if !t.Enabled || !t.Connected { continue } switch t.Mode { case TCP, UDP: if t.conn == nil { continue } n, err := t.conn.Write(data) if err != nil { t.stats.LastError = err.Error() m.notify("[forward] #%d write error: %v", t.ID, err) } else { atomic.AddUint64(&t.stats.WrittenBytes, uint64(n)) } case TCPServer: t.connsMu.Lock() conns := make([]net.Conn, 0, len(t.conns)) for c := range t.conns { conns = append(conns, c) } t.connsMu.Unlock() for _, c := range conns { n, err := c.Write(data) if err != nil { t.stats.LastError = err.Error() } else { atomic.AddUint64(&t.stats.WrittenBytes, uint64(n)) } } case UDPServer: t.mu.Lock() addrs := make([]net.Addr, 0, len(t.remoteAddrs)) for _, addr := range t.remoteAddrs { addrs = append(addrs, addr) } t.mu.Unlock() for _, addr := range addrs { n, err := t.packetConn.WriteTo(data, addr) if err != nil { t.stats.LastError = err.Error() } else { atomic.AddUint64(&t.stats.WrittenBytes, uint64(n)) } } case COMPort: if t.serialPort == nil { continue } n, err := t.serialPort.Write(data) if err != nil { t.stats.LastError = err.Error() m.notify("[forward] #%d write error: %v", t.ID, err) } else { atomic.AddUint64(&t.stats.WrittenBytes, uint64(n)) } } } } // List returns a snapshot of all targets. func (m *Manager) List() []Snapshot { m.mu.RLock() items := make([]Snapshot, 0, len(m.targets)) for _, t := range m.targets { items = append(items, Snapshot{ ID: t.ID, Mode: t.Mode.String(), Address: t.Address, Enabled: t.Enabled, Connected: t.Connected, ReadBytes: atomic.LoadUint64(&t.stats.ReadBytes), WriteByte: atomic.LoadUint64(&t.stats.WrittenBytes), LastError: t.stats.LastError, Conns: t.acceptedConns(), }) } m.mu.RUnlock() sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID }) return items } // Close disconnects and removes all targets. func (m *Manager) Close() { m.mu.Lock() items := make([]*Target, 0, len(m.targets)) for _, t := range m.targets { items = append(items, t) } m.targets = map[int]*Target{} m.mu.Unlock() for _, t := range items { t.close() } } func (t *Target) close() { t.mu.Lock() if t.closed { t.mu.Unlock() return } t.closed = true ch := t.closeCh conn := t.conn listener := t.listener pc := t.packetConn sp := t.serialPort t.conn = nil t.listener = nil t.packetConn = nil t.serialPort = nil t.Connected = false t.mu.Unlock() if ch != nil { close(ch) } if conn != nil { _ = conn.Close() } if listener != nil { _ = listener.Close() } if pc != nil { _ = pc.Close() } if sp != nil { _ = sp.Close() } }