Skip to content

Commit 871e8fb

Browse files
bavshin-f5pluknet
authored andcommitted
Tests: upstream configuration tests with re-resolvable servers.
Based on the NGINX Plus tests authored by Sergey Kandaurov.
1 parent eaac6eb commit 871e8fb

10 files changed

+3626
-0
lines changed

stream_upstream_resolve.t

Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
1+
#!/usr/bin/perl
2+
3+
# (C) Sergey Kandaurov
4+
# (C) Nginx, Inc.
5+
6+
# Stream tests for dynamic upstream configuration with re-resolvable servers.
7+
# Ensure that dns updates are properly applied.
8+
9+
###############################################################################
10+
11+
use warnings;
12+
use strict;
13+
14+
use Test::More;
15+
16+
use IO::Select;
17+
18+
BEGIN { use FindBin; chdir($FindBin::Bin); }
19+
20+
use lib 'lib';
21+
use Test::Nginx;
22+
use Test::Nginx::Stream qw/ stream /;
23+
24+
###############################################################################
25+
26+
select STDERR; $| = 1;
27+
select STDOUT; $| = 1;
28+
29+
my $t = Test::Nginx->new()->has(qw/stream stream_upstream_zone/);
30+
31+
$t->write_file_expand('nginx.conf', <<'EOF');
32+
33+
%%TEST_GLOBALS%%
34+
35+
daemon off;
36+
37+
events {
38+
}
39+
40+
stream {
41+
%%TEST_GLOBALS_STREAM%%
42+
43+
upstream u {
44+
zone z 1m;
45+
server example.net:%%PORT_8080%% resolve max_fails=0;
46+
}
47+
48+
# lower the retry timeout after empty reply
49+
resolver 127.0.0.1:%%PORT_8983_UDP%% valid=1s;
50+
# retry query shortly after DNS is started
51+
resolver_timeout 1s;
52+
53+
log_format test $upstream_addr;
54+
55+
server {
56+
listen 127.0.0.1:8082;
57+
proxy_pass u;
58+
access_log %%TESTDIR%%/cc.log test;
59+
proxy_next_upstream on;
60+
proxy_connect_timeout 50ms;
61+
}
62+
}
63+
64+
EOF
65+
66+
port(8084);
67+
68+
$t->run_daemon(\&dns_daemon, port(8983), $t)
69+
->waitforfile($t->testdir . '/' . port(8983));
70+
$t->try_run('no resolve in upstream server')->plan(11);
71+
72+
###############################################################################
73+
74+
my $p0 = port(8080);
75+
76+
update_name({A => '127.0.0.201'});
77+
stream('127.0.0.1:' . port(8082))->read();
78+
79+
# A changed
80+
81+
update_name({A => '127.0.0.202'});
82+
stream('127.0.0.1:' . port(8082))->read();
83+
84+
# 1 more A added
85+
86+
update_name({A => '127.0.0.201 127.0.0.202'});
87+
stream('127.0.0.1:' . port(8082))->read();
88+
89+
# 1 A removed, 2 AAAA added
90+
91+
update_name({A => '127.0.0.201', AAAA => 'fe80::1 fe80::2'});
92+
stream('127.0.0.1:' . port(8082))->read();
93+
94+
# all records removed
95+
96+
update_name();
97+
stream('127.0.0.1:' . port(8082))->read();
98+
99+
# A added after empty
100+
101+
update_name({A => '127.0.0.201'});
102+
stream('127.0.0.1:' . port(8082))->read();
103+
104+
# changed to CNAME
105+
106+
update_name({CNAME => 'alias'}, 4);
107+
stream('127.0.0.1:' . port(8082))->read();
108+
109+
# bad DNS reply should not affect existing upstream configuration
110+
111+
update_name({ERROR => 'SERVFAIL'});
112+
stream('127.0.0.1:' . port(8082))->read();
113+
114+
$t->stop();
115+
116+
Test::Nginx::log_core('||', $t->read_file('cc.log'));
117+
118+
open my $f, '<', "${\($t->testdir())}/cc.log" or die "Can't open cc.log: $!";
119+
my $line;
120+
121+
like($f->getline(), qr/127.0.0.201:$p0/, 'log - A');
122+
123+
# A changed
124+
125+
like($f->getline(), qr/127.0.0.202:$p0/, 'log - A changed');
126+
127+
# 1 more A added
128+
129+
$line = $f->getline();
130+
like($line, qr/127.0.0.201:$p0/, 'log - A A 1');
131+
like($line, qr/127.0.0.202:$p0/, 'log - A A 2');
132+
133+
# 1 A removed, 2 AAAA added
134+
135+
$line = $f->getline();
136+
like($line, qr/127.0.0.201:$p0/, 'log - A AAAA AAAA 1');
137+
like($line, qr/\[fe80::1\]:$p0/, 'log - A AAAA AAAA 2');
138+
like($line, qr/\[fe80::2\]:$p0/, 'log - A AAAA AAAA 3');
139+
140+
# all records removed
141+
142+
like($f->getline(), qr/^u$/, 'log - empty response');
143+
144+
# A added after empty
145+
146+
like($f->getline(), qr/127.0.0.201:$p0/, 'log - A added 1');
147+
148+
# changed to CNAME
149+
150+
like($f->getline(), qr/127.0.0.203:$p0/, 'log - CNAME 1');
151+
152+
# bad DNS reply should not affect existing upstream configuration
153+
154+
like($f->getline(), qr/127.0.0.203:$p0/, 'log - ERROR 1');
155+
156+
###############################################################################
157+
158+
sub update_name {
159+
my ($name, $plan) = @_;
160+
161+
$plan = 2 if !defined $plan;
162+
163+
sub sock {
164+
IO::Socket::INET->new(
165+
Proto => 'tcp',
166+
PeerAddr => '127.0.0.1:' . port(8084)
167+
)
168+
or die "Can't connect to nginx: $!\n";
169+
}
170+
171+
$name->{A} = '' unless $name->{A};
172+
$name->{AAAA} = '' unless $name->{AAAA};
173+
$name->{CNAME} = '' unless $name->{CNAME};
174+
$name->{ERROR} = '' unless $name->{ERROR};
175+
176+
my $req =<<EOF;
177+
GET / HTTP/1.0
178+
Host: localhost
179+
X-A: $name->{A}
180+
X-AAAA: $name->{AAAA}
181+
X-CNAME: $name->{CNAME}
182+
X-ERROR: $name->{ERROR}
183+
184+
EOF
185+
186+
my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/;
187+
for (1 .. 10) {
188+
my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/;
189+
190+
# let resolver cache expire to finish upstream reconfiguration
191+
select undef, undef, undef, 0.5;
192+
last unless ($gen + $plan > $gen2);
193+
}
194+
}
195+
196+
###############################################################################
197+
198+
sub reply_handler {
199+
my ($recv_data, $h) = @_;
200+
201+
my (@name, @rdata);
202+
203+
use constant NOERROR => 0;
204+
use constant SERVFAIL => 2;
205+
use constant NXDOMAIN => 3;
206+
207+
use constant A => 1;
208+
use constant CNAME => 5;
209+
use constant AAAA => 28;
210+
use constant DNAME => 39;
211+
use constant IN => 1;
212+
213+
# default values
214+
215+
my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 1);
216+
$h = {A => [ "127.0.0.201" ]} unless defined $h;
217+
218+
# decode name
219+
220+
my ($len, $offset) = (undef, 12);
221+
while (1) {
222+
$len = unpack("\@$offset C", $recv_data);
223+
last if $len == 0;
224+
$offset++;
225+
push @name, unpack("\@$offset A$len", $recv_data);
226+
$offset += $len;
227+
}
228+
229+
$offset -= 1;
230+
my ($id, $type, $class) = unpack("n x$offset n2", $recv_data);
231+
my $name = join('.', @name);
232+
233+
if ($h->{ERROR}) {
234+
$rcode = SERVFAIL;
235+
goto bad;
236+
}
237+
238+
if ($name eq 'example.net') {
239+
if ($type == A && $h->{A}) {
240+
map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}};
241+
}
242+
if ($type == AAAA && $h->{AAAA}) {
243+
map { push @rdata, rd_addr6($ttl, $_) } @{$h->{AAAA}};
244+
}
245+
my $cname = defined $h->{CNAME} ? $h->{CNAME} : 0;
246+
if ($cname) {
247+
push @rdata, pack("n3N nCa5n", 0xc00c, CNAME, IN, $ttl,
248+
8, 5, $cname, 0xc00c);
249+
}
250+
251+
} elsif ($name eq 'alias.example.net') {
252+
if ($type == A) {
253+
push @rdata, rd_addr($ttl, '127.0.0.203');
254+
}
255+
}
256+
257+
bad:
258+
259+
Test::Nginx::log_core('||', "DNS: $name $type $rcode");
260+
261+
$len = @name;
262+
pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata,
263+
0, 0, @name, $type, $class) . join('', @rdata);
264+
}
265+
266+
sub rd_addr {
267+
my ($ttl, $addr) = @_;
268+
269+
my $code = 'split(/\./, $addr)';
270+
271+
pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code);
272+
}
273+
274+
sub expand_ip6 {
275+
my ($addr) = @_;
276+
277+
substr ($addr, index($addr, "::"), 2) =
278+
join "0", map { ":" } (0 .. 8 - (split /:/, $addr) + 1);
279+
map { hex "0" x (4 - length $_) . "$_" } split /:/, $addr;
280+
}
281+
282+
sub rd_addr6 {
283+
my ($ttl, $addr) = @_;
284+
285+
pack 'n3N nn8', 0xc00c, AAAA, IN, $ttl, 16, expand_ip6($addr);
286+
}
287+
288+
sub dns_daemon {
289+
my ($port, $t) = @_;
290+
my ($data, $recv_data, $h);
291+
292+
my $socket = IO::Socket::INET->new(
293+
LocalAddr => '127.0.0.1',
294+
LocalPort => $port,
295+
Proto => 'udp',
296+
)
297+
or die "Can't create listening socket: $!\n";
298+
299+
my $control = IO::Socket::INET->new(
300+
Proto => 'tcp',
301+
LocalHost => '127.0.0.1:' . port(8084),
302+
Listen => 5,
303+
Reuse => 1
304+
)
305+
or die "Can't create listening socket: $!\n";
306+
307+
my $sel = IO::Select->new($socket, $control);
308+
309+
local $SIG{PIPE} = 'IGNORE';
310+
311+
# signal we are ready
312+
313+
open my $fh, '>', $t->testdir() . '/' . $port;
314+
close $fh;
315+
my $cnt = 0;
316+
317+
while (my @ready = $sel->can_read) {
318+
foreach my $fh (@ready) {
319+
if ($control == $fh) {
320+
my $new = $fh->accept;
321+
$new->autoflush(1);
322+
$sel->add($new);
323+
324+
} elsif ($socket == $fh) {
325+
$fh->recv($recv_data, 65536);
326+
$data = reply_handler($recv_data, $h);
327+
$fh->send($data);
328+
$cnt++;
329+
330+
} else {
331+
$h = process_name($fh, $cnt);
332+
$sel->remove($fh);
333+
$fh->close;
334+
}
335+
}
336+
}
337+
}
338+
339+
# parse dns update
340+
341+
sub process_name {
342+
my ($client, $cnt) = @_;
343+
my $port = $client->sockport();
344+
345+
my $headers = '';
346+
my $uri = '';
347+
my %h;
348+
349+
while (<$client>) {
350+
$headers .= $_;
351+
last if (/^\x0d?\x0a?$/);
352+
}
353+
return 1 if $headers eq '';
354+
355+
$uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i;
356+
return 1 if $uri eq '';
357+
358+
$headers =~ /X-A: (.*)$/m;
359+
map { push @{$h{A}}, $_ } split(/ /, $1);
360+
$headers =~ /X-AAAA: (.*)$/m;
361+
map { push @{$h{AAAA}}, $_ } split(/ /, $1);
362+
$headers =~ /X-CNAME: (.*)$/m;
363+
$h{CNAME} = $1;
364+
$headers =~ /X-ERROR: (.*)$/m;
365+
$h{ERROR} = $1;
366+
367+
Test::Nginx::log_core('||', "$port: response, 200");
368+
print $client <<EOF;
369+
HTTP/1.1 200 OK
370+
Connection: close
371+
X-Gen: $cnt
372+
373+
OK
374+
EOF
375+
376+
return \%h;
377+
}
378+
379+
###############################################################################

0 commit comments

Comments
 (0)