Ovear's Blog https://ovear.info I'am Ovear,Ovear is me. Thu, 19 Dec 2024 16:31:05 +0000 zh-CN hourly 1 https://wordpress.org/?v=6.3.8 修改KDE Dolphin的默认视图/排序 https://ovear.info/post/874 https://ovear.info/post/874#respond Thu, 19 Dec 2024 16:31:05 +0000 https://ovear.info/?p=874 前言

作为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]子项之下。一顿修改测试之后,发现找错了地方,不是这个文件。

接下来就只能另想方法确认文件夹位置了。

通过inotifywait监听文件修改

既然确认了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_instancesfs.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的选项,我们只要找到GeneralSettingssave逻辑就可以知道位置了。但是好巧不巧,DolphinGeneralSettings是自动生成的代码。不过我们还有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这个目录也是格外受到各大应用的欢迎;挖个坑在这:过段时间空了看看能不能把目前常见的目录使用规范整理/翻译一下。)

根据日志和inotifywait进一步验证

代码中当保存视图配置文件时,会输出保存路径的调试信息,根据官方文档说明[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

]]>
https://ovear.info/post/874/feed 0
Linux中查看PCI相关信息 https://ovear.info/post/838 https://ovear.info/post/838#comments Sat, 19 Oct 2024 17:12:01 +0000 https://ovear.info/?p=838 前言

最近排查OpenWrt的一些问题时,在宿主机的内核日志中发现了一些pci相关的错误;需要具体检查下是哪个设备抛出的错误,遂有了这篇流水账记录。

PS:不知道是不是因为年龄的增长还是什么原因,感觉现在越来越开始依赖外部工具了(主要是感觉记忆力没有以前好了;也可能是强迫症/ADHD大发作);为了避免“青年痴呆”找上门,还是得多写写,多记录记录 (:з」∠),留一点赛博记忆在这;没准以后真有用得着的时候(还是希望不要用得上吧.jpg)。

lspci

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

通过 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:01PCI bridge提供了01:00子BUS,该子BUS挂载了01:00.0-3共记1个设备4个function。
而位于00:1bPCI bridge则提供了02:00子BUS,挂载了设备02:00.0
同理位于00:1cPCI 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

]]>
https://ovear.info/post/838/feed 2
排查OpenWrt DNS自动消失的问题 https://ovear.info/post/823 https://ovear.info/post/823#respond Fri, 16 Aug 2024 12:20:12 +0000 https://ovear.info/?p=823 前言

最近遇到一个奇怪的问题,偶尔/tmp目录满了之后,一段时间内就一定会导致/tmp/resolv.conf.d/resolv.conf.auto文件被清空,dnsmasq无法找到上级转发DNS导致断网。 但是查看文件修改时间,如果可用空间一直充足,这个文件的修改时间也不会发生变化;一旦没有可用空间,这个文件就会被替换为空文件。

谁动了reslove.conf

首先确认下该文件的同于是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对比一定会不一致,从而导致老文件被替换为新的空文件。

谁触发了刷新resolve

到这里事情基本都理清楚了,但是还剩下一个问题:
什么情况下系统DNS会更新,为什么每隔10分钟左右系统就会更新resolve文件;一旦有空间,几分钟过后resolve文件就会恢复成正常状态。

反向查找后,发现interface_write_resolv_conf有如下几个调用者:

interface_proto_event_cb
interface_change_config
interface_ip_update_complete

其中interface_proto_event_cb可以排除,因为代码中只有在IFPEV_UPIFPEV_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_configinterface_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来使用的;只有在mainnetifd_reload会调用。在检查日志和pid后,确认netifd并未重启,看起来似乎也没reload的痕迹,interfaces似乎也没有重置,我们可以基本排除这条调用栈。

那就剩下interface_update_complete这个函数了,看起来就非常可疑;其唯一调用者proto_shell_update_linkproto-shell.c这个令人非常在意的源码中。
一通跳转后,在包括proto_shell_notifyproto_shell_attachproto_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中跨进程/服务通讯主要有两种方式:

  • 外置shell脚本通讯
  • ubus通讯
    其中ubus脚本一般需要在源代码中引入相关类库,并在源代码中发送信号;而shell脚本则比较灵活,通过引用OpenWrt事先准备好的lib,就可以方便灵活的通讯(虽然底层也可能是ubus/luci之类的)。

因为各种原因(懒),我在这就先排查了引用的外置脚本:

其中ppp引用了/lib/netifd/目录下的三个脚本ppp-upppp6-upppp-down;可是我们的ppp连接并未重播,先排除这个。

dhcp/6我使用的是OpenWrt的udhcpcodhcp6c;分别调用了同样是/lib/netifd/目录下的dhcp.scriptdhcpv6.script

检查脚本内容中后,发现其均引用了/lib/netifd/netifd-proto.sh;并在代码中调用了其提供的proto_init_updateproto_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,所以也没发现啥新的解决方法。

折腾就是这样的,乐趣往往就在过程之中(就是真的太费时间了),解决方案可能只是其顺带的副产物~尤其是还经常会因为找不到原因,花太多时间而被迫终止折腾,最后可能还是在某次突然灵感迸发后,才有新的思路。

总之这次成功的折腾我还是很开心的,希望以后的折腾也能这么顺利~))(总觉得好像以前也折腾过,只是没有成功;还是我忘了,上了年纪越来越依靠外部记录的帮助了

参考文章

[1] https://github.com/openwrt/netifd/tree/openwrt-21.02

]]>
https://ovear.info/post/823/feed 0
解决PipeWire无法切换到Redmi电脑音箱的问题 https://ovear.info/post/806 https://ovear.info/post/806#comments Fri, 26 Jul 2024 10:31:15 +0000 https://ovear.info/?p=806 前言

前两个月在滚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文件拷贝到仓库所在的文件夹中,在PKGBUILDprepare方法中使用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

]]>
https://ovear.info/post/806/feed 3
使用tcpdump抓取DHCP/DHCPv6封包 https://ovear.info/post/801 https://ovear.info/post/801#respond Fri, 19 Jul 2024 18:28:56 +0000 https://ovear.info/?p=801 最近在诊断一些ICMP/DHCP问题,使用wireshark的display filter可以直接使用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 )

]]>
https://ovear.info/post/801/feed 0
修改802.1p解决宽带丢包 https://ovear.info/post/783 https://ovear.info/post/783#respond Tue, 16 Jul 2024 16:21:42 +0000 https://ovear.info/?p=783 发生了什么

群里的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

]]>
https://ovear.info/post/783/feed 0
OpenWrt中降低preferred_time和valid_time增加IPv6的稳定性 https://ovear.info/post/771 https://ovear.info/post/771#respond Sun, 14 Jul 2024 13:39:54 +0000 https://ovear.info/?p=771 前言

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_timevalid_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_timevalid_time的一半。

需要注意:preferred_timevalid_time可以通过RA更新。即和IPv4一样,IPv6地址也可以进行“续租”。其具体时间应该是从preferred_leftpreferred_time一半的时间开始续约(具体记不清了,有一个T0的时间点;如果我记错了还劳烦大家纠正;除此之外还有一个隐私地址机制IPv6 privacy extension,这个机制开启后系统会定期分配新的地址使用),即在时间到达valid_time的1/4时系统就会开始积极续约。

对于Linux系统来说每当preferred_lft到达0以后,在ip -6 ad中可以观察到地址被标记为deprecated,意味着对于新发起的连接,系统会尽量避免使用该地址;而当preferred_lft为0后,则会将该地址从系统中删除。

虽迟但到

Design is prefect, 一切都是这么的完美,会有什么意外呢?不出意外的话,就要出现意外了。

确实在大多数情况下,这套机制能非常完美的工作,但是现实总是没那么完美。当以下问题出现时,这套机制就可能发生问题:

  • 路由器[异常/非异常]重启:对于OpenWRT 21.02来说,只要路由器重启了,旧的IPv6地址就不能正常弃用;其实也很好理解,重启后的路由就不知道之前的老的IPv6地址了,应当在重启之前将所有IPv6地址弃用,希望后续版本改进了这个问题。
  • Linux在使用Network Manager时,调整了关于temp_addrkernel sysctl:理论上temp_addr应当由内核协议栈处理,但是使用Network Manager之后,这部分逻辑似乎会有不同步的情况发生。解决该问题需要统一在Network Manager中配置,而不是系统内核。目前在Arch Linux中遇到过。
  • 局域网内丢包:该问题常见于移动设备设备休眠,感觉理论上还是内核处理不当。当系统未收到路由RA时对于某些IPv6段的弃用申明时,就会发生。或者中间交换机重启的时间,刚好和路由器拨号时间重叠,也会遇到这个问题,只能等待下次路由器RA广播时希望系统收到后能解决。
  • 其他BUG:在Network Manager已经设定正确的上述情况下,偶发也会出现只有部分地址deprecated的问题。目前也仅在Arch Linux+Network Manager中遇到,其中同网段的一个地址被正确弃用了,但是另外一个地址却没有被弃用,非常奇怪;只能怀疑不是Kernel的问题就是Network Manager的问题。Network Manager出现问题的可能性会大一些,局域网内其他Linux(Debian/Ubuntu)设备都没遇到这样的问题;Debian默认用的ifupdown,Ubuntu则是自己的私货netplan。临时解决方法当然就是重启网络。
  • 启动DHCPv6 Statful以后:DHCPv6获取的地址是由DHCP客户端管理的,所以大概率要看DHCP客户端对RA的响应了。目前来看是会慢好几拍才会变,可能还是得看看DHCPv6的租约设定。不过由于系统一般都是会优先使用SLAAC的地址,所以问题倒是还不大。

以上仅仅是Ovear个人遇到的一部分情况,可能还有更多的情况没有列出,欢迎大家一起补充。

解决方案

经过抓包诊断,发现Ovear家电信的宽带设定的时间较长,preferred_time设置为2天,valid_time设置为3天;而为了避免运营商强制断线影响到网路正常使用,目前设定的都是每隔24小时自动重播一次,所以可能加剧了这个问题;移动也是类似情况;而联通则都设定为1个小时,联通宽带也基本没有没有观测到这个问题;因此可以初步考虑将电信和移动宽带的设置统一为和联通一样。

明确了原因之后,问题就很好解决了:

  • 丢包问题:降低非请求RA广播间隔,降低RA有效时间
  • IPv6有效期时间过长,降低最大preferred_timevalid_time

通过查询OpenWrt官方文档[1]可知,相关的参数为为:

  • ra_minintervalra_maxinterval:控制RA广播间隔
  • preferred_lifetime:控制最大的preferred_time
  • ra_useleasetime:是否设定valid_timeDHCP租约时间
  • 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_minintervalra_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 - 海运的博客

]]>
https://ovear.info/post/771/feed 0
iptables-extensions中socket模块是个啥? https://ovear.info/post/509 https://ovear.info/post/509#comments Fri, 10 Feb 2023 23:33:21 +0000 https://ovear.info/?p=509 今天看到这个叫做socket的iptables 模块挺有意思的,但是没有找到太多的资料;想了想翻译一下文档,应该可以作为理解一下这个模块的作用的开始。

(然而发现并不能,所以又研究了一大堆相关的其他东西,就有了这一篇)

文档

   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绑定监听器是因为本地服
 *     务可能会拦截通过机器的流量。
 */

PowerDNS的说明

你在到处看到的-m socket

网上很多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-exmaple中的说明

唯一需要的步骤就是策略路由和TPROXY iptables-rule. DIVERT规则是为了防止tproxy目标做不必要的操作而做的优化(-m socks检查socket是否符合一个网络分组头部)

到底socket 模块是什么

我看了这么多注释,其实对socket模块也没有一个清晰地了解,很多使用到的也没有说的太细,看的云里雾里的。

但是普遍接受的一个观点是新建 DIVERT 规则,避免已有TCP连接的包二次通过 TPROXY,理论上有一定的性能提升;仔细想了想可能有这么个道理。

所以要说清楚socket模块到底是什么,我们有必要稍微说明下TPROXY模块是什么

TPROXY模块系咩来尬?

它做了什么?

TPROXY的作用主要是将链接在想要建立的socket(skb)上执行预操作,使内核在协议栈层面上对这些分组/socket重定向到本地做准备;其中很重要的一点就是,重定向到某个端口,以及重定向到某个IP监听的socket。而在这个过程中,整个分组(packet)本身并未被修改,只是socket本身被系统悄悄咪咪地替换了,所以我们还是能够获取到这个分组原始的目的地(通过分析分组头部)。

为啥要策略路由?

那有的同学会问了,既然这样要给分组打标记策略路由的目的是什么?

原因其实就是上面说明的内容,虽然系统把socket偷梁换柱了,但是分组本身没有发生变化,它还会在系统中走完剩下的正常流程(比如说路由决策)。这时我们就需要将这个包引导到本地,不然就会给转发出去了。因为我们做的骚操作其实是在L4,而路由决策等操作是在L3,所以我们得一并把L3的事情给做了(路由),所以就需要策略路由来把我们悄悄咪咪做的事做完全套。

附赠的收获:TPROXY不支持转发到远程

从上述分析我们可以得出一个额外的结论: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监听选(后)项(门)来说服系统让我们这么做(毕竟这种高风险操作,那不能是谁都可以做的呀)。

有了这个选项以后,我们就可以假装是目标地址,使用它的信息来在客户端不知情的情况下成功建立链接。

所以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模块作用的这个问题,就呼之欲出了。

但是我们还是不要飞的这么快,再捋一下这个逻辑:

TPROXYsocket模块的匹配规则有重叠之处;链接建立后(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]介绍,应该是能达到和最初优化语句一样的目标的,同时还增加了更多可能性。

小尾巴2——来自Nftables的说明

时隔文章发布一年半以后,今天看到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

]]>
https://ovear.info/post/509/feed 2
记一次因CONNTRACK和ICMP导致的奇怪问题 https://ovear.info/post/434 https://ovear.info/post/434#comments Wed, 16 Nov 2022 16:27:00 +0000 https://ovear.info/?p=434 起因

策略路由是个好东西,就是用起来特别麻烦,经常产生一些很奇怪的问题,这次遇到的问题也是特别迷惑。

有一条规则会使用CONNTRACK对链接进行分类,然后把整个链接打上标记,如果不符合任何规则就打一个默认标记。

这条规则看起来一点问题都没有,使用起来也非常正常。但是如果使用MTR对负责这条规则的链接做路由追踪的话,就会出现第一次路由追踪完毕后,从第二跳开始就会丢包。

排查

遇到这个问题之后,第一时间就怀疑是标记发生了错误。检查CONNTRACK之后,发现标记果然被打上了默认的标记

但是仔细查找后发现,并未发现规则有明显的问题,且丢包只会发生在第2轮开始后第2个hop开始丢包;而手动使用ping和traceroute则是全程没有问题的。

用iptables检查日志后,发现在第一轮的一个包时,mark被更改为默认的标记,但是后续的包又变回正常的。而在第二轮的第一个包时,mark又被更改为默认的标记,此时后续的包都变为默认的标记的了。

那么就有下面两个疑问

  1. 第一轮的第一个包后,CONNMARK被置为默认,但是后续的包又恢复正常的了。而第二轮的第一个包后,后续包为什么又不正常了。
  2. 为什么第一二轮的第一个包,会被设置为默认标记

原因

又经过一番研究,通过iptables日志中发现了问题的关键所在:

第一个包的TTL都是为1,即如果不是本地的IP,则会被本机直接返回TTL超时,那么就会经过netfilter的OUTPUT chain。而OUTPUT chain中有一条,如果DST是本地网络就设置为默认标记的规则。

这就回答了第2个问题:因为TTL exceed包是由本机发出的,导致被包被标记为默认标记并存入CONNTRACK。

但是这让问题更加迷惑了:第一轮的一个包被标记后,链接中后续的包怎么没有被设置为默认标记反而使用了正常的标记?按照CONNTRACK的描述来看,应该是对整条链接打标记才对,这是一条链接,应该已经绑起来了才对呀?

未解之谜

在网上搜寻了半天,也没有找到任何线索之后,决定还是使用老办法:检查nf_conntrack和iptables日志,最终发发现了conntrack的一个奇怪行为:

  1. conntrack对于ICMP链接也会进行追踪,猜测应该是通过type/code/seq来实现的
  2. conntrack追踪ICMP链接时,只会对与TTL>1的非本地连接进行追踪;而对于本地IP即使TTL=1时,也会进行追踪

这个行为我猜测可能是因为,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跟踪时,除了将单个方向的数据包关联为一个连接以外,对应回复的数据包也会关联进来。具体而言:

  1. 对于只有TTL=1的包的连接,比如说ping -t 1 www.qq.com,conntrack只会关联其发送的单个数据包以及回复的数据包。那么在两个发送的TTL=1的数据包之间,restore-mark将会不起作用,如果使用了save-mark那么restore-mark仅会在这个ICMP包对应回复的数据包起作用。
  2. 对于同时有TTL=1、和TTL>1的包的连接,就如同上面我们讨论的一样,conntrack会将数据包关联进入两个连接,对应的restore-marksave-mark也只在对应的两个连接中的作用域中生效。

在本文中,我的OUTPUT chain中未考虑到非目标主机中发出ICMP包也被归入conntrack的同一个连接中,同时也因为一些原因,也未检查对应连接是否已经设定过mark,导致问题的发生。

回溯一下发生的事情:

  1. 本地执行mtr www.qq.com
  2. 第1轮TTL=1的包及其回应包,被正确打上标记【被conntrack视为连接1】
  3. 第1论TTL=2的包及其回应包,被正确打上标记【被conntrack视为连接2】
  4. 第1轮剩余的包及其回应包,被正确打上标记
  5. 第2轮TTL=1的请求包(ICMP Echo Request),被正确打上标记,注意这里是连接2
  6. 第2轮TTL=1的回应包(ICMP TTL time Exceeded)有本地路由器发出,经过OUTPUT Chain,被错误打上默认标记。注意这里还是连接2
  7. 被默认路由策略匹配中,导致路由路径变更。该连接的路由发生意外的路由路径变更,导致后续的MTR数据包路由路径也产生变更。
  8. 第2轮剩余其他的所有数据包,都因为错误的标记被restore-mark恢复,导致错误的路由路径。
  9. 第3轮TTL=1的回应包,再次回到5还是连接2

所以第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

]]>
https://ovear.info/post/434/feed 1
解决Laravel获取获取配置不稳定的问题 https://ovear.info/post/410 https://ovear.info/post/410#comments Tue, 14 Apr 2020 19:09:33 +0000 https://ovear.info/?p=410 (还在更新排版中...)
原因
在Apache开启worker或者event配合PHP Thread-safe版本的多线程情况下,环境变量由各个PHP线程共享。
在前一个线程初始化/退出清理环境的时候,会影响到其他线程读取环境变量,导致phpdotenv无法获取到正确的值(被初始化),从而导致Laravel获取不到设定的config,导致config初始化。

解决方法
建立Config cache,就不用去调用DotEnv了

php artisan config:clear
php artisan config:cache

参考资料

Laravel 在极高的瞬间并发时出现的问题


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

]]>
https://ovear.info/post/410/feed 2