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:
134
sdk/agent.go
134
sdk/agent.go
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user