• tcp/ip协议学习 第六章 ICMP:Internet控制报文协议

    关于ICMP的RFC文档在此!

    干嘛的

    在我看来, ICMP协议主要就是为了互相传递/查询一些基本信息, 大部分是传递一些错误信息.

    比如A发送UDP信息到B的10000端口, 但B的10000端口并没有开放, 就会回一个ICMP包给A, 告诉A10000端口未开放.

    基本的查询信息, 比如最常用的ping命令, 就是发送ICMP包到目的主机, 然后等待目的主机的响应(响应也是ICMP包).

    协议

    协议定义的非常简单. ICMP在IP层上面一层. 前面是20个字节的IP头, 然后就是ICMP头.

    ICMP头, 截图来自TCP/IP协议详解卷一 ICMP头, 截图来自TCP/IP协议详解卷一

    类型和代码两个字段的组合决定了这个ICMP包的用途, 比如我们常用的ping就是0,0组合和8,0组合. 具体如下:

    各种类型的ICMP报文, 截图来自TCP/IP协议详解卷一 各种类型的ICMP报文, 截图来自TCP/IP协议详解卷一

    代码放上

    好像没有什么好说的. 直接代码放上吧. 实现了一下书中的例子, 一个是查询子网掩码, 一个是查询时间. github地址, 点我点我

    端口不可达

    这也是书中的一个例子. 比如A发送UDP信息到B的10000端口, 但B的10000端口并没有开放, 就会回一个ICMP包给A, 告诉A10000端口未开放.
    来看一下效果.

    用瑞士军刀发送个UDP消息到192.168.0.108的10000端口.

    % nc -u 192.168.0.108 10000                                                   ✭
    abcd
    

    同时开一个Tcpdump监听看看:

    # tcpdump -vvv -x -nn icmp                                                    ✭
    tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
    01:48:01.420363 IP (tos 0x0, ttl 64, id 45430, offset 0, flags [DF], proto ICMP (1), length 56)
        192.168.0.108 > 192.168.0.104: ICMP 192.168.0.108 udp port 10000 unreachable, length 36
    	IP (tos 0x0, ttl 64, id 42558, offset 0, flags [DF], proto UDP (17), length 33)
        192.168.0.104.60181 > 192.168.0.108.10000: [no cksum] UDP, length 5
    	0x0000:  4500 0038 b176 4000 4001 072a c0a8 006c
    	0x0010:  c0a8 0068 0303 eac9 0000 0000 4500 0021
    	0x0020:  a63e 4000 4011 1269 c0a8 0068 c0a8 006c
    	0x0030:  eb15 2710 000d 0000
    

    ping

    代码还是放在github了.

  • kibana4打包和安装

    对于Kibana4,官方只提供了一个打包好的JAVA的包(现在已经是用nodejs了,使用很方便,修改源码也很方便,所以后面这些话忽略吧). 如果想自己修改一些代码添加一些自定义功能, impossible. 至少我还是希望能像Kibana3一样,就是一普通的hmlt静态网站,放在nginx下面跑. 可以添加一些自己的panel. 好吧, 虽然Kibana4好像已经不需要添加什么panel了,但改改css, html总行吧. 而且还可以利用nginx做一些权限控制什么的.

    虽然github有源码了,但做为一个新手, 对于grunt这些东西只是有最最最基本的一些了解,还是折腾了一会才搞定. 纪录一下.

    以下操作都是基于8cef836c61b5749164156e19accb2a0881e902e4这个commit

    1. 从github下载kibana4代码.

       git clone [email protected]:elasticsearch/kibana.git
      
    2. 默认你已经有node了, 如果没有, 用apt-get 或者 yum 或者brew等工具装上. 然后先把bower grunt装好, 接下来就要用.

       npm install -g bower grunt-cli
      
    3. 进入kibana目录. 我没有在master分支, 我是切换到了v4.0.0-beta2分支上面.

       cd kibana
       git checkout v4.0.0-beta2
      
    4. 安装需要的npm包.

      其实, 我不确定是不是所有的包都要装, 我只是要用grunt build一下.甚至只是生成css文件而已.
      这里要注意, 如果是用官方源, 而你身在大陆的话, 那就等死吧. 淘宝源在此

       npm install
      
    5. bower install

      安装需要的js css包. 从github下载. 对于大陆电信30M用户来说, 也是极其痛苦, 一个400K的包死活就是下载不下来.
      osx系统的bower cache文件夹好像在这里/private/var/folders/j9/37cyszz92cg1xkfc46cl5w5r0000gn/T/yourusername/bower 浏览器明明能下载下来, bower就是死活不行. 没办法, 先用浏览器下载之后放在cache文件夹里面

       bower install
      
    6. grunt 可以看到默认是跑了两个任务.

      grunt.registerTask(‘default’, [‘jshint:source’, ‘less’]); less生成css文件. jshint大概就是检查一下JS语法错误吧.

       grunt
      
    7. grunt build

      其实生成css之后, 把src/kibana放在nginx下面就可以跑了.
      build, 就是把一些文件合并压缩一下, 加速用户访问.

    8. config 配置.

      grunt之后, 会在目录下生成一个config文件, 这是一个json文件, 里面是kibana4的一些配置. 里面的kibanaIndex需要改成kibana_index这种格式. 这个生成的原始配置文件应该是nodejs用的, nodejs应该会转成kibana_index这种格式, 但我们现在不用Nodejs做后端服务器了, 所以需要用人改一下.

       {
           "request_timeout": 60,
           "kibana_index": ".kibana",
           "bundled_plugin_ids": [
               "plugins/dashboard/index",
               "plugins/discover/index",
               "plugins/settings/index",
               "plugins/table_vis/index",
               "plugins/vis_types/index",
               "plugins/visualize/index"
           ],
           "default_app_id": "discover",
           "host": "0.0.0.0",
           "shard_timeout": 30000,
           "plugins": [
               "plugins/dashboard/index",
               "plugins/discover/index",
               "plugins/settings/index",
               "plugins/table_vis/index",
               "plugins/vis_types/index",
               "plugins/visualize/index"
           ],
           "port": 5601,
           "verifySSL": false
       }
      
    9. 还差一点点了.

      到这里…居然还不行…
      打开components/config/config.js, 可以看到:

        var configFile = JSON.parse(require('text!config'));
        configFile.elasticsearch = (function () {
          var a = document.createElement('a');
          a.href = 'elasticsearch';
          return a.href;
        }());
      

      最重要的elasticsearch的配置, 不是放在配置文件里, 而是写在代码里面了. 这里改成真正的elasticsearch的地址就可以了.

    10. k4要求ES版本至少也是1.4. 但其实1.3版本的也可以正常使用. 我们就是1.3. 暂时也没打算升级. 限制的代码在index.js里面, 改成constant('minimumElasticsearchVersion', '1.3.0')就OK了.

    OVER.

    补充 (曾经的某个版本下还需要):

    1. script_fileds

      安全起见, 我们的ES禁用script.

      但是k4有些请求, 至少在找index pattern的时候, 使用了script_fileds.

      我简单粗暴的把components/courier/data_source/_abstract.js里面带script_fileds的代码注释了…
      目前还没有发现副作用, 先这样用吧.

    2. 要设置一个默认index pattern

      新建index pattern之后, 要设置一个为默认. 否则k4有些2去找名叫.kibana这个index pattern, 当然找不到, 就报错了.
      beta2还没这个要求.
      我也不是完全确认就是这个原因造成的, 但的确设置一个默认之后就好了.

  • tcp/ip协议学习 第四章 ARP:地址解析协议

    关于ARP的RFC文档在此!

    ARP干嘛的

    曾经有段时间, 六七年前了吧. 本科的时候, 流行了一阵子ARP病毒攻击, 导致整个局域网都不能上网了. 当时只听说这个东西防不住, 只要有一个人中毒, 就导致所有人上不了网. 现在也终于知道这是怎么回事了, 也能手工让某个同学上不了网了, 咳咳.

    大家应该也都知道ARP是干嘛的, 我再啰嗦一下.. 比如我访问了百度, 百度回了包给我, 百度只知道我的IP是什么,不知道我的MAC地址. 这个包到网关的时候, IP这一层再把数据交给下一层的链路层, 链路层不知道IP是什么东西的, 它只认MAC地址. 所以就需要把IP转成MAC地址, ARP请求就是做这个的.

    就是说, 我可以通过这个协议广播问一下所有机器 , 谁的IP是XXX.XXX.XXX.XXX, 请把你的MAC地址告诉我. 这个IP是XXX.XXX.XXX.XXX的机器收到请求之后, 就会告诉我, XXX.XXX.XXX.XXX的MAC地址是啥啥啥.

    不幸这个是基于互相信任的, 理论上大家都会相信别人说的是正确的. 但是, 我可以撒谎说, XXX.XXX.XXX.XXX是我, 我的MAC是啥啥啥. 然后本来应该到 XXX.XXX.XXX.XXX那里的数据包就到我这里来了. XXX.XXX.XXX.XXX不仅仅是断网了, 我还能窃听他的数据.

    协议格式

    要想伪造, 啊,不, 发送一个ARP请求或者应答, 一定要了解协议格式. 从RFC文件抄了一份.

    To communicate mappings from <protocol, address> pairs to 48.bit
    Ethernet addresses, a packet format that embodies the Address
    Resolution protocol is needed.  The format of the packet follows.
    
        Ethernet transmission layer (not necessarily accessible to
             the user):
            48.bit: Ethernet address of destination
            48.bit: Ethernet address of sender
            16.bit: Protocol type = ether_type$ADDRESS_RESOLUTION
        Ethernet packet data:
            16.bit: (ar$hrd) Hardware address space (e.g., Ethernet,
                             Packet Radio Net.)
            16.bit: (ar$pro) Protocol address space.  For Ethernet
                             hardware, this is from the set of type
                             fields ether_typ$<protocol>.
             8.bit: (ar$hln) byte length of each hardware address
             8.bit: (ar$pln) byte length of each protocol address
            16.bit: (ar$op)  opcode (ares_op$REQUEST | ares_op$REPLY)
            nbytes: (ar$sha) Hardware address of sender of this
                             packet, n from the ar$hln field.
            mbytes: (ar$spa) Protocol address of sender of this
                             packet, m from the ar$pln field.
            nbytes: (ar$tha) Hardware address of target of this
                             packet (if known).
            mbytes: (ar$tpa) Protocol address of target.
    

    这个还是TCP/IP协议详解书里面的图好看一些, 且等我截个图来, 呃,截歪了. arp协议,截取自是TCP/IP协议详解

    其实里面有些值, 像”硬件地址长度”什么的, 是个变量, 后面的”发送端以太网地址”就是根据这个而变化 .就我们目前的应用和环境来说, 拿这个图就可以啦.
    这里的长度单位是字节, 不是上一篇IP协议里面的bit了.

    详细解释我也copy一下吧. 也是出自TCP/IP协议详解一书.

    以太网报头中的前两个字段是以太网的源地址和目的地址。目的地址为全 1的特殊地址是 广播地址。电缆上的所有以太网接口都要接收广播的数据帧。

    两个字节长的以太网帧类型表示后面数据的类型。对于 ARP请求或应答来说,该字段的 值为0x0806。

    形容词hardware(硬件)和protocol(协议)用来描述 ARP分组中的各个字段。例如,一个 ARP 请求分组询问协议地址(这里是 IP地址)对应的硬件地址(这里是以太网地址)。

    硬件类型字段表示硬件地址的类型。它的值为 1即表示以太网地址。协议类型字段表示要 映射的协议地址类型。它的值为 0x0800即表示 IP地址。它的值与包含 IP数据报的以太网数据 帧中的类型字段的值相同,这是有意设计的(参见图 2-1)。

    接下来的两个 1字节的字段,硬件地址长度和协议地址长度分别指出硬件地址和协议地址 的长度,以字节为单位。对于以太网上 IP地址的 ARP请求或应答来说,它们的值分别为 6和4。 操作字段指出四种操作类型,它们是 ARP请求(值

    (值为3)和RARP应答(值为4)(我们在第5章讨论RARP)。这个字段必需的,因为 ARP请求 和ARP应答的帧类型字段值是相同的。

    接下来的四个字段是发送端的硬件地址(在本例中是以太网地址)、发送端的协议地址 (IP地址)、目的端的硬件地址和目的端的协议地址。注意,这里有一些重复信息:在以太网的数据帧报头中和 ARP请求数据帧中都有发送端的硬件地址。

    对于一个 ARP请求来说,除目的端硬件地址外的所有其他的字段都有填充值。当系统收到一份目的端为本机的 ARP请求报文后,它就把硬件地址填进去,然后用两个目的端地址分 别替换两个发送端地址,并把操作字段置为 2,最后把它发送回去。

    来段程序吧

    其它也没啥好说的了, 先来段程序吧.

    广播一个ARP请求, 问一下网关的MAC地址是多少. 运行环境是ubuntu12.04. osx上面是不行的. 据说windows也不行. 搜索了好久, 也不知道mac上面怎么搞.

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    '''
    广播一个ARP请求, 问一下网关的MAC地址是多少.
    运行环境是ubuntu12.04.
    osx上面是不行的. 据说windows也不行.  搜索了好久, 也不知道mac上面怎么搞.
    '''
    
    import socket
    import struct
    
    
    def main():
        st = struct.Struct('!6s 6s h h h b b h 6s 4s 6s 4s')
    
        GATEWAY = '192.168.1.1'
        MYIP = '192.168.1.8'
        MYMAC = '20:c9:d0:88:96:3f'
    
        dst_ethernet_addr = ''.join(
            [chr
             (int(e, 16))
             for e in 'FF:FF:FF:FF:FF:FF'.split(':')])
        protocol_type = 0x0806
        hw_addr_space = 1
        protocol_addr_space = 0x800
        hw_addr_length = 6
        protocol_addr_length = 4
        op = 1
        my_mac = ''.join([chr(int(e, 16)) for e in MYMAC.split(':')])
        my_ip = socket.inet_aton(MYIP)
        target_hw_addr = ''.join(
            [chr
             (int(e, 16))
             for e in '00:00:00:00:00:00'.split(':')])
        des_ip = socket.inet_aton(GATEWAY)
        data = (
            dst_ethernet_addr,
            my_mac,
            protocol_type,
            hw_addr_space,
            protocol_addr_space,
            hw_addr_length,
            protocol_addr_length,
            op,
            my_mac,
            my_ip,
            target_hw_addr,
            des_ip,
            )
        packed_data = st.pack(*data)
    
        s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW, socket.SOCK_RAW)
        s.bind(('eth0', socket.SOCK_RAW))
    
        # 下面这样也行, 不知道区别.
        #http://sock-raw.org/papers/sock_raw 这个应该可以参考
        #s = socket.socket(socket.PF_PACKET, socket.SOCK_RAW)
        #s.bind(('eth0',0))
    
        r = s.send(packed_data)
        print r
        return
    
    if __name__ == '__main__':
        main()
    

    抓包结果, 这里已经忽略了以太网首部的14个字节. 是从上图中的”硬件类型”开始的.

    % sudo tcpdump -nn -vvv -x -c1 arp   
    tcpdump: listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
    19:28:43.209235 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has 192.168.1.1 tell 192.168.1.8, length 28
    	0x0000:  0001 0800 0604 0001 20c9 d088 963f c0a8
    	0x0010:  0108 0000 0000 0000 c0a8 0101
    1 packet captured
    1 packet received by filter
    0 packets dropped by kernel
    

    ARP攻击

    上面的代码简单改一下就可以做ARP攻击了.

    对网关做一个ARP应答.
    op改为2, 代表ARP应答. 然后把 “以太网源地址”和”发送端以太网地址”都写自己的. IP地址写要攻击的人. OVER

  • tcp/ip协议学习 第三章 IP协议

    关于IP的RFC文档在此!

    IP的头文件还是先贴一下, 总是记不住.

       0                   1                   2                   3
        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |Version|  IHL  |Type of Service|          Total Length         |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |         Identification        |Flags|      Fragment Offset    |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |  Time to Live |    Protocol   |         Header Checksum       |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                       Source Address                          |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                    Destination Address                        |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       |                    Options                    |    Padding    |
       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    

    网络和子网划分

    不太明白ABCD四类地址划分的意义,除了在路由选择的匹配中会配合子网划分用到.

    路由表, 路由选择

    以前本科的时候, IP配置好之后, 学校内网是可以访问的. 如果想访问外网, 需要拨号.但拨号之后内网就不方便上了.

    学校网站有个小程序, 可以傻瓜式的修改系统路由. 拨号之后内网外网一起上.

    当时不明白这是个什么东西, 其实现在也想不太明白怎么回事, 也没条件回学校试验下了. 但至少现在也算大概明白路由表以及路由选择了.

    之前为了学习, 拿公司的网络实验了一把. osx下面的双网卡路由配置

    来段程序吧

    python

    用程序发送raw IP包的时候, Total Length 和 Checksum 会由内核自动生成. 至于为啥,可能看到tcpip协议详解下(实现)的时候就知道了吧.

    Checksum 算法在模拟后面协议的时候就要自己实现了.ICMP、IGMP、UDP和TCP都采用相同的检验和算法.

    有个不同点是: 首部检验和字段是根据IP头计算的检验和码。它不对body进行计算。 ICMP、 IGMP、UDP和TCP的checksum覆盖首部和数据。

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    '''发送一个裸的IP包, 20字节的IP头, 后面跟一个随便写的字符串.
    还不知道IP包的ID应该根据什么生成, 就随便写了一个54321
    IP头里面:
        IP包总长度属性和checksum属性都是内核自动生成的.
        协议是用的socket.IPPROTO_TCP,也就是6.但没什么用,IP包里面就随便的字符串,不是按TCP协议来的.
    '''
    
    import socket
    from struct import pack
    import sys
    
    
    def main():
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
        except socket.error as msg:
            print 'Socket could not be created. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]
            return
    
        packet = ''
    
        source_ip = '127.0.0.1'
        dest_ip = '127.0.0.1'
    
        # ip header fields
        ip_ver = 4
        ip_ihl = 5
        ip_tos = 0
        ip_tot_len = 0  # kernel will fill the correct total length
        ip_id = 54321
        ip_frag_off = 0
        ip_ttl = 32
        ip_proto = socket.IPPROTO_TCP  # no use in this case
        ip_checksum = 0    # kernel will fill the correct checksum
    
        ip_saddr = socket.inet_aton(source_ip)
        ip_daddr = socket.inet_aton(dest_ip)
    
        ip_ihl_ver = (ip_ver << 4) + ip_ihl
    
        # the ! in the pack format string means network order
        ip_header = pack(
            '!BBHHHBBH4s4s',
            ip_ihl_ver,
            ip_tos,
            ip_tot_len,
            ip_id,
            ip_frag_off,
            ip_ttl,
            ip_proto,
            ip_checksum,
            ip_saddr,
            ip_daddr)
    
        user_data = sys.argv[1] if sys.argv[1:] else '0123456789'
    
        packet = ip_header + user_data
    
        # the port specified has no effect
        r = s.sendto(packet, (dest_ip, 0))
    
        # result is the length of packet sent out
        print r
    
    if __name__ == '__main__':
        main()
        ip_check = 1231    # kernel will fill the correct checksum
    

    抓包. 运行环境, ubuntu12.04

    % sudo tcpdump -i lo -x -vvv -t -c 1                                                                                                ✭
    tcpdump: listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
    IP (tos 0x0, ttl 32, id 54321, offset 0, flags [none], proto TCP (6), length 30)
        localhost.12337 > localhost.12851:  tcp 10 [bad hdr length 0 - too short, < 20]
    	0x0000:  4500 001e d431 0000 2006 c8a6 7f00 0001
    	0x0010:  7f00 0001 3031 3233 3435 3637 3839
    1 packet captured
    2 packets received by filter
    0 packets dropped by kernel
    

    tcp 10 [bad hdr length 0 - too short, < 20] 是说IP body里面的字节数太少, 只有10, 小于TCP header应有的20字节.

    • 1字节, 45, 4代表Version, 5代表Header Length, 单位是4Byte.

    • 2字节, 00, OTS(Type of Service), 代表此IP包的最小时延,最大吞吐量,最高可靠性, 最小费用等特征. 比如用于FTP的控制的IP包, 应该是最小时延, 用于FTP的数据的IP包, 则应该是最大吞吐量.书中提到:

      现在大多数的TCP/IP实现都不支持TOS特性,但是自4.3BSD Reno以后的新版系统都对它 进行了设置。另外,新的路由协议如 OSPF和IS-IS都能根据这些字段的值进行路由决策。

    • 3,4字节, 001e, Total Length. 0x1e = 30; 发送的01234567890+20字节的IP Header.
      因为一些数据链路(如以太网)需要填充一些数据以达到最小长度。尽管以太网的最小帧长为 46字节,但是IP数据可能会更短,像我们就是30。如果没有总长度字段,那么IP层就不知道46字节中有多少是IP数据报的内容。所以IP包总长度这个字段是一定需要的.

    • 5,6字节, Identification. 0xd431=54321.

    • 7,8字节, 0000.

    • 9字节, 20. TTL. 路由转发此IP包的时候就把这个数值减1, 减到0的时候就直接丢弃, 并发送ICMP通知主机. trouceroute即利用了这个.

    • 10字节, 06, 代表TCP协议.

    • 11,12字节 c8a6. Checksum.

    • 13-16字节, 7f00 0001. Source Address. 代表127.0.0.1

    • 17-20字节, 7f00 0001. Destination Address. 代表127.0.0.1

    Checksum是内核帮我们算好的, 但以后的TCP等就要自己算了, 这里先看一下算法.

    首先把检验和字段置为 0。然后,对首部中每个 16 bit 进行二进制反码求和(整个首部看成是由一串 16 bit的字组成)

    def checksum(ip_header):
        ip_header += chr(0x00)*(len(ip_header)%2)
        r = 0
        while ip_header:
            # 因为ip_header已经是网络序了, 所以这里用!H. 否则需要用H
            r += unpack('H',ip_header[:2])[0]
            ip_header = ip_header[2:]
    
        if r > 0xffff:
            r = r&0xffff + (r>>16);
    
        return ~r
    
  • OSX下面的双网卡路由配置

    曾经的公司网络

    公司网络现在已经好多了, 以前的时候挺奇葩的:无线网络可以连外网, 但不能连公办网络, 有线的正好反过来.

    想工作的时候吧,就把无线停了,或者把有线的优先级设高. 遇到问题想google一下呢,就要再反过来把有线停了,无线打开.

    有同事说osx设置好有线和无线的优先级,不会有这个问题,系统会自己尝试,测试下来不可以.

    当前配置

    这种配置下, 只能走有线到办公网络. 不能通过wifi连外网.

    无线网卡

    % ifconfig en0 
    en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
    	ether 20:c9:d0:88:96:4f 
    	inet6 fe80::22c9:d0ff:fe88:963f%en0 prefixlen 64 scopeid 0x5 
    	inet 172.16.27.226 netmask 0xfffff800 broadcast 172.16.31.255
    	nd6 options=1<PERFORMNUD>
    	media: autoselect
    	status: active
    

    有线网卡

    % ifconfig en2
    en2: flags=8963<UP,BROADCAST,SMART,RUNNING,PROMISC,SIMPLEX,MULTICAST> mtu 1500
    	options=4<VLAN_MTU>
    	ether 00:8a:8d:8a:12:2a 
    	inet6 fe80::28a:8dff:fe8a:121a%en2 prefixlen 64 scopeid 0x4 
    	inet 172.16.142.76 netmask 0xffffff00 broadcast 172.16.142.255
    	nd6 options=1<PERFORMNUD>
    	media: autoselect (100baseTX <full-duplex>)
    	status: active
    

    路由表 osx下面要用netstat查路由表

    % netstat -rl -f inet
    Routing tables
    
    Internet:
    Destination        Gateway            Flags        Refs      Use    Mtu   Netif Expire
    default            172.16.142.251     UGSc           31        0   1500     en2
    default            172.16.24.1        UGScI           3        0   1500     en0
    127                localhost          UCS             0        0  16384     lo0
    localhost          localhost          UH              8 17513401  16384     lo0
    169.254            link#4             UCS             0        0   1500     en2
    172.16.24/21       link#5             UCS             8        0   1500     en0
    172.16.24.1        0:1a:e3:5b:f:d7    UHLWIir         4       43   1500     en0   1119
    172.16.142.251     0:1a:e3:5b:1c:44   UHLWIir        32       14   1500     en2   1200
    

    先看一下系统是怎么找一个IP包应该走哪个路由的:

    IP路由选择主要完成以下这些功能: 1)搜索路由表,寻找能与目的IP地址完全匹配的表目(网络号和主机号都要匹配)。如果 找到,则把报文发送给该表目指定的下一站路由器或直接连接的网络接口(取决于标 志字段的值)。

    2)搜索路由表,寻找能与目的网络号相匹配的表目。如果找到,则把报文发送给该表目 指定的下一站路由器或直接连接的网络接口(取决于标志字段的值)。目的网络上的所有主机都可以通过这个表目来处置。例如,一个以太网上的所有主机都是通过这种表目进行寻径的。这种搜索网络的匹配方法必须考虑可能的子网掩码。关于这一点我们在下一节中进行讨论。

    3)搜索路由表,寻找标为“默认(default)”的表目。如果找到,则把报文发送给该表目指定的下一站路由器。

    from TCP/IP 协议卷1 3.3节

    可以看到我这里路由默认走的是有线的, 毕竟在上班, 办公环境要优先, 然后是wifi. 但实际上路由表搜索下来, 除了172.16.24, 其他是不会走到wifi的. 就是说wifi其实是废的.

    添加路由

    思路就是:

    1. 把wifi设置成优先的default路由

    2. 配置内网的网段走有线的网关

    3. 除了有线的, 自然就是走wifi default了

    走起.

    1. 网络偏好设置里面, 把wifi调整为最高优先级

    2. 添加路由

    route add -net 192.168.0.0/16 172.16.142.251
    route add -net 10.0.0.0/8 172.16.142.251
    

    就这么简单, 搞定! 来Ping 一下

    % ping -c 1 www.baidu.com  
    PING www.a.shifen.com (61.135.169.105): 56 data bytes
    64 bytes from 61.135.169.105: icmp_seq=0 ttl=50 time=23.199 ms
    
    --- www.a.shifen.com ping statistics ---
    1 packets transmitted, 1 packets received, 0.0% packet loss
    round-trip min/avg/max/stddev = 23.199/23.199/23.199/0.000 ms
    
    % ping -c 1 10.8.84.90                                                                          64 ↵
    PING 10.8.84.90 (10.8.84.90): 56 data bytes
    64 bytes from 10.8.84.90: icmp_seq=0 ttl=58 time=1.286 ms
    
    --- 10.8.84.90 ping statistics ---
    1 packets transmitted, 1 packets received, 0.0% packet loss
    round-trip min/avg/max/stddev = 1.286/1.286/1.286/0.000 ms
    

    赞, 都是通的.

    再来一个内网的域名

    % ping es.XXXcorp.com    
    ping: cannot resolve es.XXXcorp.com: Unknown host
    

    擦, 解析不了域名!

    更改DNS

    域名解析不了嘛, 肯定是dns的问题. 手动更改一下/etc/resolv.conf嘛. 看一下有线的dns配置, 找到有线的dns服务器加进去.

    再来一次!

    % ping es.XXXcorp.com    
    ping: cannot resolve es.XXXcorp.com: Unknown host
    

    还是不行…

    清理一下dns再来一次. 不同版本的osx不一样的命令, 咱保险起见, 都来一次.

    dscacheutil -flushcache
    killall -HUP mDNSResponder
    

    还是不行…

    nslookup手工指定server是没问题的.

    % nslookup
    > server 192.168.102.20
    Default server: 192.168.102.20
    Address: 192.168.102.20#53
    > es.XXXcorp.com    
    Server:		192.168.102.20
    Address:	192.168.102.20#53
    
    Non-authoritative answer:
    Name:	es.XXXcorp.com
    Address: 10.8.81.10
    > 
    

    总之, 一翻测试下来, 才发现
    nslookup, host命令是可以的, ping 浏览器都解析不了.

    然后又一翻搜索, 原来还要到网络偏好设置里面,去那里改dns才行.
    nslookup和浏览器是去不同地方找配置的.

    做如上修改之后, 整个网络都畅通了.

  • python里多线程的写法

    今天用到python多线程的时候, 发现不知道如何正确的等待所有线程结束后再结束主线程.

    其实到最后我才知道这都是杞人忧天, Thread()出来的实例本来就是等到主进程结束后才结束.

    官方解释:

    daemon A boolean value indicating whether this thread is a daemon thread (True) or not (False). This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False.

    The entire Python program exits when no alive non-daemon threads are left.

    默认daemon是false, start之后也不会阻塞在那里(废话, 否则要多线程干嘛, 不过总会想到join会阻塞这点). 对于我的需要来说简直完美,其实什么都不用做嘛.

    可以用setDaemon(True)改变daemon值. 这样的话, 就需要调用join等待子线程结束了. (好多此一举..)

    for t in threads: t.join()

    可以把所有要起的线程放到一个队列里面, 然后对每一个线程join. 这样的确是可以实现, 但看起来太丑, 用着实在别扭.

    像如下这样, 其实for循环里面, 进程等待在第一个join那里:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    from threading import Thread
    import time
    
    
    def target(i):
        time.sleep(10-i)
    
    
    def main():
        threads = []
        for i in range(10):
            t = Thread(target=target,args=(i,))
            threads.append(t)
    
        for t in threads:
            t.start()
    
        for idx,t in enumerate(threads):
            print idx
            t.join()
    
    if __name__ == '__main__':
        main()
    

    multiprocessing.Pool

    又用Queue实现了一下, 但还是太丑陋, 封装不好. 然后才搜索到Python其实有个multiprocessing.Pool, 可以很好看的实现这个功能:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    from multiprocessing import Pool
    import time
    
    def work():
        print time.time()
        time.sleep(4)
    
    def main():
        pool = Pool(processes=4)
        for i in range(4):
            pool.apply_async(work)
    
        pool.close()
        pool.join()
    
    if __name__ == '__main__':
        main()
    

    借鉴官方的Pool, 尝试自己实现一下最简单的Multiprocessing Pool

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    from threading import Thread
    from Queue import Queue
    import time
    
    
    def hello(msg=None):
        print time.time(),msg
        time.sleep(5)
    
    class MPool(object):
        def posttask(self,func):
            def inner(*args, **kwargs):
                q = args[0]
                real_args = args[1:]
                func(*real_args, **kwargs)
                q.task_done()
            return inner
    
    
        def __init__(self):
            self._queue = Queue() #简单起见没有设置队列上限
    
        def run_task(self, target, *args, **kwargs):
            task = Thread(target=self.posttask(target), args=(self._queue,)+args, kwargs=kwargs)
            self._queue.put(task)
            task.setDaemon(True)
            task.start()
    
        def join(self):
            self._queue.join()
    
    
    def main():
        p = MPool()
        for i in range(10):
            p.run_task(hello,i)
    
        p.join()
    
    
    if __name__ == '__main__':
        main()
    

    最后再提一下, 我的初衷只是想等所有线程结束后再退出. 默认就是这样的, join什么的都不用.

  • rabbitmq 学习记录 -- 权限控制

    好懂? 不好懂?

    看懂之后, 觉得官网上的文档还是写的挺全面的. 但没看懂的时候, 觉得这写的啥啊…看不懂啊..

    简单说下权限控制的层次

    首先, 权限都是针对virtual host设置的. 毕竟virtual host也是个host.. 当一个用户建立连接的时候, 就先要判断这个用户对这个virtual host有没有权限. 没有的话, 连接也建立不了.

    在一个virtual host里面, 对一个资源(原文是resource, 我理解大概就是指的amqp command, 比如说basic_consume), rabbitmq有三个方面的权限控制, 分别是configure, write, read. 对于一个amqp command, 需要对这三个方面(中的一个或者多个)设置相应的权限. 权限的表现形式就是一个正则表达式, 匹配queue或者是exchange的名字

    preprocess是我们的一个用户, preprocessed rawevent是两个virtual host.
    在下面的命令中, 可以看到一个用户的权限都是针对virtual host来的.
    针对一个 virtual host, 按顺序分别列出了configure, write, read的权限 对于rawevent这个virtual host, 我们的configure权限是^$, write是^$, read是^rawevent$ , 没错, 就是普通的正则表达式.

    # rabbitmqctl list_user_permissions preprocess  
    Listing permissions for user "preprocess" ...  
    preprocessed    ^$      ^amq\\.default$ ^$  
    rawevent        ^$      ^$      ^rawevent$  
    ...done
    

    来几个例子

    官网文档这个表格里面, 第一列就相当于权限啦, 一共有16个权限可以用.

    如果想拥有声明queue的权限, 找到queue.declare这行, 可以看到需要在拥有configure的权限, 权限是一个针对queue的正则表达式.

    反过来说, 如果我们的configure权限是 .* , 就代表我们可以声明任何名字的queue

    如果我们限制他声明的队列只能以abcd开头, 就给他configure权限是 ^abcd.*$

    如果还想要queue.bind的权限, 同样找到queue.bind这一行. 看到需要read write两个权限, read权限是queue的正则, write是exchange的正则.

    比如, 某用户要订阅消息, 我们限制用户不能自己给queue取名字,只能用服务器生成的队列名字, 而且只能绑定在amq.fanout这个exchange.

    比对这张表格,

    声明queue需要:
    configure权限应该是^mq.gen.*$

    绑定到amq.fanout需要:
    write是^mq.gen.*$
    read是 ^amq.fanout$

    basic_consume需要:
    read是^mq.gen.*$

    结合起来! 就是:(configure write read的顺序)

    ^mq.gen.$ ^mq.gen.$ ^(amq.fanout) (mq.gen.*)$
  • rabbitmq 学习记录 -- TTL

    queue本身的TTL

    注意, 这里说的是queue本身的TTL. 不是说里面的消息

    声明一个队列的时候, 可以用x-expires指定队列的TTL值. 过期之后, 这个队列就被删掉了.

    不管里面是不是还有消息没有消费

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    '''
    the queue exists for only 5 seconds, whether there is messages!
    '''
    import pika
    import sys
    
    
    def main():
        body = ' '.join(sys.argv[1:]) or 'Hello World'
        connection = pika.BlockingConnection(
            pika.ConnectionParameters(
                host='localhost'))
        channel = connection.channel()
        channel.queue_declare(queue='ttlhello',arguments={"x-expires":5000}) # ttl 5 second
        channel.basic_publish(exchange='',
                              routing_key='ttlhello',
                              body=body,
                              )
        connection.close()
    
    if __name__ == '__main__':
        main()
    

    跑一下看看效果

    # python queueTTL.py ;sleep 1; rabbitmqctl list_queues; sleep 6;rabbitmqctl list_queues  
    Listing queues ...  
    ttlhello	1  
    Listing queues ...  
    

    Per-Queue Message TTL

    这个也是队列的属性, 而不是消息的. 队列中的所有消息过了TTL就会被删除.

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    '''
    the messages in the queue exist for only 5 seconds
    '''
    import pika
    import sys
    
    
    def main():
        body = ' '.join(sys.argv[1:]) or 'Hello World'
        connection = pika.BlockingConnection(
            pika.ConnectionParameters(
                host='localhost'))
        channel = connection.channel()
        channel.queue_declare(queue='ttlmessagehello',arguments={"x-message-ttl":5000}) # ttl 5 second
        channel.basic_publish(exchange='',
                              routing_key='ttlmessagehello',
                              body=body,
                              )
        connection.close()
    
    if __name__ == '__main__':
        main()
    

    跑一下看看效果

    # python queueMessageTTL.py; rabbitmqctl list_queues; sleep 6; rabbitmqctl list_queues  
    Listing queues ...  
    ttlmessagehello	1  
    Listing queues ...  
    ttlmessagehello	0  
    

    #Per-Message TTL

    发送消息的时候, 也可以给每条消息一个TTL的属性.

    messageTTLSend.py

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import pika
    import sys
    
    
    def main():
        body = ' '.join(sys.argv[1:]) or 'Hello World'
        connection = pika.BlockingConnection(
            pika.ConnectionParameters(
                host='localhost'))
        channel = connection.channel()
        channel.queue_declare(queue='hello')
        channel.basic_publish(exchange='',
                              routing_key='hello',
                              body=body,
                              properties=pika.BasicProperties(
                                  expiration="5000"
                                 )
    
                              )
        connection.close()
    
    if __name__ == '__main__':
        main()
    

    跑一下看看效果

    # python messageTTLSend.py; rabbitmqctl list_queues; sleep 6; rabbitmqctl list_queues;  
    Listing queues ...  
    hello	1  
    Listing queues ...  
    hello	0  
    

    最后, 如per-message ttl 和 per-queue message ttl不一样, 按小的来.

  • rabbitmq 学习记录 -- ACK和数据持久化

    为了数据不丢失, 需要在两个层面上做一些配置. 一个是ACK, 一个是数据持久化.

    ACK

    如果没有启用的话, 消费者拿走消息的时候, queue就把它删除了.

    消费者拿走一条消息之后, 还没有处理完就crash了. 那么这条消息就丢失了. 为了保证消息一定被处理完了才从queue中被删掉, 就要启用Message acknowledgment .

    启用之后, queue会在收到ack之后把消息删掉.

    在这里没有timeout的概念, 哪怕这个任务执行很久, 不管多久, 会一直等ack. 或者是tcp链接断了, 才会把消息再给另外一个消费者.

    ack默认是开启的, 也可以显示显示地关闭

    channel.basic_consume(callback, queue=queue_name, no_ack=True) 
    

    callbak里面要记得发送ack,否则消息要被一次又一次的处理,然后再次回到队列 … …

    def callback(ch, method, properties, body):
        print " [x] Received %r" % (body,)
        time.sleep( 10 )
        raise SystemExit(1) # message will put back to the original queue
        ch.basic_ack(delivery_tag = method.delivery_tag)
        print " [x] Done"
    

    来跑几个例子测试一下

    生产者:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import pika
    import sys
    
    
    def main():
        body = ' '.join(sys.argv[1:]) or 'Hello World'
        connection = pika.BlockingConnection(
            pika.ConnectionParameters(
                host='localhost'))
        channel = connection.channel()
        channel.queue_declare(queue='hello')
        channel.basic_publish(exchange='',
                              routing_key='hello',
                              body=body,
                              )
        connection.close()
    
    if __name__ == '__main__':
        main()
    

    消费者:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    import pika
    import time
    
    
    def callback(ch, method, properties, body):
        print " [x] Received %r" % (body,)
        time.sleep(10)
        raise SystemExit(1)
        ch.basic_ack(delivery_tag = method.delivery_tag)
        print " [x] Done"
    
    
    def main():
        connection = pika.BlockingConnection(
            pika.ConnectionParameters(
                host='localhost'))
        channel = connection.channel()
        channel.queue_declare(queue='hello')
        channel.basic_consume(callback,
                              queue='hello',
                              )
        channel.start_consuming()
    
    if __name__ == '__main__':
        main()
    

    发送一条消息到队列 , 然后消费. 观察一下状态

    # rabbitmqctl list_queues name messages_ready messages_unacknowledged
    Listing queues ...
    hello	0	1
    

    等10秒, 再看, 消息没有被消费成功, 再次回到队列中.

    # rabbitmqctl list_queues name messages_ready messages_unacknowledged
    Listing queues ...
    hello	1	0
    

    REJECT

    可以用ACK告诉rabbitmq任务处理完了, 但是如果没有成功的话, 也可以再把消息塞回队列. 就是Negative Acknowledgement. pika对应的方法是basic_reject

    但可得注意, 不要搞成死循环了

    数据持久化

    启用ack之后, 消费者死掉不会丢失数据, 但rabbitmq进程死掉的话, 消息就丢掉了. 为保证数据不丢失, 还需要启动数据持久化. 需要在两个层面上做持久化:

    1. 队列的持久化
    2. 消息的持久化
    channel.queue_declare(queue='hello', durable=True)
    

    这样就申明了一个持久化的队列, durable的属性是不会变的, 如果之前hello队列已经申明过且不是持久化的, 这个再次申明会失败. 这个队列不会因为rabbitmq重启而丢失, 接下来还要继续做消息的持久化.

    channel.basic_publish(exchange='',
                         routing_key="task_queue",
                         body=message,
                         properties=pika.BasicProperties(
                            delivery_mode = 2, # make message persistent
                         ))
    

    Q: 如果在一个非持久化的队列上发送数据时, 指明要持久化, 为发生什么情况?

    A: 可以正常发送, 但重启rabbitmq之后, 队列丢失, 当然消息找不到了.

  • rabbitmq 学习记录 -- 基本概念

    messag queue嘛, 就是生产者往里扔东西, 消费者取走. 但是要涉及到细节,还是有些多的.

    #基本概念 其实就是官网文档的搬运工.

    路由模型

    先来看看一条消息的生命线, 生产者把消息发送到exchange, 然后根据exchange的类型和routing key(消息发送时的一个参数), 把这条消息路由到不同的队列中去, 图片中是发到了一个列队, 其实也可以到多个. 然后消费都从队列中把消息取走. 和kafka有些不同, rabbitmq里面的一个队列里面一条消息被一个消费者拿走之后, 就不可能再被其他人取到了(不考虑ACK的时候). 来自官网的图片

    queue

    队列, 就是存储消息的容器. 有些属性

    • Name
    • Durable (the queue will survive a broker restart)
    • Exclusive (used by only one connection and the queue will be deleted when that connection closes)
    • Auto-delete (queue is deleted when last consumer unsubscribes)
    • Arguments (some brokers use it to implement additional features like message TTL)

    使用queue之前, 需要先声明. 生产者和消费者都可以申明声明. 声明的时候如果队列已经存在了, 也没啥事, 但是如果再次声明的时候, 已经存在的队列参数和当时申明的参数不一样, 是会报错的.

    exchange

    生产者不和queue接触, 消息全部是通过exchange转到对应的queue的. 每一个队列都和一个或者多个exchange绑定在一起. 声明一个queue的时候, 它已经和default exchange绑定在一起了.

    exchange有四种类型, 每种有不同的路由方式~

    先看一下exchange的属性吧

    • Name
    • Durability (exchanges survive broker restart)
    • Auto-delete (exchange is deleted when all queues have finished using it)
    • Arguments (these are broker-dependent)

    说exchange类型之前, 要先知道routing key的概念.

    • queue和exchange绑定的时候, 是有一个routing key的, 为了和下面的routing key区别开来, 还是叫做binding key吧.
    • 发送一条消息时, 有个参数, 叫routing key
    • exchange type, bind key, routing key这三个东西结合在一起, 就决定了这条消息最终被路由到哪个/哪几个queue里面.
    1. direct exchange

      发送到direct的消息, 会找到和routing key一样的binding key的队列, 发过去. 一般来说, 这种exchange就是为了点对点的发消息, 一个消息就是固定发到一个特定的queue中. 但是一定要用来发到多个queue也是可以的.

      #!/usr/bin/env python  
      # -*- coding: utf-8 -*-
      import pika
      import sys
      
      def main():        
          body = ' '.join(sys.argv[1:]) or 'Hello World'
              
          connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
          channel = connection.channel()
               
          channel.queue_declare(queue='hello1')
          channel.queue_bind(exchange='amq.direct', queue='hello1',routing_key="hello")
      
          channel.queue_declare(queue='hello2')
          channel.queue_bind(exchange='amq.direct', queue='hello2',routing_key="hello")
      
          channel.basic_publish(exchange='amq.direct',
                              routing_key='hello',
                              body=body)
          connection.close()
      
      if __name__ == '__main__':
          main()
      
    2. fanout

      这种类型的exchange会把消息发到每一个和他绑定的队列, routing/binding key被忽略. 适合用广播(和简单订阅?)

    3. topic

      比较灵活的路由方式, routing key可以用通配符.

      * (star) can substitute for exactly one word.
      # (hash) can substitute for zero or more words.

      直接看图吧

      topic exchange

    4. headers 这种路由的方式还是很灵活的.

      如果需要绑定的不是一个特定的字符串, 而多个属性. 比如一条服务器的消息, 有OS, 有CPU核数, 有内存大小, 希望这些全部都匹配成功时,也可以是有一条匹配成功时,发到你的队列中来. 这个时候用headers exchange就比较方便.

      绑定的时候,一个重要的参数是x-match. 如果是all,就是说所有属性匹配成功才发到这个队列. 如果是any,就是任意一个属性匹配成功啦.

      这里也有个python的例子 Using pika to create headers exchanges with RabbitMQ in python

    5. default 官网上把default exchange单独列了出来, 不过在我看来, default exchange就是一个direct exchange. 只是有些特殊的地方:

      1. 创建一个queue的时候, 自动绑定到default exchange. binding key就是队列名字
      2. 一个queue不能和default exchange解除绑定 (这点我不100%确定)

    我只是大概翻译一下官网文档,记录一下. 这里对exchange介绍的更加详细. Working with RabbitMQ exchanges 这里面介绍比较详细.