Закон №193-ФЗ: взгляд со стороны небольшого провайдера

от автора

Пару недель назад к моему хорошему знакомому (по совместительству системному администратору в городском, но не очень крупном провайдере) коллеги из Роскомнадзора поставили на вид, что по IP он, конечно, блокирует запрещенные сайты, но по URL или доменным именам совершенно нет, а при смене IP адреса запрещенным ресурсом тот снова начинает открываться. Товарищ пожаловался мне на судьбу, заодно намекнув, что один раз лично его уже штрафовали за невыполнение требований печально известного № 139-ФЗ.


Схема подключений у данного провайдера следующая:

  • Все пользователи (кроме купивших статический IP) находятся за NAT;
  • NAT реализован как обычный forwarding на не менее обычном debian;
  • На каждом NAT есть не менее обычный iptables;
  • Есть два своих DNS-сервера, работающих на том же debian и PowerDNS.

После некоторых раздумий, а также гугления информации о layer 7 filtering на коленке, гугл нам сказал, что подобные вещи — прерогатива дорогого оборудования, a l7-filter для iptables перестал развиваться достаточно давно. После этого мы начали думать в сторону прокси-серверов. Со squid в промышленных масштабах ни один из нас не работал, однако было довольно много экспериментов с nginx — очень мощным продуктом Игоря Сысоева.

Общая схема работы была выработана следующая:

  • iptables на NAT перехватывает все DNS-запросы и перенаправляет их на наш DNS-сервер;
  • DNS-сервер держит зоны в том числе и запрещенных ресурсов, не пытаясь их обновить через recursor;
  • Эти зоны единственным адресом данных ресурсов указывают адрес сервера с nginx.

Понятно, что конфигурацию nginx и список зон запрещенных ресурсов необходимо обновлять динамически: в требованиях Роскомнадзора указана частота 3 раза в сутки. Также очевиден и недостаток: доступ по HTTPS придется либо ограничивать, либо разрешать, но с использованием собственного сертификата, иначе трафик через наш nginx будет шифрован и свою основную функцию (фильтрацию) он осуществлять не сможет. Во втором варианте неизбежна ругань браузеров клиентов на неправильный сертификат. Более того, если под блокировку попадет адрес из списка доменов google, а у клиента установлен google chrome, то клиента на данные сайты не пустит в принципе. Но других наколенных вариантов мы в ограниченное время изобрести не смогли. Итак, что у нас получилось:

  1. Скрипт, загружающий список запрещенных сайтов (обычный XML, отдается SOAP-сервисом после соответствующего запроса. Необходима авторизация с помощью сертификата, уникального для каждого провайдера.
  2. Скрипт, загружающий список зон в DNS-сервера.
  3. Скрипт, создающий необходимую конфигурацию для nginx.

Первый скрипт я приводить не буду: провайдеры сами знают как получить информацию, а обычным пользователям ее разглашать не рекомендуется.

Второй скрипт (SQL для работы с базой PowerDNS):

USE pdns;  create temporary table if not exists dump(         domain text );  TRUNCATE TABLE dump;  load data local infile '/opt/zapret/dump.xml' into table dump LINES STARTING BY '<domain>' TERMINATED BY '</domain>' (@tmp) SET domain = ExtractValue(@tmp, '/');  create temporary table if not exists locked(         domain varchar(767) primary key );  TRUNCATE TABLE locked;  INSERT INTO locked SELECT DISTINCT domain FROM dump;  create temporary table if not exists locked1(         domain varchar(767) primary key );  TRUNCATE TABLE locked1;  INSERT INTO locked1 SELECT * FROM locked;  DELETE FROM l USING locked l INNER JOIN locked1 l1 ON l.domain=SUBSTR(l1.domain from 5);  UPDATE locked SET domain=SUBSTR(LCASE(domain) FROM 5) WHERE LEFT(LCASE(domain), 4) = 'www.';  DELETE FROM locked WHERE domain LIKE '%youtube.com' OR domain LIKE '%google.com' OR domain LIKE '%google.ru';  create temporary table if not exists old_locked(         id int,         domain varchar(767) primary key );  TRUNCATE TABLE old_locked;  INSERT INTO old_locked SELECT DISTINCT d.id, d.name as domain FROM domains d INNER JOIN records r ON d.id=r.domain_id WHERE r.content='1.2.3.4' AND d.name NOT LIKE '%provider.ru';  INSERT INTO domains (name, master, last_check, type, notified_serial, account) SELECT n.domain, NULL, NULL, 'NATIVE', NULL, NULL FROM locked n LEFT JOIN old_locked o ON n.domain=o.domain WHERE o.domain IS NULL;  INSERT INTO records (domain_id, name, type, content, ttl, prio, change_date, ordername, auth) SELECT d.id AS domain_id, d.name, 'SOA' as type, 'ns.provider.ru dns.provider.ru 2014022701 28800 7200 604800 86400' as content, 86400 as ttl, 0 as prio, 1393508792 as change_date, '' as ordername, 1 as auth FROM domains d INNER JOIN locked l ON d.name=l.domain LEFT JOIN old_locked o ON d.name=o.domain WHERE o.domain IS NULL;  INSERT INTO records (domain_id, name, type, content, ttl, prio, change_date, ordername, auth) SELECT d.id AS domain_id, CONCAT('*.', d.name), 'A' as type, '1.2.3.4' as content, 86400 as ttl, 0 as prio, 1393508820 as change_date, '' as ordername, 1 as auth FROM domains d INNER JOIN locked l ON d.name=l.domain LEFT JOIN old_locked o ON d.name=o.domain WHERE o.domain IS NULL;  DELETE FROM r, d USING records r INNER JOIN domains d ON r.domain_id=d.id INNER JOIN old_locked o ON d.name=o.domain LEFT JOIN locked l ON o.domain=l.domain WHERE l.domain IS NULL;  

После исполнения данного скрипта надо не забыть выполнить

# pdnssec rectify-all-zones 

для того, чтобы powerdns осознал изменения.

Третий скрипт (формирование списка blocked):

<?php  $xml = simplexml_load_file ('/opt/zapret/dump.xml');  $dirty = array();  $excl = array(); $excl[] = 'youtube.com'; $excl[] = 'google.ru'; $excl[] = 'google.com'; $excl[] = 'badsite.org';  foreach($xml as $node) { 	if( strlen( (string)$node->domain )>0 ) { 		$parsed = parse_url((string)$node->url); 		if( $parsed!=false ) { 		    if( isset($parsed['path']) ) { 			if( isset($parsed['scheme']) ) 				$scheme = $parsed['scheme'] . "://"; 			else 				$scheme = "http://";  			if( isset($parsed['port']) ) { 				$port = ':' . $parsed['port']; 				if( $scheme=="https://" ) $port = $port . " ssl"; 				} 			else {                                 if( $scheme=="https://" ) $port = ":443 ssl"; 				else $port = ":80"; 			}  			$port = $port . ";";  			$domain = (string)$node->domain;  			if( strcmp(strtolower(substr($domain, 0, 4)), 'www.') == 0 ) $domain = substr($domain, 4);  			if( isset($parsed['query']) ) 				$que = $parsed['query']; 			else 				$que = '';  			$que = str_replace('\\E', '\\E\\\\E\\Q', $que); 			$que = '\\Q' . $que . '\\E'; 			if ( strcmp($que, '\\Q\\E')==0 ) 				$que = '';   			$path = $parsed['path'];  			$path = str_replace('\\E', '\\E\\\\E\\Q', $path); 			if ( strcmp($path, '/')<>0 ) { 				$path = '\\Q' . $path . '\\E'; 				if ( strcmp($path, '\\Q\\E')==0 ) 					$path = ''; 			}  			$keys = preg_grep('/' . $domain . '/', $excl);  			if( count($keys)<1 ) 				$dirty[] = array('domain'=>$domain, 'url'=>(string)$node->url, 'loc'=>$path, 'query'=>$que, 'port'=>$port, 'scheme'=>$scheme); 		    } 		} 	} }  // Т.к. он забанен весь, а в списке фрагментарно $dirty[] = array('domain'=>'badsite.org', 'url'=>'badsite.org', 'loc'=>'/', 'query'=>'', 'port'=>':80;', 'scheme'=>'http://');  $sort_func = function($obj_1, $obj_2) {     	return strnatcasecmp($obj_1['domain'] . $obj_1['url'], $obj_2['domain'] . $obj_2['url']); };  $domains = array_unique($dirty, SORT_REGULAR);  usort($domains, $sort_func);  $old_domain = ""; $old_loc = ""; $wasroot = false; $alldomain = false; $allloc = false;  foreach($domains as $node) { 	$domain = $node['domain']; 	$url = $node['url']; 	$loc = $node['loc']; 	$query = $node['query']; 	$port = $node['port']; 	$scheme = $node['scheme'];  // echo "\n1. Root " . (string)$wasroot . "; alldomain " . (string)$alldomain . "; alloc " . (string) $allloc . "; loc '" . $loc . "'; query '" . $query . "'\n";  	if( strcmp($domain, $old_domain) ) { 		loc_close( $old_loc, ($alldomain || $wasroot || $allloc)); 		dom_close( $old_domain, $wasroot ); 		dom_open( $domain, $port, $scheme ); 		$old_loc = ''; 		$alldomain = false; 		$wasroot = false; 	}  	if( !$alldomain ){ 		if( strcmp($loc, $old_loc) ) { 			loc_close( $old_loc, ($alldomain || $allloc) ); 			loc_open( $loc ); 			$allloc = false; 		}  		if( strlen($loc)<2 && strlen($query)>0 ) 			$wasroot = true; 		if( strlen($loc)<2 && strlen($query)<1 ) 			{ $alldomain = true; $wasroot = true; } 		if( strlen($query)<1 ) 			$allloc = true; 		if( !$allloc ) 			args_check( $query ); 	} // echo "\n2. Root " . (string)$wasroot . "; alldomain " . (string)$alldomain . "; alloc " . (string) $allloc . "; loc '" . $loc . "'; query '" . $query . "'\n";  	$old_loc = $loc; 	$old_domain = $domain; }  loc_close( $loc, ($alldomain || $wasroot || $allloc) ); dom_close( $domain, $wasroot );   function loc_close( $_loc, $_alldomain ) {     if (strlen($_loc)>0 ) { 	if( $_alldomain ) { ?> 	return 301 http://eais.rkn.gov.ru/; <?php 	} 	else { //if ( strcmp($_loc, '/')==0 ) { ?>          include /etc/nginx/proxy_params;         if ($args = '') {                 proxy_pass $scheme://$host$uri;         }         if ($args != '') {                 proxy_pass $scheme://$host$uri?$args;         } <?php 	} 	echo "    } # location\n";     } }  function dom_close( $_dom, $_wasroot ) {     if( strlen($_dom)>0 ) { 	if( !$_wasroot ) { ?>      location / {         include /etc/nginx/proxy_params;          if ($args = '') {                 proxy_pass $scheme://$host$uri;         }         if ($args != '') {                 proxy_pass $scheme://$host$uri?$args;         }     } #root location <?php 	} 	echo "} #domain\n";     } }  function loc_open( $_loc ) { ?>      location ~* <?php echo $_loc . "* {\n"; ?> <?php }  function dom_open( $_domain, $_port, $_scheme ) { ?> server {     listen  1.2.3.4<?php echo $_port; 	if( strcmp( $_scheme, 'https:\/\/' )==0 ) echo ' ssl'; ?>      server_name <?php echo $_domain . " " . "*." . $_domain . ";\n";  }  function args_check( $_query ) {     if ( strlen($_query)>0 ) { 	echo "\t    if (\$args ~* \"";         if(strlen($_query)>0) echo $_query;         echo "*\") {\n";         echo "\t\treturn 301 http://eais.rkn.gov.ru/;\n"; 	echo "\t    } #args\n"; 	}     else echo "\treturn 301 http://eais.rkn.gov.ru/;\n"; } ?> 

По итогам исполнения третьего скрипта мы получаем конфигурацию для nginx, которая проксирует те доменные имена, которые получила. В случае, если адрес заблокирован, то осуществляется безусловный редирект (301) на адрес eais.rkn.gov.ru — реестра запрещенных сайтов.
Блокировка может быть трех типов:

1. Весь домен. Для таких сайтов мы получаем следующую запись:

server {     listen  1.2.3.4:80;     server_name badsite.org *.badsite.org;      location ~* /* {         return 301 http://eais.rkn.gov.ru/;     } # location } #domain 

2. Определенные URL в домене. В этом случае мы получаем другую запись:

server {     listen  1.2.3.4:80;     server_name badsite.hk *.badsite.hk;      location ~* \Q/h/\E* {         return 301 http://eais.rkn.gov.ru/;     } # location      location ~* \Q/h/res/214.html\E* {         return 301 http://eais.rkn.gov.ru/;     } # location      location / {         include /etc/nginx/proxy_params;          if ($args = '') {                 proxy_pass $scheme://$host$uri;         }         if ($args != '') {                 proxy_pass $scheme://$host$uri?$args;         }     } #root location } #domain 

3. Определенные аргументы у определенного URL (например, конкретный пост в PHPBB):

server {     listen  1.2.3.4:80;     server_name badsite.com *.badsite.com;      location ~* \Q/forum/viewforum.php\E* {             if ($args ~* "\Qf=6\E*") {                 return 301 http://eais.rkn.gov.ru/;             } #args             if ($args ~* "\Qf=6&start=25\E*") {                 return 301 http://eais.rkn.gov.ru/;             } #args          include /etc/nginx/proxy_params;         if ($args = '') {                 proxy_pass $scheme://$host$uri;         }         if ($args != '') {                 proxy_pass $scheme://$host$uri?$args;         }     } # location      location / {         include /etc/nginx/proxy_params;          if ($args = '') {                 proxy_pass $scheme://$host$uri;         }         if ($args != '') {                 proxy_pass $scheme://$host$uri?$args;         }     } #root location } #domain< 

И, конечно, в default есть проброс всех запросов к соответствующим адресам (на всякий случай):

server {         listen 1.2.3.4:80 default_server;          location / {                 include /etc/nginx/proxy_params;                 if ($args = '') {                         proxy_pass $scheme://$host$uri;                 }                 if ($args != '') {                         proxy_pass $scheme://$host$uri?$args;                 }         } }  server {         listen 1.2.3.4:443 ssl default_server;          location / {                 include /etc/nginx/proxy_params;                 if ($args = '') {                         proxy_pass $scheme://$host$uri;                 }                 if ($args != '') {                         proxy_pass $scheme://$host$uri?$args;                 }         } } 

После генерации конфигурации необходимо не забыть сказать

# service nginx reload

что сообщит nginx о необходимости перезагрузить конфигурацию, мягко погасив старые пулы.

Система с nginx проходила проверку на прочность неделю назад, когда в данный список был внесен один ролик с youtube.com. Кроме повышенного расхода памяти побочных эффектов замечено не было. С расходом памяти удалось побороться отключив keep-alive соединения с клиентами. А вот с удобством для пользователей вышло, конечно, не очень: просмотр и загрузка роликов на youtube.com в целом работала, но многие ролики были внедрены в другие страницы с помощью https, а с подмененным сертификатом браузеры их отображать не хотели. Волевым решением руководства провайдера домены google.com, google.ru, youtube.com были вынесены в список исключений, а еще один из сайтов был внесен в список «исключений наоборот»: по нему есть давнее решение блокировать целиком, однако в данном реестре он выгружается только с двумя запрещенными URL.
В целом данное решение показало себя вполне работоспособным для маленького провайдера, который хочет продолжать работать в непростых условиях нашего Российского законодательства.

ссылка на оригинал статьи http://habrahabr.ru/post/216209/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *