Go语言玩转原始套接字通信:从入门到飞起

Go语言玩转原始套接字通信:从入门到飞起

1. 概述:从协议小白到网络老手

想象一下,你就是个网络魔法师,可以操控每一个数据包,直接给它们安排源头、目的地,甚至在中途变个花样。这就是原始套接字的魅力所在!在本教程中,我们将通过Go语言实现一个简单的原始套接字通信系统,玩转数据包的发送和接收。


2. 初识原始套接字

原始套接字是个有趣的家伙。它能让你绕过传输层,直接操控 IP 层,甚至更底层的协议栈。一般来说,这种能力常用于:

  • 网络嗅探:捕获并分析网络数据包,进行安全审计。
  • 自定义协议:实现自定义的网络协议。
  • 网络诊断:直接发送和接收特定的数据包,检测网络状态。

注意:使用原始套接字需要管理员权限,因为它太强大了,容易玩脱。


3. 深入浅出 IP 协议头

3.1 IP 头部结构

每个 IP 数据包的头部包含了各种重要信息,就像寄快递时的包裹单。下面是一个典型的 IPv4 头部结构:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |Version|  IHL  |Type of Service|          Total Length         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         Identification        |Flags|      Fragment Offset    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Time to Live |    Protocol   |         Header Checksum       |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                       Source Address                          |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Destination Address                        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                    Options                    |    Padding    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段长度(位)描述
版本号4表示 IPv4。
头部长度4头部长度,单位为 32 位字。
服务类型8数据包的优先级等。
总长度16数据包的总长度。
标识符16数据包的唯一标识。
标志3分片控制位。
片偏移13数据分片的偏移量。
TTL8数据包生存时间。
协议8指示上层协议类型。
校验和16检查 IP 头部错误。
源 IP 地址32数据包的来源。
目标 IP 地址32数据包的目的地。

3.2 校验和计算

校验和是 IP 头部的一种自我检查机制,用于检测传输中的错误。计算方法很简单:

  1. 把头部按 16 位为一组分割,逐组相加。
  2. 如果有进位,加到结果里。
  3. 对结果取反。

4. 项目结构与实现思路

4.1 客户端

  • 构建自定义 IP 头部和数据:客户端首先需要构建一个符合 IP 协议格式的自定义 IP 头部,并将其与要发送的消息内容组合成一个完整的 IP 数据包。这个过程包括构造源 IP 地址、目标 IP 地址以及必要的校验和等字段。
  • 发送数据包到服务器:客户端将构建好的数据包通过原始套接字发送到目标服务器。此时,客户端的任务是确保数据包格式符合 IP 协议规范,并能够成功地将数据发送到指定的服务器地址。
  • 接收服务器的返回数据:客户端发送数据包后,需要等待服务器的响应。客户端会监听原始套接字,接收从服务器返回的响应数据包,并对接收到的数据包进行解析,提取有效信息,最终将响应内容展示给用户。

4.2 服务器

  • 监听原始套接字,接收数据包:服务器使用原始套接字监听网络接口,等待客户端发送的数据包。当服务器接收到数据包时,它会对数据包进行初步的处理和验证,确保数据包符合预期。
  • 解析 IP 头部,提取源 IP 和目标 IP:在接收到数据包后,服务器需要解析 IP 头部,从中提取源 IP 地址和目标 IP 地址。这一步非常重要,因为服务器需要知道数据包的来源和目标,以便进行后续的处理。
  • 把数据再发回客户端:服务器在接收到数据包并解析出相关信息后,会将数据包内容原封不动地返回给客户端。服务器通过原始套接字将数据包发送回客户端,确保客户端可以接收到并处理响应数据。

4.2 目录结构

project/
│
├── client.go        # 客户端代码
├── server.go        # 服务器端代码
├── rawutil.go       # 公共工具代码 (IP头部处理、校验和计算等)
  1. client.go:客户端的核心逻辑,包括构建 IP 头部、发送数据包和接收服务器响应数据。
  2. server.go:服务器的核心逻辑,包括监听数据包、解析 IP 头部并响应客户端数据包。
  3. rawutil.go:公用工具函数,包括 IP 头部构造、校验和计算、数据包解析等。

5. 代码实现

5.1 代码结构说明

  • rawutil.go: 封装了与 IP 头部相关的功能,包括创建 IP 头部、计算校验和、解析 IP 头部等函数,这些功能在客户端和服务器端都可以重用。

  • server.go: 服务器监听原始套接字,接收数据包,解析 IP 头部,提取源 IP 和目标 IP,并将数据包返回给客户端。

  • client.go: 客户端构建自定义 IP 头部,发送数据包到服务器,并接收服务器的回包。

5.2 测试地址说明

为了方便测试, 这里服务器, 客户端都在同一台电脑上

  • 服务器, 绑定本地的127.0.0.1地址
  • 客户端, 绑定本地的127.0.0.2地址(没错,这个地址也是发送到本地的, 实际上127开头的地址都是发送到本地的)

5.3 server.go - 服务器端实现

package main

import (
	"log"
	"syscall"

	"rawutil"
)

func main() {
	// 创建原始套接字,监听所有 IP 数据包
	sock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
	if err != nil {
		log.Fatalf("Error creating raw socket: %v", err)
	}
	defer syscall.Close(sock)

	// 绑定原始套接字到127.0.0.1
	if err := rawutil.BindToAddress(sock, "127.0.0.1"); err != nil {
		log.Fatalf("Error binding raw socket: %v", err)
	}

	// 缓冲区用来接收数据
	buffer := make([]byte, 1500)

	for {
		// 接收数据包
		n, from, err := syscall.Recvfrom(sock, buffer, 0)
		if err != nil {
			log.Printf("Error reading packet: %v", err)
			break
		}

		log.Printf("Received %d bytes from %v", n, from)

		// 解析接收到的 IP 头部
		srcIP, dstIP, err := rawutil.ParseIPHeader(buffer[:n])
		if err != nil {
			log.Printf("Error parsing IP header: %v", err)
			continue
		}
		msgRecv := string(buffer[rawutil.IPHeaderLength:])
		log.Printf("Received packet ip.src: %s ip.dst: %s [%s]", srcIP, dstIP, msgRecv)

		// 发送数据包
		log.Printf("Replying to %s", srcIP)
		err = rawutil.SendRawPacket(sock, dstIP, srcIP, []byte(msgRecv), syscall.IPPROTO_RAW)
		if err != nil {
			log.Printf("Error sending packet: %v", err)
		}
	}
}

5.4 client.go - 客户端实现

package main

import (
	"fmt"
	"log"
	"syscall"

	"rawutil"
)

func main() {
	srcIP := "127.0.0.2"
	dstIP := "127.0.0.1"
	message := "Hello, world!"

	// 创建原始套接字
	sock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
	if err != nil {
		fmt.Printf("Error creating raw socket: %v\n", err)
		return
	}
	defer syscall.Close(sock)

	// 绑定原始套接字到本地地址
	if err := rawutil.BindToAddress(sock, srcIP); err != nil {
		log.Fatalf("Error binding raw socket: %v", err)
	}
	fmt.Printf("Bind raw socket to %s\n", srcIP)

	// 发送数据包
	err = rawutil.SendRawPacket(sock, srcIP, dstIP, []byte(message), syscall.IPPROTO_RAW)
	if err != nil {
		log.Fatalf("Error sending raw packet: %v\n", err)
	}
	fmt.Printf("Sent message [%s] to %s\n", message, dstIP)

	// 接收数据包
	var packet [1500]byte
	n, _, err := syscall.Recvfrom(sock, packet[:], 0)
	if err != nil {
		fmt.Printf("Error receiving raw packet: %v\n", err)
		return
	}

	// 解析 IP 头部
	rcv_srcIP, rcv_dstIP, err := rawutil.ParseIPHeader(packet[:n])
	if err != nil {
		fmt.Printf("Error parsing IP header: %v\n", err)
		return
	}
	recv_msg := string(packet[rawutil.IPHeaderLength:])

	// 打印接收到的消息
	fmt.Printf("Received message ip.src %s ip.dst %s: [%s]\n", rcv_srcIP, rcv_dstIP, recv_msg)
}


5.5 rawutil.go - 公用工具函数

package rawutil

import (
	"encoding/binary"
	"fmt"
	"net"
	"syscall"
)

// 定义 IP 头部结构
const (
	IPHeaderLength = 20 // IP 头部长度
)

// IPHeader 用于构造和解析 IP 头部
type IPHeader struct {
	VersionIhl      uint8  // 版本 + 头部长度
	TypeOfService   uint8  // 服务类型
	TotalLength     uint16 // 总长度
	Identification  uint16 // 标识符
	FlagsFragOffset uint16 // 标志 + 片偏移
	TTL             uint8  // TTL
	Protocol        uint8  // 协议类型
	Checksum        uint16 // 校验和
	SrcAddr         uint32 // 源 IP 地址
	DstAddr         uint32 // 目标 IP 地址
}

// 校验和计算
func checksum(data []byte) uint16 {
	var sum uint32
	// 每 2 字节为一个单位进行加和
	for i := 0; i < len(data); i += 2 {
		word := uint16(data[i])<<8 + uint16(data[i+1])
		sum += uint32(word)
	}
	// 加上高 16 位和低 16 位的进位
	for sum>>16 > 0 {
		sum = (sum & 0xFFFF) + (sum >> 16)
	}
	return ^uint16(sum)
}

// 解析 IP 头部,提取源 IP 和目标 IP
func ParseIPHeader(packet []byte) (srcIP, dstIP string, err error) {
	if len(packet) < IPHeaderLength {
		return "", "", fmt.Errorf("packet too short to be an IP packet")
	}

	ipHeader := IPHeader{
		VersionIhl:      packet[0],
		TypeOfService:   packet[1],
		TotalLength:     binary.BigEndian.Uint16(packet[2:4]),
		Identification:  binary.BigEndian.Uint16(packet[4:6]),
		FlagsFragOffset: binary.BigEndian.Uint16(packet[6:8]),
		TTL:             packet[8],
		Protocol:        packet[9],
		Checksum:        binary.BigEndian.Uint16(packet[10:12]),
		SrcAddr:         binary.BigEndian.Uint32(packet[12:16]),
		DstAddr:         binary.BigEndian.Uint32(packet[16:20]),
	}

	// 将二进制地址转换为点分十进制的字符串形式
	srcIP = fmt.Sprintf("%d.%d.%d.%d", byte(ipHeader.SrcAddr>>24), byte(ipHeader.SrcAddr>>16&0xFF), byte(ipHeader.SrcAddr>>8&0xFF), byte(ipHeader.SrcAddr&0xFF))
	dstIP = fmt.Sprintf("%d.%d.%d.%d", byte(ipHeader.DstAddr>>24), byte(ipHeader.DstAddr>>16&0xFF), byte(ipHeader.DstAddr>>8&0xFF), byte(ipHeader.DstAddr&0xFF))

	return srcIP, dstIP, nil
}

// 构造 IP 头部
func createIPHeader(srcIP, dstIP string, protocol uint8, msg_len uint16) ([]byte, error) {
	srcAddr := net.ParseIP(srcIP).To4()
	if srcAddr == nil {
		return nil, fmt.Errorf("invalid source IP address")
	}

	dstAddr := net.ParseIP(dstIP).To4()
	if dstAddr == nil {
		return nil, fmt.Errorf("invalid destination IP address")
	}

	// 设置 IP 头部
	ipHeader := IPHeader{
		VersionIhl:      (4 << 4) | 5, // IPv4 + 头部长度 5
		TypeOfService:   0,
		TotalLength:     IPHeaderLength + msg_len, // IP 头部 + 数据部分
		Identification:  0,
		FlagsFragOffset: 0,
		TTL:             64,
		Protocol:        protocol,
		SrcAddr:         binary.BigEndian.Uint32(srcAddr),
		DstAddr:         binary.BigEndian.Uint32(dstAddr),
	}

	// 构造 IP 头部
	header := make([]byte, IPHeaderLength)
	header[0] = ipHeader.VersionIhl
	header[1] = ipHeader.TypeOfService
	binary.BigEndian.PutUint16(header[2:4], ipHeader.TotalLength)
	binary.BigEndian.PutUint16(header[4:6], ipHeader.Identification)
	binary.BigEndian.PutUint16(header[6:8], ipHeader.FlagsFragOffset)
	header[8] = ipHeader.TTL
	header[9] = ipHeader.Protocol
	binary.BigEndian.PutUint16(header[10:12], ipHeader.Checksum)
	binary.BigEndian.PutUint32(header[12:16], ipHeader.SrcAddr)
	binary.BigEndian.PutUint32(header[16:20], ipHeader.DstAddr)

	// 计算校验和
	checksumValue := checksum(header)
	binary.BigEndian.PutUint16(header[10:12], checksumValue)

	return header, nil
}

// 发送原始数据包
func SendRawPacket(sock int, srcIP, dstIP string, message []byte, protocol uint8) error {
	ipHeader, err := createIPHeader(srcIP, dstIP, protocol, uint16(len(message)))
	if err != nil {
		return fmt.Errorf("failed to create IP header: %v", err)
	}

	// 构造完整的数据包:IP 头部 + 数据
	packet := append(ipHeader, message...)

	// 设置目标地址
	addr := &syscall.SockaddrInet4{}
	copy(addr.Addr[:], net.ParseIP(dstIP).To4())

	// 发送数据包
	err = syscall.Sendto(sock, packet, 0, addr)
	return err
}

// 绑定到指定地址
func BindToAddress(sock int, ip string) error {
	sa := &syscall.SockaddrInet4{
		Port: 0, // raw socket 不使用端口,所以设置为0
	}
	ipAddr := net.ParseIP(ip)
	if ipAddr == nil {
		return fmt.Errorf("failed to Parse IP: %v", ip)
	}
	copy(sa.Addr[:], ipAddr.To4())

	if err := syscall.Bind(sock, sa); err != nil {
		return fmt.Errorf("failed to bind to address: %v  err=%v", sa, err)
	}
	return nil
}

5.6 运行与测试

  1. 启动服务器

    sudo go run server.go
    
  2. 启动客户端

    sudo go run client.go  
    
  3. 观察输出:客户端将发送一个数据包,服务器收到数据包后会返回数据包,客户端会接收到服务器的回应,并打印响应的源和目标 IP 地址。

6. 总结与下一步探索

恭喜你!现在你已经掌握了使用 Go 语言实现原始套接字通信的基本技能。通过本教程,你学会了如何构造和解析 IP 数据包,使用原始套接字发送和接收数据,并实现了一个简易的客户端-服务器通信系统。你已经打下了扎实的网络编程基础,接下来有很多更深层次的知识等待你去探索。

6.1 下一步,你可以尝试:

  • 构建更复杂的协议:在本教程中,我们主要讨论了 IP 协议,但网络通信不仅仅限于此。你可以尝试设计和实现自己的应用层协议,甚至是自定义的传输协议。通过研究 TCP、UDP 或其他协议栈的实现原理,进一步扩展你的网络编程知识。

  • 实现数据包嗅探工具:数据包嗅探是网络分析中重要的一环。你可以编写一个简单的数据包嗅探工具,捕获并分析网络上的流量。通过这个项目,你将进一步理解协议栈的工作原理,甚至可能发现一些有趣的网络安全漏洞。

  • 深入研究网络安全:你已经了解了原始套接字的基础,接下来可以尝试探索更高级的网络安全技术。例如,如何利用网络嗅探技术进行攻击与防御,或者深入了解如何通过操控数据包进行渗透测试。这不仅能提高你的网络安全意识,还能帮助你更好地理解如何保护网络免受潜在威胁。

6.2 动手实验吧,网络魔法师!

理论知识的积累固然重要,但最重要的是通过实践来加深理解。去试试更多的网络编程项目,挑战自己,解决新的问题,甚至和其他网络编程爱好者一起分享你的经验。无论你选择深入哪个领域,记住:网络世界充满了未知与挑战,只有不断探索,才能成为真正的网络专家。

祝你在未来的探索旅程中,始终保持好奇心和创造力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值