Golang 匹配域名通配符
Golang match domain names wild card
我有主机名白名单映射
var hostnameWhitelist = map[string] bool { "test.mydomain.com":true, "test12.mydomaindev.com":true}
我检查传入请求的主机名是否被允许的方法是 -
url, errURL := url.Parse("test.mydomain.com")
if errURL != nil {
fmt.Println("error during parsing URL")
return false
}
fmt.Println("HOSTNAME = " + url.Hostname())
if ok := hostnameWhitelist[url.Hostname()]; ok {
fmt.Println("valid domain, allow access")
} else {
fmt.Println("NOT valid domain")
return false
}
虽然这很好用,但我如何进行外卡匹配 -
*.mydomain.com
*.mydomaindev.com
这两个都应该通过。
同时,
*.test.com
*.hello.com
应该失败
您可以将地图的键以*.domain.com
的格式存储
使用 strings.SplitAfterN
and strings.Join
.
将您获得的所有主机名转换为该格式
split := strings.SplitAfterN(url.Hostname(),".",2)
split[0] = "*"
hostName := strings.Join(split,".")
...
hostnameWhitelist[hostName]
...
不相关的改进
如果您将地图纯粹用作白名单,则可以使用 map[string]struct{}
而不是 map[string]bool
。但正如 Peter
在他的评论中提到的,只有当你有一个非常大的白名单时,它才可能是相关的。
正则表达式是您问题的解决方案,当您尝试将正则表达式与单个值匹配时,map[string]bool 可能无法按预期工作。
package main
import (
"fmt"
"regexp"
)
func main() {
if matched, _ := regexp.MatchString(".*\.?mydomain.*", "mydomaindev.com"); matched {
fmt.Println("Valid domain")
}
}
这将匹配所有具有模式 mydomain 的域,因此 www.mydomain.com www.mydomaindev.com 将匹配 byt test.com 而 hello.com 将失败
其他方便的字符串操作是,
//This would match a.mydomain.com, b.mydomain.com etc.,
if strings.HasSuffix(url1.Hostname(), ".mydomain.com") {
fmt.Println("Valid domain allow access")
}
//To match anything with mydomain - like mydomain.com, mydomaindev.com
if strings.Contains(url2.Hostname(), "mydomain.com") {
fmt.Println("Valid domain allow access")
}
如果您希望能够在您的域中有多个深度,例如:
- *.foo.example.org
- *.example.com
然后我会为通配符添加第二个容器:
var wdomains = []string { ".foo.example.org", ".example.com"}
然后检查您要测试的域是否以这些条目之一结尾:
func inWdomain(wdomains []string, domain string) bool {
for _, suffix := range wdomains {
if strings.HasSuffix(domain, suffix) {
return true
}
}
return false
}
注意:如果您的域超过数百个,您可以使用基数树。
在开始时使用通配符进行通配符匹配非常昂贵。正则表达式在性能方面可能很困难,具体取决于数据集的大小和对数据集进行评估的速度。您可以尝试使用后缀树,但我怀疑性能可能会成为一个问题(我还没有在我们的数据上测试过)。
我们使用的一种方法是使用反向八位字节顺序的签名域名标签构建 Radix Trie(紧凑型前缀 trie)。您的签名域 *.foo.example.com
变为 com.example.foo.*
,将通配符放在末尾。您自定义构建的 Radix 树仅在到达通配符节点时才需要停止匹配。您的 Trie 可以同时支持精确字符串匹配和通配符匹配。如果您希望允许通配符位于域名中间,性能可能会成为问题。
我们使用 Trie 评估域名时遇到的最大挑战之一不是搜索时间,而是内存消耗,因此当您有很多签名时启动程序需要多长时间。
我们评估了一些实现(在开始时主要没有通配符支持)测试加载时间、分配、内部节点数量、内存消耗、GC 时间和 search/insert/remove 时间。
我们测试过的实现:
- golang 映射
- https://github.com/armon/go-radix
- https://github.com/tchap/go-patricia
- https://github.com/fanyang01/radix
- 我们自己的实现
显然,使用 golang 映射会提供最佳性能,但是当需要检索(Trie 这个词的来源)时,例如来自数据集的前缀信息,golang maps 没有给我们提供我们需要的特征。
我们在 Trie 中保留了大约 700 000 个域名签名。构建时间为2秒,300MB内存,500万分配,2秒GC和搜索成本150ns/op.
如果我们对相同的签名(没有通配符)使用 golang 映射,我们得到加载时间 0.5 秒、50MB 内存、可忽略的分配、1.6 秒 GC 和搜索成本 25ns/op。
在我们的初始实施中,构建时间为 6 秒、1GB 内存、6000 万次分配、5 秒 GC 和搜索成本约为 200 ns/op。
正如您从这些结果中看到的,我们设法降低了内存消耗和加载时间,同时搜索成本大致保持不变。
如果您要进行 CIDR 匹配,我建议您查看 https://github.com/kentik/patricia。为了减少 GC 时间,实现了避免指针。
祝你工作顺利!
您可以像使用 Set 数据结构一样使用 fstest.MapFS
,还有以下好处
全局匹配:
package main
import "testing/fstest"
var tests = []struct {
pat string
res int
} {
{"*.hello.com", 0},
{"*.mydomain.com", 1},
{"*.mydomaindev.com", 1},
{"*.test.com", 0},
}
func main() {
m := fstest.MapFS{"test.mydomain.com": nil, "test12.mydomaindev.com": nil}
for _, test := range tests {
a, e := m.Glob(test.pat)
if e != nil {
panic(e)
}
if len(a) != test.res {
panic(len(a))
}
}
}
我有主机名白名单映射
var hostnameWhitelist = map[string] bool { "test.mydomain.com":true, "test12.mydomaindev.com":true}
我检查传入请求的主机名是否被允许的方法是 -
url, errURL := url.Parse("test.mydomain.com")
if errURL != nil {
fmt.Println("error during parsing URL")
return false
}
fmt.Println("HOSTNAME = " + url.Hostname())
if ok := hostnameWhitelist[url.Hostname()]; ok {
fmt.Println("valid domain, allow access")
} else {
fmt.Println("NOT valid domain")
return false
}
虽然这很好用,但我如何进行外卡匹配 -
*.mydomain.com
*.mydomaindev.com
这两个都应该通过。
同时,
*.test.com
*.hello.com
应该失败
您可以将地图的键以*.domain.com
的格式存储使用 strings.SplitAfterN
and strings.Join
.
split := strings.SplitAfterN(url.Hostname(),".",2)
split[0] = "*"
hostName := strings.Join(split,".")
...
hostnameWhitelist[hostName]
...
不相关的改进
如果您将地图纯粹用作白名单,则可以使用 map[string]struct{}
而不是 map[string]bool
。但正如 Peter
在他的评论中提到的,只有当你有一个非常大的白名单时,它才可能是相关的。
正则表达式是您问题的解决方案,当您尝试将正则表达式与单个值匹配时,map[string]bool 可能无法按预期工作。
package main
import (
"fmt"
"regexp"
)
func main() {
if matched, _ := regexp.MatchString(".*\.?mydomain.*", "mydomaindev.com"); matched {
fmt.Println("Valid domain")
}
}
这将匹配所有具有模式 mydomain 的域,因此 www.mydomain.com www.mydomaindev.com 将匹配 byt test.com 而 hello.com 将失败
其他方便的字符串操作是,
//This would match a.mydomain.com, b.mydomain.com etc.,
if strings.HasSuffix(url1.Hostname(), ".mydomain.com") {
fmt.Println("Valid domain allow access")
}
//To match anything with mydomain - like mydomain.com, mydomaindev.com
if strings.Contains(url2.Hostname(), "mydomain.com") {
fmt.Println("Valid domain allow access")
}
如果您希望能够在您的域中有多个深度,例如:
- *.foo.example.org
- *.example.com
然后我会为通配符添加第二个容器:
var wdomains = []string { ".foo.example.org", ".example.com"}
然后检查您要测试的域是否以这些条目之一结尾:
func inWdomain(wdomains []string, domain string) bool {
for _, suffix := range wdomains {
if strings.HasSuffix(domain, suffix) {
return true
}
}
return false
}
注意:如果您的域超过数百个,您可以使用基数树。
在开始时使用通配符进行通配符匹配非常昂贵。正则表达式在性能方面可能很困难,具体取决于数据集的大小和对数据集进行评估的速度。您可以尝试使用后缀树,但我怀疑性能可能会成为一个问题(我还没有在我们的数据上测试过)。
我们使用的一种方法是使用反向八位字节顺序的签名域名标签构建 Radix Trie(紧凑型前缀 trie)。您的签名域 *.foo.example.com
变为 com.example.foo.*
,将通配符放在末尾。您自定义构建的 Radix 树仅在到达通配符节点时才需要停止匹配。您的 Trie 可以同时支持精确字符串匹配和通配符匹配。如果您希望允许通配符位于域名中间,性能可能会成为问题。
我们使用 Trie 评估域名时遇到的最大挑战之一不是搜索时间,而是内存消耗,因此当您有很多签名时启动程序需要多长时间。
我们评估了一些实现(在开始时主要没有通配符支持)测试加载时间、分配、内部节点数量、内存消耗、GC 时间和 search/insert/remove 时间。
我们测试过的实现:
- golang 映射
- https://github.com/armon/go-radix
- https://github.com/tchap/go-patricia
- https://github.com/fanyang01/radix
- 我们自己的实现
显然,使用 golang 映射会提供最佳性能,但是当需要检索(Trie 这个词的来源)时,例如来自数据集的前缀信息,golang maps 没有给我们提供我们需要的特征。
我们在 Trie 中保留了大约 700 000 个域名签名。构建时间为2秒,300MB内存,500万分配,2秒GC和搜索成本150ns/op.
如果我们对相同的签名(没有通配符)使用 golang 映射,我们得到加载时间 0.5 秒、50MB 内存、可忽略的分配、1.6 秒 GC 和搜索成本 25ns/op。
在我们的初始实施中,构建时间为 6 秒、1GB 内存、6000 万次分配、5 秒 GC 和搜索成本约为 200 ns/op。
正如您从这些结果中看到的,我们设法降低了内存消耗和加载时间,同时搜索成本大致保持不变。
如果您要进行 CIDR 匹配,我建议您查看 https://github.com/kentik/patricia。为了减少 GC 时间,实现了避免指针。
祝你工作顺利!
您可以像使用 Set 数据结构一样使用 fstest.MapFS
,还有以下好处
全局匹配:
package main
import "testing/fstest"
var tests = []struct {
pat string
res int
} {
{"*.hello.com", 0},
{"*.mydomain.com", 1},
{"*.mydomaindev.com", 1},
{"*.test.com", 0},
}
func main() {
m := fstest.MapFS{"test.mydomain.com": nil, "test12.mydomaindev.com": nil}
for _, test := range tests {
a, e := m.Glob(test.pat)
if e != nil {
panic(e)
}
if len(a) != test.res {
panic(len(a))
}
}
}