# HG changeset patch # User Aleksei Bavshin <a.bavs...@nginx.com> # Date 1712098324 25200 # Tue Apr 02 15:52:04 2024 -0700 # Node ID cf291bf98576d5ddd36ead2b7eacc0808120e66b # Parent b396d0c2c62c796c76921dcf20960f8ba2515aba Tests: upstream configuration tests with re-resolvable servers.
Based on the NGINX Plus tests authored by Sergey Kandaurov. diff --git a/stream_upstream_resolve.t b/stream_upstream_resolve.t new file mode 100644 --- /dev/null +++ b/stream_upstream_resolve.t @@ -0,0 +1,379 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Stream tests for dynamic upstream configuration with re-resolvable servers. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + upstream u { + zone z 1m; + server example.net:%%PORT_8080%% max_fails=0 resolve; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8983_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + log_format test $upstream_addr; + + server { + listen 127.0.0.1:8082; + proxy_pass u; + access_log %%TESTDIR%%/cc.log test; + proxy_next_upstream on; + proxy_connect_timeout 50ms; + } +} + +EOF + +port(8084); + +$t->run_daemon(\&dns_daemon, port(8983), $t) + ->waitforfile($t->testdir . '/' . port(8983)); +$t->try_run('no resolve in upstream server')->plan(11); + +############################################################################### + +my $p0 = port(8080); + +update_name({A => '127.0.0.201'}); +stream('127.0.0.1:' . port(8082))->read(); + +# A changed + +update_name({A => '127.0.0.202'}); +stream('127.0.0.1:' . port(8082))->read(); + +# 1 more A added + +update_name({A => '127.0.0.201 127.0.0.202'}); +stream('127.0.0.1:' . port(8082))->read(); + +# 1 A removed, 2 AAAA added + +update_name({A => '127.0.0.201', AAAA => 'fe80::1 fe80::2'}); +stream('127.0.0.1:' . port(8082))->read(); + +# all records removed + +update_name(); +stream('127.0.0.1:' . port(8082))->read(); + +# A added after empty + +update_name({A => '127.0.0.201'}); +stream('127.0.0.1:' . port(8082))->read(); + +# changed to CNAME + +update_name({CNAME => 'alias'}, 4); +stream('127.0.0.1:' . port(8082))->read(); + +# bad DNS reply should not affect existing upstream configuration + +update_name({ERROR => 'SERVFAIL'}); +stream('127.0.0.1:' . port(8082))->read(); + +$t->stop(); + +Test::Nginx::log_core('||', $t->read_file('cc.log')); + +open my $f, '<', "${\($t->testdir())}/cc.log" or die "Can't open cc.log: $!"; +my $line; + +like($f->getline(), qr/127.0.0.201:$p0/, 'log - A'); + +# A changed + +like($f->getline(), qr/127.0.0.202:$p0/, 'log - A changed'); + +# 1 more A added + +$line = $f->getline(); +like($line, qr/127.0.0.201:$p0/, 'log - A A 1'); +like($line, qr/127.0.0.202:$p0/, 'log - A A 2'); + +# 1 A removed, 2 AAAA added + +$line = $f->getline(); +like($line, qr/127.0.0.201:$p0/, 'log - A AAAA AAAA 1'); +like($line, qr/\[fe80::1\]:$p0/, 'log - A AAAA AAAA 2'); +like($line, qr/\[fe80::2\]:$p0/, 'log - A AAAA AAAA 3'); + +# all records removed + +like($f->getline(), qr/^u$/, 'log - empty response'); + +# A added after empty + +like($f->getline(), qr/127.0.0.201:$p0/, 'log - A added 1'); + +# changed to CNAME + +like($f->getline(), qr/127.0.0.203:$p0/, 'log - CNAME 1'); + +# bad DNS reply should not affect existing upstream configuration + +like($f->getline(), qr/127.0.0.203:$p0/, 'log - ERROR 1'); + +############################################################################### + +sub update_name { + my ($name, $plan) = @_; + + $plan = 2 if !defined $plan; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8084) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{AAAA} = '' unless $name->{AAAA}; + $name->{CNAME} = '' unless $name->{CNAME}; + $name->{ERROR} = '' unless $name->{ERROR}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-AAAA: $name->{AAAA} +X-CNAME: $name->{CNAME} +X-ERROR: $name->{ERROR} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant CNAME => 5; + use constant AAAA => 28; + use constant DNAME => 39; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 1); + $h = {A => [ "127.0.0.201" ]} unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR}) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq 'example.net') { + if ($type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + } + if ($type == AAAA && $h->{AAAA}) { + map { push @rdata, rd_addr6($ttl, $_) } @{$h->{AAAA}}; + } + my $cname = defined $h->{CNAME} ? $h->{CNAME} : 0; + if ($cname) { + push @rdata, pack("n3N nCa5n", 0xc00c, CNAME, IN, $ttl, + 8, 5, $cname, 0xc00c); + } + + } elsif ($name eq 'alias.example.net') { + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.203'); + } + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub expand_ip6 { + my ($addr) = @_; + + substr ($addr, index($addr, "::"), 2) = + join "0", map { ":" } (0 .. 8 - (split /:/, $addr) + 1); + map { hex "0" x (4 - length $_) . "$_" } split /:/, $addr; +} + +sub rd_addr6 { + my ($ttl, $addr) = @_; + + pack 'n3N nn8', 0xc00c, AAAA, IN, $ttl, 16, expand_ip6($addr); +} + +sub dns_daemon { + my ($port, $t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1:' . port(8084), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h); + $fh->send($data); + $cnt++; + + } else { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-AAAA: (.*)$/m; + map { push @{$h{AAAA}}, $_ } split(/ /, $1); + $headers =~ /X-CNAME: (.*)$/m; + $h{CNAME} = $1; + $headers =~ /X-ERROR: (.*)$/m; + $h{ERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/stream_upstream_resolve_reload.t b/stream_upstream_resolve_reload.t new file mode 100644 --- /dev/null +++ b/stream_upstream_resolve_reload.t @@ -0,0 +1,336 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Stream tests for dynamic upstream configuration with re-resolvable servers. +# Ensure that upstream configuration is inherited on reload. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new()->has(qw/stream stream_upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + upstream u { + zone z 1m; + server example.net:%%PORT_8081%% resolve; + } + + upstream u2 { + zone z 1m; + server 127.0.0.203:%%PORT_8081%% max_fails=0; + server example.net:%%PORT_8081%% resolve max_fails=0; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8980_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + log_format test $upstream_addr; + + server { + listen 127.0.0.1:8082; + proxy_pass u; + proxy_connect_timeout 50ms; + access_log %%TESTDIR%%/cc.log test; + } + + server { + listen 127.0.0.1:8083; + proxy_pass u2; + proxy_connect_timeout 50ms; + access_log %%TESTDIR%%/cc2.log test; + } +} + +EOF + +$t->run_daemon(\&dns_daemon, $t)->waitforfile($t->testdir . '/' . port(8980)); +$t->try_run('no resolve in upstream server')->plan(9); + +############################################################################### + +my $p = port(8081); + +update_name({A => '127.0.0.201'}); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8083))->read(); + +update_name({ERROR => 'SERVFAIL'}, 0); + +my $conf = $t->read_file('nginx.conf'); +$conf =~ s/$p/port(8082)/gmse; +$t->write_file('nginx.conf', $conf); + +$t->reload(); +waitforworker($t); + +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8083))->read(); + +update_name({A => '127.0.0.202'}); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8083))->read(); + +$t->stop(); + +Test::Nginx::log_core('||', $t->read_file('cc.log')); + +open my $f, '<', "${\($t->testdir())}/cc.log" or die "Can't open cc.log: $!"; + +like($f->getline(), qr/127.0.0.201:$p/, 'log - before'); +like($f->getline(), qr/127.0.0.201:$p/, 'log - before 2'); + +$p = port(8082); + +like($f->getline(), qr/127.0.0.201:$p/, 'log - preresolve'); +like($f->getline(), qr/127.0.0.201:$p/, 'log - preresolve 2'); + +like($f->getline(), qr/127.0.0.202:$p/, 'log - update'); +like($f->getline(), qr/127.0.0.202:$p/, 'log - update 2'); + +Test::Nginx::log_core('||', $t->read_file('cc2.log')); + +$p = port(8081); + +open $f, '<', "${\($t->testdir())}/cc2.log" or die "Can't open cc2.log: $!"; + +like($f->getline(), qr/127.0.0.(201:$p, 127.0.0.203|203:$p, 127.0.0.201):$p/, + 'log many - before'); + +$p = port(8082); + +like($f->getline(), qr/127.0.0.(201:$p, 127.0.0.203|203:$p, 127.0.0.201):$p/, + 'log many - preresolve'); + +like($f->getline(), qr/127.0.0.(202:$p, 127.0.0.203|203:$p, 127.0.0.202):$p/, + 'log many - update'); + +############################################################################### + +sub waitforworker { + my ($t) = @_; + + for (1 .. 30) { + last if $t->read_file('error.log') =~ /exited with code/; + select undef, undef, undef, 0.2; + } +} + +sub update_name { + my ($name, $plan) = @_; + + $plan = 2 if !defined $plan; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8081) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{ERROR} = '' unless $name->{ERROR}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-ERROR: $name->{ERROR} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 1); + $h = {A => [ "127.0.0.201" ]} unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR}) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq 'example.net' && $type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => port(8980), + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => "127.0.0.1:" . port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . port(8980); + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h); + $fh->send($data); + $cnt++; + + } else { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-ERROR: (.*)$/m; + $h{ERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/stream_upstream_resolver.t b/stream_upstream_resolver.t new file mode 100644 --- /dev/null +++ b/stream_upstream_resolver.t @@ -0,0 +1,281 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for re-resolvable servers with resolver in stream upstream. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + resolver 127.0.0.1:%%PORT_8980_UDP%%; + + upstream u { + zone z 1m; + server example.net:%%PORT_8080%% resolve; + } + + upstream u1 { + zone z 1m; + server example.net:%%PORT_8080%% resolve; + resolver 127.0.0.1:%%PORT_8981_UDP%%; + } + + upstream u2 { + zone z 1m; + server example.net:%%PORT_8080%% resolve; + resolver 127.0.0.1:%%PORT_8982_UDP%%; + resolver_timeout 200s; # for coverage + } + + log_format test $upstream_addr; + + proxy_connect_timeout 50ms; + + server { + listen 127.0.0.1:8081; + proxy_pass u; + + access_log %%TESTDIR%%/access.log test; + } + + server { + listen 127.0.0.1:8082; + proxy_pass u1; + + access_log %%TESTDIR%%/access1.log test; + } + + server { + listen 127.0.0.1:8083; + proxy_pass u2; + + access_log %%TESTDIR%%/access2.log test; + } +} + +EOF + +$t->run_daemon(\&dns_daemon, $t, port($_), port($_ + 10)) for (8980 .. 8982); +$t->waitforfile($t->testdir . '/' . port($_)) for (8980 .. 8982); + +$t->try_run('no resolver in upstream')->plan(6); + +############################################################################### + +ok(waitfordns(8980), 'resolved'); +ok(waitfordns(8981), 'resolved in upstream 1'); +ok(waitfordns(8982), 'resolved in upstream 2'); + +stream('127.0.0.1:' . port(8081))->read(); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8083))->read(); + +$t->stop(); + +like($t->read_file('access.log'), qr/127.0.0.200/, 'resolver'); +like($t->read_file('access1.log'), qr/127.0.0.201/, 'resolver upstream 1'); +like($t->read_file('access2.log'), qr/127.0.0.202/, 'resolver upstream 2'); + +############################################################################### + +sub waitfordns { + my ($port, $plan) = @_; + + $plan = 1 if !defined $plan; + + sub sock { + my ($port) = @_; + + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port($port + 10) + ) + or die "Can't connect to dns control socket: $!\n"; + } + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost + +EOF + + for (1 .. 10) { + my ($gen) = http($req, socket => sock($port)) =~ /X-Gen: (\d+)/; + select undef, undef, undef, 0.5; + return 1 if $gen >= $plan; + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $port) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 1); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + if ($name eq 'example.net' && $type == A) { + if ($port == port(8980)) { + push @rdata, rd_addr($ttl, "127.0.0.200"); + } + + if ($port == port(8981)) { + push @rdata, rd_addr($ttl, "127.0.0.201"); + } + + if ($port == port(8982)) { + push @rdata, rd_addr($ttl, "127.0.0.202"); + } + } + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($t, $port, $control_port) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1', + LocalPort => $control_port, + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port); + $fh->send($data); + $cnt++; + + } else { + process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/stream_upstream_service.t b/stream_upstream_service.t new file mode 100644 --- /dev/null +++ b/stream_upstream_service.t @@ -0,0 +1,544 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Stream tests for dynamic upstream configuration with service (SRV) feature. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/stream stream_upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + upstream u { + zone z 1m; + server example.net max_fails=0 resolve service=http; + } + + upstream u2 { + zone z2 1m; + server example.net max_fails=0 resolve service=_http._tcp; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8981_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + log_format test $upstream_addr; + + server { + listen 127.0.0.1:8081; + proxy_pass u; + proxy_next_upstream on; + proxy_connect_timeout 50ms; + access_log %%TESTDIR%%/cc.log test; + } + + server { + listen 127.0.0.1:8082; + proxy_pass u2; + proxy_next_upstream on; + proxy_connect_timeout 50ms; + access_log %%TESTDIR%%/cc.log test; + } +} + +EOF + +port(8080); +port(8084); + +$t->write_file('t', ''); + +$t->run_daemon(\&dns_daemon, $t)->waitforfile($t->testdir . '/' . port(8981)); +port(8981, socket => 1)->close(); +$t->try_run('no resolve in upstream server')->plan(20); + +############################################################################### + +my ($p0, $p2, $p3) = (port(8080), port(8082), port(8083)); + +update_name({A => '127.0.0.201', SRV => "1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# fully specified service + +stream('127.0.0.1:' . port(8082))->read(); + +# A changed + +update_name({A => '127.0.0.202', SRV => "1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# 1 more A added + +update_name({A => '127.0.0.201 127.0.0.202', SRV => "1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# 1 A removed, 2 AAAA added + +update_name({A => '127.0.0.201', AAAA => 'fe80::1 fe80::2', + SRV => "1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# all records removed + +update_name({SRV => "1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# all SRV records removed + +update_name(); +stream('127.0.0.1:' . port(8081))->read(); + +# A added after empty + +update_name({A => '127.0.0.201', SRV => "1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# SRV changed its weight + +update_name({A => '127.0.0.201', SRV => "1 6 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# changed to CNAME + +update_name({CNAME => 'alias'}, 2, 2); +stream('127.0.0.1:' . port(8081))->read(); + +# bad SRV reply should not affect existing upstream configuration + +update_name({CNAME => 'alias', ERROR => 'SERVFAIL'}, 1, 0); +stream('127.0.0.1:' . port(8081))->read(); +update_name({ERROR => ''}, 1, 0); + +# 2 equal SRV RR + +update_name({A => '127.0.0.201', + SRV => "1 5 $p0 example.net;1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +# all equal records removed + +update_name(); +stream('127.0.0.1:' . port(8081))->read(); + +# 2 different SRV RR + +update_name({A => '127.0.0.201', + SRV => "1 5 $p2 example.net;2 6 $p3 alias.example.net"}, 1, 2); +stream('127.0.0.1:' . port(8081))->read(); + +# all different records removed + +update_name(); +stream('127.0.0.1:' . port(8081))->read(); + +# bad subordinate reply should not affect existing upstream configuration + +update_name({A => '127.0.0.201', + SRV => "1 5 $p0 example.net;1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +update_name({A => '127.0.0.201', SERROR => 'SERVFAIL', + SRV => "1 5 $p0 example.net;1 5 $p0 example.net"}); +stream('127.0.0.1:' . port(8081))->read(); + +$t->stop(); + +Test::Nginx::log_core('||', $t->read_file('cc.log')); + +open my $f, '<', "${\($t->testdir())}/cc.log" or die "Can't open cc.log: $!"; +my $line; + +like($f->getline(), qr/127.0.0.201:$p0/, 'log - A'); + +# fully specified service + +like($f->getline(), qr/127.0.0.201:$p0/, 'log - A full'); + +# A changed + +like($f->getline(), qr/127.0.0.202:$p0/, 'log - A changed'); + +# 1 more A added + +$line = $f->getline(); +like($line, qr/127.0.0.201:$p0/, 'log - A A 1'); +like($line, qr/127.0.0.202:$p0/, 'log - A A 2'); + +# 1 A removed, 2 AAAA added + +$line = $f->getline(); +like($line, qr/127.0.0.201:$p0/, 'log - A AAAA AAAA 1'); +like($line, qr/\[fe80::1\]:$p0/, 'log - A AAAA AAAA 2'); +like($line, qr/\[fe80::2\]:$p0/, 'log - A AAAA AAAA 3'); + +# all records removed + +like($f->getline(), qr/^u$/, 'log - empty response'); + +# all SRV records removed + +like($f->getline(), qr/^u$/, 'log - empty response'); + +# A added after empty + +like($f->getline(), qr/127.0.0.201:$p0/, 'log - A added 1'); + +# SRV changed its weight + +like($f->getline(), qr/127.0.0.201:$p0/, 'log - SRV weight'); + +# changed to CNAME + +like($f->getline(), qr/127.0.0.203:$p0/, 'log - CNAME'); + +# bad SRV reply should not affect existing upstream configuration + +like($f->getline(), qr/127.0.0.203:$p0/, 'log - ERROR'); + +# 2 equal SRV RR + +like($f->getline(), qr/127.0.0.201:$p0, 127.0.0.201:$p0/, 'log - SRV same'); + +# all equal records removed + +like($f->getline(), qr/^u$/, 'log - SRV same removed'); + +# 2 different SRV RR + +$line = $f->getline(); +like($line, qr/127.0.0.201:$p2, 127.0.0.203:$p3/, 'log - SRV diff'); + +# all different records removed + +like($f->getline(), qr/^u$/, 'log - SRV diff removed'); + +# bad subordinate reply should not affect existing upstream configuration + +like($f->getline(), qr/, /, 'log - subordinate good'); +like($f->getline(), qr/, /, 'log - subordinate error'); + +############################################################################### + +sub update_name { + my ($name, $plan, $plan6) = @_; + + $plan = 1, $plan6 = 0 if !defined $name; + $plan = $plan6 = 1 if !defined $plan; + $plan += $plan6 + $plan6; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8084) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{AAAA} = '' unless $name->{AAAA}; + $name->{CNAME} = '' unless $name->{CNAME}; + $name->{ERROR} = '' unless $name->{ERROR}; + $name->{SERROR} = '' unless $name->{SERROR}; + $name->{SRV} = '' unless $name->{SRV}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-AAAA: $name->{AAAA} +X-CNAME: $name->{CNAME} +X-ERROR: $name->{ERROR} +X-SERROR: $name->{SERROR} +X-SRV: $name->{SRV} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h, $cnt, $tcp) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant FORMERR => 1; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant CNAME => 5; + use constant AAAA => 28; + use constant SRV => 33; + + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl, $port) = (0x8180, NOERROR, 3600, port(8080)); + $h = {A => [ "127.0.0.1" ], SRV => [ "1 5 $port example.net" ]} + unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR} && $type == SRV) { + $rcode = SERVFAIL; + goto bad; + } + + # subordinate error + + if ($h->{SERROR} && $type != SRV) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq '_http._tcp.example.net') { + if ($type == SRV && $h->{SRV}) { + map { push @rdata, rd_srv($ttl, (split ' ', $_)) } + @{$h->{SRV}}; + } + + my $cname = defined $h->{CNAME} ? $h->{CNAME} : 0; + if ($cname) { + push @rdata, pack("n3N nCa5n", 0xc00c, CNAME, IN, $ttl, + 8, 5, "alias", 0xc00c + length("_http._tcp ")); + } + + } elsif ($name eq '_http._tcp.trunc.example.net' && $type == SRV) { + push @rdata, $tcp + ? rd_srv($ttl, 1, 1, $port, 'tcp.example.net') + : rd_srv($ttl, 1, 1, $port, 'example.net'); + + $hdr |= 0x0300 if $name eq '_http._tcp.trunc.example.net' + and !$tcp; + + } elsif ($name eq 'example.net' || $name eq 'tcp.example.net') { + if ($type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + } + if ($type == AAAA && $h->{AAAA}) { + map { push @rdata, rd_addr6($ttl, $_) } @{$h->{AAAA}}; + } + my $cname = defined $h->{CNAME} ? $h->{CNAME} : 0; + if ($cname) { + push @rdata, pack("n3N nCa5n", 0xc00c, CNAME, IN, $ttl, + 8, 5, $cname, 0xc00c); + } + + } elsif ($name eq 'alias.example.net') { + if ($type == SRV) { + push @rdata, rd_srv($ttl, 1, 5, $port, 'example.net'); + } + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.203'); + } + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $$cnt++ if $type == SRV || keys %$h; + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_srv { + my ($ttl, $pri, $w, $port, $name) = @_; + my @rdname = split /\./, $name; + my $rdlen = length(join '', @rdname) + @rdname + 7; # pri w port x + + pack 'n3N n n3 (C/a*)* x', + 0xc00c, SRV, IN, $ttl, $rdlen, $pri, $w, $port, @rdname; +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub expand_ip6 { + my ($addr) = @_; + + substr ($addr, index($addr, "::"), 2) = + join "0", map { ":" } (0 .. 8 - (split /:/, $addr) + 1); + map { hex "0" x (4 - length $_) . "$_" } split /:/, $addr; +} + +sub rd_addr6 { + my ($ttl, $addr) = @_; + + pack 'n3N nn8', 0xc00c, AAAA, IN, $ttl, 16, expand_ip6($addr); +} + +sub dns_daemon { + my ($t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => port(8981), + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1:' . port(8084), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $tcp = port(8981, socket => 1); + my $sel = IO::Select->new($socket, $control, $tcp); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . port(8981); + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh || $tcp == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h, \$cnt); + $fh->send($data); + + } elsif ($fh->sockport() == port(8084)) { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + + } elsif ($fh->sockport() == port(8981)) { + $fh->recv($recv_data, 65536); + unless (length $recv_data) { + $sel->remove($fh); + $fh->close; + next; + } + +again: + my $len = unpack("n", $recv_data); + my $data = substr $recv_data, 2, $len; + $data = reply_handler($data, $h, \$cnt, 1); + $data = pack("n", length $data) . $data; + $fh->send($data); + $recv_data = substr $recv_data, 2 + $len; + goto again if length $recv_data; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-AAAA: (.*)$/m; + map { push @{$h{AAAA}}, $_ } split(/ /, $1); + $headers =~ /X-SRV: (.*)$/m; + map { push @{$h{SRV}}, $_ } split(/;/, $1); + $headers =~ /X-CNAME: (.+)$/m and $h{CNAME} = $1; + $headers =~ /X-ERROR: (.+)$/m and $h{ERROR} = $1; + $headers =~ /X-SERROR: (.+)$/m and $h{SERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/stream_upstream_service_reload.t b/stream_upstream_service_reload.t new file mode 100644 --- /dev/null +++ b/stream_upstream_service_reload.t @@ -0,0 +1,317 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Stream tests for dynamic upstream configuration with service (SRV) feature. +# Ensure that upstream configuration is inherited on reload. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::Stream qw/ stream /; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new()->has(qw/stream stream_upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +stream { + %%TEST_GLOBALS_STREAM%% + + upstream u { + zone z 1m; + server example.net resolve service=http; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8980_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + log_format test $upstream_addr; + + server { + listen 127.0.0.1:8082; + proxy_pass u; + proxy_connect_timeout 50ms; + access_log %%TESTDIR%%/cc.log test; + } +} + +EOF + +port(8081); + +$t->run_daemon(\&dns_daemon, $t)->waitforfile($t->testdir . '/' . port(8980)); +$t->try_run('no resolve in upstream server')->plan(6); + +############################################################################### + +update_name({A => '127.0.0.201', SRV => "1 5 8080 example.net"}); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8082))->read(); + +update_name({ERROR => 'SERVFAIL'}, 0); + +$t->reload(); +waitforworker($t); + +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8082))->read(); + +update_name({A => '127.0.0.202', SRV => "1 5 8080 example.net"}); +stream('127.0.0.1:' . port(8082))->read(); +stream('127.0.0.1:' . port(8082))->read(); + +$t->stop(); + +Test::Nginx::log_core('||', $t->read_file('cc.log')); + +open my $f, '<', "${\($t->testdir())}/cc.log" or die "Can't open cc.log: $!"; + +like($f->getline(), qr/127.0.0.201:8080/, 'log - before'); +like($f->getline(), qr/127.0.0.201:8080/, 'log - before 2'); + +like($f->getline(), qr/127.0.0.201:8080/, 'log - preresolve'); +like($f->getline(), qr/127.0.0.201:8080/, 'log - preresolve 2'); + +like($f->getline(), qr/127.0.0.202:8080/, 'log - update'); +like($f->getline(), qr/127.0.0.202:8080/, 'log - update 2'); + +############################################################################### + +sub waitforworker { + my ($t) = @_; + + for (1 .. 30) { + last if $t->read_file('error.log') =~ /exited with code/; + select undef, undef, undef, 0.2; + } +} + +sub update_name { + my ($name, $plan) = @_; + + $plan = 3 if !defined $plan; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8081) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{ERROR} = '' unless $name->{ERROR}; + $name->{SRV} = '' unless $name->{SRV}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-ERROR: $name->{ERROR} +X-SRV: $name->{SRV} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant SRV => 33; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl, $port) = (0x8180, NOERROR, 3600, port(8080)); + $h = {A => [ "127.0.0.201" ], SRV => [ "1 5 $port example.net" ]} + unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR}) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq 'example.net' && $type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + + } + if ($name eq '_http._tcp.example.net' && $type == SRV && $h->{SRV}) { + map { push @rdata, rd_srv($ttl, (split ' ', $_)) } + @{$h->{SRV}}; + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_srv { + my ($ttl, $pri, $w, $port, $name) = @_; + my @rdname = split /\./, $name; + my $rdlen = length(join '', @rdname) + @rdname + 7; # pri w port x + + pack 'n3N n n3 (C/a*)* x', + 0xc00c, SRV, IN, $ttl, $rdlen, $pri, $w, $port, @rdname; +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => port(8980), + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => "127.0.0.1:" . port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . port(8980); + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h); + $fh->send($data); + $cnt++; + + } else { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-SRV: (.*)$/m; + map { push @{$h{SRV}}, $_ } split(/;/, $1); + $headers =~ /X-ERROR: (.*)$/m; + $h{ERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/upstream_resolve.t b/upstream_resolve.t new file mode 100644 --- /dev/null +++ b/upstream_resolve.t @@ -0,0 +1,368 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for dynamic upstream configuration with re-resolvable servers. +# Ensure that dns updates are properly applied. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http proxy upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + zone z 1m; + server example.net:%%PORT_8080%% resolve max_fails=0; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8982_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + server { + listen 127.0.0.1:8080; + listen [::1]:%%PORT_8080%%; + server_name localhost; + + location / { + proxy_pass http://u/t; + proxy_connect_timeout 50ms; + add_header X-IP $upstream_addr; + error_page 502 504 redirect; + } + + location /2 { + proxy_pass http://u/t; + add_header X-IP $upstream_addr; + } + + location /t { } + } +} + +EOF + +port(8083); + +$t->write_file('t', ''); + +$t->run_daemon(\&dns_daemon, $t)->waitforfile($t->testdir . '/' . port(8982)); +$t->try_run('no resolve in upstream server')->plan(18); + +############################################################################### + +my ($r, @n); +my $p0 = port(8080); + +update_name({A => '127.0.0.201'}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'A'); +like($r, qr/127.0.0.201:$p0/, 'A 1'); + +# A changed + +update_name({A => '127.0.0.202'}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'A changed'); +like($r, qr/127.0.0.202:$p0/, 'A changed 1'); + +# 1 more A added + +update_name({A => '127.0.0.201 127.0.0.202'}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 2, 'A A'); +like($r, qr/127.0.0.201:$p0/, 'A A 1'); +like($r, qr/127.0.0.202:$p0/, 'A A 2'); + +# 1 A removed, 2 AAAA added + +update_name({A => '127.0.0.201', AAAA => 'fe80::1 fe80::2'}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 3, 'A AAAA AAAA responses'); +like($r, qr/127.0.0.201:$p0/, 'A AAAA AAAA 1'); +like($r, qr/\[fe80::1\]:$p0/, 'A AAAA AAAA 2'); +like($r, qr/\[fe80::1\]:$p0/, 'A AAAA AAAA 3'); + +# all records removed + +update_name(); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 0, 'empty response'); + +# A added after empty + +update_name({A => '127.0.0.201'}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'A added'); +like($r, qr/127.0.0.201:$p0/, 'A added 1'); + +# changed to CNAME + +update_name({CNAME => 'alias'}, 4); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'CNAME'); +like($r, qr/127.0.0.203:$p0/, 'CNAME 1'); + +# bad DNS reply should not affect existing upstream configuration + +update_name({ERROR => 'SERVFAIL'}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'ERROR'); +like($r, qr/127.0.0.203:$p0/, 'ERROR 1'); +update_name({A => '127.0.0.1'}); + +############################################################################### + +sub update_name { + my ($name, $plan) = @_; + + $plan = 2 if !defined $plan; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8083) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{AAAA} = '' unless $name->{AAAA}; + $name->{CNAME} = '' unless $name->{CNAME}; + $name->{ERROR} = '' unless $name->{ERROR}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-AAAA: $name->{AAAA} +X-CNAME: $name->{CNAME} +X-ERROR: $name->{ERROR} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant CNAME => 5; + use constant AAAA => 28; + use constant DNAME => 39; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 1); + $h = {A => [ "127.0.0.201" ]} unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR}) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq 'example.net') { + if ($type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + } + if ($type == AAAA && $h->{AAAA}) { + map { push @rdata, rd_addr6($ttl, $_) } @{$h->{AAAA}}; + } + my $cname = defined $h->{CNAME} ? $h->{CNAME} : 0; + if ($cname) { + push @rdata, pack("n3N nCa5n", 0xc00c, CNAME, IN, $ttl, + 8, 5, $cname, 0xc00c); + } + + } elsif ($name eq 'alias.example.net') { + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.203'); + } + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub expand_ip6 { + my ($addr) = @_; + + substr ($addr, index($addr, "::"), 2) = + join "0", map { ":" } (0 .. 8 - (split /:/, $addr) + 1); + map { hex "0" x (4 - length $_) . "$_" } split /:/, $addr; +} + +sub rd_addr6 { + my ($ttl, $addr) = @_; + + pack 'n3N nn8', 0xc00c, AAAA, IN, $ttl, 16, expand_ip6($addr); +} + +sub dns_daemon { + my ($t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => port(8982), + Proto=> 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => "127.0.0.1:" . port(8083), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . port(8982); + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h); + $fh->send($data); + $cnt++; + + } else { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-AAAA: (.*)$/m; + map { push @{$h{AAAA}}, $_ } split(/ /, $1); + $headers =~ /X-CNAME: (.*)$/m; + $h{CNAME} = $1; + $headers =~ /X-ERROR: (.*)$/m; + $h{ERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/upstream_resolve_reload.t b/upstream_resolve_reload.t new file mode 100644 --- /dev/null +++ b/upstream_resolve_reload.t @@ -0,0 +1,306 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for dynamic upstream configuration with re-resolvable servers. +# Ensure that upstream configuration is inherited on reload. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new()->has(qw/http proxy upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + zone z 1m; + server example.net:%%PORT_8081%% resolve; + } + + upstream u2 { + zone z 1m; + server 127.0.0.203:%%PORT_8081%% max_fails=0; + server example.net:%%PORT_8081%% resolve max_fails=0; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8980_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + proxy_pass http://u; + proxy_connect_timeout 50ms; + add_header X-IP $upstream_addr always; + } + + location /2 { + proxy_pass http://u2; + proxy_connect_timeout 50ms; + add_header X-IP $upstream_addr always; + } + } +} + +EOF + +$t->run_daemon(\&dns_daemon, $t)->waitforfile($t->testdir . '/' . port(8980)); +$t->try_run('no resolve in upstream server')->plan(9); + +############################################################################### + +my $p = port(8081); + +update_name({A => '127.0.0.201'}); +like(http_get('/'), qr/X-IP: 127.0.0.201:$p/, 'reload - before - request'); +like(http_get('/'), qr/X-IP: 127.0.0.201:$p/, 'reload - before - request 2'); +like(http_get('/2'), qr/127.0.0.(201:$p, 127.0.0.203|203:$p, 127.0.0.201):$p/, + 'reload - before - many'); + +update_name({ERROR => 'SERVFAIL'}, 0); + +my $conf = $t->read_file('nginx.conf'); +$conf =~ s/$p/port(8082)/gmse; +$p = port(8082); +$t->write_file('nginx.conf', $conf); + +$t->reload(); +waitforworker($t); + +like(http_get('/'), qr/X-IP: 127.0.0.201:$p/, 'reload - preresolve - request'); +like(http_get('/'), qr/X-IP: 127.0.0.201:$p/, 'reload - preresolve - request 2'); +like(http_get('/2'), qr/127.0.0.(201:$p, 127.0.0.203|203:$p, 127.0.0.201):$p/, + 'reload - preresolve - many'); + +update_name({A => '127.0.0.202'}); +like(http_get('/'), qr/X-IP: 127.0.0.202:$p/, 'reload - update - request'); +like(http_get('/'), qr/X-IP: 127.0.0.202:$p/, 'reload - update - request 2'); +like(http_get('/2'), qr/127.0.0.(202:$p, 127.0.0.203|203:$p, 127.0.0.202):$p/, + 'reload - update - many'); + +############################################################################### + +sub waitforworker { + my ($t) = @_; + + for (1 .. 30) { + last if $t->read_file('error.log') =~ /exited with code/; + select undef, undef, undef, 0.2; + } +} + +sub update_name { + my ($name, $plan) = @_; + + $plan = 2 if !defined $plan; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8081) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{ERROR} = '' unless $name->{ERROR}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-ERROR: $name->{ERROR} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 1); + $h = {A => [ "127.0.0.201" ]} unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR}) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq 'example.net' && $type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => port(8980), + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => "127.0.0.1:" . port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . port(8980); + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h); + $fh->send($data); + $cnt++; + + } else { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-ERROR: (.*)$/m; + $h{ERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/upstream_resolver.t b/upstream_resolver.t new file mode 100644 --- /dev/null +++ b/upstream_resolver.t @@ -0,0 +1,265 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for re-resolvable servers with resolver in http upstream. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http proxy upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + resolver 127.0.0.1:%%PORT_8980_UDP%%; + + upstream u { + zone z 1m; + server example.net:%%PORT_8080%% resolve; + } + + upstream u1 { + zone z 1m; + server example.net:%%PORT_8080%% resolve; + resolver 127.0.0.1:%%PORT_8981_UDP%%; + } + + upstream u2 { + zone z 1m; + server example.net:%%PORT_8080%% resolve; + resolver 127.0.0.1:%%PORT_8982_UDP%%; + resolver_timeout 200s; # for coverage + } + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + proxy_pass http://$args/t; + proxy_connect_timeout 50ms; + add_header X-IP $upstream_addr; + error_page 502 504 redirect; + } + + } +} + +EOF + +$t->run_daemon(\&dns_daemon, $t, port($_), port($_ + 10)) for (8980 .. 8982); +$t->waitforfile($t->testdir . '/' . port($_)) for (8980 .. 8982); + +$t->try_run('no resolver in upstream')->plan(6); + +############################################################################### + +ok(waitfordns(8980), 'resolved'); +ok(waitfordns(8981), 'resolved in upstream 1'); +ok(waitfordns(8982), 'resolved in upstream 2'); + +like(http_get('/?u'), qr/127.0.0.200/, 'resolver'); +like(http_get('/?u1'), qr/127.0.0.201/, 'resolver upstream 1'); +like(http_get('/?u2'), qr/127.0.0.202/, 'resolver upstream 2'); + +############################################################################### + +sub waitfordns { + my ($port, $plan) = @_; + + $plan = 1 if !defined $plan; + + sub sock { + my ($port) = @_; + + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port($port + 10) + ) + or die "Can't connect to dns control socket: $!\n"; + } + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost + +EOF + + for (1 .. 10) { + my ($gen) = http($req, socket => sock($port)) =~ /X-Gen: (\d+)/; + select undef, undef, undef, 0.5; + return 1 if $gen >= $plan; + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $port) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant A => 1; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 1); + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + + my $name = join('.', @name); + if ($name eq 'example.net' && $type == A) { + if ($port == port(8980)) { + push @rdata, rd_addr($ttl, "127.0.0.200"); + } + + if ($port == port(8981)) { + push @rdata, rd_addr($ttl, "127.0.0.201"); + } + + if ($port == port(8982)) { + push @rdata, rd_addr($ttl, "127.0.0.202"); + } + } + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($t, $port, $control_port) = @_; + + my ($data, $recv_data); + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => $port, + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalAddr => '127.0.0.1', + LocalPort => $control_port, + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $port); + $fh->send($data); + $cnt++; + + } else { + process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/upstream_service.t b/upstream_service.t new file mode 100644 --- /dev/null +++ b/upstream_service.t @@ -0,0 +1,489 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for dynamic upstream configuration with service (SRV) feature. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; +use Socket qw/ CRLF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http proxy upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + zone z 1m; + server example.net resolve service=http max_fails=0; + } + + upstream u2 { + zone z2 1m; + server example.net resolve service=_http._tcp; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8981_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + add_header X-IP $upstream_addr; + error_page 502 504 redirect; + proxy_connect_timeout 50ms; + + location / { + proxy_pass http://u/t; + } + + location /full { + proxy_pass http://u2/t; + } + + location /t { } + } +} + +EOF + +port(8084); + +$t->write_file('t', ''); + +$t->run_daemon(\&dns_daemon, $t)->waitforfile($t->testdir . '/' . port(8981)); +port(8981, socket => 1)->close(); +$t->try_run('no service in upstream server')->plan(30); + +############################################################################### + +my ($r, @n); +my ($p0, $p2, $p3) = (port(8080), port(8082), port(8083)); + +update_name({A => '127.0.0.201', SRV => "1 5 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'A'); +like($r, qr/127.0.0.201:$p0/, 'A 1'); + +# fully specified service + +$r = http_get('/full'); +is(@n = $r =~ /:$p0/g, 1, 'A full'); +like($r, qr/127.0.0.201:$p0/, 'A full 1'); + +# A changed + +update_name({A => '127.0.0.202', SRV => "1 5 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'A changed'); +like($r, qr/127.0.0.202:$p0/, 'A changed 1'); + +# 1 more A added + +update_name({A => '127.0.0.201 127.0.0.202', SRV => "1 5 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 2, 'A A'); +like($r, qr/127.0.0.201:$p0/, 'A A 1'); +like($r, qr/127.0.0.202:$p0/, 'A A 2'); + +# 1 A removed, 2 AAAA added + +update_name({A => '127.0.0.201', AAAA => 'fe80::1 fe80::2', + SRV => "1 5 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 3, 'A AAAA AAAA responses'); +like($r, qr/127.0.0.201:$p0/, 'A AAAA AAAA 1'); +like($r, qr/\[fe80::1\]:$p0/, 'A AAAA AAAA 2'); +like($r, qr/\[fe80::1\]:$p0/, 'A AAAA AAAA 3'); + +# all records removed + +update_name({SRV => "1 5 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 0, 'empty SRV response'); + +# all SRV records removed + +update_name(); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 0, 'empty response'); + +# A added after empty + +update_name({A => '127.0.0.201', SRV => "1 5 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'A added'); +like($r, qr/127.0.0.201:$p0/, 'A added 1'); + +# SRV changed its weight + +update_name({A => '127.0.0.201', SRV => "1 6 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'SRV weight'); +like($r, qr/127.0.0.201:$p0/, 'SRV weight 1'); + +# changed to CNAME + +update_name({CNAME => 'alias'}, 2, 2); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'CNAME'); +like($r, qr/127.0.0.203:$p0/, 'CNAME 1'); + +# bad SRV reply should not affect existing upstream configuration + +update_name({CNAME => 'alias', ERROR => 'SERVFAIL'}, 1, 0); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 1, 'ERROR'); +like($r, qr/127.0.0.203:$p0/, 'ERROR 1'); +update_name({ERROR => ''}, 1, 0); + +# 2 equal SRV RR + +update_name({A => '127.0.0.201', + SRV => "1 5 $p0 example.net;1 5 $p0 example.net"}); +$r = http_get('/'); +is(@n = $r =~ /:$p0/g, 2, 'SRV same'); +like($r, qr/127.0.0.201:$p0, 127.0.0.201:$p0/, 'SRV same peers'); + +# all equal records removed + +update_name(); +$r = http_get('/'); +is(@n = $r =~ /:($p0|$p2|$p3)/g, 0, 'SRV same removed'); + +# 2 different SRV RR + +update_name({A => '127.0.0.201', + SRV => "1 5 $p2 example.net;2 6 $p3 alias.example.net"}, 1, 2); +$r = http_get('/'); +is(@n = $r =~ /:($p2|$p3)/g, 2, 'SRV diff'); +like($r, qr/127.0.0.201:$p2/, 'SRV diff 1'); +like($r, qr/127.0.0.203:$p3/, 'SRV diff 2'); + +# all different records removed + +update_name(); +$r = http_get('/'); +is(@n = $r =~ /:($p0|$p2|$p3)/g, 0, 'SRV diff removed'); + +############################################################################### + +sub update_name { + my ($name, $plan, $plan6) = @_; + + $plan = 1, $plan6 = 0 if !defined $name; + $plan = $plan6 = 1 if !defined $plan; + $plan += $plan6 + $plan6; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8084) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{AAAA} = '' unless $name->{AAAA}; + $name->{CNAME} = '' unless $name->{CNAME}; + $name->{ERROR} = '' unless $name->{ERROR}; + $name->{SERROR} = '' unless $name->{SERROR}; + $name->{SRV} = '' unless $name->{SRV}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-AAAA: $name->{AAAA} +X-CNAME: $name->{CNAME} +X-ERROR: $name->{ERROR} +X-SERROR: $name->{SERROR} +X-SRV: $name->{SRV} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h, $cnt, $tcp) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant FORMERR => 1; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant CNAME => 5; + use constant AAAA => 28; + use constant SRV => 33; + + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl, $port) = (0x8180, NOERROR, 3600, port(8080)); + $h = {A => [ "127.0.0.1" ], SRV => [ "1 5 $port example.net" ]} + unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR} && $type == SRV) { + $rcode = SERVFAIL; + goto bad; + } + + # subordinate error + + if ($h->{SERROR} && $type != SRV) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq '_http._tcp.example.net') { + if ($type == SRV && $h->{SRV}) { + map { push @rdata, rd_srv($ttl, (split ' ', $_)) } + @{$h->{SRV}}; + } + + my $cname = defined $h->{CNAME} ? $h->{CNAME} : 0; + if ($cname) { + push @rdata, pack("n3N nCa5n", 0xc00c, CNAME, IN, $ttl, + 8, 5, "alias", 0xc00c + length("_http._tcp ")); + } + + } elsif ($name eq '_http._tcp.trunc.example.net' && $type == SRV) { + push @rdata, $tcp + ? rd_srv($ttl, 1, 1, $port, 'tcp.example.net') + : rd_srv($ttl, 1, 1, $port, 'example.net'); + + $hdr |= 0x0300 if $name eq '_http._tcp.trunc.example.net' + and !$tcp; + + } elsif ($name eq 'example.net' || $name eq 'tcp.example.net') { + if ($type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + } + if ($type == AAAA && $h->{AAAA}) { + map { push @rdata, rd_addr6($ttl, $_) } @{$h->{AAAA}}; + } + my $cname = defined $h->{CNAME} ? $h->{CNAME} : 0; + if ($cname) { + push @rdata, pack("n3N nCa5n", 0xc00c, CNAME, IN, $ttl, + 8, 5, $cname, 0xc00c); + } + + } elsif ($name eq 'alias.example.net') { + if ($type == SRV) { + push @rdata, rd_srv($ttl, 1, 5, $port, 'example.net'); + } + if ($type == A) { + push @rdata, rd_addr($ttl, '127.0.0.203'); + } + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $$cnt++ if $type == SRV || keys %$h; + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_srv { + my ($ttl, $pri, $w, $port, $name) = @_; + my @rdname = split /\./, $name; + my $rdlen = length(join '', @rdname) + @rdname + 7; # pri w port x + + pack 'n3N n n3 (C/a*)* x', + 0xc00c, SRV, IN, $ttl, $rdlen, $pri, $w, $port, @rdname; +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub expand_ip6 { + my ($addr) = @_; + + substr ($addr, index($addr, "::"), 2) = + join "0", map { ":" } (0 .. 8 - (split /:/, $addr) + 1); + map { hex "0" x (4 - length $_) . "$_" } split /:/, $addr; +} + +sub rd_addr6 { + my ($ttl, $addr) = @_; + + pack 'n3N nn8', 0xc00c, AAAA, IN, $ttl, 16, expand_ip6($addr); +} + +sub dns_daemon { + my ($t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => port(8981), + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => '127.0.0.1:' . port(8084), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $tcp = port(8981, socket => 1); + my $sel = IO::Select->new($socket, $control, $tcp); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . port(8981); + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh || $tcp == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h, \$cnt); + $fh->send($data); + + } elsif ($fh->sockport() == port(8084)) { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + + } elsif ($fh->sockport() == port(8981)) { + $fh->recv($recv_data, 65536); + unless (length $recv_data) { + $sel->remove($fh); + $fh->close; + next; + } + +again: + my $len = unpack("n", $recv_data); + my $data = substr $recv_data, 2, $len; + $data = reply_handler($data, $h, \$cnt, 1); + $data = pack("n", length $data) . $data; + $fh->send($data); + $recv_data = substr $recv_data, 2 + $len; + goto again if length $recv_data; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-AAAA: (.*)$/m; + map { push @{$h{AAAA}}, $_ } split(/ /, $1); + $headers =~ /X-SRV: (.*)$/m; + map { push @{$h{SRV}}, $_ } split(/;/, $1); + $headers =~ /X-CNAME: (.+)$/m and $h{CNAME} = $1; + $headers =~ /X-ERROR: (.+)$/m and $h{ERROR} = $1; + $headers =~ /X-SERROR: (.+)$/m and $h{SERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### diff --git a/upstream_service_reload.t b/upstream_service_reload.t new file mode 100644 --- /dev/null +++ b/upstream_service_reload.t @@ -0,0 +1,303 @@ +#!/usr/bin/perl + +# (C) Sergey Kandaurov +# (C) Nginx, Inc. + +# Tests for dynamic upstream configuration with service (SRV) feature. +# Ensure that upstream configuration is inherited on reload. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use IO::Select; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +plan(skip_all => 'win32') if $^O eq 'MSWin32'; + +my $t = Test::Nginx->new()->has(qw/http upstream_zone/); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + upstream u { + zone z 1m; + server example.net resolve service=http; + } + + # lower the retry timeout after empty reply + resolver 127.0.0.1:%%PORT_8980_UDP%% valid=1s; + # retry query shortly after DNS is started + resolver_timeout 1s; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location / { + proxy_pass http://u; + proxy_connect_timeout 50ms; + add_header X-IP $upstream_addr always; + } + } +} + +EOF + +port(8081); + +$t->run_daemon(\&dns_daemon, $t)->waitforfile($t->testdir . '/' . port(8980)); +$t->try_run('no resolve in upstream server')->plan(6); + +############################################################################### + +update_name({A => '127.0.0.201', SRV => "1 5 42 example.net"}); +like(http_get('/'), qr/X-IP: 127.0.0.201:42/, 'reload - before - request'); +like(http_get('/'), qr/X-IP: 127.0.0.201:42/, 'reload - before - request 2'); + +update_name({ERROR => 'SERVFAIL'}, 0); + +$t->reload(); +waitforworker($t); + +like(http_get('/'), qr/X-IP: 127.0.0.201:42/, 'reload - preresolve - request'); +like(http_get('/'), qr/X-IP: 127.0.0.201:42/, 'reload - preresolve - request 2'); + +update_name({A => '127.0.0.202', SRV => "1 5 42 example.net"}); +like(http_get('/'), qr/X-IP: 127.0.0.202:42/, 'reload - update - request'); +like(http_get('/'), qr/X-IP: 127.0.0.202:42/, 'reload - update - request 2'); + +############################################################################### + +sub waitforworker { + my ($t) = @_; + + for (1 .. 30) { + last if $t->read_file('error.log') =~ /exited with code/; + select undef, undef, undef, 0.2; + } +} + +sub update_name { + my ($name, $plan) = @_; + + $plan = 3 if !defined $plan; + + sub sock { + IO::Socket::INET->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1:' . port(8081) + ) + or die "Can't connect to nginx: $!\n"; + } + + $name->{A} = '' unless $name->{A}; + $name->{ERROR} = '' unless $name->{ERROR}; + $name->{SRV} = '' unless $name->{SRV}; + + my $req =<<EOF; +GET / HTTP/1.0 +Host: localhost +X-A: $name->{A} +X-ERROR: $name->{ERROR} +X-SRV: $name->{SRV} + +EOF + + my ($gen) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + for (1 .. 10) { + my ($gen2) = http($req, socket => sock()) =~ /X-Gen: (\d+)/; + + # let resolver cache expire to finish upstream reconfiguration + select undef, undef, undef, 0.5; + last unless ($gen + $plan > $gen2); + } +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $h) = @_; + + my (@name, @rdata); + + use constant NOERROR => 0; + use constant SERVFAIL => 2; + use constant NXDOMAIN => 3; + + use constant A => 1; + use constant SRV => 33; + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl, $port) = (0x8180, NOERROR, 3600, port(8080)); + $h = {A => [ "127.0.0.201" ], SRV => [ "1 5 $port example.net" ]} + unless defined $h; + + # decode name + + my ($len, $offset) = (undef, 12); + while (1) { + $len = unpack("\@$offset C", $recv_data); + last if $len == 0; + $offset++; + push @name, unpack("\@$offset A$len", $recv_data); + $offset += $len; + } + + $offset -= 1; + my ($id, $type, $class) = unpack("n x$offset n2", $recv_data); + my $name = join('.', @name); + + if ($h->{ERROR}) { + $rcode = SERVFAIL; + goto bad; + } + + if ($name eq 'example.net' && $type == A && $h->{A}) { + map { push @rdata, rd_addr($ttl, $_) } @{$h->{A}}; + + } + if ($name eq '_http._tcp.example.net' && $type == SRV && $h->{SRV}) { + map { push @rdata, rd_srv($ttl, (split ' ', $_)) } + @{$h->{SRV}}; + } + +bad: + + Test::Nginx::log_core('||', "DNS: $name $type $rcode"); + + $len = @name; + pack("n6 (C/a*)$len x n2", $id, $hdr | $rcode, 1, scalar @rdata, + 0, 0, @name, $type, $class) . join('', @rdata); +} + +sub rd_srv { + my ($ttl, $pri, $w, $port, $name) = @_; + my @rdname = split /\./, $name; + my $rdlen = length(join '', @rdname) + @rdname + 7; # pri w port x + + pack 'n3N n n3 (C/a*)* x', + 0xc00c, SRV, IN, $ttl, $rdlen, $pri, $w, $port, @rdname; +} + +sub rd_addr { + my ($ttl, $addr) = @_; + + my $code = 'split(/\./, $addr)'; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($t) = @_; + my ($data, $recv_data, $h); + + my $socket = IO::Socket::INET->new( + LocalAddr => '127.0.0.1', + LocalPort => port(8980), + Proto => 'udp', + ) + or die "Can't create listening socket: $!\n"; + + my $control = IO::Socket::INET->new( + Proto => 'tcp', + LocalHost => "127.0.0.1:" . port(8081), + Listen => 5, + Reuse => 1 + ) + or die "Can't create listening socket: $!\n"; + + my $sel = IO::Select->new($socket, $control); + + local $SIG{PIPE} = 'IGNORE'; + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . port(8980); + close $fh; + my $cnt = 0; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($control == $fh) { + my $new = $fh->accept; + $new->autoflush(1); + $sel->add($new); + + } elsif ($socket == $fh) { + $fh->recv($recv_data, 65536); + $data = reply_handler($recv_data, $h); + $fh->send($data); + $cnt++; + + } else { + $h = process_name($fh, $cnt); + $sel->remove($fh); + $fh->close; + } + } + } +} + +# parse dns update + +sub process_name { + my ($client, $cnt) = @_; + my $port = $client->sockport(); + + my $headers = ''; + my $uri = ''; + my %h; + + while (<$client>) { + $headers .= $_; + last if (/^\x0d?\x0a?$/); + } + return 1 if $headers eq ''; + + $uri = $1 if $headers =~ /^\S+\s+([^ ]+)\s+HTTP/i; + return 1 if $uri eq ''; + + $headers =~ /X-A: (.*)$/m; + map { push @{$h{A}}, $_ } split(/ /, $1); + $headers =~ /X-SRV: (.*)$/m; + map { push @{$h{SRV}}, $_ } split(/;/, $1); + $headers =~ /X-ERROR: (.*)$/m; + $h{ERROR} = $1; + + Test::Nginx::log_core('||', "$port: response, 200"); + print $client <<EOF; +HTTP/1.1 200 OK +Connection: close +X-Gen: $cnt + +OK +EOF + + return \%h; +} + +############################################################################### _______________________________________________ nginx-devel mailing list nginx-devel@nginx.org https://mailman.nginx.org/mailman/listinfo/nginx-devel