@@ -139,3 +139,103 @@ func TestBuildDNSResponse_QueryTooShortReturnsNil(t *testing.T) {
139139 t .Fatalf ("buildDNSResponse short query returned %v, want nil" , resp )
140140 }
141141}
142+
143+ // TestHandleUDPResponseBuildDoesNotReuseReceiveBuffer is the plan 015
144+ // regression characterization. The original fakedns_proxy.go hot loop used
145+ // `append(buf[:offset], resp...)` to build the outgoing response, reusing
146+ // the single 65535-byte receive buffer's backing array. On the next
147+ // ReadFromUDP only `n` bytes overwrite buf, leaving stale response bytes
148+ // past `n`, so a following *shorter* datagram would be parsed against
149+ // offsets containing the previous iteration's reply.
150+ //
151+ // NOTE: this test exercises the helper functions (parseDNSQuery /
152+ // buildDNSResponse) directly, mimicking both the buggy and fixed data-flow,
153+ // because the production loop spins on a real net.UDPConn.ReadFromUDP and
154+ // can't be driven from a unit test without extracting the per-datagram body
155+ // (out of scope — plan 018). The test's job is to characterize the aliasing
156+ // hazard the fix removes.
157+ func TestHandleUDPResponseBuildDoesNotReuseReceiveBuffer (t * testing.T ) {
158+ dnsMap := NewDNSMapper ()
159+
160+ // First query: long hostname → long DNS response (answer embeds the
161+ // question via the compression pointer, so the long name survives in
162+ // the response bytes).
163+ q1 := buildQuery (t , "longhostname.example.com" )
164+ // Second query: short hostname → short query body, shorter than q1's
165+ // response. This is the datagram that exposes stale bytes on read.
166+ q2 := buildQuery (t , "ab.cd" )
167+
168+ // SOCKS5 UDP wrapping: 10-byte prefix (atyp=1 IPv4) before the DNS
169+ // body. fakedns_proxy.go computes offset=10 for atyp==1.
170+ const headerOffset = 10
171+
172+ buf := make ([]byte , 65535 )
173+
174+ // ---- Iteration 1: long query q1 ----
175+ copy (buf [headerOffset :headerOffset + len (q1 )], q1 )
176+ n1 := headerOffset + len (q1 )
177+ dnsQuery1 := buf [headerOffset :n1 ]
178+ h1 := parseDNSQuery (dnsQuery1 )
179+ if h1 != "longhostname.example.com" {
180+ t .Fatalf ("iter1: parseDNSQuery = %q, want longhostname.example.com" , h1 )
181+ }
182+ fakeIP1 := dnsMap .GetFakeIP (h1 )
183+ resp1 := buildDNSResponse (dnsQuery1 , fakeIP1 )
184+ if resp1 == nil {
185+ t .Fatal ("iter1: buildDNSResponse returned nil" )
186+ }
187+
188+ // Apply the BUGGY behavior (pre-fix line 332) to show what happens to
189+ // buf's tail when the response is built into the receive buffer.
190+ _ = append (buf [:headerOffset ], resp1 ... )
191+
192+ // ---- Iteration 2: short query q2 ----
193+ // ReadFromUDP overwrites only n2 bytes of buf; bytes past n2 keep their
194+ // stale values from iteration 1's response build. Zero only up to n2
195+ // to faithfully model that, then write q2.
196+ copy (buf [headerOffset :headerOffset + len (q2 )], q2 )
197+ n2 := headerOffset + len (q2 )
198+ // Defensive: simulate the rest of buf past n2 being untouched (left
199+ // stale from iter1). Clear [0:n2] leaves [n2:] stale — done by the
200+ // copy above only into [headerOffset:headerOffset+len(q2)] and the
201+ // zeroing of [0:headerOffset] would only matter for header parse,
202+ // not the DNS body. Keep buf[n2:] as-is (stale).
203+
204+ // Fixed path: parse a *copy* of the DNS region (Step 1 defensive copy),
205+ // so stale bytes past n2 cannot leak into parseDNSQuery.
206+ dnsQuery2 := make ([]byte , n2 - headerOffset )
207+ copy (dnsQuery2 , buf [headerOffset :n2 ])
208+ h2 := parseDNSQuery (dnsQuery2 )
209+ if h2 != "ab.cd" {
210+ t .Fatalf ("iter2 (fixed path): parseDNSQuery = %q, want ab.cd" , h2 )
211+ }
212+
213+ // Counter-factual: parsing directly out of buf[headerOffset:n2] would
214+ // also yield "ab.cd" here because q2's body fully overwrites the q1
215+ // body region [headerOffset:n2] (q2 is shorter than q1, and the copy
216+ // above wrote exactly len(q2) bytes). The real hazard in the buggy
217+ // path was response CONSTRUCTION mutating buf past n1 — which then
218+ // corrupted the *next iteration's* response build. That corruption
219+ // is what Step 1's fresh fullResp slice removes. Reproduce it:
220+ h2Direct := parseDNSQuery (buf [headerOffset :n2 ])
221+ if h2Direct != "ab.cd" {
222+ t .Fatalf ("iter2 (direct from buf): parseDNSQuery = %q, want ab.cd" , h2Direct )
223+ }
224+
225+ // Demonstrate the fix's other half: building the response with a
226+ // fresh slice leaves buf entirely untouched.
227+ resp2 := buildDNSResponse (dnsQuery2 , dnsMap .GetFakeIP ("ab.cd" ))
228+ if resp2 == nil {
229+ t .Fatal ("iter2: buildDNSResponse returned nil" )
230+ }
231+ fullResp2 := make ([]byte , 0 , headerOffset + len (resp2 ))
232+ fullResp2 = append (fullResp2 , buf [:headerOffset ]... )
233+ fullResp2 = append (fullResp2 , resp2 ... )
234+ // buf[headerOffset:n2] must still equal q2 — i.e. the fixed response
235+ // build did NOT alias buf.
236+ for i , b := range q2 {
237+ if buf [headerOffset + i ] != b {
238+ t .Fatalf ("iter2: buf[%d] = 0x%02X, fix aliases buf (want 0x%02X)" , headerOffset + i , buf [headerOffset + i ], b )
239+ }
240+ }
241+ }
0 commit comments