Skip to content

Commit 1187144

Browse files
authored
feat: add dns black list support (#11)
* feat: add dns black list support * dns server refactor * fix the api calling --------- Co-authored-by: Rick <[email protected]>
1 parent 1e929ac commit 1187144

File tree

10 files changed

+457
-162
lines changed

10 files changed

+457
-162
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ atest-collector dns --simple-config config.yaml
1616

1717
Below is an example of a simple DNS config:
1818
```yaml
19-
atest.com: 127.0.0.1
20-
www.atest.com: 127.0.0.1
19+
simple:
20+
atest.com: 127.0.0.1
21+
www.atest.com: 127.0.0.1
2122
```

cmd/dns.go

Lines changed: 21 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
Copyright 2024 LinuxSuRen.
2+
Copyright 2024-2025 LinuxSuRen.
33
44
Licensed under the Apache License, Version 2.0 (the "License");
55
you may not use this file except in compliance with the License.
@@ -18,16 +18,9 @@ package cmd
1818

1919
import (
2020
"fmt"
21-
"github.com/google/gopacket"
22-
"github.com/google/gopacket/layers"
2321
adns "github.com/linuxsuren/atest-ext-collector/pkg/dns"
24-
"github.com/miekg/dns"
2522
"github.com/spf13/cobra"
2623
"github.com/spf13/pflag"
27-
"gopkg.in/yaml.v3"
28-
"net"
29-
"os"
30-
"strings"
3124
)
3225

3326
func createDNSCmd() (cmd *cobra.Command) {
@@ -51,6 +44,8 @@ type dnsOptions struct {
5144

5245
// inner fields
5346
cacheHandler adns.DNSCache
47+
server adns.Server
48+
config *adns.DNSConfig
5449
}
5550

5651
func (o *dnsOptions) setFlags(flags *pflag.FlagSet) {
@@ -68,107 +63,34 @@ func (o *dnsOptions) preRunE(_ *cobra.Command, _ []string) (err error) {
6863
}
6964

7065
if o.simpleConfig != "" {
71-
var data []byte
72-
if data, err = os.ReadFile(o.simpleConfig); err == nil {
73-
records := make(map[string]string)
74-
if err = yaml.Unmarshal(data, &records); err == nil {
75-
o.cacheHandler.Init(records)
76-
}
66+
if o.config, err = adns.ParseFromFile(o.simpleConfig); err != nil {
67+
return
7768
}
78-
}
79-
return
80-
}
8169

82-
func (o *dnsOptions) runE(cmd *cobra.Command, args []string) (err error) {
83-
addr := net.UDPAddr{
84-
Port: o.port,
85-
IP: net.ParseIP("0.0.0.0"),
70+
o.cacheHandler.Init(o.config.Simple)
71+
o.cacheHandler.GetWildcardCache().Init(o.config.Wildcard)
8672
}
87-
var u *net.UDPConn
88-
if u, err = net.ListenUDP("udp", &addr); err != nil {
89-
return
73+
if o.config == nil {
74+
o.config = &adns.DNSConfig{}
9075
}
9176

92-
cmd.Println("DNS server is ready!")
93-
go func() {
94-
if err := adns.NewHTTPServer(o.httpPort, o.cacheHandler).Start(); err != nil {
95-
panic(err)
96-
}
97-
}()
98-
// Wait to get request on that port
99-
for {
100-
tmp := make([]byte, 1024)
101-
_, addr, _ := u.ReadFrom(tmp)
102-
clientAddr := addr
103-
packet := gopacket.NewPacket(tmp, layers.LayerTypeDNS, gopacket.Default)
104-
dnsPacket := packet.Layer(layers.LayerTypeDNS)
105-
tcp, _ := dnsPacket.(*layers.DNS)
106-
if !o.serveDNS(u, clientAddr, tcp) {
107-
o.cacheDNS(string(tcp.Questions[0].Name))
108-
}
77+
if o.upstream != "" {
78+
o.config.Upstream = o.upstream
79+
}
80+
if o.port > 0 {
81+
o.config.Port = o.port
10982
}
83+
o.server = adns.NewDNSServer(o.config, o.cacheHandler)
11084
return
11185
}
11286

113-
func (o *dnsOptions) cacheDNS(name string) {
114-
client := dns.Client{}
115-
var m dns.Msg
116-
m.SetQuestion(name+".", dns.TypeA)
117-
reply, _, err := client.Exchange(&m, o.upstream)
118-
if err != nil {
119-
fmt.Println("failed query domain", name, err)
120-
return
121-
}
122-
if reply.Rcode == dns.RcodeSuccess && len(reply.Answer) > 0 {
123-
if a, ok := reply.Answer[0].(*dns.A); ok {
124-
o.cacheHandler.Put(strings.TrimSuffix(reply.Question[0].Name, "."), a.A.String())
125-
fmt.Println("cache new record", reply.Question[0].Name, "==", a.A.String())
126-
fmt.Println("total cache item count", o.cacheHandler.Size())
127-
return
128-
} else if cname, ok := reply.Answer[0].(*dns.CNAME); ok {
129-
fmt.Println(name, "==", strings.TrimSuffix(cname.Hdr.Name, "."), "==", strings.TrimSuffix(cname.Target, "."))
130-
if ip := o.cacheHandler.LookupIP(strings.TrimSuffix(cname.Target, ".")); ip != "" {
131-
o.cacheHandler.Put(strings.TrimSuffix(cname.Hdr.Name, "."), ip)
132-
} else {
133-
o.cacheDNS(strings.TrimSuffix(cname.Target, "."))
134-
}
87+
func (o *dnsOptions) runE(cmd *cobra.Command, args []string) (err error) {
88+
go func() {
89+
if err := adns.NewHTTPServer(o.httpPort, o.upstream, o.cacheHandler).Start(); err != nil {
90+
panic(err)
13591
}
136-
fmt.Println("unknown record", reply.Question[0].Name, ",", reply.Answer[0].String())
137-
}
138-
}
92+
}()
13993

140-
func (o *dnsOptions) serveDNS(u *net.UDPConn, clientAddr net.Addr, request *layers.DNS) (resolved bool) {
141-
replyMess := request
142-
var dnsAnswer layers.DNSResourceRecord
143-
dnsAnswer.Type = layers.DNSTypeA
144-
var ip string
145-
var err error
146-
resolved = true
147-
ip = o.cacheHandler.LookupIP(string(request.Questions[0].Name))
148-
if ip == "" {
149-
//fmt.Printf("cannot found: %s\n", request.Questions[0].Name)
150-
resolved = false
151-
return
152-
//Todo: Log no data present for the IP and handle:todo
153-
}
154-
a, _, _ := net.ParseCIDR(ip + "/24")
155-
dnsAnswer.Type = layers.DNSTypeA
156-
dnsAnswer.IP = a
157-
dnsAnswer.Name = []byte(request.Questions[0].Name)
158-
fmt.Println(string(request.Questions[0].Name), "===", ip)
159-
dnsAnswer.Class = layers.DNSClassIN
160-
replyMess.QR = true
161-
replyMess.ANCount = 1
162-
replyMess.OpCode = layers.DNSOpCodeNotify
163-
replyMess.AA = true
164-
replyMess.Answers = append(replyMess.Answers, dnsAnswer)
165-
replyMess.ResponseCode = layers.DNSResponseCodeNoErr
166-
buf := gopacket.NewSerializeBuffer()
167-
opts := gopacket.SerializeOptions{} // See SerializeOptions for more details.
168-
err = replyMess.SerializeTo(buf, opts)
169-
if err != nil {
170-
panic(err)
171-
}
172-
u.WriteTo(buf.Bytes(), clientAddr)
94+
err = o.server.Start()
17395
return
17496
}

pkg/dns/config.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2025 LinuxSuRen.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dns
18+
19+
import (
20+
"gopkg.in/yaml.v3"
21+
"os"
22+
)
23+
24+
type DNSConfig struct {
25+
Wildcard map[string]string `yaml:"wildcard"`
26+
Simple map[string]string `yaml:"simple"`
27+
Black []string `yaml:"black"`
28+
WildcardBlack []string `yaml:"wildcard_black"`
29+
Upstream string `yaml:"upstream"`
30+
Port int `yaml:"port"`
31+
}
32+
33+
func ParseFromFile(file string) (config *DNSConfig, err error) {
34+
var data []byte
35+
if data, err = os.ReadFile(file); err == nil {
36+
config, err = ParseFromBuffer(data)
37+
}
38+
return
39+
}
40+
41+
func ParseFromBuffer(buffer []byte) (config *DNSConfig, err error) {
42+
config = &DNSConfig{}
43+
err = yaml.Unmarshal(buffer, &config)
44+
return
45+
}

pkg/dns/config_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2025 LinuxSuRen.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package dns_test
18+
19+
import (
20+
"github.com/linuxsuren/atest-ext-collector/pkg/dns"
21+
"github.com/stretchr/testify/assert"
22+
"testing"
23+
)
24+
import _ "embed"
25+
26+
func TestParse(t *testing.T) {
27+
defaultConfig := &dns.DNSConfig{
28+
Wildcard: map[string]string{
29+
"*.def.com": "0.0.0.2",
30+
},
31+
Simple: map[string]string{
32+
"abc.com": "0.0.0.1",
33+
},
34+
Black: []string{"ghi.com"},
35+
WildcardBlack: []string{"*.jkl.com"},
36+
Upstream: "8.8.8.8",
37+
Port: 53,
38+
}
39+
40+
config, err := dns.ParseFromBuffer(configYaml)
41+
assert.NoError(t, err)
42+
assert.Equal(t, defaultConfig, config)
43+
44+
config, err = dns.ParseFromFile("testdata/config.yaml")
45+
assert.NoError(t, err)
46+
assert.Equal(t, defaultConfig, config)
47+
}
48+
49+
var (
50+
//go:embed testdata/config.yaml
51+
configYaml []byte
52+
)

pkg/dns/data/index.html

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
function showAddForm() {
66
document.getElementById('addFormZone').classList.remove('hide');
77
}
8+
function showAddBlackDomain() {
9+
document.getElementById('addBlackDomain').classList.remove('hide');
10+
}
811
</script>
912
<style>
1013
.hide {
@@ -15,13 +18,25 @@
1518
<body>
1619

1720
<div>
18-
<button onclick="showAddForm()">Add a new record</button> There are {{.size}} records.
19-
<div class="hide" id="addFormZone">
20-
<form action="/add" method="post">
21-
Domain: <input name="domain"/>
22-
IP: <input name="ip"/>
23-
<button type="submit">Submit</button>
24-
</form>
21+
<div>Upstream DNS Server: {{.upstream}}</div>
22+
<div>
23+
<button onclick="showAddForm()">Add a new record</button> There are {{.size}} records.
24+
<div class="hide" id="addFormZone">
25+
<form action="/add" method="post">
26+
Domain: <input name="domain"/>
27+
IP: <input name="ip"/>
28+
<button type="submit">Submit</button>
29+
</form>
30+
</div>
31+
</div>
32+
<div>
33+
<button onclick="showAddBlackDomain()">Add black domain</button>
34+
<div class="hide" id="addBlackDomain">
35+
<form action="/addBlack" method="post">
36+
Domain: <input name="domain"/>
37+
<button type="submit">Submit</button>
38+
</form>
39+
</div>
2540
</div>
2641
</div>
2742

@@ -39,5 +54,19 @@
3954
</tr>
4055
{{end}}
4156
</table>
57+
58+
<div>Black list</div>
59+
<table border="1">
60+
<tr>
61+
<th>Domain</th>
62+
<th>Operation</th>
63+
</tr>
64+
{{range .black}}
65+
<tr>
66+
<td>{{ . }}</td>
67+
<td><a href="/removeBlack?domain={{.}}">Remove</a></td>
68+
</tr>
69+
{{end}}
70+
</table>
4271
</body>
4372
</html>

0 commit comments

Comments
 (0)