mirror of
https://github.com/safing/portmaster
synced 2025-09-01 10:09:11 +00:00
Move LMS scoring under new Domain Heuristics
This commit is contained in:
parent
85e4beafa1
commit
3b896ee892
5 changed files with 87 additions and 16 deletions
|
@ -4,16 +4,13 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// LmsScoreOfDomain calculates the mean longest meaningful substring of a domain. It follows some special rules to increase accuracy. It returns a value between 0 and 100, representing the length-based percentage of the meaningful substring.
|
||||
// LmsScoreOfDomain calculates the mean longest meaningful substring of a domain.
|
||||
// It follows some special rules to increase accuracy. It returns a value between
|
||||
// 0 and 100, representing the length-based percentage of the meaningful substring.
|
||||
func LmsScoreOfDomain(domain string) float64 {
|
||||
var totalScore float64
|
||||
domain = strings.ToLower(domain)
|
||||
subjects := strings.Split(domain, ".")
|
||||
// ignore the last two parts
|
||||
if len(subjects) <= 3 {
|
||||
return 100
|
||||
}
|
||||
subjects = subjects[:len(subjects)-3]
|
||||
var totalLength int
|
||||
for _, subject := range subjects {
|
||||
totalLength += len(subject)
|
||||
|
@ -27,7 +24,9 @@ func LmsScoreOfDomain(domain string) float64 {
|
|||
return totalScore
|
||||
}
|
||||
|
||||
// LmsScore calculates the longest meaningful substring of a domain. It returns a value between 0 and 100, representing the length-based percentage of the meaningful substring.
|
||||
// LmsScore calculates the longest meaningful substring of a domain. It returns a
|
||||
// value between 0 and 100, representing the length-based percentage of the
|
||||
// meaningful substring.
|
||||
func LmsScore(subject string) float64 {
|
||||
lmsStart := -1
|
||||
lmsStop := -1
|
||||
|
|
|
@ -5,8 +5,8 @@ import "testing"
|
|||
func TestLmsScoreOfDomain(t *testing.T) {
|
||||
testDomain(t, "g.symcd.com.", 100, 100)
|
||||
testDomain(t, "www.google.com.", 100, 100)
|
||||
testDomain(t, "55ttt5.12abc3.test.com.", 50, 50)
|
||||
testDomain(t, "mbtq6opnuodp34gcrma65fxacgxv5ukr7lq6xuhr4mhoibe7.yvqptrozfbnqyemchpovw3q5xwjibuxfsgb72mix3znhpfhc.i2n7jh2gadqaadck3zs3vg3hbv5pkmwzeay4gc75etyettbb.isi5mhmowtfriu33uxzmgvjur5g2p3tloynwohfrggee6fkn.meop7kqyd5gwxxa3.er.spotify.com.", 0, 30)
|
||||
testDomain(t, "55ttt5.12abc3.test.com.", 68, 69)
|
||||
testDomain(t, "mbtq6opnuodp34gcrma65fxacgxv5ukr7lq6xuhr4mhoibe7.yvqptrozfbnqyemchpovw3q5xwjibuxfsgb72mix3znhpfhc.i2n7jh2gadqaadck3zs3vg3hbv5pkmwzeay4gc75etyettbb.isi5mhmowtfriu33uxzmgvjur5g2p3tloynwohfrggee6fkn.meop7kqyd5gwxxa3.er.spotify.com.", 0, 31)
|
||||
}
|
||||
|
||||
func testDomain(t *testing.T, domain string, min, max float64) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
|
||||
"github.com/safing/portmaster/detection/dga"
|
||||
"github.com/safing/portmaster/netenv"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/network"
|
||||
|
@ -59,7 +60,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
|
|||
checkBypassPrevention,
|
||||
checkFilterLists,
|
||||
checkInbound,
|
||||
checkLMSScore,
|
||||
checkDomainHeuristics,
|
||||
checkDefaultPermit,
|
||||
checkAutoPermitRelated,
|
||||
checkDefaultAction,
|
||||
|
@ -283,19 +284,63 @@ func checkFilterLists(ctx context.Context, conn *network.Connection, pkt packet.
|
|||
return false
|
||||
}
|
||||
|
||||
func checkLMSScore(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
|
||||
func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
|
||||
p := conn.Process().Profile()
|
||||
|
||||
if !p.DomainHeuristics() {
|
||||
return false
|
||||
}
|
||||
|
||||
if conn.Entity.Domain == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// check for possible DNS tunneling / data transmission
|
||||
lms := dga.LmsScoreOfDomain(conn.Entity.Domain)
|
||||
if lms < 10 {
|
||||
log.Tracer(ctx).Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", conn.Process(), conn.Entity.Domain, lms)
|
||||
conn.BlockWithContext("Possible data tunnel", conn.ReasonContext)
|
||||
trimmedDomain := strings.TrimRight(conn.Entity.Domain, ".")
|
||||
etld1, err := publicsuffix.EffectiveTLDPlusOne(trimmedDomain)
|
||||
if err != nil {
|
||||
// we don't apply any checks here and let the request through
|
||||
// because a malformed domain-name will likely be dropped by
|
||||
// checks better suited for that.
|
||||
log.Tracer(ctx).Warningf("nameserver: failed to get eTLD+1: %s", err)
|
||||
return false
|
||||
}
|
||||
|
||||
domainToCheck := strings.Split(etld1, ".")[0]
|
||||
score := dga.LmsScore(domainToCheck)
|
||||
if score < 5 {
|
||||
log.Tracer(ctx).Warningf(
|
||||
"nameserver: possible data tunnel by %s in eTLD+1 %s: %s has an lms score of %.2f, returning nxdomain",
|
||||
conn.Process(),
|
||||
etld1,
|
||||
domainToCheck,
|
||||
score,
|
||||
)
|
||||
conn.Block("Possible data tunnel")
|
||||
return true
|
||||
}
|
||||
log.Tracer(ctx).Infof("LMS score of eTLD+1 %s is %.2f", etld1, score)
|
||||
|
||||
// 100 is a somewhat arbitrary threshold to ensure we don't mess
|
||||
// around with CDN domain names to early. They use short second-level
|
||||
// domains that would trigger LMS checks but are to small to actually
|
||||
// exfiltrate data.
|
||||
if len(conn.Entity.Domain) > len(etld1)+100 {
|
||||
domainToCheck = trimmedDomain[0:len(etld1)]
|
||||
score := dga.LmsScoreOfDomain(domainToCheck)
|
||||
if score < 10 {
|
||||
log.Tracer(ctx).Warningf(
|
||||
"nameserver: possible data tunnel by %s in subdomain %s: %s has an lms score of %.2f, returning nxdomain",
|
||||
conn.Process(),
|
||||
conn.Entity.Domain,
|
||||
domainToCheck,
|
||||
score,
|
||||
)
|
||||
conn.Block("Possible data tunnel")
|
||||
return true
|
||||
}
|
||||
log.Tracer(ctx).Infof("LMS score of entire domain is %.2f", score)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -80,6 +80,10 @@ var (
|
|||
cfgOptionRemoveBlockedDNS config.IntOption // security level option
|
||||
cfgOptionRemoveBlockedDNSOrder = 113
|
||||
|
||||
CfgOptionDomainHeuristicsKey = "filter/domainHeuristics"
|
||||
cfgOptionDomainHeuristics config.IntOption // security level option
|
||||
cfgOptionDomainHeuristicsOrder = 114
|
||||
|
||||
// Permanent Verdicts Order = 128
|
||||
)
|
||||
|
||||
|
@ -378,6 +382,24 @@ Examples:
|
|||
cfgOptionRemoveBlockedDNS = config.Concurrent.GetAsInt(CfgOptionRemoveBlockedDNSKey, int64(status.SecurityLevelsAll))
|
||||
cfgIntOptions[CfgOptionRemoveBlockedDNSKey] = cfgOptionRemoveBlockedDNS
|
||||
|
||||
// Domain heuristics
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Enable Domain Heuristics",
|
||||
Key: CfgOptionDomainHeuristicsKey,
|
||||
Description: "Domain Heuristics checks for suspicious looking domain names and blocks them. Ths option currently targets domains generated by malware and DNS data tunnels.",
|
||||
Order: cfgOptionDomainHeuristicsOrder,
|
||||
OptType: config.OptTypeInt,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ExternalOptType: "security level",
|
||||
DefaultValue: status.SecurityLevelsAll,
|
||||
ValidationRegex: "^(0|4|6|7)$",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgOptionDomainHeuristics = config.Concurrent.GetAsInt(CfgOptionDomainHeuristicsKey, int64(status.SecurityLevelsAll))
|
||||
|
||||
// Bypass prevention
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Prevent Bypassing",
|
||||
Key: CfgOptionPreventBypassingKey,
|
||||
|
|
|
@ -45,6 +45,7 @@ type LayeredProfile struct {
|
|||
FilterSubDomains config.BoolOption
|
||||
FilterCNAMEs config.BoolOption
|
||||
PreventBypassing config.BoolOption
|
||||
DomainHeuristics config.BoolOption
|
||||
}
|
||||
|
||||
// NewLayeredProfile returns a new layered profile based on the given local profile.
|
||||
|
@ -108,6 +109,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
|||
CfgOptionPreventBypassingKey,
|
||||
cfgOptionPreventBypassing,
|
||||
)
|
||||
new.DomainHeuristics = new.wrapSecurityLevelOption(
|
||||
CfgOptionDomainHeuristicsKey,
|
||||
cfgOptionDomainHeuristics,
|
||||
)
|
||||
|
||||
// TODO: load linked profiles.
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue