diff --git a/profile/endpoints/endpoint-domain.go b/profile/endpoints/endpoint-domain.go index 3cc3450f..b02fc9a5 100644 --- a/profile/endpoints/endpoint-domain.go +++ b/profile/endpoints/endpoint-domain.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/safing/portmaster/intel" + "github.com/safing/portmaster/network/netutils" ) const ( @@ -16,8 +17,7 @@ const ( ) var ( - domainRegex = regexp.MustCompile(`^\*?(([a-z0-9][a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z]{2,}\.?$`) - altDomainRegex = regexp.MustCompile(`^\*?[a-z0-9\.-]+\*$`) + allowedDomainChars = regexp.MustCompile(`^[a-z0-9\.-]+$`) ) // EndpointDomain matches domains. @@ -90,51 +90,63 @@ func (ep *EndpointDomain) String() string { func parseTypeDomain(fields []string) (Endpoint, error) { domain := fields[1] - - if domainRegex.MatchString(domain) || altDomainRegex.MatchString(domain) { - ep := &EndpointDomain{ - OriginalValue: domain, - } - - // fix domain ending - switch domain[len(domain)-1] { - case '.': - case '*': - default: - domain += "." - } - - // fix domain case - domain = strings.ToLower(domain) - - switch { - case strings.HasPrefix(domain, "*") && strings.HasSuffix(domain, "*"): - ep.MatchType = domainMatchTypeContains - ep.Domain = strings.Trim(domain, "*") - return ep.parsePPP(ep, fields) - - case strings.HasSuffix(domain, "*"): - ep.MatchType = domainMatchTypePrefix - ep.Domain = strings.Trim(domain, "*") - return ep.parsePPP(ep, fields) - - case strings.HasPrefix(domain, "*"): - ep.MatchType = domainMatchTypeSuffix - ep.Domain = strings.Trim(domain, "*") - return ep.parsePPP(ep, fields) - - case strings.HasPrefix(domain, "."): - ep.MatchType = domainMatchTypeZone - ep.Domain = strings.TrimLeft(domain, ".") - ep.DomainZone = "." + ep.Domain - return ep.parsePPP(ep, fields) - - default: - ep.MatchType = domainMatchTypeExact - ep.Domain = domain - return ep.parsePPP(ep, fields) - } + ep := &EndpointDomain{ + OriginalValue: domain, } - return nil, nil + // Fix domain ending. + switch domain[len(domain)-1] { + case '.', '*': + default: + domain += "." + } + + // Fix domain case. + domain = strings.ToLower(domain) + needValidFQDN := true + + switch { + case strings.HasPrefix(domain, "*") && strings.HasSuffix(domain, "*"): + ep.MatchType = domainMatchTypeContains + ep.Domain = strings.TrimPrefix(domain, "*") + ep.Domain = strings.TrimSuffix(ep.Domain, "*") + needValidFQDN = false + + case strings.HasSuffix(domain, "*"): + ep.MatchType = domainMatchTypePrefix + ep.Domain = strings.TrimSuffix(domain, "*") + needValidFQDN = false + + // Prefix matching cannot be combined with zone matching + if strings.HasPrefix(ep.Domain, ".") { + return nil, nil + } + + case strings.HasPrefix(domain, "*"): + ep.MatchType = domainMatchTypeSuffix + ep.Domain = strings.TrimPrefix(domain, "*") + needValidFQDN = false + + case strings.HasPrefix(domain, "."): + ep.MatchType = domainMatchTypeZone + ep.Domain = strings.TrimPrefix(domain, ".") + ep.DomainZone = "." + ep.Domain + + default: + ep.MatchType = domainMatchTypeExact + ep.Domain = domain + } + + // Validate domain "content". + switch { + case needValidFQDN && !netutils.IsValidFqdn(ep.Domain): + return nil, nil + case !needValidFQDN && !allowedDomainChars.MatchString(ep.Domain): + return nil, nil + case strings.Contains(ep.Domain, ".."): + // The above regex does not catch double dots. + return nil, nil + } + + return ep.parsePPP(ep, fields) } diff --git a/profile/endpoints/endpoints_test.go b/profile/endpoints/endpoints_test.go index 85164a4d..531544d8 100644 --- a/profile/endpoints/endpoints_test.go +++ b/profile/endpoints/endpoints_test.go @@ -5,6 +5,8 @@ import ( "runtime" "testing" + "github.com/stretchr/testify/assert" + "github.com/safing/portmaster/core/pmtesting" "github.com/safing/portmaster/intel" ) @@ -27,6 +29,31 @@ func testEndpointMatch(t *testing.T, ep Endpoint, entity *intel.Entity, expected } } +func testFormat(t *testing.T, endpoint string, shouldSucceed bool) { + _, err := parseEndpoint(endpoint) + if shouldSucceed { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } +} + +func TestEndpointFormat(t *testing.T) { + testFormat(t, "+ .", false) + testFormat(t, "+ .at", true) + testFormat(t, "+ .at.", true) + testFormat(t, "+ 1.at", true) + testFormat(t, "+ 1.at.", true) + testFormat(t, "+ 1.f.ix.de.", true) + testFormat(t, "+ *contains*", true) + testFormat(t, "+ *has.suffix", true) + testFormat(t, "+ *.has.suffix", true) + testFormat(t, "+ *has.prefix*", true) + testFormat(t, "+ *has.prefix.*", true) + testFormat(t, "+ .sub.and.prefix.*", false) + testFormat(t, "+ *.sub..and.prefix.*", false) +} + func TestEndpointMatching(t *testing.T) { // ANY