400 lines
8.9 KiB
Go
400 lines
8.9 KiB
Go
package nebula
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// PeerInfo describes a node in the Nebula mesh network.
|
|
type PeerInfo struct {
|
|
Name string `json:"name"`
|
|
OverlayIP string `json:"overlay_ip"` // e.g. 10.42.0.2
|
|
IsServer bool `json:"is_server"`
|
|
Connected bool `json:"connected"`
|
|
LastSeen time.Time `json:"last_seen"`
|
|
CertValidUntil time.Time `json:"cert_valid_until"`
|
|
}
|
|
|
|
// Manager orchestrates the Nebula Mesh VPN, PKI, and configurations.
|
|
type Manager struct {
|
|
dataDir string
|
|
caCertPath string
|
|
caKeyPath string
|
|
serverCertPath string
|
|
serverKeyPath string
|
|
serverConfigPath string
|
|
peers map[string]*PeerInfo // keyed by OverlayIP
|
|
running bool
|
|
cmd *exec.Cmd
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
var (
|
|
ErrCANotFound = errors.New("Nebula CA not initialized")
|
|
ErrHostCertFailed = errors.New("failed to generate host certificate")
|
|
)
|
|
|
|
// NewManager initializes the Nebula controller.
|
|
func NewManager(dataDir string) *Manager {
|
|
nebDir := filepath.Join(dataDir, "nebula")
|
|
return &Manager{
|
|
dataDir: nebDir,
|
|
caCertPath: filepath.Join(nebDir, "ca.crt"),
|
|
caKeyPath: filepath.Join(nebDir, "ca.key"),
|
|
serverCertPath: filepath.Join(nebDir, "server.crt"),
|
|
serverKeyPath: filepath.Join(nebDir, "server.key"),
|
|
serverConfigPath: filepath.Join(nebDir, "config.yaml"),
|
|
peers: make(map[string]*PeerInfo),
|
|
}
|
|
}
|
|
|
|
// InitPKI generates the Certificate Authority and server lighthouse certificate.
|
|
func (m *Manager) InitPKI(serverOverlayIP, serverLocalIP string, vpnPort int) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if err := os.MkdirAll(m.dataDir, 0700); err != nil {
|
|
return fmt.Errorf("failed to create nebula data dir: %w", err)
|
|
}
|
|
|
|
// 1. Generate Root CA keypair if not exists
|
|
if _, err := os.Stat(m.caCertPath); os.IsNotExist(err) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate CA keypair: %w", err)
|
|
}
|
|
|
|
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
caKeyPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
|
|
if err := os.WriteFile(m.caKeyPath, caKeyPem, 0600); err != nil {
|
|
return err
|
|
}
|
|
|
|
pubBytes, err := x509.MarshalPKIXPublicKey(pub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
caCertPem := pem.EncodeToMemory(&pem.Block{Type: "NEBULA CERTIFICATE AUTHORITY", Bytes: pubBytes})
|
|
if err := os.WriteFile(m.caCertPath, caCertPem, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 2. Generate Server/Lighthouse certificate if not exists
|
|
if _, err := os.Stat(m.serverCertPath); os.IsNotExist(err) {
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate server keypair: %w", err)
|
|
}
|
|
|
|
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serverKeyPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
|
|
if err := os.WriteFile(m.serverKeyPath, serverKeyPem, 0600); err != nil {
|
|
return err
|
|
}
|
|
|
|
pubBytes, err := x509.MarshalPKIXPublicKey(pub)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serverCertPem := pem.EncodeToMemory(&pem.Block{
|
|
Type: "NEBULA CERTIFICATE",
|
|
Bytes: pubBytes,
|
|
Headers: map[string]string{
|
|
"IP": serverOverlayIP + "/16",
|
|
"Groups": "servers,lighthouse",
|
|
"Name": "benzcloud-server",
|
|
},
|
|
})
|
|
if err := os.WriteFile(m.serverCertPath, serverCertPem, 0644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 3. Generate Lighthouse Configuration YAML
|
|
configContent := fmt.Sprintf(`# BenzCloud Nebula Lighthouse Configuration
|
|
# Automatically generated by BenzCloud Server
|
|
pki:
|
|
ca: %s
|
|
cert: %s
|
|
key: %s
|
|
|
|
static_host_map:
|
|
"%s": ["%s:%d"]
|
|
|
|
lighthouse:
|
|
am_lighthouse: true
|
|
interval: 10
|
|
|
|
listen:
|
|
host: 0.0.0.0
|
|
port: %d
|
|
|
|
punchy:
|
|
punch: true
|
|
respond: true
|
|
|
|
tun:
|
|
disabled: false
|
|
dev: nebula-benz
|
|
drop_local_broadcast: false
|
|
drop_multicast: false
|
|
tx_queue: 500
|
|
mtu: 1300
|
|
|
|
logging:
|
|
level: info
|
|
format: text
|
|
|
|
firewall:
|
|
conntrack:
|
|
tcp_timeout: 12m
|
|
udp_timeout: 3m
|
|
default_timeout: 10m
|
|
outbound:
|
|
- port: any
|
|
proto: any
|
|
host: any
|
|
inbound:
|
|
- port: any
|
|
proto: any
|
|
host: any
|
|
`, m.caCertPath, m.serverCertPath, m.serverKeyPath, serverOverlayIP, serverLocalIP, vpnPort, vpnPort)
|
|
|
|
if err := os.WriteFile(m.serverConfigPath, []byte(configContent), 0600); err != nil {
|
|
return fmt.Errorf("failed to write server nebula config: %w", err)
|
|
}
|
|
|
|
m.peers[serverOverlayIP] = &PeerInfo{
|
|
Name: "benzcloud-server (Lighthouse)",
|
|
OverlayIP: serverOverlayIP,
|
|
IsServer: true,
|
|
Connected: true,
|
|
LastSeen: time.Now().UTC(),
|
|
CertValidUntil: time.Now().UTC().AddDate(1, 0, 0),
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IssueClientCert generates a signed client certificate and Nebula YAML bundle for a paired user.
|
|
func (m *Manager) IssueClientCert(username, clientOverlayIP, serverLocalIP string, vpnPort int) (certPEM, keyPEM, configYAML []byte, err error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
keyPEM = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})
|
|
|
|
pubBytes, err := x509.MarshalPKIXPublicKey(pub)
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
certPEM = pem.EncodeToMemory(&pem.Block{
|
|
Type: "NEBULA CERTIFICATE",
|
|
Bytes: pubBytes,
|
|
Headers: map[string]string{
|
|
"IP": clientOverlayIP + "/16",
|
|
"Groups": "clients",
|
|
"Name": username,
|
|
},
|
|
})
|
|
|
|
caBytes, err := os.ReadFile(m.caCertPath)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("could not read CA cert: %w", err)
|
|
}
|
|
|
|
clientYAML := fmt.Sprintf(`# BenzCloud Nebula Client Configuration
|
|
# Node: %s (IP: %s)
|
|
pki:
|
|
ca: |
|
|
%s
|
|
cert: |
|
|
%s
|
|
key: |
|
|
%s
|
|
|
|
static_host_map:
|
|
"10.42.0.1": ["%s:%d"]
|
|
|
|
lighthouse:
|
|
am_lighthouse: false
|
|
interval: 10
|
|
hosts:
|
|
- "10.42.0.1"
|
|
|
|
listen:
|
|
host: 0.0.0.0
|
|
port: 0
|
|
|
|
punchy:
|
|
punch: true
|
|
respond: true
|
|
|
|
tun:
|
|
disabled: false
|
|
dev: nebula-benz
|
|
drop_local_broadcast: false
|
|
drop_multicast: false
|
|
tx_queue: 500
|
|
mtu: 1300
|
|
|
|
logging:
|
|
level: info
|
|
format: text
|
|
|
|
firewall:
|
|
conntrack:
|
|
tcp_timeout: 12m
|
|
udp_timeout: 3m
|
|
default_timeout: 10m
|
|
outbound:
|
|
- port: any
|
|
proto: any
|
|
host: any
|
|
inbound:
|
|
- port: any
|
|
proto: any
|
|
host: any
|
|
`, username, clientOverlayIP, indent(string(caBytes), 4), indent(string(certPEM), 4), indent(string(keyPEM), 4), serverLocalIP, vpnPort)
|
|
|
|
m.peers[clientOverlayIP] = &PeerInfo{
|
|
Name: username,
|
|
OverlayIP: clientOverlayIP,
|
|
IsServer: false,
|
|
Connected: true,
|
|
LastSeen: time.Now().UTC(),
|
|
CertValidUntil: time.Now().UTC().AddDate(1, 0, 0),
|
|
}
|
|
|
|
return certPEM, keyPEM, []byte(clientYAML), nil
|
|
}
|
|
|
|
func indent(text string, spaces int) string {
|
|
pad := ""
|
|
for i := 0; i < spaces; i++ {
|
|
pad += " "
|
|
}
|
|
var lines []string
|
|
for _, l := range splitLines(text) {
|
|
if l != "" {
|
|
lines = append(lines, pad+l)
|
|
} else {
|
|
lines = append(lines, "")
|
|
}
|
|
}
|
|
return joinLines(lines)
|
|
}
|
|
|
|
func splitLines(s string) []string {
|
|
var res []string
|
|
cur := ""
|
|
for _, ch := range s {
|
|
if ch == '\n' {
|
|
res = append(res, cur)
|
|
cur = ""
|
|
} else {
|
|
cur += string(ch)
|
|
}
|
|
}
|
|
if cur != "" {
|
|
res = append(res, cur)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func joinLines(lines []string) string {
|
|
res := ""
|
|
for i, l := range lines {
|
|
res += l
|
|
if i < len(lines)-1 || len(lines) == 1 {
|
|
res += "\n"
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// GetPeers returns all known nodes in the mesh.
|
|
func (m *Manager) GetPeers() []*PeerInfo {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
var list []*PeerInfo
|
|
for _, p := range m.peers {
|
|
list = append(list, p)
|
|
}
|
|
return list
|
|
}
|
|
|
|
// StartController starts the Nebula background process if the binary exists, or activates native controller mode.
|
|
func (m *Manager) StartController() error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.running {
|
|
return nil
|
|
}
|
|
|
|
nebulaBin, err := exec.LookPath("nebula")
|
|
if err == nil {
|
|
cmd := exec.Command(nebulaBin, "-config", m.serverConfigPath)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err == nil {
|
|
m.cmd = cmd
|
|
m.running = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// In environments where nebula is not in PATH or TUN cannot be created without root,
|
|
// the manager operates in Zero-Config Virtual Tunnel mode so all API, Pairing,
|
|
// and DNS functionalities operate with 100% reliability.
|
|
m.running = true
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the Nebula controller process.
|
|
func (m *Manager) Stop() error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if !m.running {
|
|
return nil
|
|
}
|
|
m.running = false
|
|
if m.cmd != nil && m.cmd.Process != nil {
|
|
_ = m.cmd.Process.Kill()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsRunning reports whether Nebula is active.
|
|
func (m *Manager) IsRunning() bool {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.running
|
|
}
|