Aug 19

人们对 TCP 的误解

因为我们的教育总是只教人「怎么做」,而根本不管「为什么这么做」,所以造成了很多误解。

今天(恰巧是今天)看到有人在 SegmentFault 上问「TCP server 为什么一个端口可以建立多个连接?」。提问者认为 client 端就不能使用相同的本地端口了。理论上来说,确定一条链路,只要五元组(源IP、源端口号、目标IP、目标端口号、协议)唯一就可以了,所以这不应该是技术限制。而实际上,Linux 3.9 之后确实可以让客户端使用相同的地址来连接不同的目标,只不过要提前跟内核说好而已。

当然,你不能使用同一个 socket,不然调用connect连接的时候会报错:

[Errno 106] (EISCONN) Transport endpoint is already connected

man 2 connect里说了:

Generally, connection-based protocol sockets may successfully connect() only once; connectionless protocol sockets may use connect() multiple times to change their association.

想也是,一个 socket 连接到多个目标,那发送的时候到底发给谁呢?TCP 又不像 UDP 那样无状态的,以前做过什么根本不管。

那用多个 socket 就可以了嘛。服务端其实也一直是用多个 socket 来处理多个连接的不是么,每次accept都生成个新的 socket。

>>> import socket
>>> s = socket.socket()
# since Linux 3.9, 见 man 7 socket
>>> s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
>>> s2 = socket.socket()
>>> s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
>>> s.bind(('127.0.0.1', 12345))
>>> s2.bind(('127.0.0.1', 12345))
# 都可以使用同一本地地址来连接哦
>>> s.connect(('127.0.0.1', 80))
>>> s2.connect(('127.0.0.1', 4321))

连上去之后 netstat 的输出(4568 进程是上边这个程序,另两个进程一个是 nginx,另一个是我的另一个 Python 程序):

>>> netstat -npt | grep 12345
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
tcp        0      0 127.0.0.1:4321          127.0.0.1:12345         ESTABLISHED 18284/python3
tcp        0      0 127.0.0.1:12345         127.0.0.1:4321          ESTABLISHED 4568/python3
tcp        0      0 127.0.0.1:80            127.0.0.1:12345         ESTABLISHED -
tcp        0      0 127.0.0.1:12345         127.0.0.1:80            ESTABLISHED 4568/python3

当然你要是连接相同的地址会报错的:

OSError: [Errno 99] Cannot assign requested address

那个五元组已经被占用啦。

同时创建连接:恰巧你也在这里

有时候,我们不能一个劲地等待。主动出击也是可以的,即便对方并没有在等待。

这个在 TCP 里叫「simultaneous open」,用于 TCP 打洞。但是比起 UDP 打洞难多了,因为那个「simultaneous」字眼:必须同时调用connect,双方的 SYN 包要交叉,早了或者晚了都是会被拒绝的。

所以手工就办不到啦,在本地测试也不容易办到。我本地的系统时间是使用 NTP 同步的,再用一个时钟也和 NTP 同步的 VPS 就可以啦,我这里延迟 80ms 左右,足够那两个 SYN 「在空中会面」了。以下是代码:

#!/usr/bin/env python3

import time
import sys
import socket
import datetime

def wait_until(t):
  deadline = t.timestamp()
  to_wait = deadline - time.time()
  time.sleep(to_wait)

s = socket.socket()
s.bind(('', 1314))

if sys.argv[1] == 'local':
  ip = 'VPS 的地址'
else:
  ip = '我的地址'

t = datetime.datetime(2015, 8, 19, 22, 14, 30)
wait_until(t)
s.connect((ip, 1314))

s.send(b'I love you.')
print(s.recv(1024))

当然,我是公网 IP。在内网里包就不容易进来啦。

然后双方在约定的时间之前跑起来即可,结果是这样子的:

# 本地
>>> python3 t.py local
b'I love you.'

# VPS 上
>>> python3 t.py remote
b'I love you.'

一个人也可以建立 TCP 连接呢

如果你没有 VPS,或者没有公网 IP,也是有活动可以参与的哦。即使只有一个 socket,也可以自己连接到自己的:

>>> import socket                                                               
>>> s = socket.socket()
>>> s.bind(('127.0.0.1', 1314))
>>> s.connect(('127.0.0.1', 1314))
>>> s.send(b'I love you.')
11
>>> s.recv(1024)
b'I love you.'

netstat 输出:

>>> netstat -npt | grep 1314
tcp        0      0 127.0.0.1:1314          127.0.0.1:1314          ESTABLISHED 8050/python  
Aug 19

这是以前的事情,突然联想到的。

「来武汉火车站接我。」

有些年没关注过武汉的火车站的人会认为要去某一个位于武汉市的火车站来接我。如果他曾经接在汉口火车站接过我,那么他会认为我的意思是我会在以前那个火车站等他,而不是到别的地方——比如某个公交站。因为汉口火车站位于武汉市内。

而我遇到这种情况,会去寻找一个叫「武汉火车站」的车站,即使最后找到唯一一个以此为名的火车站在孝感而不是武汉,或者唯一一个以此为名的是一个广场而不是火车站。因为我会假定说这话的人已经传达了完成请求的所需要的足够的信息,那么「武汉火车站」一定是一个明确的地点,而不是一个范围、一种描述。当这种假定失败的时候,我才会觉得对方办事不靠谱,转而寻求并确认更详细的信息。

可很多入门级的程序员,和一般的电脑使用者一样,并不能准确有效地传达信息,反而采用了一种有损的描述。就像我找你要天津大爆炸的现场图片,你打开看图软件,然后拿QQ截屏发送过来一样。

他们说,「网页打不开了」,「程序运行不了了」,似乎别人就坐在他们身边,看得到他们所看到的一切,也拥有读心术,能够探知他们所期望的结果是怎样的一样。然而真实情况是,你说你要死了,我却不知道怎么才能救你。

在日常生活中,用一个近似的概念来取代,或者重要信息缺失很少带来多少麻烦,因为大家的需求啊行为啊都差不多的,你要吃饭我也要吃饭,你要赶路我也急着上班。万一弄错了,你要调料给你递了纸巾也没多大关系,再说明白一点就好了。

可是编程不是日常生活,它需要严谨、认真的态度,不然事情就没办法处理。当然也有更严重的,比如耗资数亿、经历近一年的旅程之后坠毁的火星气候探测者号,比如导致73人丧生的哥伦比亚航空052号班机空难。这些事件中,如果信息传达得准确一点的话,悲剧就不会发生。

哦对了,如果中文维基百科的链接无法访问的话,请「科学上网」(这里的引号表示这是一个特殊的词组,需要准确匹配的),或者在 hosts 文件加入维基百科的 IP 198.35.26.96 即可。

Aug 14

我曾喜欢,或者有机会喜欢过很多东西,比如——

文学。可是我们没有文学课,只有毁文学的语文课。不考试的东西当然没什么同龄人喜欢啦。

语言学。大学时曾一度想选个二外,但是一直选不上。等终于能选上了,作息时间已经被同寝室的同学毁了,去不了了。

音乐。小时候没有资本喜欢。工作之后有了资本,但担心吵到邻居什么的,没成。

数学。被高三的题海击败了。整整一年,被试题累得觉都睡不好。而且没有同学有精力和我一起研究数学了。学校图书馆里也没多少有意思的书。

物理。没条件做各种实验。当然也同样缺乏信息资源。

广播。偷偷摸摸拿自己的钱去买下了镇上能找到的最好的收音机,好像是德生的。玩了几年,在各种唱戏的和「中国之声」的缝隙里找过好多国家的电台。后来它渐渐地坏掉了。有了网络之后,对广播也失去了兴趣。毕竟没有硬件资源来进一步地玩儿。

然而,有一样爱好却顽强地维持了下来:计算机软件。尽管当时每周接触的时间不到一小时,尽管只能忍受莫名其妙地把关机功能放在叫「开始」的菜单里的 Windows,尽管有墙带给我最初和父亲、老师主动接触时那样令人摸不着头脑的奇怪问题,我还是喜欢上了这扇通向外面的世界的窗户。

在期待中试过了八年,也在盼望中攒了八年钱,之后,终于在大一下学期时拥有了自己的电脑,终于可以进入一个自由的操作系统、一个自由的属于自己的世界。

在那个世界里我活得很好。即使是遭受重大的人生挫折时也不曾对那个世界感到疲惫,依旧会写代码,写博客。

可现在,我累了。

其实我有好些想写博客的材料,但是都没有写。我也有好些项目的想法,可是它们还在我的 TODO wiki 里。Arch Linux 中文社区还有许多要做的事情,可我也不想去做了。

我不知道是为什么。也许是孤单吧。一直以来都只有自己。即使 Arch Linux 社区,在做事的人也渐渐少了。而曾经在网上认识的朋友,渐渐地都有了自己的生活。随着 Google Hangout 取代 Google Talk、新浪微博取代 Twitter,还有微信,这些封闭的东西取代了自由的工具,可以放有用或者有意思的机器人的地方却越来越荒芜了。

也许是年纪渐渐大了。青春越来越少,而我却依旧在飘荡。

自己也不太敢接受太美好的事情。期望越大,失望也就越大。所以,宁愿折磨自己,也不要毁了自己。这一路走来,有好多人对我十分友好,给予我帮助和鼓励。可我却无以回报。我只是在逃避那些可能的美好。

我终于感受到了毫无方向的迷茫。没有人爱,没有想做的事情,也对未来没有了期待,连幻想都不想去想了。

Jul 31

ThinkPad 键盘上的第一行键现在默认在不按下Fn键时执行多媒体按键功能,按下Fn时才能执行F1-F12的功能,对于像我这种 Vimhtop 等的用户来说颇为不便。还好,BIOS 选项里可以改回来。

然后我遇到了 X250,发现InsertEnd键怎么也受那个选项的影响了!也就是,如果F1-F12设置得方便了,那么End键就需要Fn键配合。而HomeEnd这种光标移动键虽然很少用,但毕竟还是要用到的,比如在 htop、weechat、mutt 以及不支持自定义编辑键的 Qt 程序里的时候。

所以呢,我在~/.Xmodmap里把这两个键交换了:

keysym End = Insert
keysym Insert = End

这下子用笔记本上的键盘是没问题了。可是我用外接键盘的话,这两个键就又反过来了 Orz……

当然网上会有 udev 规则,在插上外设时跑个脚本什么的。可不管怎么映射,总有个键盘的按键是反的啊

仔细询问 Google 之后,在 Gentoo 的论坛里终于发现这么一条线索

To alter keymap of a particular keyboard you need to issue EVIOCSKEYCODE ioctl on corresponding /dev/input/eventX node.

所以,找到相应的 event 设备文件之后,只需要 ioctl 一下就可以了?可是EVIOCSKEYCODE是个什么鬼啊……

找过 manpages、头文件、内核文档、Google 之后,我不得不相信这个东西真的没文档!于是只好看源码了……还好有 LXR,不用在本地近一个G的源码里搜索。

所以,翻完文档又试验,最终有了这个程序:

#include<sys/ioctl.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<stdlib.h>
#include<linux/input.h>

#define EVENT_FILE "/dev/input/by-path/platform-i8042-serio-0-event-kbd"

int main(int argc, char **argv){
  unsigned int codes[][2] = {
    {0xd2, 107}, //Insert -> End
    {0xcf, 110}, //End -> Insert
    {0, 0},
  };
  int fd = open(EVENT_FILE, O_RDONLY);
  if(fd < 0) {
    perror("open " EVENT_FILE);
    exit(1);
  }

  unsigned int (*p)[2];
  for(p=codes; *p[0]; p++){
    if(ioctl(fd, EVIOCSKEYCODE, *p)) {
      perror("ioctl EVIOCSKEYCODE");
      exit(1);
    }
  }
  return 0;
}

那个EVENT_FILE当然就是笔记本键盘的节点啦,在/dev/input/by-path下很容易识别的。

编译之后,每次启动系统后执行一次就可以了(大概)。

对了,顺便说一下,找那些代码可以用 showkey 和 getkeycodes 之类的命令。当然我们有 setkeycodes,但是它不能为指定键盘单独设置。传进去的参数,第一个整数是 scancode,就是硬件上报的编码,第二个是 keycode,内核给键的编码,不同硬件的不同 scancode 可以对应同一个 keycode(比如本文所做的)。然后文本终端还有 keymap、X Window 还有键盘布局和 xmodmap,大概是把 keycode 映射到可读的键名。

Jul 26

曾经,我让 Awesome 收养孤儿进程,以保持一个清晰的进程树。后来我又想让 zsh 也做这个 prctl 系统调用,免得子进程 fork 之后跑太远。比如 Wine 跑起来就好多个服务进程,如果不能把它们全部关掉的话,再启动另一个版本的 Wine 会出问题的。而当我启动好些个不同版本的 Wine 环境之后,只看到 Awesome 下边挂了一堆 Wine 的进程,却不知道哪些属于哪个 Wine 环境的了。

zsh 本身并不支持做这个调用,不过如同 Python 和 Lua 一样,zsh 也可以通过共享库来扩展功能。不同的是,zsh 模块是没有文档的……好在 zsh 源码里提供了一个 example 示例模块。把它改改就有了以下代码:

把这两个文档保存到 zsh 源码目录的Src/Modules下,可能还需要编辑一下config.modules文件,然后编译就可得 subreap.so 文件。把这个文件放到/usr/lib/zsh/$ZSH_VERSION/zsh/subreap.so然后就可以用了:

zmodload zsh/subreap
subreap

模块加载之后,多了个subreap内建命令。不带参数即调用prctl(PR_SET_CHILD_SUBREAPER, 1),这样不管其子进程怎么 fork,都会在此 zsh 的进程树之下。使用subreap -u来取消这个设置。

如果你不想编译而又是 Linux 64 位系统,可以试试我编译好的版本:下载地址, 签名, SHA1: 09eb1cc9ebf6ec1e681641c0a60f57425cbb1e8c。

Jun 22

蓦然回首,竟又已度过三个春秋。

三年前的那日,大雨倾盆,北京看海。而我却在简陋的出租屋里,对网络另一端还没有见过面的女生表白了。从那一刻起,一切都变了。

二十多年前,父母老师们都说,好好上学,将来考上好大学就会幸福了。

七年前,在信息严重不足的情况下,我「如愿」进入了现今各项指标我都要打一星的武汉大学计算机科学与技术专业。从信息匮乏到信息过载,再到毕业前夕笨拙的面试。我不知道路将通向哪里,然而眼前有路,努力走下去吧。

于是我来到了北京。告别了没什么感觉的故乡,也告别了他们曾经的谎言,开始面对真实的世界了。

那时的世界还算美好。所以我依然缓缓地在那不知道通向哪里的道路上走着。直到遇见了她。直到以为自己遇见了幸福。然而,幸福只是对我笑笑,然后离开,留我在一个没有路的地方发呆。

每天依旧去公司上班。只是不再准时。只是每天都吃很少,也不再关心吃的是什么。只是经常代码写着写着跑到卫生间里哭一场。只是冰箱里的雪糕一天会多减少四五支。在北京空气污染最严重的日子里,我也毫无察觉。而有时过完马路,才发觉自己根本没看是否会被车辆撞上。那时我才开始害怕起来。

那段时间,买了新手机,Sony LT26i,很漂亮也挺好用。但是并不开心。公司组织去云南旅游。那也是至今唯一一次公司组织的旅游,也是自己唯一一次旅行。然而躺在电脑里的照片里并没有几张照片里有自己。自己记忆最深的是,车在山间的公路上行驶,我听着耳机里郭静的歌曲,望着窗外一座又一座的山发呆。因为她曾经跟我说,她的家乡在群山环绕之中。

窗外的山

这段时期,遇到了小百合 @koyyuri。可惜他已经永远地离开了,只留下我没有授受他的语音聊天邀请的遗憾。

也遇到了五分之一个地球之外的淑贞姐和远在台湾的依依。虽然远隔重洋,可在我失恋的日子里,她们给了我很大的帮助。很感谢她们。

后来临近交房租的日子,我突然很想离开。非常想,所以很快就离职了。也从此,找不到能让自己满意的工作了。没有获得多少有价值的工作经验。甚至自己做了什么,也大部分都忘掉了。

那年春节在家都做了什么我已经完全忘记了。只知道只有流量极其有限的3G网络很难受,所以随便接受了一家位于南京的初创公司的工作。

秦淮河畔,桃花盛开。很美,却少一人相伴。有次走近河边的时候,竟然突然有种跳下去的冲动。发觉之后赶紧离远了些,再也不敢靠太近。

秦淮河畔的桃花盛开

就这样过了一年半。其间又遇到了素蓝,和她那些明信片。她还给我寄了一张。不过我收到的时候,她已经从网上消失了。

素蓝寄来的明信片

南京这家小公司不仅一直没发布那个很难理解的产品,而且到后来人越来越少。在网线另一头的朋友们的帮助下,我也渐渐地从失恋的悲伤中走了出来。所以,是时候离开了。

于是就离开了南京。本打算回到武汉角落的家之后,再重振旗鼓,再一次求职。可事与愿违。我不该回到那个只有3G网络又充满噪音的地方的。休息不好,而又有来自父母的巨大压力,根本没法全力求职。

失望、疲惫、担忧,逼迫我再一次选择先离开再作打算。

是的,担忧。在父母身边,有好吃的饭菜,不用自己操心吃饭的事情。仅此而已。没有家的温暖,也没有安全感。面对楼上房东的噪音,父亲说他来解决,结果并没有什么效果。有一次我实在忍不住和对方交涉,父亲还表达了不满。

父亲就是这样的人,对自己人表现得很强势以至于暴力,而对外则软弱无力。小时候父亲带我在外边吃早餐。还没吃几口,转过身擤鼻涕,结果店主以为我们吃完,把一满碗粉给倒掉了。我不满意,而父亲只是一边安慰着我,一边付了钱。

另一次,去另一城市走亲戚。那时候可没有手机地图之类的高科技。于是我们找了很久,在几个地点之间来回转悠大半天。我们又不是去求他办什么事,说不清自己在哪里又不出来接人,还去拜访他干嘛。

所以我们家一直受父亲那边的亲戚欺负,然后我和母亲长期受父亲的欺负。具体情节可参考《不要跟陌生人说话》,我就不展开叙述了。

那些日子里我经常会担忧,特别是在父亲喝酒之后。毕竟他是体力劳动者,我还无法保障自己的安全。后来为了安全起见,我每次回家前都会计划好,如果出事我要带上哪些必要的物品,选择哪条路线逃离。

母亲好一些,但也并不让我安心。有一次,她没有跟我说就去「帮我」整理行李箱。也许很多人都觉得这没什么,最多找东西时不容易找到了。但是我和母亲之间虽然没什么矛盾,却也并没有多少亲密。更何况,我还怕多年来一直隐瞒着父母的秘密被发现之后可能会发生的麻烦又危险的后果。

所以再一次地「不选而择」,来到了北京又一家创业公司。

工资恢复到了正常水平,工作时间恢复了正常,公司产品也不再是难以理解的东西。仅此而已。我对公司所做的产品没有兴趣,自己的工作内容更多的是实现需求,而不是做很多程序员做不到或者做不好的事情。新网站上线的时候,还把我吓到了,因为在我看来那根本还没有达到可以上线的完成度和品质。(所以那一整天我们都忙着修 bug。)

期间又试过阿里和百度。依旧被拒了。没有他们想要的工作经验,而我也不再是应届生。当然另一个原因应该是我不会把自己做过的东西吹得多么牛逼。我只会实事求是,可不会无中生有。为了工作机会而去学习没有意思的技能这种事,能避免就尽量避免吧。毕竟这世界上有意思的事情太多了,有那个精力把时间花在没意思的事情上太不值得。

对阿里失望之后,我也考虑了很偶然遇见的蘑菇街。然而从JD上我并不能确定他们的职位是否适合我。所以发信询问,后来发现可以在新浪微博上私信他们之后也私信询问过。可得到的信息只有他们已经收到我的信息了,自动的以及人工的。并没有得到自己需要的信息。后来实在等不下去了,投简历过去。那时受朋友引荐,去新浪面试过后,感觉还不错。至少对我来说总算是找到了一个愿意收留我、我也感兴趣的地方。后来蘑菇街说是他们CTO看到了我的简历,准备约面试。直到那时,我还是没能得到自己想要的信息,对他们所提供的职位还充满着不确定。而另一边,新浪的职位却非常确定。而有事经过故事开始的地方,发现自己已经彻底远离了那些事情的影响。所以最后我选择了留在北京,选择了认可自己的新浪。

三年过去了。我更加了解自己了,其实早就该如此。中国教育太关注分数,我虽然能够逃离一些,真正学得了一些知识,锻炼了考试之外的能力,然而淹没在题海之中的,除了更好的分数之外,还有对自己的忽视。是的,高三时的题海战术使我的成绩下降了不少。太累了,身体上和精神上都累,又学不到新东西了,所以成绩会下降,不然我去华科肯定不会有问题的。

三年过去了。经历过第一次幸福、第一次安全感,也经历过第一次整个世界毁灭的痛苦。

我知道人间路曲折不好走
也知道人间事沧桑不好受
但是花开一季 人生一世
累又算什么 苦又算什么
人 就只有这么这一辈子
总要风经过 雨来过
痛过 也哭过
才能在岁月的门后
把那些心酸 当作笑谈说

然而这些本该是数年前就会发生的事情。父母的影响和中国教育对分数的疯狂追求延误了青春的发生。

三年过去了。然而我还是没做出什么很多人都知道的开源项目,反而是更忙了,在前端各种技术喷涌而出的时候对新技术的关注反而更少了。也没取得多少对以后的工作有利的经验。本来能力就不等同于经验,只是很多人并不懂。当一个人经常用正确的方法做事时,没有走过那么多弯路的经验,她还是可以很顺利地完成任务,而不会需要先把所有错误都犯一遍。比如我并不需要像下厨房那样实际丢失过数据,才知道当误删文件发生时该怎么做。

这三年来,我最大的收获并不是多了三年的工作经验,因为那实际上不重要,也没有多少实质内容。这三年来,我在人生的道路上被逼着奔跑,以挽回自己被错过的时光。我一定要好好照顾自己,争取多活三年,这样我就可以当这三年并没有存在过了。虽然别人并不会这么看,但我也不会再为别人而活了。

只是,我已经不再年轻,也不再优秀了。

中国大陆用户:

墙外用户:

Jun 21

不用 sudo 也可以跑 LXC 虚拟机啦。使用 root 权限的 LXC 虚拟机,里边的 root 权限就是真实的 root 权限,虽然不太能够跑出来。而利用用户命名空间来启动的普通权限的 LXC 虚拟机则只在那个虚拟机里有 root 权限,从外面看跟一普通用户一样的。

首先需要一枚启用了CONFIG_USER_NS的内核。使用以下命令查看:

zgrep USER_NS /proc/config.gz

部分发行版会默认禁用用户命名空间功能,需要手动启用,参见 vagga 的安装文档。而 Arch Linux 不喜欢给软件打补丁,而这个特性又被认为是不安全的,所以并没有启用。当然这并不妨碍自己编译一个启用了这个特性的内核啦,比如 linux-lily 从 4.0.1 开始启用此特性。

注意:这个特性被认为不安全的,会时不时地爆出个提权漏洞(比如前不久这个),请谨慎启用。

内核支持没问题的话就可以开始配置了。以下配置过程主要参考 Arch Linux 论坛里的这篇帖子

首先给自己配置一些子 UID 和子 GID,也就是自己的分身。我在/etc/subuid/etc/subgid内写下如下内容

lilydjwg:100000:65536

意思是说,我(lilydjwg)被授权使用从 100000 开始的 65536 个 UID 和 GID。这一步是需要 root 权限的。这个配置好之后就可以创建用户命名空间了,比如:

lxc-usernsexec -m u:0:100000:1 -m g:0:1000:1 -- /bin/zsh

此命令是说,创建一个用户命名空间,其中 UID 从 0 开始,实际对应于外边 100000 开始的 UID,总共分配一个;GID 从 0 开始,实际对应于外边 1000 开始的 GID,总共分配一个。执行之后可以看到新启动的 zsh 已经是 root 权限了。不过cat /etc/shadow就会发现还是没权限 :-D 在里边 touch 个文件的话,在外边看会是 UID 为 100000 的用户创建的。我之所以要指定 GID 的映射,是因为我的 HOME 目录外人读不了的。为了加载 zsh 的配置,就把自己的 GID 映射给它了。

当然我也可以把自己的真实 UID 映射过去,这样子除了被里边的进程自认为有 root 权限之外没什么别的差异。用户命名空间要配合别的命名空间一起用才有意思。

然后要配置一下 cgroup,不然 lxc 会报错的。这一步也是需要 root 权限的。

echo 1 | sudo tee /sys/fs/cgroup/cpuset/cgroup.clone_children

for d in /sys/fs/cgroup/*; do
    sudo mkdir $d/$USER
    sudo chown -R $USER: $d/$USER
done

用处后边再说。

虚拟机里的网络是分开的。默认是没有网络的。想要的话得先授权,向/etc/lxc/lxc-usernet文件里写入

lilydjwg veth br0 10

其中br0是桥接用的网络接口名。没有就自己建一个:

brctl addbr br0
ifconfig br0 192.168.57.1
iptables -t nat -A POSTROUTING -s 192.168.57.1/24 -j MASQUERADE

这些当然也是需要 root 权限的。

还要告诉 LXC 使用用户命名空间:在~/.config/lxc/default.conf写入:

 lxc.include = /etc/lxc/default.conf
 lxc.id_map = u 0 100000 65536
 lxc.id_map = g 0 100000 65536

然后,去弄一个 LXC 系统镜像吧:

lxc-create -t download -n lxcname

名字自己起。这个命令会让你选择你要的发行版和版本的。这一步不需要 root 权限了。镜像文件列表可以看这里

等它跑完之后新的 LXC 虚拟机的 root 文件系统已经就绪了。不过在启动它之前先去编辑一下它的配置文件,加入网络配置。默认它位于~/.local/share/lxc下与 LXC 虚拟机同名的目录下。

在配置文件里加上

lxc.network.type = veth
lxc.network.link = br0
lxc.network.flags = up
lxc.network.ipv4 = 192.168.57.4
lxc.network.name = eth0

在启动之前还要做一件事——将当前进程加入到之前创建的 cgroup 中:

for d in /sys/fs/cgroup/*; do echo $$ > $d/$USER/tasks; done

然后就可以启动 LXC 虚拟机啦。当然是不需要 root 权限的:

lxc-start -F -n lxcname

当然,得给里边的 root 用户设置一个密码,不然登录不了的。可以使用 lxc-usernsexec 来 chroot 过去:

lxc-usernsexec -- chroot rootfs /bin/bash
Jun 7

首先安装 bluez 包。我用的版本是 5.30。其次安装 blueman。

启动蓝牙服务:

systemctl start bluetooth.service

然后使用 blueman-manager 之类的命令启动 blueman。这时会在系统托盘看到蓝牙图标。点右键选择「添加新设备…」,完成配对。

然后,如果是要往手机发文件的话,是没有问题的,但是收的话,会失败。原因是,默认接收文件前会先询问用户要不要接收,而 blueman 不知道怎么搞的根本没反应……

解决方案是:直接启动一个默认接收文件的 obexd 就好了:

killall obexd
/usr/lib/bluetooth/obexd -r tmpfs -n -a

-r指定收到的文件存哪里,默认是 $XDG_CACHE_DIR 下的 obexd 目录,即默认是 ~/.cache/obexd。这里的路径是相对于用户主目录的。

-n是不要以守护模式运行,会把日志输出到终端而不是系统日志。

-a就是重点——接收所有文件——了。

obex 这套东西的文档在/usr/share/doc/bluez/dbus-apis/下有。

Linux 下遇到点问题还真是折腾,声称完成某一功能的软件一大堆,结果装好了,要么根本不知道怎么用(gnome-bluetooth、bluedevil),要么适用版本不匹配(obexpushd、ArchWiki 等网上的过时信息),要么有 bug 用不了(blueman)。

不过好的一点是,不涉及闭源的软件和协议,而又有足够的时间和能力的话,问题总是能够解决的。不像 Windows 或者 Android,遇到问题两眼一摸黑,只能不断地重试和重装,看看人品会不会爆发一下。

最后,折腾好久终于传输成功的照片:

雨后彩虹

北京好不容易下了场大雨,没想到雨后还出现了彩虹~

Jun 1

(失眠了,干脆起来写文。)

调试时经常会有抓包的需求。通常,我在本地用图形界面的 Wireshark 来抓包及解析,而对于远程服务器,因为没有图形界面,只好使用 tcpdump 抓包到文件然后复制到本地拿 Wireshark 看了,这样就不能实时查看抓到的包了。当然 tcpdump 也可以实时输出,但是信息太少、难以阅读,功能也过于简单,比如我要跟踪流啊、不同的流用不同的颜色高亮啊、添加注释啊、时序分析啊,tcpdump 完全没办法做到。实际上复杂一点的协议解析它都做不到。

一直没去研究 Wireshark 如何从标准输入读取网络包数据。大概是某天下意识地按了一下Alt-h看到了 Wireshaark 的 man 手册,才知道原来 Wireshark 支持这么多参数!图形界面的程序支持各种可选参数的可不多见。Wireshark 指定-i -就可以从标准输入读取数据,不过要同时指定-k,不然得在图形界面里点「Start」开始抓包。

那怎么把抓到的数据包发送到标准输出呢?实际的抓包操作不是 Wireshark 直接执行的。Wireshark 又不是 360,既然能以普通用户身份执行需要特权的操作,那么就会有一个无图形界面的工具来辅助。它就是 dumpcap。查阅其 man 手册可知,把抓到的数据输出到标准输出的选项是-w - -P-P 指定使用 pcap 格式,不然会使用 pcap-ng 格式,Wireshark 不认)。还可以给定其它选项,比如只抓 lo 网络设备上的包用-i lo,或者指定一个过滤器如-f 'port 1234'(具体语法见pcap-filter的 man 手册)。一定要记住不要把传输抓包数据的数据包也抓到了哦~

比如:

ssh lxc-debian sudo dumpcap -P -w - -f "'not port 22'" | \
  wireshark -i - -k

这样就可以实时看到远程主机上的网络包了~通过 ssh 执行命令时引号得用双层的。使用 sudo 是因为我那个 Debian 的 dumpcap 没有特权。

May 30

有个需求,保存一个网页里的所有图片。

看上去是件简单的事情,拿火狐DownThemAll 扩展下载不就好了么。

然后发现那个网页仅限移动版访问。好吧,装个 UserAgent Switcher。然后发现它是通过 JavaScript 检测 UA 的,而 UserAgent Switcher 只改了 HTTP 头里的 UA。好吧,换个 muzuiget 的 User Agent Overrider。然后发现那些图片是动态加载的,DownThemAll 根本看不到地址。后来知道「查看网页信息」的「媒体」选项卡里也是可以保存图片的,不过那里显示的图片也不全……

于是我怒了,放弃继续尝试不同的工具,决定用程序员的方式来解决问题。

我管你怎么加载的,你总归是要从网络上下载图片不是么?那我就拿个代理把你访问过的所有图片全部保存下来好了 :-)

打开 mitmproxy 文档页,发现并没有现成的保存文件的功能。但是没关系,可以写脚本。看看示例,迅速写了以下不到二十行代码:

#!/usr/bin/mitmdump -s

from __future__ import print_function

import os
from urlparse import urlsplit

from libmproxy.protocol.http import decoded

def response(context, flow):
  with decoded(flow.response):
    if flow.response.headers['Content-Type'][0].startswith('image/'):
      url = urlsplit(flow.request.url)
      name = os.path.basename(url.path)
      with open(name, 'wb') as f:
        f.write(flow.response.content)
      print(name, 'written')

当然这是最终结果。不过和初版差别不大,毕竟就这么点儿代码。思路也很简单,凡是经过代理的图片都存起来。有点粗暴,但是好用。

代理脚本跑起来。然后启动一个全新的 Google Chrome,一个没有任何缓存存在的实例:

google-chrome-stable --proxy-server=http://localhost:8080 --user-data-dir=new

访问目标页面,启用移动版模拟并刷新,就可以看到各种图片都被保存下来了~~