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