feat: Docker Compose E2E test env, agent fixes, TAP MAC config, testclient binary

- Add docker-compose.yml with server + init + 3 clients + tester
- Add Dockerfile.server (CGO multi-stage) and Dockerfile.client
- Add .dockerignore for efficient builds
- Add cmd/testclient/main.go: full E2E test binary using SDK
- Fix agent handshake loop: only respond to new handshakes
- Add tapInterface.SetMAC() to match agent's generated MAC
- Call SetMAC() in agent SetTAPIP to fix ARP resolution
- Add data-plane debug logging for troubleshooting
- Change config database path to /tmp/data/ for container use
- Add SetMAC stub for non-Linux builds
This commit is contained in:
xieyao
2026-06-17 05:04:19 +08:00
parent e3147ac06f
commit 807c4cf323
9 changed files with 535 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ import (
"crypto/cipher"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"log/slog"
@@ -62,6 +63,11 @@ func NewAgent(client *Client) *Agent {
return a
}
func (a *Agent) SetIdentity(id *Identity) {
a.identity = id
a.localMAC = generateMAC(a.cfg.NetworkID, id.Address)
}
func (a *Agent) Start() error {
port := a.cfg.ListenPort
if port == 0 {
@@ -109,6 +115,104 @@ func (a *Agent) LocalPort() int {
return a.udpConn.LocalAddr().(*net.UDPAddr).Port
}
func (a *Agent) TAPName() string {
if a.tapDev != nil {
return a.tapDev.Name
}
return ""
}
func (a *Agent) SetTAPIP(cidr string) error {
if a.tapDev == nil {
return fmt.Errorf("tap not opened")
}
if err := a.tapDev.SetMAC(a.localMAC); err != nil {
a.log.Warn("set tap mac", "error", err)
}
return a.tapDev.SetIP(cidr)
}
func (a *Agent) ControllerHello() error {
ctrlAddr := &net.UDPAddr{
IP: a.parseControllerHost(),
Port: 19993,
}
payload := make([]byte, 32)
copy(payload, a.identity.PublicKey)
a.sendPacket(pktHandshake, 0, payload, ctrlAddr)
return nil
}
func (a *Agent) SyncPeers() error {
nodes, err := a.client.ListOnlineNodes()
if err != nil {
return err
}
myAddr := a.identity.Address.String()
for _, n := range nodes {
if n.NodeID == myAddr || n.PublicKey == "" {
continue
}
pk, err := hex.DecodeString(n.PublicKey)
if err != nil || len(pk) != 32 {
continue
}
ip := net.ParseIP(n.IPAddress)
if ip == nil || n.Port == 0 {
continue
}
peerAddr := AddressFromPublicKey(pk)
udpAddr := &net.UDPAddr{IP: ip, Port: n.Port}
var pubKeyArr [32]byte
copy(pubKeyArr[:], pk)
existing := a.peers.getByAddr(peerAddr)
if existing != nil && existing.Connected {
continue
}
peer := a.peers.upsert(peerAddr, pubKeyArr, udpAddr)
peer.Touch()
sendKey, recvKey := deriveKeys([]byte(a.cfg.PSK), a.identity.PublicKey, pk)
peer.sendCipher, _ = chacha20poly1305.New(sendKey[:])
peer.recvCipher, _ = chacha20poly1305.New(recvKey[:])
peer.Connected = true
a.log.Info("synced peer", "addr", peerAddr, "endpoint", udpAddr)
a.sendHello(peer)
}
return nil
}
func (a *Agent) PeerCount() int {
return len(a.peers.all())
}
func (a *Agent) parseControllerHost() net.IP {
host := a.cfg.ControllerURL
if len(host) > 7 && host[:7] == "http://" {
host = host[7:]
}
if len(host) > 8 && host[:8] == "https://" {
host = host[8:]
}
idx := indexOfByte(host, ':')
if idx >= 0 {
host = host[:idx]
}
return net.ParseIP(host)
}
func indexOfByte(s string, b byte) int {
for i := 0; i < len(s); i++ {
if s[i] == b {
return i
}
}
return -1
}
func (a *Agent) udpReadLoop() {
defer a.wg.Done()
buf := make([]byte, 65535)
@@ -162,16 +266,17 @@ func (a *Agent) handleHandshake(payload []byte, addr *net.UDPAddr) {
var pubKey [32]byte
copy(pubKey[:], payload[:32])
peerAddr := AddressFromPublicKey(pubKey[:])
existing := a.peers.getByAddr(peerAddr)
if existing != nil && existing.Connected {
existing.Touch()
return
}
peer := a.peers.upsert(peerAddr, pubKey, addr)
peer.Touch()
a.log.Info("handshake from peer", "addr", peerAddr, "endpoint", addr)
sendKey, recvKey := deriveKeys([]byte(a.cfg.PSK), a.identity.PublicKey, pubKey[:])
c, err := chacha20poly1305.New(sendKey[:])
if err != nil {
return
}
_ = c
var err error
peer.sendCipher, err = chacha20poly1305.New(sendKey[:])
if err != nil {
return
@@ -180,6 +285,7 @@ func (a *Agent) handleHandshake(payload []byte, addr *net.UDPAddr) {
if err != nil {
return
}
peer.Connected = true
hello := make([]byte, 32)
copy(hello, a.identity.PublicKey)
@@ -200,6 +306,7 @@ func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
binary.BigEndian.PutUint64(nonce[4:], binary.BigEndian.Uint64(payload[:8]))
decrypted, err := peer.recvCipher.Open(nil, nonce, payload[8:], nil)
if err != nil {
a.log.Warn("decrypt failed", "addr", peer.addr, "endpoint", addr)
return
}
frame := decrypted
@@ -207,6 +314,8 @@ func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
return
}
etherType := (uint16(frame[12]) << 8) | uint16(frame[13])
a.log.Info("handle data", "len", len(frame), "ether", fmt.Sprintf("0x%04x", etherType))
a.macTable.learn(frame[6:12], peer.addr)
dstMAC := frame[:6]
@@ -214,12 +323,14 @@ func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
_ = srcMAC
if isLocalMAC(dstMAC, a.localMAC[:]) {
a.log.Info("data to tap local")
if a.tapDev != nil {
a.tapDev.Write(frame)
}
return
}
if isBroadcastMAC(dstMAC) {
a.log.Info("data to tap broadcast", "ether", fmt.Sprintf("0x%04x", etherType))
if a.tapDev != nil {
a.tapDev.Write(frame)
}
@@ -229,12 +340,14 @@ func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
dstAddr := a.macTable.lookup(string(dstMAC))
if dstAddr != nil {
a.log.Info("data forward unicast", "ether", fmt.Sprintf("0x%04x", etherType))
if p := a.peers.getByAddr(*dstAddr); p != nil {
a.sendToPeer(frame, p)
} else {
a.broadcastToPeers(frame, peer.addr)
}
} else {
a.log.Info("data forward miss", "ether", fmt.Sprintf("0x%04x", etherType))
a.broadcastToPeers(frame, peer.addr)
}
}
@@ -257,21 +370,25 @@ func (a *Agent) tapReadLoop() {
if len(frame) < 14 {
continue
}
etherType := (uint16(frame[12]) << 8) | uint16(frame[13])
a.macTable.learn(frame[6:12], zeroAddr)
dstMAC := frame[:6]
if isBroadcastMAC(dstMAC) {
a.log.Info("tap broadcast", "len", n, "ether", fmt.Sprintf("0x%04x", etherType))
a.broadcastToPeers(frame, zeroAddr)
continue
}
dstAddr := a.macTable.lookup(string(dstMAC))
if dstAddr != nil {
a.log.Info("tap unicast", "len", n, "ether", fmt.Sprintf("0x%04x", etherType))
if p := a.peers.getByAddr(*dstAddr); p != nil {
a.sendToPeer(frame, p)
} else {
a.broadcastToPeers(frame, zeroAddr)
}
} else {
a.log.Info("tap miss", "len", n, "ether", fmt.Sprintf("0x%04x", etherType), "dst", fmt.Sprintf("%x", dstMAC))
a.broadcastToPeers(frame, zeroAddr)
}
}
@@ -281,11 +398,16 @@ func (a *Agent) maintenanceLoop() {
defer a.wg.Done()
ticker := time.NewTicker(maintenanceTick)
defer ticker.Stop()
var syncCount int
for {
select {
case <-a.ctx.Done():
return
case <-ticker.C:
syncCount++
if syncCount%3 == 0 {
a.SyncPeers()
}
a.peers.cleanDead()
a.macTable.clean()
for _, p := range a.peers.all() {
@@ -320,6 +442,7 @@ func (a *Agent) sendPacket(pktType byte, netID uint32, payload []byte, addr *net
func (a *Agent) sendToPeer(frame []byte, p *peer) {
if p == nil || p.sendCipher == nil {
a.log.Warn("sendToPeer: nil peer or cipher")
return
}
p.LastSend = time.Now()
@@ -330,6 +453,7 @@ func (a *Agent) sendToPeer(frame []byte, p *peer) {
header := make([]byte, 8)
binary.BigEndian.PutUint64(header, binary.BigEndian.Uint64(nonceBuf[4:]))
payload := append(header, encrypted...)
a.log.Info("send data", "to", p.addr, "len", len(payload))
a.sendPacket(pktData, a.cfg.NetworkID, payload, p.Endpoint)
}

View File

@@ -4,6 +4,7 @@ package sdk
import (
"fmt"
"os/exec"
"syscall"
"unsafe"
)
@@ -35,7 +36,6 @@ func openTap(name string, mtu int) (*tapInterface, error) {
}
ti := &tapInterface{Name: devName, MTU: mtu, fd: fd}
// Set MTU via socket ioctl
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
if err == nil {
defer syscall.Close(s)
@@ -45,7 +45,7 @@ func openTap(name string, mtu int) (*tapInterface, error) {
}
copy(mtuReq.name[:], []byte(devName))
mtuReq.mtu = int32(mtu)
syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), 0x400c4a52, uintptr(unsafe.Pointer(&mtuReq))) // SIOCSIFMTU
syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), 0x400c4a52, uintptr(unsafe.Pointer(&mtuReq)))
}
return ti, nil
}
@@ -63,6 +63,21 @@ func (ti *tapInterface) Close() error {
return syscall.Close(ti.fd)
}
func (ti *tapInterface) SetMAC(mac [6]byte) error {
macStr := fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5])
return exec.Command("ip", "link", "set", ti.Name, "address", macStr).Run()
}
func (ti *tapInterface) SetIP(cidr string) error {
if err := exec.Command("ip", "addr", "add", cidr, "dev", ti.Name).Run(); err != nil {
return fmt.Errorf("add ip: %w", err)
}
if err := exec.Command("ip", "link", "set", ti.Name, "up").Run(); err != nil {
return fmt.Errorf("link up: %w", err)
}
return nil
}
func indexOfZero(s string) int {
for i, c := range []byte(s) {
if c == 0 {

View File

@@ -24,3 +24,7 @@ func (ti *tapInterface) Write(buf []byte) error {
func (ti *tapInterface) Close() error {
return nil
}
func (ti *tapInterface) SetIP(cidr string) error {
return fmt.Errorf("TAP device requires Linux")
}