diff --git a/firewall/master.go b/firewall/master.go index 3307a5ff..9e5be52c 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -1,15 +1,14 @@ package firewall import ( - "net" "os" "strings" "github.com/Safing/portbase/log" "github.com/Safing/portmaster/intel" "github.com/Safing/portmaster/network" - "github.com/Safing/portmaster/network/netutils" "github.com/Safing/portmaster/network/packet" + "github.com/Safing/portmaster/status" "github.com/agext/levenshtein" ) @@ -25,6 +24,7 @@ import ( // 4. DecideOnLink // is called when when the first packet of a link arrives only if connection has verdict UNDECIDED or CANTSAY +// DecideOnConnectionBeforeIntel makes a decision about a connection before the dns query is resolved and intel is gathered. func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) { // check: // Profile.DomainWhitelist @@ -35,244 +35,227 @@ func DecideOnConnectionBeforeIntel(connection *network.Connection, fqdn string) // grant self if connection.Process().Pid == os.Getpid() { log.Infof("firewall: granting own connection %s", connection) - connection.Accept() + connection.Accept("") return } // check if there is a profile - profileSet := connection.Process().ProfileSetSet - if profile == nil { - log.Infof("firewall: no profile, denying connection %s", connection) - connection.AddReason("no profile") - connection.Block() + profileSet := connection.Process().ProfileSet() + if profileSet == nil { + log.Errorf("firewall: denying connection %s, no profile set", connection) + connection.Deny("no profile set") return } - - // check user class - if profileSet.CheckFlag(profile.System) { - if !connection.Process().IsSystem() { - log.Infof("firewall: denying connection %s, profile has System flag set, but process is not executed by System", connection) - connection.AddReason("must be executed by system") - connection.Block() - return - } - } - if profileSet.CheckFlag(profile.Admin) { - if !connection.Process().IsAdmin() { - log.Infof("firewall: denying connection %s, profile has Admin flag set, but process is not executed by Admin", connection) - connection.AddReason("must be executed by admin") - connection.Block() - return - } - } - if profileSet.CheckFlag(profile.User) { - if !connection.Process().IsUser() { - log.Infof("firewall: denying connection %s, profile has User flag set, but process is not executed by a User", connection) - connection.AddReason("must be executed by user") - connection.Block() - return - } - } + profileSet.Update(status.CurrentSecurityLevel()) // check for any network access - if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LocalNet) { - log.Infof("firewall: denying connection %s, profile denies Internet and local network access", connection) - connection.Block() + if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LAN) { + log.Infof("firewall: denying connection %s, accessing Internet or LAN not allowed", connection) + connection.Deny("accessing Internet or LAN not allowed") return } - // check domain whitelist/blacklist - if len(profile.DomainWhitelist) > 0 { - matched := false - for _, entry := range profile.DomainWhitelist { - if !strings.HasSuffix(entry, ".") { - entry += "." - } - if strings.HasPrefix(entry, "*") { - if strings.HasSuffix(fqdn, strings.Trim(entry, "*")) { - matched = true - break - } - } else { - if entry == fqdn { - matched = true - break - } - } - } - if matched { - if profile.DomainWhitelistIsBlacklist { - log.Infof("firewall: denying connection %s, profile has %s in domain blacklist", connection, fqdn) - connection.AddReason("domain blacklisted") - connection.Block() - return - } + // check domain list + permitted, ok := profileSet.CheckDomain(fqdn) + if ok { + if permitted { + log.Infof("firewall: accepting connection %s, domain is whitelisted", connection, domainElement, processElement) + connection.Accept("domain is whitelisted") } else { - if !profile.DomainWhitelistIsBlacklist { - log.Infof("firewall: denying connection %s, profile does not have %s in domain whitelist", connection, fqdn) - connection.AddReason("domain not in whitelist") - connection.Block() - return + log.Infof("firewall: denying connection %s, domain is blacklisted", connection, domainElement, processElement) + connection.Deny("domain is blacklisted") + } + return + } + + switch profileSet.GetProfileMode() { + case profile.Whitelist: + log.Infof("firewall: denying connection %s, domain is not whitelisted", connection, domainElement, processElement) + connection.Deny("domain is not whitelisted") + case profile.Prompt: + + // check Related flag + // TODO: improve this! + if profileSet.CheckFlag(profile.Related) { + matched := false + pathElements := strings.Split(connection.Process().Path, "/") // FIXME: path seperator + // only look at the last two path segments + if len(pathElements) > 2 { + pathElements = pathElements[len(pathElements)-2:] } - } - } + domainElements := strings.Split(fqdn, ".") -} + var domainElement string + var processElement string -func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, rrCache *intel.RRCache) *intel.RRCache { - // check: - // TODO: Profile.ClassificationBlacklist - // TODO: Profile.ClassificationWhitelist - // Profile.Flags - // - network specific: Strict - - // check if there is a profile - profileSet := connection.Process().ProfileSet - // FIXME: there should always be a profile - if profileSet == nil { - log.Infof("firewall: no profile, denying connection %s", connection) - connection.AddReason("no profile") - connection.Block() - return rrCache - } - - // check Strict flag - // TODO: drastically improve this! - if profileSet.CheckFlag(profile.Related) { - matched := false - pathElements := strings.Split(connection.Process().Path, "/") - if len(pathElements) > 2 { - pathElements = pathElements[len(pathElements)-2:] - } - domainElements := strings.Split(fqdn, ".") - matchLoop: - for _, domainElement := range domainElements { - for _, pathElement := range pathElements { - if levenshtein.Match(domainElement, pathElement, nil) > 0.5 { + matchLoop: + for _, domainElement = range domainElements { + for _, pathElement := range pathElements { + if levenshtein.Match(domainElement, pathElement, nil) > 0.5 { + matched = true + processElement = pathElement + break matchLoop + } + } + if levenshtein.Match(domainElement, profile.Name, nil) > 0.5 { matched = true + processElement = profile.Name + break matchLoop + } + if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 { + matched = true + processElement = connection.Process().Name break matchLoop } } - if levenshtein.Match(domainElement, profile.Name, nil) > 0.5 { - matched = true - break matchLoop - } - if levenshtein.Match(domainElement, connection.Process().Name, nil) > 0.5 { - matched = true - break matchLoop + + if matched { + log.Infof("firewall: accepting connection %s, match to domain was found: %s ~= %s", connection, domainElement, processElement) + connection.Accept("domain is related to process") } } - if !matched { - log.Infof("firewall: denying connection %s, profile has declared Strict flag and no match to domain was found", connection) - connection.AddReason("domain does not relate to process") - connection.Block() - return rrCache + + if connection.Verdict != network.ACCEPT { + // TODO + log.Infof("firewall: accepting connection %s, domain permitted (prompting is not yet implemented)", connection, domainElement, processElement) + connection.Accept("domain permitted (prompting is not yet implemented)") } + + case profile.Blacklist: + log.Infof("firewall: denying connection %s, domain is not blacklisted", connection, domainElement, processElement) + connection.Deny("domain is not blacklisted") } - // tunneling - // TODO: link this to real status - // 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 } -func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { - // check: - // Profile.Flags - // - process specific: System, Admin, User - // - network specific: Internet, LocalNet, Service, Directconnect +// DecideOnConnectionAfterIntel makes a decision about a connection after the dns query is resolved and intel is gathered. +func DecideOnConnectionAfterIntel(connection *network.Connection, fqdn string, rrCache *intel.RRCache) *intel.RRCache { // grant self if connection.Process().Pid == os.Getpid() { log.Infof("firewall: granting own connection %s", connection) - connection.Accept() + connection.Accept("") + return rrCache + } + + // check if there is a profile + profileSet := connection.Process().ProfileSet() + if profileSet == nil { + log.Errorf("firewall: denying connection %s, no profile set", connection) + connection.Deny("no profile") + return rrCache + } + profileSet.Update(status.CurrentSecurityLevel()) + + // TODO: Stamp integration + + // TODO: Gate17 integration + // tunnelInfo, err := AssignTunnelIP(fqdn) + + rrCache.Duplicate().FilterEntries(profileSet.CheckFlag(profile.Internet), profileSet.CheckFlag(profile.LAN), false) + if len(rrCache.Answer) == 0 { + if profileSet.CheckFlag(profile.Internet) { + connection.Deny("server is located in the LAN, but LAN access is not permitted") + } else { + connection.Deny("server is located in the Internet, but Internet access is not permitted") + } + } + + return rrCache +} + +// DeciceOnConnection makes a decision about a connection with its first packet. +func DecideOnConnection(connection *network.Connection, pkt packet.Packet) { + + // grant self + if connection.Process().Pid == os.Getpid() { + log.Infof("firewall: granting own connection %s", connection) + connection.Accept("") return } // check if there is a profile profileSet := connection.Process().ProfileSet if profile == nil { - log.Infof("firewall: no profile, denying connection %s", connection) - connection.AddReason("no profile") - connection.Block() - return - } - - // check user class - if profileSet.CheckFlag(profile.System) { - if !connection.Process().IsSystem() { - log.Infof("firewall: denying connection %s, profile has System flag set, but process is not executed by System", connection) - connection.AddReason("must be executed by system") - connection.Block() - return - } - } - if profileSet.CheckFlag(profile.Admin) { - if !connection.Process().IsAdmin() { - log.Infof("firewall: denying connection %s, profile has Admin flag set, but process is not executed by Admin", connection) - connection.AddReason("must be executed by admin") - connection.Block() - return - } - } - if profileSet.CheckFlag(profile.User) { - if !connection.Process().IsUser() { - log.Infof("firewall: denying connection %s, profile has User flag set, but process is not executed by a User", connection) - connection.AddReason("must be executed by user") - connection.Block() - return - } - } - - // check for any network access - if !profileSet.CheckFlag(profile.Internet) && !profileSet.CheckFlag(profile.LocalNet) { - log.Infof("firewall: denying connection %s, profile denies Internet and local network access", connection) - connection.AddReason("no network access allowed") - connection.Block() + log.Errorf("firewall: denying connection %s, no profile set", connection) + connection.Deny("no profile") return } + profileSet.Update(status.CurrentSecurityLevel()) + // check connection type switch connection.Domain { - case "I": - // check Service flag + case IncomingHost, IncomingLAN, IncomingInternet, IncomingInvalid: if !profileSet.CheckFlag(profile.Service) { - log.Infof("firewall: denying connection %s, profile does not declare service", connection) - connection.AddReason("not a service") - connection.Drop() + log.Infof("firewall: denying connection %s, not a service", connection) + if connection.Domain == IncomingHost { + connection.Block("not a service") + } else { + connection.Drop("not a service") + } return } - // check if incoming connections are allowed on any port, but only if there no other restrictions - if !!profileSet.CheckFlag(profile.Internet) && !!profileSet.CheckFlag(profile.LocalNet) && len(profile.ListenPorts) == 0 { - log.Infof("firewall: granting connection %s, profile allows incoming connections from anywhere and on any port", connection) - connection.Accept() - return - } - case "D": - // check PeerToPeer flag + case PeerLAN, PeerInternet, PeerInvalid: // Important: PeerHost is and should be missing! if !profileSet.CheckFlag(profile.PeerToPeer) { - log.Infof("firewall: denying connection %s, profile does not declare direct connections", connection) - connection.AddReason("direct connections (without DNS) not allowed") - connection.Drop() + log.Infof("firewall: denying connection %s, peer to peer connections (to an IP) not allowed", connection) + connection.Deny("peer to peer connections (to an IP) not allowed") return } } - log.Infof("firewall: could not decide on connection %s, deciding on per-link basis", connection) - connection.CantSay() + // check network scope + switch connection.Domain { + case IncomingHost: + if !profileSet.CheckFlag(profile.Localhost) { + log.Infof("firewall: denying connection %s, serving localhost not allowed", connection) + connection.Block("serving localhost not allowed") + return + } + case IncomingLAN: + if !profileSet.CheckFlag(profile.LAN) { + log.Infof("firewall: denying connection %s, serving LAN not allowed", connection) + connection.Deny("serving LAN not allowed") + return + } + case IncomingInternet: + if !profileSet.CheckFlag(profile.Internet) { + log.Infof("firewall: denying connection %s, serving Internet not allowed", connection) + connection.Deny("serving Internet not allowed") + return + } + case IncomingInvalid: + log.Infof("firewall: denying connection %s, invalid IP address", connection) + connection.Drop("invalid IP address") + return + case PeerHost: + if !profileSet.CheckFlag(profile.Localhost) { + log.Infof("firewall: denying connection %s, accessing localhost not allowed", connection) + connection.Block("accessing localhost not allowed") + return + } + case PeerLAN: + if !profileSet.CheckFlag(profile.LAN) { + log.Infof("firewall: denying connection %s, accessing the LAN not allowed", connection) + connection.Deny("accessing the LAN not allowed") + return + } + case PeerInternet: + if !profileSet.CheckFlag(profile.Internet) { + log.Infof("firewall: denying connection %s, accessing the Internet not allowed", connection) + connection.Deny("accessing the Internet not allowed") + return + } + case PeerInvalid: + log.Infof("firewall: denying connection %s, invalid IP address", connection) + connection.Deny("invalid IP address") + return + } + + log.Infof("firewall: accepting connection %s", connection) + connection.Accept() } +// DecideOnLink makes a decision about a link with the first packet. func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet.Packet) { // check: // Profile.Flags @@ -284,107 +267,44 @@ func DecideOnLink(connection *network.Connection, link *network.Link, pkt packet profileSet := connection.Process().ProfileSet if profile == nil { log.Infof("firewall: no profile, denying %s", link) - link.AddReason("no profile") - link.UpdateVerdict(network.BLOCK) + link.Block("no profile") + return + } + profileSet.Update(status.CurrentSecurityLevel()) + + // get remote Port + protocol := pkt.GetIPHeader().Protocol + var remotePort uint16 + tcpUdpHeader := pkt.GetTCPUDPHeader() + if tcpUdpHeader != nil { + remotePort = tcpUdpHeader.DstPort + } + + // check port list + permitted, ok := profileSet.CheckPort(connection.Direction, protocol, remotePort) + if ok { + if permitted { + log.Infof("firewall: accepting link %s", link) + link.Accept("port whitelisted") + } else { + log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort) + link.Deny("port blacklisted") + } return } - // check LocalNet and Internet flags - var remoteIP net.IP - if connection.Direction { - remoteIP = pkt.GetIPHeader().Src - } else { - remoteIP = pkt.GetIPHeader().Dst - } - if netutils.IPIsLocal(remoteIP) { - if !profileSet.CheckFlag(profile.LocalNet) { - log.Infof("firewall: 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 !profileSet.CheckFlag(profile.Internet) { - log.Infof("firewall: 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) - return - } - } - - // check connect ports - if connection.Domain != "I" && len(profile.ConnectPorts) > 0 { - - tcpUdpHeader := pkt.GetTCPUDPHeader() - if tcpUdpHeader == nil { - log.Infof("firewall: blocking link %s, profile has declared connect port whitelist, but link is not TCP/UDP", link) - link.AddReason("profile has declared connect port whitelist, but link is not TCP/UDP") - link.UpdateVerdict(network.BLOCK) - return - } - - // packet *should* be outbound, but we could be deciding on an already active connection. - var remotePort uint16 - if connection.Direction { - remotePort = tcpUdpHeader.SrcPort - } else { - remotePort = tcpUdpHeader.DstPort - } - - matched := false - for _, port := range profile.ConnectPorts { - if remotePort == port { - matched = true - break - } - } - - if !matched { - log.Infof("firewall: blocking link %s, remote port %d not in profile connect port whitelist", link, remotePort) - link.AddReason("destination port not in whitelist") - link.UpdateVerdict(network.BLOCK) - return - } - - } - - // check listen ports - if connection.Domain == "I" && len(profile.ListenPorts) > 0 { - - tcpUdpHeader := pkt.GetTCPUDPHeader() - if tcpUdpHeader == nil { - log.Infof("firewall: dropping link %s, profile has declared listen port whitelist, but link is not TCP/UDP", link) - link.AddReason("profile has declared listen port whitelist, but link is not TCP/UDP") - link.UpdateVerdict(network.DROP) - return - } - - // packet *should* be inbound, but we could be deciding on an already active connection. - var localPort uint16 - if connection.Direction { - localPort = tcpUdpHeader.DstPort - } else { - localPort = tcpUdpHeader.SrcPort - } - - matched := false - for _, port := range profile.ListenPorts { - if localPort == port { - matched = true - break - } - } - - if !matched { - log.Infof("firewall: blocking link %s, local port %d not in profile listen port whitelist", link, localPort) - link.AddReason("listen port not in whitelist") - link.UpdateVerdict(network.BLOCK) - return - } - + switch profileSet.GetProfileMode() { + case profile.Whitelist: + log.Infof("firewall: denying link %s: port %d is not whitelisted", link, remotePort) + link.Deny("port is not whitelisted") + case profile.Prompt: + log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort) + link.Accept("port permitted (prompting is not yet implemented)") + case profile.Blacklist: + log.Infof("firewall: denying link %s: port %d is blacklisted", link, remotePort) + link.Deny("port is not blacklisted") } log.Infof("firewall: accepting link %s", link) - link.UpdateVerdict(network.ACCEPT) - + link.Accept("") } diff --git a/intel/namerecord.go b/intel/namerecord.go index 8e711a75..1424a527 100644 --- a/intel/namerecord.go +++ b/intel/namerecord.go @@ -27,6 +27,7 @@ type NameRecord struct { Ns []string Extra []string TTL int64 + Filtered bool } func makeNameRecordKey(domain string, question string) string { diff --git a/intel/resolve.go b/intel/resolve.go index 86f0a054..a389b4f0 100644 --- a/intel/resolve.go +++ b/intel/resolve.go @@ -15,6 +15,7 @@ import ( "github.com/Safing/portbase/database" "github.com/Safing/portbase/log" + "github.com/Safing/portmaster/network/netutils" "github.com/Safing/portmaster/status" ) @@ -76,26 +77,6 @@ func Resolve(fqdn string, qtype dns.Type, securityLevel uint8) *RRCache { // timed := time.Now() // defer log.Tracef("intel: took %s to get resolve %s%s", time.Now().Sub(timed).String(), fqdn, qtype.String()) - // handle request for localhost - if fqdn == "localhost." { - var rr dns.RR - var err error - switch uint16(qtype) { - case dns.TypeA: - rr, err = dns.NewRR("localhost. 17 IN A 127.0.0.1") - case dns.TypeAAAA: - rr, err = dns.NewRR("localhost. 17 IN AAAA ::1") - default: - return nil - } - if err != nil { - return nil - } - return &RRCache{ - Answer: []dns.RR{rr}, - } - } - // check cache rrCache, err := GetRRCache(fqdn, qtype) if err != nil { @@ -322,6 +303,14 @@ func tryResolver(resolver *Resolver, lastFailBoundary int64, fqdn string, qtype return nil, false } resolver.Initialized.SetToIf(false, true) + + // remove localhost entries, remove LAN entries if server is in global IP space. + if resolver.ServerIPScope == netutils.Global { + rrCache.FilterEntries(true, false, false) + } else { + rrCache.FilterEntries(true, true, false) + } + return rrCache, true } @@ -368,11 +357,11 @@ func query(resolver *Resolver, fqdn string, qtype dns.Type) (*RRCache, error) { } new := &RRCache{ - Domain: fqdn, + Domain: fqdn, Question: qtype, - Answer: reply.Answer, - Ns: reply.Ns, - Extra: reply.Extra, + Answer: reply.Answer, + Ns: reply.Ns, + Extra: reply.Extra, } // TODO: check if reply.Answer is valid diff --git a/intel/resolver.go b/intel/resolver.go index ae210c93..7fd8f112 100644 --- a/intel/resolver.go +++ b/intel/resolver.go @@ -26,6 +26,7 @@ type Resolver struct { ServerType string ServerAddress string ServerIP net.IP + ServerIPScope int8 ServerPort uint16 VerifyDomain string Source string @@ -151,6 +152,7 @@ configuredServersLoop: ServerType: parts[0], ServerAddress: parts[1], ServerIP: ip, + ServerIPScope: netutils.ClassifyAddress(ip), ServerPort: port, LastFail: &lastFail, Source: "config", @@ -205,6 +207,7 @@ assignedServersLoop: ServerType: "dns", ServerAddress: urlFormatAddress(nameserver.IP, 53), ServerIP: nameserver.IP, + ServerIPScope: netutils.ClassifyAddress(nameserver.IP), ServerPort: 53, LastFail: &lastFail, Source: "dhcp", @@ -213,7 +216,7 @@ assignedServersLoop: } new.clientManager = newDNSClientManager(new) - if netutils.IPIsLocal(nameserver.IP) && len(nameserver.Search) > 0 { + if netutils.IPIsLAN(nameserver.IP) && len(nameserver.Search) > 0 { // only allow searches for local resolvers var newSearch []string for _, value := range nameserver.Search { @@ -236,7 +239,7 @@ assignedServersLoop: // make list with local resolvers localResolvers = make([]*Resolver, 0) for _, resolver := range globalResolvers { - if resolver.ServerIP != nil && netutils.IPIsLocal(resolver.ServerIP) { + if resolver.ServerIP != nil && netutils.IPIsLAN(resolver.ServerIP) { localResolvers = append(localResolvers, resolver) } } diff --git a/intel/rrcache.go b/intel/rrcache.go index d48b708b..0dd9c707 100644 --- a/intel/rrcache.go +++ b/intel/rrcache.go @@ -3,9 +3,13 @@ package intel import ( + "fmt" "net" + "strings" "time" + "github.com/Safing/portbase/log" + "github.com/Safing/portmaster/network/netutils" "github.com/miekg/dns" ) @@ -22,6 +26,7 @@ type RRCache struct { updated int64 servedFromCache bool requestingNew bool + Filtered bool } // Clean sets all TTLs to 17 and sets cache expiry with specified minimum. @@ -77,6 +82,7 @@ func (m *RRCache) ToNameRecord() *NameRecord { Domain: m.Domain, Question: m.Question.String(), TTL: m.TTL, + Filtered: m.Filtered, } // stringify RR entries @@ -130,6 +136,7 @@ func GetRRCache(domain string, question dns.Type) (*RRCache, error) { } } + rrCache.Filtered = nameRecord.Filtered rrCache.servedFromCache = true return rrCache, nil } @@ -146,19 +153,104 @@ func (m *RRCache) RequestingNew() bool { // Flags formats ServedFromCache and RequestingNew to a condensed, flag-like format. func (m *RRCache) Flags() string { - switch { - case m.servedFromCache && m.requestingNew: - return " [CR]" - case m.servedFromCache: - return " [C]" - case m.requestingNew: - return " [R]" // should never enter this state, but let's leave it here, just in case - default: - return "" + var s string + if m.servedFromCache { + s += "C" } + if m.requestingNew { + s += "R" + } + if m.Filtered { + s += "F" + } + + if s != "" { + return fmt.Sprintf(" [%s]", s) + } + return "" } // IsNXDomain returnes whether the result is nxdomain. func (m *RRCache) IsNXDomain() bool { return len(m.Answer) == 0 } + +// Duplicate returns a duplicate of the cache. slices are not copied, but referenced. +func (m *RRCache) Duplicate() *RRCache { + return &RRCache{ + Domain: m.Domain, + Question: m.Question, + Answer: m.Answer, + Ns: m.Ns, + Extra: m.Extra, + TTL: m.TTL, + updated: m.updated, + servedFromCache: m.servedFromCache, + requestingNew: m.requestingNew, + Filtered: m.Filtered, + } +} + +// FilterEntries filters resource records according to the given permission scope. +func (m *RRCache) FilterEntries(internet, lan, host bool) { + var filtered bool + + m.Answer, filtered = filterEntries(m, m.Answer, internet, lan, host) + if filtered { + m.Filtered = true + } + m.Extra, filtered = filterEntries(m, m.Extra, internet, lan, host) + if filtered { + m.Filtered = true + } +} + +func filterEntries(m *RRCache, entries []dns.RR, internet, lan, host bool) (filteredEntries []dns.RR, filtered bool) { + filteredEntries = make([]dns.RR, 0, len(entries)) + var classification int8 + var deletedEntries []string + +entryLoop: + for _, rr := range entries { + + classification = -1 + switch v := rr.(type) { + case *dns.A: + classification = netutils.ClassifyAddress(v.A) + case *dns.AAAA: + classification = netutils.ClassifyAddress(v.AAAA) + } + + if classification >= 0 { + switch { + case !internet && classification == netutils.Global: + filtered = true + deletedEntries = append(deletedEntries, rr.String()) + continue entryLoop + case !lan && (classification == netutils.SiteLocal || classification == netutils.LinkLocal): + filtered = true + deletedEntries = append(deletedEntries, rr.String()) + continue entryLoop + case !host && classification == netutils.HostLocal: + filtered = true + deletedEntries = append(deletedEntries, rr.String()) + continue entryLoop + } + } + + filteredEntries = append(filteredEntries, rr) + } + + if len(deletedEntries) > 0 { + log.Infof("intel: filtered DNS replies for %s%s: %s (Settings: Int=%v LAN=%v Host=%v)", + m.Domain, + m.Question.String(), + strings.Join(deletedEntries, ", "), + internet, + lan, + host, + ) + } + + return +} diff --git a/nameserver/nameserver.go b/nameserver/nameserver.go index 5acd1000..9e24a271 100644 --- a/nameserver/nameserver.go +++ b/nameserver/nameserver.go @@ -18,8 +18,28 @@ import ( "github.com/Safing/portmaster/firewall" ) +var ( + localhostIPs []dns.RR +) + func init() { - modules.Register("nameserver", nil, start, nil, "intel") + modules.Register("nameserver", prep, start, nil, "intel") +} + +func prep() error { + localhostIPv4, err := dns.NewRR("localhost. 17 IN A 127.0.0.1") + if err != nil { + return err + } + + localhostIPv6, err := dns.NewRR("localhost. 17 IN AAAA ::1") + if err != nil { + return err + } + + localhostIPs = []dns.RR{localhostIPv4, localhostIPv6} + + return nil } func start() error { @@ -49,7 +69,6 @@ func nxDomain(w dns.ResponseWriter, query *dns.Msg) { func handleRequest(w dns.ResponseWriter, query *dns.Msg) { // TODO: if there are 3 request for the same domain/type in a row, delete all caches of that domain - // TODO: handle securityLevelOff // only process first question, that's how everyone does it. question := query.Question[0] @@ -84,6 +103,14 @@ func handleRequest(w dns.ResponseWriter, query *dns.Msg) { return } + // handle request for localhost + if fqdn == "localhost." { + m := new(dns.Msg) + m.SetReply(query) + m.Answer = localhostIPs + w.WriteMsg(m) + } + // get remote address // start := time.Now() rAddr, ok := w.RemoteAddr().(*net.UDPAddr) diff --git a/network/connection.go b/network/connection.go index 85fcb26a..112503be 100644 --- a/network/connection.go +++ b/network/connection.go @@ -38,51 +38,57 @@ 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 (conn *Connection) CantSay() { - if conn.Verdict != CANTSAY { - conn.Verdict = CANTSAY - conn.Save() - } - return +// Accept accepts the connection and adds the given reason. +func (conn *Link) Accept(reason string) { + conn.AddReason(reason) + conn.UpdateVerdict(ACCEPT) } -// Drop sets the connection verdict to drop. -func (conn *Connection) Drop() { - if conn.Verdict != DROP { - conn.Verdict = DROP - conn.Save() +// Deny blocks or drops the connection depending on the connection direction and adds the given reason. +func (conn *Link) Deny(reason string) { + if conn.Direction { + conn.Drop(reason) + } else { + conn.Block(reason) } - return } -// Block sets the connection verdict to block. -func (conn *Connection) Block() { - if conn.Verdict != BLOCK { - conn.Verdict = BLOCK - conn.Save() - } - return +// Block blocks the connection and adds the given reason. +func (conn *Link) Block(reason string) { + conn.AddReason(reason) + conn.UpdateVerdict(BLOCK) } -// Accept sets the connection verdict to accept. -func (conn *Connection) Accept() { - if conn.Verdict != ACCEPT { - conn.Verdict = ACCEPT +// Drop drops the connection and adds the given reason. +func (conn *Link) Drop(reason string) { + conn.AddReason(reason) + conn.UpdateVerdict(DROP) +} + +// UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts +func (conn *Connection) UpdateVerdict(newVerdict Verdict) { + conn.Lock() + defer conn.Unlock() + + if newVerdict > conn.Verdict { + conn.Verdict = newVerdict conn.Save() } - return } // AddReason adds a human readable string as to why a certain verdict was set in regard to this connection -func (conn *Connection) AddReason(newReason string) { +func (conn *Connection) AddReason(reason string) { + if reason == "" { + return + } + conn.Lock() defer conn.Unlock() if conn.Reason != "" { conn.Reason += " | " } - conn.Reason += newReason + conn.Reason += reason } // GetConnectionByFirstPacket returns the matching connection from the internal storage. @@ -92,13 +98,25 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) { if err != nil { return nil, err } + var domain string - // if INBOUND + // Incoming if direction { - connection, ok := GetConnection(proc.Pid, "I") + switch netutils.ClassifyIP(pkt.GetIPHeader().Src) { + case HostLocal: + domain = IncomingHost + case LinkLocal, SiteLocal, LocalMulticast: + domain = IncomingLAN + case Global, GlobalMulticast: + domain = IncomingInternet + case Invalid: + domain = IncomingInvalid + } + + connection, ok := GetConnection(proc.Pid, domain) if !ok { connection = &Connection{ - Domain: "I", + Domain: domain, Direction: Inbound, process: proc, Inspect: true, @@ -111,12 +129,26 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) { // get domain ipinfo, err := intel.GetIPInfo(pkt.FmtRemoteIP()) + + // PeerToPeer if err != nil { // if no domain could be found, it must be a direct connection - connection, ok := GetConnection(proc.Pid, "D") + + switch netutils.ClassifyIP(pkt.GetIPHeader().Dst) { + case HostLocal: + domain = PeerHost + case LinkLocal, SiteLocal, LocalMulticast: + domain = PeerLAN + case Global, GlobalMulticast: + domain = PeerInternet + case Invalid: + domain = PeerInvalid + } + + connection, ok := GetConnection(proc.Pid, domain) if !ok { connection = &Connection{ - Domain: "D", + Domain: domain, Direction: Outbound, process: proc, Inspect: true, @@ -127,6 +159,7 @@ func GetConnectionByFirstPacket(pkt packet.Packet) (*Connection, error) { return connection, nil } + // To Domain // FIXME: how to handle multiple possible domains? connection, ok := GetConnection(proc.Pid, ipinfo.Domains[0]) if !ok { diff --git a/network/environment/location.go b/network/environment/location.go index 41ff50a8..cccc3c09 100644 --- a/network/environment/location.go +++ b/network/environment/location.go @@ -100,7 +100,7 @@ next: if ip == nil { return nil, errors.New(fmt.Sprintf("failed to parse IP: %s", peer.String())) } - if !netutils.IPIsLocal(ip) { + if !netutils.IPIsLAN(ip) { return ip, nil } continue next diff --git a/network/link.go b/network/link.go index e7e69813..2b85ea34 100644 --- a/network/link.go +++ b/network/link.go @@ -80,8 +80,38 @@ func (link *Link) HandlePacket(pkt packet.Packet) { pkt.Drop() } +// Accept accepts the link and adds the given reason. +func (link *Link) Accept(reason string) { + link.AddReason(reason) + link.UpdateVerdict(ACCEPT) +} + +// Deny blocks or drops the link depending on the connection direction and adds the given reason. +func (link *Link) Deny(reason string) { + if link.connection.Direction { + link.Drop(reason) + } else { + link.Block(reason) + } +} + +// Block blocks the link and adds the given reason. +func (link *Link) Block(reason string) { + link.AddReason(reason) + link.UpdateVerdict(BLOCK) +} + +// Drop drops the link and adds the given reason. +func (link *Link) Drop(reason string) { + link.AddReason(reason) + link.UpdateVerdict(DROP) +} + // UpdateVerdict sets a new verdict for this link, making sure it does not interfere with previous verdicts func (link *Link) UpdateVerdict(newVerdict Verdict) { + link.Lock() + defer link.Unlock() + if newVerdict > link.Verdict { link.Verdict = newVerdict link.Save() @@ -89,14 +119,18 @@ func (link *Link) UpdateVerdict(newVerdict Verdict) { } // AddReason adds a human readable string as to why a certain verdict was set in regard to this link -func (link *Link) AddReason(newReason string) { +func (link *Link) AddReason(reason string) { + if reason == "" { + return + } + link.Lock() defer link.Unlock() if link.Reason != "" { link.Reason += " | " } - link.Reason += newReason + link.Reason += reason } // packetHandler sequentially handles queued packets diff --git a/network/netutils/cleandns.go b/network/netutils/cleandns.go index 67c9a1e3..66bcb24a 100644 --- a/network/netutils/cleandns.go +++ b/network/netutils/cleandns.go @@ -11,6 +11,7 @@ var ( cleanDomainRegex = regexp.MustCompile("^((xn--)?[a-z0-9-_]{0,61}[a-z0-9]{1,1}\\.)*(xn--)?([a-z0-9-]{1,61}|[a-z0-9-]{1,30}\\.[a-z]{2,}\\.)$") ) +// IsValidFqdn returns whether the given string is a valid fqdn. func IsValidFqdn(fqdn string) bool { return cleanDomainRegex.MatchString(fqdn) } diff --git a/network/netutils/ip.go b/network/netutils/ip.go index 96dc92a9..c8066ba1 100644 --- a/network/netutils/ip.go +++ b/network/netutils/ip.go @@ -4,95 +4,101 @@ package netutils import "net" -// IP types +// IP classifications const ( - hostLocal int8 = iota - linkLocal - siteLocal - global - localMulticast - globalMulticast - invalid + HostLocal int8 = iota + LinkLocal + SiteLocal + Global + LocalMulticast + GlobalMulticast + Invalid ) -func classifyAddress(ip net.IP) int8 { +// ClassifyAddress returns the classification for the given IP address. +func ClassifyAddress(ip net.IP) int8 { if ip4 := ip.To4(); ip4 != nil { // IPv4 switch { case ip4[0] == 127: // 127.0.0.0/8 - return hostLocal + return HostLocal case ip4[0] == 169 && ip4[1] == 254: // 169.254.0.0/16 - return linkLocal + return LinkLocal case ip4[0] == 10: // 10.0.0.0/8 - return siteLocal + return SiteLocal case ip4[0] == 172 && ip4[1]&0xf0 == 16: // 172.16.0.0/12 - return siteLocal + return SiteLocal case ip4[0] == 192 && ip4[1] == 168: // 192.168.0.0/16 - return siteLocal + return SiteLocal case ip4[0] == 224: // 224.0.0.0/8 - return localMulticast + return LocalMulticast case ip4[0] >= 225 && ip4[0] <= 239: // 225.0.0.0/8 - 239.0.0.0/8 - return globalMulticast + return GlobalMulticast case ip4[0] >= 240: // 240.0.0.0/8 - 255.0.0.0/8 - return invalid + return Invalid default: - return global + return Global } } else if len(ip) == net.IPv6len { // IPv6 switch { case ip.Equal(net.IPv6loopback): - return hostLocal + return HostLocal case ip[0]&0xfe == 0xfc: // fc00::/7 - return siteLocal + return SiteLocal case ip[0] == 0xfe && ip[1]&0xc0 == 0x80: // fe80::/10 - return linkLocal + return LinkLocal case ip[0] == 0xff && ip[1] <= 0x05: // ff00::/16 - ff05::/16 - return localMulticast + return LocalMulticast case ip[0] == 0xff: // other ff00::/8 - return globalMulticast + return GlobalMulticast default: - return global + return Global } } - return invalid + return Invalid } -// IPIsLocal returns true if the given IP is a site-local or link-local address -func IPIsLocal(ip net.IP) bool { - switch classifyAddress(ip) { - case siteLocal: +// IPIsLocalhost returns whether the IP refers to the host itself. +func IPIsLocalhost(ip net.IP) bool { + return ClassifyAddress(ip) == HostLocal +} + +// IPIsLAN returns true if the given IP is a site-local or link-local address. +func IPIsLAN(ip net.IP) bool { + switch ClassifyAddress(ip) { + case SiteLocal: return true - case linkLocal: + case LinkLocal: return true default: return false } } -// IPIsGlobal returns true if the given IP is a global address +// IPIsGlobal returns true if the given IP is a global address. func IPIsGlobal(ip net.IP) bool { - return classifyAddress(ip) == global + return ClassifyAddress(ip) == Global } -// IPIsLinkLocal returns true if the given IP is a link-local address +// IPIsLinkLocal returns true if the given IP is a link-local address. func IPIsLinkLocal(ip net.IP) bool { - return classifyAddress(ip) == linkLocal + return ClassifyAddress(ip) == LinkLocal } -// IPIsSiteLocal returns true if the given IP is a site-local address +// IPIsSiteLocal returns true if the given IP is a site-local address. func IPIsSiteLocal(ip net.IP) bool { - return classifyAddress(ip) == siteLocal + return ClassifyAddress(ip) == SiteLocal } diff --git a/network/netutils/ip_test.go b/network/netutils/ip_test.go index a479f7ab..bc9848b4 100644 --- a/network/netutils/ip_test.go +++ b/network/netutils/ip_test.go @@ -6,14 +6,14 @@ import ( ) func TestIPClassification(t *testing.T) { - testClassification(t, net.IPv4(71, 87, 113, 211), global) - testClassification(t, net.IPv4(127, 0, 0, 1), hostLocal) - testClassification(t, net.IPv4(127, 255, 255, 1), hostLocal) - testClassification(t, net.IPv4(192, 168, 172, 24), siteLocal) + testClassification(t, net.IPv4(71, 87, 113, 211), Global) + testClassification(t, net.IPv4(127, 0, 0, 1), HostLocal) + testClassification(t, net.IPv4(127, 255, 255, 1), HostLocal) + testClassification(t, net.IPv4(192, 168, 172, 24), SiteLocal) } func testClassification(t *testing.T, ip net.IP, expectedClassification int8) { - c := classifyAddress(ip) + c := ClassifyAddress(ip) if c != expectedClassification { t.Errorf("%s is %s, expected %s", ip, classificationString(c), classificationString(expectedClassification)) } @@ -21,19 +21,19 @@ func testClassification(t *testing.T, ip net.IP, expectedClassification int8) { func classificationString(c int8) string { switch c { - case hostLocal: + case HostLocal: return "hostLocal" - case linkLocal: + case LinkLocal: return "linkLocal" - case siteLocal: + case SiteLocal: return "siteLocal" - case global: + case Global: return "global" - case localMulticast: + case LocalMulticast: return "localMulticast" - case globalMulticast: + case GlobalMulticast: return "globalMulticast" - case invalid: + case Invalid: return "invalid" default: return "unknown" diff --git a/network/status.go b/network/status.go index bc77620c..0763da23 100644 --- a/network/status.go +++ b/network/status.go @@ -9,7 +9,6 @@ type Verdict uint8 const ( // UNDECIDED is the default status of new connections UNDECIDED Verdict = iota - CANTSAY ACCEPT BLOCK DROP @@ -20,3 +19,15 @@ const ( Inbound = true Outbound = false ) + +// Non-Domain Connections +const ( + IncomingHost = "IH" + IncomingLAN = "IL" + IncomingInternet = "II" + IncomingInvalid = "IX" + PeerHost = "PH" + PeerLAN = "PL" + PeerInternet = "PI" + PeerInvalid = "PX" +) diff --git a/profile/active.go b/profile/active.go index e03a7d52..fdf1f057 100644 --- a/profile/active.go +++ b/profile/active.go @@ -35,29 +35,17 @@ func updateActiveUserProfile(profile *Profile) { } } -func updateActiveGlobalProfile(profile *Profile) { - updateActiveProfile(1, profile) -} - func updateActiveStampProfile(profile *Profile) { - updateActiveProfile(2, profile) -} - -func updateActiveFallbackProfile(profile *Profile) { - updateActiveProfile(3, profile) -} - -func updateActiveProfile(setID int, profile *Profile) { activeProfileSetsLock.RLock() defer activeProfileSetsLock.RUnlock() for _, activeSet := range activeProfileSets { activeSet.Lock() - activeProfile := activeSet.profiles[setID] + activeProfile := activeSet.profiles[2] if activeProfile != nil { activeProfile.Lock() if activeProfile.ID == profile.ID { - activeSet.profiles[setID] = profile + activeSet.profiles[2] = profile } activeProfile.Unlock() } diff --git a/profile/fingerprint.go b/profile/fingerprint.go index 3908aac6..3d4b6dc9 100644 --- a/profile/fingerprint.go +++ b/profile/fingerprint.go @@ -1,5 +1,7 @@ package profile +import "time" + var ( fingerprintWeights = map[string]int{ "full_path": 2, @@ -10,41 +12,21 @@ var ( } ) +// Fingerprint links processes to profiles. type Fingerprint struct { - OS string - Type string - Value string - Comment string + OS string + Type string + Value string + Comment string + LastUsed int64 } +// MatchesOS returns whether the Fingerprint is applicable for the current OS. func (fp *Fingerprint) MatchesOS() bool { return fp.OS == osIdentifier } -// -// func (fp *Fingerprint) Equals(other *Fingerprint) bool { -// return fp.OS == other.OS && -// fp.Type == other.Type && -// fp.Value == other.Value -// } -// -// func (fp *Fingerprint) Check(type, value string) (weight int) { -// if fp.Match(fpType, value) { -// return GetFingerprintWeight(fpType) -// } -// return 0 -// } -// -// func (fp *Fingerprint) Match(fpType, value string) (matches bool) { -// switch fp.Type { -// case "partial_path": -// return -// default: -// return fp.OS == osIdentifier && -// fp.Type == fpType && -// fp.Value == value -// } -// +// GetFingerprintWeight returns the weight of the given fingerprint type. func GetFingerprintWeight(fpType string) (weight int) { weight, ok := fingerprintWeights[fpType] if ok { @@ -53,47 +35,14 @@ func GetFingerprintWeight(fpType string) (weight int) { return 0 } -// -// func (p *Profile) GetApplicableFingerprints() (fingerprints []*Fingerprint) { -// for _, fp := range p.Fingerprints { -// if fp.OS == osIdentifier { -// fingerprints = append(fingerprints, fp) -// } -// } -// return -// } -// +// AddFingerprint adds the given fingerprint to the profile. func (p *Profile) AddFingerprint(fp *Fingerprint) { if fp.OS == "" { fp.OS = osIdentifier } + if fp.LastUsed == 0 { + fp.LastUsed = time.Now().Unix() + } p.Fingerprints = append(p.Fingerprints, fp) } - -// -// func (p *Profile) GetApplicableFingerprintTypes() (types []string) { -// for _, fp := range p.Fingerprints { -// if fp.OS == osIdentifier && !utils.StringInSlice(types, fp.Type) { -// types = append(types, fp.Type) -// } -// } -// return -// } -// -// func (p *Profile) MatchFingerprints(fingerprints map[string]string) (score int) { -// for _, fp := range p.Fingerprints { -// if fp.OS == osIdentifier { -// -// } -// } -// return -// } -// -// func FindUserProfiles() { -// -// } -// -// func FindProfiles(path string) (*ProfileSet, error) { -// -// } diff --git a/profile/flags.go b/profile/flags.go index a691008b..1f238467 100644 --- a/profile/flags.go +++ b/profile/flags.go @@ -77,19 +77,6 @@ var ( } ) -// FlagsFromNames creates Flags from a comma seperated list of flagnames (e.g. "System,Strict,Secure") -// func FlagsFromNames(words []string) (*Flags, error) { -// var flags Flags -// for _, entry := range words { -// flag, ok := flagIDs[entry] -// if !ok { -// return nil, ErrFlagsParseFailed -// } -// flags = append(flags, flag) -// } -// return &flags, nil -// } - // Check checks if a flag is set at all and if it's active in the given security level. func (flags Flags) Check(flag, level uint8) (active bool, ok bool) { if flags == nil { diff --git a/profile/index/index.go b/profile/index/index.go index 52caac55..17d13817 100644 --- a/profile/index/index.go +++ b/profile/index/index.go @@ -15,12 +15,14 @@ type ProfileIndex struct { record.Base sync.Mutex + ID string + UserProfiles []string StampProfiles []string } -func makeIndexRecordKey(id string) string { - return fmt.Sprintf("index:profiles/%s", base64.RawURLEncoding.EncodeToString([]byte(id))) +func makeIndexRecordKey(fpType, id string) string { + return fmt.Sprintf("index:profiles/%s:%s", fpType, base64.RawURLEncoding.EncodeToString([]byte(id))) } // NewIndex returns a new ProfileIndex. @@ -32,8 +34,8 @@ func NewIndex(id string) *ProfileIndex { // AddUserProfile adds a User Profile to the index. func (pi *ProfileIndex) AddUserProfile(identifier string) (changed bool) { - if !utils.StringInSlice(pi.UserProfiles, id) { - pi.UserProfiles = append(pi.UserProfiles, id) + if !utils.StringInSlice(pi.UserProfiles, identifier) { + pi.UserProfiles = append(pi.UserProfiles, identifier) return true } return false @@ -41,8 +43,8 @@ func (pi *ProfileIndex) AddUserProfile(identifier string) (changed bool) { // AddStampProfile adds a Stamp Profile to the index. func (pi *ProfileIndex) AddStampProfile(identifier string) (changed bool) { - if !utils.StringInSlice(pi.StampProfiles, id) { - pi.StampProfiles = append(pi.StampProfiles, id) + if !utils.StringInSlice(pi.StampProfiles, identifier) { + pi.StampProfiles = append(pi.StampProfiles, identifier) return true } return false @@ -59,8 +61,8 @@ func (pi *ProfileIndex) RemoveStampProfile(id string) { } // Get gets a ProfileIndex from the database. -func Get(id string) (*ProfileIndex, error) { - key := makeIndexRecordKey(id) +func Get(fpType, id string) (*ProfileIndex, error) { + key := makeIndexRecordKey(fpType, id) r, err := indexDB.Get(key) if err != nil { diff --git a/profile/index/indexer.go b/profile/index/indexer.go index d86d0e13..0776d24f 100644 --- a/profile/index/indexer.go +++ b/profile/index/indexer.go @@ -1,8 +1,6 @@ package index import ( - "strings" - "github.com/Safing/portbase/database" "github.com/Safing/portbase/database/query" "github.com/Safing/portbase/database/record" @@ -25,7 +23,7 @@ var ( ) func init() { - modules.Register("profile:index", nil, start, stop, "database") + modules.Register("profile:index", nil, start, stop, "profile", "database") } func start() (err error) { @@ -49,13 +47,17 @@ func indexer() { case <-shutdownIndexer: return case r := <-indexSub.Feed: + if r == nil { + return + } + prof := ensureProfile(r) if prof != nil { - for _, id := range prof.Identifiers { - if strings.HasPrefix(id, profile.IdentifierPrefix) { + for _, fp := range prof.Fingerprints { + if fp.MatchesOS() && fp.Type == "full_path" { // get Profile and ensure identifier is set - pi, err := GetIndex(id) + pi, err := Get("full_path", fp.Value) if err != nil { if err == database.ErrNotFound { pi = NewIndex(id) diff --git a/profile/module.go b/profile/module.go index ddfa7407..f6652aa1 100644 --- a/profile/module.go +++ b/profile/module.go @@ -1,3 +1,24 @@ package profile -. +import "github.com/Safing/portbase/modules" + +var ( + shutdownSignal = make(chan struct{}) +) + +func init() { + modules.Register("profile", nil, start, stop, "database") +} + +func start() error { + err := initSpecialProfiles() + if err != nil { + return err + } + return initUpdateListener() +} + +func stop() error { + close(shutdownSignal) + return nil +} diff --git a/profile/ports_test.go b/profile/ports_test.go new file mode 100644 index 00000000..b068e40e --- /dev/null +++ b/profile/ports_test.go @@ -0,0 +1,46 @@ +package profile + +import ( + "testing" + "time" +) + +func TestPorts(t *testing.T) { + var ports Ports + ports = map[int16][]*Port{ + 6: []*Port{ + &Port{ // SSH + Permit: true, + Created: time.Now().Unix(), + Start: 22, + End: 22, + }, + }, + -17: []*Port{ + &Port{ // HTTP + Permit: false, + Created: time.Now().Unix(), + Start: 80, + End: 81, + }, + }, + 93: []*Port{ + &Port{ // HTTP + Permit: true, + Created: time.Now().Unix(), + Start: 93, + End: 93, + }, + }, + } + if ports.String() != "TCP:[permit:22],