mirror of
https://github.com/safing/portmaster
synced 2025-09-04 03:29:12 +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"
|
"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 {
|
func LmsScoreOfDomain(domain string) float64 {
|
||||||
var totalScore float64
|
var totalScore float64
|
||||||
domain = strings.ToLower(domain)
|
domain = strings.ToLower(domain)
|
||||||
subjects := strings.Split(domain, ".")
|
subjects := strings.Split(domain, ".")
|
||||||
// ignore the last two parts
|
|
||||||
if len(subjects) <= 3 {
|
|
||||||
return 100
|
|
||||||
}
|
|
||||||
subjects = subjects[:len(subjects)-3]
|
|
||||||
var totalLength int
|
var totalLength int
|
||||||
for _, subject := range subjects {
|
for _, subject := range subjects {
|
||||||
totalLength += len(subject)
|
totalLength += len(subject)
|
||||||
|
@ -27,7 +24,9 @@ func LmsScoreOfDomain(domain string) float64 {
|
||||||
return totalScore
|
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 {
|
func LmsScore(subject string) float64 {
|
||||||
lmsStart := -1
|
lmsStart := -1
|
||||||
lmsStop := -1
|
lmsStop := -1
|
||||||
|
|
|
@ -5,8 +5,8 @@ import "testing"
|
||||||
func TestLmsScoreOfDomain(t *testing.T) {
|
func TestLmsScoreOfDomain(t *testing.T) {
|
||||||
testDomain(t, "g.symcd.com.", 100, 100)
|
testDomain(t, "g.symcd.com.", 100, 100)
|
||||||
testDomain(t, "www.google.com.", 100, 100)
|
testDomain(t, "www.google.com.", 100, 100)
|
||||||
testDomain(t, "55ttt5.12abc3.test.com.", 50, 50)
|
testDomain(t, "55ttt5.12abc3.test.com.", 68, 69)
|
||||||
testDomain(t, "mbtq6opnuodp34gcrma65fxacgxv5ukr7lq6xuhr4mhoibe7.yvqptrozfbnqyemchpovw3q5xwjibuxfsgb72mix3znhpfhc.i2n7jh2gadqaadck3zs3vg3hbv5pkmwzeay4gc75etyettbb.isi5mhmowtfriu33uxzmgvjur5g2p3tloynwohfrggee6fkn.meop7kqyd5gwxxa3.er.spotify.com.", 0, 30)
|
testDomain(t, "mbtq6opnuodp34gcrma65fxacgxv5ukr7lq6xuhr4mhoibe7.yvqptrozfbnqyemchpovw3q5xwjibuxfsgb72mix3znhpfhc.i2n7jh2gadqaadck3zs3vg3hbv5pkmwzeay4gc75etyettbb.isi5mhmowtfriu33uxzmgvjur5g2p3tloynwohfrggee6fkn.meop7kqyd5gwxxa3.er.spotify.com.", 0, 31)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDomain(t *testing.T, domain string, min, max float64) {
|
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/detection/dga"
|
||||||
"github.com/safing/portmaster/netenv"
|
"github.com/safing/portmaster/netenv"
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
|
||||||
"github.com/safing/portbase/log"
|
"github.com/safing/portbase/log"
|
||||||
"github.com/safing/portmaster/network"
|
"github.com/safing/portmaster/network"
|
||||||
|
@ -59,7 +60,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
|
||||||
checkBypassPrevention,
|
checkBypassPrevention,
|
||||||
checkFilterLists,
|
checkFilterLists,
|
||||||
checkInbound,
|
checkInbound,
|
||||||
checkLMSScore,
|
checkDomainHeuristics,
|
||||||
checkDefaultPermit,
|
checkDefaultPermit,
|
||||||
checkAutoPermitRelated,
|
checkAutoPermitRelated,
|
||||||
checkDefaultAction,
|
checkDefaultAction,
|
||||||
|
@ -283,19 +284,63 @@ func checkFilterLists(ctx context.Context, conn *network.Connection, pkt packet.
|
||||||
return false
|
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 == "" {
|
if conn.Entity.Domain == "" {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for possible DNS tunneling / data transmission
|
trimmedDomain := strings.TrimRight(conn.Entity.Domain, ".")
|
||||||
lms := dga.LmsScoreOfDomain(conn.Entity.Domain)
|
etld1, err := publicsuffix.EffectiveTLDPlusOne(trimmedDomain)
|
||||||
if lms < 10 {
|
if err != nil {
|
||||||
log.Tracer(ctx).Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", conn.Process(), conn.Entity.Domain, lms)
|
// we don't apply any checks here and let the request through
|
||||||
conn.BlockWithContext("Possible data tunnel", conn.ReasonContext)
|
// 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
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,6 +80,10 @@ var (
|
||||||
cfgOptionRemoveBlockedDNS config.IntOption // security level option
|
cfgOptionRemoveBlockedDNS config.IntOption // security level option
|
||||||
cfgOptionRemoveBlockedDNSOrder = 113
|
cfgOptionRemoveBlockedDNSOrder = 113
|
||||||
|
|
||||||
|
CfgOptionDomainHeuristicsKey = "filter/domainHeuristics"
|
||||||
|
cfgOptionDomainHeuristics config.IntOption // security level option
|
||||||
|
cfgOptionDomainHeuristicsOrder = 114
|
||||||
|
|
||||||
// Permanent Verdicts Order = 128
|
// Permanent Verdicts Order = 128
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -378,6 +382,24 @@ Examples:
|
||||||
cfgOptionRemoveBlockedDNS = config.Concurrent.GetAsInt(CfgOptionRemoveBlockedDNSKey, int64(status.SecurityLevelsAll))
|
cfgOptionRemoveBlockedDNS = config.Concurrent.GetAsInt(CfgOptionRemoveBlockedDNSKey, int64(status.SecurityLevelsAll))
|
||||||
cfgIntOptions[CfgOptionRemoveBlockedDNSKey] = cfgOptionRemoveBlockedDNS
|
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{
|
err = config.Register(&config.Option{
|
||||||
Name: "Prevent Bypassing",
|
Name: "Prevent Bypassing",
|
||||||
Key: CfgOptionPreventBypassingKey,
|
Key: CfgOptionPreventBypassingKey,
|
||||||
|
|
|
@ -45,6 +45,7 @@ type LayeredProfile struct {
|
||||||
FilterSubDomains config.BoolOption
|
FilterSubDomains config.BoolOption
|
||||||
FilterCNAMEs config.BoolOption
|
FilterCNAMEs config.BoolOption
|
||||||
PreventBypassing config.BoolOption
|
PreventBypassing config.BoolOption
|
||||||
|
DomainHeuristics config.BoolOption
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLayeredProfile returns a new layered profile based on the given local profile.
|
// NewLayeredProfile returns a new layered profile based on the given local profile.
|
||||||
|
@ -108,6 +109,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
|
||||||
CfgOptionPreventBypassingKey,
|
CfgOptionPreventBypassingKey,
|
||||||
cfgOptionPreventBypassing,
|
cfgOptionPreventBypassing,
|
||||||
)
|
)
|
||||||
|
new.DomainHeuristics = new.wrapSecurityLevelOption(
|
||||||
|
CfgOptionDomainHeuristicsKey,
|
||||||
|
cfgOptionDomainHeuristics,
|
||||||
|
)
|
||||||
|
|
||||||
// TODO: load linked profiles.
|
// TODO: load linked profiles.
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue