配置 Nginx 子域名的泛解析

简单记录:

需求描述

Web 服务器为 Nginx,希望配置泛子域名解析。 其实稍加修改,配置泛域名解析也不是难事。

  • 不影响主域名,domain.com 和 www.domain.com 的 root 依然为 www 目录
  • 子域名 sub.domain.com 的 root 希望放在 www-sub 目录下,其他子域名同理

解决及分析

在 Nginx 的配置文件中做如下配置(示意):

    server {
        server_name
            domain.com
            www.domain.com
            *.domain.com
        ;
 
        set $subdomain '';
        if ($host ~* (\b(?!www\b).+)\.domain.com) {
            set $subdomain -$1;
        }
 
        root    /home/user/www$subdomain/;
    }

解释:

首先,在定义 server_name 时使用通配符 * ,使 Nginx 接受任意子域名的访问。

然后,对 $host 进行分析,找到子域名的名称。 这个正则表达式是在网上抄的,目的是为了在匹配子域名的同时, 不匹配 www 开头的访问和不带 www 的访问。 前面的 set 是因为 Nginx 的 If 没有 Else,所以默认先将 $subdomain 置空。

最后,在定义 root 的时候,使用 $subdomain 变量。

Git subtree 要不要使用 –squash 参数

上一篇文章 中把 Snoopy 理顺了, 其实 Gregarius 使用的是 MagpieRSS, 而 MagpieRSS 又使用了 Snoopy, 是一个两层的包含关系。

Git submodule 的繁琐似乎是世人皆知了, 所以我用 subtree 来解决上面的包含关系。即: 在 Gregarius 中以 subtree 的方式管理 MagpieRSS, 然后在 MagpieRSS 中以 subtree 的方式管理 Snoopy。

问题的产生

subtree 处理多层包含是没有问题的,因为包含进项目之后, 别人根本看不出这是一个 subtree, 所以它本质上还只是管理本地 repo 的一种方法。

使用 Git subtree 新建或更新子项目的时候,可以选用 --squash 参数, 它的作用就是把 subtree 子项目的更新记录进行合并,再合并到主项目中。

所以,在使用 --squash 参数的情况下, subtree add 或者 pull 操作的结果对应两个 commit, 一个是 Squash 了子项目的历史记录, 一个是 Merge 到主项目中。

这种做法下,主项目的历史记录看起来还是比较整齐的。 但在子项目有更新,需要 subtree pull 的时候,却经常需要处理冲突。 严重的,在每次 subtree pull 的时候都需要重复处理同样的冲突,非常烦人。

如果不使用 --squash 参数,子项目更新的时候,subtree pull 很顺利, 能够自动处理已解决过的冲突,缺点就是子项目的更新记录“污染”了主项目的。

原因分析

简单说,subtree add/pull 操作中,需要用到 merge,而 merge 顺利进行的前提, 是要有相同的 parent commit。对照上面的情况:

使用 --squash 参数,原子项目历史记录被合并后就消失了,相当于一个“新”的提交。 下次再进行 add/pull 时,新添加的内容找不到“上一次的修改”, 于是在更新 subtree 内文件的时候,就会提示冲突,需要手工解决。

不使用 --squash 参数,原子项目的历史复制到了父项目中, 下次再进行 add/pull 时,新增的 commit 能够找到“上一次的修改”, 那么他会像在子项目中逐个 am patch 那样更新 subtree 下的内容, 不会提示冲突。

注:我使用的 Git subtree 是 PPA 上的一个 旧版本 , 或许新版已经解决了上面的问题。

解决问题

就像 这篇文章 结尾说的那样,是否使用 squash 都是可以的, 但需要在开始阶段作出选择,并 一直坚持下去 。 如果一会儿用一会儿不用,得到的不是两者的优点,而是两者的缺点之和。

出于个人偏好,我既希望能够比较顺利的更新子项目, 又不希望子项目的历史记录直接合并在主项目中。StackOverflow 上有人提到了 一种做法 , 就是另外建立一个分支进行 –no-squash 的 subtree 更新, 这样就保留了子项目的历史记录,没有烦人的反复冲突问题; 然后在合并到主分支(比如 master)时合并提交( git merge --squash ), 这样主项目的主分支上只会体现一个 commit, 比直接 git subtree add/pull --squash 还要简洁。

这种做法也有缺点,但在能够接受的范围内:

  • 新开分支的历史记录比较乱,无视吧
  • 新开分支与 master 分支不同步,记着每次在新开分支上做 subtree 操作之前 要 merge master

在新开分支上进行 subtree split 操作是没有问题的。 merge master 以后,subtree push 操作也没有大问题, 也许刚开始会出现 push 被 reject 的状况。

在这种情况下,可以先在本地 split 一份,比如 git subtree split -P extlib/magpierss -b test --rejoin , 然后切换到这个 test 分支,可以看到之所以被 reject , 是因为主项目的那个合并提交也被 split 出来了。 这里会麻烦一些,需要通过 rebase 操作,把这些合并的提交删掉, 换成合并内容包含的每个提交(用 pick HASH)。 成功之后,可以在这个分支直接 push 到 子项目: git push remote_of_subtree branch_on_local:branch_on_remote , 注意后面是指定将本地的哪个分支 push 进 remote 的哪个分支。 这次 push 会很顺利。 接下来再作一次正常的 subtree pull 就可以了, 下次再进行 subtree split 操作时, split 出来的临时分支和 remote 是一致的。

通过上面 push 的例子可以看出,为了 split 和 push 顺利, 即使用了 subtree 分支, 如果能在 master 分支中保存子项目历史记录还是有好处的。 同时,我们还可以参考这个来决定 subtree 使用策略:

  • subtree 里面放外围项目,只接收更新,不发送更新, 那么无论是用 squash 还是用 subtree 分支都不麻烦。
  • 将一个大项目拆分成若干小项目, 那么最好不要用 squash,并且活用 subtree, 最好是所有提交都在主项目中作, 然后 subtree split 出子项目来发布, 子项目原则上不直接修改,即和上一条相反, 只向子项目发送更新,不从子项目接收更新。 Symfony2 使用的就是这种做法。

总体上都有些麻烦,subtree 分支算不上是完美解决方法,但看起来好歹清爽了很多。

@link https://github.com/fwolf/magpierss

@link https://github.com/fwolf/gregarius

将 CVS 转到 Git 并和 Github 上 Fork 的项目合并

在捣鼓我的 Gregarius 时,发现无法读取 HTTPS 的 RSS , 追查发现是他所使用的 HTTP 客户端类 Snoopy 的原因。 想升级新版 Snoopy 却发现原作者已经几年都不更新了, Github 上倒是有人弄了几个镜像, 其中 hurrycaner 的这个 还对 README 进行了一些改进。 但所有镜像都没有 SourceForge 上的修改历史。

所以,我想作的是,基于 hurrycaner 的镜像进行 Fork, 但是要把 SourceForge 上的修改历史也弄进来。

CVS –> Git

现在应该没有人用 CVS 了把,SourceForge 也支持 Git 了, 但上面有些古老项目依然只有 CVS 。

把 CVS 转换成 Git 的工具还是有一些的,但从 一些讨论看来 似乎都做不到完美。 也难怪,CVS 的存储格式实在是有些奇怪, 代码、修改记录、修改注释都堆在一个文件中,解析起来肯定头疼。

由于害怕 cvs2git 会像 svn2git 那样转换时把作者缀上 UUID, 我先试了试 parsecvs , 但这货连使用说明都没有,放弃了。 然后用的是 StackOverflow 上最后一个人推荐的 crap 。 和上面的一样,都是简单 make 一下就有可执行文件用, 但比上面的帮助全,还有一个非常简单的例子。

这就可以开始了,先把 SourceForge 上的仓库下载下来:

$ mkdir Snoopy.cvs
 
$ rsync -av rsync://snoopy.cvs.sourceforge.net/cvsroot/snoopy/ Snoopy.cvs
receiving incremental file list
./
CVSROOT/
CVSROOT/.#checkoutlist
CVSROOT/.#commitinfo
CVSROOT/.#config
CVSROOT/.#cvswrappers
CVSROOT/.#editinfo
CVSROOT/.#loginfo
CVSROOT/.#modules
CVSROOT/.#notify
CVSROOT/.#rcsinfo
CVSROOT/.#taginfo
CVSROOT/.#verifymsg
CVSROOT/checkoutlist
CVSROOT/checkoutlist,v
CVSROOT/commitinfo
CVSROOT/commitinfo,v
CVSROOT/config
CVSROOT/config,v
CVSROOT/cvswrappers
CVSROOT/cvswrappers,v
CVSROOT/editinfo
CVSROOT/editinfo,v
CVSROOT/history
CVSROOT/loginfo
CVSROOT/loginfo,v
CVSROOT/modules
CVSROOT/modules,v
CVSROOT/notify
CVSROOT/notify,v
CVSROOT/passwd
CVSROOT/rcsinfo
CVSROOT/rcsinfo,v
CVSROOT/readers
CVSROOT/taginfo
CVSROOT/taginfo,v
CVSROOT/val-tags
CVSROOT/verifymsg
CVSROOT/verifymsg,v
CVSROOT/writers
CVSROOT/Emptydir/
Snoopy/
Snoopy/AUTHORS,v
Snoopy/COPYING.lib,v
Snoopy/ChangeLog,v
Snoopy/FAQ,v
Snoopy/INSTALL,v
Snoopy/Makefile.am,v
Snoopy/NEWS,v
Snoopy/README,v
Snoopy/Snoopy.class.php,v
Snoopy/TODO,v
Snoopy/autogen.sh,v
Snoopy/configure.in,v
Snoopy/Attic/
Snoopy/Attic/.cvsignore,v
Snoopy/Attic/COPYING,v
Snoopy/Attic/Snoopy.class.inc,v
 
sent 1,066 bytes  received 229,013 bytes  17,042.89 bytes/sec
total size is 225,573  speedup is 0.98

注意这和下载 CVS 代码是不一样的,这里下载的是 CVSROOT,仓库的原始码。

然后初始化一个 Git 仓库目录,用 crap 开始转换:

$ mkdir Snoopy.git
$ cd Snoopy.git
 
$ git init
 
$ ../crap/crap-clone /home/fwolf/dev/Snoopy.cvs Snoopy
Valid-requests Root Valid-responses valid-requests Repository Directory Max-dotdot Static-directory Sticky Entry Kopt Checkin-time Modified Is-modified Empty-conflicts UseUnchanged Unchanged Notify Questionable Argument Argumentx Global_option Gzip-stream wrapper-sendme-rcsOptions Set Gssapi-authenticate expand-modules ci co update diff log rlog add remove update-patches gzip-file-contents status rdiff tag rtag import admin export history release watch-on watch-off watch-add watch-remove watchers editors init annotate rannotate noop version
*********** CYCLE **********
Changeset  andrei
*** empty log message ***
 
    INSTALL:1.1
    Makefile.am:1.1
    NEWS:1.1
    autogen.sh:1.1
    configure.in:1.1
    .cvsignore:1.1
Deferring:
    autogen.sh:1.2
Tag 'Snoopy' placing on branch ''
Tag 'start' placing on branch 'Snoopy'
opening version cache failed: No such file or directory
1970-01-01 08:00:00 CST BRANCH
2000-02-03 23:40:59 CST COMMIT
2000-02-03 23:40:59 CST BRANCH Snoopy
2000-02-03 23:40:59 CST COMMIT
2000-02-03 23:40:59 CST COMMIT
2000-02-03 23:40:59 CST TAG start
2000-02-04 00:10:54 CST COMMIT
2000-02-04 00:10:54 CST COMMIT
2000-02-04 00:28:59 CST COMMIT
2000-02-22 23:44:57 CST COMMIT
2000-03-10 04:52:59 CST COMMIT
2000-03-10 04:54:47 CST COMMIT
2000-05-18 22:50:14 CST COMMIT
2000-05-18 23:36:34 CST COMMIT
2000-05-18 23:44:00 CST COMMIT
2000-06-30 02:37:25 CST COMMIT
2000-08-23 04:36:52 CST COMMIT
2000-09-14 04:52:04 CST COMMIT
2000-09-14 22:09:58 CST COMMIT
2000-09-15 21:11:11 CST COMMIT
2000-09-16 05:57:37 CST COMMIT
2000-09-27 03:34:38 CST COMMIT
2000-09-27 04:28:45 CST COMMIT
2000-10-09 21:13:52 CST COMMIT
2001-03-25 04:15:18 CST COMMIT
2001-07-07 05:24:11 CST COMMIT
2001-08-22 23:43:24 CST COMMIT
2001-11-21 04:23:02 CST COMMIT
2002-10-03 22:38:49 CST COMMIT
2002-10-03 22:55:06 CST COMMIT
2002-10-03 22:57:39 CST COMMIT
2002-10-10 04:25:50 CST COMMITMissed first time round: ChangeLog 1.11
Missed first time round: Snoopy.class.inc 1.21
 
2002-10-10 04:41:24 CST COMMITcvs checkout ChangeLog 1.14 - version is duplicate
cvs checkout Snoopy.class.inc 1.24 - version is duplicate
Missed first time round: ChangeLog 1.12
Missed first time round: Snoopy.class.inc 1.22
 
2002-10-10 04:51:57 CST COMMITcvs checkout ChangeLog 1.14 - version is duplicate
cvs checkout Snoopy.class.inc 1.24 - version is duplicate
Missed first time round: ChangeLog 1.13
Missed first time round: Snoopy.class.inc 1.23
 
2002-10-10 04:56:14 CST COMMIT
2003-03-12 22:40:55 CST COMMIT
2003-09-15 21:58:28 CST COMMIT
2003-10-22 03:18:39 CST COMMIT
2003-11-08 03:52:58 CST COMMIT
2003-12-24 03:34:35 CST COMMIT
2004-01-08 03:16:10 CST COMMIT
2004-07-25 02:23:27 CST COMMITMissed first time round: ChangeLog 1.19
Missed first time round: Snoopy.class.php 1.5
 
2004-07-25 02:34:28 CST COMMITcvs checkout ChangeLog 1.22 - version is duplicate
cvs checkout Snoopy.class.php 1.8 - version is duplicate
Missed first time round: ChangeLog 1.20
Missed first time round: Snoopy.class.php 1.6
 
2004-07-25 08:49:02 CST COMMIT
2004-07-25 10:42:48 CST COMMIT
2004-07-25 10:46:34 CST COMMIT
2004-07-25 10:46:59 CST COMMIT
2004-07-25 11:18:32 CST COMMIT
2004-10-16 13:14:11 CST COMMIT
2004-10-16 13:17:41 CST COMMIT
2004-10-16 13:44:51 CST COMMIT
2004-10-16 14:27:09 CST COMMIT
2004-10-16 14:28:30 CST COMMIT
2004-10-16 14:40:42 CST COMMIT
2004-10-17 00:33:58 CST COMMIT
2004-10-17 00:36:18 CST COMMIT
2004-10-18 13:12:55 CST COMMIT
2004-10-18 13:18:27 CST COMMIT
2004-10-18 13:19:04 CST COMMIT
2004-10-18 13:19:28 CST COMMIT
2004-10-18 13:19:51 CST COMMIT
2004-11-18 13:51:32 CST COMMIT
2004-11-18 13:52:28 CST COMMIT
2004-11-18 14:37:05 CST COMMIT
2005-02-03 12:43:26 CST COMMIT
2005-02-03 12:57:05 CST COMMIT
2005-10-23 10:08:40 CST COMMIT
2005-10-23 10:16:26 CST COMMIT
2005-10-24 00:30:34 CST COMMIT
2005-10-24 23:34:50 CST COMMIT
2005-10-24 23:44:12 CST COMMIT
2005-10-24 23:44:59 CST COMMIT
2005-10-24 23:46:10 CST COMMIT
2005-10-30 13:33:15 CST COMMIT
2005-10-30 13:45:09 CST COMMIT
2005-10-31 02:32:42 CST COMMIT
2005-10-31 02:51:35 CST COMMIT
2005-11-08 14:53:56 CST COMMIT
2005-11-08 15:01:47 CST COMMIT
2008-10-22 23:30:41 CST COMMIT
2008-10-22 23:53:14 CST COMMIT
2008-11-09 05:09:09 CST COMMIT
Emitted 79 commits (= total 79).
Exact     2 +     1 =     3 branches + tags.
Fixup     0 +     0 =     0 branches + tags.
Download 147 cvs versions in 84 transactions.
String cache: 141 items, 132/1024 buckets used, mean search 1.06383
git-fast-import statistics:
---------------------------------------------------------------------
Alloc'd objects:       5000
Total objects:          289 (         8 duplicates                  )
      blobs  :          134 (         7 duplicates         46 deltas of        133 attempts)
      trees  :           77 (         0 duplicates         70 deltas of         71 attempts)
      commits:           78 (         1 duplicates          0 deltas of          0 attempts)
      tags   :            0 (         0 duplicates          0 deltas of          0 attempts)
Total branches:           3 (         2 loads     )
      marks:           1024 (       220 unique    )
      atoms:             15
Memory total:          2294 KiB
       pools:          2098 KiB
     objects:           195 KiB
---------------------------------------------------------------------
pack_report: getpagesize()            =       4096
pack_report: core.packedGitWindowSize =   33554432
pack_report: core.packedGitLimit      =  268435456
pack_report: pack_used_ctr            =          7
pack_report: pack_mmap_calls          =          3
pack_report: pack_open_windows        =          1 /          1
pack_report: pack_mapped              =     350104 /     350104
---------------------------------------------------------------------

这样这个 Git 仓库就包含了已经转换过了的 CVS 历史记录, 如果看不到文件可以 reset 一下。

按说后续的操作理论上可以在这个仓库目录中操作,但为了更好的和 Fork 的项目合并, 我使用导出 Patch 的方法,后面再 am:

$ git log --pretty=oneline |wc -l
78
 
$ git format-patch -78

其实在这里,也可以在目标 repo 里面,通过添加 Snoopy.git 为 Git remote, 然后 merge remote 的方式进行,效果更好,还不用修改提交时间。

Fork 项目,移花接木

在 Github 上 Fork https://github.com/hurrycaner/snoopy , 得到 https://github.com/fwolf/snoopy , 但先不下载到本地,后面的操作方法和正常 Fork 项目是 不一样 的。

在本地再新建一个 Git 仓库,这个仓库是我们今后维护 Snoopy 的主仓库:

$ mkdir Snoopy
$ cd Snoopy
$ git init
$ git remote add origin git@github.com:fwolf/snoopy.git
$ touch .gitignore
$ git add .gitignore
$ git commit -a -m "Initial commit"
$ git push -f origin master

和新建项目的方法基本一样,不同点是我们的 origin 是 Fork 后的项目, 并且进行了 push -f 操作,覆盖掉了 hurrycaner 的所有提交。

接下来新建一个 sourceforge 分支,保留 SourceForge 上 CVS 代码的最终状态, 提交是通过 am 导入的, --committer-date-is-author-date 参数是将作者的时间作为提交时间, 也可以不要。Patch 0002 是空的,会导致 am 失败,所以删除掉:

$ git branch sourceforge
$ git checkout sourceforge
 
$ rm ../Snoopy.git/0002-Initial-check-in.patch
$ git am ../Snoopy.git/00* --committer-date-is-author-date
 
$ git checkout master
$ git merge sourceforge
$ git push

现在,master 分支上是我作的一个初始提交,加上 CVS 上导过来的提交内容, 相当于是 CVS 被完整的导入了 Git。

添加只有一个空 .gitignore 文件的初始提交是 Git 的一个习惯, 因为 Git 的初始提交可以视为是“不可以操作”的, 所以最好是空或者只包含最少内容。

接下来,我们要将 hurrycaner 所作的修改合并进来。 由于他是基于 Snoopy 1.2.4 代码修改的, 和我导入的最终代码差距不大,所以合并还比较顺利,只有几处冲突而已:

$ git branch hurrycaner
$ git checkout hurrycaner
$ git remote add upstream git@github.com:hurrycaner/snoopy.git
$ git fetch upstream
$ git merge upstream/master     # 手工解决冲突
 
$ git checkout master
$ git merge hurrycaner
$ git push

这就基本上完成了,保留了从 CVS 到 hurrycaner 的完整修改记录, 并且还能像正常 Fork 的项目那样继续工作。

修改记录看起来是这个样子的:

2013-10-18-000402_722x358_scrot

我已经向原项目作者推送 Pull Request 了。 hurrycaner 在 Github 上并不活跃,不知道能不能看到、会不会收啊。

尾声

Git 的使用是比较灵活的,我相信其他分布式 SCM 也能做到,没研究过,不对比。 话说回来,本文中的做法,是不是有点鸠占鹊巢的感觉?

@link https://github.com/fwolf/snoopy

防火墙的目标地址转换和源地址转换

遇到一起防火墙故障,对防火墙的工作原理和目标地址转换、 源地址转换有了进一步的了解,记录于此。

结果说在前面

网络结构非常简单:外网-防火墙-内网。内网中架有网站,在外网使用域名访问。 内网中机器需要上外网,也需要用域名互访。 在这种情况下,地址转换规则应当这样配:

  • 外网访问内网,只需要做目标地址转换,不需要做源地址转换。
  • 内网通过域名访问内网,除了做目标地址转换,还必须做源地址转换。

上面第一条有一种“例外”情况,就是网关配置错误的情况,比如我们遇到的: 内网6有 2 块网卡,都配置了同一网段的内网地址,并且每块网卡都配置了网关。 由于是同一网段,所以两个网关是一样的。 在这种情况下,外网访问也必须做源地址转换, 否则内网能收到请求,但处理结果无法返回外网,外网看到的结果是没有回应。

防火墙数据包转换发送原理

为什么会这样?这要从防火墙的原理说起。 上面的外网访问内网,正常处理过程应该是这样的:

  1. 防火墙收到外网访问请求数据包,格式中包含【防火墙外网IP 外网用户IP】
  2. 防火墙进行目标地址转换,数据包变成【内网IP 外网用户IP】
  3. 内网服务器收到请求,进行处理,返回结果数据包【外网用户IP 内网IP】, 由于目标地址是外网IP,所有经过路由,此数据包被发向网关(防火墙)
  4. 针对返回数据包,防火墙做目标地址转换的反操作, 数据包变成【外网用户IP 防火墙外网IP】
  5. 数据包发回请求用户。

刚才说到的“例外”情况,问题就出在步骤 3,内网服务器返回结果数据包中, 内网IP 不是防火墙指向的那个 IP,而是这太内网服务器的另一个 IP 地址, 也就是说内网服务器通过网卡1接受了请求,却从网卡2发送返回数据。 这样的数据包到了防火墙之后,目标地址转换的反操作会失败(内网IP 不一致), 所以数据依然会通过防火墙发给用户,但却是一个新的 TCP 会话/连接, 无法和原先的用户请求包对应。 结果就是新的数据包到了用户那里,不知道是给谁的, 而用户原先发送的请求,也一直“收不到”回应数据包。 外在表现就是用户看到请求发送成功,却没有回应。

在这种情况下,如果同时做了源地址转换是能够通的,处理过程如下:

  1. 防火墙收到外网访问请求数据包,格式中包含【防火墙外网IP 外网用户IP】
  2. 防火墙进行目标地址转换,数据包变成【内网IP 外网用户IP】, 进行源地址转换,数据包变成【内网IP 防火墙/网关IP】
  3. 内网服务器收到请求,进行处理,返回结果数据包【防火墙/网关IP 内网IP】, 由于目标地址仍然是防火墙/网关IP,所以防火墙能够找到相应的连接, 会接受数据包并进行处理。
  4. 针对返回数据包,防火墙 做源地址转换的反操作,数据包变成【外网用户IP 内网IP】, 做目标地址转换的反操作, 数据包变成【外网用户IP 防火墙外网IP】(R)
  5. 数据包发回请求用户。

可以看到,即使(R)操作失败,由于防火墙已经将内网服务器返回数据包和 防火墙接收的外网用户请求连接成功对应, 结果数据包的目标地址也成功转换成了外网用户IP, 所以数据能够成功发回请求用户,并和用户请求会话/连接相对应。

从另外一个角度看,外网访问做双向地址转换虽然可行,但有一个缺点, 就是数据包到达内网服务器时,外网用户IP 被转换成了防火墙/网关IP, 这样内网中的应用程序就无法获取外网用户的真实IP了, 只能看到请求来自防火墙/网关IP。

内网通过域名访问内网

上面的都理解之后,内网通过域名访问内网为什么必须要做双向转换就清楚了。 问题同样出在接受请求的服务器处理完,发送返回数据包时。

内网1 通过域名访问内网6,在没有做源地址转换时的情形:

  • 由于是通过域名访问,所以请求数据通过防火墙转发给内网6, 用户IP是内网地址(这是关键)
  • 内网6处理完毕发送结果数据包,返回数据包的目标地址就是内网1的地址 由于同在一个子网内,数据包会通过交换机直接发向内网1,不过防火墙

问题产生了,在内网1 上,请求数据直接发向防火墙,返回数据直接来自内网6, 不对应,又是一宗没有回应数据包的情形。

如果同时做了源地址转换,问题就能够解决。因为内网1 的请求数据在通过防火墙时, 用户地址(内网1 IP)会被转换成防火墙/网关IP, 返回数据包也就会发往防火墙/网关IP,再被防火墙转换发回 内网1, 不存在内网1 和内网6 的直接对话, TCP 数据包也能对应上,访问就没有问题了。

不同品牌、类型的防火墙工作机制也许不同,但原理都差不多。 不专业的地方,还请过路仙人不吝赐教。

在U盘上安装 Arch Linux (2013-07)

前言

1 月份在移动硬盘上装了个 Arch , 但这块移动硬盘实在是太旧了,在我的旧电脑上经常供电不足, 弄了块 USB 2.0 PCI 卡 也无济于事。 最近终于挺不住了,频繁的报分区损坏、无法进入系统, 严重的时候会在进行硬盘操作时死机。 无奈,拿出一个比较新的 8G U盘,重做随身系统。

本篇文章基于上次移动硬盘安装的经历修改补充而来, 不过半年的时间,一些基础组件就已经发生了变化, Arch Linux 可真是够“折腾”的。

PS:基于半年的 Arch Linux 使用经验, 我 合租的 Linode VPS 也用上了 Arch, 目前感觉尚好,Nginx + PHP-FPM 确实是不错的组合。 Linode 使用修改过的 Linux 内核, AUR 上也有人发布 自己制作的更新版 , 还没研究怎么升级。

基本系统安装

和上次移动硬盘安装一样,选择 32 位系统。 安装介质使用 archlinux-2013.07.01-dual.iso dd 到另一个U盘。

  • 使用U盘启动,自动以 root 身份进入系统。

  • 插入移动硬盘,分区,格式化,挂载。

    本想使用 GPT 分区表,查了一些资料, 为了使这个U盘在 BIOS 和 UEFI 电脑上都能用, 需要创建一个 BIOS boot 分区,2M 大小足够,位置尽量靠前。 多系统的话还要创建一个 200M 的 EFI System Partition(ESP) 分区。 这些都不是问题,但实际做下来, GPT 中的 NTFS 分区在 Win7 下死活不认, 想用这个分区作为常规U盘使用就不行了, 只好再回到 MBR 分区表。 如果是移动硬盘用 GPT 应该没有问题。

    所以仍然使用 MBR 分区,所以就要用 fdisk 或者 cfdisk 了, 不能使用支持 GPT 的 gdisk 和 cgdisk, 4k 对齐也是自动完成。 有趣的是,如果想把 GPT 分区表转换成 MBR 分区表, 还得借助 gdisk:r 进入 Recovery 模式, g 进入 MBR 模式,w 进行转换。 根据实际情况, 有些分区转换未必能够成功, 不过变回 MBR 分区表是没有问题的。

    NTFS 分区得在第一个分区,不然 Windows 不认。

    启动U盘是 /dev/sdb,目标U盘就成了 /dev/sdc。

    # fdisk /dev/sdb
     
    Disk /dev/sdc: 8022 MB, 8022982656 bytes, 15669888 sectors
    Units = sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disk label type: dos
    Disk identifier: 0x00000000
     
    Device      Boot    Start       End         Blocks      Id      System
    /dev/sdb1           2048        1804287     901120      7       HPFS/NTFS/exFAT
    /dev/sdb2   *       1804288     2009087     102400      83      Linux
    /dev/sdb3           2009088     10405887    4198400     83      Linux
    /dev/sdb4           10405888    15669887    2632000     83      Linux

    第一个分区先用 ntfs,当正常U盘使用, 如果以后需要用到 ESP 分区,直接用这个分区转,或者切一部分空间过去。 第二个分区 100M,挂 /boot; 第三个分区 4G,作为 ArchLinux 根分区; 第四个分区 2.5G,作为 $HOME

    格式化分区:

    如果使用 xfs 分区格式:

    # mkfs.ntfs -f /dev/sdc1 -L f004-c
    # mkfs.xfs -n size=64k -l lazy-count=1 /dev/sdc2 -L f004-boot
    # mkfs.xfs -n size=64k -l size=128m,lazy-count=1 /dev/sdc3 -L f004-a
    # mkfs.xfs -n size=64k -l size=128m,lazy-count=1 /dev/sdc4 -L f004-b

    -n size=64k 参数增加了文件名(目录)区域大小(默认 4k), 这样可以减少 IO 消耗,代价是 CPU 消耗多一些, 这对现代计算机显然不是问题。 -l size=128m 参数扩大日志的容量(默认 10m),更安全, 代价是挂载时间会长一些。 -l lazy-count=1 参数减少 superblock 读写次数,提高性能。

    如果使用 ext4 分区格式:

    # mkfs.ntfs -f /dev/sdc1 -L f004-c
    # mkfs.ext4 -b 4096 -m 0 -i 16384 -O '^has_journal' /dev/sdc2 -L f004-boot
    # mkfs.ext4 -b 4096 -m 1 -i 16384 -O '^has_journal' /dev/sdc3 -L f004-a
    # mkfs.ext4 -b 4096 -m 0 -i 16384 -O '^has_journal' /dev/sdc4 -L f004-b

    -b 4096 是每个存储块的大小。 -m 1 是指定 root 保留空间为 1%,home 区就不留了。 -i 16384 是指定多少字节的数据设置一个 inode 节点, 增加它的值会减少 inode 的总数,占用的空间会少一些, 相应的能够存储的文件数量也减少了,这个稍微注意一下就好,一般都够用。 -O '^has_journal' 是关掉文件系统日志,有点小危险。

    经过两天的折腾,个人感觉 ext4 在U盘上读写速度快一些。

    挂载分区:

    # mount /dev/sdc3 /mnt
    # mkdir /mnt/boot
    # mount /dev/sdc2 /mnt/boot
    # mkdir /mnt/home
    # mount /dev/sdc4 /mnt/home

    df -h 检查一下:

    # xfs
    Filesystem  Size    Used    Avail   Use%    Mounted on
    /dev/sdc3   3.9G    33M     3.9G    1%      /mnt
    /dev/sdc2   82M     5.2M    77M     7%      /mnt
    /dev/sdc4   2.4G    33M     2.4G    2%      /mnt/home
     
    # ext4
    Filesystem  Size    Used    Avail   Use%    Mounted on
    /dev/sdc3   3.9G    8.1M    3.8G    1%      /mnt
    /dev/sdc2   96M     48K     96M     1%      /mnt
    /dev/sdc4   2.5G    3.8M    2.4G    1%      /mnt/home

    ext4 改变 bytes-per-inode 确实对分区所占空间影响很大。

  • 安装基本系统

    家里有无线路由,网络不用配置,已经自动连上了。

    编辑 /etc/pacman.d/mirrorlist ,把最快的源挪到最上面。 一般来说 163 和台湾的源比较快,163 似乎有时候不稳定, 几个中国大学的源有些是 ipv6 的,用不了。 安装完成后,这个配置文件也会自动拷贝到新系统中。 自带的 vi 真的是比 vim 难用多了,一会儿第一时间换掉。

    按照官网说明,用 # pacstrap /mnt base base-devel 安装基本系统, base-devel 也一并装上,迟早会用到 AUR 或 ABS。 提示共 128 个软件包,需要下载 158.37M 内容,安装完成后是 507.09M。

    安装 grub: # arch-chroot /mnt pacman -S grub

    生成 fstab: # genfstab -p -U /mnt >> /mnt/etc/fstab , 然后更改 fstab (系统默认一般就比较好了,不优化也行):

    • 使用 relatime 挂载参数。

      以前是推荐增加 noatime 挂载参数,不记录文件读取时间, 但这会导致 Mutt 等需要文件读取时间的软件出错。 现在改为使用 relatime 参数了 (已经默认加上了,Linux 2.6.30 起此参数成为默认值), 只有在文件读取时间早于文件更新时间时,才更新读取时间数据。

    • 使用 nodiratime 挂载参数,不记录目录读取访问时间。

    • /tmp 放到 tmpfs 上去。(没单独分区,免了)

    • 不再使用 discard 挂载参数。

      这个参数主要是针对 SSD 硬盘的,对不支持 TRIM 的机械硬盘无效, 对U盘貌似作用不明显。

    • 不再使用 async 挂载参数,开启异步读写模式。 由于只是“看起来”快了,实际数据写入速度并没有改善, 考虑到稳定性,是否应使用此参数?

    • 不要使用 barrier=0 挂载参数,这个选项似乎在突然断电、 拔U盘时,有几率丢失文件或损坏分区。 (基于我的经历 + 参考

    由于不会跑什么大应用,为了降低 swap 的使用频率, 修改 /mnt/etc/sysctl.conf

    vm.swappiness = 1
    vm.vfs_cache_pressure = 50

    前一句是尽量不使用 swap,后一句是缓存文件系统信息。

    下面的操作可以在 chroot 环境下运行:

    # arch-chroot /mnt

    设置 hostname: echo 'f004' > /etc/hostname

    设置时区:

    # ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

    新建 /etc/locale.conf 内容为:

    LANG='en_US.UTF-8'
    LC_COLLATE='C'
    LC_MESSAGES='C'

    编辑 /etc/locale.gen ,取消 en_US.UTF-8, zh_CN.UTF-8, zh_TW.UTF-8 前面的注释,然后执行 locale-gen 命令。

    更改 root 密码: passwd root

  • 安装 Grub 引导系统。

    仍然在 chroot 环境中操作。 编辑 /etc/mkinitcpio.conf ,检查 HOOKS 段, 让 block 参数紧挨着 udev 参数之后(早一点加载), 然后 # mkinitcpio -p linux 生成 img 文件。 同时加上了 shutdown 参数,作用以后再试。 resume 参数就算了,U盘本来就小、慢,支持休眠更痛苦。 (注:新版本中 block 参数替代了 usb pata sata scsi 等一众参数)

    安装 grub:

    # grub-install --target=i386-pc --recheck --boot-directory=/boot --no-floppy /dev/sdc
    # cp -v /usr/share/grub/{unicode.pf2,ascii.pf2} /boot/grub/
    # cp -v /usr/share/locale/en\@quot/LC_MESSAGES/grub.mo /boot/grub/locale/en.mo

    无论是 32 位还是 64 位系统,都是使用 --target=i386-pc 参数, --no-floppy 是不检查软驱(这玩意儿现在应该没人用了)。 后两句不执行也行,还没弄懂是做什么的。

    然后,千万不要忘记 生成 grub.cfg 文件

    # grub-mkconfig -o /boot/grub/grub.cfg
    # grep 'set=root' /boot/grub/grub.cfg
    # blkid /dev/sdc2

    现在的 Grub2 使用 UUID 来找硬盘分区, 后两句就是检查新生成的 grub.cfg 使用的分区 UUID 和硬盘是否相符。 如果忘记生成 grub.cfg 了,可以再次用光盘启动进去做。

    最后,退出 chroot 环境,umount,重启。 启动U盘可以收起来了。

    启动后,新安装的U盘变成了 /dev/sdb, 此时最好将 mkinitcpio grub-install grub-mkconfig 重新做一遍, 否则有可能在下次或另外一台机器上启动时,损坏分区表,丢失文件。 或许没有道理,但这是我重复安装好多次以后的感觉,可能有以下原因:

    • 带电插拔,可我都是 umount 了啊,不应当这么脆弱。
    • 被 USB 3.0 高电流给破坏了,没拔U盘烧了算我走运? 倒是听说过 USB 3.0 损坏U盘数据。
    • /boot 原先没有单独分区,这个不应该。
    • 是原先使用 GPT 分区在 BIOS 电脑上水土不服? U盘难道就不能用 GPT 分区么?

    反正换回 MBR 分区后,貌似比较正常了。

初始设置

如果一切正常,现在可以用 root 登录系统了:

# uname -a
Linux f004 3.9.9-1-ARCH #1 SMP PREEMPT Wed Jul 3 22:52:05 CET 2013 i686 GNU/Linux
 
# free -h
            total   used    free    shared  buffers cached
Mem:        1.0G    54M     951M    0B      5.6M    27M
-/+ buffers/cache:  21M     984M
Swap:       0B      0B      0B
 
# df -h
Filesystem      Size    Used    Avail   Use%    Mounted on
/dev/sdb3       3.9G    764M    3.1G    20%     /
dev             499M    0       499M    0%      /dev
run             503M    7.1M    496M    2%      /run
tmpfs           503M    0       503M    0%      /dev/shm
tmpfs           503M    0       503M    0%      /sys/fs/cgroup
tmpfs           503M    0       503M    0%      /tmp
/dev/sdb4       2.5G    3.8M    2.4G    1%      /home
/dev/sdb3       96M     39M     58M     41%     /boot

安装U盘拿掉后,这个U盘就成为 /dev/sdb 了。

  • 配置有线网络。

    没网络的时候,可以直接设定ip应急,后面 netctl 才是正规设置:

    # ip addr add 192.168.0.100/24 dev enp0s4
    # ip link set dev enp0s4 up
    # ip route add default via 192.168.0.1
    # echo nameserver 208.67.222.222 >> /etc/resolv.conf

    觉得设备名 enp0s4 不习惯,也可以在 udev 中改名(重启生效):

    # cat /etc/udev/rules.d/10-network.rules
    SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="00:50:70:a2:49:7d", NAME="eth0"

    如果使用 dhcd 动态分配地址:

    # dhcpcd        # 自动让 eth0 获取 IP
    #               # 或者配置成服务自启动(更加方便)
    # systemctl enable dhcpcd@eth0
    # systemctl start dhcpcd@eth0

    如果使用静态地址,现在是用 netctl 进行管理:

    # pacman -S ifplugd
    # systemctl disable dhcpcd@eth0
    # systemctl stop dhcpcd@eth0    # 要停掉 DHCP,不然有干扰
    # cd /etc/netctl
    # cp examples/ethernet-static home
    # vi home           # 设置我的静态 IP 设置 profile
    # systemctl enable netctl
    # 重启一下,清除刚才的临时 IP 设置
    # netctl start home
    # netctl enable home

    如果要快速切换网络环境, 再在 /etc/netctl/ 下建立一个新的 profile 文件, 用 netctl switch-to PROFILE 来进行切换,会自动停掉旧的。 更详细的可以看 我以前写的 netctl 如何选择 profile

  • 安装 vim,默认的 vi 实在是用不惯:

    # pacman -S vim
    # pacman -R vi
    # ln -s /usr/bin/vim /usr/bin/vi
  • 关闭 PC 小喇叭。

    这个实在是太烦人了,在启动时就禁用:

    # cat /etc/modprobe.d/nopcspkr.conf
    blacklist pcspkr

    这个文件需要自己创建。

  • 安装 ntp 自动更新时间

    # pacman -S ntp libedit
    # systemctl enable ntpd
    # systemctl start ntpd

    另外,Arch 建议在 /etc/ntp.conf 中添加 iburst 参数,比如:

    server 0.pool.ntp.org iburst
    server 1.pool.ntp.org iburst
    server 2.pool.ntp.org iburst
    server 3.pool.ntp.org iburst

    检查 ntpd 同步情况:

    # systemctl restart ntpd
    # ntpq -np

    时间同步完成后,用 hwclock -w 写入硬件时钟。

  • 创建普通用户。

    # useradd -m fwolf
    # passwd fwolf

    通常,这个用户我们要赋予 sudo 权限:

    # pacman -S sudo
    # cat /etc/sudoers.d/fwolf_sudo_conf
    Defaults    env_reset
    Defaults    secure_path="/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin"
    fwolf   ALL=(ALL:ALL) ALL
    %admin  ALL=(ALL) ALL
    %sudo   ALL=(ALL:ALL) ALL
    # chmod 0440 /etc/sudoers.d/fwolf_sudo_conf

    注意, /etc/sudoers.d/ 下的文件,名称带有 ~ 或者 . 的不会生效, 参见 CentOS – 0005017: The #includedir directive in sudoers does not work , 文件属性也必须是 0440 。

  • 安装 Yaourt

    /etc/pacman.conf 中添加:

    [archlinuxfr]
    SigLevel = Never
    Server = http://repo.archlinux.fr/$arch

    然后 pacman -Sy yaourt 完成安装。

  • 安装其他常用工具

    bash-completion ctags git htop openssh subversion tk

图形系统

由于U盘要带着到处跑,所以图形驱动一定是什么都有。 也不追求多么花哨的界面效果,简洁明快,所以选择 LXDE+Fluxbox 。

  • 字符界面分辨率

    默认进入字符界面后,分辨率是最大分辨率,如果要另行指定, 可以修改 /etc/default/grub

    GRUB_CMDLINE_LINUX_DEFAULT="quiet video-1024x768M@75m"

    然后 grub-mkconfig -o /boot/grub/grub.cfg 重新生成 grub 配置文件。

  • 默认点亮 Numlock

    需要用到两个命令:系统自带的 setleds 和 通过 pacman 安装的 numlockx。

    要在字符界面下打开 Numlock,可以在 $HOME/.bashrc 中添加:

    # Numlock
    if [ -x /usr/bin/setleds ]; then
        for tty in /dev/tty{1..6}; do
            /usr/bin/setleds -D +num < /dev/tty > /dev/null 2>&1
        done
    fi

    这样设置会在登录之后自动打开 Numlock,若要登录之前就打开,可以参照 Arch Wiki 将上述内容添加到 /etc/rc.local 或者 /etc/inittab , 但不如跟着用户配置容易备份。

    要在 X 下打开 Numlock,可以在 $HOME/.xinitrc 中添加:

    # Set numlock
    if [ -x /usr/bin/numlockx ]; then
        /usr/bin/numlockx on
    fi
  • 安装 xorg 和显卡驱动

    • Xorg: xorg-server xorg-xinit xorg-server-utils mesa
    • 显卡驱动:xf86-video-ati xf86-video-intel xf86-video-nouveau nouveau-dri
    • 笔记本触摸板支持:xf86-input-synaptics
    • 测试 X 工作是否正常:xorg-twm xorg-xclock xterm

    现在可以用 startx 启动 X 看到简陋的图形界面了。 如果想用 Ctrl+Alt+Backspace 关闭 X,需要在 /etc/X11/xorg.conf.d/10-evdev.conf 中增加一段:

    Section "InputClass"
        Identifier "Keyboard Defaults"
        MatchIsKeyboard "yes"
        Option "XkbOptions" "terminate:ctrl_alt_bksp"
    EndSection
  • 安装 LXDE & Fluxbox

    • LXDE 组的所有包
    • Fluxbox
    • 监测文件系统变化的 Gamin
    • 常用软件:leafpad obconf epdfview

    配置一个简单的 $HOME/.xinitrc ,从 /etc/skel/.xinitrc 复制一份, 然后添加:

    #xrandr -s 1024x768 # 如果需要的话,提前指定分辨率
    exec startlxde

    现在都是液晶的天下,很少出现分辨率刷新率超出界限不显示的情况, 命令行下的分辨率可以用自动,嫌字小上面有修改 grub 设置的方法, 图形下 LXDE 也支持 auto 分辨率,一般也都自动设置好了。

    为了把 Openbox 换成 Fluxbox,需要修改 /etc/xdg/lxsession/LXDE/desktop.conf

    [Session]
    window_manager=fluxbox
  • 中文相关

    • 基本字体 ttf-dejavu artwiz-fonts wqy-microhei

    • 输入法 fcitx-im fcitx-configtool fcitx-fbterm ,在 $HOME/.xinitrc 里添加:

      export GTK_IM_MODULE=fcitx
      export QT_IM_MODULE=fcitx
      export XMODIFIERS="@im=fcitx"
    • 字体配置,把原先用的其他常用字体和配置文件都搬过来了, 就是 $HOME/.fonts 目录,Ubuntu 下用的很好,这里也可以直接用。 把 $HOME/.fonts/fonts.conf 链接为 /etc/fonts/local.conf 即生效, 然后修改 /etc/fonts/conf.avail 下的 40-nonlatin.conf60-latin.conf 将 SimSun 或者其他你喜欢的字体设置为首选字体, 最后 $ fc-cache -vf 更新下字体缓存,重启 X 就可以了。

至此,一个基本的、具备图形界面的 Arch Linux 就安装完成了, pacman -Scc 清理一下,已安装程序占用空间约 1.4G。 总体感觉,运行速度受U盘读写速度影响非常大,有时候会卡。

……大约反复两周后,经历了无数次的重装,我终于下了结论: U盘上的 Linux 系统除非只以只读方式使用,否则几乎没有可用性。 大概是因为重启、关机时,umount 总是等不及U盘的灯闪来闪去, 就强制重启或关机了,然后文件系统未正常 umount, 然后就是文件系统损坏,一修复都是文件丢失,损坏的 inode。 难道是因为我的U盘质量不好么? 不管怎样,拿这个做随身系统的想法破灭了, 还是磁介质的机械硬盘靠谱一点。

Claws Mail 不识别 PHPMailer 发送的附件

环境:Claws Mail 3.9.1, PHP 5.4.16, PHPMailer 5.2.6 c5e9f7873f

现象:PHPMailer 发送带附件的邮件,直接使用 AddAttachment() 方法

$mailer->AddAttachment($attach_file);

没有其他设置。Claws Mail 收到信以后,查看邮件内容为空白, 附件栏显示:

message/rfc822
    multipart/mixed

以下就是空白了。 而能够正常识别附件的邮件,附件栏内容一般为:

message/rfc822
    multipart/mixed
        text/plain
        text/html   (这个是附件的 mime 类型)

gmail 和 mutt 中识别这样的邮件是正常的。

分析:通过对比正常和不正常的邮件原始码, 发现不正常邮件在声明内容是分节之后,多了一句传输编码声明,比如:

Content-Type: multipart/mixed;
    boundary="b1_95a848b14cb4385965320b915d5829dd"
Content-Transfer-Encoding: base64

最后的 Content-Transfer-Encoding 就是比正常邮件多的一行。

由于邮件原始码的这个部分,只是用来声明后续邮件是多个部分组成, 并定义了每个部分的辨识边界 boundary,并没有实际的内容, 所以应当是不需要声明编码类型的。在 PHPMailer 中相关代码为:

  public function GetMailMIME() {
    $result = '';
    switch($this->message_type) {
      case 'inline':
        $result .= $this->HeaderLine('Content-Type', 'multipart/related;');
        $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1].'"');
        break;
      case 'attach':
      case 'inline_attach':
      case 'alt_attach':
      case 'alt_inline_attach':
        $result .= $this->HeaderLine('Content-Type', 'multipart/mixed;');
        $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1].'"');
        break;
      case 'alt':
      case 'alt_inline':
        $result .= $this->HeaderLine('Content-Type', 'multipart/alternative;');
        $result .= $this->TextLine("\tboundary=\"" . $this->boundary[1].'"');
        break;
      default:
        // Catches case 'plain': and case '':
        $result .= $this->TextLine('Content-Type: '.$this->ContentType.'; charset='.$this->CharSet);
        break;
    }
    //RFC1341 part 5 says 7bit is assumed if not specified
    if ($this->Encoding != '7bit') {
      $result .= $this->HeaderLine('Content-Transfer-Encoding', $this->Encoding);
    }

特意加上了这个申明,因为按照 RFC1341,7bit 编码类型是默认的。

解决: 或许问题是出在 Claws Mail 上,但我暂时只能修改 PHPMailer 来适应这个问题了。 上面的问题弄清楚之后,在 multipart 后面不添加传输编码声明即可:

    //RFC1341 part 5 says 7bit is assumed if not specified
    // Not after multipart/mixed, claws-mail will not recoginize attachment
    if (($this->Encoding != '7bit') && (!in_array($this->message_type, array(
        'attach',
        'inline_attach',
        'alt_attach',
        'alt_inline_attach',
    )))) {
      $result .= $this->HeaderLine('Content-Transfer-Encoding', $this->Encoding);
    }