blob: cb4f618a913713e34f495efdabb8dbe9eeb69353 [file] [log] [blame]
Jan Schär75ea9f42024-07-29 17:01:41 +02001package proxy
2
3// Taken and modified from CoreDNS, under Apache 2.0.
4
5import (
6 "crypto/tls"
7 "sort"
8 "time"
9
10 "github.com/miekg/dns"
11)
12
13// a persistConn hold the dns.Conn and the last used time.
14type persistConn struct {
15 c *dns.Conn
16 used time.Time
17}
18
19// Transport hold the persistent cache.
20type Transport struct {
21 avgDialTime int64 // kind of average time of dial time
22 conns [typeTotalCount][]*persistConn // Buckets for udp, tcp and tcp-tls.
23 expire time.Duration // After this duration a connection is expired.
24 addr string
25 tlsConfig *tls.Config
26
27 dial chan string
28 yield chan *persistConn
29 ret chan *persistConn
30 stop chan bool
31}
32
33func newTransport(addr string) *Transport {
34 t := &Transport{
35 avgDialTime: int64(maxDialTimeout / 2),
36 conns: [typeTotalCount][]*persistConn{},
37 expire: defaultExpire,
38 addr: addr,
39 dial: make(chan string),
40 yield: make(chan *persistConn),
41 ret: make(chan *persistConn),
42 stop: make(chan bool),
43 }
44 return t
45}
46
47// connManager manages the persistent connection cache for UDP and TCP.
48func (t *Transport) connManager() {
49 ticker := time.NewTicker(defaultExpire)
50 defer ticker.Stop()
51Wait:
52 for {
53 select {
54 case proto := <-t.dial:
55 transtype := stringToTransportType(proto)
56 // take the last used conn - complexity O(1)
57 if stack := t.conns[transtype]; len(stack) > 0 {
58 pc := stack[len(stack)-1]
59 if time.Since(pc.used) < t.expire {
60 // Found one, remove from pool and return this conn.
61 t.conns[transtype] = stack[:len(stack)-1]
62 t.ret <- pc
63 continue Wait
64 }
65 // clear entire cache if the last conn is expired
66 t.conns[transtype] = nil
67 // now, the connections being passed to closeConns() are not reachable from
68 // transport methods anymore. So, it's safe to close them in a separate goroutine
69 go closeConns(stack)
70 }
71 t.ret <- nil
72
73 case pc := <-t.yield:
74 transtype := t.transportTypeFromConn(pc)
75 t.conns[transtype] = append(t.conns[transtype], pc)
76
77 case <-ticker.C:
78 t.cleanup(false)
79
80 case <-t.stop:
81 t.cleanup(true)
82 close(t.ret)
83 return
84 }
85 }
86}
87
88// closeConns closes connections.
89func closeConns(conns []*persistConn) {
90 for _, pc := range conns {
91 pc.c.Close()
92 }
93}
94
95// cleanup removes connections from cache.
96func (t *Transport) cleanup(all bool) {
97 staleTime := time.Now().Add(-t.expire)
98 for transtype, stack := range t.conns {
99 if len(stack) == 0 {
100 continue
101 }
102 if all {
103 t.conns[transtype] = nil
104 // now, the connections being passed to closeConns() are not reachable from
105 // transport methods anymore. So, it's safe to close them in a separate goroutine
106 go closeConns(stack)
107 continue
108 }
109 if stack[0].used.After(staleTime) {
110 continue
111 }
112
113 // connections in stack are sorted by "used"
114 good := sort.Search(len(stack), func(i int) bool {
115 return stack[i].used.After(staleTime)
116 })
117 t.conns[transtype] = stack[good:]
118 // now, the connections being passed to closeConns() are not reachable from
119 // transport methods anymore. So, it's safe to close them in a separate goroutine
120 go closeConns(stack[:good])
121 }
122}
123
124// It is hard to pin a value to this, the import thing is to no block forever,
125// losing at cached connection is not terrible.
126const yieldTimeout = 25 * time.Millisecond
127
128// Yield returns the connection to transport for reuse.
129func (t *Transport) Yield(pc *persistConn) {
130 pc.used = time.Now() // update used time
131
132 // Make this non-blocking, because in the case of a very busy forwarder
133 // we will *block* on this yield. This blocks the outer go-routine and stuff
134 // will just pile up. We timeout when the send fails to as returning
135 // these connection is an optimization anyway.
136 select {
137 case t.yield <- pc:
138 return
139 case <-time.After(yieldTimeout):
140 return
141 }
142}
143
144// Start starts the transport's connection manager.
145func (t *Transport) Start() { go t.connManager() }
146
147// Stop stops the transport's connection manager.
148func (t *Transport) Stop() { close(t.stop) }
149
150// SetExpire sets the connection expire time in transport.
151func (t *Transport) SetExpire(expire time.Duration) { t.expire = expire }
152
153// SetTLSConfig sets the TLS config in transport.
154func (t *Transport) SetTLSConfig(cfg *tls.Config) { t.tlsConfig = cfg }
155
156const (
157 defaultExpire = 10 * time.Second
158 minDialTimeout = 1 * time.Second
159 maxDialTimeout = 30 * time.Second
160)