mirror of
https://github.com/slackhq/nebula.git
synced 2025-11-09 00:43:57 +01:00
This commit adds support for Nebula to be started without creating a tun device. A node started in this mode still has a full "control plane", but no effective "data plane". Its use is suited to a lighthouse that has no need to partake in the mesh VPN. Consequently, creation of the tun device is the only reason nebula neesd to be started with elevated privileged, so this example lighthouse can also be run as a non-root user.
447 lines
14 KiB
Go
447 lines
14 KiB
Go
package nebula
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/slackhq/nebula/sshd"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// The caller should provide a real logger, we have one just in case
|
|
var l = logrus.New()
|
|
|
|
type m map[string]interface{}
|
|
|
|
type CommandRequest struct {
|
|
Command string
|
|
Callback chan error
|
|
}
|
|
|
|
func Main(config *Config, configTest bool, block bool, buildVersion string, logger *logrus.Logger, tunFd *int, commandChan <-chan CommandRequest) error {
|
|
l = logger
|
|
l.Formatter = &logrus.TextFormatter{
|
|
FullTimestamp: true,
|
|
}
|
|
|
|
// Print the config if in test, the exit comes later
|
|
if configTest {
|
|
b, err := yaml.Marshal(config.Settings)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Print the final config
|
|
l.Println(string(b))
|
|
}
|
|
|
|
err := configLogger(config)
|
|
if err != nil {
|
|
return NewContextualError("Failed to configure the logger", nil, err)
|
|
}
|
|
|
|
config.RegisterReloadCallback(func(c *Config) {
|
|
err := configLogger(c)
|
|
if err != nil {
|
|
l.WithError(err).Error("Failed to configure the logger")
|
|
}
|
|
})
|
|
|
|
// trustedCAs is currently a global, so loadCA operates on that global directly
|
|
trustedCAs, err = loadCAFromConfig(config)
|
|
if err != nil {
|
|
//The errors coming out of loadCA are already nicely formatted
|
|
return NewContextualError("Failed to load ca from config", nil, err)
|
|
}
|
|
l.WithField("fingerprints", trustedCAs.GetFingerprints()).Debug("Trusted CA fingerprints")
|
|
|
|
cs, err := NewCertStateFromConfig(config)
|
|
if err != nil {
|
|
//The errors coming out of NewCertStateFromConfig are already nicely formatted
|
|
return NewContextualError("Failed to load certificate from config", nil, err)
|
|
}
|
|
l.WithField("cert", cs.certificate).Debug("Client nebula certificate")
|
|
|
|
fw, err := NewFirewallFromConfig(cs.certificate, config)
|
|
if err != nil {
|
|
return NewContextualError("Error while loading firewall rules", nil, err)
|
|
}
|
|
l.WithField("firewallHash", fw.GetRuleHash()).Info("Firewall started")
|
|
|
|
// TODO: make sure mask is 4 bytes
|
|
tunCidr := cs.certificate.Details.Ips[0]
|
|
routes, err := parseRoutes(config, tunCidr)
|
|
if err != nil {
|
|
return NewContextualError("Could not parse tun.routes", nil, err)
|
|
}
|
|
unsafeRoutes, err := parseUnsafeRoutes(config, tunCidr)
|
|
if err != nil {
|
|
return NewContextualError("Could not parse tun.unsafe_routes", nil, err)
|
|
}
|
|
|
|
ssh, err := sshd.NewSSHServer(l.WithField("subsystem", "sshd"))
|
|
wireSSHReload(ssh, config)
|
|
if config.GetBool("sshd.enabled", false) {
|
|
err = configSSH(ssh, config)
|
|
if err != nil {
|
|
return NewContextualError("Error while configuring the sshd", nil, err)
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// All non system modifying configuration consumption should live above this line
|
|
// tun config, listeners, anything modifying the computer should be below
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
var tun Inside
|
|
if !configTest {
|
|
config.CatchHUP()
|
|
|
|
switch {
|
|
case config.GetBool("tun.disabled", false):
|
|
tun = newDisabledTun(tunCidr, l)
|
|
case tunFd != nil:
|
|
tun, err = newTunFromFd(
|
|
*tunFd,
|
|
tunCidr,
|
|
config.GetInt("tun.mtu", DEFAULT_MTU),
|
|
routes,
|
|
unsafeRoutes,
|
|
config.GetInt("tun.tx_queue", 500),
|
|
)
|
|
default:
|
|
tun, err = newTun(
|
|
config.GetString("tun.dev", ""),
|
|
tunCidr,
|
|
config.GetInt("tun.mtu", DEFAULT_MTU),
|
|
routes,
|
|
unsafeRoutes,
|
|
config.GetInt("tun.tx_queue", 500),
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
return NewContextualError("Failed to get a tun/tap device", nil, err)
|
|
}
|
|
}
|
|
|
|
// set up our UDP listener
|
|
udpQueues := config.GetInt("listen.routines", 1)
|
|
var udpServer *udpConn
|
|
|
|
if !configTest {
|
|
udpServer, err = NewListener(config.GetString("listen.host", "0.0.0.0"), config.GetInt("listen.port", 0), udpQueues > 1)
|
|
if err != nil {
|
|
return NewContextualError("Failed to open udp listener", nil, err)
|
|
}
|
|
udpServer.reloadConfig(config)
|
|
}
|
|
|
|
sigChan := make(chan os.Signal)
|
|
killChan := make(chan CommandRequest)
|
|
if commandChan != nil {
|
|
go func() {
|
|
cmd := CommandRequest{}
|
|
for {
|
|
cmd = <-commandChan
|
|
switch cmd.Command {
|
|
case "rebind":
|
|
udpServer.Rebind()
|
|
case "exit":
|
|
killChan <- cmd
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Set up my internal host map
|
|
var preferredRanges []*net.IPNet
|
|
rawPreferredRanges := config.GetStringSlice("preferred_ranges", []string{})
|
|
// First, check if 'preferred_ranges' is set and fallback to 'local_range'
|
|
if len(rawPreferredRanges) > 0 {
|
|
for _, rawPreferredRange := range rawPreferredRanges {
|
|
_, preferredRange, err := net.ParseCIDR(rawPreferredRange)
|
|
if err != nil {
|
|
return NewContextualError("Failed to parse preferred ranges", nil, err)
|
|
}
|
|
preferredRanges = append(preferredRanges, preferredRange)
|
|
}
|
|
}
|
|
|
|
// local_range was superseded by preferred_ranges. If it is still present,
|
|
// merge the local_range setting into preferred_ranges. We will probably
|
|
// deprecate local_range and remove in the future.
|
|
rawLocalRange := config.GetString("local_range", "")
|
|
if rawLocalRange != "" {
|
|
_, localRange, err := net.ParseCIDR(rawLocalRange)
|
|
if err != nil {
|
|
return NewContextualError("Failed to parse local_range", nil, err)
|
|
}
|
|
|
|
// Check if the entry for local_range was already specified in
|
|
// preferred_ranges. Don't put it into the slice twice if so.
|
|
var found bool
|
|
for _, r := range preferredRanges {
|
|
if r.String() == localRange.String() {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
preferredRanges = append(preferredRanges, localRange)
|
|
}
|
|
}
|
|
|
|
hostMap := NewHostMap("main", tunCidr, preferredRanges)
|
|
hostMap.SetDefaultRoute(ip2int(net.ParseIP(config.GetString("default_route", "0.0.0.0"))))
|
|
hostMap.addUnsafeRoutes(&unsafeRoutes)
|
|
hostMap.metricsEnabled = config.GetBool("stats.message_metrics", false)
|
|
|
|
l.WithField("network", hostMap.vpnCIDR).WithField("preferredRanges", hostMap.preferredRanges).Info("Main HostMap created")
|
|
|
|
/*
|
|
config.SetDefault("promoter.interval", 10)
|
|
go hostMap.Promoter(config.GetInt("promoter.interval"))
|
|
*/
|
|
|
|
punchy := NewPunchyFromConfig(config)
|
|
if punchy.Punch && !configTest {
|
|
l.Info("UDP hole punching enabled")
|
|
go hostMap.Punchy(udpServer)
|
|
}
|
|
|
|
port := config.GetInt("listen.port", 0)
|
|
// If port is dynamic, discover it
|
|
if port == 0 && !configTest {
|
|
uPort, err := udpServer.LocalAddr()
|
|
if err != nil {
|
|
return NewContextualError("Failed to get listening port", nil, err)
|
|
}
|
|
port = int(uPort.Port)
|
|
}
|
|
|
|
amLighthouse := config.GetBool("lighthouse.am_lighthouse", false)
|
|
|
|
// warn if am_lighthouse is enabled but upstream lighthouses exists
|
|
rawLighthouseHosts := config.GetStringSlice("lighthouse.hosts", []string{})
|
|
if amLighthouse && len(rawLighthouseHosts) != 0 {
|
|
l.Warn("lighthouse.am_lighthouse enabled on node but upstream lighthouses exist in config")
|
|
}
|
|
|
|
lighthouseHosts := make([]uint32, len(rawLighthouseHosts))
|
|
for i, host := range rawLighthouseHosts {
|
|
ip := net.ParseIP(host)
|
|
if ip == nil {
|
|
return NewContextualError("Unable to parse lighthouse host entry", m{"host": host, "entry": i + 1}, nil)
|
|
}
|
|
if !tunCidr.Contains(ip) {
|
|
return NewContextualError("lighthouse host is not in our subnet, invalid", m{"vpnIp": ip, "network": tunCidr.String()}, nil)
|
|
}
|
|
lighthouseHosts[i] = ip2int(ip)
|
|
}
|
|
|
|
lightHouse := NewLightHouse(
|
|
amLighthouse,
|
|
ip2int(tunCidr.IP),
|
|
lighthouseHosts,
|
|
//TODO: change to a duration
|
|
config.GetInt("lighthouse.interval", 10),
|
|
port,
|
|
udpServer,
|
|
punchy.Respond,
|
|
punchy.Delay,
|
|
config.GetBool("stats.lighthouse_metrics", false),
|
|
)
|
|
|
|
remoteAllowList, err := config.GetAllowList("lighthouse.remote_allow_list", false)
|
|
if err != nil {
|
|
return NewContextualError("Invalid lighthouse.remote_allow_list", nil, err)
|
|
}
|
|
lightHouse.SetRemoteAllowList(remoteAllowList)
|
|
|
|
localAllowList, err := config.GetAllowList("lighthouse.local_allow_list", true)
|
|
if err != nil {
|
|
return NewContextualError("Invalid lighthouse.local_allow_list", nil, err)
|
|
}
|
|
lightHouse.SetLocalAllowList(localAllowList)
|
|
|
|
//TODO: Move all of this inside functions in lighthouse.go
|
|
for k, v := range config.GetMap("static_host_map", map[interface{}]interface{}{}) {
|
|
vpnIp := net.ParseIP(fmt.Sprintf("%v", k))
|
|
if !tunCidr.Contains(vpnIp) {
|
|
return NewContextualError("static_host_map key is not in our subnet, invalid", m{"vpnIp": vpnIp, "network": tunCidr.String()}, nil)
|
|
}
|
|
vals, ok := v.([]interface{})
|
|
if ok {
|
|
for _, v := range vals {
|
|
parts := strings.Split(fmt.Sprintf("%v", v), ":")
|
|
addr, err := net.ResolveIPAddr("ip", parts[0])
|
|
if err == nil {
|
|
ip := addr.IP
|
|
port, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return NewContextualError("Static host address could not be parsed", m{"vpnIp": vpnIp}, err)
|
|
}
|
|
lightHouse.AddRemote(ip2int(vpnIp), NewUDPAddr(ip2int(ip), uint16(port)), true)
|
|
}
|
|
}
|
|
} else {
|
|
//TODO: make this all a helper
|
|
parts := strings.Split(fmt.Sprintf("%v", v), ":")
|
|
addr, err := net.ResolveIPAddr("ip", parts[0])
|
|
if err == nil {
|
|
ip := addr.IP
|
|
port, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
return NewContextualError("Static host address could not be parsed", m{"vpnIp": vpnIp}, err)
|
|
}
|
|
lightHouse.AddRemote(ip2int(vpnIp), NewUDPAddr(ip2int(ip), uint16(port)), true)
|
|
}
|
|
}
|
|
}
|
|
|
|
err = lightHouse.ValidateLHStaticEntries()
|
|
if err != nil {
|
|
l.WithError(err).Error("Lighthouse unreachable")
|
|
}
|
|
|
|
var messageMetrics *MessageMetrics
|
|
if config.GetBool("stats.message_metrics", false) {
|
|
messageMetrics = newMessageMetrics()
|
|
} else {
|
|
messageMetrics = newMessageMetricsOnlyRecvError()
|
|
}
|
|
|
|
handshakeConfig := HandshakeConfig{
|
|
tryInterval: config.GetDuration("handshakes.try_interval", DefaultHandshakeTryInterval),
|
|
retries: config.GetInt("handshakes.retries", DefaultHandshakeRetries),
|
|
waitRotation: config.GetInt("handshakes.wait_rotation", DefaultHandshakeWaitRotation),
|
|
triggerBuffer: config.GetInt("handshakes.trigger_buffer", DefaultHandshakeTriggerBuffer),
|
|
|
|
messageMetrics: messageMetrics,
|
|
}
|
|
|
|
handshakeManager := NewHandshakeManager(tunCidr, preferredRanges, hostMap, lightHouse, udpServer, handshakeConfig)
|
|
lightHouse.handshakeTrigger = handshakeManager.trigger
|
|
|
|
//TODO: These will be reused for psk
|
|
//handshakeMACKey := config.GetString("handshake_mac.key", "")
|
|
//handshakeAcceptedMACKeys := config.GetStringSlice("handshake_mac.accepted_keys", []string{})
|
|
|
|
serveDns := config.GetBool("lighthouse.serve_dns", false)
|
|
checkInterval := config.GetInt("timers.connection_alive_interval", 5)
|
|
pendingDeletionInterval := config.GetInt("timers.pending_deletion_interval", 10)
|
|
ifConfig := &InterfaceConfig{
|
|
HostMap: hostMap,
|
|
Inside: tun,
|
|
Outside: udpServer,
|
|
certState: cs,
|
|
Cipher: config.GetString("cipher", "aes"),
|
|
Firewall: fw,
|
|
ServeDns: serveDns,
|
|
HandshakeManager: handshakeManager,
|
|
lightHouse: lightHouse,
|
|
checkInterval: checkInterval,
|
|
pendingDeletionInterval: pendingDeletionInterval,
|
|
DropLocalBroadcast: config.GetBool("tun.drop_local_broadcast", false),
|
|
DropMulticast: config.GetBool("tun.drop_multicast", false),
|
|
UDPBatchSize: config.GetInt("listen.batch", 64),
|
|
MessageMetrics: messageMetrics,
|
|
}
|
|
|
|
switch ifConfig.Cipher {
|
|
case "aes":
|
|
noiseEndianness = binary.BigEndian
|
|
case "chachapoly":
|
|
noiseEndianness = binary.LittleEndian
|
|
default:
|
|
return fmt.Errorf("unknown cipher: %v", ifConfig.Cipher)
|
|
}
|
|
|
|
var ifce *Interface
|
|
if !configTest {
|
|
ifce, err = NewInterface(ifConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize interface: %s", err)
|
|
}
|
|
|
|
ifce.RegisterConfigChangeCallbacks(config)
|
|
|
|
go handshakeManager.Run(ifce)
|
|
go lightHouse.LhUpdateWorker(ifce)
|
|
}
|
|
|
|
err = startStats(config, configTest)
|
|
if err != nil {
|
|
return NewContextualError("Failed to start stats emitter", nil, err)
|
|
}
|
|
|
|
if configTest {
|
|
return nil
|
|
}
|
|
|
|
//TODO: check if we _should_ be emitting stats
|
|
go ifce.emitStats(config.GetDuration("stats.interval", time.Second*10))
|
|
|
|
attachCommands(ssh, hostMap, handshakeManager.pendingHostMap, lightHouse, ifce)
|
|
ifce.Run(config.GetInt("tun.routines", 1), udpQueues, buildVersion)
|
|
|
|
// Start DNS server last to allow using the nebula IP as lighthouse.dns.host
|
|
if amLighthouse && serveDns {
|
|
l.Debugln("Starting dns server")
|
|
go dnsMain(hostMap, config)
|
|
}
|
|
|
|
if block {
|
|
// Just sit here and be friendly, main thread.
|
|
shutdownBlock(ifce, sigChan, killChan)
|
|
} else {
|
|
// Even though we aren't blocking we still want to shutdown gracefully
|
|
go shutdownBlock(ifce, sigChan, killChan)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func shutdownBlock(ifce *Interface, sigChan chan os.Signal, killChan chan CommandRequest) {
|
|
var cmd CommandRequest
|
|
var sig string
|
|
|
|
signal.Notify(sigChan, syscall.SIGTERM)
|
|
signal.Notify(sigChan, syscall.SIGINT)
|
|
|
|
select {
|
|
case rawSig := <-sigChan:
|
|
sig = rawSig.String()
|
|
case cmd = <-killChan:
|
|
sig = "controlling app"
|
|
}
|
|
|
|
l.WithField("signal", sig).Info("Caught signal, shutting down")
|
|
|
|
//TODO: stop tun and udp routines, the lock on hostMap effectively does that though
|
|
//TODO: this is probably better as a function in ConnectionManager or HostMap directly
|
|
ifce.hostMap.Lock()
|
|
for _, h := range ifce.hostMap.Hosts {
|
|
if h.ConnectionState.ready {
|
|
ifce.send(closeTunnel, 0, h.ConnectionState, h, h.remote, []byte{}, make([]byte, 12, 12), make([]byte, mtu))
|
|
l.WithField("vpnIp", IntIp(h.hostId)).WithField("udpAddr", h.remote).
|
|
Debug("Sending close tunnel message")
|
|
}
|
|
}
|
|
ifce.hostMap.Unlock()
|
|
|
|
l.WithField("signal", sig).Info("Goodbye")
|
|
select {
|
|
case cmd.Callback <- nil:
|
|
default:
|
|
}
|
|
}
|