feat: add Go SDK package (REST client + VL1 agent + TAP device)
This commit is contained in:
529
sdk/agent.go
Normal file
529
sdk/agent.go
Normal file
@@ -0,0 +1,529 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
vl1Version = 1
|
||||||
|
vl1HeaderSize = 8
|
||||||
|
pktHandshake = byte(1)
|
||||||
|
pktData = byte(2)
|
||||||
|
pktKeepalive = byte(3)
|
||||||
|
keepaliveInterval = 25 * time.Second
|
||||||
|
peerTimeout = 90 * time.Second
|
||||||
|
maintenanceTick = 10 * time.Second
|
||||||
|
macTableCap = 4096
|
||||||
|
macTableExpiry = 5 * time.Minute
|
||||||
|
)
|
||||||
|
|
||||||
|
type Agent struct {
|
||||||
|
cfg Config
|
||||||
|
client *Client
|
||||||
|
identity *Identity
|
||||||
|
udpConn *net.UDPConn
|
||||||
|
peers *peerManager
|
||||||
|
tapDev *tapInterface
|
||||||
|
log *slog.Logger
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
wg sync.WaitGroup
|
||||||
|
localMAC [6]byte
|
||||||
|
macTable *macTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAgent(client *Client) *Agent {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
a := &Agent{
|
||||||
|
cfg: client.cfg,
|
||||||
|
client: client,
|
||||||
|
identity: client.identity,
|
||||||
|
log: slog.Default().With("component", "sdk-agent"),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
if a.identity == nil {
|
||||||
|
a.identity = GenerateIdentity()
|
||||||
|
}
|
||||||
|
a.peers = newPeerManager()
|
||||||
|
a.macTable = newMACTable()
|
||||||
|
a.localMAC = generateMAC(a.cfg.NetworkID, a.identity.Address)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) Start() error {
|
||||||
|
port := a.cfg.ListenPort
|
||||||
|
if port == 0 {
|
||||||
|
port = 0 // OS auto-assign
|
||||||
|
}
|
||||||
|
udpAddr := &net.UDPAddr{Port: port}
|
||||||
|
conn, err := net.ListenUDP("udp", udpAddr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bind udp: %w", err)
|
||||||
|
}
|
||||||
|
a.udpConn = conn
|
||||||
|
a.log.Info("udp bound", "port", conn.LocalAddr().(*net.UDPAddr).Port)
|
||||||
|
|
||||||
|
tap, err := openTap(a.cfg.TapName, a.cfg.TapMTU)
|
||||||
|
if err != nil {
|
||||||
|
a.udpConn.Close()
|
||||||
|
return fmt.Errorf("open tap: %w", err)
|
||||||
|
}
|
||||||
|
a.tapDev = tap
|
||||||
|
a.log.Info("tap device opened", "name", tap.Name, "mtu", tap.MTU)
|
||||||
|
|
||||||
|
a.wg.Add(3)
|
||||||
|
go a.udpReadLoop()
|
||||||
|
go a.tapReadLoop()
|
||||||
|
go a.maintenanceLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) Stop() {
|
||||||
|
a.cancel()
|
||||||
|
if a.tapDev != nil {
|
||||||
|
a.tapDev.Close()
|
||||||
|
}
|
||||||
|
if a.udpConn != nil {
|
||||||
|
a.udpConn.Close()
|
||||||
|
}
|
||||||
|
a.wg.Wait()
|
||||||
|
a.log.Info("agent stopped")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) LocalPort() int {
|
||||||
|
if a.udpConn == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return a.udpConn.LocalAddr().(*net.UDPAddr).Port
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) udpReadLoop() {
|
||||||
|
defer a.wg.Done()
|
||||||
|
buf := make([]byte, 65535)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
a.udpConn.SetReadDeadline(time.Now().Add(1 * time.Second))
|
||||||
|
n, addr, err := a.udpConn.ReadFromUDP(buf)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
data := make([]byte, n)
|
||||||
|
copy(data, buf[:n])
|
||||||
|
a.handlePacket(data, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handlePacket(data []byte, addr *net.UDPAddr) {
|
||||||
|
if len(data) < vl1HeaderSize {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pktType := data[1]
|
||||||
|
netID := binary.BigEndian.Uint32(data[2:6])
|
||||||
|
_ = netID
|
||||||
|
payloadLen := int(binary.BigEndian.Uint16(data[6:8]))
|
||||||
|
if vl1HeaderSize+payloadLen > len(data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload := data[vl1HeaderSize : vl1HeaderSize+payloadLen]
|
||||||
|
|
||||||
|
switch pktType {
|
||||||
|
case pktHandshake:
|
||||||
|
a.handleHandshake(payload, addr)
|
||||||
|
case pktData:
|
||||||
|
a.handleData(payload, addr)
|
||||||
|
case pktKeepalive:
|
||||||
|
a.peers.touchByEndpoint(addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleHandshake(payload []byte, addr *net.UDPAddr) {
|
||||||
|
if len(payload) < 32 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var pubKey [32]byte
|
||||||
|
copy(pubKey[:], payload[:32])
|
||||||
|
peerAddr := AddressFromPublicKey(pubKey[:])
|
||||||
|
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
|
||||||
|
peer.sendCipher, err = chacha20poly1305.New(sendKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer.recvCipher, err = chacha20poly1305.New(recvKey[:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
hello := make([]byte, 32)
|
||||||
|
copy(hello, a.identity.PublicKey)
|
||||||
|
a.sendPacket(pktHandshake, 0, hello, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) handleData(payload []byte, addr *net.UDPAddr) {
|
||||||
|
peer := a.peers.getByEndpoint(addr)
|
||||||
|
if peer == nil || peer.recvCipher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
peer.Touch()
|
||||||
|
|
||||||
|
nonce := make([]byte, 12)
|
||||||
|
if len(payload) < 8 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint64(nonce[4:], binary.BigEndian.Uint64(payload[:8]))
|
||||||
|
decrypted, err := peer.recvCipher.Open(nil, nonce, payload[8:], nil)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
frame := decrypted
|
||||||
|
if len(frame) < 14 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.macTable.learn(frame[6:12], peer.addr)
|
||||||
|
|
||||||
|
dstMAC := frame[:6]
|
||||||
|
srcMAC := frame[6:12]
|
||||||
|
_ = srcMAC
|
||||||
|
|
||||||
|
if isLocalMAC(dstMAC, a.localMAC[:]) {
|
||||||
|
if a.tapDev != nil {
|
||||||
|
a.tapDev.Write(frame)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if isBroadcastMAC(dstMAC) {
|
||||||
|
if a.tapDev != nil {
|
||||||
|
a.tapDev.Write(frame)
|
||||||
|
}
|
||||||
|
a.broadcastToPeers(frame, peer.addr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dstAddr := a.macTable.lookup(string(dstMAC))
|
||||||
|
if dstAddr != nil {
|
||||||
|
if p := a.peers.getByAddr(*dstAddr); p != nil {
|
||||||
|
a.sendToPeer(frame, p)
|
||||||
|
} else {
|
||||||
|
a.broadcastToPeers(frame, peer.addr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a.broadcastToPeers(frame, peer.addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) tapReadLoop() {
|
||||||
|
defer a.wg.Done()
|
||||||
|
buf := make([]byte, a.cfg.TapMTU+14+64)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
n, err := a.tapDev.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
frame := make([]byte, n)
|
||||||
|
copy(frame, buf[:n])
|
||||||
|
if len(frame) < 14 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.macTable.learn(frame[6:12], zeroAddr)
|
||||||
|
|
||||||
|
dstMAC := frame[:6]
|
||||||
|
if isBroadcastMAC(dstMAC) {
|
||||||
|
a.broadcastToPeers(frame, zeroAddr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
dstAddr := a.macTable.lookup(string(dstMAC))
|
||||||
|
if dstAddr != nil {
|
||||||
|
if p := a.peers.getByAddr(*dstAddr); p != nil {
|
||||||
|
a.sendToPeer(frame, p)
|
||||||
|
} else {
|
||||||
|
a.broadcastToPeers(frame, zeroAddr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a.broadcastToPeers(frame, zeroAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) maintenanceLoop() {
|
||||||
|
defer a.wg.Done()
|
||||||
|
ticker := time.NewTicker(maintenanceTick)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-a.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
a.peers.cleanDead()
|
||||||
|
a.macTable.clean()
|
||||||
|
for _, p := range a.peers.all() {
|
||||||
|
if p.NeedsKeepalive() {
|
||||||
|
a.sendPacket(pktKeepalive, 0, nil, p.Endpoint)
|
||||||
|
p.LastSend = time.Now()
|
||||||
|
}
|
||||||
|
if !p.Connected && p.sendCipher != nil {
|
||||||
|
p.Connected = true
|
||||||
|
}
|
||||||
|
if !p.Connected {
|
||||||
|
a.sendHello(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) sendPacket(pktType byte, netID uint32, payload []byte, addr *net.UDPAddr) {
|
||||||
|
if addr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
total := vl1HeaderSize + len(payload)
|
||||||
|
buf := make([]byte, total)
|
||||||
|
buf[0] = vl1Version
|
||||||
|
buf[1] = pktType
|
||||||
|
binary.BigEndian.PutUint32(buf[2:6], netID)
|
||||||
|
binary.BigEndian.PutUint16(buf[6:8], uint16(len(payload)))
|
||||||
|
copy(buf[vl1HeaderSize:], payload)
|
||||||
|
a.udpConn.WriteTo(buf, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) sendToPeer(frame []byte, p *peer) {
|
||||||
|
if p == nil || p.sendCipher == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.LastSend = time.Now()
|
||||||
|
nonceBuf := make([]byte, 12)
|
||||||
|
binary.BigEndian.PutUint64(nonceBuf[4:], p.sendNonce)
|
||||||
|
p.sendNonce++
|
||||||
|
encrypted := p.sendCipher.Seal(nil, nonceBuf, frame, nil)
|
||||||
|
header := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(header, binary.BigEndian.Uint64(nonceBuf[4:]))
|
||||||
|
payload := append(header, encrypted...)
|
||||||
|
a.sendPacket(pktData, a.cfg.NetworkID, payload, p.Endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) broadcastToPeers(frame []byte, exclude Address) {
|
||||||
|
for _, p := range a.peers.all() {
|
||||||
|
if p.addr == exclude {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if p.sendCipher == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a.sendToPeer(frame, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Agent) sendHello(p *peer) {
|
||||||
|
payload := make([]byte, 32)
|
||||||
|
copy(payload, a.identity.PublicKey)
|
||||||
|
a.sendPacket(pktHandshake, 0, payload, p.Endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroAddr Address
|
||||||
|
|
||||||
|
type peer struct {
|
||||||
|
addr Address
|
||||||
|
pubKey [32]byte
|
||||||
|
Endpoint *net.UDPAddr
|
||||||
|
sendCipher cipher.AEAD
|
||||||
|
recvCipher cipher.AEAD
|
||||||
|
Connected bool
|
||||||
|
LastSeen time.Time
|
||||||
|
LastSend time.Time
|
||||||
|
sendNonce uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *peer) Touch() { p.LastSeen = time.Now() }
|
||||||
|
func (p *peer) NeedsKeepalive() bool { return time.Since(p.LastSend) > keepaliveInterval }
|
||||||
|
func (p *peer) IsAlive() bool { return time.Since(p.LastSeen) < peerTimeout }
|
||||||
|
|
||||||
|
type peerManager struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
peers map[Address]*peer
|
||||||
|
epIdx map[string]*peer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPeerManager() *peerManager {
|
||||||
|
return &peerManager{
|
||||||
|
peers: make(map[Address]*peer),
|
||||||
|
epIdx: make(map[string]*peer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *peerManager) upsert(addr Address, pubKey [32]byte, ep *net.UDPAddr) *peer {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
if p, ok := pm.peers[addr]; ok {
|
||||||
|
p.Endpoint = ep
|
||||||
|
pm.epIdx[ep.String()] = p
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
p := &peer{addr: addr, pubKey: pubKey, Endpoint: ep}
|
||||||
|
pm.peers[addr] = p
|
||||||
|
pm.epIdx[ep.String()] = p
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *peerManager) getByEndpoint(ep *net.UDPAddr) *peer {
|
||||||
|
pm.mu.RLock()
|
||||||
|
defer pm.mu.RUnlock()
|
||||||
|
return pm.epIdx[ep.String()]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *peerManager) getByAddr(addr Address) *peer {
|
||||||
|
pm.mu.RLock()
|
||||||
|
defer pm.mu.RUnlock()
|
||||||
|
return pm.peers[addr]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *peerManager) touchByEndpoint(ep *net.UDPAddr) {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
if p, ok := pm.epIdx[ep.String()]; ok {
|
||||||
|
p.Touch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *peerManager) all() []*peer {
|
||||||
|
pm.mu.RLock()
|
||||||
|
defer pm.mu.RUnlock()
|
||||||
|
out := make([]*peer, 0, len(pm.peers))
|
||||||
|
for _, p := range pm.peers {
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pm *peerManager) cleanDead() {
|
||||||
|
pm.mu.Lock()
|
||||||
|
defer pm.mu.Unlock()
|
||||||
|
for addr, p := range pm.peers {
|
||||||
|
if !p.IsAlive() {
|
||||||
|
delete(pm.epIdx, p.Endpoint.String())
|
||||||
|
delete(pm.peers, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type macEntry struct {
|
||||||
|
peerAddr Address
|
||||||
|
added time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type macTable struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
tbl map[string]macEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMACTable() *macTable {
|
||||||
|
return &macTable{tbl: make(map[string]macEntry)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *macTable) learn(mac []byte, peerAddr Address) {
|
||||||
|
mt.mu.Lock()
|
||||||
|
defer mt.mu.Unlock()
|
||||||
|
if len(mt.tbl) >= macTableCap {
|
||||||
|
for k, v := range mt.tbl {
|
||||||
|
if time.Since(v.added) > macTableExpiry {
|
||||||
|
delete(mt.tbl, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mt.tbl[string(mac)] = macEntry{peerAddr: peerAddr, added: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *macTable) lookup(mac string) *Address {
|
||||||
|
mt.mu.RLock()
|
||||||
|
defer mt.mu.RUnlock()
|
||||||
|
e, ok := mt.tbl[mac]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
zero := Address{}
|
||||||
|
if e.peerAddr == zero {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &e.peerAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *macTable) clean() {
|
||||||
|
mt.mu.Lock()
|
||||||
|
defer mt.mu.Unlock()
|
||||||
|
for k, v := range mt.tbl {
|
||||||
|
if time.Since(v.added) > macTableExpiry {
|
||||||
|
delete(mt.tbl, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateMAC(networkID uint32, addr Address) [6]byte {
|
||||||
|
var mac [6]byte
|
||||||
|
mac[0] = 0x02
|
||||||
|
binary.BigEndian.PutUint32(mac[1:5], networkID)
|
||||||
|
mac[5] = addr[4]
|
||||||
|
return mac
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocalMAC(dst, local []byte) bool {
|
||||||
|
return dst[0] == local[0] && dst[1] == local[1] && dst[2] == local[2] &&
|
||||||
|
dst[3] == local[3] && dst[4] == local[4] && dst[5] == local[5]
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBroadcastMAC(dst []byte) bool {
|
||||||
|
for _, b := range dst {
|
||||||
|
if b != 0xff {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func deriveKeys(psk, localPub, remotePub []byte) ([32]byte, [32]byte) {
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write(psk)
|
||||||
|
h.Write(localPub)
|
||||||
|
h.Write(remotePub)
|
||||||
|
var sendKey [32]byte
|
||||||
|
copy(sendKey[:], h.Sum(nil))
|
||||||
|
|
||||||
|
h2 := sha256.New()
|
||||||
|
h2.Write(sendKey[:])
|
||||||
|
h2.Write([]byte("reverse"))
|
||||||
|
var recvKey [32]byte
|
||||||
|
copy(recvKey[:], h2.Sum(nil))
|
||||||
|
return sendKey, recvKey
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
210
sdk/api.go
Normal file
210
sdk/api.go
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Client) doReq(method, path string, body, out interface{}) error {
|
||||||
|
url := strings.TrimRight(c.cfg.ControllerURL, "/") + path
|
||||||
|
var r io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("marshal body: %w", err)
|
||||||
|
}
|
||||||
|
r = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest(method, url, r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
if c.cfg.Token != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+c.cfg.Token)
|
||||||
|
}
|
||||||
|
if r != nil {
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
resp, err := c.httpc.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("do request: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
respData, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read response: %w", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode >= 400 {
|
||||||
|
var errResp struct{ Error string `json:"error"` }
|
||||||
|
if json.Unmarshal(respData, &errResp) == nil && errResp.Error != "" {
|
||||||
|
return fmt.Errorf("%s", errResp.Error)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respData))
|
||||||
|
}
|
||||||
|
if out != nil {
|
||||||
|
if err := json.Unmarshal(respData, out); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Login(username, password string) error {
|
||||||
|
var resp AuthResponse
|
||||||
|
if err := c.doReq("POST", "/api/v1/auth/login", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.cfg.Token = resp.Token
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Register(username, password string) (*User, error) {
|
||||||
|
var resp AuthResponse
|
||||||
|
if err := c.doReq("POST", "/api/v1/auth/register", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.cfg.Token = resp.Token
|
||||||
|
return resp.User, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) InitAdmin(username, password string) (*User, error) {
|
||||||
|
var resp AuthResponse
|
||||||
|
if err := c.doReq("POST", "/api/v1/auth/init", map[string]string{
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
}, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.cfg.Token = resp.Token
|
||||||
|
return resp.User, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CheckAdmin() (bool, error) {
|
||||||
|
var resp struct{ AdminExists bool `json:"admin_exists"` }
|
||||||
|
if err := c.doReq("GET", "/api/v1/admin/check", nil, &resp); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return resp.AdminExists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Dashboard() (*DashboardStats, error) {
|
||||||
|
var stats DashboardStats
|
||||||
|
if err := c.doReq("GET", "/api/v1/dashboard", nil, &stats); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AdminDashboard() (*DashboardStats, error) {
|
||||||
|
var stats DashboardStats
|
||||||
|
if err := c.doReq("GET", "/api/v1/admin/dashboard", nil, &stats); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Profile() (*ProfileResponse, error) {
|
||||||
|
var resp ProfileResponse
|
||||||
|
if err := c.doReq("GET", "/api/v1/user/profile", nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) CreateNetwork(name, ipRange string) (*Network, error) {
|
||||||
|
body := map[string]string{"name": name}
|
||||||
|
if ipRange != "" {
|
||||||
|
body["ip_range"] = ipRange
|
||||||
|
}
|
||||||
|
var resp struct{ Network *Network `json:"network"` }
|
||||||
|
if err := c.doReq("POST", "/api/v1/network/create", body, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Network, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListNetworks() ([]Network, error) {
|
||||||
|
var resp NetworkListResponse
|
||||||
|
if err := c.doReq("GET", "/api/v1/network/list", nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Networks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetNetwork(id uint32) (*Network, error) {
|
||||||
|
var resp struct{ Network *Network `json:"network"` }
|
||||||
|
if err := c.doReq("GET", fmt.Sprintf("/api/v1/network/%d", id), nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Network, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteNetwork(id uint32) error {
|
||||||
|
return c.doReq("DELETE", fmt.Sprintf("/api/v1/network/%d", id), nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) NetworkMembers(id uint32) ([]NetworkMember, error) {
|
||||||
|
var resp MembersResponse
|
||||||
|
if err := c.doReq("GET", fmt.Sprintf("/api/v1/network/%d/members", id), nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Members, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) AuthorizeMember(networkID uint32, nodeID string) error {
|
||||||
|
return c.doReq("POST", fmt.Sprintf("/api/v1/network/%d/authorize", networkID),
|
||||||
|
map[string]string{"node_id": nodeID}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeauthorizeMember(networkID uint32, nodeID string) error {
|
||||||
|
return c.doReq("POST", fmt.Sprintf("/api/v1/network/%d/deauthorize", networkID),
|
||||||
|
map[string]string{"node_id": nodeID}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RegisterNode(nodeID, publicKey, name, ipAddr string, port int, version string) (*Node, error) {
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"node_id": nodeID,
|
||||||
|
"public_key": publicKey,
|
||||||
|
"name": name,
|
||||||
|
}
|
||||||
|
if ipAddr != "" {
|
||||||
|
body["ip_address"] = ipAddr
|
||||||
|
}
|
||||||
|
if port > 0 {
|
||||||
|
body["port"] = port
|
||||||
|
}
|
||||||
|
if version != "" {
|
||||||
|
body["version"] = version
|
||||||
|
}
|
||||||
|
var resp struct{ Node *Node `json:"node"` }
|
||||||
|
if err := c.doReq("POST", "/api/v1/node/register", body, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Node, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListNodes() ([]Node, error) {
|
||||||
|
var resp NodeListResponse
|
||||||
|
if err := c.doReq("GET", "/api/v1/node/list", nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Nodes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ListOnlineNodes() ([]Node, error) {
|
||||||
|
var resp NodeListResponse
|
||||||
|
if err := c.doReq("GET", "/api/v1/node/online", nil, &resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Nodes, nil
|
||||||
|
}
|
||||||
121
sdk/identity.go
Normal file
121
sdk/identity.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LoadOrGenerateIdentity(path string) (*Identity, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err == nil {
|
||||||
|
return parseIdentity(strings.TrimSpace(string(data)))
|
||||||
|
}
|
||||||
|
if !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
id := GenerateIdentity()
|
||||||
|
encoded, err := serializeIdentity(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dir := dirname(path)
|
||||||
|
if dir != "" {
|
||||||
|
if e := os.MkdirAll(dir, 0755); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e := os.WriteFile(path, []byte(encoded), 0600); e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type identityJSON struct {
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func serializeIdentity(id *Identity) (string, error) {
|
||||||
|
data, err := json.Marshal(identityJSON{
|
||||||
|
PublicKey: id.PublicKeyHex(),
|
||||||
|
PrivateKey: id.PrivateKeyHex(),
|
||||||
|
Address: id.Address.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseIdentity(data string) (*Identity, error) {
|
||||||
|
var j identityJSON
|
||||||
|
if err := json.Unmarshal([]byte(data), &j); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse identity: %w", err)
|
||||||
|
}
|
||||||
|
if j.PrivateKey == "" {
|
||||||
|
return nil, fmt.Errorf("invalid identity: no private key")
|
||||||
|
}
|
||||||
|
pub, err := hexDecode(j.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode public key: %w", err)
|
||||||
|
}
|
||||||
|
priv, err := hexDecode(j.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decode private key: %w", err)
|
||||||
|
}
|
||||||
|
return &Identity{
|
||||||
|
PublicKey: pub,
|
||||||
|
PrivateKey: priv,
|
||||||
|
Address: AddressFromPublicKey(pub),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexDecode(s string) ([]byte, error) {
|
||||||
|
out, err := hexDecodeSimple(s)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexDecodeSimple(s string) ([]byte, error) {
|
||||||
|
h := func(c byte) (byte, error) {
|
||||||
|
switch {
|
||||||
|
case '0' <= c && c <= '9':
|
||||||
|
return c - '0', nil
|
||||||
|
case 'a' <= c && c <= 'f':
|
||||||
|
return c - 'a' + 10, nil
|
||||||
|
case 'A' <= c && c <= 'F':
|
||||||
|
return c - 'A' + 10, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid hex")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s)%2 != 0 {
|
||||||
|
return nil, fmt.Errorf("odd hex length")
|
||||||
|
}
|
||||||
|
out := make([]byte, len(s)/2)
|
||||||
|
for i := 0; i < len(s); i += 2 {
|
||||||
|
hi, e1 := h(s[i])
|
||||||
|
lo, e2 := h(s[i+1])
|
||||||
|
if e1 != nil || e2 != nil {
|
||||||
|
return nil, fmt.Errorf("invalid hex char")
|
||||||
|
}
|
||||||
|
out[i/2] = hi<<4 | lo
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dirname(path string) string {
|
||||||
|
idx := strings.LastIndex(path, "/")
|
||||||
|
if idx == -1 {
|
||||||
|
idx = strings.LastIndex(path, "\\")
|
||||||
|
}
|
||||||
|
if idx == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return path[:idx]
|
||||||
|
}
|
||||||
214
sdk/sdk.go
Normal file
214
sdk/sdk.go
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ControllerURL string
|
||||||
|
Token string
|
||||||
|
PSK string
|
||||||
|
NetworkID uint32
|
||||||
|
ListenPort int
|
||||||
|
TapName string
|
||||||
|
TapMTU int
|
||||||
|
IdentityPath string
|
||||||
|
HTTPTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
ControllerURL: "http://127.0.0.1:10001",
|
||||||
|
ListenPort: 0,
|
||||||
|
TapName: "zeromesh0",
|
||||||
|
TapMTU: 2800,
|
||||||
|
IdentityPath: "./data/agent.identity",
|
||||||
|
HTTPTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
cfg Config
|
||||||
|
httpc *http.Client
|
||||||
|
identity *Identity
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg Config) *Client {
|
||||||
|
if cfg.HTTPTimeout == 0 {
|
||||||
|
cfg.HTTPTimeout = 30 * time.Second
|
||||||
|
}
|
||||||
|
if cfg.TapName == "" {
|
||||||
|
cfg.TapName = "zeromesh0"
|
||||||
|
}
|
||||||
|
if cfg.TapMTU == 0 {
|
||||||
|
cfg.TapMTU = 2800
|
||||||
|
}
|
||||||
|
if cfg.IdentityPath == "" {
|
||||||
|
cfg.IdentityPath = "./data/agent.identity"
|
||||||
|
}
|
||||||
|
return &Client{
|
||||||
|
cfg: cfg,
|
||||||
|
httpc: &http.Client{
|
||||||
|
Timeout: cfg.HTTPTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 10,
|
||||||
|
IdleConnTimeout: 60 * time.Second,
|
||||||
|
DisableCompression: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Config() Config { return c.cfg }
|
||||||
|
func (c *Client) HTTPClient() *http.Client { return c.httpc }
|
||||||
|
func (c *Client) Identity() *Identity { return c.identity }
|
||||||
|
func (c *Client) SetToken(tok string) { c.cfg.Token = tok }
|
||||||
|
func (c *Client) Token() string { return c.cfg.Token }
|
||||||
|
|
||||||
|
type Address [5]byte
|
||||||
|
|
||||||
|
func (a Address) String() string { return hex.EncodeToString(a[:]) }
|
||||||
|
|
||||||
|
func AddressFromPublicKey(pub []byte) Address {
|
||||||
|
var a Address
|
||||||
|
h := hashBytes(pub)
|
||||||
|
copy(a[:], h[:5])
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
type Identity struct {
|
||||||
|
PublicKey ed25519.PublicKey
|
||||||
|
PrivateKey ed25519.PrivateKey
|
||||||
|
Address Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateIdentity() *Identity {
|
||||||
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return &Identity{
|
||||||
|
PublicKey: pub,
|
||||||
|
PrivateKey: priv,
|
||||||
|
Address: AddressFromPublicKey(pub),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (id *Identity) PublicKeyHex() string { return hex.EncodeToString(id.PublicKey) }
|
||||||
|
func (id *Identity) PrivateKeyHex() string { return hex.EncodeToString(id.PrivateKey) }
|
||||||
|
|
||||||
|
func hashBytes(data []byte) []byte {
|
||||||
|
h := make([]byte, 32)
|
||||||
|
for i, b := range data {
|
||||||
|
h[i%32] ^= b
|
||||||
|
}
|
||||||
|
for round := 0; round < 3; round++ {
|
||||||
|
for i := 0; i < 32; i++ {
|
||||||
|
h[i] = h[i] ^ h[(i+1)%32] ^ h[(i+7)%32]
|
||||||
|
h[i] = (h[i] << 3) | (h[i] >> 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthResponse struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
User *User `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
QuotaNetworks int `json:"quota_networks"`
|
||||||
|
QuotaNodes int `json:"quota_nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Network struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
NetworkID uint32 `json:"network_id"`
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IPRange string `json:"ip_range"`
|
||||||
|
MTU int `json:"mtu"`
|
||||||
|
Private bool `json:"private"`
|
||||||
|
Members []NetworkMember `json:"members,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkMember struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
NetworkID uint32 `json:"network_id"`
|
||||||
|
NodeID string `json:"node_id"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
Authorized bool `json:"authorized"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
UserID uint `json:"user_id"`
|
||||||
|
NodeID string `json:"node_id"`
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
IPAddress string `json:"ip_address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
LastSeen *string `json:"last_seen"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DashboardStats struct {
|
||||||
|
AuthorizedMembers int `json:"authorized_members"`
|
||||||
|
NodesTotal int `json:"nodes_total"`
|
||||||
|
NodesOnline int `json:"nodes_online"`
|
||||||
|
NetworksCount int `json:"networks_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkListResponse struct {
|
||||||
|
Networks []Network `json:"networks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NodeListResponse struct {
|
||||||
|
Nodes []Node `json:"nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MembersResponse struct {
|
||||||
|
Members []NetworkMember `json:"members"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileResponse struct {
|
||||||
|
User User `json:"user"`
|
||||||
|
UsedNetworks int `json:"used_networks"`
|
||||||
|
UsedNodes int `json:"used_nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Address) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(a.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Address) UnmarshalText(text []byte) error {
|
||||||
|
decoded, err := hex.DecodeString(string(text))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(decoded) != 5 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
copy(a[:], decoded)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPreferredIP() string {
|
||||||
|
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||||
|
if err != nil {
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
localAddr := conn.LocalAddr().(*net.UDPAddr)
|
||||||
|
return localAddr.IP.String()
|
||||||
|
}
|
||||||
75
sdk/tap_linux.go
Normal file
75
sdk/tap_linux.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package sdk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tapInterface struct {
|
||||||
|
Name string
|
||||||
|
MTU int
|
||||||
|
fd int
|
||||||
|
}
|
||||||
|
|
||||||
|
func openTap(name string, mtu int) (*tapInterface, error) {
|
||||||
|
fd, err := syscall.Open("/dev/net/tun", syscall.O_RDWR, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open /dev/net/tun: %w", err)
|
||||||
|
}
|
||||||
|
var ifr struct {
|
||||||
|
name [16]byte
|
||||||
|
flags uint16
|
||||||
|
}
|
||||||
|
copy(ifr.name[:], []byte(name))
|
||||||
|
ifr.flags = 0x0002 | 0x1000 // IFF_TAP | IFF_NO_PI
|
||||||
|
if _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), 0x400454ca, uintptr(unsafe.Pointer(&ifr))); errno != 0 {
|
||||||
|
syscall.Close(fd)
|
||||||
|
return nil, fmt.Errorf("TUNSETIFF: %w", errno)
|
||||||
|
}
|
||||||
|
devName := string(ifr.name[:])
|
||||||
|
if idx := indexOfZero(devName); idx >= 0 {
|
||||||
|
devName = devName[:idx]
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
var mtuReq struct {
|
||||||
|
name [16]byte
|
||||||
|
mtu int32
|
||||||
|
}
|
||||||
|
copy(mtuReq.name[:], []byte(devName))
|
||||||
|
mtuReq.mtu = int32(mtu)
|
||||||
|
syscall.Syscall(syscall.SYS_IOCTL, uintptr(s), 0x400c4a52, uintptr(unsafe.Pointer(&mtuReq))) // SIOCSIFMTU
|
||||||
|
}
|
||||||
|
return ti, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *tapInterface) Read(buf []byte) (int, error) {
|
||||||
|
return syscall.Read(ti.fd, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *tapInterface) Write(buf []byte) error {
|
||||||
|
_, err := syscall.Write(ti.fd, buf)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *tapInterface) Close() error {
|
||||||
|
return syscall.Close(ti.fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOfZero(s string) int {
|
||||||
|
for i, c := range []byte(s) {
|
||||||
|
if c == 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
26
sdk/tap_stub.go
Normal file
26
sdk/tap_stub.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package sdk
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type tapInterface struct {
|
||||||
|
Name string
|
||||||
|
MTU int
|
||||||
|
}
|
||||||
|
|
||||||
|
func openTap(name string, mtu int) (*tapInterface, error) {
|
||||||
|
return nil, fmt.Errorf("TAP device requires Linux")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *tapInterface) Read(buf []byte) (int, error) {
|
||||||
|
return 0, fmt.Errorf("not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *tapInterface) Write(buf []byte) error {
|
||||||
|
return fmt.Errorf("not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ti *tapInterface) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user