-
Notifications
You must be signed in to change notification settings - Fork 0
/
serverlist.go
288 lines (241 loc) · 6.79 KB
/
serverlist.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
package dnsyo
import (
"fmt"
"github.com/gocarina/gocsv"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"io/ioutil"
"math/rand"
"net"
"net/http"
"strings"
"sync"
"time"
)
const (
answerResult = 4 // tab index for the result in a DNS response. Used to eliminate type and TTL etc.
reliabilityThreshold = 0.97 // minimum public-dns.info reliability threshold for a server to be loaded from csv
)
// representation of a nameserver in CSV from from public-dns.info
type csvNameserver struct {
// IPAddress is the ipv4 address of the server
IPAddress string `csv:"ip"`
// Name is the hostname of the server if the server has a hostname
Name string `csv:"name"`
// Country is the two-letter ISO 3166-1 alpha-2 code of the country
Country string `csv:"country_id"`
// City specifies the city that the server is hosted on
City string `csv:"city"`
// Version is the software version of the dns daemon that the server is using
Version string `csv:"version"`
// Error is the error that the server returned. Probably will be empty if you use the valid nameserver dataset
Error string `csv:"error"`
// DNSSec is a boolean to indicate if the server supports DNSSec or not
DNSSec bool `csv:"dnssec"`
// Realiability is a normalized value - from 0.0 - 1.0 - to indicate how stable the server is
Reliability float64 `csv:"reliability"`
// CheckedAt is a timestamp to indicate the date that the server was last checked
CheckedAt time.Time `csv:"checked_at"`
// CreatedAt is a timestamp to indicate when the server was inserted in the database
CreatedAt time.Time `csv:"created_at"`
}
// ServerList is an alias for a slice of Server objects used for performing bulk actions on multiple threads
type ServerList []Server
// ServersFromFile loads a ServerList from a YAML file. Will raise an error if the file cannot be opened or processed
func ServersFromFile(filename string) (sl ServerList, err error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return
}
err = yaml.Unmarshal(data, &sl)
if err != nil {
return nil, err
}
return
}
// ServersFromCSVURL loads a ServerList from a CSV provided at a given URL.
// Designed for use with public-info.dns
func ServersFromCSVURL(url string) (sl ServerList, err error) {
csvFile, err := http.Get(url)
if err != nil {
return
}
// sometimes the file can be truncated and have an incomplete final line.
// run a basic check to ensure it isn't going to error later.
bytes, err := ioutil.ReadAll(csvFile.Body)
if err != nil {
return
}
rows := strings.Split(string(bytes), "\n")
nCols := strings.Count(rows[0], ",")
if strings.Count(rows[len(rows)-1], ",") < nCols {
rows = rows[:len(rows)-2]
}
data := strings.Join(rows, "\n")
var servers []csvNameserver
err = gocsv.Unmarshal(strings.NewReader(data), &servers)
if err != nil {
return
}
for _, ns := range servers {
if ip := net.ParseIP(ns.IPAddress); ip.To4() == nil {
continue // we can't process IPv6 yet
}
if ns.Reliability >= reliabilityThreshold {
s := Server{
IP: ns.IPAddress,
Country: strings.ToUpper(ns.Country),
Name: ns.Name,
}
sl = append(sl, s)
}
}
return
}
// DumpToFile a the current server list to a YAML file.
// Includes a commented header to identify the fact it is generated.
func (sl *ServerList) DumpToFile(filename string) (err error) {
yml, err := yaml.Marshal(sl)
if err != nil {
return
}
data := append([]byte("#### GENERATED BY dnsyo update ####\n\n"), yml...)
err = ioutil.WriteFile(filename, data, 0744)
return
}
// FilterCountry filters the current server list by country and returns a new server list with the matching servers in it.
// Returns an error if no servers were found.
func (sl *ServerList) FilterCountry(country string) (fl ServerList, err error) {
for _, s := range *sl {
if s.Country == strings.ToUpper(country) {
fl = append(fl, s)
}
}
if len(fl) == 0 {
err = fmt.Errorf("no servers matching country %s were found", country)
}
return
}
// NRandom returns n random servers from the current server list in a new list.
// Will return an error if there are less than n servers in the current list.
func (sl *ServerList) NRandom(n int) (rl ServerList, err error) {
ql := *sl
if len(ql) < n {
return nil, fmt.Errorf("insufficient servers to populate list: %d of %d", len(ql), n)
}
rl = make(ServerList, n)
r := rand.New(rand.NewSource(time.Now().UnixNano()))
perm := r.Perm(n)
for i, randIndex := range perm[:n] {
rl[i] = ql[randIndex]
}
return
}
// ExecuteQuery runs a Query object in a specified number of threads.
// The returned QueryResult is not associated with the provided Query, however may be set by the caller.
func (sl *ServerList) ExecuteQuery(q *Query, threads int) (qr QueryResults) {
qr = make(QueryResults)
var wg sync.WaitGroup
var mtx sync.Mutex
queue := make(chan Server, len(*sl))
// start workers
for i := 0; i < threads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for s := range queue {
res, err := s.Lookup(q.Domain, q.Type)
ans := strings.Join(res, "\n")
r := new(Result)
if err != nil {
r.Error = err.Error()
} else {
r.Answer = ans
}
mtx.Lock()
qr[s.String()] = r
mtx.Unlock()
}
}(i)
}
for _, s := range *sl {
queue <- s
}
close(queue)
wg.Wait()
return
}
//func (sl *ServerList) StreamQuery(q *Query, threads int, results chan QueryResults) {
// recordType := dns.StringToType[q.Type]
//
// var wg sync.WaitGroup
//
// queue := make(chan Server, len(*sl))
//
// // start workers
// for i := 0; i < threads; i++ {
// wg.Add(1)
// go func(i int) {
// defer wg.Done()
// for s := range queue {
// res, err := s.Lookup(q.Domain, recordType)
// ans := strings.Join(res, "\n")
//
// r := &Result{
// Answer: ans,
// }
// if err != nil {
// r.Error = err.Error()
// }
//
// qr := QueryResults{
// s: r,
// }
//
// results <- qr
// }
// }(i)
// }
//
// for _, s := range *sl {
// queue <- s
// }
// close(queue)
//
// wg.Wait()
// return
//}
// TestAll tests all the servers in the current list and returns a new list with only the workings ones.
func (sl *ServerList) TestAll(threads int) (working ServerList) {
var wg sync.WaitGroup
var mutex sync.Mutex
testQueue := make(chan Server, len(*sl))
// start workers
for i := 0; i < threads; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for s := range testQueue {
log.WithField("thread", i).Debug("Testing " + s.Name)
if ok, err := s.Test(); ok {
mutex.Lock()
working = append(working, s)
mutex.Unlock()
} else {
log.WithFields(log.Fields{
"thread": i,
"server": s.String(),
"reason": err,
}).Info("Disabling server")
}
}
}(i)
}
// add test servers
for _, s := range *sl {
testQueue <- s
}
close(testQueue)
wg.Wait()
return working
}