Skip to content

Commit e96a590

Browse files
committed
Refactor more daemons to work with common TCP and UDP server
1 parent fbfa4c3 commit e96a590

File tree

18 files changed

+352
-832
lines changed

18 files changed

+352
-832
lines changed

daemon/common/tcpsrv_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import (
55
"fmt"
66
"github.com/HouzuoGuo/laitos/lalog"
77
"github.com/HouzuoGuo/laitos/misc"
8-
"github.com/labstack/gommon/log"
98
"io"
9+
"log"
1010
"net"
1111
"testing"
1212
"time"

daemon/dnsd/dnsd.go

+61-15
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ type Daemon struct {
8484
UDPPort int `json:"UDPPort"` // UDP port to listen on
8585
TCPPort int `json:"TCPPort"` // TCP port to listen on
8686

87-
tcpListener net.Listener // Once TCP daemon is started, this is its listener.
88-
udpListener *net.UDPConn // Once UDP daemon is started, this is its listener.
87+
tcpServer *common.TCPServer
88+
udpServer *common.UDPServer
8989

9090
/*
9191
blackList is a map of domain names (in lower case) and their resolved IP addresses that should be blocked. In
@@ -162,9 +162,12 @@ func (daemon *Daemon) Initialise() error {
162162
}
163163
daemon.rateLimit.Initialise()
164164

165+
daemon.latestCommands = NewLatestCommands()
166+
daemon.tcpServer = common.NewTCPServer(daemon.Address, daemon.TCPPort, "dnsd", daemon, daemon.PerIPLimit)
167+
daemon.udpServer = common.NewUDPServer(daemon.Address, daemon.UDPPort, "dnsd", daemon, daemon.PerIPLimit)
168+
165169
// Always allow server itself to query the DNS servers via its public IP
166170
daemon.allowMyPublicIP()
167-
daemon.latestCommands = NewLatestCommands()
168171
return nil
169172
}
170173

@@ -324,15 +327,15 @@ func (daemon *Daemon) StartAndBlock() error {
324327
if daemon.UDPPort != 0 {
325328
numListeners++
326329
go func() {
327-
err := daemon.StartAndBlockUDP()
330+
err := daemon.udpServer.StartAndBlock()
328331
errChan <- err
329332
stopAdBlockUpdater <- true
330333
}()
331334
}
332335
if daemon.TCPPort != 0 {
333336
numListeners++
334337
go func() {
335-
err := daemon.StartAndBlockTCP()
338+
err := daemon.tcpServer.StartAndBlock()
336339
errChan <- err
337340
stopAdBlockUpdater <- true
338341
}()
@@ -348,16 +351,8 @@ func (daemon *Daemon) StartAndBlock() error {
348351

349352
// Close all of open TCP and UDP listeners so that they will cease processing incoming connections.
350353
func (daemon *Daemon) Stop() {
351-
if listener := daemon.tcpListener; listener != nil {
352-
if err := listener.Close(); err != nil {
353-
daemon.logger.Warning("Stop", "", err, "failed to close TCP listener")
354-
}
355-
}
356-
if listener := daemon.udpListener; listener != nil {
357-
if err := listener.Close(); err != nil {
358-
daemon.logger.Warning("Stop", "", err, "failed to close UDP listener")
359-
}
360-
}
354+
daemon.tcpServer.Stop()
355+
daemon.udpServer.Stop()
361356
}
362357

363358
/*
@@ -413,11 +408,62 @@ func isTextQuery(queryBody []byte) bool {
413408
return typeTXTClassIN > 0
414409
}
415410

411+
// TestServer contains the comprehensive test cases for both TCP and UDP DNS servers.
412+
func TestServer(dnsd *Daemon, t testingstub.T) {
413+
// Server should start within two seconds
414+
var stoppedNormally bool
415+
go func() {
416+
if err := dnsd.StartAndBlock(); err != nil {
417+
t.Fatal(err)
418+
}
419+
stoppedNormally = true
420+
}()
421+
time.Sleep(2 * time.Second)
422+
// Use go DNS client to verify that the server returns satisfactory response
423+
tcpResolver := &net.Resolver{
424+
PreferGo: true,
425+
StrictErrors: true,
426+
Dial: func(ctx context.Context, network, address string) (conn net.Conn, e error) {
427+
return net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", dnsd.TCPPort))
428+
},
429+
}
430+
testResolveNameAndBlackList(t, dnsd, tcpResolver)
431+
udpResolver := &net.Resolver{
432+
PreferGo: true,
433+
StrictErrors: true,
434+
Dial: func(ctx context.Context, network, address string) (conn net.Conn, e error) {
435+
return net.Dial("udp", fmt.Sprintf("127.0.0.1:%d", dnsd.UDPPort))
436+
},
437+
}
438+
testResolveNameAndBlackList(t, dnsd, udpResolver)
439+
// Daemon must stop in a second
440+
dnsd.Stop()
441+
time.Sleep(1 * time.Second)
442+
if !stoppedNormally {
443+
t.Fatal("did not stop")
444+
}
445+
// Repeatedly stopping the daemon should have no negative consequence
446+
dnsd.Stop()
447+
dnsd.Stop()
448+
}
449+
416450
/*
417451
testResolveNameAndBlackList is a common test case that tests name resolution of popular domain names as well as black
418452
list domain names.
419453
*/
420454
func testResolveNameAndBlackList(t testingstub.T, daemon *Daemon, resolver *net.Resolver) {
455+
if misc.HostIsWindows() {
456+
/*
457+
As of 2019-08, net.Resolver does not use custom dialer on Windows due to:
458+
- https://github.com/golang/go/issues/33621 (net: Resolver does not appear to use its dialer function on Windows)
459+
- https://github.com/golang/go/issues/29621 (net: DNS default resolver vs custom resolver behaviors)
460+
- https://github.com/golang/go/issues/33086 (net.Resolver Ignores Custom Dialer)
461+
- https://github.com/golang/go/issues/33097 (proposal: net: Enable built-in DNS stub resolver on Windows)
462+
*/
463+
t.Log("due to outstanding issues in Go, DNS server resolution routines cannot be tested on on Windows.")
464+
return
465+
}
466+
t.Helper()
421467
// Track and verify the last resolved name
422468
var lastResolvedName string
423469
daemon.processQueryTestCaseFunc = func(queryInput string) {

daemon/dnsd/dnsd_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,5 @@ func TestDNSD(t *testing.T) {
8282
t.Fatal(err)
8383
}
8484

85-
TestUDPQueries(&daemon, t)
86-
TestTCPQueries(&daemon, t)
85+
TestServer(&daemon, t)
8786
}

daemon/dnsd/tcp.go

+17-131
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,52 @@
11
package dnsd
22

33
import (
4-
"context"
5-
"fmt"
6-
"io/ioutil"
74
"math/rand"
85
"net"
9-
"strconv"
10-
"strings"
116
"time"
127

138
"github.com/HouzuoGuo/laitos/lalog"
149
"github.com/HouzuoGuo/laitos/toolbox/filter"
1510

1611
"github.com/HouzuoGuo/laitos/daemon/common"
1712
"github.com/HouzuoGuo/laitos/misc"
18-
"github.com/HouzuoGuo/laitos/testingstub"
1913
)
2014

21-
/*
22-
StartAndBlockTCP starts DNS daemon to listen on TCP port only, until daemon is told to stop.
23-
Daemon must have already been initialised prior to this call.
24-
*/
25-
func (daemon *Daemon) StartAndBlockTCP() error {
26-
listenAddr := net.JoinHostPort(daemon.Address, strconv.Itoa(daemon.TCPPort))
27-
listener, err := net.Listen("tcp", listenAddr)
28-
if err != nil {
29-
return err
30-
}
31-
defer func() {
32-
daemon.logger.MaybeMinorError(listener.Close())
33-
}()
34-
daemon.tcpListener = listener
35-
// Process incoming TCP DNS queries
36-
daemon.logger.Info("StartAndBlockTCP", listenAddr, nil, "going to listen for queries")
37-
for {
38-
if misc.EmergencyLockDown {
39-
daemon.logger.Warning("StartAndBlockTCP", "", misc.ErrEmergencyLockDown, "")
40-
return misc.ErrEmergencyLockDown
41-
}
42-
clientConn, err := listener.Accept()
43-
if err != nil {
44-
if strings.Contains(err.Error(), "closed") {
45-
return nil
46-
}
47-
return fmt.Errorf("DNSD.StartAndBlockTCP: failed to accept new connection - %v", err)
48-
}
49-
// Check address against rate limit and allowed IP prefixes
50-
clientIP := clientConn.RemoteAddr().(*net.TCPAddr).IP.String()
51-
if !daemon.rateLimit.Add(clientIP, true) {
52-
daemon.logger.MaybeMinorError(clientConn.Close())
53-
continue
54-
}
55-
go daemon.handleTCPQuery(clientConn)
56-
}
15+
// GetTCPStatsCollector returns stats collector for the TCP server of this daemon.
16+
func (daemon *Daemon) GetTCPStatsCollector() *misc.Stats {
17+
return common.DNSDStatsTCP
5718
}
5819

59-
func (daemon *Daemon) handleTCPQuery(clientConn net.Conn) {
60-
// Put query duration (including IO time) into statistics
61-
beginTimeNano := time.Now().UnixNano()
62-
defer func() {
63-
daemon.logger.MaybeMinorError(clientConn.Close())
64-
common.DNSDStatsTCP.Trigger(float64(time.Now().UnixNano() - beginTimeNano))
65-
}()
66-
clientIP := clientConn.RemoteAddr().(*net.TCPAddr).IP.String()
20+
// HandleConnection converses with a TCP DNS client.
21+
func (daemon *Daemon) HandleTCPConnection(logger lalog.Logger, ip string, conn *net.TCPConn) {
6722
// Read query length
68-
daemon.logger.MaybeMinorError(clientConn.SetDeadline(time.Now().Add(ClientTimeoutSec * time.Second)))
23+
logger.MaybeMinorError(conn.SetDeadline(time.Now().Add(ClientTimeoutSec * time.Second)))
6924
queryLen := make([]byte, 2)
70-
_, err := clientConn.Read(queryLen)
25+
_, err := conn.Read(queryLen)
7126
if err != nil {
72-
daemon.logger.Warning("handleTCPQuery", clientIP, err, "failed to read query length from client")
27+
logger.Warning("handleTCPQuery", ip, err, "failed to read query length from client")
7328
return
7429
}
7530
queryLenInteger := int(queryLen[0])*256 + int(queryLen[1])
7631
// Read query packet
7732
if queryLenInteger > MaxPacketSize || queryLenInteger < MinNameQuerySize {
78-
daemon.logger.Warning("handleTCPQuery", clientIP, nil, "invalid query length from client")
33+
logger.Warning("handleTCPQuery", ip, nil, "invalid query length from client")
7934
return
8035
}
8136
queryBody := make([]byte, queryLenInteger)
82-
_, err = clientConn.Read(queryBody)
37+
_, err = conn.Read(queryBody)
8338
if err != nil {
84-
daemon.logger.Warning("handleTCPQuery", clientIP, err, "failed to read query from client")
39+
logger.Warning("handleTCPQuery", ip, err, "failed to read query from client")
8540
return
8641
}
8742
// Formulate a response
8843
var respBody, respLen []byte
8944
if isTextQuery(queryBody) {
9045
// Handle toolbox command that arrives as a text query
91-
respLen, respBody = daemon.handleTCPTextQuery(clientIP, queryLen, queryBody)
46+
respLen, respBody = daemon.handleTCPTextQuery(ip, queryLen, queryBody)
9247
} else {
9348
// Handle other query types such as name query
94-
respLen, respBody = daemon.handleTCPNameOrOtherQuery(clientIP, queryLen, queryBody)
49+
respLen, respBody = daemon.handleTCPNameOrOtherQuery(ip, queryLen, queryBody)
9550
}
9651
// Close client connection in case there is no appropriate response
9752
if respBody == nil || len(respBody) < 2 {
@@ -100,11 +55,11 @@ func (daemon *Daemon) handleTCPQuery(clientConn net.Conn) {
10055
// Send response to the client, match transaction ID of original query, the deadline is shared with the read deadline above.
10156
respBody[0] = queryBody[0]
10257
respBody[1] = queryBody[1]
103-
if _, err := clientConn.Write(respLen); err != nil {
104-
daemon.logger.Warning("handleTCPQuery", clientIP, err, "failed to answer length to client")
58+
if _, err := conn.Write(respLen); err != nil {
59+
logger.Warning("handleTCPQuery", ip, err, "failed to answer length to client")
10560
return
106-
} else if _, err := clientConn.Write(respBody); err != nil {
107-
daemon.logger.Warning("handleTCPQuery", clientIP, err, "failed to answer to client")
61+
} else if _, err := conn.Write(respBody); err != nil {
62+
logger.Warning("handleTCPQuery", ip, err, "failed to answer to client")
10863
return
10964
}
11065
}
@@ -217,72 +172,3 @@ func (daemon *Daemon) handleTCPRecursiveQuery(clientIP string, queryLen, queryBo
217172
}
218173
return
219174
}
220-
221-
// Run unit tests on DNS TCP daemon. See TestDNSD_StartAndBlockTCP for daemon setup.
222-
func TestTCPQueries(dnsd *Daemon, t testingstub.T) {
223-
if misc.HostIsWindows() {
224-
// FIXME: fix this test case for Windows
225-
t.Log("FIXME: enable TestTCPQueries for Windows")
226-
return
227-
}
228-
// Prevent daemon from listening to UDP queries in this TCP test case
229-
udpListenPort := dnsd.UDPPort
230-
dnsd.UDPPort = 0
231-
defer func() {
232-
dnsd.UDPPort = udpListenPort
233-
}()
234-
// Server should start within two seconds
235-
var stoppedNormally bool
236-
go func() {
237-
if err := dnsd.StartAndBlock(); err != nil {
238-
t.Fatal(err)
239-
}
240-
stoppedNormally = true
241-
}()
242-
time.Sleep(2 * time.Second)
243-
// Use go DNS client to verify that the server returns satisfactory response
244-
resolver := &net.Resolver{
245-
PreferGo: true,
246-
StrictErrors: true,
247-
Dial: func(ctx context.Context, network, address string) (conn net.Conn, e error) {
248-
return net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", dnsd.TCPPort))
249-
},
250-
}
251-
testResolveNameAndBlackList(t, dnsd, resolver)
252-
// Try to flood the server and reach rate limit
253-
success := 0
254-
dnsd.blackList = map[string]struct{}{}
255-
for i := 0; i < 40; i++ {
256-
go func() {
257-
clientConn, err := net.Dial("tcp", "127.0.0.1:"+strconv.Itoa(dnsd.TCPPort))
258-
if err != nil {
259-
t.Fatal(err)
260-
}
261-
if err := clientConn.SetDeadline(time.Now().Add(3 * time.Second)); err != nil {
262-
lalog.DefaultLogger.MaybeMinorError(clientConn.Close())
263-
t.Fatal(err)
264-
}
265-
if _, err := clientConn.Write(githubComTCPQuery); err != nil {
266-
lalog.DefaultLogger.MaybeMinorError(clientConn.Close())
267-
t.Fatal(err)
268-
}
269-
resp, err := ioutil.ReadAll(clientConn)
270-
lalog.DefaultLogger.MaybeMinorError(clientConn.Close())
271-
if err == nil && len(resp) > 50 {
272-
success++
273-
}
274-
}()
275-
}
276-
// Wait for rate limit to reset and verify that regular name resolution resumes
277-
time.Sleep(RateLimitIntervalSec * time.Second)
278-
testResolveNameAndBlackList(t, dnsd, resolver)
279-
// Daemon must stop in a second
280-
dnsd.Stop()
281-
time.Sleep(1 * time.Second)
282-
if !stoppedNormally {
283-
t.Fatal("did not stop")
284-
}
285-
// Repeatedly stopping the daemon should have no negative consequence
286-
dnsd.Stop()
287-
dnsd.Stop()
288-
}

0 commit comments

Comments
 (0)