立夏 发布的文章

Rust 中的 trait 很神奇,有静态和动态两种用法,当静态使用时相当于 C++20 中引入的概念,动态使用时又相当于抽象基类,或者说是接口。
虽然 trait 可以派生自另一个 trait,结构体可以实现 trait,但是结构体不支持继承和派生,对于一个用惯了 C++ 的程序员来说,多少还是有些不适应,于是就想着用宏来模拟类的功能。
为简化实现,我不打算支持多重继承、私有继承、保护继承等不常用的特性,也不支持在同一个类中定义参数不同的同名函数,在 C++ 中叫做函数重载。
我想实现的效果如下,通过给结构体添加属性 #[class] 来提供类的功能,通过在函数前添加关键字 virtual 来声明虚方法,通过关键字 override 来重写基类的虚方法:

#[class]
pub struct Base
{
    x: i32,
    y: i32,
    fn new(x: i32, y: i32) -> Self { Base{ x, y } }
    virtual fn func1(&self) -> i32 { self.x }
    virtual fn func2(&self, i: i32) -> i32 { self.y + i }
}
#[class]
pub struct Derive1 : Base
{
    z: i32,
    fn new(x: i32, y: i32, z: i32) -> Self { Derive1 { Base::new(x, y), z} }
    override fn func1(&self) -> i32 { self.z }
    virtual fn func3(&self) -> i32 { self.z }
}
#[class]
pub struct Derive2 : Derive1
{
    override fn func2(&self, i: i32) -> i32 { Base::func2(self, i) + 200 }
    override fn func3(&self) -> i32 { Derive1::func3(self) + 200 }
}

为了实现虚函数及重载,需要我们自己来构建虚函数表,并在类中添加虚指针,Base 类可能会展开为如下的形式:

#[repr(C)]
pub struct BaseData
{
    x: i32,
    y: i32,
}
#[repr(C)]
pub struct BaseVTable
{
    func1: fn(this: &Base) -> i32,
    func2: fn(this: &Base, i: i32) -> i32,
}
#[repr(C)]
pub struct Base
{
    vptr: &'static BaseVTable,
    data: BaseData,
}
impl BaseData
{
    fn new(x: i32, y: i32) -> Self { BaseData{ x, y } }
}
impl Base
{
    const VTABLE: BaseVTable = BaseVTable
    {
        func1: Self::func1_impl,
        func2: Self::func2_impl,
    };
    pub fn new(x: i32, y: i32) -> Self
    {
        Base { vptr: &Self::VTABLE, data: BaseData::new(x, y) }
    }
    fn func1_impl(this: &Base) -> i32 { this.data.x }
    fn func2_impl(this: &Base, i: i32) -> i32 { this.data.y + i }
    pub fn func1(&self) -> i32 { (self.vptr.func1)(self) }
    pub fn func2(&self, i: i32) -> i32 { (self.vptr.func2)(self, i) }
}

一切都很完美,然后 Derive1 类应该展开为如下的形式:

#[repr(C)]
struct Derive1Data
{
    base: BaseData,
    z: i32,
}
#[repr(C)]
struct Derive1VTable
{
    func1: fn(this: &Base) -> i32,
    func2: fn(this: &Base, i: i32) -> i32,
    func3: fn(this: &Derive1) -> i32,
}
#[repr(C)]
pub struct Derive1
{
    vptr: &'static Derive1VTable,
    data: Derive1Data,
}
impl Derive1Data
{
    fn new(x: i32, y: i32, z: i32) -> Self { Derive1Data{ base: BaseData::new(x, y), z } }
}
impl Derive1
{
    const VTABLE: Derive1VTable = Derive1VTable
    {
        func1: Self::func1_impl,
        func2: Base::VTABLE.func2,
        func3: Self::func3_impl,
    };
    pub fn new(x: i32, y: i32, z: i32) -> Self
    {
        Derive1 { vptr: &Self::VTABLE, data: Derive1Data::new(x, y, z) }
    }
    fn func1_impl(this: &Base) -> i32
    {
        let this: &Self = unsafe { reinterpret_cast(this) };
        this.data.z
    }
    fn func3_impl(this: &Derive1) -> i32 { this.data.z }
    pub fn func3(&self) -> i32 { (self.vptr.func3)(self) }
}

我们看到对于 Derive1 的数据成员和虚函数表,我们采用了不同的方式,是因为我将数据成员为认定为私有的,派生类不可以直接访问,所以在派生类中访问基类的数据成员,需要多个 base. 我并不关心,也可以避免派生类和基类数据成员的命名冲突。
对于虚函数表,我不能接受 base.base.base.base.func1 = Self::func1_impl 这样的写法,不仅仅是丑的问题,最主要的是,我不知道需要多少个 base. 才能访问到 func1,为此我需要将基类的函数表展开到派生类,接下来是 Derive2 的展开:

type Derive2Data = Derive1Data;
type Derive2VTable = Derive1VTable;
#[repr(C)]
pub struct Derive2
{
    vptr: &'static Derive2VTable,
    data: Derive2Data,
}
impl Derive2
{
    const VTABLE: Derive2VTable = Derive2VTable
    {
        func1: Derive1::VTABLE.func1,
        func2: Self::func2_impl,
        func3: Self::func3_impl,
    };
    pub new(x: i32, y: i32, z: i32) -> Self
    {
        Derive2 { vptr: &Self::VTABLE, data: Derive2Data(x, y, z) }
    }
    fn func2_impl(this: &Base, i: i32) -> i32 { (Base::VTABLE.func2)(this, i) + 200 }
    fn func3_impl(this: &Derive1) -> i32 { (Derive1::VTABLE.func3)(this) + 200 }
}

由于 Derive2 没有数据成员和新增虚函数,所以数据和虚函数表的定义直接使用 Derive1 的就好了。
接下来到了验证阶段,我们来看下,我们实现的类及虚函数重写机制是否能正常工作:

#[cfg(test)]
mod tests
{
    fn func(base: &super::Base) -> (i32, i32)
    { (base.func1(), base.func2(100)) }
    fn func2(d1: &super::Derive1) -> (i32, i32, i32)
    { (d1.func1(), d1.func2(100), d1.func3() }
    #[test]
    fn test_fn()
    {
        let b = super::Base::new(1, 2);
        assert_eq!((1, 102), func(&b));
        let d1 = super::Derive1::new(1, 2, 3);
        assert_eq!((3, 102), func(&d1));
        assert_eq!((3, 102, 3), func2(&d1));
        let d2 = super::Derive2::new(1, 2, 3);
        assert_eq!((3, 302), func(&d2));
        assert_eq!((3, 302, 203), func2(&d2));
    }
}

很不幸,测试代码还不能工作。Rust 还不能理解三个类之间的关系,需要我们给类和基类之间建立联系,但在这之前,有两个基础函数,需要现行实现,如下:

#[inline(always)]
pub unsafe fn reinterpret_cast<'a, T, U>(t: &'a T) -> &'a U
{
    let p = t as *const T;
    &*(p as *const U)
}
#[inline(always)]
pub unsafe fn reinterpret_cast_mut<'a, T, U>(t: &'a mut T) -> &'a mut U
{
    let p = t as *mut T;
    &mut *(p as *mut U)
}

对于 C++ 程序员来说,可能会觉得眼熟。方法 reinterpret_cast 和 reinterpret_cast_mut,正如它们的名字一样,它们可以将任意一种类型转换为另外一种类型,而不做安全性检查,因此它们是不安全的。接下来我们要用这两个不安全的函数来做一些不寻常的事情。

use std::ops::Deref;
use std::ops::DerefMut;
impl Deref for Derive1
{
    type Target = Base;
    fn deref(&self) -> &Self::Target
    {
        unsafe { reinterpret_cast(self) }
    }
}
impl DerefMut for Derive1
{
    fn deref_mut(&mut self) -> &mut Self::Target
    {
        unsafe { reinterpret_cast_mut(self) }
    }
}
impl Deref for Derive2
{
    type Target = Derive1;
    fn deref(&self) -> &Self::Target
    {
        unsafe { reinterpret_cast(self) }
    }
}
impl DerefMut for Derive2
{
    fn deref_mut(&mut self) -> &mut Self::Target
    {
        unsafe { reinterpret_cast_mut(self) }
    }
}

我们为派生类实现 Deref 和 DerefMut 两个 triat,让派生类引用能够转换为基类的引用,而实现方法是 reinterpret_cast,可以说我们是在挑战 Rust 的安全性。我们可以这样做,是因为我们确信派生类和基类在基类大小的部分有着相同的内存布局。这也是我们给生成的结构体添加 #[repr(C)] 属性的原因。
现在,我们测试可以通过了,我们可以将派生类的引用转为基类的引用,也可以通过基类的引用来调用派生类的方法。
但是,如果我希望将基类引用转换为派生类引用该怎么办呢?在 C++ 中是通过 dynamic_cast 来进行向下转换,在下一节我们来实现它。

打开 /etc/postfix/master.cf,找到 submission 一行,将注释打开,其他参数也可按照需要打开,如下:

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
#  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_tls_auth_only=yes
#  -o smtpd_reject_unlisted_recipient=no
#  -o smtpd_client_restrictions=$mua_client_restrictions
#  -o smtpd_helo_restrictions=$mua_helo_restrictions
#  -o smtpd_sender_restrictions=$mua_sender_restrictions
#  -o smtpd_recipient_restrictions=
#  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
#  -o milter_macro_daemon_name=ORIGINATING

重启 postfix 服务,再次尝试发送邮件,查看日志,可以看到已经在使用 465 端口了。

结论就是:如果你想要搭建邮件服务器,不管是多大规模,千万别选腾讯云。血的教训。

为啥呢?
1.腾讯云封了 25 端口,当然仅仅是封 25 端口,问题并不大,目前主流的邮箱服务商都支持邮件的加密传输,25 端口本来也是用不到的,用 465 端口或者 587 端口就好了。想法的确很好,但是我们都能想到的,腾讯又怎么会想不到?接下来才是腾讯的杀招:
2.QQ 邮箱只能通过 25 端口来连接。试想一下,你从零基础学起,看了无数教程,排除了无数故障,不知道白了多少根头发才搭建成的邮件服务器,到头来无法给 QQ 邮箱发邮件,一言难尽。

这里还发现一个新的问题,QQ 邮箱不支持 465 端口和 587 端口,安全吗?
这里来说一下这段时间以来,对三个端口的理解:
25 端口,就是最早的 SMTP 协议端口,是不支持加密传输的;
587 端口是 SMTP 协议增加了加密传输之后启用的端口,端口的连接过程和 25 端口是相同的,但是可以通过 STARTTLS 命令启用加密传输;
465 端口是从连接开始就要求是全程加密传输的。

我们来检验一下,首先我们找到 QQ 邮箱的 SMTP 服务器:

$ nslookup -query=mx qq.com
Non-authoritative answer:
qq.com    mail exchanger = 20 mx2.qq.com.
qq.com    mail exchanger = 30 mx1.qq.com.
qq.com    mail exchanger = 10 mx3.qq.com.
...

我们看到有三个服务器,其中 mx3.qq.com 的优先级最高,我们来连接 mx3.qq.com 的 465 端口,实际上,三个服务器的响应都是一致的,因为 465 端口是全程加密连接,所以我们用 openssl 来连接,如下:

$ openssl s_client -connect mx3.qq.com:465 -crlf -4
40CC6696657F0000:error:8000006E:system library:BIO_connect:Connection timed out:../crypto/bio/bio_sock2.c:125:calling connect()
40CC6696657F0000:error:10000067:BIO routines:BIO_connect:connect error:../crypto/bio/bio_sock2.c:127:
connect:errno=110
$ openssl s_client -connect mx3.qq.com:465 -crlf -6
407C4295987F0000:error:80000065:system library:BIO_connect:Network is unreachable:../crypto/bio/bio_sock2.c:125:calling connect()
407C4295987F0000:error:10000067:BIO routines:BIO_connect:connect error:../crypto/bio/bio_sock2.c:127:
connect:errno=101

结果和我们在 postfix 日志中看到的一致,ipv4 地址超时,ipv6 地址无法连接,QQ 邮箱根本就不开放 465 端口,这也是很多人将 postfix 改成通过 465 端口发送邮件之后,仍然无法给 QQ 邮箱发邮件的原因。我们再尝试 587 端口,如下:

telnet mx3.qq.com 587
Trying 120.241.17.157...
Trying 2402:4e00:8010::b0...
telnet: Unable to connect to remote host: Network is unreachable

587 端口同样无法连接,再尝试 25 端口,如下:

$ telnet mx3.qq.com 25
Trying 120.241.17.157...
Connected to mx3.qq.com.
Escape character is '^]'.
220 newxmmxsza11-4.qq.com MX QQ Mail Server.
ehlo mx3.qq.com
250-newxmmxsza11-4.qq.com
250-STARTTLS
250-8BITMIME
250-SIZE 73400320
250 OK
quit
221 Bye.
Connection closed by foreign host.

我们看到 25 端口支持 STARTTLS 命令,也就是说 QQ 邮箱在 25 端口上做了 587 端口的事情,即保障了传输安全,又让 587 端口下了岗。这就是腾讯的个性,走出了自己的路,让别人无路可走。

看来只有申请解封 25 端口这一条路了,我们看看腾讯云是这样说的:

为了提升腾讯云 IP 地址发邮件的质量,默认限制云服务器 TCP 25 端口连接外部地址,如果您没有在云上部署邮件服务,该限制不会影响您的服务;如果您需要使用邮件服务,我们诚挚地向您推荐腾讯企业邮箱。如果您一定要使用云主机向外连接 TCP 25 端口,请确保 TCP 25 端口仅用来连接第三方 SMTP 服务器,从第三方 SMTP 服务器外发邮件。如发现您使用云主机直接 SMTP 发送邮件,腾讯云有权永久封禁 TCP 25 端口,并不再提供相关服务。

企业邮箱这个提议,看的我都有点动心了,省时省力,不用担心服务器被黑,不担心垃圾邮件,虽然花点钱,但是值。而且不想花钱的也可以选择免费版。但我没有企业,更没有营业执照,想买企业邮箱,钱都花不出去。

我们把 SMTP 协议看作是一个邮局,它有两个窗口,一个窗口面向发邮件的个人,一个窗口面向其他邮局。
假设场景:用户甲、乙,邮局子、丑,甲给乙写一封信,然后拿着信来到子的个人窗口,子要求甲进行身份验证,验证成功,子收下信,然后将信送达乙所在的邮局丑的邮局窗口,丑核实子的邮局资质,以及乙是否是丑的客户,核实成功,丑收下信,然后投递到乙的邮箱。
我们看到个人窗口和邮局窗口功能不同,职责不同,对客户的要求也不同,在 SMTP 协议上都使用同一个端口。在实现上,邮箱服务商通常通过不同的域名将两个窗口拆开。
上面我们看到 mx3.qq.com 就是 QQ 邮箱的对邮局窗口,不收个人信件,而我们所熟知的 smtp.qq.com 是个人窗口,不收其它邮局发来的信件,我们来看下 QQ 邮箱个人窗口提供的服务,我们尝试连接 465 端口,如下:

$ openssl s_client -connect smtp.qq.com:465 -crlf
...
220 newxmesmtplogicsvrszc9.qq.com XMail Esmtp QQ Mail Server.
ehlo smtp.qq.com
250-newxmesmtplogicsvrszc9.qq.com
250-PIPELINING
250-SIZE 73400320
250-AUTH LOGIN PLAIN XOAUTH XOAUTH2
250-AUTH=LOGIN
250-MAILCOMPRESS
250 8BITMIME
mail from: <xxxx@yyyy.com>
503 Error: need EHLO and AUTH first !
405CE295207F0000:error:0A000126:SSL routines:ssl3_read_n:unexpected eof while reading:../ssl/record/rec_layer_s3.c:308:

连接成功后,会看服务器返回 220 之前有好大一段的证书相关的内容,像我这样的业余人士,直接忽略就好了。
我们看到 smtp.qq.com 返回了 250-AUTH LOGIN PLAIN XOAUTH XOAUTH2,表明它提供 4 种身份验证服务,在这里身份验证是必须的,不登陆不能发送邮件,会被拒绝,而且要求发件人和登陆的用户必须为同一人
同样我们可以验证 25 和 587 端口都能够连接,提供的服务也是相同的。

而 mx3.qq.com 则不提供身份验证服务,个人用户如果跑到这里来发邮件,人家可不会给你好脸色。在这里,它会对发邮件的邮局进行资质认证,以确保不是冒名邮件或垃圾邮件,同时要求收件人必须时本邮局的客户。

腾讯云解封 25 端口的条件,简单来说,腾讯云允许你作为一个普通用户去寄信,而不能提供邮箱服务。也就是说,你可以连接 smtp.qq.com,而 smtp.qq.com 支持 465 和 587 端口,没有开 25 端口的必要。你不可以连接 mx3.qq.com,即便是开了 25 端口也不可以。

所以,解封 25 端口这条路也被堵死了。

上次的解决方案中我们安装了 xinput 来解决问题,但还是存在疑问:为什么新安装的系统中会丢失 xinput 组件?以及为什么没有 xinput 的情况下 gdm3 可以正常启动?
让我们做个实验,删除 xinput 然后写一个脚本来替代它:

$ sudo apt remove xinput
$ sudo vim /usr/bin/xinput
# !/bin/sh
exit 0;
$ sudo chmod +x /usr/bin/xinput
$ sudo systemctl restart lightdm

一切正常,进入桌面没有发现任何异常,可见 xinput 已经被废弃了,而 unity-settings-daemon 源码没有及时更新。

在上面的解决方案中,我们实现一个什么事情都不做,仅仅返回 0 的程序。其实在我们的系统中就有这样一个程序,完全不需要自己写一个,那就是 /usr/bin/true,让我们来试一下:

$ sudo rm /usr/bin/xinput
$ sudo ln -s true /usr/bin/xinput
$ sudo systemctl restart lightdm

完美替代。

Ubuntu Unity 22.04 用了一段时间,发现系统休眠时风扇声音特别大,走近一看黑屏了,还没等看清楚屏幕上写的是什么,屏幕又恢复成了登陆的界面,登录进入系统一切正常。
虽然说不影响使用,但风扇总这么转也不环保不是?听着风扇转的声音我有点心疼家里的电费和我的电脑。终于在一次黑屏时抓拍到了屏幕的画面。
休眠时黑屏.jpg
这回终于可以从容的辨认屏幕上的文字了,如下:

[42772.370565] Bluetooth: hci0: SSR or FM download time out
[42772.370573] PM: dpm_run_callback(): acpi_subsys_suspend+0x0/0x60 returns -110
[42772.370592] hci_uart_qca serial0-0: PM: failed to suspend: error -110
[42772.370600] PM: Some devices failed to suspend, or early wake event detected

虽然还是看不懂为什么,但好像和蓝牙有关,正好目前还用不到蓝牙的功能,关闭好了。
蓝牙关.jpg

完美解决。