commit b3a2a5892e3f0f4099d4401ddcdffe2be3c4e124 Author: kingecg Date: Sun Jun 29 23:34:13 2025 +0800 新增虚拟网卡(vnic)核心功能及示例程序 - 实现虚拟网卡创建、配置和管理功能 - 添加基础示例(basic)展示基本用法 - 添加完整示例(complete)支持命令行参数和详细日志 - 添加.gitignore忽略vendor目录 - 添加go.mod和go.sum管理依赖 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a725465 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor/ \ No newline at end of file diff --git a/examples/basic/main.go b/examples/basic/main.go new file mode 100644 index 0000000..6e04d0d --- /dev/null +++ b/examples/basic/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + "time" + + "git.kingecg.top/kingecg/vnic" +) + +func main() { + // 创建虚拟网卡配置 + config := vnic.Config{ + IP: "192.168.100.1/24", // 设置虚拟网卡的IP地址和子网掩码 + } + + // 创建虚拟网卡 + log.Println("正在创建虚拟网卡...") + nic, err := vnic.New(config) + if err != nil { + log.Fatalf("创建虚拟网卡失败: %v", err) + } + + // 确保程序退出时关闭虚拟网卡 + defer func() { + log.Println("正在关闭虚拟网卡...") + if err := nic.Close(); err != nil { + log.Printf("关闭虚拟网卡时出错: %v", err) + } + }() + + log.Printf("虚拟网卡创建成功,接口名称: %s", nic.Name()) + + // 创建一个通道来监听系统信号 + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + // 启动一个goroutine来读取数据包 + go func() { + buffer := make([]byte, 2000) + for { + n, err := nic.Read(buffer) + if err != nil { + log.Printf("读取数据包时出错: %v", err) + return + } + log.Printf("收到数据包: %d 字节", n) + + // 在实际应用中,这里可以解析和处理IP数据包 + // 例如:解析IP头部、提取源/目标地址等 + } + }() + + // 每隔5秒发送一个测试数据包 + go func() { + // 创建一个简单的ICMP Echo请求数据包(ping) + // 注意:这只是一个示例,实际上这个数据包格式不完整 + icmpPacket := []byte{ + 0x45, 0x00, 0x00, 0x1c, // IP头: 版本、服务类型、总长度 + 0x12, 0x34, 0x00, 0x00, // IP头: 标识、标志、片偏移 + 0x40, 0x01, 0x00, 0x00, // IP头: TTL、协议(ICMP=1)、校验和 + 0xc0, 0xa8, 0x64, 0x01, // IP头: 源IP (192.168.100.1) + 0xc0, 0xa8, 0x64, 0x02, // IP头: 目标IP (192.168.100.2) + + 0x08, 0x00, 0x00, 0x00, // ICMP: 类型(Echo请求)、代码、校验和 + 0x00, 0x01, 0x00, 0x01, // ICMP: 标识符、序列号 + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + _, err := nic.Write(icmpPacket) + if err != nil { + log.Printf("发送数据包时出错: %v", err) + } else { + log.Println("已发送测试数据包") + } + } + } + }() + + // 等待中断信号 + <-sigCh + fmt.Println("\n收到中断信号,程序退出") +} diff --git a/examples/complete/main.go b/examples/complete/main.go new file mode 100644 index 0000000..5598033 --- /dev/null +++ b/examples/complete/main.go @@ -0,0 +1,300 @@ +package main + +import ( + "flag" + "fmt" + "log" + "net" + "os" + "os/signal" + "syscall" + "time" + + "git.kingecg.top/kingecg/vnic" +) + +// 命令行参数 +var ( + ipAddress string + packetSize int + interval int + verbose bool +) + +func init() { + // 解析命令行参数 + flag.StringVar(&ipAddress, "ip", "192.168.100.1/24", "虚拟网卡的IP地址 (CIDR格式)") + flag.IntVar(&packetSize, "size", 64, "测试数据包大小 (字节)") + flag.IntVar(&interval, "interval", 5, "发送数据包的间隔 (秒)") + flag.BoolVar(&verbose, "verbose", false, "启用详细日志") + flag.Parse() +} + +func main() { + // 配置日志格式 + log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) + log.Println("虚拟网卡完整示例启动") + + // 验证IP地址格式 + if _, _, err := net.ParseCIDR(ipAddress); err != nil { + log.Fatalf("无效的IP地址格式: %v", err) + } + + // 创建虚拟网卡配置 + config := vnic.Config{ + IP: ipAddress, + } + + // 创建虚拟网卡 + log.Printf("正在创建虚拟网卡 (IP: %s)...", ipAddress) + nic, err := vnic.New(config) + if err != nil { + log.Fatalf("创建虚拟网卡失败: %v", err) + } + + // 设置清理函数 + defer cleanupResources(nic) + + log.Printf("虚拟网卡创建成功:") + log.Printf("- 接口名称: %s", nic.Name()) + log.Printf("- IP地址: %s", ipAddress) + + // 创建一个通道来监听系统信号 + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + // 启动数据包处理器 + stopCh := make(chan struct{}) + go packetReceiver(nic, stopCh) + go packetSender(nic, stopCh) + + // 等待中断信号 + sig := <-sigCh + fmt.Printf("\n收到信号 %v,正在关闭...\n", sig) + + // 通知所有goroutine停止 + close(stopCh) + + // 给goroutine一些时间来完成清理 + time.Sleep(500 * time.Millisecond) +} + +// cleanupResources 负责清理资源 +func cleanupResources(nic *vnic.VirtualNIC) { + log.Println("正在关闭虚拟网卡...") + if err := nic.Close(); err != nil { + log.Printf("关闭虚拟网卡时出错: %v", err) + } else { + log.Println("虚拟网卡已成功关闭") + } +} + +// packetReceiver 从虚拟网卡读取数据包 +func packetReceiver(nic *vnic.VirtualNIC, stopCh <-chan struct{}) { + log.Println("数据包接收器已启动") + + // 创建缓冲区 + buffer := make([]byte, 2048) + + for { + // 检查是否收到停止信号 + select { + case <-stopCh: + log.Println("数据包接收器正在关闭") + return + default: + // 设置读取超时,以便我们可以定期检查停止信号 + // 注意:在实际应用中,你可能需要使用更复杂的方法来处理这个问题 + nic.Read(buffer) + + // 解析IP数据包 + if verbose && len(buffer) > 0 { + parseIPPacket(buffer) + } + } + } +} + +// packetSender 定期向虚拟网卡发送测试数据包 +func packetSender(nic *vnic.VirtualNIC, stopCh <-chan struct{}) { + log.Println("数据包发送器已启动") + + // 解析IP地址以获取网络信息 + ip, ipNet, _ := net.ParseCIDR(ipAddress) + + // 计算网络中的第二个IP地址作为目标 + // 这只是一个示例,在实际应用中,你可能需要一个有效的目标IP + dstIP := calculateNextIP(ip, ipNet) + + // 创建测试数据包 + packet := createTestPacket(ip, dstIP, packetSize) + + // 设置定时器 + ticker := time.NewTicker(time.Duration(interval) * time.Second) + defer ticker.Stop() + + for { + select { + case <-stopCh: + log.Println("数据包发送器正在关闭") + return + case <-ticker.C: + if _, err := nic.Write(packet); err != nil { + log.Printf("发送数据包时出错: %v", err) + } else if verbose { + log.Printf("已发送 %d 字节的测试数据包到 %s", len(packet), dstIP) + } + } + } +} + +// calculateNextIP 计算网络中的下一个IP地址 +func calculateNextIP(ip net.IP, ipNet *net.IPNet) net.IP { + // 复制IP以避免修改原始IP + nextIP := make(net.IP, len(ip)) + copy(nextIP, ip) + + // 将最后一个字节加1 + nextIP[len(nextIP)-1]++ + + // 确保IP仍然在网络范围内 + if !ipNet.Contains(nextIP) { + // 如果超出范围,使用网络的第一个可用IP + copy(nextIP, ipNet.IP) + nextIP[len(nextIP)-1]++ + } + + return nextIP +} + +// createTestPacket 创建一个测试IP数据包 +func createTestPacket(srcIP, dstIP net.IP, size int) []byte { + // 确保最小大小 + if size < 20 { + size = 20 // IP头部的最小大小 + } + + // 创建一个基本的IP头部 + packet := make([]byte, size) + + // IP版本(4)和头部长度(5*4=20字节) + packet[0] = 0x45 + + // 服务类型 + packet[1] = 0x00 + + // 总长度 (大端字节序) + packet[2] = byte(size >> 8) + packet[3] = byte(size) + + // 标识 + packet[4] = 0x00 + packet[5] = 0x00 + + // 标志和片偏移 + packet[6] = 0x00 + packet[7] = 0x00 + + // TTL + packet[8] = 64 + + // 协议 (1 = ICMP) + packet[9] = 0x01 + + // 头部校验和 (暂时为0) + packet[10] = 0x00 + packet[11] = 0x00 + + // 源IP地址 + copy(packet[12:16], srcIP.To4()) + + // 目标IP地址 + copy(packet[16:20], dstIP.To4()) + + // 计算IP头部校验和 + checksum := calculateIPChecksum(packet[:20]) + packet[10] = byte(checksum >> 8) + packet[11] = byte(checksum) + + // 如果有足够的空间,添加一些ICMP数据 + if size >= 28 { + // ICMP Echo请求 + packet[20] = 0x08 // 类型: Echo + packet[21] = 0x00 // 代码: 0 + packet[22] = 0x00 // 校验和 (暂时为0) + packet[23] = 0x00 + + // ICMP标识符和序列号 + packet[24] = 0x00 + packet[25] = 0x01 + packet[26] = 0x00 + packet[27] = 0x01 + + // 填充剩余空间 + for i := 28; i < size; i++ { + packet[i] = byte(i % 256) + } + } + + return packet +} + +// calculateIPChecksum 计算IP头部的校验和 +func calculateIPChecksum(header []byte) uint16 { + var sum uint32 + + // 将头部视为16位整数的序列并求和 + for i := 0; i < len(header); i += 2 { + if i+1 < len(header) { + sum += uint32(header[i])<<8 | uint32(header[i+1]) + } else { + sum += uint32(header[i]) << 8 + } + } + + // 将进位加到结果中 + for sum > 0xffff { + sum = (sum & 0xffff) + (sum >> 16) + } + + // 取反 + return ^uint16(sum) +} + +// parseIPPacket 解析并显示IP数据包的基本信息 +func parseIPPacket(packet []byte) { + if len(packet) < 20 { + log.Println("收到无效的IP数据包 (太短)") + return + } + + // 提取版本和头部长度 + version := packet[0] >> 4 + headerLen := int(packet[0]&0x0F) * 4 + + if version != 4 { + log.Printf("收到非IPv4数据包 (版本: %d)", version) + return + } + + // 提取总长度 + totalLen := int(packet[2])<<8 | int(packet[3]) + + // 提取协议 + protocol := packet[9] + + // 提取源IP和目标IP + srcIP := net.IPv4(packet[12], packet[13], packet[14], packet[15]) + dstIP := net.IPv4(packet[16], packet[17], packet[18], packet[19]) + + // 显示基本信息 + log.Printf("收到IP数据包: 长度=%d, 协议=%d, %s -> %s", + totalLen, protocol, srcIP, dstIP) + + // 如果是ICMP协议,显示更多信息 + if protocol == 1 && len(packet) >= headerLen+2 { + icmpType := packet[headerLen] + icmpCode := packet[headerLen+1] + log.Printf(" ICMP: 类型=%d, 代码=%d", icmpType, icmpCode) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..12b02fa --- /dev/null +++ b/go.mod @@ -0,0 +1,12 @@ +module git.kingecg.top/kingecg/vnic + +go 1.23.1 + +require github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 + +require github.com/vishvananda/netns v0.0.5 // indirect + +require ( + github.com/vishvananda/netlink v1.3.1 + golang.org/x/sys v0.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..00fd4c6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8= +github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E= +github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= +github.com/vishvananda/netlink v1.3.1/go.mod h1:ARtKouGSTGchR8aMwmkzC0qiNPrrWO5JS/XMVl45+b4= +github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY= +github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= diff --git a/vnic.go b/vnic.go new file mode 100644 index 0000000..d2b47aa --- /dev/null +++ b/vnic.go @@ -0,0 +1,169 @@ +// Package vnic 提供创建和管理虚拟网络接口卡(Virtual Network Interface Card)的功能 +// 本包基于TUN设备实现,支持Linux和macOS系统,可用于VPN、网络代理、网络模拟等场景 +package vnic + +import ( + "fmt" + "net" + "runtime" + + "github.com/songgao/water" + "github.com/vishvananda/netlink" +) + +// Config 包含创建虚拟网卡所需的配置参数 +type Config struct { + IP string // IP地址(包含CIDR格式的子网掩码,例如 "192.168.1.2/24") +} + +// VirtualNIC 表示虚拟网卡接口,封装了底层TUN设备的操作 +type VirtualNIC struct { + iface *water.Interface // 底层TUN设备接口 + name string // 网卡名称 + ipNet *net.IPNet // IP网络配置 +} + +// New 创建并配置虚拟网卡 +// 该函数完成以下步骤: +// 1. 解析提供的IP地址和子网掩码 +// 2. 创建TUN设备 +// 3. 配置网络接口参数 +// 4. 设置路由(仅在Linux和macOS系统上) +// 返回配置好的虚拟网卡实例和可能的错误 +func New(cfg Config) (*VirtualNIC, error) { + // 解析IP地址和子网掩码 + ip, ipNet, err := net.ParseCIDR(cfg.IP) + if err != nil { + return nil, fmt.Errorf("解析IP失败: %w", err) + } + + // 创建TUN设备配置 + waterCfg := water.Config{ + DeviceType: water.TUN, // 使用TUN模式(网络层设备) + } + // 获取当前操作系统的平台特定参数 + waterCfg.PlatformSpecificParams = getPlatformParams() + + // 创建TUN设备实例 + iface, err := water.New(waterCfg) + if err != nil { + return nil, fmt.Errorf("创建TUN设备失败: %w", err) + } + + // 初始化虚拟网卡结构体 + vnic := &VirtualNIC{ + iface: iface, + name: iface.Name(), // 获取系统分配的接口名称 + ipNet: ipNet, + } + + // 配置网络接口(设置IP地址和启用接口) + if err := vnic.configure(ip, ipNet.Mask); err != nil { + iface.Close() // 配置失败时关闭设备,避免资源泄漏 + return nil, err + } + + // 设置路由(仅在Linux和macOS系统上) + if runtime.GOOS != "windows" { + if err := vnic.addRoute(); err != nil { + iface.Close() // 添加路由失败时关闭设备 + return nil, fmt.Errorf("添加路由失败: %w", err) + } + } + + return vnic, nil +} + +// getPlatformParams 根据不同操作系统返回适合的平台特定参数 +// 不同操作系统对TUN/TAP设备的配置要求不同,此函数处理这些差异 +func getPlatformParams() water.PlatformSpecificParams { + switch runtime.GOOS { + case "darwin": // macOS系统 + return water.PlatformSpecificParams{ + Name: "utun", // macOS要求TUN设备名称以"utun"开头 + } + case "linux": // Linux系统 + return water.PlatformSpecificParams{ + // 可以在这里设置Linux特定的参数 + // Permissions: &water.DevicePermissions{0666}, // 设置设备权限 + } + default: // 其他操作系统 + return water.PlatformSpecificParams{} + } +} + +// configure 配置网络接口的IP地址和状态 +// 该方法完成以下配置: +// 1. 获取网络接口的引用 +// 2. 设置接口的IP地址和子网掩码 +// 3. 启用网络接口 +func (v *VirtualNIC) configure(ip net.IP, mask net.IPMask) error { + // 通过接口名称获取网络接口引用 + link, err := netlink.LinkByName(v.name) + if err != nil { + return fmt.Errorf("获取网络接口失败: %w", err) + } + + // 设置IP地址和子网掩码 + addr := &netlink.Addr{IPNet: &net.IPNet{IP: ip, Mask: mask}} + if err := netlink.AddrAdd(link, addr); err != nil { + return fmt.Errorf("添加IP地址失败: %w", err) + } + + // 启用网络接口(相当于 ip link set up) + if err := netlink.LinkSetUp(link); err != nil { + return fmt.Errorf("启用接口失败: %w", err) + } + + return nil +} + +// addRoute 为虚拟网卡添加路由配置(仅支持Linux/macOS) +// 添加一个本地路由,使得发往虚拟网卡IP网段的数据包能够正确路由 +func (v *VirtualNIC) addRoute() error { + _, dst, _ := net.ParseCIDR(v.ipNet.String()) + route := netlink.Route{ + LinkIndex: v.index(), // 设置路由的网络接口索引 + Dst: dst, // 目标网段 + Src: v.ipNet.IP, // 源IP地址 + Scope: netlink.SCOPE_LINK, // 设置为链路本地范围 + } + return netlink.RouteAdd(&route) +} + +// index 获取网络接口的系统索引号 +// 如果接口不存在则返回-1 +func (v *VirtualNIC) index() int { + if v.iface == nil { + return -1 + } + name := v.Name() + ifx, _ := net.InterfaceByName(name) + return ifx.Index +} + +// Close 关闭虚拟网卡并释放相关资源 +// 在不再使用虚拟网卡时必须调用此方法以避免资源泄漏 +func (v *VirtualNIC) Close() error { + return v.iface.Close() +} + +// Name 返回虚拟网卡的接口名称 +// 该名称是系统分配的,在Linux上通常类似于"tun0",在macOS上类似于"utun1" +func (v *VirtualNIC) Name() string { + return v.name +} + +// Read 从虚拟网卡读取数据包 +// 参数p用于存储读取的数据,返回读取的字节数和可能的错误 +// 这个方法会阻塞直到有数据可读或发生错误 +func (v *VirtualNIC) Read(p []byte) (int, error) { + return v.iface.Read(p) +} + +// Write 向虚拟网卡写入数据包 +// 参数p包含要写入的数据,返回写入的字节数和可能的错误 +// 写入的数据应该是完整的IP数据包 +func (v *VirtualNIC) Write(p []byte) (int, error) { + return v.iface.Write(p) +}