diff --git a/ngx_http_proxy_connect_module.c b/ngx_http_proxy_connect_module.c index cd7e517..c480c52 100644 --- a/ngx_http_proxy_connect_module.c +++ b/ngx_http_proxy_connect_module.c @@ -69,6 +69,7 @@ struct ngx_http_proxy_connect_upstream_s { ngx_buf_t from_client; ngx_output_chain_ctx_t output; + ngx_chain_writer_ctx_t writer; ngx_buf_t buffer; @@ -81,6 +82,8 @@ struct ngx_http_proxy_connect_upstream_s { ngx_msec_t start_time; ngx_http_proxy_connect_upstream_state_t state; + + unsigned request_body_blocked:1; }; struct ngx_http_proxy_connect_address_s { @@ -146,6 +149,14 @@ static ngx_int_t ngx_http_proxy_connect_sock_ntop(ngx_http_request_t *r, static ngx_int_t ngx_http_proxy_connect_create_peer(ngx_http_request_t *r, ngx_http_upstream_resolved_t *ur); +#if (NGX_HTTP_V2) +static void ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r); +static ngx_int_t ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r); +ssize_t ngx_http_v2_proxy_connect_recv(ngx_connection_t *c, u_char *buf, size_t size); +ssize_t ngx_http_v2_proxy_connect_send(ngx_connection_t *c, u_char *buf, size_t size); +void ngx_http_v2_proxy_connect_process_upstream(ngx_http_request_t *r, ngx_uint_t do_write); +void ngx_http_v2_proxy_connect_process_downstream(ngx_http_request_t *r, ngx_uint_t do_write); +#endif static ngx_command_t ngx_http_proxy_connect_commands[] = { @@ -554,6 +565,13 @@ ngx_http_proxy_connect_send_connection_established(ngx_http_request_t *r) u->state.connect_time = ngx_current_msec - u->start_time; } +#if (NGX_HTTP_V2) + if (r->stream) { + ngx_http_v2_proxy_connect_send_connection_established(r); + return; + } +#endif + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); b = &ctx->buf; @@ -645,6 +663,117 @@ ngx_http_proxy_connect_send_connection_established(ngx_http_request_t *r) } +#if (NGX_HTTP_V2) +static void +ngx_http_v2_proxy_connect_send_connection_established(ngx_http_request_t *r) +{ + // A proxy that supports CONNECT establishes a TCP connection [TCP] to the server identified in the :authority pseudo-header field. Once this connection is successfully established, the proxy sends a HEADERS frame containing a 2xx series status code to the client, as defined in [RFC7231], Section 4.3.6. + ngx_int_t rc; + ngx_connection_t *c; + ngx_http_proxy_connect_ctx_t *ctx; + ngx_http_proxy_connect_upstream_t *u; + + ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_connect_module); + + u = ctx->u; + + c = r->connection; + + if (ctx->send_established) { + rc = ngx_http_output_filter(r, NULL); + + } else { + ctx->send_established = 1; + + r->headers_out.status = 200; + + /* ensure HERADERS frame: fin=0 */ + r->headers_out.content_length_n = -1; + + rc = ngx_http_send_header(r); + } + + if (rc == NGX_AGAIN) { + + r->write_event_handler = ngx_http_proxy_connect_send_handler; + + ngx_add_timer(c->write, ctx->data_timeout); + + return; + } + + if (rc == NGX_ERROR) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + if (rc > NGX_OK && rc != NGX_HTTP_OK) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + /* NGX_HTTP_OK or NGX_OK */ + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, + "proxy_connect: sent 200 connection established"); + + if (c->write->timer_set) { + ngx_del_timer(c->write); + } + + ctx->send_established_done = 1; + + r->write_event_handler = ngx_http_proxy_connect_write_downstream; + r->read_event_handler = ngx_http_proxy_connect_read_downstream; + + /* init request_body for reading DATA frame from http2 */ + +#if 0 + r->main->count--; /* ++ in ngx_http_read_client_request_body */ + rc = ngx_http_read_client_request_body(r, + ngx_http_proxy_connect_read_downstream); + + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } +#else + { + /* init upstream output writer */ + + u->output.pool = r->pool; + u->output.bufs.num = 1; + u->output.bufs.size = u->conf->buffer_size; + + if (u->output.output_filter == NULL) { + u->output.output_filter = ngx_chain_writer; + u->output.filter_ctx = &u->writer; + } + + u->writer.pool = r->pool; + u->writer.out = NULL; + u->writer.last = &u->writer.out; + u->writer.connection = u->peer.connection; + u->writer.limit = 0; + } + + + { + + rc = ngx_http_v2_read_request_body(r); + + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + return; + } +#endif +} +#endif + + static void ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, ngx_uint_t from_upstream, ngx_uint_t do_write) @@ -669,6 +798,38 @@ ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, pc = u->peer.connection; +#if (NGX_HTTP_V2) + +#if 1 + /* + * There is no low-level recv/send IO operations of http/v2 in nginx core, + * so we just need use high-level buf/chain IO operations. + */ + + if (r->stream) { + if (from_upstream) { + ngx_http_v2_proxy_connect_process_downstream(r, do_write); + } else { + ngx_http_v2_proxy_connect_process_upstream(r, do_write); + } + return ; + } +#else + + /* + * Alough there is no low-level IO operations, we can monitor them + * trickly. We need to rewrite c->send, c->recv interface to read/write + * HTTP/2 DATA frame + */ + + if (r->stream) { + r->connection->send = ngx_http_v2_proxy_connect_send; + r->connection->recv = ngx_http_v2_proxy_connect_recv; + } +#endif + +#endif + if (from_upstream) { src = pc; dst = c; @@ -805,6 +966,296 @@ ngx_http_proxy_connect_tunnel(ngx_http_request_t *r, } +#if (NGX_HTTP_V2) +/* + * data flow: downstream -> upstream + * + * referer to + * ngx_http_upstream_send_request() + * -> ngx_http_upstream_send_request_body() + */ +void +ngx_http_v2_proxy_connect_process_upstream(ngx_http_request_t *r, + ngx_uint_t do_write) +{ + ngx_int_t rc; + ngx_chain_t *out, /**cl,*/ *ln; + ngx_connection_t *c; + ngx_http_proxy_connect_ctx_t *ctx; + ngx_http_proxy_connect_upstream_t *u; + + ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "proxy_connect: h2 downstream -> upstream"); + + ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_connect_module); + u = ctx->u; + +#if 0 + if (!r->request_body_no_buffering) { + + /* buffered request body */ + + if (!u->request_sent) { + u->request_sent = 1; + out = u->request_bufs; + + } else { + out = NULL; + } + + rc = ngx_output_chain(&u->output, out); + + if (rc == NGX_AGAIN) { + u->request_body_blocked = 1; + + } else { + u->request_body_blocked = 0; + } + + goto done; + } + + if (!u->request_sent) { + u->request_sent = 1; + out = u->request_bufs; + + if (r->request_body->bufs) { + for (cl = out; cl->next; cl = cl->next) { /* void */ } + cl->next = r->request_body->bufs; + r->request_body->bufs = NULL; + } + + c = u->peer.connection; + clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); + + if (clcf->tcp_nodelay && ngx_tcp_nodelay(c) != NGX_OK) { + return NGX_ERROR; + } + + r->read_event_handler = ngx_http_upstream_read_request_handler; + + } else { + out = NULL; + } +#else + out = NULL; + r->request_body_no_buffering = 1; +#endif + + for ( ;; ) { + + if (do_write) { + rc = ngx_output_chain(&u->output, out); + + if (rc == NGX_ERROR) { + ngx_http_proxy_connect_finalize_request(r, u, NGX_ERROR); + return; + } + + while (out) { + ln = out; + out = out->next; + ngx_free_chain(r->pool, ln); + } + + if (rc == NGX_AGAIN) { + u->request_body_blocked = 1; + + } else { + u->request_body_blocked = 0; + } + + if (rc == NGX_OK && !r->reading_body) { + break; + } + } + + if (r->reading_body) { + /* read client request body */ + + rc = ngx_http_read_unbuffered_request_body(r); + + if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { + ngx_http_proxy_connect_finalize_request(r, u, rc); + return; + } + + out = r->request_body->bufs; + r->request_body->bufs = NULL; + } + + /* stop if there is nothing to send */ + + if (out == NULL) { + rc = NGX_AGAIN; + break; + } + + do_write = 1; + } + +//done: + +#if 0 + if (!r->reading_body) { + if (!u->store && !r->post_action && !u->conf->ignore_client_abort) { + r->read_event_handler = + ngx_http_upstream_rd_check_broken_connection; + } + } +#endif + + c = u->peer.connection; + + if (rc == NGX_AGAIN) { + if (!c->write->ready || u->request_body_blocked) { + ngx_add_timer(c->write, ctx->data_timeout); + + } else if (c->write->timer_set) { + ngx_del_timer(c->write); + } + + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + +#if 0 + if (c->write->ready && c->tcp_nopush == NGX_TCP_NOPUSH_SET) { + if (ngx_tcp_push(c->fd) == -1) { + ngx_log_error(NGX_LOG_CRIT, c->log, ngx_socket_errno, + ngx_tcp_push_n " failed"); + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + c->tcp_nopush = NGX_TCP_NOPUSH_UNSET; + } +#endif + + if (c->read->ready) { + ngx_post_event(c->read, &ngx_posted_events); + } + + return; + } + + /* rc == NGX_OK */ + + // TODO: how to handle timer + + if (c->write->timer_set) { + ngx_del_timer(c->write); + } + +#if 0 + if (c->tcp_nopush == NGX_TCP_NOPUSH_SET) { + if (ngx_tcp_push(c->fd) == -1) { + ngx_log_error(NGX_LOG_CRIT, c->log, ngx_socket_errno, + ngx_tcp_push_n " failed"); + ngx_http_upstream_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + + c->tcp_nopush = NGX_TCP_NOPUSH_UNSET; + } + + if (!u->conf->preserve_output) { + u->write_event_handler = ngx_http_upstream_dummy_handler; + } +#endif + + if (ngx_handle_write_event(c->write, 0) != NGX_OK) { + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; + } + +#if 0 + if (!u->request_body_sent) { + u->request_body_sent = 1; + + if (u->header_sent) { + return; + } + + ngx_add_timer(c->read, u->conf->read_timeout); + + if (c->read->ready) { + ngx_http_upstream_process_header(r, u); + return; + } + } +#endif + + return; +} + + +void +ngx_http_v2_proxy_connect_process_downstream(ngx_http_request_t *r, + ngx_uint_t do_write) +{ + /* downstream -> upstream */ + +} + + +ssize_t +ngx_http_v2_proxy_connect_recv(ngx_connection_t *c, u_char *buf, size_t size) +{ + ngx_http_request_t *r = c->data; + // TODO: read body data + // ngx_http_upstream_send_request_body + // -> ngx_http_read_unbuffered_request_body + // ... get bufs from DATA frame into r->request_body->bufs + // -> then we need return r->request_body->bufs to caller + r->request_body_no_buffering = 1; + + r->headers_in.content_length_n = -1; + r->headers_in.chunked = 1; + + return 0; +} + + +ssize_t +ngx_http_v2_proxy_connect_send(ngx_connection_t *c, u_char *buf, size_t size) +{ + ngx_http_request_t *r; + ngx_buf_t *b; + ngx_chain_t out; + ngx_http_proxy_connect_ctx_t *ctx; + ngx_http_proxy_connect_upstream_t *u; + + r = c->data; + ctx = ngx_http_get_module_ctx(r, ngx_http_proxy_connect_module); + + c = r->connection; + u = ctx->u; + + /* disable the function of http core module: limit rate */ + r->limit_rate = 0; + r->limit_rate_set = 1; + + b = ngx_calloc_buf(r->pool); //TODO: free b + if (b == NULL) { + return NGX_ERROR; + } + + *b = u->buffer; + + out.buf = b; + out.next = NULL; + + return ngx_http_write_filter(r, &out); +} +#endif + + static void ngx_http_proxy_connect_read_downstream(ngx_http_request_t *r) { @@ -1342,6 +1793,12 @@ ngx_http_proxy_connect_check_broken_connection(ngx_http_request_t *r, return; } +#if (NGX_HTTP_V2) + if (r->stream) { + return; + } +#endif + #if (NGX_HAVE_KQUEUE) if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) { @@ -1471,6 +1928,41 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) return NGX_DECLINED; } +#if (NGX_HTTP_V2) + if (r->stream) { + rc = ngx_http_v2_proxy_connect_process_header(r); + if (rc != NGX_OK) { + return rc; + } + + { + /* + * init request body before handling DATA frames and init_upstream, + * otherwise the DATA frame may not be received in + * ngx_http_v2_state_read_data()? + */ + ngx_http_request_body_t *rb; + + rb = ngx_pcalloc(r->pool, sizeof(ngx_http_request_body_t)); + + if (rb == NULL) { + return NGX_HTTP_INTERNAL_SERVER_ERROR; +#if 0 + ngx_http_proxy_connect_finalize_request(r, u, + NGX_HTTP_INTERNAL_SERVER_ERROR); + return; +#endif + } + + rb->rest = -1; + // TODO: changed to init_upstream () + rb->post_handler = ngx_http_proxy_connect_read_downstream; + + r->request_body = rb; + } + } +#endif + rc = ngx_http_proxy_connect_allow_handler(r, plcf); if (rc != NGX_OK) { @@ -1487,6 +1979,8 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) u->conf = plcf; + /* get or resolve upstream peer address */ + ngx_memzero(&url, sizeof(ngx_url_t)); if (plcf->address) { @@ -1515,7 +2009,7 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "proxy_connect: %s in connect host \"%V\"", url.err, &url.url); - return NGX_HTTP_FORBIDDEN; + return NGX_HTTP_BAD_REQUEST; } return NGX_HTTP_INTERNAL_SERVER_ERROR; @@ -1614,6 +2108,82 @@ ngx_http_proxy_connect_handler(ngx_http_request_t *r) } +#if (NGX_HTTP_V2) +static ngx_int_t +ngx_http_v2_proxy_connect_process_header(ngx_http_request_t *r) +{ + ngx_str_t host; + ngx_int_t port; + + /* check and preprocess v2 header */ + /* The :scheme and :path pseudo-header fields MUST be omitted. */ + if (r->unparsed_uri.len) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent :path header."); + + return NGX_HTTP_BAD_REQUEST; + } + + if (r->schema.len) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent :schema header."); + + return NGX_HTTP_BAD_REQUEST; + } + + /* The :authority pseudo-header field contains the host and port to connect to */ + /* :authority field is handled by ngx_http_process_host() */ + if (r->headers_in.host == NULL) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent no :authority header."); + + + return NGX_HTTP_BAD_REQUEST; + } + + /* initialized but not used */ + + host = r->headers_in.host->value; + r->connect_host_start = host.data; + r->connect_host_end = host.data + r->headers_in.server.len; + + r->connect_port_end = host.data + host.len; + + if (r->connect_port_end == r->connect_host_end) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent no port in :authority header"); + + return NGX_HTTP_BAD_REQUEST; + } + + /* get connect_host: fields that has been processed by http/2 logic */ + + r->connect_host = r->headers_in.server; + + /* get connect_port */ + + r->connect_port.data = r->connect_host_end + 1; + r->connect_port.len = r->connect_port_end - r->connect_host_end - 1; + + port = ngx_atoi(r->connect_port.data, r->connect_port.len); + if (port == NGX_ERROR || port < 1 || port > 65535) { + ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, + "proxy_connect: client sent invalid port in :authority"); + + return NGX_HTTP_BAD_REQUEST; + } + + r->connect_port_n = port; + + ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, + "proxy_connect: process h2 host: %V, port: %d", + &r->connect_host, r->connect_port_n); + + return NGX_OK; +} +#endif + + static ngx_int_t ngx_http_proxy_connect_sock_ntop(ngx_http_request_t *r, ngx_http_proxy_connect_upstream_t *u) @@ -2310,6 +2880,7 @@ ngx_http_proxy_connect_variable_set_response(ngx_http_request_t *r, ctx->buf.last = ctx->buf.pos + v->len; } + static ngx_int_t ngx_http_proxy_connect_add_variables(ngx_conf_t *cf) { diff --git a/t/http_proxy_connect_h2.t b/t/http_proxy_connect_h2.t new file mode 100644 index 0000000..e31107d --- /dev/null +++ b/t/http_proxy_connect_h2.t @@ -0,0 +1,346 @@ +#!/usr/bin/perl + +# (C) Xiaochen Wang + +# Tests for HTTP/2 protocol with proxy_connect module. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +use Socket qw/ SOL_SOCKET SO_RCVBUF /; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; +use Test::Nginx::HTTP2; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +# SRV record, not used +my %route_map; + +# A record +my %aroute_map = ( + 'localhost' => [[300, "127.0.0.1"]], +); + +my $t = Test::Nginx->new()->has(qw/http http_ssl http_v2 socket_ssl_alpn/) + ->has_daemon('openssl'); + +$t->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + resolver 127.0.0.1:%%PORT_8981_UDP%% ipv6=off; # NOTE: cannot connect ipv6 address ::1 in mac os x. + + server { + listen 127.0.0.1:8080 http2 ssl; + server_name localhost; + + ssl_certificate_key localhost.key; + ssl_certificate localhost.crt; + + lingering_close off; + + proxy_connect; + proxy_connect_allow all; + proxy_connect_connect_timeout 10s; + proxy_connect_data_timeout 10s; + + location / { } + } + + server { + listen 127.0.0.1:8081; + return 200 "OK\n"; + } +} + +EOF + +$t->write_file('openssl.conf', <testdir(); + +foreach my $name ('localhost') { + system('openssl req -x509 -new ' + . "-config $d/openssl.conf -subj /CN=$name/ " + . "-out $d/$name.crt -keyout $d/$name.key " + . ">>$d/openssl.out 2>&1") == 0 + or die "Can't create certificate for $name: $!\n"; +} + +$t->write_file('index.html', ''); +$t->write_file('tbig.html', + join('', map { sprintf "XX%06dXX", $_ } (1 .. 500000))); + +$t->run_daemon(\&dns_daemon, port(8981), $t); +$t->waitforfile($t->testdir . '/' . port(8981)); + +open OLDERR, ">&", \*STDERR; close STDERR; +$t->run(); +open STDERR, ">&", \*OLDERR; + +plan(skip_all => 'no ALPN negotiation') unless defined getconn(); +$t->plan(5); + +############################################################################### + +SKIP: { +skip 'LibreSSL too old', 1 + if $t->has_module('LibreSSL') + and not $t->has_feature('libressl:3.4.0'); +skip 'OpenSSL too old', 1 + if $t->has_module('OpenSSL') + and not $t->has_feature('openssl:1.1.0'); + +TODO: { +local $TODO = 'not yet' unless $t->has_version('1.21.4'); + +ok(!get_ssl_socket(['unknown']), 'alpn rejected'); + +} + +} + +like(http_get('/', socket => get_ssl_socket(['http/1.1'])), + qr/200 OK/, 'alpn to HTTP/1.1 fallback'); + +my $s = getconn(['http/1.1', 'h2']); +my $sid = $s->new_stream(); +my $frames = $s->read(all => [{ sid => $sid, fin => 1 }]); +my ($frame) = grep { $_->{type} eq "HEADERS" } @$frames; +is($frame->{headers}->{':status'}, 200, 'alpn to HTTP/2'); +# h2c preface on ssl-enabled socket is rejected as invalid HTTP/1.x request, +# ensure that HTTP/2 auto-detection doesn't kick in + +like(http("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"), qr/Bad Request/, + 'no h2c on ssl socket'); + +# client cancels last stream after HEADERS has been created, +# while some unsent data was left in the SSL buffer +# HEADERS frame may stuck in SSL buffer and won't be sent producing alert + +$s = getconn(['http/1.1', 'h2']); +$s->{socket}->setsockopt(SOL_SOCKET, SO_RCVBUF, 1024*1024) or die $!; + +# for proxy_connect +$sid = $s->new_stream({ headers => [ + { name => ':method', value => 'CONNECT', mode => 1 }, + { name => ':authority', value => 'localhost:'.port(8081), mode => 1 } ]}); +$frames = $s->read(all => [{ sid => $sid, fin => 1 }]); + +($frame) = grep { $_->{type} eq "HEADERS" } @$frames; + +TODO: { + local $TODO = 'not yet'; + is($frame->{headers}->{':status'}, 200, 'CONNECT - connection established'); +} + +$t->stop(); + +############################################################################### + +sub getconn { + my ($alpn) = @_; + $alpn = ['h2'] if !defined $alpn; + + my $sock = get_ssl_socket($alpn); + my $s = Test::Nginx::HTTP2->new(undef, socket => $sock) + if $sock->alpn_selected(); +} + +sub get_ssl_socket { + my ($alpn) = @_; + my $s; + + eval { + local $SIG{ALRM} = sub { die "timeout\n" }; + local $SIG{PIPE} = sub { die "sigpipe\n" }; + alarm(8); + $s = IO::Socket::SSL->new( + Proto => 'tcp', + PeerAddr => '127.0.0.1', + PeerPort => port(8080), + SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(), + SSL_alpn_protocols => $alpn, + SSL_error_trap => sub { die $_[1] } + ); + alarm(0); + }; + alarm(0); + + if ($@) { + log_in("died: $@"); + return undef; + } + + return $s; +} + +############################################################################### + +sub reply_handler { + my ($recv_data, $port, $state, %extra) = @_; + + 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 DNAME => 39; + + use constant IN => 1; + + # default values + + my ($hdr, $rcode, $ttl) = (0x8180, NOERROR, 3600); + + # 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 (($type == A) && exists($aroute_map{$name})) { + + my @records = @{$aroute_map{$name}}; + + for (my $i = 0; $i < scalar(@records); $i++) { + my ($ttl, $origin_addr) = @{$records[$i]}; + push @rdata, rd_addr($ttl, $origin_addr); + + #print("dns reply: $name $ttl $class $type $origin_addr\n"); + } + } + + $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)'; + + return pack 'n3N', 0xc00c, A, IN, $ttl if $addr eq ''; + + pack 'n3N nC4', 0xc00c, A, IN, $ttl, eval "scalar $code", eval($code); +} + +sub dns_daemon { + my ($port, $t, %extra) = @_; + + print("+ dns daemon: try to listen on 127.0.0.1:$port\n"); + + 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 $sel = IO::Select->new($socket); + my $tcp = 0; + + if ($extra{tcp}) { + $tcp = port(8983, socket => 1); + $sel->add($tcp); + } + + local $SIG{PIPE} = 'IGNORE'; + + # track number of relevant queries + + my %state = ( + cnamecnt => 0, + twocnt => 0, + ttlcnt => 0, + ttl0cnt => 0, + cttlcnt => 0, + cttl2cnt => 0, + manycnt => 0, + casecnt => 0, + idcnt => 0, + fecnt => 0, + ); + + # signal we are ready + + open my $fh, '>', $t->testdir() . '/' . $port; + close $fh; + + while (my @ready = $sel->can_read) { + foreach my $fh (@ready) { + if ($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, $port, + \%state); + $fh->send($data); + + } else { + $fh->recv($recv_data, 65536); + unless (length $recv_data) { + $sel->remove($fh); + $fh->close; + next; + } + +again: + my $len = unpack("n", $recv_data); + $data = substr $recv_data, 2, $len; + $data = reply_handler($data, $port, \%state, + tcp => 1); + $data = pack("n", length $data) . $data; + $fh->send($data); + $recv_data = substr $recv_data, 2 + $len; + goto again if length $recv_data; + } + } + } +} + +###############################################################################