sexta-feira, 1 de fevereiro de 2008

Roteando múltiplos links de internet

A empresa onde trabalho depende 100% da internet para seu funcionamento, devido a isso procurei escolher uma boa opção de link de internet levando em consideração o melhor preço X a melhor qualidade de serviço.
É claro que as melhores opções são sempre os links dedicados, no entanto esses serviços são caros se comparados aos serviços DSL disponíveis.

Depois de um bom tempo usando o Velox da Telemar, fui apresentado a MundiVox que até que tem um serviço de boa qualidade, só deixando a desejar no primeiro mês de uso.

Apesar da boa qualidade do serviço, inevitavelmente acontecem algumas pequenas interrupções que as vezes não passam de 1 minuto, mas as vezes pode passar dos 10. Sendo assim decidi contratar mais um link de internet de outro fornecedor para efeitos de backup.

Uma vez que o outro link já estava instalado e funcionando surgiu o desafio: Como inserir o novo link na rede de modo a garantir que a internet não pare nunca (failover), e como combinar os 2 links para aumentar a velocidade de acesso a internet (load balancing)?

Inicialmente pensei que a solução fosse utilizar o recurso de “bonding” do kernel, no entanto após ler bem a pouca documentação disponível descobri que o bonding só funciona com links que façam parte de uma mesma rede (lógica).

Pesquisando mais a fundo descobri que a solução está nas tabelas de roteamento do sistema, na verdade na parte Avançada de Roteamento do sistema (o conhecido Linux Advanced Routing and Traffic Control)

A idéia básica do sistema é de criar tabelas de roteamento separadas para cada link e utilizar um gateway multipath ( com rotas alternativas ) como rota padrão. Para isso é necessário utilizar o pacote iproute2, disponível em http://linux-net.osdl.org/index.php/Iproute2

O pacote iproute2 fornece ferramentas avançadas para a configuração de interfaces, endereços, rotas e filtros e pode substituir as ferramentas padrões já conhecidas: ifconfig, route, arp.
No kernel, a opção IP: advanced router provê o suporte necessário para o recurso.

A configuração é simples e basicamente se dá em configurar o kernel e adicionar algumas tabelas de roteamento.

Configuração do kernel

# cd /usr/src/linux
# make menuconfig

Networking —>
Networking options —>
[*] IP: advanced router
[*] IP: policy routing
[*] IP: use netfilter MARK value as routing key
[*] IP: equal cost multipath
[ ] IP: equal cost multipath with caching support (EXPERIMENTAL)

O suporte a equal cost multipath with caching ainda é experimental e não costuma funcionar corretamente, é altamente recomendado desativar essa opção.
Nos meus testes ao habilitar o recurso, as tabelas de roteamento demoravam muito mais tempo para serem trocadas (quando falhas) além de que não conseguia pingar endereços externos por ambas interfaces (embora o tráfego funcionasse).

Compile e instale o novo kernel

# make
# cp arch/i386/boot/bzImage /boot/kernel-2.6.17-gentoo-r4

Instalação do iproute2

O iproute2 está disponível na árvore do portage e portanto basta um emerge para instalar o pacote.

# emerge iproute2

Usuários de outras distribuições podem usar os devidos sistemas de pacotes para instalar o iproute2 ou instalar a partir dos fontes.

# cd /usr/src/
# wget http://developer.osdl.org/dev/iproute2/download/iproute2-2.6.18-061002.tar.gz
# tar xzf iproute2-2.6.18-061002.tar.gz
# cd iproute2
# ./configure
# make
# make install

Configurando

Supondo que temos 2 links A e B (com IPs estáticos) de provedores distintos e gostaríamos de compartilha-los para nossa rede local na interface de rede C, podemos considerar o seguinte esquema:

INTERNET         INTERNET
| |
ISP1 ISP2
10.0.0.1 172.16.0.1
eth1 eth2/
10.0.0.5 172.16.0.7
NAT ROUTER (eth0)
|192.168.0.1
----+-----------------+---

No esquema acima temos a seguinte configuração:

IP0 =  192.168.0.1
NET0 = 192.168.0.0
BCAST0 = 192.168.0.255

IP1 = 10.0.0.5
GW1 = 10.0.0.1
NET1 = 10.0.0.0
BCAST1 = 10.0.0.255

IP2 = 172.16.0.1
GW2 = 172.16.0.7
NET2 = 172.16.0.0
BCAST2 = 172.16.0.255

Onde IP(0,1,2) é o endereço IP da interface, GW(1,2) é o gateway, NET(0,1,2) é o endereço da rede e BCAST(0,1,2) é o endereço de broadcast.

Interfaces de rede

Devemos configurar as interfaces normalmente porém não deve ser adicionada uma rota default.

# ip link set eth0 up
# ip addr add IP0/24 broadcast BCAST0 dev eth0
# ip link set eth1 up
# ip addr add IP1/24 broadcast BCAST1 dev eth1
# ip link set eth2 up
# ip addr add IP2/24 broadcast BCAST2 dev eth2

(o número 24 após a barra representa o endereço de rede)

Tabelas de roteamento

Com o suporte a advanced router e policy routing no kernel, é possível criar tabelas de roteamento separadas para cada link.

O arquivo /etc/iproute2/rt_tables define as tabelas de roteamento disponiveis no sistema. Nele são definidas 4 tabelas padrões do sistema que são: local, main, default e unspec.
A tabela local armazena as rotas para as redes configuradas no sistema e é manipulada diretamente pelo kernel. As rotas padrões (configuradas pelo usuário) são inseridas na tabela main.
É possível ainda adicionar novas tabelas ao arquivo embora isso não seja extremamente necessário a menos que as tabelas sejam sempre chamadas através do nome ao invés do seu número.

# vi /etc/iproute2/rt_tables

#
# reserved values
#
255 local
254 main
253 default
0 unspec
#
# local
#
20 isp1
30 isp2
100 netgw

Certifique-se de que não existe nenhuma rota padrão configurada e defina a prioridade da tabela main para um número menor que os das interfaces de rede.

# ip route show table main|grep default
# ip route del default
# ip rule add prio 10 table main

Para que a configuração funcione corretamente é preciso se certificar de que os pacotes voltem sempre pela interface pela qual entraram. Dessa forma é necessário criar uma tabela separada para cada link especificando sua rota padrão. Também é necessário definir uma regra para cada tabela.

# ip rule add prio 20 table isp1
# ip route add route from NET1/24 default via GW1 dev eth1
# ip route append prohibit default table isp1 metric 1
# ip rule add prio 30 table isp2
# ip route add route from NET2/24 default via GW2 dev eth2
# ip route append prohibit default table isp2 metric 1

A linha com o parâmetro prohibit funciona de forma similar ao REJECT do itptables. Quando um cliente está transferindo dados por uma certa interface e ela deixa de funcionar, uma mensagem ICMP é enviada ao cliente de modo a fazer com que o mesmo termine a conexão e comece uma nova usando uma rota funcional.

Também é necessário criar uma tabela para o gateway multipath.

# ip rule add prio 100 table netgw
# ip route add default nexthop via GW1/24 dev eth1 nexthop via GW2/24 dev eth2

Pode-se utilizar o parâmetro weight de modo a especificar o peso (preferência) da rota

NAT

O compartilhamento para a rede interna pode ser feito através do iptables mascarando a saída nas interfaces eth0 e eth1.

# iptables -t nat -A POSTROUTING -o eth1 -j MASQUERADE
# iptables -t nat -A POSTROUTING -o eth2 -j MASQUERADE
# echo 1 > /proc/sys/net/ipv4/ip_forward

Testando

Nesse momento já deve ser possível acessar um endereço externo por ambas as rotas.

# ping google.com -c 1 -I eth1 
PING google.com (64.233.167.99) from 10.0.0.5 eth1: 56(84) bytes of data.
64 bytes from 64.233.167.99: icmp_seq=1 ttl=243 time=191 ms

# ping google.com -c 1 -I eth2
PING google.com (64.233.167.99) from 172.16.0.1 eth2: 56(84) bytes of data.
64 bytes from 64.233.187.99: icmp_seq=1 ttl=243 time=115 ms

Vale lembrar que como o kernel guarda um cache das rotas utilizadas é provável que quando um dos links falhar demore alguns mimnutos até que a rota indisponível seja deletada e uma nova seja criada utilizando o link funcional.

É possível definir através do arquivo /proc/sys/net/ipv4/route/gc_timeout o tempo máximo que o kernel espera até tentar uma nova rota quando perceber que a atual está morta.

Também é possível forçar a “limpeza” do cache de roteamento com o seguinte comando:

# ip route flush cache dev ethX
(onde X é o número da interface)

Dessa forma é possível criar um script que tente pingar os gateways (ou outro endereço público) e caso não haja resposta apagar o cache da tabela de roteamento.

Melhorando a detecção de rotas mortas

O kernel linux armazena as rotas de rede utilizadas em um tabela de cache que é consultada a cada acesso. Quando uma rota não é encontrada utiliza-se os gateways disponíveis para traçar uma nova rota e automaticamente ela é armazenada.

Quando tentamos acessar um endereço por uma certa rota e esta falha, não necessariamente temos problemas com o gateway ou com a interface, as vezes o problema pode ser com o endereço de destino. Dependendo do motivo da falha, certas ações são tomadas pelo sistema.

Após um certo tempo, o gateway será considerado como morto e marcado como indisponível (”unreachable”). Se o problema for com a interface o link será desativado e as rotas serão limpadas (flushed) do cache forçando a utilização de uma nova rota.
Caso o problema seja outro (um gateway “morto” no caminho por exemplo) não será possível detectar a falha e as tentativas de conexão continuarão tentando essa rota e falhan do. Após mais um certo tempo o cache de rotas irá expirar e só então uma nova rota (funcional) será tentatada.

Da mesma forma, caso a falha tenha sido da interface e/ou cabeamento, não será possível re-inserir as rotas automaticamente quando o problema se resolver automaticamente, necessitando assim de uma intervenção do administrador para ativação das mesmas.

O Julian Anastasov já há alguns anos disponibiliza alguns patchs para melhor esse recurso no kernel, tornando a detecção de gateway/rotas mortas mais inteligente. Os patches podem ser encontrados em http://www.ssi.bg/~ja/

Com os patches aplicados é possível por exemplo usar “prot static” para todas as rotas. Dessa forma, mesmo que uma interface se torne indisponível o kernel não irá remover as rotas e uma vez que a interface seja disponibilizada novamente tudo estará funcionando perfeitamente (ideal para esquemas de failover).

# cd /usr/src/
# wget http://www.ssi.bg/~ja/routes-2.6.17-12.diff
# patch -p1 < routes-2.6.17-12.diff
# cd linux
# make && make modules install
# cp arch/i386/boot/bzImage /boot/kernel-2.6.17-gentoo-r4

Configure o gerenciador de boot e reinicialize com o novo kernel

Atualizado: Gráficos Comparativos

O balanceamento de uso dos links pode ser comprovado nos gráficos abaixo. O primeiro gráfico representa as 2 semanas anteriores a instala~ao do segundo link. Os demais representam 2 semanas de uso com os 2 links.

Link 01 - 2 semanas antes

Link 01 - 2 semanas após

Link 02 - 2 semanas após

Automatizando a configuração no Gentoo

No gentoo é possível definir funções no arquivo de configurações de redes. Tais funções são executadas antes/após carregar uma interface e antes/após descarregar uma interface. Dessa forma é possível automatizar o processo de criar as rotas e regras.
A função postup que implemento abaixo não é muito inteligente, porém dá uma idéia básica de como automatizar o processo de configuração das redes.
A cada interface que for configurada as ações de adicionar/remover regra/rota são executadas.

# vi /etc/conf.d/net

config_eth0=( “192.168.0.1 netmask 255.255.255.0″ )
config_eth1=( “10.0.0.5 netmask 255.255.255.0″ )
config_eth2=( “172.16.0.1 netmask 255.255.255.0″ )
append_eth0=( “default via 10.0.0.1 src 10.0.0.5 proto static table isp1″
“prohibit default table isp1 metric 1 proto static”)
append_eth1=( “default via 172.16.0.7 src 172.16.0.1 proto static table isp2″
“prohibit default table isp2 metric 1 proto static”)
rule_eth0=( “from 10.0.0.0/24 priority 20 table isp1″ )
rule_eth1=( “from 172.16.0.0/24 priority 30 table isp2″ )
multipath_route=( “table netgw proto static nexthop via 10.0.0.1
dev eth1 nexthop via 172.16.0.7 dev eth2″ )
multipath_rule=( “prio 100 table netgw”
“prio 10 table main” )

postup() {
local x=”append_${IFVAR}[@]”
local -a appends=( “${!x}” )
if [[ -n ${appends} ]] ; then
einfo “Appending route”
eindent

for x in “${appends[@]}”; do
ip route del ${x} 1>&2 2> /dev/null
ebegin “${x}”

ip route append ${x}
eend $?
done
eoutdent

#Flush the cache
ip route flush cache dev “${IFACE}”
fi

local x=”rule_${IFVAR}[@]”
local -a rules=( “${!x}” )
if [[ -n ${rules} ]]; then
einfo “Adding IP policy routing rules”
eindent

for x in “${rules[@]}”; do
ip rule del ${x} 1>&2 2> /dev/null
ebegin “${x}”

ip rule add ${x}
eend $?
done
eoutdent
fi

if [[ -n ${multipath_route} ]]; then
for x in “${multipath_route[@]}”; do
einfo “Adding multipath route”
eindent
ip route del default 1>&2 2> /dev/null
ip route del ${x} 1>&2 2> /dev/null
ebegin “${x}”
ip route add default ${x}
eend $?
eoutdent
done
fi

if [[ -n ${multipath_rule} ]]; then
for x in “${multipath_rule[@]}”; do
einfo “Adding multipath rule”

eindent
ip rule del ${x} 1>&2 2> /dev/null
ebegin “${x}”
ip rule add ${x}
eend $?
eoutdent
done
fi
}

Referências

2 comentários:

Erick Marllon disse...

Existe alguma limitação referente ao número máximo de links que se pode usar ???

Dum Leão disse...

Como eu faria para fazer esse failover no debian?