mirror of
https://github.com/slackhq/nebula.git
synced 2025-11-10 01:43:57 +01:00
This change is for Linux only. Previously, when running with multiple tun.routines, we would only have one file descriptor. This change instead sets IFF_MULTI_QUEUE and opens a file descriptor for each routine. This allows us to process with multiple threads while preventing out of order packet reception issues. To attempt to distribute the flows across the queues, we try to write to the tun/UDP queue that corresponds with the one we read from. So if we read a packet from tun queue "2", we will write the outgoing encrypted packet to UDP queue "2". Because of the nature of how multi queue works with flows, a given host tunnel will be sticky to a given routine (so if you try to performance benchmark by only using one tunnel between two hosts, you are only going to be using a max of one thread for each direction). Because this system works much better when we can correlate flows between the tun and udp routines, we are deprecating the undocumented "tun.routines" and "listen.routines" parameters and introducing a new "routines" parameter that sets the value for both. If you use the old undocumented parameters, the max of the values will be used and a warning logged. Co-authored-by: Nate Brown <nbrown.us@gmail.com>
412 lines
13 KiB
Go
412 lines
13 KiB
Go
package nebula
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"fmt"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"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{}
|
|
|
|
func Main(config *Config, configTest bool, buildVersion string, logger *logrus.Logger, tunFd *int) (*Control, 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 nil, err
|
|
}
|
|
|
|
// Print the final config
|
|
l.Println(string(b))
|
|
}
|
|
|
|
err := configLogger(config)
|
|
if err != nil {
|
|
return nil, 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 nil, 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 nil, 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 nil, 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 nil, NewContextualError("Could not parse tun.routes", nil, err)
|
|
}
|
|
unsafeRoutes, err := parseUnsafeRoutes(config, tunCidr)
|
|
if err != nil {
|
|
return nil, 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 nil, 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 routines int
|
|
|
|
// If `routines` is set, use that and ignore the specific values
|
|
if routines = config.GetInt("routines", 0); routines != 0 {
|
|
if routines < 1 {
|
|
routines = 1
|
|
}
|
|
if routines > 1 {
|
|
l.WithField("routines", routines).Info("Using multiple routines")
|
|
}
|
|
} else {
|
|
// deprecated and undocumented
|
|
tunQueues := config.GetInt("tun.routines", 1)
|
|
udpQueues := config.GetInt("listen.routines", 1)
|
|
if tunQueues > udpQueues {
|
|
routines = tunQueues
|
|
} else {
|
|
routines = udpQueues
|
|
}
|
|
if routines != 1 {
|
|
l.WithField("routines", routines).Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead")
|
|
}
|
|
}
|
|
|
|
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),
|
|
routines > 1,
|
|
)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, NewContextualError("Failed to get a tun/tap device", nil, err)
|
|
}
|
|
}
|
|
|
|
// set up our UDP listener
|
|
udpConns := make([]*udpConn, routines)
|
|
port := config.GetInt("listen.port", 0)
|
|
|
|
if !configTest {
|
|
for i := 0; i < routines; i++ {
|
|
udpServer, err := NewListener(config.GetString("listen.host", "0.0.0.0"), port, routines > 1)
|
|
if err != nil {
|
|
return nil, NewContextualError("Failed to open udp listener", m{"queue": i}, err)
|
|
}
|
|
udpServer.reloadConfig(config)
|
|
udpConns[i] = udpServer
|
|
|
|
// If port is dynamic, discover it
|
|
if port == 0 {
|
|
uPort, err := udpServer.LocalAddr()
|
|
if err != nil {
|
|
return nil, NewContextualError("Failed to get listening port", nil, err)
|
|
}
|
|
port = int(uPort.Port)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 nil, 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 nil, 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(udpConns[0])
|
|
}
|
|
|
|
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 nil, NewContextualError("Unable to parse lighthouse host entry", m{"host": host, "entry": i + 1}, nil)
|
|
}
|
|
if !tunCidr.Contains(ip) {
|
|
return nil, 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,
|
|
udpConns[0],
|
|
punchy.Respond,
|
|
punchy.Delay,
|
|
config.GetBool("stats.lighthouse_metrics", false),
|
|
)
|
|
|
|
remoteAllowList, err := config.GetAllowList("lighthouse.remote_allow_list", false)
|
|
if err != nil {
|
|
return nil, NewContextualError("Invalid lighthouse.remote_allow_list", nil, err)
|
|
}
|
|
lightHouse.SetRemoteAllowList(remoteAllowList)
|
|
|
|
localAllowList, err := config.GetAllowList("lighthouse.local_allow_list", true)
|
|
if err != nil {
|
|
return nil, 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 nil, 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 nil, 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 nil, 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, udpConns[0], 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: udpConns[0],
|
|
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),
|
|
routines: routines,
|
|
MessageMetrics: messageMetrics,
|
|
version: buildVersion,
|
|
}
|
|
|
|
switch ifConfig.Cipher {
|
|
case "aes":
|
|
noiseEndianness = binary.BigEndian
|
|
case "chachapoly":
|
|
noiseEndianness = binary.LittleEndian
|
|
default:
|
|
return nil, fmt.Errorf("unknown cipher: %v", ifConfig.Cipher)
|
|
}
|
|
|
|
var ifce *Interface
|
|
if !configTest {
|
|
ifce, err = NewInterface(ifConfig)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to initialize interface: %s", err)
|
|
}
|
|
|
|
// TODO: Better way to attach these, probably want a new interface in InterfaceConfig
|
|
// I don't want to make this initial commit too far-reaching though
|
|
ifce.writers = udpConns
|
|
|
|
ifce.RegisterConfigChangeCallbacks(config)
|
|
|
|
go handshakeManager.Run(ifce)
|
|
go lightHouse.LhUpdateWorker(ifce)
|
|
}
|
|
|
|
err = startStats(config, configTest)
|
|
if err != nil {
|
|
return nil, NewContextualError("Failed to start stats emitter", nil, err)
|
|
}
|
|
|
|
if configTest {
|
|
return nil, 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)
|
|
|
|
// 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)
|
|
}
|
|
|
|
return &Control{ifce, l}, nil
|
|
}
|