本文通过一个简单的需求介绍了在一个 ARM 设备上开发一个程序实现远程打开服务器的过程,通过这个实例大致介绍了一个简单的嵌入式 Linux 开发的过程。本文并不会详细介绍网络唤醒的原理以及 Magic Packet。
上面的解决方案显然非常粗糙,但是这个项目本身确实也比较简单,没有必要做非常详尽的设计方案,所以我们下面仅列出一些可能的要点及解决方法
Magic Packet
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
内网穿透
其它要面对的麻烦
下面我们会依照这个开发步骤去实现我们的方案
需求分析和设计
本项目的开发的需求分析和设计已经在前面完成,鉴于该项目比较简单,就不做更详细设计了
硬件开发和验证
要找一个24小时运行的设备
这个设备的硬件显然不需要验证,迅雷已经帮我们验证了;
实际上,这个设备可以有多种选择,如果你的路由器是 OEM 的,上面通常运行的都是 openwrt,那么你可以直接使用它;或者你手头有闲置的机顶盒等,都有可能用得上;但是通常不建议使用平板,一是太耗电,二是把安卓刷成 Linux 有难度。
赚钱宝上原有的 Linux 应该是迅雷自己编译的,很难知道迅雷在这个系统上做了什么,所以还是不用为好,需要刷个新的系统,CPU 为 S805 的设备通常都是支持 USB 刷机的,所以其实根本不用把它拆开,直接一条 USB 线就可以刷新系统了;
刷个 openwrt 是比较现实的,有现成的教程,而且 openwrt 资料丰富,便于今后折腾;
刷机教程:赚钱宝一代刷OpenWrt固件;
刷机包及相应软件:百度网盘;提取码:ow2l
刷机包提供了 openwrt 18 和 19 两个,建议刷 openwrt 19(因为我刷的是 openwrt 19)
具体刷机过程自己去享受吧;
刷好的机器应该是可以使用 ssh 登录的
首先要从你的路由器上找到这台 openwrt 的 IP 地址,比如为:192.168.2.100,然后用 ssh 登录,新刷的 openwrt 没有密码
ssh [email protected]
我这里登录以后的样子,登录以后一定要改一下 root 的密码
openwrt 还会有一个 web 界面,直接在浏览器上输入 IP 即可进入
这台设备最好不要使用 DHCP,而是使用固定 IP,这样便于以后远程登录,有三种方法设置固定 IP
使用 openwrt 的 web 界面修改
从浏览器登录 openwrt 的 web 页面,其密码与 ssh 的密码一致,选择:Network --> Interfaces --> Edit
要在赚钱宝上写程序,需要我们在本地电脑完成编程,然后用交叉编译的工具编译成在赚钱宝上可以运行的程序,传到赚钱宝上才可以在赚钱宝上运行;
所以我们需要有一个工具链,对我们编写的程序进行交叉编译;
这个工具链不是放在 wakener 上的,因为 wakener 通常性能比较差,而且为了节省存储空间,上面通常只放一些运行时(runtime)库,不具备开发的能力;所以这个工具链要放在另外一台运行着 Linux 的机器上,也可以运行在虚拟机上,我们把这台电脑叫做 开发机,建议在开发机上运行 ubuntu;
下面是建立这个编译环境的过程
首先 ssh 登录你刚刷的 openwrt,查看你刷的 openwrt 的版本号:
去 openwrt官网 找到你所需要的版本号下的 sdk,要选择 at91/sama5 目录下的,这个是 cortex-A5 架构(CPU S805 的架构)的工具链
注意这个工具链只能运行在 Linux 下,我是运行在 Ubuntu 下,而且 openwrt-19.07.7 的 SDK 只有 64 位 X86 版本;
先将这个工具链下载到 *~/Downloads/ 目录下
wget https://downloads.openwrt.org/releases/19.07.7/targets/at91/sama5/openwrt-sdk-19.07.7-at91-sama5_gcc-7.5.0_musl_eabi.Linux-x86_64.tar.xz -C ~/Downloads/
再将其解压出来
cd ~/Downloads
tar -xvf openwrt-sdk-19.07.7-at91-sama5_gcc-7.5.0_musl_eabi.Linux-x86_64.tar.xz
在 ~/Downloads/ 目录下会建立一个新目录 openwrt-sdk-19.07.7-at91-sama5_gcc-7.5.0_musl_eabi.Linux-x86_64,进入到这个目录,可以看到一个 staging_dir 目录
cd openwrt-sdk-19.07.7-at91-sama5_gcc-7.5.0_musl_eabi.Linux-x86_64/
ls
这个 staging_dir 目录下的所有内容就是一个完整的工具链,我们可以把这个工具链单独拿出来使用;
我把这个目录拷贝到了 ~/tooschain/openwrt-a5/ 下,我们之所以放到 ~/toolschain/ 下,是因为我们还可能有别的设备的工具链,都放到这个目录下便于管理
mkdir -p ~/toolschain/openwrt-a5/
cp -fr ~/Downloads/openwrt-sdk-19.07.7-at91-sama5_gcc-7.5.0_musl_eabi.Linux-x86_64/staging_dir/* ~/toolschain/openwrt-a5/
现在我们已经有了一个完整的工具链,但这个工具链基本上是没办法用的,我们需要简单的配置一下,其实就是设置一些环境变量;
#!/bin/bash
#A5 arm-linux-musl-eabi工具链,用于一代赚钱宝openwrt
export PATH=/home/whowin/toolschain/openwrt-a5/toolchain-arm_cortex-a5+vfpv4_gcc-7.5.0_musl_eabi/bin:$PATH
export STAGING_DIR=/home/whowin/toolschain/openwrt-a5
cd ~
vi a5.sh
(编辑内容并存盘)
chmod 755 a5.sh
sudo cp a5.sh /bin/
source /bin/a5.sh
arm-openwrt-linux-gcc -v
有了工具链就可以编程了,编程的过程要在开发机上完成,不是在 openwrt 下,但是用这个源程序编译出来的可执行文件是要在 openwrt 下运行的;
下面是这个项目中需要在 openwrt 下运行的程序 wakeOnLan 的源程序
/******************************************************************************
File Name: wakeOnLan.c
Description: 向局域网中的计算机发出远程唤醒的指令
该程序将运行在迅雷一代赚钱宝上(已刷openwrt),用于唤醒主机
Compile: source /bin/a5.sh
arm-openwrt-linux-gcc -Wall wakeOnLan.c -o wakeOnLan
Usage: wakeOnLan [boradcast IP] [MAC]
Example: sudo ./wakeOnLan 192.168.2.255 00:e0:2b:68:00:03
Date: 2022-07-26
*******************************************************************************/
#include
#include
#include
#include
#include
#include
#include inet_ntop
#include //inet_addr
#define BIND_PORT 7 // 端口号
#define MSG_LEN 102 // magic packet的长度
#define USAGE "wakeOnLan [broadcast IP] [MAC]\n" \
"Example: sudo ./wakeOnLan 192.168.1.255 00:e0:2b:69:00:03\n"
/*****************************************************
* Function: int is_IP(char *IP)
* Description: 检查IP是否为一个合法的IPv4
* Return: 1 合法的IPv4
* 0 非法的IPv4
*****************************************************/
int is_IP(char *IP) {
int a, b, c, d;
char e;
if (4 == sscanf(IP, "%d.%d.%d.%d%c", &a, &b, &c, &d, &e)) {
if (a >= 0 && a < 256 &&
b >= 0 && b < 256 &&
c >= 0 && c < 256 &&
d >= 0 && d < 256) {
return 1;
}
}
return 0;
}
/*************************************************************************
* Function: int is_MAC(char *mac_str, char *mac)
* Description: 检查MAC是否为合法的MAC格式,如果合法,将其转换成6组数字放到mac中
*
* Return: 1 合法的MAC,char *mac中为转换后的mac地址
* 0 非法的MAC
*************************************************************************/
int is_MAC(char *mac_str, char *mac) {
int temp[6];
char e;
int i;
if (6 == sscanf(mac_str, "%x:%x:%x:%x:%x:%x%c", &temp[0], &temp[1], &temp[2], &temp[3], &temp[4], &temp[5], &e)) {
for (i = 0; i < 6; ++i) {
if (temp[i] < 0 || temp[i] > 255) break;
}
if (i == 6) {
for (i = 0; i < 6; ++i) {
mac[i] = temp[i];
}
return 1;
}
}
return 0;
}
// ===================主程序================================================
int main(int argc, char *argv[]) {
struct sockaddr_in sin;
char *broadcast_ip;
char wol_msg[MSG_LEN + 2]; // magic packet
char mac[6];
int socket_fd;
int i, j;
int on = 1;
// 检查参数数量
if (argc < 3) {
// 参数数量不对
printf("Incorrect input parameters.\n");
printf(USAGE);
return -1;
}
// 检查第一个参数是否为一个IP地址
if (! is_IP(argv[1])) {
printf("%s is an invalid IPv4.\n", argv[1]);
printf(USAGE);
return -1;
}
// 检查第二个参数是否为一个MAC地址
if (! is_MAC(argv[2], mac)) {
printf("%s is an invalid MAC address.\n", argv[2]);
printf(USAGE);
return -1;
}
printf("MAC is %02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1],mac[2],mac[3],mac[4],mac[5]);
// 建立socket
socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
if (socket_fd < 0) {
printf("Can not set up socket. Program exits(%d).\n", socket_fd);
return -1;
}
// 为IP地址分配内存
broadcast_ip = calloc(strlen(argv[1]) + 1, sizeof(char));
if (! broadcast_ip) {
printf("Can not allocate memory. Program exits\n");
return -1;
}
// 允许在 socket_fd 上发送广播消息
setsockopt(socket_fd, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
memset((void *)&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = inet_addr(broadcast_ip); // 广播IP
sin.sin_port = htons(BIND_PORT); // 端口号
// 6个 ff
for (i = 0; i < 6; i++) {
wol_msg[i] = 0xFF;
}
// 16遍 mac 地址
for (j = 0; j < 16; j++) {
for (i = 0; i < 6; i++) {
wol_msg[6 + j * 6 + i] = mac[i];
}
}
// 发送 magic packet
sendto(socket_fd, &wol_msg, MSG_LEN, 0, (struct sockaddr *)&sin, sizeof(sin));
close(socket_fd);
// 释放前面分配的内存空间
free(broadcast_ip);
printf("Magic Packet has been sended.\n");
return 0;
}
这个程序本身比较简单,注释比较完整,也没什么好说明的
cd ~/wake_on_lan
source /bin/a5.sh
arm-openwrt-linux-gcc -Wall wakeOnLan.c -o wakeOnLan
cd ~/wake_on_lan
scp wakeOnLan [email protected]:/root/
用 ssh 登录到 openwrt,登录后应该就在 /root/ 目录下,因为 /root/ 是 root 用户的 HOME 目录,然后运行程序
ssh [email protected]
./wakeOnLan 192.168.2.255 00:e0:2b:69:00:03
第一个参数是局域网上的广播 IP,第二个参数是要被远程唤醒的机器的 MAC 地址,请根据你的具体情况进行修改
在我的机器上运行效果是这样的
这个程序的调试主要是确保程序能够正确地发出 magic packet,需要在局域网上找另一台机器进行数据包的监听,这台监听的机器既可以运行 windows 也可以运行 Linux,最好是使用准备远程唤醒机器作为监听的机器,我们以一台运行 ubuntu 的机器为例来完成调试
使用 ubuntu 下的工具 tcpdump 来进行数据包的监听,tcpdump 必须在 root 权限下运行;
首先在监听机器上运行 tcpdump
sudo tcpdump -vv -x udp port 7
这行命令的意思就是监听 udp 端口 7 的数据包,-vv 的意思是显示详细的信息,-x 的意思是按照 16 进制显示,这两个参数也可以写成 -vvx
在 openwrt 上运行 wakeOnLan
./wakeOnLan 192.168.2.255 00:e0:2b:69:00:03
其中的广播 IP 和 MAC 地址请按照实际情况填写
正常情况下,在监听机器上可以看到程序发出的 Magic Packet,仔细看一下这个数据包的格式是否正确
在我的环境下,看到的输出如下:
其中黄线标识的部分是 IP 头,占 20 个字节;绿线标识的部分是 UDP 头,占 8 个字节,剩下的就是 Magic Packet;
如果你正常地侦听到了一个完整且正确的 Magic Packet,那么恭喜你,就快要成功了;
如果你使用 windows 侦听 Magic Packet 数据包,通常使用著名的 Wireshark
frp内网穿透项目
该项目有中文文档,大家可以按照文档下载适当的 release,其服务器软件 frps 运行在服务器(VPS)上,客户端 frpc 运行在 openwrt 上;
通常服务器端软件都是 64 位的 X86 架构,比较容易搞定;
要注意的是,要看清楚运行 openwrt 的设备是什么架构,是 32 位的还是 64 位的,比如本文中的设备 CPU 为 S805,就是一个 32 位的 arm 架构,否则你下载的客户端软件可能无法运行;
这个软件的设置还是要费一些功夫,请认真阅读该项目的文档,并参考其范例;这里我给出我的实例
服务器端配置文件:frps.ini,其中的 xxx.xxx.xxx.xxx 请按照实际情况设置,frp_log_path 请指向实际存放 frp 日志的目录
[common]
bind_addr = xxx.xxx.xxx.xxx
bind_port = 57000
bind_udp_port = 57001
kcp_bind_port = 57000
proxy_bind_addr = xxx.xxx.xxx.xxx
vhost_http_port = 58080
vhost_https_port = 58443
log_file = /frp_log_path/frps.log
log_level = info
log_max_days = 3
disable_log_color = false
detailed_errors_to_client = true
authentication_method = token
authenticate_heartbeats = false
authenticate_new_work_conns = false
token = skyline.admin
oidc_client_id =
oidc_client_secret =
oidc_audience =
oidc_token_endpoint_url =
allow_ports = 58000-59000,50000-53000
max_pool_count = 15
max_ports_per_client = 0
tls_only = false
subdomain_host = frps.com
tcp_mux = true
客户端配置文件:frpc.ini,其中的 xxx.xxx.xxx.xxx 请按照实际情况设置;openwrt.aaa.com 是一个 A 记录指向 VPS 的域名(子域名),也要根据实际情况进行设置
[common]
server_addr=xxx.xxx.xxx.xxx
server_port=57000
log_file=/tmp/frpc.log
log_level=info
log_max_days=3
disable_log_color=false
token=skyline.admin
pool_count=5
tcp_mux=true
user=whowin
login_fail_exit=false
protocol=tcp
tls_enable=falset
dns_server=8.8.8.8
admin_addr=127.0.0.1
admin_port=7400
admin_user=skyline
admin_pwd=admin
[ssh]
type=tcp
local_ip=192.168.2.100
local_port=22
use_encryption=false
use_compression=false
custom_domain=openwrt.aaa.com
remote_port=52998
health_check_type=tcp
health_check_timeout_s=3
health_check_max_failed=10
health_check_interval_s=30
[openwrt_web]
type=http
local_ip=192.168.2.100
local_port=80
use_encryption=false
use_compression=true
custom_domains=openwrt.aaa.com
header_X-From-Where=frp
health_check_type=http
health_check_url=/
health_check_interval_s=90
health_check_max_failed=3
health_check_timeout_s=3
/path_to/frps -c /path_to/frps.ini &
/path_to/frpc -c /path_to/frpc.ini &
ssh [email protected] -p 52998
ssh [email protected] -p 52998
测试远程开机使用的电脑、平板或者手机,不能连接到家里的 wifi 上,建议使用手机,关掉 wifi,打开 ConnectBot
设置连接如下:
通过 frp 连接到 openwrt
在 openwrt 上运行 wakeOnLan
正常情况下,MAC 地址指定的机器应该已经开机了;为了运行方便,你可以在 openwrt 上写一个 shell 脚本,免得每次都要输入 IP 地址和 MAC 地址,像下面这样,注意,openwrt 下的 shell 是 ash
$ cat wakeOnLan.sh
#!/bin/ash
/home/whowin/wakeOnLan 192.168.2.255 00:e0:2b:68:00:03
$
前面提过,Magic Packet 可以在任意端口发送,通常使用端口 7 或 9,我们这个例子使用端口 7 发送,读者可以试一下从其他端口发送,比如端口 1234;
实际上,openwrt 是有现成的网络唤醒模块的,可以在 openwrt 的 web 界面上搜索 luci-app-wol,安装这个软件的同时,还会安装一个 etherwake 的软件,安装好以后,可以使用类似 etherwake -b AA:BB:CC:DD:EE:FF 的命令发送 Magic Packet 唤醒指定的机器,也可以通过 web 界面唤醒指定的电脑
至此,我们的这个项目就算做完了,我们基本上经历了一个嵌入式开发的全过程,只是每一个阶段都比较简单而已,嵌入式开发,貌似复杂,其实并没有想象的那么难;
在嵌入式项目的设计阶段,我们需要有足够的知识储备以便为我们的需求提出最合理的方案,在这个项目的设计中,正是由于我们储备了 Magic Packet 和反向代理的知识,才可以为我们的需求提出这样一个软硬件结合的方案;
我们这个项目基本没有硬件开发,但是通常情况下,嵌入式开发的软件工程师是需要参与到硬件开发中去的,包括芯片方案的选择等都要参与意见,以便设计开发的硬件产品在今后的软件开发中可以比较顺利,所以嵌入式软件工程师同样要具备阅读芯片的 datasheet 的能力和看懂硬件原理图的能力,好的嵌入式软件工程师也可以拥有非常不错的焊接技能;
嵌入式开发的最关键的地方还是软件开发,但比普通的软件开发所要了解的知识要多很多,比如在我们这个项目中,我们必须要知道我们所用的硬件的 CPU 是什么,甚至要知道这个 CPU 的架构,否则,我们无法为这个硬件构建正确的操作系统,也无法在开发机上为这个硬件构建正确的交叉编译环境;
如果你喜欢做嵌入式开发,希望这篇文章能给予你帮助。