DNS Cache em PHP + SQLite
ou
Como contornar problemas com DNS
Vou começar a minha saga. Eu tenho banda larga (500Kbps) Speedy, e conecto usando uma conta gratuita no Terra. Não sei por que, mas às vezes (geralmente à noite, quase todos os dias) a internet não navega. Como o problema não acontece 100% do tempo, achei que seria perda de tempo chamar um técnico, pois provavelmente ele chegaria em casa num momento em que a internet estivesse funcionando e eu não poderia mostrar-lhe o problema. Além disso, achei que o problema se devia simplesmente ao fato de minha casa ficar longe da central, o que não me permite fisicamente uma banda maior. Mas depois descobri que, durante o problema, se eu acessasse um site diretamente pelo seu IP (em vez de pelo nome do host), eu conseguia a conexão. Então o problema não era a internet. Era o DNS. Eu simplesmente não conseguia me conectar com os servidores DNS. Nenhum! Nem mesmo pingavam.
Se o problema fosse esporádico, eu deixaria quieto, mas era frequente demais e eu tive que tomar uma providência. De cara já concebi um DNS Cache tão simples quanto o arquivo hosts (aquele arquivo que contém os IPs e os hosts correspondentes; no linux fica em
/etc/hosts, e no Windows geralmente fica em
C:\Windows\System32\Drivers\etc\hosts). Esse servidor ficaria no próprio micro, e o DNS da conexão deveria então ser direcionado para
127.0.0.1 (localhost). Quando o navegador precisasse de algum IP, faria a requisição ao servidor local. Se o host ainda não estivesse no banco de dados, bastaria este servidor repetir a busca em um servidor externo, devolver a resposta (o IP) e armazená-lo no banco. Se o host já estivesse no banco, bastaria apenas responder com o IP armazenado, e atualizá-lo de vez em quando (uma vez por semana, p. ex.), porque os IPs raramente mudam (mas mudam). E, finalmente, quando os servidores externos de DNS estivessem inacessíveis, "Ahá, eu não preciso mesmo de vocês! Eu tenho o meu!" e navegaria feliz da vida.
Imaginei: "Claro que isso já existe; é tão básico!", mas me frustrei ao procurar por alternativas freeware. Ou eram muito complexos (tente entender os arquivos de configuração do
BIND, por exemplo) ou não rodavam no Vista (ou ambos).
Pensei então em fazer o meu, em PHP com sockets, com já vi num
servidor SMTP, e usando o banco de dados MySQL, com o qual já tenho bastante familiaridade. No entanto, eu não sabia como é o formato do pacote do DNS. Eu não sabia como interpretar os dados do pacote; se eu receberia o pacote de DNS puro ou misturado com os dados da conexão, etc. Aprender isso exigiria um tempo que eu não tinha.
Por um bom tempo a minha saída foi, pasmem, editar o arquivo
hosts na mão. Eu punha lá tudo o que eu mais usava. Quando algum site não estava lá, eu o procurava
aqui
(manualmente), ou tentava acessá-lo através de um
proxy externo, o que não é nada seguro. (Óbvio que o
OpenDNS
e o
g-proxy foram os primeiros a irem para o arquivo hosts, para que eu pudesse ao menos acessar esses sites.)
Só que o uso corriqueiro de internet usa MUITOS hosts em apenas uma noite. Uma simples página faz referência a muitos outros lugares. As figuras estão em um lugar, os scripts em outro, a propaganda em outro, as ferramentas do google em outro... E não são só os sites: o MSN, o antivírus, as atualizações do Windows, e outras coisas acessam diariamente diversos hosts, e que variam de um dia para outro: serv431.antivirus.com, serv547.antivirus.com, etc., cada um deles com um IP diferente. Todo dia eu tinha que cadastrar um conjunto diferente de hosts se quisesse ver meu computador funcionando a contento.
Eu até aguentaria editar o arquivo de hosts por mais algum tempo, mas eu não aguentei ver minha esposa submetida ao mesmo processo. O meu
DNS Cache era inadiável.
"No caminho, tive muitas dificuldades" (eu sei, é um clichê bem batido, mas é a mais pura verdade!...) O DNS trafega pelo protocolo UDP. E descobri que a documentação do uso de sockets com UDP é muito rara. Tentei repetir os exemplos que havia para o protocolo TCP, apenas trocando-o pelo UDP, mas eles não funcionaram. Há um motivo bem simples para isso: TCP gera conexões estáveis; UDP não: os dados vão só uma vez e pronto. Eu não poderia usar para UDP a mesma sequência de
funções de sockets
usada para o TCP. Mas até descobrir isso...
O que me abriu a estrada foi o exemplo dado num
comentário sobre a função socket_recvfrom()
. O código tem algumas falhas na sintaxe, mas clareou a minha mente e me serviu como ponto de partida. Adaptei-o para minhas necessidades, introduzi loops, acrescentei o armazenamento das respostas num banco MySQL e seu acesso. Porém, em vez de salvar o IP, como havia concebido anteriormente, resolvi salvar toda a resposta do servidor externo, e devolvê-la idêntica quando alguém a solicitasse. Simples assim. Sem precisar entender os detalhes dos
campos do DNS, SOA, MX, etc.
No início funcionou que foi uma beleza. Isto é, quando os hosts não estavam no BD. Porque, quando estavam, o que o meu servidor respondia era simplesmente ignorado. Então tive que entender um pouquinho dos campos do DNS. Só um pouquinho. O suficiente para saber que os dois primeiros bytes do pacote são o id da requisição. São aleatórios, imprevisíveis. Mas quem requisitou aceitará somente a resposta que começar com o mesmo id. Fiz a adaptação e finalmente o meu servidor funcionou! Quando o host já estava no BD, bastava devolver a resposta armazenada adaptando o id. Perfeito!
Depois veio a fase de refinar o código. Colocar timeout, mudar seus valores, etc. O servidor roda em modo CLI (Command-Line Interface), isto é, não pela web como tradicionalmente, mas pela linha de comando:
php.exe dns.php. Para rodá-lo basta ter o PHP instalado, sem depender do Apache. Mas ainda dependia do servidor MySQL. Então pensei como seria perfeito se o programa não dependesse de nenhum outro servidor estar rodando. E o banco de dados que não precisa de servidor é o SQLite. Foi minha primeira experiência com o SQLite; tive que aprender algumas manhas, mas não foi nada traumatizante. A diferença para o MySQL está basicamente nas funções de conexão, query e
fetch.
E, se o programa fosse compilado, não dependeria nem mesmo do PHP instalado na máquina! Então tentei compilá-lo com o excelente
Bambalam PHP Compiler, facílimo de usar. Mas não funcionou. Por um motivo muito simples: o Bambalam compila até o PHP 4, e o suporte ao SQLite aparece só a partir da versão 5. Experimentei outros compiladores, mas já não eram tão simples como o Bambalam. Isso me custou alguns dias com tentativas frustradas de contornar o problema, mas não foi impedimento. Como eu tenho o PHP nos dois computadores de casa, coloquei o caminho do
php.exe no
path do sistema e um atalho para
php.exe c:\dns\dns.php no menu iniciar (o caminho do
dns.php pode variar, mas é onde você o salvar.)
Você pode querer me perguntar: "E por que não usar Delphi ou outra linguagem compilável?" A resposta é: eu AMO PHP. Mas este é um assunto para outro artigo.
No entanto, o número de sites não cadastrados no banco ainda era muito grande e dificultava a navegação cotidiana. Então instalei um repetidor externo que respondia na porta TCP 80 (http). Isto é, fiz uma página web simples, que fica num servidor bem longe da minha casa e dos meus problemas de conexão, que recebe na URL a requisição DNS e, por sua vez, repete-a pela porta UDP 53 a um servidor DNS externo. Como nunca tenho problemas de navegar pela porta 80, nunca mais tive problemas com DNS.
O banco de dados deve ser criado à parte. O nome do arquivo SQLite é
dns.db e deverá estar na mesma pasta do
dns.php:
-- A tabela de servidores DNS externos
create table ns (
ip varchar(15) not null primary key,
nome varchar(30) not null,
acessos int not null,
sucessos int not null,
ult_ac datetime not null,
ult_suc datetime not null,
ult_erro varchar(100) not null
);
-- Alguns servidores.
-- É bom que não haja muitos servidores cadastrados;
-- assim fica rápido descobrir quando se está sem conexão,
-- pois ele vai testar com cada um antes de concluir isso.
INSERT INTO ns (ip, nome) VALUES('200.204.0.10', 'itelefonica 1');
INSERT INTO ns (ip, nome) VALUES('200.204.0.138', 'itelefonica 2');
INSERT INTO ns (ip, nome) VALUES('208.67.220.220', 'opendns 1');
INSERT INTO ns (ip, nome) VALUES('208.67.222.222', 'opendns 2');
INSERT INTO ns (ip, nome) VALUES('200.176.2.10', 'terra 1');
INSERT INTO ns (ip, nome) VALUES('200.176.2.12', 'terra 2');
-- A tabela de hosts.
create table hosts (
nome varchar(100) not null primary key,
ret blob not null,
alt datetime not null
);
O código principal, o
dns.php:
<?php
# DNS Cache de Sony Santos
# http://gigawiki.com/sony/dns-cache-em-php-sqlite
# Licença: domínio público (você pode fazer o que você quiser, mas não sou responsável por nada!)
# Baseado em:
# DNS RELAY USING UDP SOCKETS
# by Ryan (ryanfisher.com)
# http://www.php.net/manual/pt_BR/function.socket-recvfrom.php#70063
# Se eu não fizer isso, dá erro...
if (function_exists('date_default_timezone_set'))
date_default_timezone_set('America/Sao_Paulo');
# evita repetir a mensagem do erro de conexão
error_reporting(0);
$timeout = 1;
$prot = 'udp';
# Se você quiser implementar um repetidor externo, coloque o IP do repetidor aqui:
$ip_repetidor = '';
$usa_repetidor = (bool) $ip_repetidor;
function hex2bin($s) {
return pack("H*" , $s);
}
# funções para acesso ao banco SQLite
function execsql($query) {
global $db;
$r = sqlite_unbuffered_query($db, $query, SQLITE_ASSOC, $erro);
if (!$r) {
echo("SQL error $erro\n$query\n");
}
return $r;
}
# retorna os dados da primeira linha da query num array
function pegalinha($query) {
$r = execsql($query);
if ($r) {
return sqlite_fetch_array($r, SQLITE_ASSOC);
}
return false;
}
# retorna o valor do primeiro campo da primeira linha da query
function pegaval($query) {
$r = pegalinha($query);
if ($r) return reset($r);
return false;
}
# Abre a conexão com o BD.
function sql_conecta($banco) {
global $db;
$db = sqlite_open("./$banco.db", 0666, $erro);
if (!$db) exit("Erro ao conectar com banco de dados: $erro\n");
return $db;
}
# Prepara os dados para serem inclusos na query
function sql($dado) {
return sqlite_escape_string($dado);
}
# quando ocorre um erro ao tentar conexão com um servidor DNS.
function erro_dns($erro) {
global $now, $ns, $sem_conexao;
# mostra erro na tela
echo "Erro: $erro\n";
# sem conexão com nameservers
if ($sem_conexao) return false;
# se chegou aqui, o erro veio de uma conexão com o namesever em $ns, vamos atualizá-lo
$erro_sql = sql($erro);
execsql("update ns set acessos = acessos + 1, ult_ac = '$now', ult_erro = '$erro_sql' where ip='$ns[ip]'");
return true;
}
$sem_conexao = false;
sql_conecta('dns');
# Início
echo "Bem-vindo ao DNS Cache de Sony Santos!\n";
echo "http://gigawiki.com/sony/dns-cache-em-php-sqlite\n";
echo pegaval("select count(*) from ns") . " nameservers cadastrados!\n";
echo pegaval("select count(*) from hosts") . " hosts registrados!\n\n";
# loop da conexão UDP (infinito)
do {
$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
if($socket === FALSE) {
echo 'Socket_create failed: '.socket_strerror(socket_last_error())."\n";
exit;
}
if(!socket_bind($socket, "0.0.0.0", 53)) {
socket_close($socket);
echo 'socket_bind failed: '.socket_strerror(socket_last_error())."\n";
exit;
}
# loop da requisição DNS
do {
$buf = '';
$clientIP = '0.0.0.0';
$clientPort = 0;
# aguarda a requisição
$s = @socket_recvfrom($socket, $buf, 65535, 0, $clientIP, $clientPort);
if ($s === false or $s == -1) {
if (socket_last_error())
echo "socket_recvfrom failed: ".socket_strerror(socket_last_error())."\n";
break;
}
# isola o nome do host para mostrar na tela
$tx = preg_replace('/[\x00-\x1F]/', '.', substr($buf, 13, -5));
echo "\n$clientIP:$clientPort Buscando $tx... ";
$tx_sql = sql($tx);
# procura na bd
$host = pegalinha("select ret, alt from hosts where nome='$tx_sql'");
$ret = $novo = false;
$len = 0;
# separa o id da requisição
$id = substr($buf, 0, 2);
# se achou, já manda!
if ($host) {
echo("Encontrado! ");
# monta a resposta com o id da req.
$ret = $id . $host['ret'];
$len = strlen($ret);
# manda a resposta ao cliente
socket_sendto($socket, $ret, $len, 0, $clientIP, $clientPort);
# vê se é novo ou se já tem uma semana ou mais.
$alt = $host['alt'];
$limite = date('Y-m-d H:i:s', time() - 7 * 24 * 3600);
$novo = ($limite < $alt);
if ($novo) {
# não precisa atualizar; vamos esperar a próxima requisição.
echo("Recente.\n");
continue;
}
echo("Atualizando endereço...\n");
}
else echo "Novo!\n";
# vamos atualizar: procura um nameserver pela ordem do último que deu certo.
$r = execsql("select ip, nome from ns order by ult_suc desc");
$ret2 = false;
# loop pelos nameservers
while ($ns = sqlite_fetch_array($r) or ($usa_repetidor and ($sem_conexao = true) and ($reconecta = time() + 3600))) {
# se estiver sem conexão com nameservers, tenta por último um repetidor pela porta TCP:80 (http)
if ($sem_conexao) {
echo("Tentando repetidor externo... ");
# evita dados binários
$reqhex = bin2hex($buf);
# conecta-se com o repetidor externo
$fp = fopen("http://$ip_repetidor/dnsrepeater.php?req=$reqhex", 'rb');
if (!$fp) {
erro_dns("Sem conexão com repetidor");
# não teve jeito, mesmo!
break;
}
# tenta ler a resposta do repetidor por no máx. 10s.
stream_set_timeout($fp, 10);
$rethex = fread($fp, 1024);
fclose($fp);
$ret2 = hex2bin($rethex);
}
else {
echo("Tentando $ns[nome]... ");
$fp = fsockopen("$prot://$ns[ip]", 53, $errno, $errstr, $timeout);
$now = date("Y-m-d H:i:s");
if (!$fp) {
erro_dns("$errno - $errstr");
continue;
}
# envia a requisição ao nameserver externo
fwrite($fp, $buf);
# tenta ler a resposta do ns por no máx. $timeout segundos
stream_set_timeout($fp, $timeout);
$ret2 = fread($fp, 667);
fclose($fp);
}
# Trata os diversos erros da resposta
if ($ret2 === false) {
if (erro_dns("Sem resposta")) continue; else break;
}
$len2 = strlen($ret2);
if (!$len2) {
if (erro_dns("Resposta vazia")) continue; else break;
}
# resposta deve ter a pergunta + ao menos o IP da resposta (4 bytes)
if ($len2 < strlen($buf) + 4) {
if (erro_dns("Resposta insuficiente")) continue; else break;
}
echo("Sucesso: $len2 bytes\n");
# ok, temos uma resposta.
$bin = bin2hex($ret2);
$ret2_sql = sql(substr($ret2,2));
# se já existia faz um update
if ($host) {
$ret = $id . $host['ret'];
if ($ret != $ret2) {
echo "Diferenças encontradas!\n";
execsql("update hosts set ret='$ret2_sql', alt='$now' where nome='$tx_sql'");
}
else echo("Idêntico ao anterior\n");
}
else {
# se é novo, insere na tabela e manda pro cliente.
execsql("insert into hosts (nome, ret, alt) values('$tx_sql', '$ret2_sql', '$now')");
socket_sendto($socket, $ret2, $len2, 0, $clientIP, $clientPort);
}
if ($sem_conexao) {
# tenta reconectar
if (time() > $reconecta) $sem_conexao = false;
}
else {
# atualiza ns
execsql("update ns set acessos = acessos + 1, sucessos = sucessos + 1, ult_ac = '$now', ult_suc='$now' where ip='$ns[ip]'");
}
break;
}
if (!$ret2) {
echo("Sem conexao com nameservers!\n");
}
} while (true);
socket_close($socket);
} while (true);
?>
O repetidor
dnsrepeater.php (opcional), a ser colocado em seu servidor web:
<?php
error_reporting(0);
$timeout = 2;
function hex2bin($s) {
return pack("H*" , $s);
}
# pega a requisição
if (isset($_GET['req'])) $reqhex = $_GET['req'];
else {
echo "sem req<br>";
exit;
}
# transforma em binário
$reqbin = hex2bin($reqhex);
# repete a requisição a um servidor DNS
$fp = fsockopen("udp://208.67.220.220", 53, $errno, $errstr, $timeout);
if (!$fp) {
exit;
}
fwrite($fp, $reqbin);
stream_set_timeout($fp, $timeout);
# lê a resposta
$retbin = fread($fp, 512);
fclose($fp);
# transforma-a em hexadecimal e a retorna
echo bin2hex($retbin);
?>
Depois de ativar o seu DNS Cache local, não se esqueça de mudar o DNS da sua conexão para
127.0.0.1, para que a conexão possa buscar as requisições no seu cache.
Bom, é isso. Aqui em casa funciona a contento. Espero que isto possa lhe se útil de alguma forma.
Como o gigawiki
ainda não tem suporte a respostas ou comentários, podem entrar em contato comigo pelo
twitter
, ou pelo meu e-mail no bol: sony-santos@...
Até mais!