Tiếp tục trong bài học này, chúng ta sẽ tìm hiểu về giao thức ICMP
Xem bài trước : Gói tin ARP
Nếu như các bạn đã biết tới Ping, thì đó chính là ICMP đó.
Giao thức ICMP ( Internet Control Message Protocol ) ICMP được dùng để thông báo các lỗi xảy ra trong quá trình truyền đi của các gói dữ liệu trên mạng. Nó là 1 giao giức của IP ( Internet Protocol ). Do vậy, để làm việc với nó, chúng ta phải tìm hiểu về cấu trúc của các gói IP trước, cụ thể ở đây là IPv4 Protocol
IP Protocol có Ether Type là 0x0800 .Các bạn có thể tìm hiểu kĩ hơn các thuộc tính của từng trường, còn trong bài này, chúng ta chỉ cần quan tâm tới:
- Trường Protocol để xác định xem gói data này có phải là ICMP không !
- Source IP và Dest IP (đương nhiên phải quan tâm rồi)
- Data
1. Trường Protocol
Protocol = 0x01 => Đây là gói ICMP
Sử dụng phần mềm WireShark phân tích rất tiện lợi, vị trí byte protocol trong gói tin là 23
2. Trường Source IP và Dest IP
Tương tự, với source ip vị trí bắt đầu là 26
Với dest ip vị trí bắt đầu là 30
3. Trường data
Trường data chính là gói ICMP
Bây giờ chúng ta phân tích kĩ hơn về cấu trúc của gói ICMP
Chúng ta quan tâm tới các trường sau:
- Type
- Data
Nếu Type = 0x08 thì đây là 1 gói ping hỏi, còn type = 0x00 thì đây là ping trả lời, vì số lượng kiểu của ICMP quá nhiều nên trong bài này, mình chỉ hướng dẫn các bạn xử lí type = 0x08 (ping hỏi) còn mấy cái type khác chưa hỗ trợ nhé, chứ ngồi viết hết thì thốn lắm
Trường Data chính là nội dung của tin nhắn
Để trả lời 1 gói ping thì chúng ta trả lời lại đúng cái nội dung tin nhắn đã nhắn được là ok
Lí thuyết có vậy thôi, hết rồi đó, bắt tay vào code luôn
Do ICMP thuộc bộ giao thức IP, nên ta phải kiểm tra gói tin gửi tới có phải là gói IP không đã
Trong thư viện net.c ngay dưới phần kiểm tra gói tin ARP, nếu gói tin đó không phải gói ARP, chúng ta tiếp tục check xem có phải là gói IP không
1 2 3 4 5 6 7 |
//kiem tra xem co phai goi tin IP khong else if(net_buffer[12] == 0x08 && net_buffer[13] == 0x00) { //check ip //UART_putString("Day la goi tin IP\r\n"); NET_ipRead(net_buffer,len); } |
Sau khi đã biết đây là gói tin IP, chúng ta tiếp tục kiểm tra xem nó có phải là gói ICMP không (hay là TCP UDP ….)
Mình sẽ tạo 1 hàm trên là NET_ipRead để kiểm tra
1 2 3 4 5 6 7 8 9 10 11 12 |
void NET_ipRead(uint8_t *IP_Frame,uint16_t len) { //chung ta se kiem tra xem goi tin do thuoc loai nao tcp hay udp hay icmp if(IP_Frame[23] == 0x01) //it is ICMP packet { if(IP_Frame[34] == 8) //ping { UART_putString("Da nhan 1 goi ping\r\n"); } } } |
Làm tới đâu test tới đó, ngay bây giờ mình sẽ ping thử tới enc28j60 nhé ! Các bạn chạy mô phỏng và mở cmd gõ ping 192.168.1.197
Trong đó 192.168.1.197 là ip tĩnh của enc28j60 mà ta đã cài ở file enc28j60.c
Toẹt vời ! Chúng ta đã bắt được 1 gói ping
Bây giờ viết mã để phản hồi cho gói tin ping này nào !
Tiếp tục tạo file thư viện icmp.h và icmp.c và add vào project
Copy nội dung khởi tạo cho file icmp.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#ifndef ICMP_H_ #define ICMP_H_ //-------------------------------------------------- //Include cac thu vien can thiet #include <mega328p.h> #include <delay.h> #include <string.h> #include <stdlib.h> #include <stdint.h> #include <stdio.h> #include <spi.h> #include "uart.h" #include "net.h" //------------------------------------------------- //-------------------------------------------------- #endif /* ICMP_H_ */ |
Copy nội dung khởi tạo cho file icmp.c
1 2 3 4 5 6 7 |
#include "icmp.h" extern const uint8_t macaddr[6]; extern const uint8_t ip[4]; extern uint8_t debug_string[60]; //-------------------------------------------------- //End file |
Đồng thời add thư viện icmp.h vào file net.h
Sử dụng Struct để xử lí các gói tin 1 cách tiện lợi
Hiện tại mình đang sử dụng truy xuất array để xử lí các gói tin, nó ổn, nhưng không tiện lợi bằng struct. Đương nhiên các bạn phải khá thành thạo con trỏ để có thể kết hợp nó với struct 1 cách nhuần nhuyễn. Mình sẽ code sử dụng lẫn lộn giữa array và struct dần rồi sẽ chuyển dần hoàn toàn sang xài struct cho tiện, vì sau này xử lí các gói tin TCP nó phức tạp lắp không dễ như mấy cái ARP với ICMP này đâu 🙁
Mình sẽ khai báo 1 struct vào file icmp.h có cấu trúc tương tự như của 1 ICMP frame
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
typedef struct { uint8_t MAC_dich[6]; //--------------| uint8_t MAC_nguon[6]; // | => It is Ethernet Frame II uint16_t Ethernet_type; //--------------| uint8_t Header_length; //--------------| => IP uint8_t Services; // | uint16_t TotoLength; // | uint16_t Identification; // | uint16_t Flag; // | uint8_t TimeToLive; // | uint8_t Protocol; // | uint16_t CheckSum; // | uint8_t SourceIP[4]; // | uint8_t DestIP[4]; //--------------| uint8_t ICMP_Type; //---------------| //ICMP uint8_t ICMP_Code; // | uint16_t ICMP_checkSum; // | uint16_t ICMP_Identifier;// | uint16_t ICMP_Squenquer; // | uint8_t *data; //---------------| }ICMP_struct; |
Struct phía trên là cấu trúc của 1 gói ping ( gồm cả request và và reponse)
Sau khi nhận được gói tin, mình sẽ phang thẳng gói tin ấy vào struct bằng cách ép kiểu
Ở bên trên mình có kiểm tra xem có phải gói ping không ở trong file net.c ( hàm NET_ipRead ) mục đích là để test nhanh thôi chứ công việc này sẽ xử lí ở file icmp.c. Do vậy các bạn xóa dòng kiểm tra này đi và gọi hàm ICMP_read để cho hàm này xử lí thì hơn
Chúng ta sẽ viết hàm ICMP_read trong file icmp.c nhé
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void ICMP_read(uint8_t *ICMP_Frame,uint16_t len) { ICMP_struct *ICMP_Ping_Packet = (ICMP_struct *)ICMP_Frame; //kiem tra dia chi ip xem co phai no gui cho minh khong if( memcmp(ICMP_Ping_Packet->DestIP,ip,4) )return; // dung memcmp de so sanh, neu khac thi thoat //kiem tra xem co phai ping reuqest khong ? if(ICMP_Ping_Packet->ICMP_Type == 0x08) { UART_putString("Da nhan 1 goi ping request\r\n"); //phan hoi lai goi ping } } |
Mình sẽ test xem code đến chỗ này đã chạy ok chưa !
Đã bắt được gói ping, các bạn có thấy xài Struct như này chương trình nhìn thân thiện và chuyên nghiệp hơn hẳn không 🙂
Nếu không hiểu dòng ICMP_struct *ICMP_Ping_Packet = (ICMP_struct *)ICMP_Frame; thì đã đến lúc bạn dừng bài viết và đi tìm hiểu về con trỏ và struct rồi đó 🙂
Gửi gói ping trả lời
Giờ chúng ta sẽ viết mã để gửi lại phản hồi
Mình sẽ tạo 1 hàm khác để gửi ping phản hồi
1 2 3 4 |
void ICMP_ping_reply(ICMP_struct * ping_packet,uint16_t len) { } |
Nhiệm vụ trong hàm phản hồi:
- Đảo lộn Source Mac và Dest Mac lại
- Sửa Type 0x08 (hỏi) thành 0x00 (trả lời)
- Sửa lại checksum cho gói icmp
- Gửi đi
Viết hàm tính checksum
Checksum là gì ?
Checksum về cơ bản là một giá trị được tính từ gói dữ liệu để kiểm tra tính toàn vẹn của nó. Thông qua checksum , chúng ta có thể kiểm tra xem dữ liệu nhận được có lỗi hay không. Điều này là do trong khi di chuyển trên mạng, một gói dữ liệu có thể bị hỏng và phải có một cách để biết rằng dữ liệu có bị hỏng hay không. Đây là lý do checksum được thêm vào hearder của mỗi gói tin. Ở phía nguồn gửi, checksum được tính toán và đặt trong tiêu đề dưới dạng một trường. Ở phía đích, checksum lại được tính toán và kiểm tra chéo với giá trị tổng kiểm tra hiện có trong tiêu đề để xem liệu gói dữ liệu có ổn hay không.
Cách tính checksum
Công tất cả các byte 16bit lại với nhau (nếu lẻ thì đắp thêm 0x00 cho chẵn),nếu kết quả > 16bit thì lại tiếp tục cộng 16bit lại cho đến khi được 1 byte 16bit hoàn chỉnh, sau đó lấy nghịch đảo
Ví dụ,1 gói ICMP có nội dung như sau:
0x00,0x00,0x49,0x66,0x00,0x01,0x0B,0xF5,0x61,0x62,0x63,0x64,0x65,0x66,
0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0x73,0x74,0x75,0x76,
0x77,0x61,0x62,0x63,0x64,0x65,0x66,0x67,0x68,0x69
Thì phần mình to đỏ chính là 2 byte check sum. Chúng ta gép các byte8 thành byte16 rồi cộng hết lại với nhau (vì đang tính checksum nên coi checksum=0x0000)
0x0000 + 0x0001 + 0x0BF5 + 0x6162 + 0x6364 + 0x6566 + 0x6768 + 0x696A + 0x6B6C + 0x6D6E + 0x6F70 + 0x7172 + 0x7374 + 0x7576 + 0x7761 + 0x6263 + 0x6465 + 0x6667 + 0x6869 = 0x6B693
Tiếp tục cộng kết quả bằng các bye16
Tức 0x0006 + 0xB693 = 0xB699
Lấy nghịch đảo => checksum = 0x4966
Trong file icmp.c mình sẽ viết hàm tính checksum
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
uint16_t ICMP_checksum(ICMP_struct *icmp_packet,uint16_t icmp_len ) { uint32_t checksum=0; uint8_t *ptr; icmp_packet->ICMP_checkSum=0; //reset check sum icmp_len-=34; //bo qua truong ethernet va truong ip ptr = &icmp_packet->ICMP_Type; while(icmp_len>1) //cong het cac byte16 lai { checksum += (uint16_t) (((uint32_t)*ptr<<8)|*(ptr+1)); ptr+=2; icmp_len-=2; } if(icmp_len) checksum+=((uint32_t)*ptr)<<8; //neu con le 1 byte while (checksum>>16) checksum=(uint16_t)checksum+(checksum>>16); //nghich dao bit checksum=~checksum; //hoan vi byte thap byte cao return ( ((checksum>>8)&0xFF) | ((checksum<<8)&0xFF00) ); } |
Giải thích: Mình giảm len đi 34 vì chúng ta không tính checksum cho các byte thuộc gói ethernet và gói ip ( nó chiếm 34 bye). Con trỏ ptr bắt đầu từ vị trí của trường Type của gói ICMP
Vòng lặp while sẽ gép 2 byte cạnh nhau thành byte lớn 16bit rồi cộng dần lại, sau đó kiểm tra nếu lẻ 1 byte thì nhét thêm 0x00 vào đít bằng lệnh dịch lên cao và cộng lại
Sau khi cộng tổng xong thì kiểm tra tổng này có lớn hơn 16bit không để cộng lại tiếp đến khi con 16bit thì thôi.
Sau đó nghịch đảo byte checksum lại, giá trị checksum trong struct cũng bị lộn thứ tự ( byte thấp đứng trước byte cao) nên mình hoán vị byte lại
Viết hàm ICMP reply
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void ICMP_ping_reply(ICMP_struct * ping_packet,uint16_t len) { uint8_t buffer_temp[6]; //buff trung gian phuc vu hoan doi //hoan vi MAC memcpy(buffer_temp,ping_packet->MAC_nguon,6); //copy mac nguon vao bo nho tam memcpy(ping_packet->MAC_nguon,ping_packet->MAC_dich,6); //copy mac dich vai mac nguon memcpy(ping_packet->MAC_dich,buffer_temp,6); //hoan vi IP memcpy(buffer_temp,ping_packet->SourceIP,4); //copy ip nguon vao bo nho tam memcpy(ping_packet->SourceIP,ping_packet->DestIP,4); //copy ip dich vai mac nguon memcpy(ping_packet->DestIP,buffer_temp,4); //make packet reply ping_packet->ICMP_Type = 0x00; //type = reply ping_packet->ICMP_checkSum = ICMP_checksum(ping_packet,len); NET_SendFrame((uint8_t *)ping_packet,len); //gui phan hoi } |
Quá rõ ràng rồi, không cần giải thích nữa nhỉ 🙂
Đừng quên gọi hàm reply trong hàm ICMP_read nhé !
Giờ chạy mô phỏng và ping thử tới enc28j60 của chúng ta thôi
Máy tính đã nhận được ping phản hồi không trượt phát nào 🙂
WireShark cũng bắt được cái gói ping request và ping reply
Dowload
Toàn bộ soure cho bài giao thức icmp các bạn có thể tải tại đây
Trong bài tiếp theo, chúng ta sẽ tìm hiểu về giao thức UDP, cũng là 1 giao thức nằm trong IP và làm 1 project đó là điều khiển led từ xa qua mạng lan bằng UDP
Cảm ơn bạn đã chia sẻ bài viết rất hay. Tôi đã thử đến bước ping đến ENC28J60 với tham số -t nhưng chỉ được một thời gian rất ngắn thì bị timeout. Bạn có thể giải thích được không? Cảm ơn nhiều!!!
Code này chưa ổn định lắm, muốn chạy ổn định thì phải sửa đổi nhiều
checksum += (uint16_t) (((uint32_t)*ptr<<8)|*(ptr+1)); taị sao ad lại ép kiểu về uint32_t, trong khi *ptr<<8 thì cùng lắm chỉ là 16 bit