feat: add TCP Server, UDP Server, and COM port forwarding modes

Extend Mode constants (3=TCPServer, 4=UDPServer, 5=COMPort) with
explicit values. Refactor Target to support multiple connection types
(listener+conns map, packetConn, serialPort). Add acceptLoop,
readLoopPacket, readLoopSerial. Mode-aware Broadcast dispatches to
all accepted conns (TCP-S), known remotes (UDP-S), or serial port.
Update flag help, command completions, and TUI panel hints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
JiXieShi
2026-05-23 23:10:02 +08:00
parent d8fc9d7374
commit 30d6c2bc3c
6 changed files with 367 additions and 57 deletions
+354 -44
View File
@@ -1,4 +1,4 @@
// Package forward manages TCP/UDP forwarding targets for serial data.
// Package forward manages TCP/UDP/COM forwarding targets for serial data.
package forward
import (
@@ -9,24 +9,35 @@ import (
"sync"
"sync/atomic"
"time"
"go.bug.st/serial"
)
// Mode is the forwarding protocol mode.
type Mode int
const (
None Mode = iota
TCP
UDP
None Mode = 0
TCP Mode = 1
UDP Mode = 2
TCPServer Mode = 3
UDPServer Mode = 4
COMPort Mode = 5
)
// ParseMode parses a mode string. Accepts "tcp"/"tcp-c"/"tcpc"/"1" → TCP, "udp"/"udp-c"/"udpc"/"2" → UDP.
// 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
}
@@ -34,10 +45,12 @@ func ParseMode(v string) (Mode, bool) {
func (m Mode) Network() string {
switch m {
case TCP:
case TCP, TCPServer:
return "tcp"
case UDP:
case UDP, UDPServer:
return "udp"
case COMPort:
return "serial"
default:
return ""
}
@@ -49,6 +62,12 @@ func (m Mode) String() string {
return "tcp"
case UDP:
return "udp"
case TCPServer:
return "tcp-s"
case UDPServer:
return "udp-s"
case COMPort:
return "com"
default:
return "none"
}
@@ -70,13 +89,34 @@ type Target struct {
Connected bool
CreatedAt time.Time
conn net.Conn
// 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
@@ -87,6 +127,7 @@ type Snapshot struct {
ReadBytes uint64
WriteByte uint64
LastError string
Conns int // accepted connection count (TCP server)
}
// Manager coordinates forwarding targets.
@@ -130,26 +171,179 @@ func (m *Manager) Add(mode Mode, address string) (int, error) {
closeCh: make(chan struct{}),
}
conn, err := net.Dial(mode.Network(), address)
if err != nil {
t.stats.LastError = err.Error()
return 0, err
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)
}
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)
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) readLoopPacket(t *Target) {
buf := make([]byte, 4096)
for {
n, addr, err := t.packetConn.ReadFrom(buf)
if n > 0 {
atomic.AddUint64(&t.stats.ReadBytes, uint64(n))
chunk := make([]byte, n)
copy(chunk, buf[:n])
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)
}
// Track remote address for Broadcast
t.mu.Lock()
t.remoteAddrs[addr.String()] = addr
t.mu.Unlock()
}
if err != nil {
select {
case <-t.closeCh:
return
default:
}
t.Connected = false
t.stats.LastError = err.Error()
m.notify("[forward] #%d disconnected: %v", t.ID, 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 {
atomic.AddUint64(&t.stats.ReadBytes, uint64(n))
chunk := make([]byte, n)
copy(chunk, buf[:n])
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)
}
}
if err != nil {
select {
case <-t.closeCh:
return
default:
}
t.Connected = false
t.stats.LastError = err.Error()
m.notify("[forward] #%d disconnected: %v", t.ID, 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 {
@@ -167,18 +361,28 @@ func (m *Manager) readLoop(t *Target, conn net.Conn, stop <-chan struct{}) {
}
if err != nil {
t.mu.Lock()
if t.conn == conn {
t.Connected = false
}
t.Connected = false
t.stats.LastError = err.Error()
t.mu.Unlock()
// Remove from TCP server conns if applicable
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:
}
@@ -216,18 +420,59 @@ func (m *Manager) Enable(id int) error {
return nil
}
conn, err := net.Dial(t.Mode.Network(), t.Address)
if err != nil {
t.stats.LastError = err.Error()
return err
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
t.Connected = true
t.conn = conn
t.closeCh = make(chan struct{})
t.closed = false
go m.readLoop(t, conn, t.closeCh)
m.notify("[forward] #%d enabled", id)
return nil
}
@@ -292,18 +537,67 @@ func (m *Manager) Broadcast(data []byte) {
m.mu.RUnlock()
for _, t := range items {
if !t.Enabled || !t.Connected || t.conn == nil {
if !t.Enabled || !t.Connected {
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)
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))
}
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))
}
}
}
}
@@ -321,6 +615,7 @@ func (m *Manager) List() []Snapshot {
ReadBytes: atomic.LoadUint64(&t.stats.ReadBytes),
WriteByte: atomic.LoadUint64(&t.stats.WrittenBytes),
LastError: t.stats.LastError,
Conns: t.acceptedConns(),
})
}
m.mu.RUnlock()
@@ -356,7 +651,13 @@ func (t *Target) close() {
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()
@@ -366,4 +667,13 @@ func (t *Target) close() {
if conn != nil {
_ = conn.Close()
}
if listener != nil {
_ = listener.Close()
}
if pc != nil {
_ = pc.Close()
}
if sp != nil {
_ = sp.Close()
}
}