Kỹ thuật lập trình với socket IP

    0

    Socket thô (raw socket), hay socket Ip, là loại socket giúp bạn gọi trực tiếp đến dịch vụ truyền thông host-to-host do giao thức IP cung cấp, bỏ qua dịch vụ truyền end-to-end của các giao thức ở tầng giao vận (TCP hoặc UDP). Loại socket này rất hữu dụng khi bạn cần xây dựng những chương trình như ping, tracert – loại chương trình cần sử dụng trực tiếp giao thức IP để truyền dữ liệu.

    Socket Ip (socket thô)

    Trước khi bắt đầu với lập trình socket thô, hãy cùng trao đổi vì sao lại cần làm việc trực tiếp với dịch vụ truyền thông host-to-host của IP.

    Khi cấu hình mạng trên các máy tính, dù là Windows hay Linux, bạn thường xuyên sử dụng lệnh ping để kiểm tra xem các máy đã có thể trao đổi thông tin hay chưa.

    Ping là một tiện ích hệ thống rất thông dụng trên các hệ điều hành desktop và thường chạy ở giao diện dòng lệnh.

    Dưới đây là ví dụ khi ping default gateway trong mạng LAN gia đình trên Windows Command Prompt.

    Lệnh ping trên windows command prompt

    Bạn đã bao giờ đặt câu hỏi làm sao để xây dựng được những chương trình như ping?

    Vấn đề ở chỗ, bạn có thể ping IP của một máy khác, và bạn đồng thời cũng có thể ping được IP của các thiết bị mạng như router, tường lửa – nơi không có chương trình ứng dụng hoạt động.

    Qua các nội dung về lập trình socket TCP và UDP, có lẽ bạn đoán được rằng chúng ta không thể sử dụng hai loại socket này để xây dựng những chương trình như ping.

    Thực tế, ping là một trong số những loại ứng dụng không sử dụng truyền thông end-to-end do TCP và UDP cung cấp. Thay vào đó chúng sử dụng truyền thông host-to-host do IP cung cấp.

    Chương trình ping sử dụng một giao thức truyền thông có tên gọi là ICMP – Internet Control Message Protocol.

    ICMP có mục tiêu là giúp trao đổi thông tin giữa các host (truyền thông host-to-host). Để thực hiện mục tiêu giúp các host trao đổi thông tin, ICMP cần phương tiện mang dữ liệu là giao thức IP. Thiết bị nào có khả năng hiểu giao thức IP thì thường cũng được cài đặt giao thức ICMP.

    TCP hay UDP lại giúp trao đổi dữ liệu giữa các tiến trình hoạt động trên các host (truyền thông end-to-end). Vì vậy, ICMP không sử dụng được TCP hay UDP cho mục tiêu của mình. Mặc dù vậy, ICMP lại không phải là một giao thức cùng cấp độ với TCP hay UDP vì nó không giúp trao đổi dữ liệu người dùng.

    Từ ví dụ với giao thức ICMP đặt ra yêu cầu là chương trình phải có khả năng sử dụng trực tiếp giao thức IP, thay vì sử dụng TCP hay UDP. Do vậy, các hệ thống cũng đều cung cấp socket API để truy xuất trực tiếp dịch vụ truyền thông của IP. Người ta gọi loại socket này là socket thô hay socket Ip.

    Kỹ thuật lập trình với socket thô

    Kỹ thuật lập trình với socket thô không khác biệt nhiều với UDP socket.

    Để tạo object socket thô, bạn có thể sử dụng overload sau của lớp Socket:

    Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);

    Hãy để ý tham số thứ hai SocketType.Raw. Đây là giá trị bắt buộc phải sử dụng để tạo ra object cho socket thô.

    Tham số thứ ba có thể nhận nhiều giá trị khác nhau, tùy thuộc vào giao thức bạn muốn sử dụng. Trong ví dụ trên chúng ta tạo socket thô để gửi đi gói tin ICMP, vì vậy chúng ta sử dụng giá trị ProtocolType.Icmp.

    Để gửi gói tin qua socket thô, bạn phải sử dụng phương thức SendTo của lớp Socket, tương tự như với UDP socket.

    IPEndPoint endpoint = new IPEndPoint(IPAddress.Parse("192.168.1.2"), 0);
    socket.SendTo(packet, endpoint);

    Do phương thức SendTo yêu cầu tham số thứ hai là một object có kiểu IPEndPoint, bạn cần khởi tạo object IPEndPoint. Tuy nhiên, do socket thô gửi gói tin trực tiếp qua giao thức IP nên giá trị port không được sử dụng. Vì vậy khi tạo object IPEndPoint bạn có thể để port là 0 hay bất kỳ giá trị nào.

    Tham số packet là chuỗi byte bạn cần gửi đi đến host đích. Khi gọi phương thức SendTo, chuỗi byte này sẽ được gắn vào một IP header để tạo thành một IP dgram. Giao thức IP sẽ gửi dgram này tới host đích.

    Bạn phải tự mình tạo chuỗi byte packet theo cấu trúc gói tin của giao thức sử dụng. Ví dụ, nếu cần gửi gói tin ICMP, bạn phải tự mình tạo chuỗi byte theo đúng cấu trúc của ICMP. Giao thức IP không can thiệp gì vào chuỗi byte này (ngoại trừ gắn thêm IP header).

    Để nhận gói tin qua socket thô, bạn sử dụng phương thức ReceiveFrom của lớp Socket (tương tự như Udp).

    public int ReceiveFrom (byte[] buffer, ref System.Net.EndPoint remoteEP);

    Tuy nhiên, khi nhận gói tin thô có một điểm khác biệt quan trọng cần lưu ý. Khi gọi ReceiveFrom, cái bạn nhận được (qua tham số buffer) là cả gói tin IP nguyên vẹn (bao gồm cả IP header). Do vậy, để lấy ra phần dữ liệu, bạn cần tự mình cắt bỏ IP header. Đây là điều bạn không gặp khi làm việc với TCP hay UDP socket.

    Một điều may mắn là IP header có độ dài cố định 20 byte. Vì vậy, khối dữ liệu của dgram bắt đầu từ byte thứ 21, nghĩa là bắt đầu từ phần tử buffer[20].

    Cũng vì lý do này, mảng byte buffer phải có kích thước đủ lớn để chứa thêm cả 20 byte của IP header (bên cạnh phần dữ liệu).

    Để học cách làm việc với socket thô, chúng ta sẽ cùng xây dựng một chương trình mô phỏng hoạt động của ứng dụng ping sử dụng giao thức ICMP.

    Giao thức ICMP

    ICMP – định nghĩa trong RFC 792 – là một giao thức được thực thi trên nhiều loại thiết bị mạng (host) giúp các thiết bị này trao đổi thông tin. Đây là giao thức được sử dụng trong các ứng dụng hỗ trợ quản lý mạng như ping hay trace route.

    Gói tin của ICMP đặt trong gói tin IP

    Toàn bộ gói tin ICMP được đặt trong phần dữ liệu của gói tin IP.

    Gói tin ICMP có cấu trúc khá đơn giản:

    Trường type (1 byte): xác định loại thông điệp. Giá trị của trường type cũng được gọi là type code.

    Mỗi type code có ý nghĩa riêng. Ví dụ: type code 0 – echo reply (gói tin phản hồi của thông điệp echo); type code 8 – echo request (gói tin yêu cầu của thông điệp echo).

    Thông điệp echo được sử dụng cho ứng dụng ping. Chúng ta sẽ quay lại với thông điệp echo sau.

    Trường code (1 byte): xác định thông tin chi tiết và làm rõ hơn ý nghĩa mô tả trong trường type.

    Ví dụ, code 0 – network unreachable, code 1 – host unreachable.

    Trường checksum (2 byte) – dùng để kiểm soát lỗi. Trường này được tính theo một thuật toán riêng.

    Trường message có độ dài biến đổi và có thể chứa nhiều loại thông tin khác nhau. Hai loại thông tin thường gặp trong trường mesage là số định danh của gói tin (identifier) và số thứ tự (sequence number).

    Xây dựng chương trình ping

    Chương trình ping là một trong những ứng dụng phổ biến của ICMP. Chương trình này sử dụng socket thô để gửi gói tin echo request của ICMP và nhận lại gói tin echo reply.

    Gói tin Echo Request có type = 8 và chứa một thông điệp bất kỳ. Khi máy đích nhận được gói tin này sẽ gửi lại gói tin Echo Reply với type = 0 và thông điệp lặp lại từ Echo Request.

    Sử dụng ICMP trong ping

    Chúng ta cùng xây dựng chương trình ping dựa trên những kiến thức vừa học.

    Bước 1. Tạo một project mới thuộc kiểu ConsoleApp và đặt tên là Ping.

    Bước 2. Thêm vào project file mã nguồn Icmp.cs và viết code như sau:

    using System;
    using System.Collections.Generic;
    using System.Net;
    
    namespace Ping {
    	internal class Icmp {
    		public byte Type { get; set; } = 0x08;
    		public byte Code { get; set; } = 0x00;
    
    		public ushort Checksum { get; set; } = 0;
    
    		private ushort _identifier;
    		public ushort Identifier {
    			get => (ushort)IPAddress.NetworkToHostOrder((short)_identifier);
    			set => _identifier = (ushort)IPAddress.HostToNetworkOrder((short)value);
    		}
    
    		private ushort _sequence;
    		public ushort Sequence {
    			get => (ushort)IPAddress.NetworkToHostOrder((short)_sequence);
    			set => _sequence = (ushort)IPAddress.HostToNetworkOrder((short)value);
    		}
    
    		public byte[] Payload { get; set; }
    
    		public int PayloadSize => Payload.Length;
    
    		public Icmp() {
    
    		}
    
    		public Icmp(byte[] ipDgram, int ipDgramLength) {
    			Type = ipDgram[20];
    			Code = ipDgram[21];
    			Checksum = BitConverter.ToUInt16(ipDgram, 22);
    			_identifier = BitConverter.ToUInt16(ipDgram, 24);
    			_sequence = BitConverter.ToUInt16(ipDgram, 26);
    			var payloadSize = ipDgramLength - 28;
    			Payload = new byte[payloadSize];
    			Array.Copy(ipDgram, 28, Payload, 0, payloadSize);
    		}
    
    		public byte[] GetBytes() {
    			var bytes = new List<byte>();
    			bytes.Add(Type);
    			bytes.Add(Code);
    			bytes.AddRange(BitConverter.GetBytes(Checksum));
    			bytes.AddRange(BitConverter.GetBytes(_identifier));
    			bytes.AddRange(BitConverter.GetBytes(_sequence));
    			bytes.AddRange(Payload);
    
    			return bytes.ToArray();
    		}
    
    		public static void SetChecksum(Icmp packet) {
    			packet.Checksum = 0;
    			uint checksum = 0;
    			byte[] data = packet.GetBytes();
    
    			for (int i = 0; i < data.Length; i += 2) {
    				checksum += Convert.ToUInt32(BitConverter.ToUInt16(data, i));
    			}
    
    			checksum = (checksum >> 16) + (checksum & 0xffff);
    			checksum += (checksum >> 16);
    
    			packet.Checksum = (ushort)(~checksum);
    		}
    	}
    }

    Bước 3. Thêm vào project file mã nguồn mới Ping.cs và viết code như sau:

    using System;
    using System.Diagnostics;
    using System.Net;
    using System.Net.Sockets;
    using System.Text;
    using System.Threading;
    
    namespace Ping {
    	internal class Ping {
    		public Action<string> Log { get; set; }
    		public void Run(IPAddress address, int loops = 4, int timeout = 3000, string message = "1234567890", int sleep = 1500) {
    			
    			var _socket = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.Icmp);
    			_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, timeout);
    
    			IPEndPoint remoteEP = new IPEndPoint(address, 0);
    			EndPoint dumpEP = new IPEndPoint(IPAddress.Any, 0);
    			byte[] data = new byte[1024];
    
    			var id = (ushort)Process.GetCurrentProcess().Id;
    			var req = new Icmp { Identifier = id, Sequence = 0, Payload = Encoding.ASCII.GetBytes(message) };
    			Icmp.SetChecksum(req);
    
    			Log?.Invoke($"Pinging {remoteEP.Address} with {req.PayloadSize} bytes of message, process id={req.Identifier}");
    			Stopwatch sw = new Stopwatch();
    
    			for (int i = 0; i < loops; i++) {
    				sw.Start();
    				_socket.SendTo(req.GetBytes(), remoteEP);
    				try {
    					int ipDgramLength = _socket.ReceiveFrom(data, ref dumpEP);
    					sw.Stop();
    					var res = new Icmp(data, ipDgramLength);
    					Log?.Invoke($"Reply from {remoteEP}: {ipDgramLength - 28} bytes, id={res.Identifier}, seq={res.Sequence}, {sw.ElapsedTicks} ticks ({sw.ElapsedMilliseconds} ms)");
    
    				} catch (SocketException) {
    					Log?.Invoke("Timeout");
    				}
    				
    				req.Sequence++;
    				Icmp.SetChecksum(req);
    				sw.Reset();
    				Thread.Sleep(sleep);
    			}
    		}
    	}
    }

    Bước 4. Viết lại file Program.cs như sau:

    using System;
    using System.Net;
    
    namespace Ping {
    	internal class Program {
    		private static void Main(string[] args) {
    			Console.Title = "My Ping";
    
    			IPAddress getIp(string arg) {
    				if (IPAddress.TryParse(arg, out IPAddress ip)) return ip;
    				else throw new Exception("Invalid Ip address");
    			}
    
    			var ping = new Ping() { Log = Console.WriteLine };
    			while (true) {
    				Console.Write("Ip > ");
    				var ip = Console.ReadLine();
    				ping.Run(getIp(ip));
    				Console.WriteLine();
    			}
    			
    		}
    	}
    }

    Bước 5. Chạy thử nghiệm chương trình từ giao diện dòng lệnh

    Kết quả hoạt động của chương trình ping vừa xây dựng

    Kết luận

    Trong bài học này chúng ta đã học cách làm việc với socket IP (socket thô) thông qua xây dựng một ứng dụng đơn giản (ping) sử dụng giao thức ICMP.

    Socket thô cho phép bỏ qua dịch vụ truyền thông end-to-end do giao thức UDP hoặc TCP cung cấp (tầng transport) để sử dụng dịch vụ truyền thông host-to-host do giao thức IP cung cấp.

    Khi lập trình với socket thô vẫn sử dụng lớp Socket của .NET với các phương thức gửi/nhận dữ liệu gần tương tự như với UDP socket nhưng chương trình cần tự mình loại bỏ 20 byte của IP header trước khi xử lý phần dữ liệu.

    * Bản quyền bài viết thuộc về Tự học ICT. Đề nghị tôn trọng bản quyền. DMCA.com Protection Status
    Subscribe
    Notify of
    guest
    0 Thảo luận
    Inline Feedbacks
    View all comments