PRUDP: Escrevendo um protocolo de rede do Nintendo 3DS
Olá, nesse artigo vou acabar detalhando como fiz um cabeçalho básico e simplificado de um protocolo que é utilizado para comunicação em rede por alguns software da Nintendo em seu portátil 3DS. Mas, antes de mostrar o código, acho importante esclarecer alguns termos para quem não está muito familiarizado tanto com a linguagem utilizada (C, C++) quanto com o funcionamento do PRUDP.
PRUDP
O PRUDP é uma evolução do User Datagram Protocol (UDP), um protocolo de rede comumente usado em jogos online e outras aplicações em tempo real, mas que não fornece garantias de entrega ou ordem dos pacotes. O PRUDP adiciona recursos de confiabilidade e correção de erros ao UDP, tornando-o mais adequado para aplicações em que é crucial que os dados sejam entregues com segurança e em ordem.
O PRUDP é usado em consoles de jogos da Nintendo, como o Nintendo 3DS, para suportar jogos online com baixa latência e alta confiabilidade.
Existem pelo menos duas versões do PRUD: uma utilizada no Friends Service (Serviço da Nintendo para interação dos jogadores) e outra que foi utilizada no Pokémon X&Y. As duas versões V0 e V1 possuem poucas diferenças, sendo a principal diferença na decodificação dos pacotes.
Versões de portáteis mais recentes como o Switch usa Websocket’s ao invés de conexões UDP. Vale ressaltar que a nintendo utiliza uma pilha rescrita da biblioteca.
Quazal Rendez-Vous library
A Quazal Rendez-Vous library é uma biblioteca de rede desenvolvida pela Quazal Technologies, que fornece funcionalidade de rede ponto-a-ponto (P2P) para jogos e outras aplicações em tempo real. A biblioteca é projetada para lidar com a descoberta de pares (peer-to-peer), estabelecimento de conexão, comunicação e gerenciamento de sessões entre dispositivos em uma rede.
A Quazal Rendez-Vous oferece suporte a vários recursos avançados, como balanceamento de carga, failover, segurança, escalabilidade, tolerância a falhas e gerenciamento de largura de banda. Ela pode ser usada em uma variedade de plataformas, incluindo PC, consoles de jogos e dispositivos móveis.
Funcionamento do PRUDP
Quando um cliente se conecta a um servidor é enviado um pacote com a flag SYN para que o servidor reconheça a conexão. Após feito reconhecimento o servidor envia um pacote com a flag CONNECT e o HANDSHAKE é feito, caso o cliente queira se desconectar um pacote com flag DESCONNECTED é enviado.
As seguintes técnicas são usadas para alcançar a confiabilidade:
- Um pacote que tem o FLAG_NEED_ACK definido deve ser reconhecido pelo receptor. Se o remetente não receber um reconhecimento após um determinado período de tempo, ele reenviará o pacote.
- Um ID de sequência é enviado junto com um pacote, para que o receptor possa reorganizar os pacotes, se necessário.
- Para manter a conexão ativa, tanto o cliente quanto o servidor podem enviar pacotes PING um para o outro após um determinado período de tempo ter decorrido.
Abaixo um exemplo de sessão com a utilização desse protocolo:
Handshake 1 → af a1 40 00 00 00 00 00 00 00 00 00 00 00 00 97
Handshake 1 ← a1 af 10 00 00 00 00 00 00 00 00 5f 22 68 ea 3a
Handshake 2 → af a1 61 00 18 5f 22 68 ea 01 00 d4 d6 91 e8 c9
Handshake 2 ← a1 af 11 00 50 d4 d6 91 e8 01 00 00 00 00 00 de
Send data → af a1 62 00 18 26 b4 01 a1 02 00 00 (25 bytes of encrypted payload) ef
Acknowledge ← a1 af 12 00 50 78 56 34 12 02 00 00 d1
Send data ← a1 af 62 00 50 67 dd f9 c3 01 00 00 (255 bytes of encrypted payload) 03
Acknowledge → af a1 12 00 18 78 56 34 12 01 00 00 97
Send data → af a1 62 00 18 8d 58 91 c0 03 00 00 (21 bytes of encrypted payload) fa
Acknowledge ← a1 af 12 00 50 78 56 34 12 03 00 00 d2
Send data ← a1 af 62 00 50 a9 c5 fa 2e 02 00 00 (130 bytes of encrypted payload) 54
Acknowledge → af a1 12 00 18 78 56 34 12 02 00 00 98
Hangup → af a1 63 00 18 5f 22 68 ea 04 00 aa
Hangup ← a1 af 13 00 50 d4 d6 91 e8 04 00 e2
Cabeçalho PRUDP
A grosso modo, o cabeçalho do PRUDP é um pouco semelhante ao cabeçalho UDP com a diferença que contém algumas informações adicionais que alteram a lógica de seu funcionamento para ser mais confiável e seguro que o UDP. Abaixo uma imagem com o conteúdo de uma documentação produzida pelo Yannik Marchand e que pode ser encontrada no seguinte link: https://github.com/kinnay/NintendoClients/wiki/PRUDP-Protocol
Basicamente cada um desses campos que compõe o pacote possui um tipo que é de extrema importância para quando escrevemos em uma linguagem de programação. Um cabeçalho mais detalhado com os tipos pode ser encontrado nesse link: https://www.3dbrew.org/wiki/PRUDP
…
Source: u16
Destination: u16
Sequence_id: u16
Checksum: u8
…
Mão na Massa!!
Ok, para iniciar eu não fazia ideia de como escrever um protocolo(talvez ainda não faça). Sei que era possível escrever sua representação em quaisquer linguagem de programação, mas que em uma compilação talvez fosse não muito eficiente fazer isso em Python ou em Java, por exemplo. Então, com isso em mente tinha que ser C/C++, mas como diabos eu poderia fazer um protocolo como ICMP, UDP ou TCP? onde encontraria o código fonte para ver seu funcionamento e regras e conjunção com um sistema operacional? Bom, depois de algumas pesquisas me deparei com um repositório de código que possuem protocolo implementados no FREE BSD e lá poderia ver como são os cabeçalhos da pilha de protocolos de redes usadas nesse sistema operacional. Com isso também consegui ver algumas especificações e regras que são de extrema importância para um protocolo.
Entendo o funcionamento do código UDP
Conhecendo um pouco de unix consegui entender que o arquivo que iria implementar as funções e estrutura de dados corretas para o UDP seria o arquivo udp.h e esse arquivo estaria localizado na biblioteca que implementa a pilha de protocolos de rede do FreeBSD (sys/netinet)
A biblioteca “netinet” é um componente essencial do FreeBSD e é usada por muitas aplicações de rede em sistemas operacionais baseados em Unix, incluindo roteadores, servidores web e servidores de e-mail.
E bingo!! com isso temos o código fonte de um cabeçalho crucial para o funcionamento do UDP:
/*-
* Copyright (c) 1982, 1986, 1993
* The Regents of the University of California.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* @(#)udp.h 8.1 (Berkeley) 6/10/93
* $FreeBSD$
*/
#ifndef _NETINET_UDP_H_
#define _NETINET_UDP_H_
/*
* UDP protocol header.
* Per RFC 768, September, 1981.
*/
struct udphdr {
u_short uh_sport; /* source port */
u_short uh_dport; /* destination port */
u_short uh_ulen; /* udp length */
u_short uh_sum; /* udp checksum */
};
/*
* User-settable options (used with setsockopt).
*/
#define UDP_ENCAP 1
/* Start of reserved space for third-party user-settable options. */
#define UDP_VENDOR SO_VENDOR
/*
* UDP Encapsulation of IPsec Packets options.
*/
/* Encapsulation types. */
#define UDP_ENCAP_ESPINUDP_NON_IKE 1 /* draft-ietf-ipsec-nat-t-ike-00/01 */
#define UDP_ENCAP_ESPINUDP 2 /* draft-ietf-ipsec-udp-encaps-02+ */
/* Default ESP in UDP encapsulation port. */
#define UDP_ENCAP_ESPINUDP_PORT 500
/* Maximum UDP fragment size for ESP over UDP. */
#define UDP_ENCAP_ESPINUDP_MAXFRAGLEN 552
#endif
Porém, no momento, vamos focar no struct implementado no código que segue o que é estabelecido no RFC 768.
A RFC 768 define a estrutura básica do cabeçalho do User Datagram Protocol (UDP), um protocolo de transporte de dados utilizado em redes de computadores. O cabeçalho UDP é composto por quatro campos, conforme descrito a seguir:
- Porta de origem (2 bytes): um campo de 16 bits que identifica a porta do remetente.
- porta de destino (2 bytes): um campo de 16 bits que identifica a porta do destinatário.
- Comprimento (2 bytes): um campo de 16 bits que especifica o comprimento do datagrama UDP, incluindo o cabeçalho e os dados.
- Checksum (2 bytes): um campo de 16 bits opcional que é usado para garantir a integridade do datagrama. O checksum é calculado com base nos valores dos campos de cabeçalho e de dados.
- Checksum (2 bytes): um campo de 16 bits opcional que é usado para garantir a integridade do datagrama. O checksum é calculado com base nos valores dos campos de cabeçalho e de dados.
Com isso bastava tentar entender porque o cabeçalho foi escrito dessa forma, abaixo vamos focar nesse Struct e como utilizei ele para escrever o cabeçalho que possivelmente é utilizado no PRUDP.
Analisando o udphdr
/*
* UDP protocol header.
* Per RFC 768, September, 1981.
*/
struct udphdr {
u_short uh_sport; /* source port */
u_short uh_dport; /* destination port */
u_short uh_ulen; /* udp length */
u_short uh_sum; /* udp checksum */
};
A estrutura udphdr define os campos do cabeçalho do UDP. Cada campo representa um valor específico, como a porta de origem, a porta de destino, o comprimento do datagrama UDP e o checksum. Os campos são definidos como variáveis do tipo u_short, que é um tipo de dado inteiro de 16 bits, usado para representar valores sem sinal.
O campo uh_sport representa a porta de origem, enquanto o campo uh_dport representa a porta de destino. Esses campos são usados para identificar os processos que estão enviando e recebendo os dados.
O campo uh_ulen especifica o comprimento do datagrama UDP, incluindo o cabeçalho e os dados. Esse campo é útil para garantir que o datagrama seja transmitido corretamente pela rede.
O campo uh_sum é opcional e é usado para verificar a integridade do datagrama. O valor desse campo é calculado usando um algoritmo de checksum que verifica se os dados foram corrompidos ou alterados durante a transmissão.
ANSI C(1989)
O seguinte código foi escrito nas especificações do C ANSI 1989: ANSI C é uma referência ao padrão da linguagem de programação C que foi adotado pela ANSI (American National Standards Institute) em 1989. A especificação definiu muitos recursos que ainda são usados atualmente na programação em C, como a definição de tipos inteiros de tamanho fixo, protótipos de função, macros variáveis, void pointers e muito mais.
Em 1999, a ISO (International Organization for Standardization) publicou uma nova revisão da especificação da linguagem C, conhecida como ISO/IEC 9899:1999. Essa revisão é geralmente referida como C99 e incluiu várias melhorias e novos recursos em relação à especificação original ANSI C de 1989. Algumas das novas adições incluem tipos de dados adicionais, suporte para comentários de estilo C++, a capacidade de declarar variáveis no meio de um bloco de código, operadores de atribuição compostos, a palavra-chave inline, e um monte de outras coisas.
Rescrevendo o cabeçalho UDP baseado no cabeçalho PRUDP
struct prudphdr {
uint16_t uh_sport; /* source port rewrite (C 99 ) */
uint16_t uh_dport; /* destination port */
uint16_t uh_ulen; /* pruudp length */
uint8_t uh_sum; /* prudp checksum rewrite to 8 bits */
uint8_t uh_type; /* prudp type msg */
uint16_t uh_seq_num; /* prudp seq number packet */
uint16_t uh_session_id; /* identificador de sessão */
uint32_t uh_timestamp;
uint16_t uh_payload_size; /* tamanho do payload do pacote de dados */
};
- uh_sport: a porta de origem do pacote.
- uh_dport: a porta de destino do pacote.
- uh_ulen: o comprimento do pacote PRUDP, incluindo o cabeçalho e os dados.
- uh_sum: um checksum de 8 bits que ajuda a verificar a integridade do pacote.
- uh_type: um campo que indica o tipo de mensagem contida no pacote PRUDP.
- uh_seq_num: um número de sequência que ajuda a identificar pacotes únicos em uma determinada sessão PRUDP.
- uh_session_id: um identificador exclusivo de sessão que ajuda a distinguir sessões PRUDP separadas.
- uh_timestamp: um campo que contém um valor de tempo que pode ser usado para ajudar a sincronizar as comunicações em várias sessões PRUDP.
- uh_payload_size: o tamanho do payload de dados contido no pacote PRUDP.
C99
Para tornar um código mais “moderno” e com algumas melhorias do UDP original resolvi utilizar o padrão C99 que apresenta algumas vantagens(e desvantagens) com relação ao ANSI C.
Ela foi a terceira revisão do padrão C, sucedendo o padrão ANSI C de 1989. A versão C99 trouxe diversas melhorias e novas funcionalidades à linguagem, como tipos de dados adicionais, suporte para comentários de uma única linha, variáveis declaradas em qualquer lugar do escopo, suporte a matrizes de tamanho variável, novos recursos para manipulação de strings e mais. Além disso, a especificação C99 inclui suporte para códigos Unicode, o que permite que os programas escritos em C trabalhem com uma ampla variedade de idiomas e conjuntos de caracteres. A maioria dos compiladores modernos de C suportam o padrão C99.
O uso do uint16_t, uint8_t e uint32_t é um exemplo dessa melhoria. Esse tipo é definido na biblioteca padrão stdint.h
, que foi introduzida no padrão C99 para garantir portabilidade em diferentes arquiteturas de computador.
O uso de tipos de dados com tamanho definido, como uint16_t
, ajuda a garantir que um programa tenha o mesmo comportamento em diferentes plataformas, independentemente da arquitetura subjacente. Isso ocorre porque o tamanho dos tipos de dados padrão, como int
e long
, podem variar de acordo com a plataforma.
Além disso é possível saber exatamente com qual tipo de dados estamos lidando durante a produção e manutenção do código.
Conclusão
Bom, para concluir vou ter que pontuar que não encontrei muita coisa na documentação oficial da Nintendo. Na verdade eles me fizeram concordar com um NDA só pra ter acesso a documentação online do 3DS :v
Muito dos conteúdos que encontrei foi por meio de pesquisas em fóruns de e documentações não oficiais(de extrema qualidade) produzida por programadores com ou sem associação a Nintendo.
No fim, foi um exercício muito interessante, apesar que: escrever apenas o cabeçalho não é uma tarefa complicada e não garante nem mesmo se a implementação detalhada aqui está correta.
Para implementar corretamente o protocolo PRUDP, é necessário seguir a especificação detalhada do protocolo, que inclui informações sobre a forma como os pacotes devem ser construídos, transmitidos, recebidos e tratados em caso de erros.
Além disso, é necessário testar a implementação em um ambiente real ou simulado para verificar se ela funciona conforme esperado e se é compatível com outras implementações do protocolo.