Posts in Category: OperatingSystem

Telegram 专用代理服务器 MTProxy 搭建

近日,Telegram 在俄罗斯遭封锁事件催生了基于其自有加密协议 MTProto 的代理服务器 MTProxy,官方源码使用 C 语言编写,托管于 TelegramMessenger/MTProxy。我试着在自己的 VPS 上搭建了一个,虽然官方文档非常简陋,但还算顺利。

服务器端编译

首先在 VPS (Ubuntu 18.04) 上克隆服务器端源码:

$ cd ~/dev/
$ git clone https://github.com/TelegramMessenger/MTProxy.git
$ cd MTProxy

编译之前需要安装必要的工具链和开发库:

$ sudo apt install build-essential libssl-dev zlib1g-dev

然后在 MTProxy 源码的根目录下直接执行 make 即可,编译出的可执行文件是 objs/bin/mtproto-proxy

之前的 README.md 没有说明需要安装的库文件,make 时在链接阶段有如下错误:

/usr/bin/x86_64-linux-gnu-ld: cannot find -lz

就是因为没有安装 zlib1g-dev 导致的。

服务器端运行

为简化说明,直接在编译结果目录中操作:

$ cd objs/bin/

获取 AES 密钥用于连接 Telegram 服务器:

$ curl -s https://core.telegram.org/getProxySecret -o proxy-secret

来看看密钥长哪样:

获取 Telegram 服务器列表及配置(不定时更新,官方推荐每天重新获取一次):

$ curl -s https://core.telegram.org/getProxyConfig -o proxy-multi.conf

Telegram 服务器列表和配置长这样:

$ cat proxy-multi.conf
# force_probability 1 10
proxy_for 1 149.154.175.50:8888;
proxy_for -1 149.154.175.50:8888;
proxy_for 2 149.154.162.38:80;
proxy_for 2 149.154.162.32:80;
proxy_for -2 149.154.162.38:80;
proxy_for -2 149.154.162.32:80;
proxy_for 3 149.154.175.100:8888;
proxy_for -3 149.154.175.100:8888;
proxy_for 4 91.108.4.200:8888;
proxy_for 4 91.108.4.212:8888;
proxy_for 4 91.108.4.207:8888;
proxy_for 4 91.108.4.168:8888;
proxy_for 4 91.108.4.167:8888;
proxy_for 4 91.108.4.138:8888;
proxy_for 4 91.108.4.191:8888;
proxy_for 4 91.108.4.136:8888;
proxy_for 4 91.108.4.172:8888;
proxy_for 4 91.108.4.156:8888;
proxy_for -4 149.154.166.120:8888;
proxy_for -4 149.154.165.109:8888;
proxy_for 5 91.108.56.163:8888;
proxy_for 5 91.108.56.118:8888;
proxy_for -5 91.108.56.163:8888;
proxy_for -5 91.108.56.118:8888;

生成密钥用于客户端连接代理服务器:

$ head -c 16 /dev/urandom | xxd -ps
de731d1e955150d03ae40579efdfb3a3

记下这个密钥,后面还会多次用到。再以后台方式运行 mtproto-proxy

$ ./mtproto-proxy -u nobody -p 8888 -H 9527 -S de731d1e955150d03ae40579efdfb3a3 –aes-pwd proxy-secret proxy-multi.conf -M 0 &

其中的几个参数说明如下:

  • nobody 为用户名,mtproto-proxy 调用 setuid() 以放弃权限。
  • 8888 为本地端口,用于获取统计数据。
  • 9527 为代理服务器端口,客户端使用此端口与代理服务器连接。
  • de731d1e955150d03ae40579efdfb3a3 为此前生成的密钥,同样用于客户端。也可同时指定多个密钥:-S <secret1> -S <secret2>
  • proxy-secret 为此前获取的用于连接 Telegram 服务器的 AES 密钥。
  • proxy-multi.conf 为此前获取的 Telegram 服务器列表及配置。
  • -M 参数指定除主线程之外的工作线程数目,此处指定为 0,仅用主线程。

看到主线程及工作线程(如果有)都进入 main loop 之后,表示代理服务器开始工作了:

[31648][2018-06-07 19:37:54.546575 local] Invoking engine mtproxy-0.01 compiled at Jun 7 2018 19:03:31 by gcc 7.3.0 64-bit after commit 580909cbca12a2f8529dbb387edf8e9bc5bd4e3a
[31648][2018-06-07 19:37:54.546833 local] config_filename = ‘proxy-multi.conf’
[31648][2018-06-07 19:37:54.549856 local] Started as [X.X.X.X:8888:31648:1528371474]
[31648][2018-06-07 19:37:54.550720 local] configuration file proxy-multi.conf re-read successfully (797 bytes parsed), new configuration active
[31648][2018-06-07 19:37:54.551195 local] main loop

假如你的 VPS 开启了防火墙,别忘了允许对应的 TCP 端口接入,例如:

$ sudo iptables -A INPUT -p tcp –dport 9527 -j ACCEPT

此外,除了 TelegramMessenger/MTProxy 之外,官方还提供了 Docker 的方式搭建代理服务器。另有第三方的 Python、PHP 等其它语言的实现,详见 p1ratrulezzz/mtproxy-server-linux

服务器注册及分享

此步骤非必须,但出于互联网分享精神,建议有条件的同学注册并分享自己的代理服务器,同时可通过设置「赞助频道(sponsored channel)」的方式获益。

服务器注册步骤如下:

  • 添加官方机器人 @MTProxybot
  • 发送 /newproxy
  • 根据提示发送代理服务器地址及端口号: lancitou.net:9527
  • 再根据提示发送此前生成的密钥: de731d1e955150d03ae40579efdfb3a3
  • 注册完成

设置赞助频道的方法:

  • 发送 /myproxies,将列出你注册过的代理服务器列表
  • 选择要设置的代理服务器: lancitou.net XXXXXXXX(此处并非以端口号型号列出,而是在注册成功后生成的十六进制标签,形如 850594e9…,请注意不要与密钥混淆)
  • 再选择 Set promotion
  • 根据提示发送赞助频道的链接或用户名: https://t.me/waytocrypto@WayToCrypto
  • 设置完成

此后所有连接到这个代理服务器的客户端,都会在聊天主界面置顶显示对应的赞助频道了。

客户端设置

为支持 MTProxy,首先需要把客户端升级到最新版本(具体哪个版本开始支持的无从获知)。添加代理服务器有手动添加和自动添加两种方法。

手动添加

仅以 iOS 客户端为例。首次设置代理的入口比较隐蔽,在 Settings -> Data and Storage -> Use Proxy,点击 Add Proxy 并选择 MTProto 类型,填上代理服务器的 IP 或域名、端口号及密钥:

此后在 Settings 页面会出现 Proxy 直达入口,所有曾经连接过的代理服务器都列于此,并且会自动检测是否可用、延时大小等信息。

自动添加

在注册服务器阶段生成的两个链接就可以用于自动添加代理服务器,其中第一个链接是标准公网链接;第二个链接为 URL Scheme 链接,在安装了 Telegram app 的设备上才能使用:

以上。

Linux 定时器的正确打开姿势

最近上层子系统使用我们封装的定时器时,发现定时不准确,比实时时间慢了一些。本文记录定位过程及解决方法。

使用定时器的一般步骤

Linux 下使用定时器的一般步骤如下:

(1) 使用 timer_create() 创建定时器

struct sigevent evp;

memset(&evp, 0, sizeof(struct sigevent));
evp.sigev_notify = SIGEV_SIGNAL;
evp.sigev_signo = timer_no;

if (timer_create(CLOCK_REALTIME, &evp, &tTimer[timer_no].timer) < 0)
{
return -1;
}

return 0;

其中的 timer_no 为定时器编号,tTimer 为事先定义的一个定时器结构体。刚创建的定时器不会自动运行。

(2) 使用 timer_settime() 设置定时器超时时间

timer_settime(tTimer[timer_no].timer, 0, &tTimer[timer_no].timevalue, NULL);

设置之后定时器立即开始运行。

(3) 在定时器线程中使用 sigwait() 等待定时器超时

sigset_t tsigmask;
int isigrcv, i;
long ret;

sigemptyset(&tsigmask);
for (i = 0; i < NUM_OF_TIMERS; i++)
{
sigaddset(&tsigmask, SIGRTMAX – i);
}

while (1)
{
ret = sigwait(&tsigmask, &isigrcv);
if (ret >= 0)
{
/* 调用定时器回调函数 */
}
}

查看内核的 date 是否准确

首先从源头开始,验证内核的实时时钟是否准确,借助 date 命令拷机实现。

先输入一次 date 命令,拷机一段时间之后,再输入一次 date 命令,通过对比两次命令输出的时间差与 SecureCRT 两条 log 记录的时间差,确认内核实时时钟准确无误。

(11:46:30.614) $ date
(11:46:30.645) Mon Jul 17 11:46:30 CST 2017
(13:48:02.798) $ date
(13:48:02.798) Mon Jul 17 13:48:02 CST 2017

查看用户态和内核的定时器计数是否一致

先在用户态的定时器线程的回调函数中,增加计数,定时器每超时一次就累加 1。

然后打开内核的 CONFIG_TIMER_STATS 配置项,重新编译内核并运行后,执行如下命令打开定时器统计:

echo 1 > /proc/timer_stats

之后使用如下命令查看所有定时器的计数信息等:

cat /proc/timer_stats

输出的信息类似如下:

Timer Stats Version: v0.2
Sample period: 55521.903 s

14205534, 1501 linux.out .common_timer_set (posix_timer_fn)

389290862 total events, 7011.482 events/sec

其中 linux.out 那行就是上层子系统所使用的定时器,超时计数为 14205534。而在应用代码中的计数为 14205535,比 timer_stats 的计数多了 1。且再次开启关闭一次定时器(同样通过调用 timer_settime() 实现),应用代码中的计数比 timer_stats 的计数多了 2,如此递增。此为疑点。

发现定时器使用时的问题

重新走查用户态中定时器相关代码,发现了问题所在。

在使用 timer_create() 创建定时器之后,设置 timer_settime() 时,对于第三个入参 new_value,只将超时时间赋值给了 it_valueit_interval 设置为 0。

参考 timer_settime(2) 中关于 it_valueit_interval 的说明:

If new_value->it_value specifies a nonzero value (i.e., either subfield is nonzero), then timer_settime() arms (starts) the timer, setting it to initially expire at the given time. (If the timer was already armed, then the previous settings are overwritten.) If new_value->it_value specifies a zero value (i.e., both subfields are zero), then the timer is disarmed.

The new_value->it_interval field specifies the period of the timer, in seconds and nanoseconds. If this field is nonzero, then each time that an armed timer expires, the timer is reloaded from the value specified in new_value->it_interval. If new_value->it_interval specifies a zero value then the timer expires just once, at the time specified by it_value.

也就是说,it_value 确定第一次超时时间,it_interval 确定后续的超时时间。那么现有的代码中如何实现定时器多次执行的呢?

原来是在定时器线程的 while 循环部分再次调用 timer_settime() 重新设置一次 it_value

while (1)
{
ret = sigwait(&tsigmask, &isigrcv);
if (ret >= 0)
{
/* 调用定时器回调函数 */
tTimer[timer_no].clkCallback(tTimer[timer_no].argCall);
timer_settime(tTimer[timer_no].timer, 0, &tTimer[timer_no].timevalue, NULL);
}
}

那么在定时器此次超时到再次调用 timer_settime() 启动定时器之间,存在一定的延迟而引入误差,长时间运行之后此误差将累积,导致定时器比实时时间慢。

这也可解释为什么启动停止定时器会导致用户态的计数比内核的计数多 1 的现象。内核的定时器已经超时停止,但用户态回调最后还会累加 1。

解决方法

  • 在调用 timer_settime() 时,同时设置 it_valueit_interval 的值,使定时器自动重新加载并循环运行。
  • 在定时器回调函数中,去掉调用 timer_settime() 重新设置定时器超时时间的代码。仅保留调用上层子系统挂载的回调函数即可。

这才是 Linux 定时器的正确打开姿势。

以上。

mmap 失败并返回 -EPERM 错误问题

问题现象

在我们的应用系统初始化过程中,用户态程序会读取 /proc/task_info 第一个字段的值作为物理地址传入 mmap/dev/mem 做内存映射。/proc/task_info 的内容如下:

$ cat /proc/task_info
0x81b0000 0xd555555555555555 0x5

也就是将 0x81b0000 (每次运行值可能不同)作为物理地址传入 mmap,此时返回 errno 为 -1,表示无权限:

#define EPERM 1 /* Operation not permitted */

源码分析

内存设备节点 /dev/mem 作为可随机读写的字符设备,在内核源码的 drivers/char/mem.c 中初始化(/dev/kmem, /dev/null, /dev/zero, /dev/random, /dev/urandom 等设备节点也都在这个文件中定义),对 /dev/memmmap 操作定义为 mmap_mem()。在 mmap_mem() 中返回 -EPERM 的代码片段如下:

if (!range_is_allowed(vma->vm_pgoff, size))
return -EPERM;

当定义了 CONFIG_STRICT_DEVMEM 时,在 range_is_allowed() 中会逐页调用 devmem_is_allowed() 检查是否可访问:

static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
u64 from = ((u64)pfn) << PAGE_SHIFT;
u64 to = from + size;
u64 cursor = from;

while (cursor < to) {
if (!devmem_is_allowed(pfn)) {
printk(KERN_INFO
“Program %s tried to access /dev/mem between %Lx->%Lx.\n”,
current->comm, from, to);
return 0;
}
cursor += PAGE_SIZE;
pfn++;
}
return 1;
}

devmem_is_allowed() 由各个体系结构实现,以 PowerPC 为例,在 arch/powerpc/mm/mem.c 中,同样也是定义了 CONFIG_STRICT_DEVMEM 才有实现:

int devmem_is_allowed(unsigned long pfn)
{
if (iomem_is_exclusive(pfn << PAGE_SHIFT))
return 0;
if (!page_is_ram(pfn))
return 1;
if (page_is_rtas_user_buf(pfn))
return 1;
return 0;
}

devmem_is_allowed() 返回 1 表示允许访问,0 表示不允许访问。其中第一个 if 语句调用的 iomem_is_exclusive() 是用于判断 PCI 的内存空间(PCI mem)是否互斥访问的。由于我们访问的是内核生成的 /proc/task_info,与 PCI 无关,因此 mmap 失败是在 devmem_is_allowed() 最后的 return 0 返回了不允许访问。

解决方法

方法一:修改 devmem_is_allowed() 函数

devmem_is_allowed() 函数中,当访问的是 /proc/task_info 对应的页号时,返回 1 表示允许访问。

这种方法实现复杂,需要在生成 /proc/task_info 时保存其对应的页号(在 fs/proc/taskinfo.c 文件的 task_info_init() 函数中分配的页号);同时这个方法污染了内核代码,不具有通用性。

方法二:关闭 CONFIG_STRICT_DEVMEM 配置

从前面的说明中可以看出,几个检查函数都是在定义了 CONFIG_STRICT_DEVMEM 才生效的,因此最简单的方法,就是将内核配置中 CONFIG_STRICT_DEVMEM 选项关闭并重新编译内核,这也是我们的应用系统所采用的方法。

以上。

Windows 保存的共享目录认证信息如何删除?

调试 Samba 服务的用户登录时,需要在 Windows 下尝试使用不同的用户名和密码登录 Samba 共享目录。若在登录时选择了「记住密码」,要使用其它用户名登录时,就需要删除之前保存的认证信息。本文参考 ServerFault,在 Windows XP 和 Windows 7 分别验证并记录。

Windows XP

进入控制面板,双击「用户账户」,选择「高级」选项卡,再点击「管理密码」。在弹出的「存储用户名和密码」窗口中,选择对应 Samba 服务器 IP 的一项删除即可。

管理用户名和密码

Windows 7

进入控制面板,双击「管理工具」,「服务」,找到「Workstation」,右键选择「重新启动」即可。

以上。