|
| 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