feat: raw binary SDWAN data plane + EncodeRaw + TUN close-on-stop

- protocol: add SubTunnelSDWANRaw subtype + EncodeRaw() for zero-copy IP packets
- client: tunReadLoop sends raw frames (no JSON/base64 overhead)
- client: SubTunnelSDWANRaw handler strips header and writes directly to TUN
- client: Stop() closes TUN file FIRST to unblock tunReadLoop
- server: SubTunnelSDWANRaw handler parses IPv4 src/dst from raw packet
- server: RouteSDWANPacket forwards as raw frame to destination

Verified: hcss(10.10.0.3) ↔ i-6986(10.10.0.2) ping 3/3, 0% loss, 46ms RTT
This commit is contained in:
2026-03-02 18:22:41 +08:00
parent 5568ea67d9
commit 489c2d191c
4 changed files with 79 additions and 33 deletions

View File

@@ -289,6 +289,18 @@ func (c *Client) registerHandlers() {
return c.writeTUN(pkt.Payload) return c.writeTUN(pkt.Payload)
}) })
// SDWAN raw packet (binary payload) from server
c.conn.OnMessage(protocol.MsgTunnel, protocol.SubTunnelSDWANRaw, func(data []byte) error {
if len(data) <= protocol.HeaderSize {
return nil
}
payload := data[protocol.HeaderSize:]
if len(payload) == 0 {
return nil
}
return c.writeTUN(payload)
})
// Handle edit app push // Handle edit app push
c.conn.OnMessage(protocol.MsgPush, protocol.SubPushEditApp, func(data []byte) error { c.conn.OnMessage(protocol.MsgPush, protocol.SubPushEditApp, func(data []byte) error {
var app protocol.AppConfig var app protocol.AppConfig
@@ -643,18 +655,15 @@ func (c *Client) tunReadLoop() {
continue continue
} }
dstIP := net.IP(pkt[16:20]).String() dstIP := net.IP(pkt[16:20]).String()
srcIP := net.IP(pkt[12:16]).String()
c.sdwanMu.RLock() c.sdwanMu.RLock()
self := c.sdwanIP self := c.sdwanIP
c.sdwanMu.RUnlock() c.sdwanMu.RUnlock()
if dstIP == self { if dstIP == self {
continue continue
} }
_ = c.conn.Write(protocol.MsgTunnel, protocol.SubTunnelSDWANData, protocol.SDWANPacket{ // send raw binary to avoid JSON base64 overhead
SrcIP: srcIP, frame := protocol.EncodeRaw(protocol.MsgTunnel, protocol.SubTunnelSDWANRaw, pkt)
DstIP: dstIP, _ = c.conn.WriteRaw(frame)
Payload: append([]byte(nil), pkt...),
})
} }
} }
@@ -690,6 +699,12 @@ func runCmd(name string, args ...string) error {
// Stop shuts down the client. // Stop shuts down the client.
func (c *Client) Stop() { func (c *Client) Stop() {
close(c.quit) close(c.quit)
c.tunMu.Lock()
if c.tunFile != nil {
_ = c.tunFile.Close()
c.tunFile = nil
}
c.tunMu.Unlock()
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
} }
@@ -701,12 +716,6 @@ func (c *Client) Stop() {
t.Close() t.Close()
} }
c.tMu.Unlock() c.tMu.Unlock()
c.tunMu.Lock()
if c.tunFile != nil {
_ = c.tunFile.Close()
c.tunFile = nil
}
c.tunMu.Unlock()
c.wg.Wait() c.wg.Wait()
} }

View File

@@ -143,5 +143,6 @@ func (s *Server) RouteSDWANPacket(from *NodeInfo, pkt protocol.SDWANPacket) {
pkt.FromNode = from.Name pkt.FromNode = from.Name
pkt.ToNode = toNode pkt.ToNode = toNode
_ = to.Conn.Write(protocol.MsgTunnel, protocol.SubTunnelSDWANData, pkt) frame := protocol.EncodeRaw(protocol.MsgTunnel, protocol.SubTunnelSDWANRaw, pkt.Payload)
_ = to.Conn.WriteRaw(frame)
} }

View File

@@ -309,7 +309,7 @@ func (s *Server) registerHandlers(conn *signal.Conn, node *NodeInfo) {
return s.handleRelayNodeReq(conn, node, req) return s.handleRelayNodeReq(conn, node, req)
}) })
// SDWAN data plane packet relay (server as control-plane router) // SDWAN data plane packet relay (JSON control payload)
conn.OnMessage(protocol.MsgTunnel, protocol.SubTunnelSDWANData, func(data []byte) error { conn.OnMessage(protocol.MsgTunnel, protocol.SubTunnelSDWANData, func(data []byte) error {
var pkt protocol.SDWANPacket var pkt protocol.SDWANPacket
if err := protocol.DecodePayload(data, &pkt); err != nil { if err := protocol.DecodePayload(data, &pkt); err != nil {
@@ -318,6 +318,26 @@ func (s *Server) registerHandlers(conn *signal.Conn, node *NodeInfo) {
s.RouteSDWANPacket(node, pkt) s.RouteSDWANPacket(node, pkt)
return nil return nil
}) })
// SDWAN data plane packet relay (raw IP payload)
conn.OnMessage(protocol.MsgTunnel, protocol.SubTunnelSDWANRaw, func(data []byte) error {
if len(data) <= protocol.HeaderSize {
return nil
}
payload := data[protocol.HeaderSize:]
if len(payload) < 20 {
return nil
}
version := payload[0] >> 4
if version != 4 {
return nil
}
srcIP := net.IP(payload[12:16]).String()
dstIP := net.IP(payload[16:20]).String()
pkt := protocol.SDWANPacket{SrcIP: srcIP, DstIP: dstIP, Payload: payload}
s.RouteSDWANPacket(node, pkt)
return nil
})
} }
// handleRelayNodeReq finds and returns the best relay node. // handleRelayNodeReq finds and returns the best relay node.

View File

@@ -1,6 +1,7 @@
// Package protocol defines the INP2P wire protocol. // Package protocol defines the INP2P wire protocol.
// //
// Message format: [Header 8B] + [JSON payload] // Message format: [Header 8B] + [JSON payload]
//
// Header: DataLen(uint32 LE) + MainType(uint16 LE) + SubType(uint16 LE) // Header: DataLen(uint32 LE) + MainType(uint16 LE) + SubType(uint16 LE)
// DataLen = len(header) + len(payload) = 8 + len(json) // DataLen = len(header) + len(payload) = 8 + len(json)
package protocol package protocol
@@ -69,6 +70,7 @@ const (
// Sub types: MsgTunnel // Sub types: MsgTunnel
const ( const (
SubTunnelSDWANData uint16 = iota SubTunnelSDWANData uint16 = iota
SubTunnelSDWANRaw
) )
// ─── Sub types: MsgRelay ─── // ─── Sub types: MsgRelay ───
@@ -151,6 +153,20 @@ func Encode(mainType, subType uint16, payload interface{}) ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
// EncodeRaw packs header + raw (binary) payload.
func EncodeRaw(mainType, subType uint16, payload []byte) []byte {
h := Header{
DataLen: uint32(HeaderSize + len(payload)),
MainType: mainType,
SubType: subType,
}
buf := new(bytes.Buffer)
buf.Grow(int(h.DataLen))
_ = binary.Write(buf, binary.LittleEndian, h)
buf.Write(payload)
return buf.Bytes()
}
// DecodeHeader reads the 8-byte header from r. // DecodeHeader reads the 8-byte header from r.
func DecodeHeader(data []byte) (Header, error) { func DecodeHeader(data []byte) (Header, error) {
if len(data) < HeaderSize { if len(data) < HeaderSize {