作为Linux桌面用户,近几年在使用Arch Linux + KDE作为日常操作系统时,我一直都对KDE挺满意的,虽然也遇到了大大小小不少Bug(但是比起Windows 11来说,感觉还是能好一些的);其中一个最主要的原因就是KDE通过一些调整,能够兼容大部分我从Windows上带来的习惯,换句现在流行的形容方式就是可切换性/兼容性更好。
文件管理器方面,Dolphin稍微调整过后还是比较顺手的;KDE自带的Dolphin可以设定两个视图记忆模式,一个是默认的“统一所有文件夹的显示风格”,另一个是我个人比较习惯的“单独记忆每个文件夹的显示风格”。
但是一直以来有一个比较难受的地方就是:虽然Dolphin会记忆每个文件夹的“显示风格”,主要就是排序和显示列表之类的,但是每次进入一个新的文件夹,或者未设定过的文件夹时,就会恢复默认的设定。这个默认的设定机缘巧合的情况下,被设定为了“按照修改时间倒叙排列”,导致每次进入文件夹时我都得手动调整一下,实在令人不爽;今天恰好有空就来解决一下这个问题。
TLDR:方案三最佳,方案一也可以,太闲了或者对你恰好有强迫症,或者实现有兴趣可以看看方案二。
其实也没有太反直觉,遇到这个问题第一想法就是在Dolphin的设定调整回“统一所有文件夹的显示风格”,然后再切换一下文件夹的排序,再设定回“单独记忆每个文件夹的显示风格”就会发现之前的修改已经作为默认设定。
大功告成,事情就这么解决了;但是可能是因为闲的吧,也可能是因为强迫症发作,感觉这种方式不够优雅,想看看能不能有更方便的方式实现,于是就有了后文。
一顿搜索和第六感之后,确认了Dolphin的配置文件果然是~/.config/dolphinrc,但是其中没有我们需要的“默认视图配置”,应该是存在了其他的地方。
这样只能在文件夹中搜索看看有没有线索了;在使用“单独记忆每个文件夹的显示风格”时,Dolphin会在每个文件夹中生成一个.directory文件,如果自定义了排序列,其中就会使用SortRole项来记录。而我目前默认的设定就是根据“最后修改时间”来排序,所以理论上默认设定也会以这个格式存储。
又是一顿搜索之后,在~/.config/kdeglobals中找到了Sort by=Date,正以为大功告成时,发现这个设定项位于[KFileDialog Settings]子项之下。一顿修改测试之后,发现找错了地方,不是这个文件。
接下来就只能另想方法确认文件夹位置了。
既然确认了Dolphin会记录默认的视图配置,我们假定它会将配置写入到磁盘中。这时理论上我们就可以通过切换视图设置的选项,再修改排序依据来触发配置文件保存,这样只要监听文件的变化,就可以“轻易”(Flag)确认它存储配置的地方了。
话不多说,inotifywait -mrq ~/.config启动,一顿操作下来除了看到Dolphin确实会在修改视图的目录下创建.directory文件以外,好像就没有其他动作了。
不过还有一些看起来很可疑的/home/user/.config/ OPEN #22278943输出,但是用“inotifywait 井号”之类的关键词都没有搜到。于是为了节省看代码的时间,后续再研究,就直接问了AI;给出答复是inode number(后续证明这个决定实在是太正确了,看了几个小时代码看的头晕脑胀,都没看出所以然,差点放弃了,最后又是在洗澡时灵光乍现,才最终找到原因的;对我这个曲折的折腾过程感兴趣的话可以看下一篇Blog)。
说起来也非常合理,毕竟软件的名字就叫做inotifywait,那么输出的奇怪数字是inode number的可能是确实很高。通过find -inum可以搜索对应inode number对应的文件;一搜果然没错,确实是inode number,但是对应的文件却是目录下的.directory,这下事情又回到原点。
后续又尝试通过inotifywait对整个home目录监测,但是没有反应,可能是出了什么bug(后期注:单纯是因为你太急了,又用了-q选项,导致每次使用递归选项-r时都会输出请耐心等待的提示给mute掉了),或者文件太多;结果还是走进了死胡同。
后期PS2:事实上如果那时候多等一会,后续就不用折腾这么久了)没准还可以省了后续对着#开头文件输出浪费的大把时间(虽然大概率还是会瞎折腾去研究这个)。如果真的遇到了超出最大inotifywait的提示,可以通过修改内核选项fs.inotify.max_user_instances和fs.inotify.max_user_watches来增加上限。
Dolphin是开源项目,在使用了搜索大法之后,我们就可以很轻易(真)的确定视图设置页面位于dolphin/src/settings/viewmodes/generalviewsettingspage.cpp。(我爱开源项目)
# https://github.com/KDE/dolphin/blob/41cde29f2e93987534c1654456a94ded1e4ae591/src/settings/viewmodes/generalviewsettingspage.cpp
void GeneralViewSettingsPage::applySettings()
{
GeneralSettings *settings = GeneralSettings::self();
ViewProperties props(m_url); // read current view properties
const bool useGlobalViewProps = m_globalViewProps->isChecked();
settings->setGlobalViewProps(useGlobalViewProps);
#if HAVE_BALOO
settings->setShowToolTips(m_showToolTips->isChecked());
#endif
settings->setShowSelectionToggle(m_showSelectionToggle->isChecked());
settings->setRenameInline(m_renameInline->isChecked());
settings->setHideXTrashFile(m_hideXtrashFiles->isChecked());
settings->setAutoExpandFolders(m_autoExpandFolders->isChecked());
settings->setBrowseThroughArchives(m_openArchivesAsFolder->isChecked());
settings->save();
if (useGlobalViewProps) {
// Remember the global view properties by applying the current view properties.
// It is important that GeneralSettings::globalViewProps() is set before
// the class ViewProperties is used, as ViewProperties uses this setting
// to find the destination folder for storing the view properties.
ViewProperties globalProps(m_url);
globalProps.setDirProperties(props);
}
}
当我们选择使用全局视图设定时,就会进入useGlobalViewProps为true的选项,我们只要找到GeneralSettings的save逻辑就可以知道位置了。但是好巧不巧,Dolphin的GeneralSettings是自动生成的代码。不过我们还有ViewProperties这条线索;果然,在好运的加持下,我们转到src/views/viewproperties.cpp,一切就水落石出了。
# https://github.com/KDE/dolphin/blob/41cde29f2e93987534c1654456a94ded1e4ae591/src/views/viewproperties.cpp
ViewProperties::ViewProperties(const QUrl &url)
: m_changedProps(false)
, m_autoSave(true)
, m_node(nullptr)
{
GeneralSettings *settings = GeneralSettings::self();
const bool useGlobalViewProps = settings->globalViewProps() || url.isEmpty();
bool useSearchView = false;
bool useTrashView = false;
bool useRecentDocumentsView = false;
bool useDownloadsView = false;
// We try and save it to the file .directory in the directory being viewed.
// If the directory is not writable by the user or the directory is not local,
// we store the properties information in a local file.
...
else if (useGlobalViewProps) {
m_filePath = destinationDir(QStringLiteral("global"));
}
...
// If the .directory file does not exist or the timestamp is too old,
// use default values instead.
const bool useDefaultProps = (!useGlobalViewProps || useSearchView || useTrashView || useRecentDocumentsView || useDownloadsView)
&& (!QFile::exists(file) || (m_node->timestamp() < settings->viewPropsTimestamp()));
...
else {
// The global view-properties act as default for directories without
// any view-property configuration. Constructing a ViewProperties
// instance for an empty QUrl ensures that the global view-properties
// are loaded.
QUrl emptyUrl;
ViewProperties defaultProps(emptyUrl);
setDirProperties(defaultProps);
m_changedProps = false;
}
...
void ViewProperties::save()
{
qCDebug(DolphinDebug) << "Saving view-properties to" << m_filePath;
QDir dir;
dir.mkpath(m_filePath);
m_node->setVersion(CurrentViewPropertiesVersion);
m_node->save();
m_changedProps = false;
}
...
QString ViewProperties::destinationDir(const QString &subDir) const
{
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
path.append("/view_properties/").append(subDir);
return path;
}
通过分析上述代码,我们能够看到代码在useGlobalViewProps=true时,也就是使用全局视图设定时,会将配置文件存储在QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)/view_properties/global/目录下。通过Qt的官方文档可以确认QStandardPaths::writableLocation对应的是/home/user/.local/share/APP_NAME/。
所以全局视图设定配置文件的完整路径就是/home/user/.local/share/dolphin/view_properties/global,只要修改这个文件夹下的.directory就可以修改默认的视图设定。
(怎么会存在这个地方...早该想到的...感觉除了默认.config以外,.local这个目录也是格外受到各大应用的欢迎;挖个坑在这:过段时间空了看看能不能把目前常见的目录使用规范整理/翻译一下。)
代码中当保存视图配置文件时,会输出保存路径的调试信息,根据官方文档说明[1][2]使用QT_LOGGING_RULES='org.kde.dolphin=true'启动dolphin后就会有调试日志输出。
再用inotifywait监听/home/user/.local/share/作为双保险,启动dolphin修改视图排序依据后可以看到两边均有输出。
org.kde.dolphin: Saving view-properties to "/home/user/.local/share/dolphin/view_properties/global"
因此确认了我们的分析是正确的。
说起来也神奇,在搜索代码时还查到了一个叫做src/settings/viewpropertiesdialog.cpp的从未见过的窗口,其中就有一个“设定当前视图为默认视图”的功能[3],他们有做这个功能,实在是帮了大忙了。
这里有一个比较tricky的地方,因为官方文档还是旧版本的(开源项目嘛,文档一直是....非常滞后的),根据官方的操作可能找不到入口。
KDE Plasma的默认情况下是隐藏了菜单栏的,此时的入口就会比较隐蔽:汉堡菜单-更多-视图-调整显示风格。
这样显然有点不方便,这时可以通过快捷键Ctrl+M来临时显示菜单栏;这时候入口就会比较容易找到了:直接就在视图-调整显示风格
虽然Dolphin这样设定好处也是挺好的,但是作为一个Windows起手多年的用户,如果后续能够给一个选项,做到跟Windows一样根据上层文件夹的视图设定和默认设定来决定内部文件夹的默认视图就更好了。
小小的吐槽一下:后面还有寻找#开头文件的“奇异之旅”,没想到就这一件小事,引出来这么一大堆东西;这两天的时间都花在这上面了,有点头大。。。
[1] Guidelines and HOWTOs/Debugging/Using Error Messages - KDE Community Wiki
[2] KDE/Debugging - Fedora Project Wiki
[3] Folder View Display Style
最近排查OpenWrt的一些问题时,在宿主机的内核日志中发现了一些pci相关的错误;需要具体检查下是哪个设备抛出的错误,遂有了这篇流水账记录。
PS:不知道是不是因为年龄的增长还是什么原因,感觉现在越来越开始依赖外部工具了(主要是感觉记忆力没有以前好了;也可能是强迫症/ADHD大发作);为了避免“青年痴呆”找上门,还是得多写写,多记录记录 (:з」∠),留一点赛博记忆在这;没准以后真有用得着的时候(还是希望不要用得上吧.jpg)。
lspci: 列出当前的pci设备信息,显示设备名
lspci -n: 列出当前的pci设备信息,以 vendor_id:product_id 形式显示设备
lspci -nn: 列出当前的pci设备信息,同时显示设备名和ID
lspci -t: 以层级/结构化方式显示pci设备信息
lspci -v[v]: 详细显示pci设备信息
lspci -s: 通过设备地址过滤显示的设备
lspci -d: 通过设备id/class来过滤显示的设备
如果要查询某个设备的上级设备,可以通过lspci -t结构化显示设备层级,在通过lspci -s过滤显示。
如果要查询某个设备的下级设备(如PCI Host下挂设备),除了通过lspci -t的方式以外,还以为通过lspci -v -s addr来显示次级PCI Bus;再通过lspci -s来过滤显示该次级PCI Bus的设备。
通过 dmidecode -q 或者 dmidecode -t slot 可以获得系统内的PCI插槽信息及Bus Address
Linux中万物皆文件,我们也可以通过sysfs中手动获取信息(也包含层级信息),具体可以查看manual说明,这里放一个debian的链接[2]。
简单来说可以查看下面的文件/文件夹
/proc/bus/pci/devices
/sys/bus/pci
/sys/bus/pci/slots/**slot_num**/address # 有可能文件夹是空的
/sys/devices/pci0000:00
如下示例:
位于00:01的PCI bridge提供了01:00子BUS,该子BUS挂载了01:00.0-3共记1个设备4个function。
而位于00:1b的PCI bridge则提供了02:00子BUS,挂载了设备02:00.0。
同理位于00:1c的PCI bridge提供了03:00子BUS,挂载了设备03:00.0。
root@m720q:~# lspci -t
-[0000:00]-+-00.0
+-01.0-[01]--+-00.0
| +-00.1
| +-00.2
| \-00.3
+-02.0
+-08.0
+-14.0
+-14.2
+-16.0
+-17.0
+-1b.0-[02]----00.0
+-1c.0-[03]----00.0
+-1f.0
+-1f.3
+-1f.4
+-1f.5
\-1f.6
root@m720q:~# lspci -nn
00:00.0 Host bridge [0600]: Intel Corporation 8th Gen Core Processor Host Bridge/DRAM Registers [8086:3ec2] (rev 07)
00:01.0 PCI bridge [0604]: Intel Corporation 6th-10th Gen Core Processor PCIe Controller (x16) [8086:1901] (rev 07)
00:02.0 VGA compatible controller [0300]: Intel Corporation CoffeeLake-S GT2 [UHD Graphics 630] [8086:3e92]
00:08.0 System peripheral [0880]: Intel Corporation Xeon E3-1200 v5/v6 / E3-1500 v5 / 6th/7th/8th Gen Core Processor Gaussian Mixture Model [8086:1911]
00:14.0 USB controller [0c03]: Intel Corporation Cannon Lake PCH USB 3.1 xHCI Host Controller [8086:a36d] (rev 10)
00:14.2 RAM memory [0500]: Intel Corporation Cannon Lake PCH Shared SRAM [8086:a36f] (rev 10)
00:16.0 Communication controller [0780]: Intel Corporation Cannon Lake PCH HECI Controller [8086:a360] (rev 10)
00:17.0 SATA controller [0106]: Intel Corporation Cannon Lake PCH SATA AHCI Controller [8086:a352] (rev 10)
00:1b.0 PCI bridge [0604]: Intel Corporation Cannon Lake PCH PCI Express Root Port #21 [8086:a32c] (rev f0)
00:1c.0 PCI bridge [0604]: Intel Corporation Cannon Lake PCH PCI Express Root Port #6 [8086:a33d] (rev f0)
00:1f.0 ISA bridge [0601]: Intel Corporation Device [8086:a308] (rev 10)
00:1f.3 Audio device [0403]: Intel Corporation Cannon Lake PCH cAVS [8086:a348] (rev 10)
00:1f.4 SMBus [0c05]: Intel Corporation Cannon Lake PCH SMBus Controller [8086:a323] (rev 10)
00:1f.5 Serial bus controller [0c80]: Intel Corporation Cannon Lake PCH SPI Controller [8086:a324] (rev 10)
00:1f.6 Ethernet controller [0200]: Intel Corporation Ethernet Connection (7) I219-V [8086:15bc] (rev 10)
01:00.0 Ethernet controller [0200]: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ [8086:1572] (rev 02)
01:00.1 Ethernet controller [0200]: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ [8086:1572] (rev 02)
01:00.2 Ethernet controller [0200]: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ [8086:1572] (rev 02)
01:00.3 Ethernet controller [0200]: Intel Corporation Ethernet Controller X710 for 10GbE SFP+ [8086:1572] (rev 02)
02:00.0 Non-Volatile memory controller [0108]: Toshiba Corporation BG3 NVMe SSD Controller [1179:0113] (rev 01)
03:00.0 Network controller [0280]: Intel Corporation Dual Band Wireless-AC 3165 Plus Bluetooth [8086:3166] (rev 99)
通过lspci -v同样可以看出来
root@m720q:~# lspci -v -s 00:01
00:01.0 PCI bridge: Intel Corporation 6th-10th Gen Core Processor PCIe Controller (x16) (rev 07) (prog-if 00 [Normal decode])
Subsystem: Lenovo 6th-10th Gen Core Processor PCIe Controller (x16)
Flags: bus master, fast devsel, latency 0, IRQ 122, IOMMU group 2
**Bus: primary=00, secondary=01, subordinate=01, sec-latency=0**
I/O behind bridge: [disabled] [16-bit]
Memory behind bridge: b4100000-b42fffff [size=2M] [32-bit]
Prefetchable memory behind bridge: b0000000-b2bfffff [size=44M] [32-bit]
Capabilities: [88] Subsystem: Lenovo 6th-10th Gen Core Processor PCIe Controller (x16)
Capabilities: [80] Power Management version 3
Capabilities: [90] MSI: Enable+ Count=1/1 Maskable- 64bit-
Capabilities: [a0] Express Root Port (Slot+), MSI 00
Capabilities: [100] Virtual Channel
Capabilities: [140] Root Complex Link
Capabilities: [d94] Secondary PCI Express
Kernel driver in use: pcieport
root@m720q:~# lspci -v -s 00:1b
00:1b.0 PCI bridge: Intel Corporation Cannon Lake PCH PCI Express Root Port #21 (rev f0) (prog-if 00 [Normal decode])
Subsystem: Lenovo Cannon Lake PCH PCI Express Root Port
Flags: bus master, fast devsel, latency 0, IRQ 123, IOMMU group 7
**Bus: primary=00, secondary=02, subordinate=02, sec-latency=0**
I/O behind bridge: [disabled] [16-bit]
Memory behind bridge: b4400000-b44fffff [size=1M] [32-bit]
Prefetchable memory behind bridge: [disabled] [64-bit]
Capabilities: [40] Express Root Port (Slot+), MSI 00
Capabilities: [80] MSI: Enable+ Count=1/1 Maskable- 64bit-
Capabilities: [90] Subsystem: Lenovo Cannon Lake PCH PCI Express Root Port
Capabilities: [a0] Power Management version 3
Capabilities: [100] Advanced Error Reporting
Capabilities: [140] Access Control Services
Capabilities: [150] Precision Time Measurement
Capabilities: [200] L1 PM Substates
Capabilities: [220] Secondary PCI Express
Capabilities: [250] Downstream Port Containment
Kernel driver in use: pcieport
root@m720q:~# lspci -v -s 00:1c
00:1c.0 PCI bridge: Intel Corporation Cannon Lake PCH PCI Express Root Port #6 (rev f0) (prog-if 00 [Normal decode])
Subsystem: Lenovo Cannon Lake PCH PCI Express Root Port
Flags: bus master, fast devsel, latency 0, IRQ 124, IOMMU group 8
**Bus: primary=00, secondary=03, subordinate=03, sec-latency=0**
I/O behind bridge: [disabled] [16-bit]
Memory behind bridge: b4300000-b43fffff [size=1M] [32-bit]
Prefetchable memory behind bridge: [disabled] [64-bit]
Capabilities: [40] Express Root Port (Slot+), MSI 00
Capabilities: [80] MSI: Enable+ Count=1/1 Maskable- 64bit-
Capabilities: [90] Subsystem: Lenovo Cannon Lake PCH PCI Express Root Port
Capabilities: [a0] Power Management version 3
Capabilities: [100] Advanced Error Reporting
Capabilities: [140] Access Control Services
Capabilities: [150] Precision Time Measurement
Capabilities: [220] Secondary PCI Express
Capabilities: [250] Downstream Port Containment
Kernel driver in use: pcieport
一个Host还有可能有多个次级Bus地址,如下面示例所示00:1d设备就提供了04-6e:00的子Bus地址
$ lspci -t
-[0000:00]-+-00.0
+-02.0
+-08.0
+-14.0
+-14.2
+-16.0
+-17.0
+-1b.0-[01]----00.0
+-1b.4-[02]----00.0
+-1c.0-[03]----00.0
+-1d.0-[04-6e]--
+-1f.0
+-1f.3
+-1f.4
+-1f.5
\-1f.6
通过dmidecode也可以看出来,但是在我其中两台设备中,其显示的slot不完整,挂载nvme硬盘的总线都没显示出来。查询了下资料说可能是主板没有正确记录信息,只能联系供应商解决?具体情况不太清楚,可能还是lspci靠谱点。
root@m720q:~# dmidecode -t slot
# dmidecode 3.4
Getting SMBIOS data from sysfs.
SMBIOS 3.2.1 present.
Handle 0x001C, DMI type 9, 17 bytes
System Slot Information
Designation: J6B2
Type: x16 PCI Express
Current Usage: In Use
Length: Long
ID: 0
Characteristics:
3.3 V is provided
Opening is shared
PME signal is supported
Bus Address: 0000:00:01.0
Handle 0x001D, DMI type 9, 17 bytes
System Slot Information
Designation: J6B1
Type: x1 PCI Express
Current Usage: In Use
Length: Short
ID: 1
Characteristics:
3.3 V is provided
Opening is shared
PME signal is supported
Bus Address: 0000:00:1c.3
Handle 0x001E, DMI type 9, 17 bytes
System Slot Information
Designation: J6D1
Type: x1 PCI Express
Current Usage: In Use
Length: Short
ID: 2
Characteristics:
3.3 V is provided
Opening is shared
PME signal is supported
Bus Address: 0000:00:1c.4
Handle 0x001F, DMI type 9, 17 bytes
System Slot Information
Designation: J7B1
Type: x1 PCI Express
Current Usage: In Use
Length: Short
ID: 3
Characteristics:
3.3 V is provided
Opening is shared
PME signal is supported
Bus Address: 0000:00:1c.5
Handle 0x0020, DMI type 9, 17 bytes
System Slot Information
Designation: J8B4
Type: x1 PCI Express
Current Usage: In Use
Length: Short
ID: 4
Characteristics:
3.3 V is provided
Opening is shared
PME signal is supported
Bus Address: 0000:00:1c.6
[1] Understand the Primary/Secondary/Subordinate Bus number in PCI/PCIe Bridge - Electrical Engineering Stack Exchange
[2] HowToIdentifyADevice/PCI - Debian Wiki
[3] c - How does lspci find out physical slot number of a PCI(E) device? - Stack Overflow
最近遇到一个奇怪的问题,偶尔/tmp目录满了之后,一段时间内就一定会导致/tmp/resolv.conf.d/resolv.conf.auto文件被清空,dnsmasq无法找到上级转发DNS导致断网。 但是查看文件修改时间,如果可用空间一直充足,这个文件的修改时间也不会发生变化;一旦没有可用空间,这个文件就会被替换为空文件。
首先确认下该文件的同于是dnsmasq用于确认转发DNS请求的上级服务器,在luci中可以设定。
既然是写入luci,目标那基本就是netifd这个库;这个库主要职责就是OpenWrt中绝大多数和网络相关配置。
通过搜索我们可以确认__interface_write_dns_entries这个函数负责实际的写入resolve;通过下面其唯一调用者interface_write_resolv_conf的代码,我们就可以确认这件事的元凶。
interface_write_resolv_conf(const char *jail)
{
size_t plen = (jail ? strlen(jail) + 1 : 0 ) +
(strlen(resolv_conf) >= strlen(DEFAULT_RESOLV_CONF) ?
strlen(resolv_conf) : strlen(DEFAULT_RESOLV_CONF) ) + 1;
char *path = alloca(plen);
char *dpath = alloca(plen);
char *tmppath = alloca(plen + 4);
FILE *f;
uint32_t crcold, crcnew;
if (jail) {
sprintf(path, "/tmp/resolv.conf-%s.d/resolv.conf.auto", jail);
strcpy(dpath, path);
dpath = dirname(dpath);
mkdir(dpath, 0755);
} else {
strcpy(path, resolv_conf);
}
sprintf(tmppath, "%s.tmp", path);
unlink(tmppath);
f = fopen(tmppath, "w+");
if (!f) {
D(INTERFACE, "Failed to open %s for writing\n", path);
return;
}
__interface_write_dns_entries(f, jail);
fflush(f);
rewind(f);
crcnew = crc32_file(f);
fclose(f);
crcold = crcnew + 1;
f = fopen(path, "r");
if (f) {
crcold = crc32_file(f);
fclose(f);
}
if (crcold == crcnew) {
unlink(tmppath);
} else if (rename(tmppath, path) < 0) {
D(INTERFACE, "Failed to replace %s\n", path);
unlink(tmppath);
}
}
代码主要行为就是在被调用时,在tmppath创建一个临时文件;通过对比临时文件和原文件的crc32,确认是否要替换为新文件。
这就解释了为什么当文件内容一致时,resolve文件的修改日期不会变化;而一旦可用空间不足时,新的临时文件创建后无法写入内容,最后生成了一个空间;这样crc32对比一定会不一致,从而导致老文件被替换为新的空文件。
到这里事情基本都理清楚了,但是还剩下一个问题:
什么情况下系统DNS会更新,为什么每隔10分钟左右系统就会更新resolve文件;一旦有空间,几分钟过后resolve文件就会恢复成正常状态。
反向查找后,发现interface_write_resolv_conf有如下几个调用者:
interface_proto_event_cb
interface_change_config
interface_ip_update_complete
其中interface_proto_event_cb可以排除,因为代码中只有在IFPEV_UP和IFPEV_DOWN以及IFPEV_LINK_LOST的情况下会触法刷新resolve文件的操作,而实际上并未有发生这类事情;根据设计文档中的说明也可以应证这一说法。
state:
IFS_SETUP:
The interface is currently being configured by the protocol handler
IFS_UP:
The interface is fully configured
IFS_TEARDOWN:
The interface is being deconfigured
IFS_DOWN:
The interface is down
而剩下的interface_change_config和interface_ip_update_complete看起来都比较可疑。
其中interface_change_config的唯一调用者是interface_update;而interface_update是作为回调函数在interface_init_list中使用的。
static void
interface_update(struct vlist_tree *tree, struct vlist_node *node_new,
struct vlist_node *node_old)
{
struct interface *if_old = container_of(node_old, struct interface, node);
struct interface *if_new = container_of(node_new, struct interface, node);
if (node_old && node_new) {
D(INTERFACE, "Update interface '%s'\n", if_new->name);
interface_change_config(if_old, if_new);
} else if (node_old) {
D(INTERFACE, "Remove interface '%s'\n", if_old->name);
set_config_state(if_old, IFC_REMOVE);
} else if (node_new) {
D(INTERFACE, "Create interface '%s'\n", if_new->name);
interface_event(if_new, IFEV_CREATE);
proto_init_interface(if_new, if_new->config);
interface_claim_device(if_new);
netifd_ubus_add_interface(if_new);
}
}
static void __init
interface_init_list(void)
{
vlist_init(&interfaces, avl_strcmp, interface_update);
interfaces.keep_old = true;
interfaces.no_delete = true;
}
也就是当interfaces发生变更(创建/删除/更新)时,会被调用,这个更新就很让人在意,但是文档中也没写什么算作更新,我暂且在蒙古里。
视线转向另外一个函数interface_ip_update_complete,其调用者有:
config_init_ip
interface_update_complete
其中config_init_ip的唯一调用者是config_init_all,这个函数是作为初始化所有interfaces来使用的;只有在main和netifd_reload会调用。在检查日志和pid后,确认netifd并未重启,看起来似乎也没reload的痕迹,interfaces似乎也没有重置,我们可以基本排除这条调用栈。
那就剩下interface_update_complete这个函数了,看起来就非常可疑;其唯一调用者proto_shell_update_link在proto-shell.c这个令人非常在意的源码中。
一通跳转后,在包括proto_shell_notify、proto_shell_attach、proto_shell_add_handler几个函数后,跳入了最上层的初始化函数proto_shell_init。
其中通过/lib/netifd/proto/目录,导入了一切系统内支持的协议:
# ls -l /lib/netifd/proto/
-rwxr-xr-x 1 root root 6279 Jun 10 00:27 bonding.sh
-rwxrwxr-x 1 root root 2868 Jun 10 00:27 dhcp.sh
-rwxr-xr-x 1 root root 4902 Jun 10 00:27 dhcpv6.sh
-rwxr-xr-x 1 root root 7833 Jun 10 00:27 ppp.sh
映入眼帘的是熟悉的几种协议;为了确认到底是哪个协议导致的reload,需要检查系统中是否有对应协议的daemon进程,以及其进程是否有调用其他外置脚本。
一通操作下来,系统中既存在dhcp又有dhcpv6,当然标配的ppp也在列;这下头大了,只能臆测一下可能的始作俑者了。
OpenWrt中跨进程/服务通讯主要有两种方式:
ubus脚本一般需要在源代码中引入相关类库,并在源代码中发送信号;而shell脚本则比较灵活,通过引用OpenWrt事先准备好的lib,就可以方便灵活的通讯(虽然底层也可能是ubus/luci之类的)。因为各种原因(懒),我在这就先排查了引用的外置脚本:
其中ppp引用了/lib/netifd/目录下的三个脚本ppp-up、ppp6-up、ppp-down;可是我们的ppp连接并未重播,先排除这个。
dhcp/6我使用的是OpenWrt的udhcpc和odhcp6c;分别调用了同样是/lib/netifd/目录下的dhcp.script和dhcpv6.script。
检查脚本内容中后,发现其均引用了/lib/netifd/netifd-proto.sh;并在代码中调用了其提供的proto_init_update和proto_send_update函数。
同时这两个脚本都有处理dns服务器相关的代码,基本可以确定原因就是这二者之中的一个了。
进一步查看netifd-proto.sh代码后也确认了这个想法,其中proto_init_update函数中有如下代码:
proto_init_update() {
......
json_add_int action 0
......
}
这段代码中,刚好对应了proto-shell.c中,proto_shell_notify函数处理NOTIFY_ACTION的代码,其传入参数值为0:
enum {
NOTIFY_ACTION,
......
};
static const struct blobmsg_policy notify_attr[__NOTIFY_LAST] = {
[NOTIFY_ACTION] = { .name = "action", .type = BLOBMSG_TYPE_INT32 },
......
};
static int
proto_shell_notify(struct interface_proto_state *proto, struct blob_attr *attr)
{
struct proto_shell_state *state;
struct blob_attr *tb[__NOTIFY_LAST];
state = container_of(proto, struct proto_shell_state, proto);
blobmsg_parse(notify_attr, __NOTIFY_LAST, tb, blob_data(attr), blob_len(attr));
if (!tb[NOTIFY_ACTION])
return UBUS_STATUS_INVALID_ARGUMENT;
switch(blobmsg_get_u32(tb[NOTIFY_ACTION])) {
case 0:
return proto_shell_update_link(state, attr, tb);
......
}
刚好这两个脚本支持hot-plug user scripts,为我们进一步确认元凶提供了帮助:
udhcpc可以在/etc/udhcpc.user文件中,或者/etc/udhcpc.user.d目录中添加对应自定义脚本;
odhcpc6同样可以在/etc/odhcp6c.user文件中,或者/etc/odhcp6c.user.d目录中(OpenWrt 21.02版本后支持,具体可以通过上述的sciprs文件确认)添加自定义脚本。
自定义脚本内容很简单,以odhcpc6为例:
#!/bin/sh
date >> /tmp/odhcp6c.user.env
echo "$*" >> /tmp/odhcp6c.user.env
export >> /tmp/odhcp6c.user.env
echo ==================== >> /tmp/odhcp6c.user.env
在脚本中记录相关参数后,通过手动模拟/tmp目录满载,检查对应日志文件果然发现了端倪。
在每次dnsmasq抱怨在文件中找不到dns服务器时,resolve文件被更新,odhcpc6也总是会有一条调用日志:
# logread | grep retry
Fri Aug 16 17:50:06 2024 daemon.warn dnsmasq[32099]: no servers found in /tmp/resolv.conf.d/resolv.conf.auto, will retry
# date -r /tmp/resolv.conf.d/resolv.conf.auto
Fri Aug 16 18:26:13 CST 2024
# cat /tmp/odhcp6c.user.env
Fri Aug 16 17:50:06 CST 2024
pppoe-wan ra-updated
折腾了这么久,终于确认了原来导致系统更新DNS的每隔10分钟左右,运营商服务器就会发布一次RA,其中除了PD/GW等相关信息之外,还一并包括了DNS的相关信息,从而导致netifd重新创建reslove文件。
而刚好在网络配置文件中,默认忽略了来自IPv6的DNS,从而导致没有注意到原来是IPv6 RA导致的问题。
折腾了这么久,终于找到问题所在,但是想了想解决方案却和找到的没什么关系;没错就是增大/tmp目录、定时清空其中部分调试日志,防止其被占用满;这么一大圈下来不仅没有发现bug,甚至还是一个feature,所以也没发现啥新的解决方法。
折腾就是这样的,乐趣往往就在过程之中(就是真的太费时间了),解决方案可能只是其顺带的副产物~尤其是还经常会因为找不到原因,花太多时间而被迫终止折腾,最后可能还是在某次突然灵感迸发后,才有新的思路。
总之这次成功的折腾我还是很开心的,希望以后的折腾也能这么顺利~))(总觉得好像以前也折腾过,只是没有成功;还是我忘了,上了年纪越来越依靠外部记录的帮助了)
前两个月在滚Arch的时候,发现滚完之后就无法将Redmi电脑音响设置为默认设备了,一番查找之后发现是PipeWire更新导致的。检查升级记录之后,确认从PipeWire 1.0.5开始就会出现这个问题,不管是用KDE的GUI设定为默认设备,还是通过PipeWire和WirePlumber的CLI都不行。
因为一直没空寻找具体原因,也就直接采用降级大法解决,但是经常会出现开机时初始化PipeWire卡死的问题,还是有点不方便;这两天终于抽空彻底排查了以下。
既然确定了出现问的版本,接下来的事情就很简单了。通过在git提交中搜寻,最终确定了引入该问题的commit是这个cc841a76。
查找的方法使用算法里面经典的图书管理员秘法之二分查找,最开始的时候还是手动二分查找的,后来想到vcs八成有自带这个功能,一搜发现是git bisect;果然大佬们早就想到这个排查bug的方法,今天又学到了新知识很开心,几次搜索之后就将出现问题的commit找到了。
既然找到了引入问题的commit,在提交官方处理的同时,revert这个commit就自然成为目前的dirty workaround了。
将pipewire打包项目从Arch仓库中clone下来,引入下面这个patch,之后在PKGBUILD中应用就大功告成了,具体方法:把下面的patch文件拷贝到仓库所在的文件夹中,在PKGBUILD的prepare方法中使用git apply ../0001-Revert-acp-add-more-properties-for-the-card.patch。
0001-Revert-acp-add-more-properties-for-the-card
最后使用pacman -U安装pipewire-audio这个包就足够了;无意中发现pipewire拆成多个包之后,不同的版本搭配其实也是可以用的,比如说pipewire-audio可以用老点的版本(最新版把我小主机内置音响更没了,虽然不常用吧,但是还是能用用的;等官方处理完这个问题再看看好了没吧,没好再去发个issue看看;主要是遇到了PipeWire的大版本更新,整个设备结构和Profile都变了,估计没这么好查了),其他包用最新版,除了开机时偶尔会卡死无法正常初始化,但是成功之后没有用出什么太大问题。
希望能够快点解决吧,这里留一个issue连接作为记录。
[1] https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/4135
]]>icmp6 or dhcpv6,但是如果是capture filter就不支持dhcpv6这样的语法了。
查询官网文档后发现,wireshark的display filter和capture filter还不一样,前者是wireshark自己解析的高级语法,后者是pcap语法,和tcpdump一样,只支持一些基础过滤。
ICMP/ICMPv6的过滤pcap直接支持,DHCP/DHCPv6使用固定的udp端口,所以可以通过端口过滤实现。
DHCP:56、57
DHCPv6:546、547
比如想过滤DHCPv6和ICMPv6的数据包,则过滤语法为icmp6 or (udp port 546 or 547 )
群里的Lihz菊苣最近在使用广州联通的时候遇到了宽带ICMP丢包/前几个包必丢的问题,经过一番研究确认了是光猫中802.1p的设定值的问题。
802.1p标记用于标记业务优先级,取值为0-7,优先级最低为0,最高为7,优先级越高,保障等级越高,具体可以参考Wiki。
如果是使用的光猫,通过超级管理员账号进入光猫后台,在WAN设置中找到Internet那一条WAN,编辑设置中的802.1p就可以了。
猫棒得看对应固件中支持否有相关选项,如果不支持的话可能暂时就无法修改。
找到对应设置的地方后,设置优先级为最高,即802.1p为7就没再出现问题。初步猜测原因可能是运营商设备设置错误(Lihz菊苣使用的广州联通还下发的是/64的IPv6 PD,很明显还没经过改造,不符合工信部最新的文件中关于IPv6二次下发的要求),直接使用了来自设备端的802.1p值0,即默认为最低优先级;在网络尖峰期,设备资源不足的时候因优先级不足而丢弃;按照通常配置该值一般都会被运营商覆盖掉。
这里记录一下默认值,因为都是使用了原厂/非对应运营商的光猫所以该值仅供参考。
广东电信(MA5671原厂固件):0
广东联通(HS8145C5/2584.A/2021电信版):0
广东联通(HS8145C5/180F.A/2019电信版):0
PS:其他业务WAN的默认802.1p值
TR069:7
OTHER/IPTV:5
2024年的今天,在各方积极的推动下,大陆IPv6的覆盖率已经非常高了;三大运营商新开的家庭宽带已经默认开通了IPv6,现存的宽带也通过改造、下发配置、设备更新的方式在迅速的推进中;而移动端则更是激进,在4G部署时就已经是按照双栈网路配置,可以说是完全覆盖,连二级热点都已经做足了充分的兼容。
就在几天前,工信部还发了一份《两部门关于开展“网络去NAT”专项工作 进一步深化IPv6部署应用的通知》的文件,其中的目标是在进一步的推进IPv6的积极应用的同时,开始为IPv4的NAT网路去除做准备(目前阶段主要关注NAT44设备规模停止增长),同时也提出了IPv6子网标准化要求,要求对IPv6网路的二次分发功能做好兼容。
Ovear自己家的网络在大约五六年前,三大运营商开通IPv6基本厘清大陆内的交换路由后,就实装进来了,也算是见过IPv6早期混乱路由的资深用户了(笑)。
PS:目前IPv6的国内路由已经完全可以满足日常使用了,可以说IPv6在移动端更是主要承载网络;除此之外,得益于政策支撑,IPv6跨境路由方面已经不逊于IPv4了,有些情况下因为使用的人稍微少一些,更是超过了IPv4。
废话扯了这么久,还是快点进入正题吧。首先要了解这个问题,必须得知道两个前提;IPv6的SLAAC最小子网大小要求是/64。
问:这个子网大小是什么概念呢?
答:这个大小比目前整个IPv4的所有地址加起来还要大。
那么这么大的子网,系统为了保护用户隐私,就会利用SLAAC为系统分配该子网下的多个IPv6地址作为临时地址,每段时间就会变更一次。同时因为IPv6极力不推荐使用NAT,就有可能出现一个内网广播域中有多个IPv6子网。
为了更好的管理这些IPv6地址,IPv6引入了两个概念:preferred_time和valid_time。在Linux中可以使用ip -6 ad来确认对应IPv6地址对应的两个时间。其中preferred_lft就是剩余的preferred_time,而valid_lft对应的就是valid_time,如下面的示例所示。
2: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
inet6 fdbd:ffff:ffff::fff/128 scope global dynamic noprefixroute
valid_lft 42525sec preferred_lft 1533sec
那么具体而言,IPv6地址的生命周期可大概分为两部分,其中:
0 - preferred_time的这段时间为IPv6的有效使用时间:即该地址有效,且系统会积极使用该地址preferred_time - valid_time则是该IPv6的非积极有效时间:即该地址有效,但是系统会极力避免使用该地址。preferred_time为valid_time的一半。需要注意:preferred_time和valid_time可以通过RA更新。即和IPv4一样,IPv6地址也可以进行“续租”。其具体时间应该是从preferred_left为preferred_time一半的时间开始续约(具体记不清了,有一个T0的时间点;如果我记错了还劳烦大家纠正;除此之外还有一个隐私地址机制IPv6 privacy extension,这个机制开启后系统会定期分配新的地址使用),即在时间到达valid_time的1/4时系统就会开始积极续约。
对于Linux系统来说每当preferred_lft到达0以后,在ip -6 ad中可以观察到地址被标记为deprecated,意味着对于新发起的连接,系统会尽量避免使用该地址;而当preferred_lft为0后,则会将该地址从系统中删除。
Design is prefect, 一切都是这么的完美,会有什么意外呢?不出意外的话,就要出现意外了。
确实在大多数情况下,这套机制能非常完美的工作,但是现实总是没那么完美。当以下问题出现时,这套机制就可能发生问题:
temp_addr的kernel sysctl:理论上temp_addr应当由内核协议栈处理,但是使用Network Manager之后,这部分逻辑似乎会有不同步的情况发生。解决该问题需要统一在Network Manager中配置,而不是系统内核。目前在Arch Linux中遇到过。deprecated的问题。目前也仅在Arch Linux+Network Manager中遇到,其中同网段的一个地址被正确弃用了,但是另外一个地址却没有被弃用,非常奇怪;只能怀疑不是Kernel的问题就是Network Manager的问题。Network Manager出现问题的可能性会大一些,局域网内其他Linux(Debian/Ubuntu)设备都没遇到这样的问题;Debian默认用的ifupdown,Ubuntu则是自己的私货netplan。临时解决方法当然就是重启网络。以上仅仅是Ovear个人遇到的一部分情况,可能还有更多的情况没有列出,欢迎大家一起补充。
经过抓包诊断,发现Ovear家电信的宽带设定的时间较长,preferred_time设置为2天,valid_time设置为3天;而为了避免运营商强制断线影响到网路正常使用,目前设定的都是每隔24小时自动重播一次,所以可能加剧了这个问题;移动也是类似情况;而联通则都设定为1个小时,联通宽带也基本没有没有观测到这个问题;因此可以初步考虑将电信和移动宽带的设置统一为和联通一样。
明确了原因之后,问题就很好解决了:
preferred_time和valid_time通过查询OpenWrt官方文档[1]可知,相关的参数为为:
ra_mininterval、ra_maxinterval:控制RA广播间隔preferred_lifetime:控制最大的preferred_timera_useleasetime:是否设定valid_time为DHCP租约时间ra_lifetime:路由器发送的RA有效期相关代码
# https://github.com/openwrt/odhcpd/blob/a29882318a4ccb3ae26f7cc0145e06ad4ead224b/src/router.c#L600-L614
if (addr->preferred_lt > (uint32_t)now) {
preferred_lt = TIME_LEFT(addr->preferred_lt, now);
if (preferred_lt > iface->preferred_lifetime) {
/* set to possibly user mandated preferred_lt */
preferred_lt = iface->preferred_lifetime;
}
}
if (addr->valid_lt > (uint32_t)now) {
valid_lt = TIME_LEFT(addr->valid_lt, now);
if (iface->ra_useleasetime && valid_lt > iface->dhcp_leasetime)
valid_lt = iface->dhcp_leasetime;
}
其中ra_mininterval、ra_maxinterval支持在luci界面中设定,设定位置为Network=>Interfaces=>lan=>IPv6 RA Settings。
为了方便统一设定,也可以使用编辑文件的方式设定,编辑OpenWrt中的/etc/config/dhcp,找到对应配置文件段落,编辑为所需要的内容。
# /etc/config/dhcp
config dhcp 'lan'
option ra_mininterval 60
option ra_maxinterval 300
option leasetime '1h' # DHCPv4租约时间
option ra_lifetime 600 # 路由RA有效期
option ra_useleasetime 1 # 使用DHCPv4租约时间作为IPv6 valid_time
option preferred_lifetime '30m' # IPv6 preferred_time
设定完成并重启后可以使用tcpdump/wireshark等工具抓包查看是否生效。
# tcpdump -i br-lan -n -vv "icmp6 && ip6[40] == 134"
这样理论上应该能将无效的IPv6地址生命周期控制在可接收范围内。
[1] [OpenWrt Wiki] odhcpd
[2] openwrt/immortalwrt修改odhcpd ipv6 preferred_lifetime和valid_lifetime - 海运的博客
(然而发现并不能,所以又研究了一大堆相关的其他东西,就有了这一篇)
socket
如果能通过分组执行socket搜索后,能够找到一个打开的TCP/UDP
socket,则会进行匹配。具体来说,这会匹配一条已建立(establshed)
的链接,或者非0绑定监听器(不是监听0.0.0.0或者::/非INADDR_ANY)
socket(比如说有可能是非本地地址)。搜索是通过TCP/UDP分组的元
组进行的,对于ICMP/ICMPv6错误分组来说,则是使用内嵌在内的原始
TCP/UDP头部中的元组。
--transparent
忽略非透明(non-transparent) sockets.
--nowildcard
不要忽略绑定在'任意'地址的sockets。默认情况下,不会
匹配0绑定监听器;因为在那种情况下,本地的服务可以
拦截本来会被转发的流量。因此,在匹配使用策略路由重
定向到本地的转发流量时,本选项有安全影响。当使用
socket 匹配器来实现绑定在非本地地址的完全透明代理
时,更推荐使用 --transparent 选项。
例如(假如被标记为1的分组会被发送到本地):
-t mangle -A PREROUTING -m socket --transparent -j MARK
--set-mark 1
--restore-skmark
将分组标记设置为匹配到的socket的标记。可以和 --transparent
以及 --nowildcard 选项组合使用来限制恢复分组标记时匹配的
sockets。
例如:一个打开了2个透明(IP_TRANSPARENT)的sockets,并
使用SO_MARK选项设置了一个标记。我们可以这样过滤匹配的分组:
-t mangle -I PREROUTING -m socket --transparent --restore-
skmark -j action
-t mangle -A action -m mark --mark 10 -j action2
-t mangle -A action -m mark --mark 11 -j action3
/* 基于 "socket" 匹配的重定向 (无特定规则)
* ===================================================
*
* 有一些链接使用了动态对端/端口(如FTP数据链接),
* 这种链接用户无法显式为其创建规则。
* 这些情况都可以通过通用的“socket”规则来处理。
* 假设信任的代理应用不需要显示的规则就可以建立链接
* (当然除了通用的"socket"规则除外)。在这种情况下
* 以下socket按照下列优先顺序匹配:
*
* - 匹配: 如果使用__分组__元组匹配到了一条完全
* 建立的链接(established)
*
* - 匹配:如果存在非0绑定(不是监听0.0.0.0或者::/非
* INADDR_ANY)的监听器(比如说有可能是非本
* 地地址)。我们不允许0绑定监听器是因为本地服
* 务可能会拦截通过机器的流量。
*/
网上很多TPROXY的iptables示例中都包含一条使用了-m socket -p tcp也没有解释的改进。这个iptables的socket模块补丁能匹配到对应的本地socket,这可能比执行到一组特定的规则更加精确或快速。
下面这个到处都是的配置设定了一个接受并标记分组的redirect chain:
iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
接下来这些规则保证了已建立的本地socket对应的所有内容都被发送到那;然后就是新分组需要进行的操作:
iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT
iptables -t mangle -A PREROUTING -p tcp --dport 25 -j TPROXY \
--tproxy-mark 0x1/0x1 --on-port 10025 --on-ip 127.0.0.1
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY \
--tproxy-mark 0x1/0x1 --on-port 10080 --on-ip 127.0.0.1
唯一需要的步骤就是策略路由和TPROXY iptables-rule. DIVERT规则是为了防止tproxy目标做不必要的操作而做的优化(-m socks检查socket是否符合一个网络分组头部)
我看了这么多注释,其实对socket模块也没有一个清晰地了解,很多使用到的也没有说的太细,看的云里雾里的。
但是普遍接受的一个观点是新建 DIVERT 规则,避免已有TCP连接的包二次通过 TPROXY,理论上有一定的性能提升;仔细想了想可能有这么个道理。
所以要说清楚socket模块到底是什么,我们有必要稍微说明下TPROXY模块是什么。
TPROXY的作用主要是将链接在想要建立的socket(skb)上执行预操作,使内核在协议栈层面上对这些分组/socket重定向到本地做准备;其中很重要的一点就是,重定向到某个端口,以及重定向到某个IP监听的socket。而在这个过程中,整个分组(packet)本身并未被修改,只是socket本身被系统悄悄咪咪地替换了,所以我们还是能够获取到这个分组原始的目的地(通过分析分组头部)。
那有的同学会问了,既然这样要给分组打标记做策略路由的目的是什么?
原因其实就是上面说明的内容,虽然系统把socket偷梁换柱了,但是分组本身没有发生变化,它还会在系统中走完剩下的正常流程(比如说路由决策)。这时我们就需要将这个包引导到本地,不然就会给转发出去了。因为我们做的骚操作其实是在L4,而路由决策等操作是在L3,所以我们得一并把L3的事情给做了(路由),所以就需要策略路由来把我们悄悄咪咪做的事做完全套。
从上述分析我们可以得出一个额外的结论:TPROXY并不支持重定向到远程机器;因为我们在L4层做的所有操作都是本地Only级别的(分组本身没有改变);一旦分组被转发出去,就GG了;远程的机器根本不知道我们暗地里做了这些事。
正如iptables-extensions所说:
It redirects the packet to a local socket without changing the packet header in any way.
他会重定向分组到一个本地socket,而不会以任何方式修改分组头部本身。
好不容易,我们终于让本地的socket有希望(其实还收不到)收到了来自TPROXY的分组。
因为分组头部未改变,我们的程序很容易的可以获取分组中原来目的的地址和端口;但是此时如果想要建立链接,我们的程序要在socket中绑定不属于自己的地址(非本地地址),才能让链接能够建立。
即程序要假装自己是目标地址,返回数据包给还蒙在鼓里的客户端。但是这种行为默认是在系统中禁止的;得过了系统的这一关,这种浑水摸鱼的操作才能成功。
所以我们就有了SOL_IP, IP_TRANSPARENT这个socket监听选(后)项(门)来说服系统让我们这么做(毕竟这种高风险操作,那不能是谁都可以做的呀)。
有了这个选项以后,我们就可以假装是目标地址,使用它的信息来在客户端不知情的情况下成功建立链接。
说了这么多,我们把TPROXY的基本操作说完了,但是似乎这些和socket模块有什么关系呢?
其实和TPROXY的模块就在刚才说的替换socket这一步。我们要怎么确定要替换的socket究竟是哪个呢?
这时候我们再回来看一下上面关于socket模块的匹配描述。
* - 匹配: 如果使用__分组__元组匹配到了一条完全
* 建立的链接(established)
*
* - 匹配:如果存在非0绑定(不是监听0.0.0.0或者::/非
* INADDR_ANY)的监听器(比如说有可能是非本
* 地地址)。我们不允许0绑定监听器是因为本地服
* 务可能会拦截通过机器的流量。
这时我们就会发现,这个匹配行为似乎和TPROXY非常相似;那么其实socket模块是不是就是为了简化连接建立后TPROXY复杂的行为,从而提升性能的优化呢?
我们来看看TPROXY的源代码中有没有蛛丝马迹。
// https://elixir.bootlin.com/linux/v6.1.11/source/include/net/netfilter/nf_tproxy.h#L75
/*
* 这是用于用户想要显式通过iptables拦截符合规则的链接。在这种情况
* 下,我们假设sockets按照以下顺序匹配:
*
* - 匹配: 如果使用__分组__元组匹配到了一条完全建立的链接,
* 就直接返回它;我们假设重定向已经完成了,因为我们正在
* 处理的是一条已经建立的链接。
*
* - 匹配:如果有与重定向目的地(就是链接的on-porth和on-ip)
* 匹配的监听socket,不管socket是监听在0.0.0.0还是一个具体
* 的地址,都直接返回它。我们这么做的理由是因为已经存在
* 显式的规则了,所以监听器不管是不是监听在某个网卡上,
* 还是0地址都不重要。用户已经声明了他想要重定向(因为
* 他新增了这条规则)。
*
* 请注意,TPROXY目标和socket模块的匹配之间存在重叠。通常情况
* 下,如果这两条规则你都创建了,“socket”模块会是第一条规则,实际
* 上所有已建立的链接都会通过这条规则。
*/
还有另一段小注释
// https://elixir.bootlin.com/linux/v6.1.11/source/net/ipv4/netfilter/nf_tproxy_ipv4.c#L103
/* 提示: 就算是监听在0.0.0.0的监听器,我们
* 也会返回。那种情况在xt_socket中被过滤
* 了,因为xt_TPROXY也需要0绑定监听器。
*/
到这里,关于socket模块作用的这个问题,就呼之欲出了。
但是我们还是不要飞的这么快,再捋一下这个逻辑:
TPROXY和socket模块的匹配规则有重叠之处;链接建立后(established)不管是TPROXY还是socket模块,都是直接匹配的这个socket。
同时链接建立后,系统中该分组对应的socket也不会再发生改变了(不然链接就错乱啦);而TPROXY内部的逻辑远比socket复杂,那么内核组的开发者们自然就想利用这个来优化下性能了(当然这里指的是TCP,UDP的TPROXY内核实现有些许不同,但是大体的思路是一致的)。
我们再回想下TPROXY之前的操作,L4偷换socket,L3标记分组修改路由;那现在socket已经换过,差的就是给分组打上对应标记从而使得策略路由生效了。
再回过头来看我们的DIVERT CHAIN做的不正好就是这一件事嘛?而socks模块的逻辑比TPROXY简单许多,这就存在了性能差异。
结论:socks模块在这里的作用,确实是在尝试优化TPROXY,以获取更高的性能。
折腾了这么久,终于找到了这个问题的答案了。
期间在网上漫无目的的搜索,也没有找到太多有用的信息,最后还是通过源代码阅读之术和运气的加持之下,才磕磕碰碰的勉强找到了答案。
其实之前也尝试过很多次看Linux内核的源码,但是Linux内核的设计实在是太高端了,没能够看懂太多东西。
但是这个Netfilter/iptables之旅,恰好给了我这么个机会。这一路上见识到了TProxy的奇妙想法、设计;简洁的实现(核心思想真的非常简单,没想到还可以有这种骚操作),充满魅力的Linux代码(特别精妙);非常强大的Netfilter/iptables。(小声:可惜iptables还没搞明白,nftables它又来了;你不要过来啊啊啊啊啊啊(:з」∠))
第一次阅读Linux内核代码(挖坑:以后看看有没有机会能不能把源码分析过程也整理出来),可能有一些错误,欢迎大家指正。
最后的最后,其实我还是好奇上面的DIVERT规则是否能优化一下;看起来socks模块现在似乎新增了一些选项,现在似乎都没有利用上。之后测试一下来更新一下结果看看哈哈哈哈哈~
// 选项1,只用 --transparent
iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT
iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j DIVERT
// 选项2,结合 --transparent 和 --restore-skmark
iptables -t mangle -A PREROUTING -p tcp -m socket --transparent --restore-skmark -j ACCEPT
选项1使用了--transparent选项,只应用本地socket中透明代理的部分,理论上应该会降低其他分组被意外打上标记的问题。
选项2则在选项1的基础上更进一步,使用了--restore-skmark的新特性,将多条语句合并为一句;根据提交记录[10]介绍,应该是能达到和最初优化语句一样的目标的,同时还增加了更多可能性。
时隔文章发布一年半以后,今天看到tproxy的Kernel文档中包含了使用Nftbales的示例,而Nftables的文档更清晰的说明了socket模块的作用,所以特意加上来以帮助大家理解。
你也可以通过以下nft命令来实现
# nft add table filter
# nft add chain filter divert "{ type filter hook prerouting priority -150; }"
# nft add rule filter divert meta l4proto tcp socket transparent 1 meta mark set 1 accept
Nftables的文档[11]
SOCKET 语法
socket {transparent | mark | wildcard}
socket cgroupv2 level NUM
Socket语法可以用来搜索一个已经存在的打开的TCP/UDP socket以及其可与分组关联的属性。它寻找的是已经建立的或者非0绑定的监听socket(可能使用非本地地址)。你也可以通过此语法来匹配给定祖先等级/ancestor level的cgroupv2 socket。比如如果一个socket属于cgroupv2 a/b,祖先等级1会检查并匹配cgroup a,而祖先等级2则会检查并匹配cgroup b.
Table 32. 可使用的socket属性
| 属性名称 | 描述 | 类型 |
|---|---|---|
| transparent | 匹配到的socket中的IP_TRANSPARENT选项的值。可以为0或者1。 | boolean (1 bit) |
| mark | socket mark的值 (SOL_SOCKET, SO_MARK)。 | mark |
| wildcard | 表明该socket是否为通配符绑定 (e.g. 0.0.0.0 or ::0)。 | boolean (1 bit) |
| cgroupv2 | 此socket的cgroup version 2 (/sys/fs/cgroup的路径) | cgroupv2 |
# 标记与透明socket相关的分组。“socket wildcard 0” 表示**不**匹配
# 0绑定监听socket(这通常正是您想要的)。
table inet x {
chain y {
type filter hook prerouting priority mangle; policy accept;
socket transparent 1 socket wildcard 0 mark set 0x00000001 accept
}
}
# 追踪mark为15的socket对应的分组
table inet x {
chain y {
type filter hook prerouting priority mangle; policy accept;
socket mark 0x0000000f nftrace set 1
}
}
# 将分组mark设置为socket mark
table inet x {
chain y {
type filter hook prerouting priority mangle; policy accept;
tcp dport 8080 mark set socket mark
}
}
# cgroupv2第一级为"user.slice"的计数器
table inet x {
chain y {
type filter hook input priority filter; policy accept;
socket cgroupv2 level 1 "user.slice" counter
}
}
[1] https://man7.org/linux/man-pages/man8/iptables-extensions.8.html
[2] https://ipset.netfilter.org/iptables-extensions.man.html
[3] https://patchwork.ozlabs.org/project/netdev/patch/20081001142431.4893.56954.stgit@este/
[4] https://elixir.bootlin.com/linux/latest/source/net/netfilter/xt_socket.c
[5] https://github.com/kristrev/tproxy-example
[6] http://wiki.squid-cache.org/Features/Tproxy4
[7] https://powerdns.org/tproxydoc/tproxy.md.html
[8] https://www.kernel.org/doc/html/latest/networking/tproxy.html
[9] https://elixir.bootlin.com/linux/v6.1.11/source/include/net/netfilter/nf_tproxy.h#L75
[10] https://patchwork.ozlabs.org/project/netfilter-devel/patch/[email protected]/
[11] https://www.netfilter.org/projects/nftables/manpage.html
策略路由是个好东西,就是用起来特别麻烦,经常产生一些很奇怪的问题,这次遇到的问题也是特别迷惑。
有一条规则会使用CONNTRACK对链接进行分类,然后把整个链接打上标记,如果不符合任何规则就打一个默认标记。
这条规则看起来一点问题都没有,使用起来也非常正常。但是如果使用MTR对负责这条规则的链接做路由追踪的话,就会出现第一次路由追踪完毕后,从第二跳开始就会丢包。
遇到这个问题之后,第一时间就怀疑是标记发生了错误。检查CONNTRACK之后,发现标记果然被打上了默认的标记。
但是仔细查找后发现,并未发现规则有明显的问题,且丢包只会发生在第2轮开始后从第2个hop开始丢包;而手动使用ping和traceroute则是全程没有问题的。
用iptables检查日志后,发现在第一轮的一个包时,mark被更改为默认的标记,但是后续的包又变回正常的。而在第二轮的第一个包时,mark又被更改为默认的标记,此时后续的包都变为默认的标记的了。
那么就有下面两个疑问
又经过一番研究,通过iptables日志中发现了问题的关键所在:
第一个包的TTL都是为1,即如果不是本地的IP,则会被本机直接返回TTL超时,那么就会经过netfilter的OUTPUT chain。而OUTPUT chain中有一条,如果DST是本地网络就设置为默认标记的规则。
这就回答了第2个问题:因为TTL exceed包是由本机发出的,导致被包被标记为默认标记并存入CONNTRACK。
但是这让问题更加迷惑了:第一轮的一个包被标记后,链接中后续的包怎么没有被设置为默认标记反而使用了正常的标记?按照CONNTRACK的描述来看,应该是对整条链接打标记才对,这是一条链接,应该已经绑起来了才对呀?
在网上搜寻了半天,也没有找到任何线索之后,决定还是使用老办法:检查nf_conntrack和iptables日志,最终发发现了conntrack的一个奇怪行为:
这个行为我猜测可能是因为,TTL=1的非本地地址ICMP链接没有追踪的意义,所以直接就不追踪了。因为对于非本地地址来说,TTL=1一定是不可达的,终止在本地的OUTPUT就行了。
知道这个行为之后,之前提到的问题2,似乎也就清晰了。
对于mtr来说,他目前的行为是从TTL=1开始,一直到目的地(即不出现TTL exceed,同时出现ICMP echo-reply)的TTL,按照从小到大顺序进行轮询;到达目的地之后,在从TTL=1开始继续递增,重复进行轮询
因为其type/code都是一致的,且seq是规律递增的,按理来说conntrack会将其归为一条连接。但是因为存在上面不追踪非本地TTL=1的ICMP链接行为的存在,导致conntrack会将其视为两个链接。
即:conntrack会忽略第一个TTL=1的MTR ICMP包,从TTL=2的第二个MTR ICMP包才开始追踪链接。
所以对于conntrack来说,我们实际的一个MTR的链接,会被视为两个链接。所以就出现了MTR中第1轮TTL=1的包和第2轮TTL=1的包不在一个链接中的情况:
连接1:只包括第1轮TTL=1发送以及回复的2个数据包。
连接2:其余剩下所有的数据包,包括第1轮TTL=2的包,和第2轮TTL=1的包,以及对应回复的数据包。
最终导致conntrack的restore-mark没有按照预期执行。
同时对于上述行为,也衍生出下面的问题:对于TTL=1的ICMP包,因为conntrack不会追踪,所以restore-mark会失效,save-mark也有意义。这样会导致这些包无法通过restore-mark保持标记,会重复的进行iptables规则匹配。
需要注意的时,我们的连接是双向的,因此conntrack跟踪时,除了将单个方向的数据包关联为一个连接以外,对应回复的数据包也会关联进来。具体而言:
ping -t 1 www.qq.com,conntrack只会关联其发送的单个数据包以及回复的数据包。那么在两个发送的TTL=1的数据包之间,restore-mark将会不起作用,如果使用了save-mark那么restore-mark仅会在这个ICMP包对应回复的数据包起作用。restore-mark和save-mark也只在对应的两个连接中的作用域中生效。在本文中,我的OUTPUT chain中未考虑到非目标主机中发出ICMP包也被归入conntrack的同一个连接中,同时也因为一些原因,也未检查对应连接是否已经设定过mark,导致问题的发生。
回溯一下发生的事情:
mtr www.qq.com所以第1轮mtr数据包走向了正确的路由,而之后的数据包都出现了丢包,同时mtr第2跳出现了意外回应的主机。
正是因为conntrack奇怪的行为,反而在有问题的策略路由规则下,保证了第1轮mtr数据包(也包括traceroute)的正常运作;而又是因为conntrack这个奇怪的行为,导致mtr才会出现(目前观察到的)的问题,也是非常神奇了。
不过也多亏上面这段这么绕的话和这个现象,又学到了奇怪的计算机网络和Linux知识,也算是一大意外收获了,总之还是蛮开心的))
// TODO下
上述这两个行为通过iptables的log也可以观察到,比较神奇;但是没有在网路中找到相关的讨论(可能是我英语太菜,没找到);感觉有空还是得翻一翻源码,看看有没有注释啥的。
既然知道原因,解决方法也很简单啦;可以直接忽略icmp,或者对icmp做特殊的规则,亦或者是对于icmp的链接不save-mark。
当然最好还是可以参考openwrt中的mwan3包的规则,其中的规则比较完善。
对于本地OUTPUT的包打标记时,也需要小心以免产生预期外的问题。
[1] https://www.frozentux.net/iptables-tutorial/chunkyhtml/x1582.html
]]>
解决方法
建立Config cache,就不用去调用DotEnv了
php artisan config:clear
php artisan config:cache
参考资料
https://laracasts.com/discuss/channels/general-discussion/env-not-reading-variables-sometimes
https://github.com/vlucas/phpdotenv/issues/160
https://bugs.php.net/bug.php?id=71607
https://bugs.php.net/bug.php?id=74986
https://laracasts.com/discuss/channels/general-discussion/issue-with-parallel-requests