From 2e2c58bfef07b9c27900dded550de5501365fb94 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Sun, 7 Oct 2018 17:13:41 +0100 Subject: [PATCH 1/4] Add session firewall (extra security for controlling traffic flow to/from a given node) --- src/yggdrasil/config/config.go | 9 ++++ src/yggdrasil/core.go | 5 ++ src/yggdrasil/peer.go | 2 +- src/yggdrasil/session.go | 84 ++++++++++++++++++++++++++++++++++ src/yggdrasil/tcp.go | 2 +- yggdrasil.go | 3 ++ 6 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/yggdrasil/config/config.go b/src/yggdrasil/config/config.go index 5a081b6..ef5f7c2 100644 --- a/src/yggdrasil/config/config.go +++ b/src/yggdrasil/config/config.go @@ -16,6 +16,7 @@ type NodeConfig struct { IfName string `comment:"Local network interface name for TUN/TAP adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN/TAP."` IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."` IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."` + SessionFirewall SessionFirewall `comment:"The session firewall controls who can send/receive network traffic\nto/from. This is useful if you want to protect this node without\nresorting to using a real firewall. This does not affect traffic\nbeing routed via this node to somewhere else."` //Net NetConfig `comment:"Extended options for connecting to peers over other networks."` } @@ -24,3 +25,11 @@ type NetConfig struct { Tor TorConfig `comment:"Experimental options for configuring peerings over Tor."` I2P I2PConfig `comment:"Experimental options for configuring peerings over I2P."` } + +type SessionFirewall struct { + Enable bool `comment:"Enable or disable the session firewall. If disabled, network traffic\nfrom any node will be allowed. If enabled, the below rules apply."` + AllowFromDirect bool `comment:"Allow network traffic from directly connected peers."` + AllowFromRemote bool `comment:"Allow network traffic from remote nodes on the network that you are\nnot directly peered with."` + WhitelistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always accepted,\nregardless of AllowFromDirect or AllowFromRemote."` + BlacklistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always rejected,\nregardless of the whitelist, AllowFromDirect or AllowFromRemote."` +} diff --git a/src/yggdrasil/core.go b/src/yggdrasil/core.go index 224bad9..64dff63 100644 --- a/src/yggdrasil/core.go +++ b/src/yggdrasil/core.go @@ -107,6 +107,11 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error { return err } + c.sessions.setSessionFirewallState(nc.SessionFirewall.Enable) + c.sessions.setSessionFirewallDefaults(nc.SessionFirewall.AllowFromDirect, nc.SessionFirewall.AllowFromRemote) + c.sessions.setSessionFirewallWhitelist(nc.SessionFirewall.WhitelistEncryptionPublicKeys) + c.sessions.setSessionFirewallBlacklist(nc.SessionFirewall.BlacklistEncryptionPublicKeys) + if err := c.router.start(); err != nil { c.log.Println("Failed to start router") return err diff --git a/src/yggdrasil/peer.go b/src/yggdrasil/peer.go index f0dd3d0..35a73cd 100644 --- a/src/yggdrasil/peer.go +++ b/src/yggdrasil/peer.go @@ -17,7 +17,7 @@ import ( type peers struct { core *Core mutex sync.Mutex // Synchronize writes to atomic - ports atomic.Value //map[Port]*peer, use CoW semantics + ports atomic.Value //map[switchPort]*peer, use CoW semantics authMutex sync.RWMutex allowedEncryptionPublicKeys map[boxPubKey]struct{} } diff --git a/src/yggdrasil/session.go b/src/yggdrasil/session.go index 7a33226..3ed118b 100644 --- a/src/yggdrasil/session.go +++ b/src/yggdrasil/session.go @@ -6,6 +6,7 @@ package yggdrasil import ( "bytes" + "encoding/hex" "time" ) @@ -107,6 +108,12 @@ type sessions struct { byTheirPerm map[boxPubKey]*handle addrToPerm map[address]*boxPubKey subnetToPerm map[subnet]*boxPubKey + // Options from the session firewall + sessionFirewallEnabled bool + sessionFirewallAllowsDirect bool + sessionFirewallAllowsRemote bool + sessionFirewallWhitelist []string + sessionFirewallBlacklist []string } // Initializes the session struct. @@ -121,6 +128,77 @@ func (ss *sessions) init(core *Core) { ss.lastCleanup = time.Now() } +// Enable or disable the session firewall +func (ss *sessions) setSessionFirewallState(enabled bool) { + ss.sessionFirewallEnabled = enabled +} + +// Set the session firewall defaults (first parameter is whether to allow +// sessions from direct peers, second is whether to allow from remote nodes). +func (ss *sessions) setSessionFirewallDefaults(allowsDirect bool, allowsRemote bool) { + ss.sessionFirewallAllowsDirect = allowsDirect + ss.sessionFirewallAllowsRemote = allowsRemote +} + +// Set the session firewall whitelist - nodes always allowed to open sessions. +func (ss *sessions) setSessionFirewallWhitelist(whitelist []string) { + ss.sessionFirewallWhitelist = whitelist +} + +// Set the session firewall blacklist - nodes never allowed to open sessions. +func (ss *sessions) setSessionFirewallBlacklist(blacklist []string) { + ss.sessionFirewallBlacklist = blacklist +} + +// Determines whether the session with a given publickey is allowed based on +// session firewall rules. +func (ss *sessions) isSessionAllowed(pubkey *boxPubKey) bool { + // Allow by default if the session firewall is disabled + if !ss.sessionFirewallEnabled { + return true + } + // Prepare for checking whitelist/blacklist + var box boxPubKey + // Reject blacklisted nodes + for _, b := range ss.sessionFirewallBlacklist { + key, err := hex.DecodeString(b) + if err == nil { + copy(box[:boxPubKeyLen], key) + if box == *pubkey { + return false + } + } + } + // Allow whitelisted nodes + for _, b := range ss.sessionFirewallWhitelist { + key, err := hex.DecodeString(b) + if err == nil { + copy(box[:boxPubKeyLen], key) + if box == *pubkey { + return true + } + } + } + // Look and see if the pubkey is that of a direct peer + var isDirectPeer bool + for _, peer := range ss.core.peers.ports.Load().(map[switchPort]*peer) { + if peer.box == *pubkey { + isDirectPeer = true + break + } + } + // Allow direct peers if appropriate + if ss.sessionFirewallAllowsDirect && isDirectPeer { + return true + } + // Allow remote nodes if appropriate + if ss.sessionFirewallAllowsRemote && !isDirectPeer { + return true + } + // Finally, default-deny if not matching any of the above rules + return false +} + // Gets the session corresponding to a given handle. func (ss *sessions) getSessionForHandle(handle *handle) (*sessionInfo, bool) { sinfo, isIn := ss.sinfos[*handle] @@ -311,6 +389,12 @@ func (ss *sessions) sendPingPong(sinfo *sessionInfo, isPong bool) { func (ss *sessions) handlePing(ping *sessionPing) { // Get the corresponding session (or create a new session) sinfo, isIn := ss.getByTheirPerm(&ping.SendPermPub) + // Check the session firewall + if ss.sessionFirewallEnabled { + if !ss.isSessionAllowed(&ping.SendPermPub) { + return + } + } if !isIn || sinfo.timedout() { if isIn { sinfo.close() diff --git a/src/yggdrasil/tcp.go b/src/yggdrasil/tcp.go index 1942acd..0bc5802 100644 --- a/src/yggdrasil/tcp.go +++ b/src/yggdrasil/tcp.go @@ -164,7 +164,7 @@ func (iface *tcpInterface) call(saddr string, socksaddr *string, sintf string) { if err != nil { return } else { - if ief.Flags & net.FlagUp == 0 { + if ief.Flags&net.FlagUp == 0 { return } addrs, err := ief.Addrs() diff --git a/yggdrasil.go b/yggdrasil.go index 7102824..447bb3e 100644 --- a/yggdrasil.go +++ b/yggdrasil.go @@ -66,6 +66,9 @@ func generateConfig(isAutoconf bool) *nodeConfig { cfg.IfName = defaults.GetDefaults().DefaultIfName cfg.IfMTU = defaults.GetDefaults().DefaultIfMTU cfg.IfTAPMode = defaults.GetDefaults().DefaultIfTAPMode + cfg.SessionFirewall.Enable = false + cfg.SessionFirewall.AllowFromDirect = true + cfg.SessionFirewall.AllowFromRemote = true return &cfg } From 3f237372c95c11695eaee398f09092d86a8a8741 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 8 Oct 2018 19:05:50 +0100 Subject: [PATCH 2/4] Only apply session firewall to pings for sessions we don't already have --- src/yggdrasil/session.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yggdrasil/session.go b/src/yggdrasil/session.go index 3ed118b..1b8d1cb 100644 --- a/src/yggdrasil/session.go +++ b/src/yggdrasil/session.go @@ -390,7 +390,7 @@ func (ss *sessions) handlePing(ping *sessionPing) { // Get the corresponding session (or create a new session) sinfo, isIn := ss.getByTheirPerm(&ping.SendPermPub) // Check the session firewall - if ss.sessionFirewallEnabled { + if !isIn && ss.sessionFirewallEnabled { if !ss.isSessionAllowed(&ping.SendPermPub) { return } From 3ed63ede1e4b85d08c8cfcabf385a80abcf84203 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 8 Oct 2018 19:51:51 +0100 Subject: [PATCH 3/4] Add AlwaysAllowOutbound to session firewall --- src/yggdrasil/config/config.go | 1 + src/yggdrasil/core.go | 6 +++++- src/yggdrasil/search.go | 4 ++++ src/yggdrasil/session.go | 29 +++++++++++++++++++++-------- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/yggdrasil/config/config.go b/src/yggdrasil/config/config.go index ef5f7c2..c08cab6 100644 --- a/src/yggdrasil/config/config.go +++ b/src/yggdrasil/config/config.go @@ -30,6 +30,7 @@ type SessionFirewall struct { Enable bool `comment:"Enable or disable the session firewall. If disabled, network traffic\nfrom any node will be allowed. If enabled, the below rules apply."` AllowFromDirect bool `comment:"Allow network traffic from directly connected peers."` AllowFromRemote bool `comment:"Allow network traffic from remote nodes on the network that you are\nnot directly peered with."` + AlwaysAllowOutbound bool `comment:"Allow outbound network traffic regardless of AllowFromDirect or\nAllowFromRemote. This does allow a remote node to send unsolicited\ntraffic back to you for the length of the session."` WhitelistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always accepted,\nregardless of AllowFromDirect or AllowFromRemote."` BlacklistEncryptionPublicKeys []string `comment:"List of public keys from which network traffic is always rejected,\nregardless of the whitelist, AllowFromDirect or AllowFromRemote."` } diff --git a/src/yggdrasil/core.go b/src/yggdrasil/core.go index 64dff63..015147c 100644 --- a/src/yggdrasil/core.go +++ b/src/yggdrasil/core.go @@ -108,7 +108,11 @@ func (c *Core) Start(nc *config.NodeConfig, log *log.Logger) error { } c.sessions.setSessionFirewallState(nc.SessionFirewall.Enable) - c.sessions.setSessionFirewallDefaults(nc.SessionFirewall.AllowFromDirect, nc.SessionFirewall.AllowFromRemote) + c.sessions.setSessionFirewallDefaults( + nc.SessionFirewall.AllowFromDirect, + nc.SessionFirewall.AllowFromRemote, + nc.SessionFirewall.AlwaysAllowOutbound, + ) c.sessions.setSessionFirewallWhitelist(nc.SessionFirewall.WhitelistEncryptionPublicKeys) c.sessions.setSessionFirewallBlacklist(nc.SessionFirewall.BlacklistEncryptionPublicKeys) diff --git a/src/yggdrasil/search.go b/src/yggdrasil/search.go index 2928faa..1b72a63 100644 --- a/src/yggdrasil/search.go +++ b/src/yggdrasil/search.go @@ -184,6 +184,10 @@ func (s *searches) checkDHTRes(info *searchInfo, res *dhtRes) bool { sinfo, isIn := s.core.sessions.getByTheirPerm(&res.Key) if !isIn { sinfo = s.core.sessions.createSession(&res.Key) + if sinfo == nil { + // nil if the DHT search finished but the session wasn't allowed + return true + } _, isIn := s.core.sessions.getByTheirPerm(&res.Key) if !isIn { panic("This should never happen") diff --git a/src/yggdrasil/session.go b/src/yggdrasil/session.go index 1b8d1cb..0bc27a1 100644 --- a/src/yggdrasil/session.go +++ b/src/yggdrasil/session.go @@ -109,11 +109,12 @@ type sessions struct { addrToPerm map[address]*boxPubKey subnetToPerm map[subnet]*boxPubKey // Options from the session firewall - sessionFirewallEnabled bool - sessionFirewallAllowsDirect bool - sessionFirewallAllowsRemote bool - sessionFirewallWhitelist []string - sessionFirewallBlacklist []string + sessionFirewallEnabled bool + sessionFirewallAllowsDirect bool + sessionFirewallAllowsRemote bool + sessionFirewallAlwaysAllowsOutbound bool + sessionFirewallWhitelist []string + sessionFirewallBlacklist []string } // Initializes the session struct. @@ -135,9 +136,10 @@ func (ss *sessions) setSessionFirewallState(enabled bool) { // Set the session firewall defaults (first parameter is whether to allow // sessions from direct peers, second is whether to allow from remote nodes). -func (ss *sessions) setSessionFirewallDefaults(allowsDirect bool, allowsRemote bool) { +func (ss *sessions) setSessionFirewallDefaults(allowsDirect bool, allowsRemote bool, alwaysAllowsOutbound bool) { ss.sessionFirewallAllowsDirect = allowsDirect ss.sessionFirewallAllowsRemote = allowsRemote + ss.sessionFirewallAlwaysAllowsOutbound = alwaysAllowsOutbound } // Set the session firewall whitelist - nodes always allowed to open sessions. @@ -152,7 +154,7 @@ func (ss *sessions) setSessionFirewallBlacklist(blacklist []string) { // Determines whether the session with a given publickey is allowed based on // session firewall rules. -func (ss *sessions) isSessionAllowed(pubkey *boxPubKey) bool { +func (ss *sessions) isSessionAllowed(pubkey *boxPubKey, initiator bool) bool { // Allow by default if the session firewall is disabled if !ss.sessionFirewallEnabled { return true @@ -179,6 +181,12 @@ func (ss *sessions) isSessionAllowed(pubkey *boxPubKey) bool { } } } + // Allow outbound sessions if appropriate + if ss.sessionFirewallAlwaysAllowsOutbound { + if initiator { + return true + } + } // Look and see if the pubkey is that of a direct peer var isDirectPeer bool for _, peer := range ss.core.peers.ports.Load().(map[switchPort]*peer) { @@ -252,6 +260,11 @@ func (ss *sessions) getByTheirSubnet(snet *subnet) (*sessionInfo, bool) { // Creates a new session and lazily cleans up old/timedout existing sessions. // This includse initializing session info to sane defaults (e.g. lowest supported MTU). func (ss *sessions) createSession(theirPermKey *boxPubKey) *sessionInfo { + if ss.sessionFirewallEnabled { + if !ss.isSessionAllowed(theirPermKey, true) { + return nil + } + } sinfo := sessionInfo{} sinfo.core = ss.core sinfo.theirPermPub = *theirPermKey @@ -391,7 +404,7 @@ func (ss *sessions) handlePing(ping *sessionPing) { sinfo, isIn := ss.getByTheirPerm(&ping.SendPermPub) // Check the session firewall if !isIn && ss.sessionFirewallEnabled { - if !ss.isSessionAllowed(&ping.SendPermPub) { + if !ss.isSessionAllowed(&ping.SendPermPub, false) { return } } From 1e6667567af80b26a9db62ac7e3e2283ccb87741 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 8 Oct 2018 19:57:14 +0100 Subject: [PATCH 4/4] Update comments for session firewall in config --- src/yggdrasil/config/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yggdrasil/config/config.go b/src/yggdrasil/config/config.go index c08cab6..99cb2ec 100644 --- a/src/yggdrasil/config/config.go +++ b/src/yggdrasil/config/config.go @@ -16,7 +16,7 @@ type NodeConfig struct { IfName string `comment:"Local network interface name for TUN/TAP adapter, or \"auto\" to select\nan interface automatically, or \"none\" to run without TUN/TAP."` IfTAPMode bool `comment:"Set local network interface to TAP mode rather than TUN mode if\nsupported by your platform - option will be ignored if not."` IfMTU int `comment:"Maximux Transmission Unit (MTU) size for your local TUN/TAP interface.\nDefault is the largest supported size for your platform. The lowest\npossible value is 1280."` - SessionFirewall SessionFirewall `comment:"The session firewall controls who can send/receive network traffic\nto/from. This is useful if you want to protect this node without\nresorting to using a real firewall. This does not affect traffic\nbeing routed via this node to somewhere else."` + SessionFirewall SessionFirewall `comment:"The session firewall controls who can send/receive network traffic\nto/from. This is useful if you want to protect this node without\nresorting to using a real firewall. This does not affect traffic\nbeing routed via this node to somewhere else. Rules are prioritised as\nfollows: whitelist, blacklist, always allow outgoing, direct, remote."` //Net NetConfig `comment:"Extended options for connecting to peers over other networks."` }