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:
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
web/node_modules/
|
||||||
|
bin/
|
||||||
|
.git/
|
||||||
|
*.md
|
||||||
9
Dockerfile.client
Normal file
9
Dockerfile.client
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
FROM golang:1.26-alpine AS builder
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN go build -o /zmclient ./cmd/testclient/
|
||||||
|
|
||||||
|
FROM alpine:3.19
|
||||||
|
RUN apk add --no-cache iproute2 iptables-legacy
|
||||||
|
COPY --from=builder /zmclient /usr/local/bin/zmclient
|
||||||
|
CMD ["zmclient"]
|
||||||
12
Dockerfile.server
Normal file
12
Dockerfile.server
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.26-alpine AS builder
|
||||||
|
RUN apk add --no-cache gcc musl-dev
|
||||||
|
WORKDIR /src
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=1 go build -o /zeromesh .
|
||||||
|
|
||||||
|
FROM alpine:3.19
|
||||||
|
RUN apk add --no-cache iproute2 iptables-legacy
|
||||||
|
COPY --from=builder /zeromesh /usr/local/bin/zeromesh
|
||||||
|
COPY config.yaml /etc/zeromesh/config.yaml
|
||||||
|
EXPOSE 10001 19993/udp
|
||||||
|
CMD ["zeromesh", "-config", "/etc/zeromesh/config.yaml"]
|
||||||
192
cmd/testclient/main.go
Normal file
192
cmd/testclient/main.go
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"zeromesh/sdk"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
serverURL = flag.String("server", "http://server:10001", "Controller HTTP URL")
|
||||||
|
name = flag.String("name", "client", "Node name")
|
||||||
|
udpPort = flag.Int("udp", 7001, "Agent UDP listen port")
|
||||||
|
tapName = flag.String("tap", "ztmesh0", "TAP device name")
|
||||||
|
identityPath = flag.String("identity", "", "Identity file path")
|
||||||
|
adminUser = flag.String("admin", "admin", "Admin username")
|
||||||
|
adminPass = flag.String("pass", "admin123456", "Admin password")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelInfo)
|
||||||
|
|
||||||
|
cfg := sdk.DefaultConfig()
|
||||||
|
cfg.ControllerURL = *serverURL
|
||||||
|
cfg.ListenPort = *udpPort
|
||||||
|
cfg.TapName = *tapName
|
||||||
|
cfg.PSK = "default-psk-change-me"
|
||||||
|
if *identityPath != "" {
|
||||||
|
cfg.IdentityPath = *identityPath
|
||||||
|
}
|
||||||
|
cli := sdk.New(cfg)
|
||||||
|
|
||||||
|
var token string
|
||||||
|
for i := 0; i < 30; i++ {
|
||||||
|
if err := cli.Login(*adminUser, *adminPass); err == nil {
|
||||||
|
token = cli.Token()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, err := cli.InitAdmin(*adminUser, *adminPass); err == nil {
|
||||||
|
token = cli.Token()
|
||||||
|
log.Print("initialized admin")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf("waiting for server (attempt %d)...", i+1)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
if token == "" {
|
||||||
|
log.Fatal("could not login or init admin after 30 attempts")
|
||||||
|
}
|
||||||
|
log.Printf("logged in as %s", *adminUser)
|
||||||
|
|
||||||
|
idPath := *identityPath
|
||||||
|
if idPath == "" {
|
||||||
|
idPath = fmt.Sprintf("/tmp/%s.id", *name)
|
||||||
|
}
|
||||||
|
id, err := sdk.LoadOrGenerateIdentity(idPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("identity: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("identity: %s pub=%s", id.Address, id.PublicKeyHex())
|
||||||
|
|
||||||
|
myIP := sdk.GetPreferredIP()
|
||||||
|
node, err := cli.RegisterNode(id.Address.String(), id.PublicKeyHex(), *name, myIP, *udpPort, "1.0.0")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("register node: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("registered node: %s", node.NodeID)
|
||||||
|
|
||||||
|
findNetwork := func() *sdk.Network {
|
||||||
|
networks, err := cli.ListNetworks()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for i := range networks {
|
||||||
|
if !strings.HasPrefix(networks[i].IPRange, "10.7.") && !strings.HasPrefix(networks[i].IPRange, "172.") && !strings.HasPrefix(networks[i].IPRange, "192.") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return &networks[i]
|
||||||
|
}
|
||||||
|
if len(networks) > 0 {
|
||||||
|
return &networks[0]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
myNet := findNetwork()
|
||||||
|
if myNet == nil {
|
||||||
|
created, err := cli.CreateNetwork(fmt.Sprintf("testnet-%s", *name), "")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("create network: %v", err)
|
||||||
|
}
|
||||||
|
myNet = created
|
||||||
|
log.Printf("created network: %s (%s) id=%d", myNet.Name, myNet.IPRange, myNet.NetworkID)
|
||||||
|
} else {
|
||||||
|
log.Printf("using network: %s (%s) id=%d", myNet.Name, myNet.IPRange, myNet.NetworkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var myIPAddr string
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
members, err := cli.NetworkMembers(myNet.NetworkID)
|
||||||
|
if err == nil {
|
||||||
|
for _, m := range members {
|
||||||
|
if m.NodeID == id.Address.String() {
|
||||||
|
myIPAddr = m.IPAddress
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if myIPAddr != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
log.Printf("authorizing self on network %d...", myNet.NetworkID)
|
||||||
|
cli.AuthorizeMember(myNet.NetworkID, id.Address.String())
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
if myIPAddr == "" {
|
||||||
|
log.Fatalf("could not get virtual IP after authorization")
|
||||||
|
}
|
||||||
|
log.Printf("my virtual IP: %s", myIPAddr)
|
||||||
|
|
||||||
|
agent := sdk.NewAgent(cli)
|
||||||
|
agent.SetIdentity(id)
|
||||||
|
|
||||||
|
if err := agent.Start(); err != nil {
|
||||||
|
log.Fatalf("agent start: %v", err)
|
||||||
|
}
|
||||||
|
defer agent.Stop()
|
||||||
|
log.Printf("agent started: tap=%s udp=%d", agent.TAPName(), agent.LocalPort())
|
||||||
|
|
||||||
|
agent.ControllerHello()
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
for i := 0; i < 6; i++ {
|
||||||
|
agent.SyncPeers()
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
c := agent.PeerCount()
|
||||||
|
if c > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cidrFull := myIPAddr + "/24"
|
||||||
|
if err := agent.SetTAPIP(cidrFull); err != nil {
|
||||||
|
log.Printf("set tap ip warning: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("tap ip set: %s", cidrFull)
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("tcp", fmt.Sprintf("%s:7777", myIPAddr))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("echo server listen error (expected if TAP not ready): %v", err)
|
||||||
|
} else {
|
||||||
|
defer listener.Close()
|
||||||
|
log.Printf("echo server listening on %s:7777", myIPAddr)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func(c net.Conn) {
|
||||||
|
defer c.Close()
|
||||||
|
buf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, err := c.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Write(buf[:n])
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
peerCount := agent.PeerCount()
|
||||||
|
log.Printf("%s ready: ip=%s tap=%s peers=%d", *name, myIPAddr, agent.TAPName(), peerCount)
|
||||||
|
|
||||||
|
sc := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sc
|
||||||
|
log.Printf("shutting down %s", *name)
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ database:
|
|||||||
auto_migrate: true
|
auto_migrate: true
|
||||||
init_data: true
|
init_data: true
|
||||||
sqlite:
|
sqlite:
|
||||||
path: ./data/zeromesh.db
|
path: /tmp/data/zeromesh.db
|
||||||
mysql:
|
mysql:
|
||||||
host: 127.0.0.1
|
host: 127.0.0.1
|
||||||
port: 3306
|
port: 3306
|
||||||
|
|||||||
167
docker-compose.yml
Normal file
167
docker-compose.yml
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
services:
|
||||||
|
server:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.server
|
||||||
|
container_name: zm-server
|
||||||
|
hostname: server
|
||||||
|
privileged: true
|
||||||
|
ports:
|
||||||
|
- "7100:10001"
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
mkdir -p /tmp/data && zeromesh -config /etc/zeromesh/config.yaml
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:10001/api/health"]
|
||||||
|
interval: 3s
|
||||||
|
retries: 30
|
||||||
|
start_period: 5s
|
||||||
|
networks:
|
||||||
|
zmt:
|
||||||
|
ipv4_address: 172.30.0.10
|
||||||
|
|
||||||
|
init:
|
||||||
|
image: alpine:3.19
|
||||||
|
container_name: zm-init
|
||||||
|
hostname: init
|
||||||
|
depends_on:
|
||||||
|
server:
|
||||||
|
condition: service_healthy
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
apk add --no-cache curl jq
|
||||||
|
echo '=== Create admin (try init then login) ==='
|
||||||
|
ADMIN=$$(curl -s -X POST http://server:10001/api/v1/auth/init \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"username":"admin","password":"admin123456"}')
|
||||||
|
TOKEN=$$(echo "$$ADMIN" | jq -r '.token // empty')
|
||||||
|
if [ -z "$$TOKEN" ]; then
|
||||||
|
echo 'Admin already exists, logging in...'
|
||||||
|
LOGIN=$$(curl -s -X POST http://server:10001/api/v1/auth/login \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"username":"admin","password":"admin123456"}')
|
||||||
|
TOKEN=$$(echo "$$LOGIN" | jq -r '.token // empty')
|
||||||
|
fi
|
||||||
|
if [ -z "$$TOKEN" ]; then echo "Failed to get admin token"; exit 1; fi
|
||||||
|
echo 'Admin token acquired'
|
||||||
|
sleep 1
|
||||||
|
echo '=== Create network ==='
|
||||||
|
curl -s -X POST http://server:10001/api/v1/network/create \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"name":"testnet","ip_range":"10.7.0.0/24"}'
|
||||||
|
echo ''
|
||||||
|
echo '=== init done ==='
|
||||||
|
networks:
|
||||||
|
- zmt
|
||||||
|
|
||||||
|
client1:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.client
|
||||||
|
container_name: zm-client1
|
||||||
|
hostname: client1
|
||||||
|
privileged: true
|
||||||
|
ports:
|
||||||
|
- "7201:7001/udp"
|
||||||
|
depends_on:
|
||||||
|
init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
zmclient -server http://server:10001 -name client1 -udp 7001 -tap ztmesh0 -identity /tmp/c1.id
|
||||||
|
networks:
|
||||||
|
zmt:
|
||||||
|
ipv4_address: 172.30.0.11
|
||||||
|
|
||||||
|
client2:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.client
|
||||||
|
container_name: zm-client2
|
||||||
|
hostname: client2
|
||||||
|
privileged: true
|
||||||
|
ports:
|
||||||
|
- "7202:7002/udp"
|
||||||
|
depends_on:
|
||||||
|
init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
zmclient -server http://server:10001 -name client2 -udp 7002 -tap ztmesh1 -identity /tmp/c2.id
|
||||||
|
networks:
|
||||||
|
zmt:
|
||||||
|
ipv4_address: 172.30.0.12
|
||||||
|
|
||||||
|
client3:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.client
|
||||||
|
container_name: zm-client3
|
||||||
|
hostname: client3
|
||||||
|
privileged: true
|
||||||
|
ports:
|
||||||
|
- "7203:7003/udp"
|
||||||
|
depends_on:
|
||||||
|
init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
zmclient -server http://server:10001 -name client3 -udp 7003 -tap ztmesh2 -identity /tmp/c3.id
|
||||||
|
networks:
|
||||||
|
zmt:
|
||||||
|
ipv4_address: 172.30.0.13
|
||||||
|
|
||||||
|
tester:
|
||||||
|
image: alpine:3.19
|
||||||
|
container_name: zm-tester
|
||||||
|
hostname: tester
|
||||||
|
privileged: true
|
||||||
|
depends_on:
|
||||||
|
init:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
apk add --no-cache iputils iproute2 curl bash
|
||||||
|
echo '=== Waiting for clients (60s) ==='
|
||||||
|
sleep 60
|
||||||
|
echo '=== Login ==='
|
||||||
|
LOGIN=$$(curl -s -X POST http://server:10001/api/v1/auth/login \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"username":"admin","password":"admin123456"}')
|
||||||
|
TOKEN=$$(echo "$$LOGIN" | sed 's/.*"token":"//' | sed 's/".*//')
|
||||||
|
echo '=== Networks ==='
|
||||||
|
curl -s http://server:10001/api/v1/network/list \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" | head -c 300
|
||||||
|
echo ''
|
||||||
|
echo '=== Members (network/1) ==='
|
||||||
|
curl -s http://server:10001/api/v1/network/1/members \
|
||||||
|
-H "Authorization: Bearer $$TOKEN" | sed 's/,/\n/g' | grep -E 'node_id|ip_address|authorized'
|
||||||
|
echo ''
|
||||||
|
echo '=== Online Nodes ==='
|
||||||
|
NODES=$$(curl -s http://server:10001/api/v1/node/online \
|
||||||
|
-H "Authorization: Bearer $$TOKEN")
|
||||||
|
echo "$$NODES" | sed 's/,/\n/g' | grep -E 'node_id|ip_address|port|name'
|
||||||
|
echo ''
|
||||||
|
echo '=== Try pinging virtual IPs ==='
|
||||||
|
networks:
|
||||||
|
- zmt
|
||||||
|
|
||||||
|
networks:
|
||||||
|
zmt:
|
||||||
|
driver: bridge
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
- subnet: 172.30.0.0/24
|
||||||
134
sdk/agent.go
134
sdk/agent.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -62,6 +63,11 @@ func NewAgent(client *Client) *Agent {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Agent) SetIdentity(id *Identity) {
|
||||||
|
a.identity = id
|
||||||
|
a.localMAC = generateMAC(a.cfg.NetworkID, id.Address)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Agent) Start() error {
|
func (a *Agent) Start() error {
|
||||||
port := a.cfg.ListenPort
|
port := a.cfg.ListenPort
|
||||||
if port == 0 {
|
if port == 0 {
|
||||||
@@ -109,6 +115,104 @@ func (a *Agent) LocalPort() int {
|
|||||||
return a.udpConn.LocalAddr().(*net.UDPAddr).Port
|
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() {
|
func (a *Agent) udpReadLoop() {
|
||||||
defer a.wg.Done()
|
defer a.wg.Done()
|
||||||
buf := make([]byte, 65535)
|
buf := make([]byte, 65535)
|
||||||
@@ -162,16 +266,17 @@ func (a *Agent) handleHandshake(payload []byte, addr *net.UDPAddr) {
|
|||||||
var pubKey [32]byte
|
var pubKey [32]byte
|
||||||
copy(pubKey[:], payload[:32])
|
copy(pubKey[:], payload[:32])
|
||||||
peerAddr := AddressFromPublicKey(pubKey[:])
|
peerAddr := AddressFromPublicKey(pubKey[:])
|
||||||
|
existing := a.peers.getByAddr(peerAddr)
|
||||||
|
if existing != nil && existing.Connected {
|
||||||
|
existing.Touch()
|
||||||
|
return
|
||||||
|
}
|
||||||
peer := a.peers.upsert(peerAddr, pubKey, addr)
|
peer := a.peers.upsert(peerAddr, pubKey, addr)
|
||||||
peer.Touch()
|
peer.Touch()
|
||||||
a.log.Info("handshake from peer", "addr", peerAddr, "endpoint", addr)
|
a.log.Info("handshake from peer", "addr", peerAddr, "endpoint", addr)
|
||||||
|
|
||||||
sendKey, recvKey := deriveKeys([]byte(a.cfg.PSK), a.identity.PublicKey, pubKey[:])
|
sendKey, recvKey := deriveKeys([]byte(a.cfg.PSK), a.identity.PublicKey, pubKey[:])
|
||||||
c, err := chacha20poly1305.New(sendKey[:])
|
var err error
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = c
|
|
||||||
peer.sendCipher, err = chacha20poly1305.New(sendKey[:])
|
peer.sendCipher, err = chacha20poly1305.New(sendKey[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -180,6 +285,7 @@ func (a *Agent) handleHandshake(payload []byte, addr *net.UDPAddr) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
peer.Connected = true
|
||||||
|
|
||||||
hello := make([]byte, 32)
|
hello := make([]byte, 32)
|
||||||
copy(hello, a.identity.PublicKey)
|
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]))
|
binary.BigEndian.PutUint64(nonce[4:], binary.BigEndian.Uint64(payload[:8]))
|
||||||
decrypted, err := peer.recvCipher.Open(nil, nonce, payload[8:], nil)
|
decrypted, err := peer.recvCipher.Open(nil, nonce, payload[8:], nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
a.log.Warn("decrypt failed", "addr", peer.addr, "endpoint", addr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
frame := decrypted
|
frame := decrypted
|
||||||
@@ -207,6 +314,8 @@ func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
|
|||||||
return
|
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)
|
a.macTable.learn(frame[6:12], peer.addr)
|
||||||
|
|
||||||
dstMAC := frame[:6]
|
dstMAC := frame[:6]
|
||||||
@@ -214,12 +323,14 @@ func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
|
|||||||
_ = srcMAC
|
_ = srcMAC
|
||||||
|
|
||||||
if isLocalMAC(dstMAC, a.localMAC[:]) {
|
if isLocalMAC(dstMAC, a.localMAC[:]) {
|
||||||
|
a.log.Info("data to tap local")
|
||||||
if a.tapDev != nil {
|
if a.tapDev != nil {
|
||||||
a.tapDev.Write(frame)
|
a.tapDev.Write(frame)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if isBroadcastMAC(dstMAC) {
|
if isBroadcastMAC(dstMAC) {
|
||||||
|
a.log.Info("data to tap broadcast", "ether", fmt.Sprintf("0x%04x", etherType))
|
||||||
if a.tapDev != nil {
|
if a.tapDev != nil {
|
||||||
a.tapDev.Write(frame)
|
a.tapDev.Write(frame)
|
||||||
}
|
}
|
||||||
@@ -229,12 +340,14 @@ func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
|
|||||||
|
|
||||||
dstAddr := a.macTable.lookup(string(dstMAC))
|
dstAddr := a.macTable.lookup(string(dstMAC))
|
||||||
if dstAddr != nil {
|
if dstAddr != nil {
|
||||||
|
a.log.Info("data forward unicast", "ether", fmt.Sprintf("0x%04x", etherType))
|
||||||
if p := a.peers.getByAddr(*dstAddr); p != nil {
|
if p := a.peers.getByAddr(*dstAddr); p != nil {
|
||||||
a.sendToPeer(frame, p)
|
a.sendToPeer(frame, p)
|
||||||
} else {
|
} else {
|
||||||
a.broadcastToPeers(frame, peer.addr)
|
a.broadcastToPeers(frame, peer.addr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
a.log.Info("data forward miss", "ether", fmt.Sprintf("0x%04x", etherType))
|
||||||
a.broadcastToPeers(frame, peer.addr)
|
a.broadcastToPeers(frame, peer.addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,21 +370,25 @@ func (a *Agent) tapReadLoop() {
|
|||||||
if len(frame) < 14 {
|
if len(frame) < 14 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
etherType := (uint16(frame[12]) << 8) | uint16(frame[13])
|
||||||
a.macTable.learn(frame[6:12], zeroAddr)
|
a.macTable.learn(frame[6:12], zeroAddr)
|
||||||
|
|
||||||
dstMAC := frame[:6]
|
dstMAC := frame[:6]
|
||||||
if isBroadcastMAC(dstMAC) {
|
if isBroadcastMAC(dstMAC) {
|
||||||
|
a.log.Info("tap broadcast", "len", n, "ether", fmt.Sprintf("0x%04x", etherType))
|
||||||
a.broadcastToPeers(frame, zeroAddr)
|
a.broadcastToPeers(frame, zeroAddr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dstAddr := a.macTable.lookup(string(dstMAC))
|
dstAddr := a.macTable.lookup(string(dstMAC))
|
||||||
if dstAddr != nil {
|
if dstAddr != nil {
|
||||||
|
a.log.Info("tap unicast", "len", n, "ether", fmt.Sprintf("0x%04x", etherType))
|
||||||
if p := a.peers.getByAddr(*dstAddr); p != nil {
|
if p := a.peers.getByAddr(*dstAddr); p != nil {
|
||||||
a.sendToPeer(frame, p)
|
a.sendToPeer(frame, p)
|
||||||
} else {
|
} else {
|
||||||
a.broadcastToPeers(frame, zeroAddr)
|
a.broadcastToPeers(frame, zeroAddr)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
a.log.Info("tap miss", "len", n, "ether", fmt.Sprintf("0x%04x", etherType), "dst", fmt.Sprintf("%x", dstMAC))
|
||||||
a.broadcastToPeers(frame, zeroAddr)
|
a.broadcastToPeers(frame, zeroAddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,11 +398,16 @@ func (a *Agent) maintenanceLoop() {
|
|||||||
defer a.wg.Done()
|
defer a.wg.Done()
|
||||||
ticker := time.NewTicker(maintenanceTick)
|
ticker := time.NewTicker(maintenanceTick)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
var syncCount int
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-a.ctx.Done():
|
case <-a.ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
|
syncCount++
|
||||||
|
if syncCount%3 == 0 {
|
||||||
|
a.SyncPeers()
|
||||||
|
}
|
||||||
a.peers.cleanDead()
|
a.peers.cleanDead()
|
||||||
a.macTable.clean()
|
a.macTable.clean()
|
||||||
for _, p := range a.peers.all() {
|
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) {
|
func (a *Agent) sendToPeer(frame []byte, p *peer) {
|
||||||
if p == nil || p.sendCipher == nil {
|
if p == nil || p.sendCipher == nil {
|
||||||
|
a.log.Warn("sendToPeer: nil peer or cipher")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.LastSend = time.Now()
|
p.LastSend = time.Now()
|
||||||
@@ -330,6 +453,7 @@ func (a *Agent) sendToPeer(frame []byte, p *peer) {
|
|||||||
header := make([]byte, 8)
|
header := make([]byte, 8)
|
||||||
binary.BigEndian.PutUint64(header, binary.BigEndian.Uint64(nonceBuf[4:]))
|
binary.BigEndian.PutUint64(header, binary.BigEndian.Uint64(nonceBuf[4:]))
|
||||||
payload := append(header, encrypted...)
|
payload := append(header, encrypted...)
|
||||||
|
a.log.Info("send data", "to", p.addr, "len", len(payload))
|
||||||
a.sendPacket(pktData, a.cfg.NetworkID, payload, p.Endpoint)
|
a.sendPacket(pktData, a.cfg.NetworkID, payload, p.Endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package sdk
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
"syscall"
|
"syscall"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@@ -35,7 +36,6 @@ func openTap(name string, mtu int) (*tapInterface, error) {
|
|||||||
}
|
}
|
||||||
ti := &tapInterface{Name: devName, MTU: mtu, fd: fd}
|
ti := &tapInterface{Name: devName, MTU: mtu, fd: fd}
|
||||||
|
|
||||||
// Set MTU via socket ioctl
|
|
||||||
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
|
s, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, 0)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
defer syscall.Close(s)
|
defer syscall.Close(s)
|
||||||
@@ -45,7 +45,7 @@ func openTap(name string, mtu int) (*tapInterface, error) {
|
|||||||
}
|
}
|
||||||
copy(mtuReq.name[:], []byte(devName))
|
copy(mtuReq.name[:], []byte(devName))
|
||||||
mtuReq.mtu = int32(mtu)
|
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
|
return ti, nil
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,21 @@ func (ti *tapInterface) Close() error {
|
|||||||
return syscall.Close(ti.fd)
|
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 {
|
func indexOfZero(s string) int {
|
||||||
for i, c := range []byte(s) {
|
for i, c := range []byte(s) {
|
||||||
if c == 0 {
|
if c == 0 {
|
||||||
|
|||||||
@@ -24,3 +24,7 @@ func (ti *tapInterface) Write(buf []byte) error {
|
|||||||
func (ti *tapInterface) Close() error {
|
func (ti *tapInterface) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ti *tapInterface) SetIP(cidr string) error {
|
||||||
|
return fmt.Errorf("TAP device requires Linux")
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user