<?xml version='1.0' encoding='UTF-8'?>
<elementos_gw>
  <documento>
    <id>591</id>
    <autor>5</autor>
    <nome>DNS Cache em PHP + SQLite</nome>
    <nome_facil>dns cache em php sqlite</nome_facil>
    <criacao>2009-10-02 00:32:49</criacao>
    <alteracao>2010-07-01 16:26:55</alteracao>
    <texto>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, &quot;Ahá, eu não preciso mesmo de vocês! Eu tenho o meu!&quot; e navegaria feliz da vida.

Imaginei: &quot;Claro que isso já existe; é tão básico!&quot;, mas me frustrei ao procurar por alternativas freeware. Ou eram muito complexos (tente entender os arquivos de configuração do [http://www.bind9.net/ 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 [http://software.filestube.com/software,2214e9d9,PHP+SMTP+Server+for+Windows.html 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 [http://www.opendns.com/support/cache/ aqui] (manualmente), ou tentava acessá-lo através de um [http://g-proxy.com proxy externo], o que não é nada seguro. (Óbvio que o [www.opendns.com OpenDNS] e o [http://g-proxy.com 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.

&quot;No caminho, tive muitas dificuldades&quot; (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 [http://www.php.net/manual/pt_BR/ref.sockets.php funções de sockets] usada para o TCP. Mas até descobrir isso...

O que me abriu a estrada foi o exemplo dado num [http://www.php.net/manual/pt_BR/function.socket-recvfrom.php#70063 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 [http://www.firewall.cx/dns-response-format.php 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 [http://www.bambalam.se/bamcompile/ 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: &quot;E por que não usar Delphi ou outra linguagem compilável?&quot; 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(&#039;200.204.0.10&#039;, &#039;itelefonica 1&#039;);
INSERT INTO ns (ip, nome) VALUES(&#039;200.204.0.138&#039;, &#039;itelefonica 2&#039;);
INSERT INTO ns (ip, nome) VALUES(&#039;208.67.220.220&#039;, &#039;opendns 1&#039;);
INSERT INTO ns (ip, nome) VALUES(&#039;208.67.222.222&#039;, &#039;opendns 2&#039;);
INSERT INTO ns (ip, nome) VALUES(&#039;200.176.2.10&#039;, &#039;terra 1&#039;);
INSERT INTO ns (ip, nome) VALUES(&#039;200.176.2.12&#039;, &#039;terra 2&#039;);

-- 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}}}:
{{{
&lt;?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(&#039;date_default_timezone_set&#039;))
  date_default_timezone_set(&#039;America/Sao_Paulo&#039;);

# evita repetir a mensagem do erro de conexão
error_reporting(0);

$timeout = 1;
$prot = &#039;udp&#039;;

# Se você quiser implementar um repetidor externo, coloque o IP do repetidor aqui:
$ip_repetidor = &#039;&#039;;
$usa_repetidor = (bool) $ip_repetidor;

function hex2bin($s) {
  return pack(&quot;H*&quot; , $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(&quot;SQL error $erro\n$query\n&quot;);
  }
  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(&quot;./$banco.db&quot;, 0666, $erro);
  if (!$db) exit(&quot;Erro ao conectar com banco de dados: $erro\n&quot;);
  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 &quot;Erro: $erro\n&quot;;

  # 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(&quot;update ns set acessos = acessos + 1, ult_ac = &#039;$now&#039;, ult_erro = &#039;$erro_sql&#039; where ip=&#039;$ns[ip]&#039;&quot;);
  return true;
}

$sem_conexao = false;

sql_conecta(&#039;dns&#039;);

# Início
echo &quot;Bem-vindo ao DNS Cache de Sony Santos!\n&quot;;
echo &quot;http://gigawiki.com/sony/dns-cache-em-php-sqlite\n&quot;;
echo pegaval(&quot;select count(*) from ns&quot;) . &quot; nameservers cadastrados!\n&quot;;
echo pegaval(&quot;select count(*) from hosts&quot;) . &quot; hosts registrados!\n\n&quot;;

# loop da conexão UDP (infinito)
do {
  $socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
  if($socket === FALSE) {
    echo &#039;Socket_create failed: &#039;.socket_strerror(socket_last_error()).&quot;\n&quot;;
    exit;
  }
  if(!socket_bind($socket, &quot;0.0.0.0&quot;, 53)) {
    socket_close($socket);
    echo &#039;socket_bind failed: &#039;.socket_strerror(socket_last_error()).&quot;\n&quot;;
    exit;
  }

  # loop da requisição DNS
  do {
    $buf = &#039;&#039;;
    $clientIP = &#039;0.0.0.0&#039;;
    $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 &quot;socket_recvfrom failed: &quot;.socket_strerror(socket_last_error()).&quot;\n&quot;;
      break;
    }

    # isola o nome do host para mostrar na tela
    $tx = preg_replace(&#039;/[\x00-\x1F]/&#039;, &#039;.&#039;, substr($buf, 13, -5));
    echo &quot;\n$clientIP:$clientPort Buscando $tx... &quot;;

    $tx_sql = sql($tx);

    # procura na bd
    $host = pegalinha(&quot;select ret, alt from hosts where nome=&#039;$tx_sql&#039;&quot;);

    $ret = $novo = false;
    $len = 0;

    # separa o id da requisição
    $id = substr($buf, 0, 2);

    # se achou, já manda!
    if ($host) {
      echo(&quot;Encontrado! &quot;);

      # monta a resposta com o id da req.
      $ret = $id . $host[&#039;ret&#039;];
      $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[&#039;alt&#039;];
      $limite = date(&#039;Y-m-d H:i:s&#039;, time() - 7 * 24 * 3600);
      $novo = ($limite &lt; $alt);
      if ($novo) {
        # não precisa atualizar; vamos esperar a próxima requisição.
        echo(&quot;Recente.\n&quot;);
        continue;
      }
      echo(&quot;Atualizando endereço...\n&quot;);
    }
    else echo &quot;Novo!\n&quot;;

    # vamos atualizar: procura um nameserver pela ordem do último que deu certo.
    $r = execsql(&quot;select ip, nome from ns order by ult_suc desc&quot;);
    $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(&quot;Tentando repetidor externo... &quot;);

        # evita dados binários
        $reqhex = bin2hex($buf);

        # conecta-se com o repetidor externo
        $fp = fopen(&quot;http://$ip_repetidor/dnsrepeater.php?req=$reqhex&quot;, &#039;rb&#039;);
        if (!$fp) {
          erro_dns(&quot;Sem conexão com repetidor&quot;);
          # 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(&quot;Tentando $ns[nome]... &quot;);
        $fp = fsockopen(&quot;$prot://$ns[ip]&quot;, 53, $errno, $errstr, $timeout);
        $now = date(&quot;Y-m-d H:i:s&quot;);
        if (!$fp) {
          erro_dns(&quot;$errno - $errstr&quot;);
          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(&quot;Sem resposta&quot;)) continue; else break;
      }

      $len2 = strlen($ret2);
      if (!$len2) {
        if (erro_dns(&quot;Resposta vazia&quot;)) continue; else break;
      }

      # resposta deve ter a pergunta + ao menos o IP da resposta (4 bytes)
      if ($len2 &lt; strlen($buf) + 4) {
        if (erro_dns(&quot;Resposta insuficiente&quot;)) continue; else break;
      }

      echo(&quot;Sucesso: $len2 bytes\n&quot;);

      # ok, temos uma resposta.
      $bin = bin2hex($ret2);
      $ret2_sql = sql(substr($ret2,2));

      # se já existia faz um update
      if ($host) {
        $ret = $id . $host[&#039;ret&#039;];
        if ($ret != $ret2) {
          echo &quot;Diferenças encontradas!\n&quot;;
          execsql(&quot;update hosts set ret=&#039;$ret2_sql&#039;, alt=&#039;$now&#039; where nome=&#039;$tx_sql&#039;&quot;);
        }
        else echo(&quot;Idêntico ao anterior\n&quot;);
      }
      else {
        # se é novo, insere na tabela e manda pro cliente.
        execsql(&quot;insert into hosts (nome, ret, alt) values(&#039;$tx_sql&#039;, &#039;$ret2_sql&#039;, &#039;$now&#039;)&quot;);
        socket_sendto($socket, $ret2, $len2, 0, $clientIP, $clientPort);
      }
      if ($sem_conexao) {
        # tenta reconectar
        if (time() &gt; $reconecta) $sem_conexao = false;
      }
      else {
        # atualiza ns
        execsql(&quot;update ns set acessos = acessos + 1, sucessos = sucessos + 1, ult_ac = &#039;$now&#039;, ult_suc=&#039;$now&#039; where ip=&#039;$ns[ip]&#039;&quot;);
      }
      break;
    }
    if (!$ret2) {
      echo(&quot;Sem conexao com nameservers!\n&quot;);
    }

  } while (true);
  socket_close($socket);
} while (true);
?&gt;
}}}

O repetidor {{{dnsrepeater.php}}} (opcional), a ser colocado em seu servidor web:
{{{
&lt;?php
error_reporting(0);
$timeout = 2;

function hex2bin($s) {
  return pack(&quot;H*&quot; , $s);
}

# pega a requisição
if (isset($_GET[&#039;req&#039;])) $reqhex = $_GET[&#039;req&#039;];
else {
  echo &quot;sem req&lt;br&gt;&quot;;
  exit;
}

# transforma em binário
$reqbin = hex2bin($reqhex);

# repete a requisição a um servidor DNS
$fp = fsockopen(&quot;udp://208.67.220.220&quot;, 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);
?&gt;
}}}

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 [http://twitter.com/sonysantos twitter], ou pelo meu e-mail no bol: sony-santos@...

Até mais!</texto>
    <publico>1</publico>
    <original>0</original>
    <anterior>0</anterior>
    <versao>0</versao>
    <traducao>0</traducao>
    <propriedade>
      <nome>nível</nome>
      <valor>técnico</valor>
      <publico>1</publico>
    </propriedade>
    <propriedade>
      <nome>categ</nome>
      <valor>artigo</valor>
      <publico>1</publico>
    </propriedade>
    <propriedade>
      <nome>assunto</nome>
      <valor>sql</valor>
      <publico>1</publico>
    </propriedade>
    <propriedade>
      <nome>assunto</nome>
      <valor>php</valor>
      <publico>1</publico>
    </propriedade>
    <propriedade>
      <nome>assunto</nome>
      <valor>dns</valor>
      <publico>1</publico>
    </propriedade>
    <propriedade>
      <nome>área</nome>
      <valor>ti</valor>
      <publico>1</publico>
    </propriedade>
  </documento>
</elementos_gw>
