package POEDaemon::TCPServer; use strict; use warnings FATAL => 'all'; no warnings 'redefine'; use Data::Dumper; use Socket qw(PF_INET PF_INET6 IPPROTO_TCP TCP_NODELAY sockaddr_family inet_ntop); use POE qw(Wheel::SocketFactory Wheel::ReadWrite Filter::Stream Filter::HTTPD Filter::SSL); use POEDaemon; sub IPPROTO_SCTP () { return getprotobyname 'sctp'; } sub SCTP_NODELAY () { return 0x00000004; } sub states { return $_[0], [qw( tcpserver_start tcpserver_stop tcpserver_init tcpserver_client_accept tcpserver_server_error tcpserver_client_disconnect_cleanup tcpserver_client_error tcpserver_client_kick tcpserver_client_ban tcpserver_ssl_error tcpserver_client_filter_buffer_full_error tcpserver_periodic3s_idle_check tcpserver_periodic3s_flood_check tcpserver_periodic3s_ssl_handshake_check tcpserver_periodic60s_bans_clear )]; } sub tcpserver_start { my ($kernel, $heap) = @_[KERNEL, HEAP]; log_enabled && logline 'tcpserver start'; log_enabled && logline "using POE::Filter::SSL v%s", $POE::Filter::SSL::VERSION || 'n/a'; my $socket_can_disable_v6only = socket_can_disable_v6only; log_enabled && logline "socket_can_disable_v6only = %s", dumper_oneline $socket_can_disable_v6only; foreach my $inet6 (qw(0 1)) { my $socket_domain = sprintf "inet%s", ($inet6 ? '6' : ''); my $bind_address_inet = cfg->{bind_address_inet}; my $bind_address_inet6 = cfg->{bind_address_inet6}; $bind_address_inet = '0.0.0.0' unless defined $bind_address_inet; $bind_address_inet6 = '::' unless defined $bind_address_inet6; my $bind_address = $inet6 ? $bind_address_inet6 : $bind_address_inet; foreach my $sctp (qw(0 1)) { next if $sctp && !cfg->{bind_sctp}; foreach my $ssl (qw(0 1)) { foreach my $http (qw(0 1)) { my $bind_port = cfg->{sprintf "bind_port%s%s", ($ssl ? '_ssl' : ''), ($http ? '_http' : '')}; next unless defined $bind_port; unless ($bind_port) { log_enabled && logline "invalid bind port: %s", dumper_oneline { ssl => $ssl, http => $http, bind_port => $bind_port, }; next; } if (!$inet6 && $bind_address_inet eq '0.0.0.0' && $bind_address_inet6 eq '::' && $socket_can_disable_v6only) { log_enabled && logline "skip inet listen (using v6only=0 on inet6): %s", dumper_oneline { socket_can_disable_v6only => $socket_can_disable_v6only, inet6 => $inet6, socket_domain => $socket_domain, bind_address_inet => $bind_address_inet, bind_address_inet6 => $bind_address_inet6, bind_address => $bind_address, sctp => $sctp, ssl => $ssl, http => $http, bind_port => $bind_port, }; next; } $kernel->yield(tcpserver_init => { socket_domain => $socket_domain, ($sctp ? (socket_protocol => 'sctp') : ()), bind_address => $bind_address, bind_port => $bind_port, ($ssl ? (ssl => $ssl) : ()), ($http ? (http => $http) : ()), }); } } } } $heap->{periodic}->{tcpserver_periodic3s_idle_check} = 3; $heap->{periodic}->{tcpserver_periodic3s_flood_check} = 3; $heap->{periodic}->{tcpserver_periodic3s_ssl_handshake_check} = 3; $heap->{periodic}->{tcpserver_periodic60s_bans_clear} = 60; } sub tcpserver_stop { my $heap = $_[HEAP]; log_enabled && logline 'tcpserver stop'; delete $heap->{tcpserver}; } sub tcpserver_init { my ($heap, $args) = @_[HEAP, ARG0]; my $socket_domain = $args->{socket_domain}; my $socket_protocol = $args->{socket_protocol}; my $bind_address = $args->{bind_address}; my $bind_port = $args->{bind_port}; my $ssl = $args->{ssl}; my $http = $args->{http}; return unless defined $socket_domain && $bind_address && $bind_port; my $socket_domain_number; if ($socket_domain =~ /^\d+$/) { $socket_domain_number = $socket_domain; } elsif ($socket_domain =~ /^inet6?$/) { if ($socket_domain eq 'inet') { $socket_domain_number = PF_INET; } elsif ($socket_domain eq 'inet6') { $socket_domain_number = PF_INET6; } } else { log_enabled && logline "invalid socket_domain '%s'", $socket_domain; return; } $socket_protocol = 'tcp' unless defined $socket_protocol; my $wheel = POE::Wheel::SocketFactory->new ( SocketDomain => $socket_domain_number, SocketProtocol => $socket_protocol, BindAddress => $bind_address, BindPort => $bind_port, (cfg->{bind_reuse} ? (Reuse => 1) : ()), (cfg->{nodelay_disabled} ? () : (NoDelay => 1)), (socket_can_disable_v6only ? (V6Only => 0) : ()), ListenQueue => 32, SuccessEvent => 'tcpserver_client_accept', FailureEvent => 'tcpserver_server_error', ); my $wheel_id = $wheel->ID; my $sockinfo_local = get_sockinfo $wheel->getsockname; log_enabled && logline "[tcpserver #%s] listen on [%s]:%s %s%s%s (local: %s %s %s)", $wheel_id, $bind_address, $bind_port, uc $socket_protocol, ($ssl ? ' SSL' : ''), ($http ? ' HTTP' : ''), uc($sockinfo_local->{family} || 'n/a'), $sockinfo_local->{addr} || 'n/a', $sockinfo_local->{port} || 'n/a'; $heap->{tcpserver}->{servers}->{$wheel_id} = { wheel => $wheel, wheel_id => $wheel_id, socket_domain => $socket_domain, socket_protocol => $socket_protocol, bind_address => $bind_address, bind_port => $bind_port, ($ssl ? (ssl => $ssl) : ()), ($http ? (http => $http) : ()), sockinfo_local => $sockinfo_local, args => $args, }; } sub tcpserver_client_accept { my ($kernel, $heap, $client_socket, $client_addr_packed, $client_port, $sock_wheel_id) = @_[KERNEL, HEAP, ARG0 .. ARG3]; my $client_sockaddr = getpeername $client_socket; unless (defined $client_sockaddr) { log_enabled && logline "invalid (undef) client_sockaddr, error = '%s'", $!; return; } my $client_addr; if ($client_addr_packed =~ /^[\w\.\-\:]+$/i) { log_enabled && logline "using workaround for addr, because addr = '%s'", hexval $client_addr_packed; $client_addr = get_sockinfo($client_sockaddr)->{addr}; } else { #log_enabled && logline "binary addr = '%s'", hexval $client_addr_packed; $client_addr = inet_ntop sockaddr_family($client_sockaddr), $client_addr_packed; } unless ($client_port =~ /^\d+$/) { log_enabled && logline "using workaround for port, because port = '%s'", hexval $client_port; $client_port = get_sockinfo($client_sockaddr)->{port}; } return if $heap->{tcpserver}->{client_banlist}->{$client_addr}; my @banned_clients = keys %{$heap->{tcpserver}->{client_banlist}}; my $banned_client_count = $#banned_clients + 1; return if $banned_client_count > 100; my @clients = keys %{$heap->{tcpserver}->{connections}}; my $client_count = $#clients + 1; my $max_client_count = cfg->{limits}->{max_client_count} || 50; if ($client_count >= $max_client_count) { log_enabled && logline "[tcpserver #%s] client connect denied, max conns ('%s') reached: addr = '%s', port = '%s'", $sock_wheel_id, $max_client_count, $client_addr, $client_port; return; } my $client_count_peraddr = 0; foreach (@clients) { $client_count_peraddr++ if $heap->{tcpserver}->{connections}->{$_}->{client_addr} eq $client_addr; } my $max_client_count_per_addr = cfg->{limits}->{max_client_count_per_addr} || 10; return if $client_count_peraddr >= $max_client_count_per_addr; my $socket_protocol = $heap->{tcpserver}->{servers}->{$sock_wheel_id}->{socket_protocol}; my $ssl = $heap->{tcpserver}->{servers}->{$sock_wheel_id}->{ssl}; my $http = $heap->{tcpserver}->{servers}->{$sock_wheel_id}->{http}; my ($filter, $stream_filter, $ssl_prefilter, $ssl_filter, $fastcgi, $poe_filter_httpd_new_params); if (0) { $stream_filter = POE::Filter::Stream->new ( MaxBuffer => 1048576, #MaxBuffer => 1024, ); } my $line_filter = POE::Filter::Line->new ( MaxBuffer => 8192, MaxLength => 4096, InputLiteral => "\n", OutputLiteral => "\n", ); my $ssl_filter_extra_params = { #MaxBuffer => 1048576, FailureEvent => [ MAIN_POE_SESSION_NAME, 'tcpserver_ssl_error' ], }; if ($http) { my $http_acl_re = cfg->{http_acl}; if (ref $http_acl_re eq 'Regexp' && $client_addr !~ $http_acl_re) { log_enabled && logline "[tcpserver #%s] http client connect denied: addr = '%s', port = '%s'", $sock_wheel_id, $client_addr, $client_port; return; } #log_enabled && logline 'http client connected'; $poe_filter_httpd_new_params = { MaxBuffer => 65536, MaxContent => 32768, }; my $httpd_filter = POE::Filter::HTTPD->new(%{$poe_filter_httpd_new_params || {}}); if ($ssl) { my $ssl_filter_config = cfg->{tcpserver_poe_filter_ssl_new_params_http}; unless (ref $ssl_filter_config eq 'HASH' && %$ssl_filter_config) { log_enabled && logline "tcpserver_poe_filter_ssl_new_params_http invalid:\n%s", Dumper $ssl_filter_config; return; } $ssl_filter = POE::Filter::SSL->new(%$ssl_filter_extra_params, %$ssl_filter_config); $filter = POE::Filter::Stackable->new ( Filters => [ ($stream_filter ? $stream_filter : ()), $ssl_filter, $httpd_filter, ], ); } else { $filter = POE::Filter::Stackable->new ( Filters => [ ($stream_filter ? $stream_filter : ()), $httpd_filter, ], ); } } elsif ($ssl) { #if ($POE::VERSION >= 1.359) #{ # log_enabled && logline 'using filter before POE::Filter::SSL (against ddos)'; # # # $ssl_prefilter = POE::Filter::Block->new # ( # MaxBuffer => 102400, # MaxLength => 1, # ); #} $ssl_filter = POE::Filter::SSL->new(%$ssl_filter_extra_params, %{cfg->{tcpserver_poe_filter_ssl_new_params}}); $filter = POE::Filter::Stackable->new ( Filters => [ ($stream_filter ? $stream_filter : ()), ($ssl_prefilter ? $ssl_prefilter : ()), $ssl_filter, $line_filter, ], ); } else { $filter = $line_filter; my $acl_re = cfg->{nonssl_server_acl}; if (ref $acl_re eq 'Regexp' && $client_addr !~ $acl_re) { log_enabled && logline "[tcpserver #%s] client connect denied: addr = '%s', port = '%s'", $sock_wheel_id, $client_addr, $client_port; return; } my $fastcgi_acl_re = cfg->{fastcgi_acl}; if (ref $fastcgi_acl_re eq 'Regexp' && $client_addr =~ $fastcgi_acl_re) { $fastcgi = 1; } } my ($tcp_nodelay, $sctp_nodelay); if ($socket_protocol eq 'tcp') { my $tcp_nodelay_packed = getsockopt $client_socket, IPPROTO_TCP, TCP_NODELAY; $tcp_nodelay = unpack 'I', $tcp_nodelay_packed if defined $tcp_nodelay_packed; } elsif ($socket_protocol eq 'sctp') { my $sctp_nodelay_packed = getsockopt $client_socket, IPPROTO_SCTP, SCTP_NODELAY; $sctp_nodelay = unpack 'I', $sctp_nodelay_packed if defined $sctp_nodelay_packed; } my $wheel = POE::Wheel::ReadWrite->new ( Handle => $client_socket, ($filter ? (Filter => $filter) : ()), InputEvent => 'tcpserver_client_input', ErrorEvent => 'tcpserver_client_error', ); my $wheel_id = $wheel->ID; log_enabled && logline "[tcpserver #%s conn #%s] connection from [%s]:%s %s%s%s", $sock_wheel_id, $wheel_id, $client_addr, $client_port, uc $socket_protocol, $tcp_nodelay ? ' (TCP_NODELAY)' : '', $sctp_nodelay ? ' (SCTP_NODELAY)' : ''; prod_log_enabled && prod_logline "server %s connection %s from %s port %s", $sock_wheel_id, $wheel_id, $client_addr, $client_port; if (exists $heap->{statelist}->{eventsystem_input} && $heap->{statelist}->{eventsystem_input}) { $kernel->yield(eventsystem_input => { type => 'clientconnect', ip => $client_addr, time => time_hires, }); } $heap->{tcpserver}->{connections}->{$wheel_id} = { wheel => $wheel, wheel_id => $wheel_id, sock_wheel_id => $sock_wheel_id, #ssl_prefilter => $ssl_prefilter, ($ssl_filter ? (ssl_filter => $ssl_filter) : ()), ($poe_filter_httpd_new_params ? (poe_filter_httpd_new_params => $poe_filter_httpd_new_params) : ()), client_addr => $client_addr, client_port => $client_port, connect_time => time_hires, ($fastcgi ? (fastcgi => $fastcgi) : ()), ($ssl ? (ssl => $ssl) : ()), ($http ? (http => $http) : ()), ($http ? (client_max_idle_time_disconnect => 300) : ()), }; $heap->{tcpserver}->{accept_client_addr} = $client_addr; $heap->{tcpserver}->{accept_count} = 0 unless defined $heap->{tcpserver}->{accept_count}; $heap->{tcpserver}->{accept_count}++; if (cfg->{tcpserver_client_connected_gpio_pin}) { $kernel->yield(gpiochange_set => { pin => cfg->{tcpserver_client_connected_gpio_pin}, value => GPIO_HIGH }); } } sub tcpserver_server_error { my ($kernel, $heap, $operation, $errnum, $errstr, $sock_wheel_id) = @_[KERNEL, HEAP, ARG0 .. ARG3]; log_enabled && logline "[tcpserver #%s] %s error %s: %s", $sock_wheel_id, $operation, $errnum, $errstr; if (!$heap->{shutdown} && $operation eq 'bind' && $errnum == 48 && $errstr eq 'Address already in use') { $kernel->delay(tcpserver_init => connretry_rand() => $heap->{tcpserver}->{servers}->{$sock_wheel_id}->{args}); } delete $heap->{tcpserver}->{servers}->{$sock_wheel_id}; } sub tcpserver_client_disconnect_cleanup { my ($kernel, $heap, $args) = @_[KERNEL, HEAP, ARG0]; my $wheel_id = $args->{wheel_id}; my $wheel_error = $args->{wheel_error}; my $reason = $args->{reason}; my @log_extra; push @log_extra, sprintf "wheel_error = '%s'", join ' ', @$wheel_error if defined $wheel_error; push @log_extra, sprintf "reason = '%s'", $reason if defined $reason; log_enabled && logline "[tcpserver #%s conn #%s] disconnect cleanup%s", $heap->{tcpserver}->{connections}->{$wheel_id}->{sock_wheel_id}, $wheel_id, (@log_extra ? sprintf ": %s", join ', ', @log_extra : ''); my $cn = $heap->{tcpserver}->{connections}->{$wheel_id}->{cn}; delete $heap->{tcpserver}->{connections_by_cn}->{$cn} if $cn; my $old_client = delete $heap->{tcpserver}->{connections}->{$wheel_id}; #log_enabled && logline "old_client=\n%s", Dumper $old_client; $kernel->yield(tcpserver_client_disconnected => { old_client => $old_client, wheel_error => $wheel_error, reason => $reason, }); if (cfg->{tcpserver_client_connected_gpio_pin} && !%{$heap->{tcpserver}->{connections}}) { $kernel->yield(gpiochange_set => { pin => cfg->{tcpserver_client_connected_gpio_pin}, value => GPIO_LOW, }); } } sub tcpserver_client_error { my ($kernel, $session, $heap, $operation, $errnum, $errstr, $wheel_id) = @_[KERNEL, SESSION, HEAP, ARG0 .. ARG3]; log_enabled && logline "[tcpserver #%s conn #%s] disconnected: operation = '%s', errnum = '%s', errstr = '%s'", $heap->{tcpserver}->{connections}->{$wheel_id}->{sock_wheel_id}, $wheel_id, $operation, $errnum, $errstr; prod_log_enabled && prod_logline "client %s disconnected - %s: %s: %s", $wheel_id, dumper_oneline($operation), dumper_oneline($errnum), dumper_oneline($errstr); my $wheel_error = [ defined $operation && length $operation ? $operation : 'n/a', defined $errnum && length $errnum ? $errnum : 'n/a', defined $errstr && length $errstr ? $errstr : 'n/a', ]; $kernel->call($session => tcpserver_client_disconnect_cleanup => { wheel_id => $wheel_id, wheel_error => $wheel_error, }); } sub tcpserver_client_kick { my ($kernel, $session, $heap, $args) = @_[KERNEL, SESSION, HEAP, ARG0]; my $wheel_id = $args->{wheel_id}; my $reason = $args->{reason}; my $client = $heap->{tcpserver}->{connections}->{$wheel_id}; my $sock_wheel_id = $client->{sock_wheel_id}; my $client_addr = $client->{client_addr}; log_enabled && logline "[tcpserver #%s conn #%s] kicked addr '%s': reason = '%s'", $sock_wheel_id, $wheel_id, $client_addr, $reason; prod_log_enabled && prod_logline "server %s connection %s ip %s kicked - %s", $sock_wheel_id, $wheel_id, $client_addr, dumper_oneline $reason; $kernel->call($session => tcpserver_client_disconnect_cleanup => { wheel_id => $wheel_id, reason => $reason, }); } sub tcpserver_client_ban { my ($kernel, $session, $heap, $args) = @_[KERNEL, SESSION, HEAP, ARG0]; my $wheel_id = $args->{wheel_id}; my $reason = $args->{reason} || ''; my $client_addr; if ($wheel_id) { my $client = $heap->{tcpserver}->{connections}->{$wheel_id}; my $sock_wheel_id = $client->{sock_wheel_id}; $client_addr = $client->{client_addr}; log_enabled && logline "[tcpserver #%s conn #%s] banned addr '%s': reason = '%s'", $sock_wheel_id, $wheel_id, $client_addr, $reason; prod_log_enabled && prod_logline "server %s connection %s ip %s banned - %s", $sock_wheel_id, $wheel_id, $client_addr, dumper_oneline $reason; } elsif ($args->{client_addr}) { $client_addr = $args->{client_addr}; log_enabled && logline "[tcpserver] banned addr '%s': reason = '%s'", $client_addr, $reason; prod_log_enabled && prod_logline "ip %s banned - %s", $client_addr, dumper_oneline $reason; } else { log_enabled && logline 'error, need wheel_id or client_addr to ban clients'; return; } $heap->{tcpserver}->{client_banlist}->{$client_addr} = { reason => $reason, time => time_hires, }; if ($wheel_id) { $kernel->call($session => tcpserver_client_disconnect_cleanup => { wheel_id => $wheel_id, reason => $reason, }); } foreach my $curr_wheel_id (keys %{$heap->{tcpserver}->{connections} || {}}) { my $curr_client = $heap->{tcpserver}->{connections}->{$curr_wheel_id}; next unless $curr_client; my $curr_client_addr = $curr_client->{client_addr}; next unless $curr_client_addr && $curr_client_addr eq $client_addr; my $curr_reason = sprintf "ip %s is already banned because: %s", $client_addr, $reason; $kernel->call($session => tcpserver_client_kick => { wheel_id => $curr_wheel_id, reason => $curr_reason, }); } } sub tcpserver_ssl_error { my ($kernel, $session, $heap, $ssl_error_message) = @_[KERNEL, SESSION, HEAP, ARG0]; my $reason = sprintf "ssl fatal error: %s", $ssl_error_message; foreach my $wheel_id (keys %{$heap->{tcpserver}->{connections}}) { my $client = $heap->{tcpserver}->{connections}->{$wheel_id}; next unless $client; my $ssl_filter = $client->{ssl_filter}; next unless $ssl_filter && $ssl_filter->{unexpected_error}; $kernel->call($session => tcpserver_client_ban => { wheel_id => $wheel_id, reason => $reason, }); } } sub tcpserver_client_filter_buffer_full_error { my ($kernel, $session, $heap, $args) = @_[KERNEL, SESSION, HEAP, ARG0]; my $error_wheel_id = $args->{error_wheel_id} || return; my $reason = 'client filter buffer full'; foreach my $wheel_id (keys %{$heap->{tcpserver}->{connections}}) { next unless $wheel_id == $error_wheel_id; $kernel->call($session => tcpserver_client_ban => { wheel_id => $wheel_id, reason => $reason, }); } } sub tcpserver_periodic3s_idle_check { my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP]; foreach my $wheel_id (keys %{$heap->{tcpserver}->{connections}}) { my $client = $heap->{tcpserver}->{connections}->{$wheel_id}; next unless $client; my $sock_wheel_id = $client->{sock_wheel_id}; my $max_idle_time = $client->{client_max_idle_time}; my $max_idle_time_disconnect = $client->{client_max_idle_time_disconnect} || 604800; my ($connected, $input_idle, $output_idle); if ($client->{connect_time}) { $connected = time_hires - $client->{connect_time}; } if ($client->{last_input_time}) { $input_idle = time_hires - $client->{last_input_time}; } if ($client->{last_output_time}) { $output_idle = time_hires - $client->{last_output_time}; } #log_enabled && logline "WID #%s max_idle_time=%s connected=%s input_idle=%s output_idle=%s", # $wheel_id, # $max_idle_time || 'n/a', # defined $connected ? concise_duration $connected : 'n/a', # defined $input_idle ? concise_duration $input_idle : 'n/a', # defined $output_idle ? concise_duration $output_idle : 'n/a'; if (defined $connected && $connected >= $max_idle_time_disconnect && (!defined $input_idle || $input_idle >= $max_idle_time_disconnect || !defined $output_idle || $output_idle >= $max_idle_time_disconnect)) { my $reason = sprintf "ping timeout, idle %s (max before disconnect %s)", (defined $input_idle ? concise_duration $input_idle : 'n/a'), concise_duration $max_idle_time_disconnect; $kernel->call($session => tcpserver_client_kick => { wheel_id => $wheel_id, reason => $reason, }); } elsif ($max_idle_time && (!defined $input_idle || $input_idle >= $max_idle_time)) { log_enabled && logline "[tcpserver #%s conn #%s] is idle for more than %s (max %s)", $sock_wheel_id, $wheel_id, (defined $input_idle ? concise_duration $input_idle : 'n/a'), concise_duration $max_idle_time; $kernel->delay_set(tcpserver_client_idle => client_periodic_cmd_delay_rand() => $wheel_id); } } } sub tcpserver_periodic3s_flood_check { my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP]; my $per_second_divider = 3; my $accept_count = $heap->{tcpserver}->{accept_count}; if ($accept_count) { my $last_accept_count = $heap->{tcpserver}->{last_accept_count}; my $accept_client_addr = $heap->{tcpserver}->{accept_client_addr}; if ($last_accept_count) { my $last_accept_client_addr = $heap->{tcpserver}->{last_accept_client_addr}; my $accept_paused = $heap->{tcpserver}->{accept_paused}; my $accepts_per_second = ($accept_count - $last_accept_count) / $per_second_divider; my $max_accepts_per_second = cfg->{limits}->{max_accepts_per_second} || 10; if ($accepts_per_second > $max_accepts_per_second) { unless ($accept_paused) { log_enabled && logline "accept rate %.3f/s > %s/s, pause_accept", $accepts_per_second, $max_accepts_per_second; prod_log_enabled && prod_logline "accept rate %.3f/s > %s/s - pause socket accept", $accepts_per_second, $max_accepts_per_second; foreach my $sock_wheel_id (keys %{$heap->{tcpserver}->{servers} || {}}) { $heap->{tcpserver}->{servers}->{$sock_wheel_id}->{wheel}->pause_accept; } $heap->{tcpserver}->{accept_paused} = 1; if ($accept_client_addr && $last_accept_client_addr && $accept_client_addr eq $last_accept_client_addr) { my $reason = sprintf "accept flood, %.3f/s > %s/s", $accepts_per_second, $max_accepts_per_second; $kernel->call($session => tcpserver_client_ban => { client_addr => $accept_client_addr, reason => $reason, }); } } } else { if ($accept_paused) { log_enabled && logline "accept rate %.3f/s > %s/s, resume_accept", $accepts_per_second, $max_accepts_per_second; prod_log_enabled && prod_logline "accept rate %.3f/s > %s/s - resume socket accept", $accepts_per_second, $max_accepts_per_second; foreach my $sock_wheel_id (keys %{$heap->{tcpserver}->{servers} || {}}) { $heap->{tcpserver}->{servers}->{$sock_wheel_id}->{wheel}->resume_accept; } delete $heap->{tcpserver}->{accept_paused}; } } } $heap->{tcpserver}->{last_accept_count} = $accept_count; $heap->{tcpserver}->{last_accept_client_addr} = $accept_client_addr if $accept_client_addr; } foreach my $wheel_id (keys %{$heap->{tcpserver}->{connections}}) { my $client = $heap->{tcpserver}->{connections}->{$wheel_id}; next unless $client; my $sock_wheel_id = $client->{sock_wheel_id}; my $client_addr = $client->{client_addr}; my $http = $client->{http}; my $websocket = $client->{websocket}; my @flood; if ($http) { if ($websocket) { my $last_input_message_count = $client->{last_input_message_count}; my $last_output_message_count = $client->{last_output_message_count}; my $input_message_count = $client->{input_message_count}; my $output_message_count = $client->{output_message_count}; unless (defined $input_message_count && $input_message_count =~ /^\d+$/) { $client->{last_input_message_count} = 0; $client->{last_input_message_speed} = 0; next; } unless (defined $output_message_count && $output_message_count =~ /^\d+$/) { $client->{last_output_message_count} = 0; $client->{last_output_message_speed} = 0; next; } unless (defined $last_input_message_count && $last_input_message_count =~ /^\d+$/) { $last_input_message_count = $input_message_count; } unless (defined $last_output_message_count && $last_output_message_count =~ /^\d+$/) { $last_output_message_count = $output_message_count; } my $input_messages_per_second = ($input_message_count - $last_input_message_count) / $per_second_divider; my $output_messages_per_second = ($output_message_count - $last_output_message_count) / $per_second_divider; $client->{last_input_message_count} = $input_message_count; $client->{last_output_message_count} = $output_message_count; $client->{last_input_message_speed} = $input_messages_per_second; $client->{last_output_message_speed} = $output_messages_per_second; my $max_input_messages_per_second = 100; my $max_output_messages_per_second = $client->{admin} ? 1000 : 300; if ($input_messages_per_second > $max_input_messages_per_second) { push @flood, sprintf "in ws msgs %.3f/s > %s/s", $input_messages_per_second, $max_input_messages_per_second; } if ($output_messages_per_second > $max_output_messages_per_second) { push @flood, sprintf "out ws msgs %.3f/s > %s/s", $output_messages_per_second, $max_output_messages_per_second; } } else { my $last_httprequest_count = $client->{last_httprequest_count}; my $httprequest_count = $client->{httprequest_count}; unless (defined $httprequest_count && $httprequest_count =~ /^\d+$/) { $client->{last_httprequest_count} = 0; $client->{last_httprequest_speed} = 0; next; } unless (defined $last_httprequest_count && $last_httprequest_count =~ /^\d+$/) { $last_httprequest_count = $httprequest_count; } my $httprequests_per_second = ($httprequest_count - $last_httprequest_count) / $per_second_divider; $client->{last_httprequest_count} = $httprequest_count; $client->{last_httprequest_speed} = $httprequests_per_second; my $max_http_requests_per_second = cfg->{limits}->{max_http_requests_per_second} || 10; if ($httprequests_per_second > $max_http_requests_per_second) { push @flood, sprintf "httpreqs %.3f/s > %s/s", $httprequests_per_second, $max_http_requests_per_second; } } } else { my $last_input_line_count = $client->{last_input_line_count}; my $last_output_line_count = $client->{last_output_line_count}; my $input_line_count = $client->{input_line_count}; my $output_line_count = $client->{output_line_count}; unless (defined $input_line_count && $input_line_count =~ /^\d+$/) { $client->{last_input_line_count} = 0; $client->{last_input_line_speed} = 0; next; } unless (defined $output_line_count && $output_line_count =~ /^\d+$/) { $client->{last_output_line_count} = 0; $client->{last_output_line_speed} = 0; next; } unless (defined $last_input_line_count && $last_input_line_count =~ /^\d+$/) { $last_input_line_count = $input_line_count; } unless (defined $last_output_line_count && $last_output_line_count =~ /^\d+$/) { $last_output_line_count = $output_line_count; } my $input_lines_per_second = ($input_line_count - $last_input_line_count) / $per_second_divider; my $output_lines_per_second = ($output_line_count - $last_output_line_count) / $per_second_divider; $client->{last_input_line_count} = $input_line_count; $client->{last_output_line_count} = $output_line_count; $client->{last_input_line_speed} = $input_lines_per_second; $client->{last_output_line_speed} = $output_lines_per_second; my $max_input_lines_per_second = 100; my $max_output_lines_per_second = $client->{admin} ? 1000 : 300; if ($input_lines_per_second > $max_input_lines_per_second) { push @flood, sprintf "in lines %.3f/s > %s/s", $input_lines_per_second, $max_input_lines_per_second; } if ($output_lines_per_second > $max_output_lines_per_second) { push @flood, sprintf "out lines %.3f/s > %s/s", $output_lines_per_second, $max_output_lines_per_second; } } if (@flood) { my $reason = sprintf "flood %s", join ', ', @flood; $kernel->call($session => tcpserver_client_ban => { wheel_id => $wheel_id, reason => $reason, }); } } } sub tcpserver_periodic3s_ssl_handshake_check { my ($kernel, $session, $heap) = @_[KERNEL, SESSION, HEAP]; my $reason = 'ssl handshake failure/timeout'; foreach my $wheel_id (keys %{$heap->{tcpserver}->{connections}}) { my $client = $heap->{tcpserver}->{connections}->{$wheel_id}; next unless $client; #my $sock_wheel_id = $client->{sock_wheel_id}; #my $client_addr = $client->{client_addr}; my $ssl_filter = $client->{ssl_filter}; next unless $ssl_filter; #log_enabled && logline "[tcpserver #%s conn #%s] (addr = '%s') length client->{ssl_prefilter}->get_pending = '%s'\n%s", # $sock_wheel_id, # $wheel_id, # $client_addr, # length($client->{ssl_prefilter}->get_pending()) || 'n/a', # Dumper # [ # $client->{ssl_prefilter}, # #$ssl_filter->get_net_ssleay_pending, # #$ssl_filter->get_buflen, # $ssl_filter->{unexpected_error}, # ]; if ($ssl_filter->{unexpected_error} || (!$ssl_filter->handshakeDone && (time_hires - $client->{connect_time}) > 30)) { $kernel->call($session => tcpserver_client_ban => { wheel_id => $wheel_id, reason => $reason, }); } } } sub tcpserver_periodic60s_bans_clear { my $heap = $_[HEAP]; foreach my $client_addr (keys %{$heap->{tcpserver}->{client_banlist}}) { my $item = $heap->{tcpserver}->{client_banlist}->{$client_addr}; my $reason = $item->{reason} || ''; my $time = $item->{time} || 0; next unless (time_hires - $time) >= 60; log_enabled && logline "unban '%s', reason was '%s'", $client_addr, $reason; prod_log_enabled && prod_logline "ip %s unbanned - %s", $client_addr, dumper_oneline $reason; delete $heap->{tcpserver}->{client_banlist}->{$client_addr}; } } 1;