Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/curiousci/networking
Tiny DNS server which gives the same answer everytime!
https://github.com/curiousci/networking
Last synced: 9 days ago
JSON representation
Tiny DNS server which gives the same answer everytime!
- Host: GitHub
- URL: https://github.com/curiousci/networking
- Owner: CuriousCI
- License: gpl-3.0
- Created: 2024-03-22T09:11:28.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2024-04-02T13:53:38.000Z (9 months ago)
- Last Synced: 2024-04-02T18:30:10.968Z (9 months ago)
- Language: Rust
- Size: 85.9 KB
- Stars: 2
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# DNS _(perché SMTP non mi piaceva!)_
Per curiosità ho voluto implementare uno dei protocolli applicativi che abbiamo visto a lezione. **HTTP** lo avevo già implementato nel primo semestre _(lo scrissi in [Rust](https://www.rust-lang.org/), insieme ad un collega che lo [fece in C](https://github.com/Warcophyr/C-web-server))_
Ho provato ad implementare SMTP, ma è una tortura a cui non voglio più sottomettermi 😅, e dopo aver visto DNS a lezione ho provato con quello _(spoiler: riuscendoci 😁!)_
Il codice è un po' incasinato _(lo scritto in un tempo relativamente breve)_, ma dovrebbe dare comunque l'idea _(e ho imparato un po' di cose sul protocollo)_
_(il server l'ho scritto in [Rust](https://www.rust-lang.org/) btw)_
## Il server
Aprire una porta e leggere i pacchetti UDP è facilissimo! Si tratta di fare il `bind()` con un **socket** (coppia **ip** e **porta**) se il sistema operativo lo permette, e creare un `loop` infinito che legge i dati dalla porta e li mette in un `buffer` _(un array)_.
La prima scoperta interessante è che se si usa UDP, il pacchetto DNS ha come limite **512 byte**, quindi non bisogna gestire un buffer dinamico, se ne può creare uno di dimensione fissata!
Quando ricevo un pacchetto UPD, mi da la dimensione del pacchetto ricevuto e l'indirizzo da cui l'ho ricevuto _(con `nslookup` la porta cambia per ogni richiesta, in genere un numero alto come 50000)_
```Rust
let socket = UdpSocket::bind(SocketAddr::from(([127, 0, 0, 1], 53))).unwrap();let mut buffer = [0; 512]; // Ringrazio il limite di 512 byte, mi facilita il lavoro 😁!
loop {
let (size, source) = socket.recv_from(&mut buffer).unwrap();// codice ...
let request = DNSMessage::from(Box::from(buffer));
let response = DNSMessage { ... } // Lo vediamo dopo 😉// codice ...
socket.send_to(&response, source).unwrap(); // Non è proprio così, ma l'idea è quella
}
```Nella foto un esempio di richiesta con `nslookup` _(ne fa più di 1)_ e dei dati che riceve tramite UDP il server _(dopo ne faccio un'analisi)_
![](./nslookup-requests.png)
## La verità dietro ai pacchetti DNS!
Un messaggio DNS completo ha tutti i campi indicati sotto... sono pochi, vero? Oltre ai campi visti a lezione, ci sono 8 byte dopo i **flag** che servono ad indicare il numero di richieste, risposte _(autoritative, non autoritative e aggiuntive)_, ed è con questo meccanismo che si fa il _parsing_ di un messaggio DNS senza separatori: sapendo in anticipo quanti _oggetti_ bisogna leggere
> `Box<[Tipo]>` sta semplicemente ad indicare un array allocato sull'**heap**, con variabili del tipo `Tipo`, nulla di troppo strano
```Rust
struct DNSMessage {
identification: u16,
query_reply: QueryReply, // 1 bit
opcode: OpCode, // 4 bit
authoritative_answer: bool, // 1 bit
truncation: bool, // 1 bit, perché DNS su UDP ha un limite di 512 byte
recursion_desired: bool, // 1 bit
recursion_available: bool, // 1 bit
_zero: (), // 3 bit, riservato per usi futuri
response_code: ResponseCode, // 4 bit
number_of_questions: u16, // 2 byte
number_of_answers: u16, // 2 byte
number_of_authority_rrs: u16, // 2 byte
number_of_additional_rrs: u16, // 2 byte
questions: Box<[Question]>,
answers: Box<[ResourceRecord]>,
authority_rrs: Box<[ResourceRecord]>,
additional_rrs: Box<[ResourceRecord]>,
}enum QueryReply {
Query,
Reply,
}enum OpCode {
Query,
InverseQuery,
Status,
}// C'è spazio anche per usi futuri
#[non_exhaustive]
enum ResponseCode {
NoError,
FormatError,
ServerFail,
NonexistentDomain,
}// Non ho messo tutti i tipi, dato che sono tantissimi, e c'è spazio per usi futuri!
#[non_exhaustive]
enum DNSRecordType {
A,
NS,
CNAME,
MX,
TXT,
AAAA,
}struct Question {
name: Box<[u8]>, // variabile, e vediamo dopo come viene gestito!
dns_type: DNSRecordType, // 2 byte
class_code: u16, // 2 byte
}struct ResourceRecord {
name: Box<[u8]>, // variabile
dns_type: DNSRecordType, // 2 byte
class_code: u16, // 2 byte
ttl: u32, // 4 byte
rd_length: u16, // 2 byte
r_data: Box<[u8]>, // variabile
}
```Ma come facciamo a gestire i campi variabili, ad esempio il dominio?
Consideriamo `google.com.wind3.hub`, viene rappresentato così:
[6, 103, 111, 111, 103, 108, 101, 3, 99, 111, 109, 5, 119, 105, 110, 100, 51, 3, 104, 117, 98, 0]
- il primo byte, con valore **6**, indica che i prossimi 6 caratteri fanno parte del dominio
- infatti `[103, 111, 111, 103, 108, 101]` è proprio la rappresentazione in ASCII di `google`
- proseguendo, abbiamo il valore **3**, stando ad indicare che i prossimi 3 caratteri fanno parte del dominio
- infatti `[99, 111, 109]` è la rappresentazione di `com`
- si procede così finché non si arriva ad un byte con valore `0` (l'ultimo byte dell'array) che sta ad indicare la fine del dominio> Quindi nella richiesta, il dominio non viene separato dal carattere `.`, è ricavato implicitamente dalla struttura del messaggio
Il codice per parsare il dominio è relativamente semplice
```Rust
let mut index = 12;
let mut length = bytes[index];
index += 1;
let mut domain = vec![];
let mut questions = vec![];for _ in 0..number_of_questions {
while length > 0 {
for _ in 0..length {
domain.push(bytes[index]);
index += 1;
}
domain.push(b'.');
length = bytes[index];
index += 1;
}
questions.push(domain); // è un po' più complicato di così, ma non di troppo
domain.clear();
index += 4;
}
```## Struttura di una risposta
```Rust
let response = DNSMessage {
identification: request.identification,
query_reply: QueryReply::Reply,
opcode: OpCode::Query,
authoritative_answer: false,
truncation: false,
recursion_desired: false,
recursion_available: false,
_zero: (),
response_code: ResponseCode::NoError,
number_of_questions: 0,
number_of_answers: 1,
number_of_authority_rrs: 0,
number_of_additional_rrs: 0,
questions: Box::new([]),
answers: Box::new([ResourceRecord {
name: request.questions[0].name.clone(),
dns_type: DNSRecordType::A,
class_code: request.questions[0].class_code,
ttl: 10, // 10 secondi
rd_length: 4,
r_data: Box::new([0, 0, 10, 8, 123, 5]),
}]),
authority_rrs: Box::new([]),
additional_rrs: Box::new([]),
};
```