diff --git a/firewall/firewall.go b/firewall/firewall.go index a9ed23f9..5b867e35 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -13,7 +13,6 @@ import ( "github.com/Safing/portmaster/firewall/interception" "github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network/packet" - "github.com/Safing/portmaster/portmaster" "github.com/Safing/portmaster/process" ) @@ -122,11 +121,11 @@ func handlePacket(pkt packet.Packet) { // check if packet is destined for tunnel switch pkt.IPVersion() { case packet.IPv4: - if portmaster.TunnelNet4 != nil && portmaster.TunnelNet4.Contains(pkt.GetIPHeader().Dst) { + if TunnelNet4 != nil && TunnelNet4.Contains(pkt.GetIPHeader().Dst) { tunnelHandler(pkt) } case packet.IPv6: - if portmaster.TunnelNet6 != nil && portmaster.TunnelNet6.Contains(pkt.GetIPHeader().Dst) { + if TunnelNet6 != nil && TunnelNet6.Contains(pkt.GetIPHeader().Dst) { tunnelHandler(pkt) } } @@ -184,12 +183,12 @@ func initialHandler(pkt packet.Packet, link *network.Link) { // make a decision if not made already if connection.Verdict == network.UNDECIDED { - portmaster.DecideOnConnection(connection, pkt) + DecideOnConnection(connection, pkt) } if connection.Verdict != network.CANTSAY { link.UpdateVerdict(connection.Verdict) } else { - portmaster.DecideOnLink(connection, link, pkt) + DecideOnLink(connection, link, pkt) } // log decision @@ -280,7 +279,7 @@ func verdict(pkt packet.Packet, action network.Verdict) { } // func tunnelHandler(pkt packet.Packet) { -// tunnelInfo := portmaster.GetTunnelInfo(pkt.GetIPHeader().Dst) +// tunnelInfo := GetTunnelInfo(pkt.GetIPHeader().Dst) // if tunnelInfo == nil { // pkt.Block() // return diff --git a/firewall/master.go b/firewall/master.go index dff661c5..ef258a9b 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -10,8 +10,6 @@ import ( "github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network/netutils" "github.com/Safing/portmaster/network/packet" - "github.com/Safing/portmaster/port17/mode" - "github.com/Safing/portmaster/profiles" "github.com/agext/levenshtein" ) @@ -51,7 +49,7 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) } // check user class - if profile.Flags.Has(profiles.System) { + if profile.Flags.Has(profile.System) { if !connection.Process().IsSystem() { log.Infof("sheriff: denying connection %s, profile has System flag set, but process is not executed by System", connection) connection.AddReason("must be executed by system") @@ -59,7 +57,7 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) return } } - if profile.Flags.Has(profiles.Admin) { + if profile.Flags.Has(profile.Admin) { if !connection.Process().IsAdmin() { log.Infof("sheriff: denying connection %s, profile has Admin flag set, but process is not executed by Admin", connection) connection.AddReason("must be executed by admin") @@ -67,7 +65,7 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) return } } - if profile.Flags.Has(profiles.User) { + if profile.Flags.Has(profile.User) { if !connection.Process().IsUser() { log.Infof("sheriff: denying connection %s, profile has User flag set, but process is not executed by a User", connection) connection.AddReason("must be executed by user") @@ -77,7 +75,7 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) } // check for any network access - if !profile.Flags.Has(profiles.Internet) && !profile.Flags.Has(profiles.LocalNet) { + if !profile.Flags.Has(profile.Internet) && !profile.Flags.Has(profile.LocalNet) { log.Infof("sheriff: denying connection %s, profile denies Internet and local network access", connection) connection.Block() return @@ -139,7 +137,7 @@ func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, r // check Strict flag // TODO: drastically improve this! - if profile.Flags.Has(profiles.Strict) { + if profile.Flags.Has(profile.Strict) { matched := false pathElements := strings.Split(connection.Process().Path, "/") if len(pathElements) > 2 { @@ -173,18 +171,18 @@ func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, r // tunneling // TODO: link this to real status - port17Active := mode.Client() - if port17Active { - tunnelInfo, err := AssignTunnelIP(fqdn) - if err != nil { - log.Errorf("portmaster: could not get tunnel IP for routing %s: %s", connection, err) - return nil // return nxDomain - } - // save original reply - tunnelInfo.RRCache = rrCache - // return tunnel IP - return tunnelInfo.ExportTunnelIP() - } + // gate17Active := mode.Client() + // if gate17Active { + // tunnelInfo, err := AssignTunnelIP(fqdn) + // if err != nil { + // log.Errorf("portmaster: could not get tunnel IP for routing %s: %s", connection, err) + // return nil // return nxDomain + // } + // // save original reply + // tunnelInfo.RRCache = rrCache + // // return tunnel IP + // return tunnelInfo.ExportTunnelIP() + // } return rrCache } @@ -212,7 +210,7 @@ func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { } // check user class - if profile.Flags.Has(profiles.System) { + if profile.Flags.Has(profile.System) { if !connection.Process().IsSystem() { log.Infof("sheriff: denying connection %s, profile has System flag set, but process is not executed by System", connection) connection.AddReason("must be executed by system") @@ -220,7 +218,7 @@ func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { return } } - if profile.Flags.Has(profiles.Admin) { + if profile.Flags.Has(profile.Admin) { if !connection.Process().IsAdmin() { log.Infof("sheriff: denying connection %s, profile has Admin flag set, but process is not executed by Admin", connection) connection.AddReason("must be executed by admin") @@ -228,7 +226,7 @@ func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { return } } - if profile.Flags.Has(profiles.User) { + if profile.Flags.Has(profile.User) { if !connection.Process().IsUser() { log.Infof("sheriff: denying connection %s, profile has User flag set, but process is not executed by a User", connection) connection.AddReason("must be executed by user") @@ -238,7 +236,7 @@ func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { } // check for any network access - if !profile.Flags.Has(profiles.Internet) && !profile.Flags.Has(profiles.LocalNet) { + if !profile.Flags.Has(profile.Internet) && !profile.Flags.Has(profile.LocalNet) { log.Infof("sheriff: denying connection %s, profile denies Internet and local network access", connection) connection.AddReason("no network access allowed") connection.Block() @@ -248,21 +246,21 @@ func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { switch connection.Domain { case "I": // check Service flag - if !profile.Flags.Has(profiles.Service) { + if !profile.Flags.Has(profile.Service) { log.Infof("sheriff: denying connection %s, profile does not declare service", connection) connection.AddReason("not a service") connection.Drop() return } // check if incoming connections are allowed on any port, but only if there no other restrictions - if !!profile.Flags.Has(profiles.Internet) && !!profile.Flags.Has(profiles.LocalNet) && len(profile.ListenPorts) == 0 { + if !!profile.Flags.Has(profile.Internet) && !!profile.Flags.Has(profile.LocalNet) && len(profile.ListenPorts) == 0 { log.Infof("sheriff: granting connection %s, profile allows incoming connections from anywhere and on any port", connection) connection.Accept() return } case "D": // check Directconnect flag - if !profile.Flags.Has(profiles.Directconnect) { + if !profile.Flags.Has(profile.Directconnect) { log.Infof("sheriff: denying connection %s, profile does not declare direct connections", connection) connection.AddReason("direct connections (without DNS) not allowed") connection.Drop() @@ -298,14 +296,14 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet remoteIP = pkt.GetIPHeader().Dst } if netutils.IPIsLocal(remoteIP) { - if !profile.Flags.Has(profiles.LocalNet) { + if !profile.Flags.Has(profile.LocalNet) { log.Infof("sheriff: dropping link %s, profile does not allow communication in the local network", link) link.AddReason("profile does not allow access to local network") link.UpdateVerdict(network.BLOCK) return } } else { - if !profile.Flags.Has(profiles.Internet) { + if !profile.Flags.Has(profile.Internet) { log.Infof("sheriff: dropping link %s, profile does not allow communication with the Internet", link) link.AddReason("profile does not allow access to the Internet") link.UpdateVerdict(network.BLOCK) diff --git a/intel/ipinfo.go b/intel/ipinfo.go index c375e6bf..aa13d932 100644 --- a/intel/ipinfo.go +++ b/intel/ipinfo.go @@ -2,12 +2,12 @@ package intel import ( "fmt" - "net" "strings" "sync" "github.com/Safing/portbase/database" "github.com/Safing/portbase/database/record" + "github.com/Safing/portbase/utils" ) var ( @@ -21,16 +21,16 @@ type IPInfo struct { record.Base sync.Mutex - IP net.IP + IP string Domains []string } -func makeIPInfoKey(ip net.IP) string { - return fmt.Sprintf("intel:IPInfo/%s", ip.String()) +func makeIPInfoKey(ip string) string { + return fmt.Sprintf("intel:IPInfo/%s", ip) } // GetIPInfo gets an IPInfo record from the database. -func GetIPInfo(ip net.IP) (*IPInfo, error) { +func GetIPInfo(ip string) (*IPInfo, error) { key := makeIPInfoKey(ip) r, err := ipInfoDatabase.Get(key) @@ -57,6 +57,17 @@ func GetIPInfo(ip net.IP) (*IPInfo, error) { return new, nil } +// AddDomain adds a domain to the list and reports back if it was added, or was already present. +func (ipi *IPInfo) AddDomain(domain string) (added bool) { + if !utils.StringInSlice(ipi.Domains, domain) { + newDomains := make([]string, 1, len(ipi.Domains)+1) + newDomains[0] = domain + ipi.Domains = append(newDomains, ipi.Domains...) + return true + } + return false +} + // Save saves the IPInfo record to the database. func (ipi *IPInfo) Save() error { ipi.SetKey(makeIPInfoKey(ipi.IP)) diff --git a/intel/main.go b/intel/main.go index e9509b3c..3052a9ab 100644 --- a/intel/main.go +++ b/intel/main.go @@ -31,6 +31,7 @@ func start() error { return nil } +// GetIntelAndRRs returns intel and DNS resource records for the given domain. func GetIntelAndRRs(domain string, qtype dns.Type, securityLevel uint8) (intel *Intel, rrs *RRCache) { intel, err := GetIntel(domain) if err != nil { diff --git a/intel/mdns.go b/intel/mdns.go index 079e3dec..0292ef6f 100644 --- a/intel/mdns.go +++ b/intel/mdns.go @@ -15,6 +15,7 @@ import ( "github.com/Safing/portbase/log" ) +// DNS Classes const ( DNSClassMulticast = dns.ClassINET | 1<<15 ) diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index adfc1216..5acd1000 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -15,7 +15,7 @@ import ( "github.com/Safing/portmaster/intel" "github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network/netutils" - "github.com/Safing/portmaster/portmaster" + "github.com/Safing/portmaster/firewall" ) func init() { @@ -113,7 +113,7 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) { // check profile before we even get intel and rr if connection.Verdict == network.UNDECIDED { // start = time.Now() - portmaster.DecideOnConnectionBeforeIntel(connection, fqdn) + firewall.DecideOnConnectionBeforeIntel(connection, fqdn) // log.Tracef("nameserver: took %s to make decision", time.Since(start)) } if connection.Verdict == network.BLOCK || connection.Verdict == network.DROP { @@ -138,7 +138,7 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) { // do a full check with intel if connection.Verdict == network.UNDECIDED { - rrCache = portmaster.DecideOnConnectionAfterIntel(connection, fqdn, rrCache) + rrCache = firewall.DecideOnConnectionAfterIntel(connection, fqdn, rrCache) } if rrCache == nil || connection.Verdict == network.BLOCK || connection.Verdict == network.DROP { nxDomain(w, query) @@ -163,6 +163,7 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) { ipInfo, err := intel.GetIPInfo(v.AAAA.String()) if err != nil { ipInfo = &intel.IPInfo{ + IP: Domains: []string{fqdn}, } ipInfo.Create(v.AAAA.String()) diff --git a/network/clean.go b/network/clean.go index 6f84fd49..556605a7 100644 --- a/network/clean.go +++ b/network/clean.go @@ -8,6 +8,11 @@ import ( "github.com/Safing/portmaster/process" ) +var ( + deadLinksTimeout = 5 * time.Minute + thresholdDuration = 1 * time.Minute +) + func init() { go cleaner() } @@ -21,18 +26,21 @@ func cleaner() { } } -func markDeadLinks() { +func cleanLinks() { activeIDs := process.GetActiveConnectionIDs() - allLinksLock.RLock() - defer allLinksLock.RUnlock() + dataLock.Lock() + defer dataLock.Lock() now := time.Now().Unix() - var found bool - for key, link := range allLinks { + deleteOlderThan := time.Now().Add(-deadLinksTimeout).Unix() - // skip dead links - if link.Ended > 0 { + var found bool + for key, link := range links { + + // delete dead links + if link.Ended > 0 && link.Ended < deleteOlderThan { + link.Delete() continue } @@ -54,50 +62,18 @@ func markDeadLinks() { } } -func purgeDeadFor(age time.Duration) { - connections := make(map[*Connection]bool) - processes := make(map[*process.Process]bool) +func cleanConnections() { + dataLock.Lock() + defer dataLock.Lock() - allLinksLock.Lock() - defer allLinksLock.Unlock() - - // delete old dead links - // make a list of connections without links - ageAgo := time.Now().Add(-1 * age).Unix() - for key, link := range allLinks { - if link.Ended != 0 && link.Ended < ageAgo { - link.Delete() - delete(allLinks, key) - _, ok := connections[link.Connection()] - if !ok { - connections[link.Connection()] = false - } - } else { - connections[link.Connection()] = true + threshold := time.Now().Add(-thresholdDuration).Unix() + for _, conn := range connections { + if conn.FirstLinkEstablished < threshold && conn.LinkCount == 0 { + conn.Delete() } } - - // delete connections without links - // make a list of processes without connections - for conn, active := range connections { - if conn != nil { - if !active { - conn.Delete() - _, ok := processes[conn.Process()] - if !ok { - processes[conn.Process()] = false - } - } else { - processes[conn.Process()] = true - } - } - } - - // delete processes without connections - for proc, active := range processes { - if proc != nil && !active { - proc.Delete() - } - } - +} + +func cleanProcesses() { + process.CleanProcessStorage(thresholdDuration) } diff --git a/network/connection.go b/network/connection.go index fa61895c..97e52d99 100644 --- a/network/connection.go +++ b/network/connection.go @@ -3,6 +3,7 @@ package network import ( + "errors" "fmt" "net" "sync" @@ -19,67 +20,69 @@ type Connection struct { record.Base sync.Mutex - Domain string - Direction bool - Intel *intel.Intel - process *process.Process - Verdict Verdict - Reason string - Inspect bool + Domain string + Direction bool + Intel *intel.Intel + process *process.Process + Verdict Verdict + Reason string + Inspect bool + FirstLinkEstablished int64 LastLinkEstablished int64 + LinkCount uint } // Process returns the process that owns the connection. -func (m *Connection) Process() *process.Process { - return m.process +func (conn *Connection) Process() *process.Process { + return conn.process } // CantSay sets the connection verdict to "can't say", the connection will be further analysed. -func (m *Connection) CantSay() { - if m.Verdict != CANTSAY { - m.Verdict = CANTSAY - m.Save() +func (conn *Connection) CantSay() { + if conn.Verdict != CANTSAY { + conn.Verdict = CANTSAY + conn.Save() } return } // Drop sets the connection verdict to drop. -func (m *Connection) Drop() { - if m.Verdict != DROP { - m.Verdict = DROP - m.Save() +func (conn *Connection) Drop() { + if conn.Verdict != DROP { + conn.Verdict = DROP + conn.Save() } return } // Block sets the connection verdict to block. -func (m *Connection) Block() { - if m.Verdict != BLOCK { - m.Verdict = BLOCK - m.Save() +func (conn *Connection) Block() { + if conn.Verdict != BLOCK { + conn.Verdict = BLOCK + conn.Save() } return } // Accept sets the connection verdict to accept. -func (m *Connection) Accept() { - if m.Verdict != ACCEPT { - m.Verdict = ACCEPT - m.Save() +func (conn *Connection) Accept() { + if conn.Verdict != ACCEPT { + conn.Verdict = ACCEPT + conn.Save() } return } // AddReason adds a human readable string as to why a certain verdict was set in regard to this connection -func (m *Connection) AddReason(newReason string) { - m.Lock() - defer m.Unlock() +func (conn *Connection) AddReason(newReason string) { + conn.Lock() + defer conn.Unlock() - if m.Reason != "" { - m.Reason += " | " + if conn.Reason != "" { + conn.Reason += " | " } - m.Reason += newReason + conn.Reason += newReason } // GetConnectionByFirstPacket returns the matching connection from the internal storage. @@ -92,16 +95,17 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) { // if INBOUND if direction { - connection, err := GetConnectionFromProcessNamespace(proc, "I") - if err != nil { + connection, ok := GetConnection(proc.Pid, "I") + if !ok { connection = &Connection{ Domain: "I", - Direction: true, + Direction: Inbound, process: proc, Inspect: true, FirstLinkEstablished: time.Now().Unix(), } } + connection.process.AddConnection() return connection, nil } @@ -109,28 +113,32 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) { ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP()) if err != nil { // if no domain could be found, it must be a direct connection - connection, err := GetConnectionFromProcessNamespace(proc, "D") - if err != nil { + connection, ok := GetConnection(proc.Pid, "D") + if !ok { connection = &Connection{ Domain: "D", + Direction: Outbound, process: proc, Inspect: true, FirstLinkEstablished: time.Now().Unix(), } } + connection.process.AddConnection() return connection, nil } // FIXME: how to handle multiple possible domains? - connection, err := GetConnectionFromProcessNamespace(proc, ipinfo.Domains[0]) - if err != nil { + connection, ok := GetConnection(proc.Pid, ipinfo.Domains[0]) + if !ok { connection = &Connection{ Domain: ipinfo.Domains[0], + Direction: Outbound, process: proc, Inspect: true, FirstLinkEstablished: time.Now().Unix(), } } + connection.process.AddConnection() return connection, nil } @@ -149,19 +157,70 @@ func GetConnectionByDNSRequest(ip net.IP, port uint16, fqdn string) (*Connection return nil, err } - connection, err := GetConnectionFromProcessNamespace(proc, fqdn) - if err != nil { + connection, ok := GetConnection(proc.Pid, fqdn) + if !ok { connection = &Connection{ Domain: fqdn, process: proc, Inspect: true, } - connection.CreateInProcessNamespace() + connection.process.AddConnection() + connection.Save() } return connection, nil } -// AddLink applies the connection to the link. +// GetConnection fetches a connection object from the internal storage. +func GetConnection(pid int, domain string) (conn *Connection, ok bool) { + dataLock.RLock() + defer dataLock.RUnlock() + conn, ok = connections[fmt.Sprintf("%d/%s", pid, domain)] + return +} + +func (conn *Connection) makeKey() string { + return fmt.Sprintf("%d/%s", conn.process.Pid, conn.Domain) +} + +// Save saves the connection object in the storage and propagates the change. +func (conn *Connection) Save() error { + if conn.process == nil { + return errors.New("cannot save connection without process") + } + + if conn.DatabaseKey() == "" { + conn.SetKey(fmt.Sprintf("network:tree/%d/%s", conn.process.Pid, conn.Domain)) + conn.CreateMeta() + } + + key := conn.makeKey() + dataLock.RLock() + _, ok := connections[key] + dataLock.RUnlock() + + if !ok { + dataLock.Lock() + connections[key] = conn + dataLock.Unlock() + } + + dbController.PushUpdate(conn) + return nil +} + +// Delete deletes a connection from the storage and propagates the change. +func (conn *Connection) Delete() { + dataLock.Lock() + defer dataLock.Unlock() + delete(connections, conn.makeKey()) + conn.Lock() + defer conn.Lock() + conn.Meta().Delete() + dbController.PushUpdate(conn) + conn.process.RemoveConnection() +} + +// AddLink applies the connection to the link and increases sets counter and timestamps. func (conn *Connection) AddLink(link *Link) { link.Lock() defer link.Unlock() @@ -172,6 +231,7 @@ func (conn *Connection) AddLink(link *Link) { conn.Lock() defer conn.Unlock() + conn.LinkCount++ conn.LastLinkEstablished = time.Now().Unix() if conn.FirstLinkEstablished == 0 { conn.FirstLinkEstablished = conn.FirstLinkEstablished @@ -179,24 +239,32 @@ func (conn *Connection) AddLink(link *Link) { conn.Save() } -// FORMATTING - -func (m *Connection) String() string { - switch m.Domain { - case "I": - if m.process == nil { - return "? <- *" - } - return fmt.Sprintf("%s <- *", m.process.String()) - case "D": - if m.process == nil { - return "? -> *" - } - return fmt.Sprintf("%s -> *", m.process.String()) - default: - if m.process == nil { - return fmt.Sprintf("? -> %s", m.Domain) - } - return fmt.Sprintf("%s -> %s", m.process.String(), m.Domain) +// RemoveLink lowers the link counter by one. +func (conn *Connection) RemoveLink() { + conn.Lock() + defer conn.Unlock() + if conn.LinkCount > 0 { + conn.LinkCount-- + } +} + +// String returns a string representation of Connection. +func (conn *Connection) String() string { + switch conn.Domain { + case "I": + if conn.process == nil { + return "? <- *" + } + return fmt.Sprintf("%s <- *", conn.process.String()) + case "D": + if conn.process == nil { + return "? -> *" + } + return fmt.Sprintf("%s -> *", conn.process.String()) + default: + if conn.process == nil { + return fmt.Sprintf("? -> %s", conn.Domain) + } + return fmt.Sprintf("%s -> %s", conn.process.String(), conn.Domain) } } diff --git a/network/link.go b/network/link.go index 00944a84..9cb6d64e 100644 --- a/network/link.go +++ b/network/link.go @@ -17,14 +17,12 @@ import ( type FirewallHandler func(pkt packet.Packet, link *Link) var ( - linkTimeout = 10 * time.Minute - allLinks = make(map[string]*Link) - allLinksLock sync.RWMutex + linkTimeout = 10 * time.Minute ) // Link describes a distinct physical connection (e.g. TCP connection) - like an instance - of a Connection. type Link struct { - record.Record + record.Base sync.Mutex ID string @@ -120,21 +118,22 @@ func (link *Link) Save() error { } if link.DatabaseKey() == "" { - link.SetKey(fmt.Sprintf("network:tree/%d/%s/%s", link.connection.Process().Pid, link.connection.Domain, link.ID)) link.CreateMeta() - - dataLock.Lock() - defer dataLock.Unlock() - - links[link.ID] = link } - if link.orphaned && link.connection != nil { - p.SetKey() + dataLock.RLock() + _, ok := links[link.ID] + dataLock.RUnlock() + + if !ok { + dataLock.Lock() + links[link.ID] = link + dataLock.Unlock() } dbController.PushUpdate(link) + return nil } // Delete deletes a link from the storage and propagates the change. @@ -146,6 +145,7 @@ func (link *Link) Delete() { defer link.Lock() link.Meta().Delete() dbController.PushUpdate(link) + link.connection.RemoveLink() } // GetLink fetches a Link from the database from the default namespace for this object @@ -159,7 +159,7 @@ func GetLink(id string) (*Link, bool) { // GetOrCreateLinkByPacket returns the associated Link for a packet and a bool expressing if the Link was newly created func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) { - link, ok := GetLink(pkt.GetConnectionID()) + link, ok := GetLink(pkt.GetLinkID()) if ok { return link, false } @@ -169,7 +169,7 @@ func GetOrCreateLinkByPacket(pkt packet.Packet) (*Link, bool) { // CreateLinkFromPacket creates a new Link based on Packet. func CreateLinkFromPacket(pkt packet.Packet) *Link { link := &Link{ - ID: pkt.GetConnectionID(), + ID: pkt.GetLinkID(), Verdict: UNDECIDED, Started: time.Now().Unix(), RemoteAddress: pkt.FmtRemoteAddress(), diff --git a/network/packet/packet.go b/network/packet/packet.go index 3d3ad336..16dcf166 100644 --- a/network/packet/packet.go +++ b/network/packet/packet.go @@ -106,10 +106,10 @@ type TCPUDPHeader struct { } type PacketBase struct { - connectionID string - Direction bool - InTunnel bool - Payload []byte + linkID string + Direction bool + InTunnel bool + Payload []byte *IPHeader *TCPUDPHeader } @@ -146,25 +146,25 @@ func (pkt *PacketBase) IPVersion() IPVersion { return pkt.Version } -func (pkt *PacketBase) GetConnectionID() string { - if pkt.connectionID == "" { - pkt.createConnectionID() +func (pkt *PacketBase) GetLinkID() string { + if pkt.linkID == "" { + pkt.createLinkID() } - return pkt.connectionID + return pkt.linkID } -func (pkt *PacketBase) createConnectionID() { +func (pkt *PacketBase) createLinkID() { if pkt.IPHeader.Protocol == TCP || pkt.IPHeader.Protocol == UDP { if pkt.Direction { - pkt.connectionID = fmt.Sprintf("%d-%s-%d-%s-%d", pkt.Protocol, pkt.Dst, pkt.DstPort, pkt.Src, pkt.SrcPort) + pkt.linkID = fmt.Sprintf("%d-%s-%d-%s-%d", pkt.Protocol, pkt.Dst, pkt.DstPort, pkt.Src, pkt.SrcPort) } else { - pkt.connectionID = fmt.Sprintf("%d-%s-%d-%s-%d", pkt.Protocol, pkt.Src, pkt.SrcPort, pkt.Dst, pkt.DstPort) + pkt.linkID = fmt.Sprintf("%d-%s-%d-%s-%d", pkt.Protocol, pkt.Src, pkt.SrcPort, pkt.Dst, pkt.DstPort) } } else { if pkt.Direction { - pkt.connectionID = fmt.Sprintf("%d-%s-%s", pkt.Protocol, pkt.Dst, pkt.Src) + pkt.linkID = fmt.Sprintf("%d-%s-%s", pkt.Protocol, pkt.Dst, pkt.Src) } else { - pkt.connectionID = fmt.Sprintf("%d-%s-%s", pkt.Protocol, pkt.Src, pkt.Dst) + pkt.linkID = fmt.Sprintf("%d-%s-%s", pkt.Protocol, pkt.Src, pkt.Dst) } } } @@ -299,7 +299,7 @@ type Packet interface { IsOutbound() bool SetInbound() SetOutbound() - GetConnectionID() string + GetLinkID() string IPVersion() IPVersion // MATCHING diff --git a/process/database.go b/process/database.go index d47f89a1..a6cb8471 100644 --- a/process/database.go +++ b/process/database.go @@ -3,6 +3,7 @@ package process import ( "fmt" "sync" + "time" "github.com/Safing/portbase/database" "github.com/tevino/abool" @@ -16,10 +17,6 @@ var ( dbControllerFlag = abool.NewBool(false) ) -func makeProcessKey(pid int) string { - return fmt.Sprintf("network:tree/%d", pid) -} - // GetProcessFromStorage returns a process from the internal storage. func GetProcessFromStorage(pid int) (*Process, bool) { processesLock.RLock() @@ -48,13 +45,18 @@ func (p *Process) Save() { defer p.Unlock() if p.DatabaseKey() == "" { - p.SetKey(makeProcessKey(p.Pid)) + p.SetKey(fmt.Sprintf("network:tree/%d", p.Pid)) p.CreateMeta() + } + processesLock.RLock() + _, ok := processes[p.Pid] + processesLock.RUnlock() + + if !ok { processesLock.Lock() - defer processesLock.Unlock() - processes[p.Pid] = p + processesLock.Unlock() } if dbControllerFlag.IsSet() { @@ -62,6 +64,33 @@ func (p *Process) Save() { } } +// Delete deletes a process from the storage and propagates the change. +func (p *Process) Delete() { + processesLock.Lock() + defer processesLock.Unlock() + delete(processes, p.Pid) + p.Lock() + defer p.Lock() + p.Meta().Delete() + + if dbControllerFlag.IsSet() { + dbController.PushUpdate(p) + } +} + +// CleanProcessStorage cleans the storage from old processes. +func CleanProcessStorage(thresholdDuration time.Duration) { + processesLock.Lock() + defer processesLock.Unlock() + + threshold := time.Now().Add(-thresholdDuration).Unix() + for _, p := range processes { + if p.FirstConnectionEstablished < threshold && p.ConnectionCount == 0 { + p.Delete() + } + } +} + // SetDBController sets the database controller and allows the package to push database updates on a save. It must be set by the package that registers the "network" database. func SetDBController(controller *database.Controller) { dbController = controller diff --git a/process/process.go b/process/process.go index 36e3dbef..8481bdfe 100644 --- a/process/process.go +++ b/process/process.go @@ -6,6 +6,7 @@ import ( "fmt" "runtime" "sync" + "time" processInfo "github.com/shirou/gopsutil/process" @@ -32,14 +33,38 @@ type Process struct { Name string Icon string // Icon is a path to the icon and is either prefixed "f:" for filepath, "d:" for database cache path or "c:"/"a:" for a the icon key to fetch it from a company / authoritative node and cache it in its own cache. + + FirstConnectionEstablished int64 + LastConnectionEstablished int64 + ConnectionCount uint } // Strings returns a string represenation of process -func (m *Process) String() string { - if m == nil { +func (p *Process) String() string { + if p == nil { return "?" } - return fmt.Sprintf("%s:%s:%d", m.UserName, m.Path, m.Pid) + return fmt.Sprintf("%s:%s:%d", p.UserName, p.Path, p.Pid) +} + +// AddConnection increases the connection counter and the last connection timestamp. +func (p *Process) AddConnection() { + p.Lock() + defer p.Unlock() + p.ConnectionCount++ + p.LastConnectionEstablished = time.Now().Unix() + if p.FirstConnectionEstablished == 0 { + p.FirstConnectionEstablished = p.LastConnectionEstablished + } +} + +// RemoveConnection lowers the connection counter by one. +func (p *Process) RemoveConnection() { + p.Lock() + defer p.Unlock() + if p.ConnectionCount > 0 { + p.ConnectionCount-- + } } // GetOrFindProcess returns the process for the given PID. diff --git a/process/unknown.go b/process/unknown.go index cbadfb35..07f62056 100644 --- a/process/unknown.go +++ b/process/unknown.go @@ -1,6 +1,7 @@ package process var ( + // UnknownProcess is used when a process cannot be found. UnknownProcess = &Process{ UserID: -1, UserName: "Unknown",