diff --git a/cmds/portmaster-core/main.go b/cmds/portmaster-core/main.go index 70a1a63f0..38939652e 100644 --- a/cmds/portmaster-core/main.go +++ b/cmds/portmaster-core/main.go @@ -11,6 +11,8 @@ import ( _ "github.com/safing/portbase/modules/subsystems" _ "github.com/safing/portmaster/core" _ "github.com/safing/portmaster/firewall" + _ "github.com/safing/portmaster/firewall/inspection/encryption" + _ "github.com/safing/portmaster/firewall/inspection/portscan" _ "github.com/safing/portmaster/nameserver" _ "github.com/safing/portmaster/ui" _ "github.com/safing/spn/captain" diff --git a/firewall/inspection/encryption/detect.go b/firewall/inspection/encryption/detect.go new file mode 100644 index 000000000..e2ce59ad8 --- /dev/null +++ b/firewall/inspection/encryption/detect.go @@ -0,0 +1,50 @@ +package encryption + +import ( + "github.com/safing/portmaster/firewall/inspection" + "github.com/safing/portmaster/network" + "github.com/safing/portmaster/network/packet" +) + +// Detector detects if a connection is encrypted. +type Detector struct{} + +// Name implements the inspection interface. +func (d *Detector) Name() string { + return "Encryption Detection" +} + +// Inspect implements the inspection interface. +func (d *Detector) Inspect(conn *network.Connection, pkt packet.Packet) (pktVerdict network.Verdict, proceed bool, err error) { + if !conn.Inbound { + switch conn.Entity.Port { + case 443, 465, 993, 995: + conn.Encrypted = true + conn.SaveWhenFinished() + } + } + + return network.VerdictUndecided, false, nil +} + +// Destroy implements the destroy interface. +func (d *Detector) Destroy() error { + return nil +} + +// DetectorFactory is a primitive detection method that runs within the factory only. +func DetectorFactory(conn *network.Connection, pkt packet.Packet) (network.Inspector, error) { + return &Detector{}, nil +} + +// Register registers the encryption detection inspector with the inspection framework. +func init() { + err := inspection.RegisterInspector(&inspection.Registration{ + Name: "Encryption Detection", + Order: 0, + Factory: DetectorFactory, + }) + if err != nil { + panic(err) + } +} diff --git a/firewall/inspection/inspection.go b/firewall/inspection/inspection.go index 7dc59494e..c761260f9 100644 --- a/firewall/inspection/inspection.go +++ b/firewall/inspection/inspection.go @@ -1,102 +1,115 @@ package inspection import ( + "errors" + "sort" "sync" + "github.com/safing/portbase/log" "github.com/safing/portmaster/network" "github.com/safing/portmaster/network/packet" ) -//nolint:golint,stylecheck // FIXME -const ( - DO_NOTHING uint8 = iota - BLOCK_PACKET - DROP_PACKET - BLOCK_CONN - DROP_CONN - STOP_INSPECTING -) +// Registration holds information about and the factory of a registered inspector. +type Registration struct { + // Name of the Inspector + Name string + + // Order defines the priority in which the inspector should run. Decrease for higher priority, increase for lower priority. Leave at 0 for no preference. + Order int + + // Factory creates a new inspector. It may also return a nil inspector, which means that no inspection is desired. + // Any processing on the packet should only occur on the first call of Inspect. After creating a new Inspector, Inspect is is called with the same connection and packet for actual processing. + Factory func(conn *network.Connection, pkt packet.Packet) (network.Inspector, error) +} + +// Registry is a sortable []*Registration wrapper. +type Registry []*Registration -type inspectorFn func(*network.Connection, packet.Packet) uint8 +func (r Registry) Len() int { return len(r) } +func (r Registry) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r Registry) Less(i, j int) bool { return r[i].Order < r[j].Order } var ( - inspectors []inspectorFn - inspectorNames []string - inspectVerdicts []network.Verdict - inspectorsLock sync.Mutex + inspectorRegistry []*Registration + inspectorRegistryLock sync.Mutex ) // RegisterInspector registers a traffic inspector. -func RegisterInspector(name string, inspector inspectorFn, inspectVerdict network.Verdict) (index int) { - inspectorsLock.Lock() - defer inspectorsLock.Unlock() - index = len(inspectors) - inspectors = append(inspectors, inspector) - inspectorNames = append(inspectorNames, name) - inspectVerdicts = append(inspectVerdicts, inspectVerdict) - return -} +func RegisterInspector(new *Registration) error { + inspectorRegistryLock.Lock() + defer inspectorRegistryLock.Unlock() -// RunInspectors runs all the applicable inspectors on the given packet. -func RunInspectors(conn *network.Connection, pkt packet.Packet) (network.Verdict, bool) { - // inspectorsLock.Lock() - // defer inspectorsLock.Unlock() - - activeInspectors := conn.GetActiveInspectors() - if activeInspectors == nil { - activeInspectors = make([]bool, len(inspectors)) - conn.SetActiveInspectors(activeInspectors) + if new.Factory == nil { + return errors.New("missing inspector factory") } - inspectorData := conn.GetInspectorData() - if inspectorData == nil { - inspectorData = make(map[uint8]interface{}) - conn.SetInspectorData(inspectorData) + // check if name exists + for _, r := range inspectorRegistry { + if new.Name == r.Name { + return errors.New("already registered") + } } - continueInspection := false - verdict := network.VerdictUndecided + // append to list + inspectorRegistry = append(inspectorRegistry, new) - for key, skip := range activeInspectors { + // sort + sort.Stable(Registry(inspectorRegistry)) - if skip { - continue + return nil +} + +// InitializeInspectors initializes all applicable inspectors for the connection. +func InitializeInspectors(conn *network.Connection, pkt packet.Packet) { + inspectorRegistryLock.Lock() + defer inspectorRegistryLock.Unlock() + + connInspectors := make([]network.Inspector, 0, len(inspectorRegistry)) + for _, r := range inspectorRegistry { + inspector, err := r.Factory(conn, pkt) + switch { + case err != nil: + log.Tracer(pkt.Ctx()).Warningf("failed to initialize inspector %s: %v", r.Name, err) + case inspector != nil: + connInspectors = append(connInspectors, inspector) } + } + + conn.SetInspectors(connInspectors) +} - // check if the current verdict is already past the inspection criteria. - if conn.Verdict > inspectVerdicts[key] { - activeInspectors[key] = true +// RunInspectors runs all the applicable inspectors on the given packet of the connection. It returns the first error received by an inspector. +func RunInspectors(conn *network.Connection, pkt packet.Packet) (pktVerdict network.Verdict, continueInspection bool) { + connInspectors := conn.GetInspectors() + for i, inspector := range connInspectors { + // check if slot is active + if inspector == nil { continue } - action := inspectors[key](conn, pkt) // Actually run inspector - switch action { - case DO_NOTHING: - if verdict < network.VerdictAccept { - verdict = network.VerdictAccept - } - continueInspection = true - case BLOCK_PACKET: - if verdict < network.VerdictBlock { - verdict = network.VerdictBlock - } - continueInspection = true - case DROP_PACKET: - verdict = network.VerdictDrop + // run inspector + inspectorPktVerdict, proceed, err := inspector.Inspect(conn, pkt) + if err != nil { + log.Tracer(pkt.Ctx()).Warningf("inspector %s failed: %s", inspector.Name(), err) + } + // merge + if inspectorPktVerdict > pktVerdict { + pktVerdict = inspectorPktVerdict + } + if proceed { continueInspection = true - case BLOCK_CONN: - conn.SetVerdict(network.VerdictBlock, "", nil) - verdict = conn.Verdict - activeInspectors[key] = true - case DROP_CONN: - conn.SetVerdict(network.VerdictDrop, "", nil) - verdict = conn.Verdict - activeInspectors[key] = true - case STOP_INSPECTING: - activeInspectors[key] = true } + // destroy if finished or failed + if !proceed || err != nil { + err = inspector.Destroy() + if err != nil { + log.Tracer(pkt.Ctx()).Debugf("inspector %s failed to destroy: %s", inspector.Name(), err) + } + connInspectors[i] = nil + } } - return verdict, continueInspection + return pktVerdict, continueInspection } diff --git a/firewall/inspection/portscan/detect.go b/firewall/inspection/portscan/detect.go new file mode 100644 index 000000000..01b292f5f --- /dev/null +++ b/firewall/inspection/portscan/detect.go @@ -0,0 +1,323 @@ +package portscan + +import ( + "context" + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portmaster/firewall/inspection" + "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/network" + "github.com/safing/portmaster/network/packet" + "github.com/safing/portmaster/process" + "github.com/safing/portmaster/status" +) + +type tcpUDPport struct { + protocol packet.IPProtocol + port uint16 +} + +type ipData struct { + score int // score needs to be big enough to keep maxScore + addScore... to prevent overflow + // greylistingWorked bool + previousOffender bool + blocked bool + ignore bool + lastSeen time.Time + lastUpdated time.Time + blockedPorts []tcpUDPport +} + +const ( + cleanUpInterval = 5 * time.Minute + cleanUpMaxDelay = 5 * time.Minute + + decreaseInterval = 11 * time.Second + unblockIdleTime = 1 * time.Hour + undoSuspicionIdleTime = 24 * time.Hour + unignoreTime = 24 * time.Hour + + registeredPortsStart = 1024 + dynamicPortsStart = 32768 + + addScoreWellKnownPort = 40 + addScoreRegisteredPort = 20 + addScoreDynamicPort = 10 + + scoreBlock = 160 + maxScore = 320 + + threatIDPrefix = "portscan:" +) + +var ( + ips map[string]*ipData + + module *modules.Module + detectorMutex sync.Mutex +) + +// Detector detects if a connection is part of a portscan which already sent some packets. +type Detector struct{} + +// Name implements the inspection interface. +func (d *Detector) Name() string { + return "Portscan Detection" +} + +// Inspect implements the inspection interface. +func (d *Detector) Inspect(conn *network.Connection, pkt packet.Packet) (network.Verdict, bool, error) { + detectorMutex.Lock() + defer detectorMutex.Unlock() + + ctx := pkt.Ctx() + + log.Tracer(ctx).Debugf("portscan-detection: new connection") + + rIP, ok := conn.Entity.GetIP() // remote IP + if !ok { // No IP => return undecided + return network.VerdictUndecided, false, nil + } + + ipString := conn.LocalIP.String() + "-" + rIP.String() //localip-remoteip + entry, inMap := ips[ipString] + + log.Tracer(ctx).Debugf("portscan-detection: Conn: %s, remotePort: %d, IP: %s, Protocol: %s, LocalIP: %s, LocalPort: %d, inMap: %t, entry: %s", conn, conn.Entity.Port, conn.Entity.IP, conn.IPProtocol, conn.LocalIP, conn.LocalPort, inMap, entry) + + if inMap { + inMap = entry.updateIPstate(ipString) // needs to be run before updating lastSeen (lastUpdated is updated within) + } + + if inMap { + entry.lastSeen = time.Now() + + if entry.ignore { + return network.VerdictUndecided, false, nil + } + } + + proc := conn.Process() + myip, _ := netenv.IsMyIP(conn.LocalIP) + + // malicious Packet? This if checks all conditions for a malicious packet + switch { + case proc != nil && proc.Pid != process.UnidentifiedProcessID: + //We don't handle connections to running apps + case !conn.Inbound: + //We don't handle outbound connections + case !(conn.IPProtocol == packet.TCP || conn.IPProtocol == packet.UDP): + //We only handle TCP and UDP + case !myip: + //we only handle connections to our own IP + case isNetBIOSoverTCPIP(conn): + //we currently ignore NetBIOS + case (conn.IPProtocol == packet.UDP && (conn.LocalPort == 67 || conn.LocalPort == 68)): + //we ignore DHCP + default: + //We count this packet as a malicious packet + handleMaliciousPacket(ctx, inMap, conn, entry, ipString) + } + + if inMap && entry.blocked { + log.Tracer(ctx).Debugf("portscan-detection: blocking") + conn.SetVerdict(network.VerdictDrop, "Portscan", nil) + } else { + log.Tracer(ctx).Debugf("portscan-detection: let through") + } + + return network.VerdictUndecided, false, nil // If dropped, the whole connection is already dropped by conn.SetVerdict above +} + +func handleMaliciousPacket(ctx context.Context, inMap bool, conn *network.Connection, entry *ipData, ipString string) { + // define Portscore + var addScore int + switch { + case conn.LocalPort < registeredPortsStart: + addScore = addScoreWellKnownPort + case conn.LocalPort < dynamicPortsStart: + addScore = addScoreRegisteredPort + default: + addScore = addScoreDynamicPort + } + + port := tcpUDPport{protocol: conn.IPProtocol, port: conn.LocalPort} + + if !inMap { + // new IP => add to List + ips[ipString] = &ipData{ + score: addScore, + blockedPorts: []tcpUDPport{port}, + lastSeen: time.Now(), + lastUpdated: time.Now(), + } + log.Tracer(ctx).Debugf("portscan-detection: New Entry: %s", ips[ipString]) + return + } + + // the Port in list of tried ports - otherwise it would have already returned + triedPort := false + for _, e := range entry.blockedPorts { + if e == port { + triedPort = true + break + } + } + + if !triedPort { + entry.blockedPorts = append(entry.blockedPorts, port) + entry.score = intMin(entry.score+addScore, maxScore) + + if entry.previousOffender || entry.score >= scoreBlock { + entry.blocked = true + entry.previousOffender = true + + // FIXME: actually I just want to know if THIS threat exists - I don't need prefixing. Maybe we can do it simpler ... (less CPU-intensive) + if t, _ := status.GetThreats(threatIDPrefix + ipString); len(t) == 0 { + log.Tracer(ctx).Infof("portscan-detection: new Threat %s", extractRemoteFromIPString(ipString)) + status.AddOrUpdateThreat(&status.Threat{ + ID: threatIDPrefix + ipString, + Name: "Detected portscan from " + extractRemoteFromIPString(ipString), + Description: "The device with the IP address " + extractRemoteFromIPString(ipString) + " is scanning network ports on your device.", + MitigationLevel: status.SecurityLevelHigh, + Started: time.Now().Unix(), + }) + } + } + } + + log.Tracer(ctx).Debugf("portscan-detection: changed Entry: %s", entry) +} + +// updateIPstate updates this 4 Values of the Struct +// ipString needs to correspond to the key of the entry in the map ips +// needs to be run before updating lastSeen (lastUpdated is updated within) +// WARNING: This function maybe deletes the entry ipString from the Map ips. (look at the returncode) +// return: still in map? (bool) +func (ip *ipData) updateIPstate(ipString string) bool { + ip.score -= intMin(int(time.Since(ip.lastUpdated)/decreaseInterval), ip.score) + + if ip.ignore { + if time.Since(ip.lastSeen) > unignoreTime { + ip.ignore = false + } + } + + if ip.previousOffender && time.Since(ip.lastSeen) > undoSuspicionIdleTime { + ip.previousOffender = false + } + + if ip.blocked && time.Since(ip.lastSeen) > unblockIdleTime { + ip.blocked = false + ip.blockedPorts = []tcpUDPport{} + + status.DeleteThreat(threatIDPrefix + ipString) + } + + ip.lastUpdated = time.Now() + + if !ip.blocked && !ip.ignore && !ip.previousOffender && ip.score == 0 { + delete(ips, ipString) + return false + } + + return true +} + +// Destroy implements the destroy interface. +func (d *Detector) Destroy() error { + return nil +} + +// DetectorFactory creates&returns a detector for a connection +func DetectorFactory(conn *network.Connection, pkt packet.Packet) (network.Inspector, error) { + return &Detector{}, nil +} + +// Register registers the encryption detection inspector with the inspection framework. +func init() { + module = modules.Register("portscan-detection", nil, start, nil, "base", "netenv") + module.Enable() // FIXME +} + +func updateWholeList() { + log.Debugf("portscan-detection: update list&cleanup") + + detectorMutex.Lock() + defer detectorMutex.Unlock() + + for ip, entry := range ips { + + if entry.updateIPstate(ip) { + log.Debugf("portscan-detection: %s: %s", ip, entry) + } else { + log.Debugf("portscan-detection: Removed %s from the list", ip) + } + } + log.Debugf("portscan-detection: finished update list&cleanup") + +} + +func start() error { + ips = make(map[string]*ipData) + + // cleanup old Threats + threats, _ := status.GetThreats(threatIDPrefix) + for _, t := range threats { + status.DeleteThreat(t.ID) + } + + log.Debugf("portscan-detection: starting") + err := inspection.RegisterInspector(&inspection.Registration{ + Name: "Portscan Detection", + Order: 0, + Factory: DetectorFactory, + }) + + if err != nil { + return err + } + + module.NewTask("portscan score update", func(ctx context.Context, task *modules.Task) error { + updateWholeList() + return nil + }).Repeat(cleanUpInterval).MaxDelay(cleanUpMaxDelay) + + return nil +} + +func isNetBIOSoverTCPIP(conn *network.Connection) bool { + return conn.LocalPort == 137 || // maybe we could limit this to UDP ... RFC1002 defines NAME_SERVICE_TCP_PORT but dosn't use it (in contrast to the other ports that are also only defined TCP or UDP) + (conn.IPProtocol == packet.UDP && conn.LocalPort == 138) || + (conn.IPProtocol == packet.TCP && conn.LocalPort == 139) + +} + +func intMin(a, b int) int { + if a < b { + return a + } + return b +} + +func (ip *ipData) String() string { + var blockedPorts strings.Builder + for k, v := range ip.blockedPorts { + if k > 0 { + blockedPorts.WriteString(", ") + } + + blockedPorts.WriteString(v.protocol.String() + " " + strconv.Itoa(int(v.port))) + } + + return fmt.Sprintf("Score: %d, previousOffender: %t, blocked: %t, ignored: %t, lastSeen: %s, lastUpdated: %s, blockedPorts: [%s]", ip.score, ip.previousOffender, ip.blocked, ip.ignore, ip.lastSeen, ip.lastUpdated, blockedPorts.String()) +} + +func extractRemoteFromIPString(ipString string) string { + return strings.SplitAfterN(ipString, "-", 2)[1] +} diff --git a/firewall/inspection/portscan/doc.go b/firewall/inspection/portscan/doc.go new file mode 100644 index 000000000..a9ddad653 --- /dev/null +++ b/firewall/inspection/portscan/doc.go @@ -0,0 +1,67 @@ +package portscan + +/* +* delay start by 1 Minutes (in order to let answer-packets from old sockets arrive (at reboot)) +* if Portscan detected: secure mode; IP-Block +* Whitelist outgoing connections +* Whitelist DHCP, ICMP, IGM, NetBios, foreign destination IPs (including especially Broadcast&Multicast) +* Score >= 160: Portscan; set previous offender-flag which is persistent until 24 hours of inactivity +* ability to set ignore-flag (persistent until 24 hours of inactivity) +* previous offender is blocked on 1st probed closed port + +flowchart: +---------- + +function inspect() { + if can't get IP { + return undecided; + } + + if IP listed { + call updateIPstate(); + update last seen; + + if IP ignored { + return undecided; + } + } + + if no process attached + && inbound && tcp/udp + && going to own singlecast-address + && not NetBIOS over TCP/IP + && not DHCP { + call handleMaliciousPacket(); + } + + return blocked if blocked, otherwise undecided; +} + +function updateIPstate() { + recalculate score; + reset ignore-flag if expired; + reset block-flag if expired and delete own threat; + update lastUpdated; + if nothing important in entry{ + delete entry; + } +} + +function handleMaliciousPacket() { + set score depending on type of port; + + if IP not listed listed { + add to List; + return; + } + + if probed port is not in th List of already ports by that IP { + add to List of Ports; + update score; + update wether IP is is blocked; + + if blocked and no threat-warning { + create threat-warning; + } + } +} */ diff --git a/firewall/interception.go b/firewall/interception.go index 15df25d56..4112a7521 100644 --- a/firewall/interception.go +++ b/firewall/interception.go @@ -223,7 +223,6 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { log.Tracer(pkt.Ctx()).Trace("filter: starting decision process") DecideOnConnection(pkt.Ctx(), conn, pkt) - conn.Inspecting = false // TODO: enable inspecting again // tunneling // TODO: add implementation for forced tunneling @@ -243,8 +242,11 @@ func initialHandler(conn *network.Connection, pkt packet.Packet) { switch { case conn.Inspecting: - log.Tracer(pkt.Ctx()).Trace("filter: start inspecting") + log.Tracer(pkt.Ctx()).Trace("filter: starting inspection") + // setup inspectors and inspection handler + inspection.InitializeInspectors(conn, pkt) conn.SetFirewallHandler(inspectThenVerdict) + // execute inspection handler inspectThenVerdict(conn, pkt) default: conn.StopFirewallHandler() @@ -265,6 +267,8 @@ func inspectThenVerdict(conn *network.Connection, pkt packet.Packet) { } // we are done with inspecting + conn.Inspecting = false + conn.SaveWhenFinished() conn.StopFirewallHandler() issueVerdict(conn, pkt, 0, true) } diff --git a/network/connection.go b/network/connection.go index 4811f9115..94a3a8404 100644 --- a/network/connection.go +++ b/network/connection.go @@ -57,8 +57,7 @@ type Connection struct { //nolint:maligned // TODO: fix alignment pktQueue chan packet.Packet firewallHandler FirewallHandler - activeInspectors []bool - inspectorData map[uint8]interface{} + inspectors []Inspector saveWhenFinished bool profileRevisionCounter uint64 @@ -188,6 +187,7 @@ func NewConnectionFromFirstPacket(pkt packet.Packet) *Connection { Entity: entity, // meta Started: time.Now().Unix(), + Inspecting: true, profileRevisionCounter: proc.Profile().RevisionCnt(), } } @@ -413,24 +413,14 @@ func (conn *Connection) packetHandler() { } } -// GetActiveInspectors returns the list of active inspectors. -func (conn *Connection) GetActiveInspectors() []bool { - return conn.activeInspectors +// GetInspectors returns the list of inspectors. +func (conn *Connection) GetInspectors() []Inspector { + return conn.inspectors } -// SetActiveInspectors sets the list of active inspectors. -func (conn *Connection) SetActiveInspectors(new []bool) { - conn.activeInspectors = new -} - -// GetInspectorData returns the list of inspector data. -func (conn *Connection) GetInspectorData() map[uint8]interface{} { - return conn.inspectorData -} - -// SetInspectorData set the list of inspector data. -func (conn *Connection) SetInspectorData(new map[uint8]interface{}) { - conn.inspectorData = new +// SetInspectors sets the list of inspectors. +func (conn *Connection) SetInspectors(new []Inspector) { + conn.inspectors = new } // String returns a string representation of conn. diff --git a/network/inspector.go b/network/inspector.go new file mode 100644 index 000000000..4c07e4d5d --- /dev/null +++ b/network/inspector.go @@ -0,0 +1,18 @@ +package network + +import ( + "github.com/safing/portmaster/network/packet" +) + +// Inspector is a connection inspection interface for detailed analysis of network connections. +type Inspector interface { + // Name returns the name of the inspector. + Name() string + + // Inspect is called for every packet. It returns whether it wants to proceed with processing and possibly an error. + Inspect(conn *Connection, pkt packet.Packet) (pktVerdict Verdict, proceed bool, err error) + + // Destroy cancels the inspector and frees all resources. + // It is called as soon as Inspect returns proceed=false, an error occures, or if the inspection has ended early. + Destroy() error +} diff --git a/status/threat.go b/status/threat.go index 632ce8354..b5d53e283 100644 --- a/status/threat.go +++ b/status/threat.go @@ -14,7 +14,19 @@ type Threat struct { MitigationLevel uint8 // Recommended Security Level to switch to for mitigation Started int64 Ended int64 - // TODO: add locking + // TODO: improve locking +} + +//Lock locking +func (t *Threat) Lock() { + // This is a preliminiary workaround for locking Threats until the system is revamped. + status.Lock() +} + +//Unlock unlocking +func (t *Threat) Unlock() { + // This is a preliminiary workaround for locking Threats until the system is revamped. + status.Unlock() } // AddOrUpdateThreat adds or updates a new threat in the system status.