• 使用 OpenConnect 代替 Cisco AnyConnect,避免路由表被锁定

    前言

    最近做一个项目,需要通过 VPN 连结到对方的服务器,对方用的是 Cisco AnyConnect。

    我们装上给的 AnyConnect 客户端,可以连上。

    问题

    后来,我们的系统跑不起来,发现了问题:我们的系统需要连接我们内网的服务 X,而这个服务 X 的 IP 地址被 AnyConnect 的路由表包含在其中了。查看路由表可以发现这个情况,把 VPN 一断开,服务 X 又能访问了,一连上马上又不行了。我们连 VPN 只需要访问对方的仅仅一个 IP,但是这 VPN 把一堆内网网段都路由了。

    我直接删了对应的路由表,再刷新路由表一看,那个条目怎么还在??

    后来找了好久,总算找到了元凶,就是 AnyConnect 本身。它就是会故意监视路由表,一旦发现被篡改就给你改回去。

    解决

    在刚才的链接中,有人就提出了办法:不用 AnyConnect,改用开源的 OpenConnect。安装很简单,Linux/Mac OS 都有现成的包可以通过命令行安装,Windows 也有对应的 GUI 版本。

    安装完成后,使用以下命令连接 VPN:

    # echo password | openconnect --background -u vpnuser \
    --servercert pin-sha256:xxxxxxxxxxxxxxxxxxxx \
    vpn.your.com

    连接建立后,可以查看路由表,发现路由表确实又被加了许多。

    但是没关系,sudo ip del xxxxxxxxxx,直接就能删掉。这样问题就解决了。

  • 解决 expecting SSH2_MSG_KEX_ECDH_REPLY 等一系列网络问题

    背景

    公司有多个内网,之间通过 VPN 互联。

    问题

    本地使用 Linux 电脑,进行软件开发等活动。遇到了各种各样奇异的网络问题,例如 SSH 不能连接内网的 git;开发的 Java 程序不能访问内网的数据库、zookeeper;远程桌面可以连接,但一旦复制粘贴文件,远程桌面就断开等。ping、telnet 连接对应端口,都是正常的。

    更奇怪的是,如果重启进入 Windows 系统,“SSH 不能连接内网的 git”就自动好了。

    一度怀疑是某些数据包触发了墙,然后整个连接被墙了。

    解决

    求助运维同事,一开始也排查不出来,后来在给 SSH 连接加上了详细的 debug 信息后,发现是”expecting SSH2_MSG_KEX_ECDH_REPLY”这步卡住了。

    上网一搜,就是 MTU 的问题。

    马上用 ifconfig 把 MTU 从 1500 改到 1400,结果问题依然存在。改为 1300,SSH 能联通了,其他问题也都没有了。

    后记

    MTU 感觉就是在学网络时才有提到的词,没想到被坑到了。很可能与公司加了一层 VPN 有关,毕竟平时 1500 MTU,还没出现过问题。

  • FIX 协议开发(3):QuickFIX/J 实战经验小结

    本系列导航

    FIX 协议开发(1):协议介绍及开发方案
    FIX 协议开发(2):QuickFIX/J 入门
    FIX 协议开发(3):QuickFIX/J 实战经验小结(本文)

    代码结构

    这里主要讲一下我们的思路,具体的代码不方便贴上来。

    首先需要实现 Application,主要是其中的 fromApp() 需要 crack 不同类型的 Message。但是,不把具体业务逻辑写在 crack() 方法里,crack() 主要就是把 message 通过一个或多个队列传到外部。

    收发信息的部分,从上一节可以看到,具体代码还是挺零碎的,所以做一层抽象会方便一点。
    另一个重点是注意同步/异步:有些方法做成同步会方便一点,例如查询持仓/资金,又例如撤单【同步/异步都可以】;而下单则应做成异步。

    Fix Session 持久化

    程序启动时,会自动读入之前保存的状态。FIX 协议对序列号的要求严格,如果收到的序列号偏大或偏小,会导致需要额外的处理甚至 session 被断掉。因此,持久化非常重要,尤其是维护收发序列号。

    以我方下一次发送的序列号 NextSenderMsgSeqNum 为例。Session.javasendRaw()方法调用了persist()方法,该方法中 MessageStore 实例把即将发送的消息持久化;该步骤完成后,MessageStore 实例接着执行incrNextSenderMsgSeqNum(),把序列号保存下来。在persist()完成之后,才真正把数据发送出去。

    MessageStore 自带了几种实现,例如基于文件的FileStore,放在内存里的MemoryStore,我们参考MemoryStore稍作修改,做了一个RedisStore,感觉会灵活一点。

    关于 session 的另一个话题是,如何手动控制一个 session,例如使 session logout、reset 等。

    可以使用静态函数Session.lookupSession(sessionID)来从 ID 获取对应的 session,接下来就可以调用 session.logon()、logout()、reset() 方法。后两个的区别,logout 是发送 FIX 的 logout 信号,然后断开连接,不影响序列号等内部状态,之后调用 logon() 即可回到正常使用的模式。reset() 会清除序列号等内部状态及其持久化,下次收发包会都从 1 开始。盘中就别用这个了。

    如果不需要手动控制 session,而是每天自动从几点连到几点,那么只需要在参数中配置StartTimeEndTime即可。

    异常处理

    进行 QFJ 开发时,特别需要注意的是,如何捕捉“异常”。不像 API 调用,时刻都有 error code 字段。对方返回的消息中如有错误信息或特殊情况时,是需要针对性处理的。

    例如查询持仓是空仓,服务器不可能什么都不返回,而会返回一个特殊的空仓标记。又例如各种字段填写不合法,报错信息很可能藏在可选的 Text 字段中。

    接收业务信息后报错

    主要有两个原因:

    • 一是不符合 xml 的定义,在底层就会自动给对方发送拒绝消息“MsgType (35): 3 (REJECT)”,不会传到 Application 的应用层;
    • 二是在应用层的 fromApp() 内 crack 信息时没有找到对应 crack 的方法,会发送“BusinessRejectReason (380): 3 (UNSUPPORTED MESSAGE TYPE)”给服务器。

    解决的要点都在于我方的字典没有适配对方的字典(对方故意发错误的数据包除外)。参考上一篇的“自定义字段”一节,根据实际情况修改 xml,自行打造合适的 Message 库才是王道。

    消息自动按类型分类、处理流程

    对比自己构造数据包、自己解析收到的数据全 DIY 的实现方式, QFJ 框架已经为我们做了很多。如果好奇的话,数据包是如何解析,最终传给我们的 fromApp() 方法?先从源头,即 MINA 框架接收到数据包说起。

    对方发来的数据包,首先经过 quickfix.mina.message.FIXMessageDecoder 解码,进行基础的校验,例如检测 FIX 协议头、获取消息体的长度、校验 checksum 等。

    如果校验正常,InitiatorIoHandler/AcceptorIoHandler 调用其父类 Quickfix.mina.AbstractIoHandler 继续处理消息,包括但不限于:从 session 获取字典、解析 FIX 数据【通过调用quickfix.MessageUtils的函数】等。解析时,先提取 msgType,调用 session 对应的 messageFactory 创建 msgType 类型的 message 实例,接下来根据字典解析数据内容。

    解析 message 完成后,InitiatorIoHandler/AcceptorIoHandler 把 message 传给 processMessage() 方法;执行 eventHandlingStrategy.onMessage(),该方法内把 message 塞到队列 queueTracker 中。

    最后,队列的元素会被“QFJ Message Processor”线程取出,给到 quickfix.Session 的 next(Message) 方法。这个 next() 方法,里面还有对字段的校验,里面的 verify(message) 再进而校验登录状态、时间、seq num 等,最终调用我们写的 fromApp() 方法。

    可见,从接收到数据,到我们实现的 Application,经历了很多很多层的调用。😅

    这个流程还没有结束。

    我们让自己的 Application 继承 quickfix.MessageCracker,从而拥有了 crack(),crack 是怎样根据不同实际类型的 message 调用对应的 onMessage() 的呢?

    MessageCracker 在初始化时,执行 initialize() ,通过反射,找到所有 handler【也就是各种自己写的 onMessage()】,并存到一个 mapping。之后 fromApp() 调用 crack() 时,就通过传入的 message 提取其类型以及 mapping 找到对应的 handler,调用对应的 onMessage()。

    如果觉得这个 crack 过程有点太“黑科技”,我们可以不用它,不继承 quickfix.MessageCracker,转而继承 quickfix.fix42.MessageCracker【按实际对应的版本】。点进去可以看到,全是各种 onMessage,这样就不涉及反射,都是 override 了。我喜欢 explicit 的方法。

    性能

    目前业务是用来接入券商。还没做过大规模的测试,但按当前测试数据估计每秒可发单 100 个以上,性能满足当前业务需求。

    相关参考

    1. https://www.quickfixj.org/usermanual/2.1.0/
    2. https://blog.csdn.net/u011279740/category_1517545.html
  • FIX 协议开发(2):QuickFIX/J 入门

    本系列导航

    FIX 协议开发(1):协议介绍及开发方案
    FIX 协议开发(2):QuickFIX/J 入门 (本文)
    FIX 协议开发(3):QuickFIX/J 实战经验小结

    安装

    QuickFIX/J 的官网是 https://www.quickfixj.org,其 GitHub 是 https://github.com/quickfix-j/quickfixj。撰写本文时,GitHub 上的版本是 2.2,官网上的文档给到了 2.1 的版本。所以这里选择安装 2.1,避免与文档不匹配。

    我们通过 Maven 来引入 quickfixj 库,见此页的“Maven Integration”部分,在 pom.xml 中添加对应 xml 片段(该网页中的xml引入的是2.0.0版本的库,或许是笔误,我将其改为2.1.0)即可:

    <!-- QuickFIX/J dependencies -->
    <dependency>
        <groupId>org.quickfixj</groupId>
        <artifactId>quickfixj-core</artifactId>
        <version>2.1.0</version>
    </dependency>
    <!-- 选择所需版本的messages -->
    <dependency>
        <groupId>org.quickfixj</groupId>
        <artifactId>quickfixj-messages-fix42</artifactId>
        <version>2.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.22</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.22</version>
    </dependency>
    阅读更多…
  • FIX 协议开发(1):协议介绍及开发方案

    本系列导航

    FIX 协议开发(1):协议介绍及开发方案(本文)
    FIX 协议开发(2):QuickFIX/J 入门
    FIX 协议开发(3):QuickFIX/J 实战经验小结

    本文背景

    公司因业务需要,准备接入 FIX 协议。在调研过程中,我发现中文的 FIX 协议相关资料不太多,准备边学边记录,预计会写 3 篇左右。

    FIX 协议简介

    FIX(Financial Information eXchange Protocol,金融信息交换协议)是由国际FIX协会组织提供的一个开放式协议,目的是推动国际贸易电子化进程,在各类参与者之间,包括投资经理、经纪人、买方、卖方建立起实时的电子化通讯协议。

    FIX协议的目标是把各类证券金融业务需求流程格式化,成为一个可用计算机语言描述的功能流程,并在每个业务功能接口上统一交换格式,方便各个功能模块的连接。

    消息格式

    FIX 协议消息均由多个 “key=value” 组成。其中 key 可以是协议规定的字段,或自定义字段。协议规定的key可查询 FIX 协议字典,【不同版本的 FIX 协议均有其字典,用于开发的库一般也有自带;也可参考第三方,如 wireshark】例如 8 代表 begin string,34 代表消息的序列号,52 代表时间戳等。自定义字段不与规定的 key 重复,供金融机构定制,开发时需要向对应金融机构获取其专有字段的字典。只要有了对应的字典,就可以读懂 FIX 数据包的内容。

    一般来说,一个 消息由“头部 + 消息体 + 尾部”构成。头部包含一些必要的字段,例如 BeginString (8)、BodyLength (9)、MsgType (35)、MsgSeqNum (34)、SenderCompID (49) 等,尾部包含的必要字段是 CheckSum (10)。

    FIX 登陆消息示例(假设”^”是分隔符):

    8=FIX.4.3^9=65^35=A^34=1^49=TESTACC^52=20130703-15:55:08.609^56=EXEC^98=0^108=30^10=225^

    对照字典可知,BeginString (8) 是 FIX.4.3;BodyLength (9) 是 65 字节;MsgType (35) 是 A,A 对应 logon 操作;MsgSeqNum (34) 是 1,即这是我方发送的第 1 个消息。

    有关更详细的协议介绍,可参考 https://blog.51cto.com/9291927/2536105

    开发方案

    不使用库

    由上面的示例可以发现,FIX 协议十分简单。可以不需要依赖第三方库,手动查字典构造消息
    8=FIX.4.3^9=65^35=A^34=1<省略>
    再通过标准的 socket 通信,即可完成交互。

    这个方案自由度最高,不依赖底层开发语言,但开发流程与查字典较为繁琐,后续维护也不太方便。

    Python 库 simplefix

    simplefix 是一个 FIX 协议的简易实现。它使用户可以方便地任意构造 FIX 消息,非常适合学习、测试协议。但这个库不包含任何网络收发、FIX 异常处理等功能模块。因此,开发 FIX 客户端时,我使用该库构造数据包,然后通过标准 socket 发送,再分析其网络底层交互。

    示例:发送 logon 消息的代码

    import socket
    import time
    
    import simplefix
    from simplefix.constants import (ENCRYPTMETHOD_NONE, MSGTYPE_LOGON,
                                     TAG_BEGINSTRING, TAG_CLIENTID,
                                     TAG_ENCRYPTMETHOD, TAG_HEARTBTINT,
                                     TAG_MSGSEQNUM, TAG_MSGTYPE, TAG_SENDER_COMPID,
                                     TAG_SENDING_TIME, TAG_TARGET_COMPID)
    
    HOST = '1.2.3.45'
    PORT = 9000
    CLIENT_ID = 12345678
    PASSWORD = 'mypassword'
    
    seq_num = 1  # 需维护msg sequence number,每次发送后加1
    
    
    if __name__ == "__main__":
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)  # 长连接
        sock.connect((HOST, PORT))
    
        # logon
        msg_logon = simplefix.FixMessage()
        msg_logon.append_pair(TAG_BEGINSTRING, 'FIX.4.2')
        msg_logon.append_pair(TAG_MSGSEQNUM, seq_num)  # 需维护msg sequence number,每次发送后加1
        msg_logon.append_pair(TAG_SENDER_COMPID, 'FIXTEST001')
        msg_logon.append_utc_timestamp(TAG_SENDING_TIME)
        msg_logon.append_pair(TAG_TARGET_COMPID, 'TESTENV')
    
        msg_logon.append_pair(TAG_MSGTYPE, MSGTYPE_LOGON)  # 类型
    
        msg_logon.append_pair(TAG_ENCRYPTMETHOD, ENCRYPTMETHOD_NONE)
        msg_logon.append_pair(TAG_HEARTBTINT, 30)
        msg_logon_buffer = msg_logon.encode()
    
        sock.send(msg_logon_buffer)
        print('seq', seq_num, 'sent')
        seq_num += 1
    
    
        time.sleep(1)
        sock.close()

    多平台 QuickFix 引擎

    QuickFix 是全功能的 FIX 开源引擎,目前很多 Fix 解决方案都是根据或参考 QuickFix 实现的。目前(2020年10月)它有 C++、Python、Java、.NET、Go 和 Ruby 共 6 种语言的实现/接口。

    根据我司的情况,选择其中的 Java 实现 QuickFIX/J 进行下一步开发。其使用方法,将在下一篇文章继续。

  • 为OpenWrt/LEDE编译rpcapd 用于Wireshark远程抓包

    rpcapd是winpcap带的专门用来远程抓包的小工具,但是winpcap是给Windows用的,Linux下需要自己编译,如果要放到路由器上用,就要交叉编译了。

    以前编译都是直接在OpenWrt/LEDE的menuconfig里面选,现在不能选了,需要直接用toolchain手动编译。

    步骤如下:

    # 首先要把OpenWrt/LEDE的SDK下载或者编译出来
    # 以下的路径自行修改
    
    export PATH=$PATH:/root/lede/staging_dir/toolchain-mipsel_24kc_gcc-5.4.0_musl-1.1.16/bin
    export STAGING_DIR=/root/lede/staging_dir/toolchain-mipsel_24kc_gcc-5.4.0_musl-1.1.16
    
    export CC=mipsel-openwrt-linux-musl-gcc
    export CXX=mipsel-openwrt-linux-musl-g++
    export AR=mipsel-openwrt-linux-musl-ar
    export RANLIB=mipsel-openwrt-linux-musl-ranlib
    export ac_cv_linux_vers=4.4.61
    
    # 在https://www.winpcap.org/devel.htm 下载winpcap源码
    unzip source.zip
    cd winpcap/wpcap/libpcap
    chmod +x configure
    ./configure --build=x86_64-unknown-linux-gnu --host=mipsel-openwrt-linux --with-pcap=linux
    make
    
    # 在winpcap/wpcap/libpcap/pcap-int.h 里加上一行 #include <string.h> 否则在 strlcpy 处会报错
    cd rpcapd
    # 修改 Makefile
    # 修改编译器,改成CC=mipsel-openwrt-linux-gcc
    make
    
    file rpcapd
    # rpcapd: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1, dynamically linked, interpreter /lib/ld-musl-mipsel-sf.so.1, not stripped

    相关参考:https://talpachen.github.io/2016/07/20/Net/rpcapd/

     

    附我编译的
      适用于LEDE(musl)的ar71xx版rpcapd:rpcapd_ar71xx.7z
      适用于LEDE(musl)的ramips版rpcapd:rpcapd_ramips.7z

  • 使用fiddler抓手机https数据包

    原理

    我们手机在连Wi-Fi的时候,都有一个”代理服务器“的选项,而fiddler就是作为代理服务器,所以就能抓包了。而https呢,fiddler能作为中间人两边骗,所以还能抓https的包。

    步骤

    1. 开启抓包

    File => 勾选capture traffic

    2. 设置抓https和解密https

    Tools => fiddler options => https => capture https traffic => decrypt https traffic => Ignore server certificate errors
    由于对本机的数据包不感兴趣,所以把”from all processes” 改为 “from remote clients only”

    切换到Connections选项卡,勾选allow remote computers to connect,注意端口号是8888

    3. 安装certmaker插件

    默认的证书在Android和iOS下可能无效,所以,需要下载certmaker插件,双击安装后,重启fiddler。我用默认的证书,HTTPS确实解密不出来。

    4. 在手机上设置Wi-Fi代理

    主机为电脑IP,端口号为8888

    5. 给手机安装假的根证书

    在手机的浏览器进入http://电脑IP:8888,下载页面最下面的FiddlerRoot certificate,完成证书安装。

     

    相关参考:
    1. fiddler 手机 https 抓包, http://blog.csdn.net/wangjun5159/article/details/52202059

  • 使用Wireshark+SSH进行实时tcpdump远程抓包分析

    背景:有一台可以tcpdump的OpenWrt路由器,用它来抓下面的客户端的数据包。以前我一直都是把tcpdump的结果保存成.cap文件再传到PC上用Wireshark打开,直到今天,才学会了更方便快捷的办法。网上给出的命令是这样的:

    plink -ssh USER@HOST -pw PASS "tcpdump -s 0 -U -n -i br-lan -w - not port 22" | wireshark -k -i -

    其中plink是一个Windows下的命令行SSH客户端程序(Linux系统下改成ssh的相应命令)。试了一下,确实可以!

    为什么可以?

    由“|”可见它是通过管道来传输的,难道数据还能直接从输入输出传输?因此去看看文档,并找到了答案:

    在tcpdump的manpage中得知,最重要的参数是”-w -“,当文件名是”-“时,输出原数据包到stdout;-U参数是让数据包打印时直接输出到stdout而不是在输出缓存满后再输出;”-s 0″表示设置最大数据包长度为默认值(262144 Bytes)。

    而从Wireshark的帮助中可知,当抓包接口为”-“时,就从stdin读取数据,所以就ok了。

  • 使用iptables模拟Symmetric NAT

    如果想让一个P2P程序不能穿透,比较好的办法就是把NAT类型改为对称型的,即Symmetric.

    之前以为NAT类型对于一个路由器是固定的,后来google到了使用iptables模拟的方法,当然Symmetric NAT也有很多不同的具体实现形式,这里模拟的是其中的一种。

    系统为OpenWrt,执行命令

    WAN_IF = “eth0”  # 你的WAN接口
    iptables -t nat -A POSTROUTING -o $WAN_IF -j MASQUERADE --random

    即可。

     

    测试的结果是,对于同一个LAN IP:Port,对不同的Dst IP,映射到的WAN Port会差异比较大;而如果只改Dst Port,端口增量不大,可能为1。

     

    相关参考:
    http://albert-oma.blogspot.bg/2013/12/nat-router.html