Skip to main content

Command Palette

Search for a command to run...

使用raw socket发送magic packet

Updated
2 min read
使用raw socket发送magic packet

Magic Packet是进行网络唤醒的数据包,将这个数据包以广播的形式发到局域网上,与数据包中所关联的MAC相同的电脑就会被唤醒开机,通常我们都是使用UDP报文的形式来发送这个数据包,但实际上在进行网络唤醒的时候,只要报文中包含Magic Packet应该就可以唤醒相关的计算机,与IP协议、UDP协议没有任何关系,本文将试图抛开网络层(IP层)和传输层(TCP/UDP层),直接在数据链路层发出Magic Packet,并成功实现网络唤醒,本文将提供完整的源代码。阅读本文需要有较好的网络编程基础,本文对网络编程的初学者有一定难度。

1. Magic Packet

  • 我比较喜欢把这个数据包称作"网络唤醒包";有很多地方把它翻译成"魔术包"或者"魔法数据包",我个人觉着太过表面,无法表达其实际的含义;本文将这个数据包称为"网络唤醒包"或者"Magic Packet",二者具有完全相同的含义;
  • 以前写过一篇与嵌入式相关的文章《远程开机:一个简单的嵌入式项目开发》,在嵌入式环境下使用Magic Packet进行远程开机的小项目,有兴趣的读者可以参考;
  • Magic Packet 就是一个指定格式的数据包,其格式为:6 个 0xff,然后16组需要被网络唤醒的电脑的 MAC 地址,比如需要被唤醒的电脑的 MAC 为:00:e0:2b:69:00:03,则 Magic Packet 为(16进制表述):
      ff ff ff ff ff ff 
      00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 
      00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 
      00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 
      00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03 00 e0 2b 69 00 03
    
  • 关于 Magic Packet 的更多的信息请参考
  • 对Magic Packet也没有什么好解释的,理论上只要一个报文中存在这个Magic Packet,那么有网络唤醒功能的NIC都会相应,What is NIC? NIC就是网卡,Network Interface Controller;
  • 但是大多数的 802.11 的无线网卡是收不到 Magic Packet 的,这一点在wikipedia-Wake-On-Lan上有明确的说明;所以不要尝试在无线网卡上做网络唤醒,但是有一个叫做WoWLAN的标准是专门支持无线网卡的网络唤醒的,以后有时间的时候试一下;
  • 通常发送Magic Packet是使用UDP广播的方式发,这方面的文章很多,有兴趣的读者可以百个度或者谷个歌去找一下,我的另一篇文章《远程开机:一个简单的嵌入式项目开发》也是用这种方式发的,这里就不赘述了;

2. 相关技术要点

以下要点将会在本文的范例程序中用到,在此需要简单回顾以下。

  • TCP/IP的五层网络模型(OSI 七层架构的简化版)

    1. 应用层
    2. 传输层(TCP/UDP)
    3. 网络层(IP)
    4. 数据链路层(Ethernet)
    5. 物理层
  • 基于TCP/IP的数据报文的结构

    • 一个基于TCP/IP的完整报文结构为:以太网报头 + IP报头 + TCP/UDP报头 + data,如下图:

    Packet structure based on TCP/IP


  • 以太网报头

    • 以太网报头定义在头文件中:
      struct ethhdr {
          unsigned char  h_dest[ETH_ALEN];    /* destination eth addr  */
          unsigned char  h_source[ETH_ALEN];  /* source ether addr  */
          __be16         h_proto;             /* packet type ID field  */
      } __attribute__((packed));
      
    • h_dest字段为目的MAC地址,h_source字段为源MAC地址;
    • h_proto表示当前数据包在网络层使用的协议,Linux支持的协议在头文件中定义;通常在网络层使用的IP协议,这个字段的值是0x0800(ETH_P_IP);
    • 但是本文中的 h_proto 字段要填写 0x842,很遗憾这个协议在头文件中没有定义,也基本找不到相关资料;
  • raw socket

  • struct sockaddr_ll

    • 这个结构在中定义,有关该结构的详细说明请参考其它文章,本文仅就相关字段做出说明;
    • 这个结构与 IPv4 socket 编程中的结构(struct sockaddr_in)的作用类似,是用在raw socket上的一个地址结构,烦请自行理解,其中'll'表示Low Level
      struct sockaddr_ll {
          unsigned short  sll_family;
          __be16          sll_protocol;
          int             sll_ifindex;
          unsigned short  sll_hatype;     // Hardware Address Type
          unsigned char   sll_pkttype;    // Packet Type
          unsigned char   sll_halen;      // Hardware Address Length
          unsigned char   sll_addr[8];    // Address(Hardware Address)
      };
      
    • sll_family为协议族,和建立raw socket是使用的协议族要一致,所以肯定是AF_PACKET;
    • sll_protocol是标准的以太网协议类型,定义在头文件中,通常情况下应该 ETH_P_IP(0x800) 表示IP协议,本文要填 0x842
    • sll_ifindex是网络接口的索引号,我们可以根据接口名称使用ioctl获得;
    • sll_halen是硬件地址(MAC)的长度,ha是Hardware Address的意思,填常数 ETH_ALEN(定义在头文件中);
    • sll_addr是目的MAC地址
    • 实际上,在发送数据时,由于sll_family和sll_protocol都是和socket中一样的,所以大多数情况下都可以不填,只要填sll_ifindex、sll_halen和sll_addr即可,但是本文的例子中,如果使用bind()绑定地址,应该尽量完整地填写,否则执行bind()是会出错。

3. 在数据链路层发送Magic Packet

  • 先说一下目标,通常使用UDP发送Magic Packet的报文结构为:以太网报头 + IP报头 + UDP报头 + Magic Packet,我们的目标是:以太网报头 + Magic Packet
  • 先看源程序,文件名为:magic-packet.c(点击文件名下载源程序)

  • 报文是以广播的形式发出去的,发送广播时,以太网报头的目的MAC要全部填写0xff,(struct sockaddr_ll)中的sll_addr也要全部填写0xff;

  • 尽管我们知道目的MAC,但是这个报文必须以广播报文发出,因为此时被唤醒的机器并没有开机,所以无法通过arp获知填写的MAC地址是否在局域网内,所以如果在以太网报头的h_dest中填上了要被唤醒的机器的MAC地址,报文是无法送达的;
  • 关于使用ioctl获取接口索引号(Interface Index)和接口MAC的问题,请自行查找资料,或者参考文章《如何使用raw socket发送UDP报文》,这篇文章中有部分内容涉及到这个问题;
  • 程序中写好了用send()或者用sendto()发送报文的代码,使用一个常量USING_SENDTO来控制,当USING_SENDTO为1时,将使用sendto()发送报文,否则使用send()发送报文;
  • 如果使用send()发送报文,需要使用bind()绑定目的地址;
  • 具体实践中,如果使用sendto()发送报文,(struct sockaddr_ll)中只需填写sll_ifindex即可,这个也许和运行环境有关,请自行测试;理论上说,只要编译通过,运行没有出错,就表示这个Magic Packet发送了出去,应该就可以起作用;
  • 源程序中有详细的注释,其它也没有什么更多解释的。
  • 编译gcc -Wall magic-packet.c -o magic-packet
  • 运行sudo ./magic-packet enp0s3 00:e0:2b:68:00:03
  • 由于使用了raw socket,所以必须以root权限运行;
  • 如果你填写的ifname和mac都正确的话,mac所对应的电脑应该被远程唤醒;
  • 这个程序并不好调试,因为能够在数据链路层侦听的工具不多,加上使用的协议号为0x842,使得大多数工具都无法使用,wireshark应该是可以的,而且wireshark的wiki上说,它有专门针对0x842号协议的侦听,不过我没有试过,有感兴趣的读者可以试一下;
  • 远程唤醒是需要硬件支持的,主板和网卡都要支持,但是目前绝大多数有线网卡都应该是支持的,但是可能要在BIOS和其它地方做一些设置,请自行搜索相关资料,并参考我的另一篇文章《远程开机:一个简单的嵌入式项目开发》
  • 运行结果截图:

    Screenshot magic_packet

欢迎订阅 『网络编程专栏』


欢迎访问我的博客:https://whowin.cn

email: hengch@163.com

donation

More from this blog

双向链表及如何使用GLib的GList实现双向链表

双向链表是一种比单向链表更为灵活的数据结构,与单向链表相比可以有更多的应用场景,本文讨论双向链表的基本概念及实现方法,并着重介绍使用GLib的GList实现单向链表的方法及步骤,本文给出了多个实际范例源代码,旨在帮助学习基于GLib编程的读者较快地掌握GList的使用方法,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;本文适合初学者阅读。 1 双向链表及其实现 在文章《单向链表以及如何使用GLib中的GSList实现单向链表》中,介绍了单向链表以及基于 G...

Oct 29, 20245 min read24
双向链表及如何使用GLib的GList实现双向链表

C程序员应该知道的最好的8个c编程框架

C 编程框架是开发人员必不可少的工具,编程框架可以为构建强大且性能优异的应用程序提供结构化的基础,本文将对 8 个最佳 C 编程框架和库做出简要的介绍,如果您正在寻找适合初学者的 C 编程框架或旨在进行 C 编程框架比较,相信本文可以给您一定的帮助。 顶级 C 编程框架 – 概述 本文将介绍以下 8 个 C 语言编程框架: 序号框架名称主要特点易于集成下载链接 1GTK全面的小部件集,跨平台支持中等的下载 2Qt跨平台支持,集成开发环境中等的下载 3CMocka轻量级,模...

Oct 19, 20244 min read36
C程序员应该知道的最好的8个c编程框架

单向链表以及如何使用GLib中的GSList实现单向链表

单向链表是一种基础的数据结构,也是一种简单而灵活的数据结构,本文讨论单向链表的基本概念及实现方法,并着重介绍使用GLib的GSList实现单向链表的方法及步骤,本文给出了多个实际范例源代码,旨在帮助学习基于GLib编程的读者较快地掌握GSList的使用方法,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;本文适合初学者阅读。 1 单向链表及其实现 在文章《使用GLib进行C语言编程的实例》中,简单介绍了 GLib,建议阅读本文前先阅读这篇文章; 单向链表是一...

Aug 19, 20246 min read23
单向链表以及如何使用GLib中的GSList实现单向链表

使用GLib进行C语言编程的实例

本文将讨论使用GLib进行编程的基本步骤,GLib是一个跨平台的,用C语言编写的3个底层库(以前是5个)的集合,GLib提供了多种高级的数据结构,如内存块、双向和单向链表、哈希表等,GLib还实现了线程相关的函数、多线程编程以及相关的工具,例如原始变量访问、互斥锁、异步队列等,GLib主要由GNOME开发;本文是使用GLib编程的入门文章,旨在通过实例帮助希望学习GLib编程的读者较快地入门,本文将给出多个使用GLib库编程范例的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gc...

Aug 9, 20245 min read9
使用GLib进行C语言编程的实例

Linux下使用libiw进行无线信号扫描的实例

打开电脑连接wifi是一件很平常的事情,但这些事情通常都是操作系统下的wifi管理程序替我们完成的,如何在程序中扫描wifi信号其实资料并不多,前面已经有两篇文章介绍了如何使用ioctl()扫描wifi信号,但其实在Linux下有一个简单的库对这些ioctl()的操作进行了封装,这个库就是libiw,使用libiw可以简化编程,本文介绍了如果使用libiw对wifi信号进行扫描的基本方法,本文将给出完整的源代码,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;尽...

Jul 4, 20244 min read21
Linux下使用libiw进行无线信号扫描的实例

whowin - 开源和分享是技术发展的源泉和动力

42 posts

一个从业30多年的退休程序员,主要从事嵌入式软件开发。