i
Happy Eyeballs
IPv6 nos softwares
Apesar do IPv6 tentar ser transparente visto da camada de aplicação, alguns problemas ainda não estão completamente resolvidos. O problema sobre qual conexão preferir caso o host possua tanto IPv6 quanto IPv4 é um deles. Por padrão os sistemas operacionais costumam dar uma preferência unânime para o IPv6, o que é bom, pois incentiva o uso do novo protocolo. No entanto, por se tratar de um protocolo ainda em seus estados iniciais de implantação, existem muitos problemas de conectividade na rede IPv6. Isso implica em um problema grave, pois nessa situação um usuário que possua uma conexão com IPv6 e IPv4 pode sofrer uma pior experiência de uso do que um usuário com apenas IPv4. Para resolver esse problema, foi desenvolvido uma técnica chamada Happy Eyeballs.Happy Eyeballs
Definida na RFC 8305, Happy Eyeballs é uma forma de tentar corrigir o problema de decisão sobre qual conexão preferir, caso as duas pontas possuam IPv4 e IPv6. Seu funcionamento consiste em tentar se conectar às duas conexões simultaneamente e utilizar aquela que é estabelecida mais rapidamente, dando uma leve preferência para a conexões IPv6. Browsers como Google Chrome e Mozilla Firefox em suas versões atuais já implementam o Happy Eyeballs e o utilizam por padrão. Em sua definição mais básica, na hora de acessar um domínio, são verificados seus IPv4 e IPv6 disponíveis para se conectar. O funcionamento padrão da maioria dos sistemas operacionais faz com que primeiro seja tentado a conexão via IPv6 e em caso de falha (timeout) é tentado a conexão via IPv4. Essa implementação apresenta um problema grave, pois caso a conexão IPv6 apresente problemas, o usuário com IPv6 sofrerá um grande delay para acessar o site, enquanto um usuário comum que apresente apenas IPv4 não sofrerá disso. Esse problema é uma das principais barreiras para os provedores de conteúdos, pois o simples fato de se implementar o IPv6 pode acarretar em um prejuízo para o provedor. Uma solução proposta para isso foi a de tentar se conectar tanto via IPv6 como via IPv4 simultaneamente. Assim o primeiro a responder seria aceito e a outra conexão seria descartada. No entanto essa solução possui 2 problemas:- Seriam desperdiçados muitos sockets para as tentativas de conexões, pois independente da qualidade do serviço sempre seriam feitas pelo menos 2 tentativas de conexão simultaneas. Além disso o excesso de tentativas poderia fazer com que regras mais rígidas de controle de acesso bloqueassem o usuário por excesso de conexões;
- Mesmo que o IPv6 possuísse uma qualidade aceitável, o simples fato dele ser um pouco mais lento que o IPv4 faria com que ele nunca fosse utilizado. A princípio isso não é um problema, porém acabaria gerando uma grande barreira para o avanço do IPv6, pois na situação atual é comum que as implementações de IPv6 não sejam tão eficientes quanto as IPv4 já existentes, o que faria com que tais conexões nunca fossem utilizadas, desestimulando assim seu uso.
conectar (endereço) { encontra IPs do endereço tenta se conectar a o endereço IPv6 enquanto nenhuma conexão se estabelece e timeout não estoura { se passou tempo de vantagem dado ao IPv6 { tenta se conectar a um endereço IPv4 (uma única vez) } } se conseguiu conectar { mantém a conexão conectada fecha as demais conexões (sockets abertos) } caso não conseguiu conectar (timeout) { fecha todas as conexões (sockets abertos) } }
- Ao encontrar os IPs do endereço deve-se tomar certo cuidado, pois alguns métodos de se encontrar os IPs (como gethostbyname) retornan apenas IPv4;
- Sempre lembrar de fechar as conexões que não foram estabelecidas (e limpar a memória em linguagens sem garbage collector).
conectar (endereço) { encontra IPs do endereço verifica cache (endereço) se não existir cache { tenta se conectar a um endereço IPv6 enquanto nenhuma conexão se estabelece e timeout não estoura { se passou tempo de vantagem dado ao IPv6 { tenta se conectar a um endereço IPv4 (uma única vez) } se passou tempo de vantagem e não se conectou nem via IPv6 nem via IPv4 { tenta se conectar a qualquer outro endereço disponível } } } se estiver no cache { tenta se conectar a um endereço com a versão definida no cache } se conseguiu conectar { mantém a conexão conectada fecha as demais conexões (sockets abertos) } caso não conseguiu conectar (timeout) { fecha todas as conexões (sockets abertos) } } verifica cache (endereço) { verifica se existe o cache para endereço se cache existe { se o cache está ativo há mais de 10 minutos { limpa o cache } caso contrario { retorna o cache } } }
Implementando o Happy Eyeballs
Como a própria RFC 8305 sugere, o ideal é que as próprias aplicações implementem o Happy Eyeballs. Para isso alguns conceitos importantes são necessários.Sockets
Basicamente um socket é um par composto por um IP e uma porta que criam uma interface de comunicação com outro socket, estabelecendo assim uma conexão entre as duas interfaces (rfc793#page-84). Sockets podem se comportar diferentemente entre as diversas linguagens de programação, além disso é importante verificar se os sockets da linguagem utilizada possuem suporte a IPv6. É importante notar que a implementação de um socket pode variar de acordo com o sistema operacional, portanto sempre devemos conferir se o resultado realmente bate com o esperado (especialmente na hora de setar as flags de blocking do socket).Blocking vs Non-blocking Sockets
Por padrão, sockets são implementados em modo blocking. Modo blocking significa que quando tentamos conectar a um servidor através da função connect() do socket a aplicação fica em standby esperando a conexão se estabelecer. Pela definição do Happy Eyeballs a aplicação deve continuar a trabalhar mesmo após tentar se conectar, por isso alguma alternativa deve ser tomada. Uma das soluções mais simples envolve tornar o socket non-blocking. Ao se tornar non-blocking, o programa continua sua execução mesmo após um connect() ou select(), possibilitando assim a implementação do Happy Eyeballs.Suporte a IPv6
É importante verificar dois requisitos com relação ao suporte a IPv6:- Verificar se o sistema operacional utilizado possui suporte a IPv6 e se possuir verificar se ele está habilitado pelo sistema (principalmente em sistemas mais antigos, que possuem suporte a IPv6, porém este vem desabilitado por padrão);
- Verificar se a linguagem utilizada possui suporte a IPv6, pois muitas vezes versões antigas não possuem suporte, ou possuem muitos bugs. Além disso pode ser necessário a instalação de pacotes adicionais para o funcionamento correto do programa.
C/C++
Em C, utiliza-se muito a função gethostbyname, da bilioteca netdb.h, para realizar consultas DNS:struct hostent *gethostbyname(const char *name); struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* list of addresses from name server */ }; Porém essa função, assim como outras da biblioteca netdb.h, só possui suporte a IPv4. Para corrigir essa falha, foi criado a função gethostbyname2: struct hostent *gethostbyname2(const char *name, int af);
- https://pubs.opengroup.org/onlinepubs/009695399/functions/gethostbyname.html
- https://tools.ietf.org/html/rfc3493
int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res);
struct addrinfo { int ai_flags; // AI_PASSIVE, AI_CANONNAME, etc. int ai_family; // AF_INET, AF_INET6, AF_UNSPEC int ai_socktype; // SOCK_STREAM, SOCK_DGRAM int ai_protocol; // use 0 for "any" size_t ai_addrlen; // size of ai_addr in bytes struct sockaddr *ai_addr; // struct sockaddr_in or _in6 char *ai_canonname; // full canonical hostname struct addrinfo *ai_next; // linked list, next node };
int main(int argc, char *argv[]) { struct addrinfo hints, *res, *p; int status; char ipstr[INET6_ADDRSTRLEN]; if (argc != 2) { fprintf(stderr, "Usage: %s hostname\n", argv[0]); return 1; } memset(&hints, 0, sizeof hints); hints.ai_family = AF_UNSPEC; // AF_INET or AF_INET6 to force version hints.ai_socktype = SOCK_STREAM; if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status)); return 2; } for(p = res;p != NULL; p = p->ai_next) { void *addr; if (p->ai_family == AF_INET) { return 1; } else { struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; addr = &(ipv6->sin6_addr); /* convert the IP to a string and print it: */ inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr); printf("Hostname: %s\n", argv[1]); printf("IP Address: %s\n", ipstr); } } freeaddrinfo(res); // free the linked list return 0; }
- https://pubs.opengroup.org/onlinepubs/009695399/functions/getaddrinfo.html
- https://pubs.opengroup.org/onlinepubs/009619199/getad.htm
int socket(int domain, int type, int protocol);
int connect(int socket, const struct sockaddr *address, socklen_t address_len); O parâmetro socket é o mesmo socket id que foi gerado na função socket. O parâmetro address é uma struct retornada pela função getaddrinfo, dentro da struct addrinfo->ai_addr O parâmetro address_len também é um parâmetro retornado pela função getaddrinfo, dentro da struct addrinfo->ai_addrlen. A struct sockaddr pode ser de vários tipos: struct sockaddr { unsigned short sa_family; // address family, AF_xxx char sa_data[14]; // 14 bytes of protocol address }; // IPv4 AF_INET sockets: struct sockaddr_in { short sin_family; // e.g. AF_INET, AF_INET6 unsigned short sin_port; // e.g. htons(3490) struct in_addr sin_addr; // see struct in_addr, below char sin_zero[8]; // zero this if you want to }; struct in_addr { unsigned long s_addr; // load with inet_pton() }; // IPv6 AF_INET6 sockets: struct sockaddr_in6 { u_int16_t sin6_family; // address family, AF_INET6 u_int16_t sin6_port; // port number, Network Byte Order u_int32_t sin6_flowinfo; // IPv6 flow information struct in6_addr sin6_addr; // IPv6 address u_int32_t sin6_scope_id; // Scope ID }; struct in6_addr { unsigned char s6_addr[16]; // load with inet_pton() }; // General socket address holding structure, big enough to hold either // struct sockaddr_in or struct sockaddr_in6 data: struct sockaddr_storage { sa_family_t ss_family; // address family // all this is padding, implementation specific, ignore it: char __ss_pad1[_SS_PAD1SIZE]; int64_t __ss_align; char __ss_pad2[_SS_PAD2SIZE]; }; Abaixo um exemplo do uso da função connect: int sd; int rc; struct addrinfo ai_hints, *ai; /* create hints for getaddrinfo for stream socket */ memset(&ai_hints, 0, sizeof(ai_hints)); ai_hints.ai_family = AF_INET6; ai_hints.ai_socktype = SOCK_STREAM; ai_hints.ai_protocol = IPPROTO_TCP; /* get list of addresses for nodename from DNS */ rc = getaddrinfo(szNodename, szService, &ai_hints, &ai); if (rc != 0) { strcpy(szError, "getaddrinfo() failed: "); strcat(szError, gai_strerror(rc)); return -1; } /* create socket for this address */ sd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if (sd < 0) { /* fail */ strcpy(szError, "socket() failed, "); strcat(szError, strerror(errno)); return -1; } rc = connect(sd, ai->ai_addr, ai->ai_addrlen);
- https://pubs.opengroup.org/onlinepubs/009695399/functions/socket.html
- https://pubs.opengroup.org/onlinepubs/009695399/functions/connect.html
- https://pubs.opengroup.org/onlinepubs/009695399/basedefs/sys/socket.h.html
int ioctl(int fildes, int request, ... /* arg */); Note que essa é uma função mais avançada, portanto deve ser utilizado com cuidado. O parâmetro fildes (File Descriptor) indica o id do arquivo a ser modificado (no nosso caso é o socket, portanto seu id). O parâmetro request indica o comando que deverá ser executado, no nosso caso seria a constante FIONBIO. O parâmetro arg são configurações ou informações adicionais específicos da operação e do tipo de fildes utilizado. Um exemplo de como utilizar a função ioctl: int sd; int opt = 1; sd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); ioctl(sd, FIONBIO, &opt); Note que nesse caso foi utilizado um parâmetro chamado opt de valor 1. A função ioctl, quando utilizada com o parâmetro FIONBIO, torna o socket non-blocking caso o parâmetro arg seja diferente de zero. Portanto no exemplo a função está tornando o socket criado em um socket non-blocking. Para retornar o socket em modo blocking basta retirar a flag setada anteriormente, utilizando o parâmetro arg com valor igual a zero: int sd; int opt = 1; sd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); // Seta socket non-blocking ioctl(sd, FIONBIO, &opt); // Seta socket blocking opt = 0; ioctl(sd, FIONBIO, &opt);
int fcntl(int fildes, int cmd, ... /* arg */ );
int sd; sd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); int flags = fcntl(sd, F_GETFL, 0); fcntl(sd, F_SETFL, flags | O_NONBLOCK);
int getpeername(int socket, struct sockaddr *address, socklen_t *address_len);
/* Faz as consultas DNS */ sd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); rc = connect(sd, ai->ai_addr, ai->ai_addrlen); // Espera estabelecer a conexão while (!connected) { rc = getpeername(sd, ai->ai_addr, ai->ai_addrlen); if (rc < 0) { connected = TRUE; } } // Conexão estabelecida
- https://pubs.opengroup.org/onlinepubs/009695399/functions/getpeername.html
- https://pubs.opengroup.org/onlinepubs/7908799/xns/getpeername.html
Java
No java devemos tomar certo cuidado, pois o tratamento de IPs é dada pela classe InetAddress (java.net.InetAddress). Esta classe possui 2 implementações diretas: Inet4Adress para IPv4 e Inet6Address para IPv6. A documentação oficial das classes pode ser encontrada em:- https://docs.oracle.com/javase/7/docs/api/java/net/InetAddress.html
- https://docs.oracle.com/javase/7/docs/api/java/net/Inet4Address.html
- https://docs.oracle.com/javase/7/docs/api/java/net/Inet6Address.html
- public boolean isAnyLocalAddress()
- public boolean isLoopbackAddress()
- public boolean isLinkLocalAddress()
- public boolean isSiteLocalAddress()
- public boolean isMCGlobal()
- public boolean isMCNodeLocal()
- public boolean isMCLinkLocal()
- public boolean isMCSiteLocal()
- public boolean isMCOrgLocal()
InetAddress address = InetAddress.getByName(“hostname”); System.out.println(address.getHostAddress); Neste caso o java irá buscar um IP retornado na consulta DNS . Porém ele irá retornar apenas um dos IPs disponíveis. Portanto se quisermos utilizar o Happy Eyeballs precisamos obter todos os IPs retornados pela consulta. Isso pode ser feito da seguinte forma: InetAddress[] addressArray = InetAddress.getAllByName(“hostname”); for (InetAddress address : addressArray) { System.out.println(address.getHostAddress); }
- https://docs.oracle.com/javase/1.4.2/docs/api/java/net/Socket.html
- https://docs.oracle.com/javase/1.4.2/docs/api/java/nio/channels/SocketChannel.html
Socket socket = new Socket(“hostname”, “porta”); socket.connect(); // Faz o que precisa com o socket socket.close();
InetAddress address = InetAddress.getByName(“hostname”); Socket socket = new Socket(address, “porta”); socket.connect(); // Faz o que precisa com o socket socket.close();
InetAddress address = InetAddress.getByName(“hostname”); SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); channel.connect(new InetSocketAddress(address, 80));
InetAddress address = InetAddress.getByName(“hostname”); SocketChannel channel = SocketChannel.open(); channel.configureBlocking(false); channel.connect(new InetSocketAddress(address, 80)); // Espera se conectar while (!channel.finishConnect()) { // Espera um tempo } // Faz o que precisa com o socket socket.close();
// Pega todos os IPs disponíveis para o hostname em específico InetAddress[] addressArray = InetAddress.getAllByName(“hostname”); for (InetAddress address : addressArray) { // Faz alguma coisa com o IP }
// Pega todos os IPs disponíveis para o hostname em específico InetAddress[] addressArray = InetAddress.getAllByName(“hostname”); for (InetAddress address : addressArray) { if (address instanceof Inet6Address) { // O endereço é IPv6 } if (address instanceof Inet4Address) { // O endereço é IPv4 } }Parte 2