Sep 3
#!/bin/bash

API=https://dnsapi.cn/Record.Ddns

IP_FILE=/tmp/dnspod_ip

function get_old_ip()
{
    ip=
    if [ -e "$IP_FILE" ]; then
        ip=`cat $IP_FILE`
    fi
    echo $ip
}

function save_ip()
{
    echo -n $1 > $IP_FILE
}

function get_new_ip()
{
    echo `nc ns1.dnspod.net 6666`
}

email=帐号邮箱
password=帐号密码  #dnspod就不能搞成个secret_key么!明文密码让人很不舒服啊。

domain_id=XXXXX #使用Domain.List API获取
record_id=YYYYY #使用Record.List API获取
sub_domain="zzz" #DDNS的二级域名

new_ip=`get_new_ip`
old_ip=`get_old_ip`

if [ "$new_ip" != "$old_ip" ];
then
    curl $API -d "format=json&login_email=$email&login_password=$password&domain_id=$domain_id&record_id=$record_id&sub_domain=$sub_domain&record_line=默认"
    save_ip $new_ip
fi

然后加入crontab,每隔15分钟跑一次进行更新

*/15 * * * * ~/bin/dnspod.sh
Jul 26
如果到Google去搜索,"How to find out which process is listening upon a port"这是第一篇文章。

事实上大部分文章都是告诉你,要么 lsof -i :80 要么 netstat -antulp | grep :80 就能找到httpd。

可是如果就这样的话,这篇BLOG就变成微博了。

事实上我的目的是希望通过编程找出这个PID,而不是调用某个命令。

第一个尝试是去看lsof的源码。找源码容易,apt-get source lsof 就行。但是源码跟大部分linux软件包一样,看起来相当晦涩。

第二个尝试是Google,但是能找到的都是命令版的。

第三个尝试是stackoverflow,没有直接搜到问题,于是准备自己提问,但是在“Questions that may already have your answer”里头找到了一篇“How to get the pid of a process that is listening on a certain port programmatically?” ( http://stackoverflow.com/questions/10996242 )。

Answer给出的步骤非常清晰:与netstat的实现一样,先读取 /proc/net/tcp 这个文件,第二个字段(local_address)冒号后面的是端口号(十六进制),第四个字段st为0A表示TCP_LISTEN,第10个字段是十进制的inode编号;而通过遍历 /proc/PIDs/fd 下面的链接,找到链接到形如 socket:[端口号] 的fd进行对比,就能知道哪些进程与该端口有一腿。

我在机器上监听的是8888端口,换成hex是22B8,但是在 /proc/net/tcp 中却找不到。幸而那篇文章的第二个Answer给了个提示,于是 strace /usr/sbin/lsof -i :8888 ,发现它还打开了 /proc/net/tcp6 (也就是对应IPv6的那个文件了)。过去一查,果然有,再顺着inode,对照lsof的结果一看,的确符合。

于是再去grep一把 lsof 的源码目录,发现在 00FAQ 文件中的 10.2.2 一节就说明了 lsof 的实现机制:
引用
Lsof identifies protocols by matching the node number associated
    with the /proc//fd entry to the node numbers found in
    selected files of the /proc/net sub-directory.  Currently
    /proc-based lsof examines these protocol files:
        /proc/net/ax25      (untested)
        /proc/net/ipx      (needs kernel patch)
        /proc/net/raw        /proc/net/raw6
        /proc/net/tcp        /proc/net/tcp6
        /proc/net/udp        /proc/net/udp6
        /proc/net/unix

看来在 Linux 下的实现方式确实只有这一种。顺便提一下,Stackoverflow上面的另一篇 http://stackoverflow.com/questions/4041003/c-what-process-is-listening-on-a-certain-port-in-windows 提到了,在Windows下可以用GetExtendedTcpTable/GetExtendedUdpTable来实现。

最后附上PHP实现的源码(这个代码用C/C++写确实蛋疼)
<?php

$filelist = array("/proc/net/tcp6", "/proc/net/tcp"); //udp/unix的就先不管了

$port2inode = array();

foreach ($filelist as $file)
{
    $lines = file($file);
    array_shift($lines);
    foreach ($lines as $line)
    {
        $values = split(" +", $line);
        list($addr, $port) = explode(":", $values[2]);
        $port = hexdec($port);
        $port2inode[$port] = $values[10];
    }
}

if ($argc < 2)
    die("usage: php {$argv[0]} PORT\n");
$findport = $argv[1];

if(!isset($port2inode[$findport]))
    die("$findport not listened\n");

$procdir = scandir("/proc");
natcasesort($procdir);

foreach ($procdir as $pid)
{
    $path = "/proc/$pid/fd";
    if (!is_readable($path) || !is_numeric($pid) || !is_dir($path)) continue;
    $dir = scandir($path);
    foreach ($dir as $file)
    {
        $link = "$path/$file";
        if (!is_link($link)) continue;
        $real = readlink($link);
        if (substr($real, 0, 7) == "socket:")
        {
            $port = substr($real, 8, -1);
            if ($port == $port2inode[$findport])
            {
                echo $pid, ": ", readlink("/proc/$pid/exe") . "\n";
                continue;
            }
        }
    }
}
?>
Apr 20
很多应用层协议都有HeartBeat机制,通常是客户端每隔一小段时间向服务器发送一个数据包,通知服务器自己仍然在线,并传输一些可能必要的数据。使用心跳包的典型协议是IM,比如QQ/MSN/飞信等协议。

学过TCP/IP的同学应该都知道,传输层的两个主要协议是UDP和TCP,其中UDP是无连接的、面向packet的,而TCP协议是有连接、面向流的协议。

所以非常容易理解,使用UDP协议的客户端(例如早期的“OICQ”,听说OICQ.com这两天被抢注了来着,好古老的回忆)需要定时向服务器发送心跳包,告诉服务器自己在线。

然而,MSN和现在的QQ往往使用的是TCP连接了,尽管TCP/IP底层提供了可选的KeepAlive(ACK-ACK包)机制,但是它们也还是实现了更高层的心跳包。似乎既浪费流量又浪费CPU,有点莫名其妙。

具体查了下,TCP的KeepAlive机制是这样的,首先它貌似默认是不打开的,要用setsockopt将SOL_SOCKET.SO_KEEPALIVE设置为1才是打开,并且可以设置三个参数tcp_keepalive_time/tcp_keepalive_probes/tcp_keepalive_intvl,分别表示连接闲置多久开始发keepalive的ack包、发几个ack包不回复才当对方死了、两个ack包之间间隔多长,在我测试的Ubuntu Server 10.04下面默认值是7200秒(2个小时,要不要这么蛋疼啊!)、9次、75秒。于是连接就了有一个超时时间窗口,如果连接之间没有通信,这个时间窗口会逐渐减小,当它减小到零的时候,TCP协议会向对方发一个带有ACK标志的空数据包(KeepAlive探针),对方在收到ACK包以后,如果连接一切正常,应该回复一个ACK;如果连接出现错误了(例如对方重启了,连接状态丢失),则应当回复一个RST;如果对方没有回复,服务器每隔intvl的时间再发ACK,如果连续probes个包都被无视了,说明连接被断开了。

这里有一篇非常详细的介绍文章: http://tldp.org/HOWTO/html_single/TCP-Keepalive-HOWTO ,包括了KeepAlive的介绍、相关内核参数、C编程接口、如何为现有应用(可以或者不可以修改源码的)启用KeepAlive机制,很值得详读。

这篇文章的2.4节说的是“Preventing disconnection due to network inactivity”,阻止因网络连接不活跃(长时间没有数据包)而导致的连接中断,说的是,很多网络设备,尤其是NAT路由器,由于其硬件的限制(例如内存、CPU处理能力),无法保持其上的所有连接,因此在必要的时候,会在连接池中选择一些不活跃的连接踢掉。典型做法是LRU,把最久没有数据的连接给T掉。通过使用TCP的KeepAlive机制(修改那个time参数),可以让连接每隔一小段时间就产生一些ack包,以降低被T掉的风险,当然,这样的代价是额外的网络和CPU负担。

前面说到,许多IM协议实现了自己的心跳机制,而不是直接依赖于底层的机制,不知道真正的原因是什么。

就我看来,一些简单的协议,直接使用底层机制就可以了,对上层完全透明,降低了开发难度,不用管理连接对应的状态。而那些自己实现心跳机制的协议,应该是期望通过发送心跳包的同时来传输一些数据,这样服务端可以获知更多的状态。例如某些客户端很喜欢收集用户的信息……反正是要发个包,不如再塞点数据,否则包头又浪费了……

大概就是这样吧,如果有大牛知道真正的原因,还望不吝赐教。


@2012-04-21

p.s. 通过咨询某个做过IM的同事,参考答案应该是,自己实现的心跳机制通用,可以无视底层的UDP或TCP协议。如果只是用TCP协议的话,那么直接使用KeepAlive机制就足够了。

@2015-09-14
补充一下 @Jack的回复:
“心跳除了说明应用程序还活着(进程还在,网络通畅),更重要的是表明应用程序还能正常工作。而 TCP keepalive 有操作系统负责探查,即便进程死锁,或阻塞,操作系统也会如常收发 TCP keepalive 消息。对方无法得知这一异常。摘自《Linux 多线程服务端编程》”
Feb 27

高延迟SSH部分解决方案 不指定

felix021 @ 2012-2-27 21:27 [IT » 网络] 评论(1) , 引用(0) , 阅读(18741) | Via 本站原创
vps在国外,延迟总有那么200~300ms,一来一回,500ms是免不了了。可是默认情况下,你每输入一个字符,ssh客户端(openssh/putty/securecrt)都会发送给服务器,然后服务器将响应返回。

典型ssh情况下是执行命令,比如ls,网络交互是:发送 l 给svr, svr返回 l ,显示 l ,发送 s 给svr,svr返回 s ,显示 s ,发送回车给svr,svr执行 ls ,返回 ls 的输出。也就是说,光输入一个ls命令就至少需要1s+的时间。但如果是要输入一个很复杂的命令,也许还没输入完,你就崩溃了。

采用putty(windows版ok,linux版未测试)内建的Local Echo和Local Line Editing支持,可以部分地解决这个问题:默认配置下,登录以后点击左上角的Putty图标,选择change settings=>Terminal,将Local Echo和Local line editing改成force on,就可以允许你在本地编辑一行命令,按下回车,然后命令才被发送到服务器。结果是服务器接收一整条命令,然后显示一整条命令,然后再输出这条命令的执行结果。

相应的代价就是:
1. 没法使用自动补全和其他bash/readline的快捷键了;
2. 使用vi这类程序的时候,就没法正常编辑了,这时需要再把这两个选项关闭。。。(为什么没有快捷键………………)
Feb 25
花了两天的时间在libevent上,想总结下,就以写简单tutorial的方式吧,貌似没有一篇简单的说明,让人马上就能上手用的。

首先给出官方文档吧: http://libevent.org ,首页有个Programming with Libevent,里面是一节一节的介绍libevent,但是感觉信息量太大了,而且还是英文的-。-(当然,如果想好好用libevent,看看还是很有必要的),还有个Reference,大致就是对各个版本的libevent使用doxgen生成的文档,用来查函数原型和基本用法什么的。

下面假定已经学习过基本的socket编程(socket,bind,listen,accept,connect,recv,send,close),并且对异步/callback有基本认识。

基本的socket编程是阻塞/同步的,每个操作除非已经完成或者出错才会返回,这样对于每一个请求,要使用一个线程或者单独的进程去处理,系统资源没法支撑大量的请求(所谓c10k problem),例如内存(默认情况下每个线程需要占用2~8M的栈空间),以及进程切换带来的原因等。posix定义了可以使用异步的select系统调用,但是因为其采用了轮询的方式来判断某个fd是否变成active,效率不高[O(n)],连接数一多,也还是撑不住。于是各系统分别提出了基于异步/callback的系统调用,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由于在内核层面做了支持,所以可以用O(1)的效率查找到active的fd。基本上,libevent就是对这些高效IO的封装,提供统一的API,简化开发。

libevent大概是这样的:

    默认情况下是单线程的(可以配置成多线程,如果有需要的话),每个线程有且只有一个event_base,对应一个struct event_base结构体(以及附于其上的事件管理器),用来schedule托管给它的一系列event,可以和操作系统的进程管理类比,当然,要更简单一点。当一个事件发生后,event_base会在合适的时间(不一定是立即)去调用绑定在这个事件上的函数(传入一些预定义的参数,以及在绑定时指定的一个参数),直到这个函数执行完,再返回schedule其他事件。
//创建一个event_base
struct event_base *base = event_base_new();
assert(base != NULL);


    event_base内部有一个循环,循环阻塞在epoll/kqueue等系统调用上,直到有一个/一些事件发生,然后去处理这些事件。当然,这些事件要被绑定在这个event_base上。每个事件对应一个struct event,可以是监听一个fd或者POSIX信号量之类(这里只讲fd了,其他的看manual吧)。struct event使用event_new来创建和绑定,使用event_add来启用:
//创建并绑定一个event
struct event *listen_event;
//参数:event_base, 监听的fd,事件类型及属性,绑定的回调函数,给回调函数的参数
listen_event = event_new(base, listener, EV_READ|EV_PERSIST, callback_func, (void*)base);
//参数:event,超时时间(struct timeval *类型的,NULL表示无超时设置)
event_add(listen_event, NULL);

    注:libevent支持的事件及属性包括(使用bitfield实现,所以要用 | 来让它们合体)
    (a) EV_TIMEOUT: 超时
    (b) EV_READ: 只要网络缓冲中还有数据,回调函数就会被触发
    (c) EV_WRITE: 只要塞给网络缓冲的数据被写完,回调函数就会被触发
    (d) EV_SIGNAL: POSIX信号量,参考manual吧
    (e) EV_PERSIST: 不指定这个属性的话,回调函数被触发后事件会被删除
    (f) EV_ET: Edge-Trigger边缘触发,参考EPOLL_ET


    然后需要启动event_base的循环,这样才能开始处理发生的事件。循环的启动使用event_base_dispatch,循环将一直持续,直到不再有需要关注的事件,或者是遇到event_loopbreak()/event_loopexit()函数。
//启动事件循环
event_base_dispatch(base);


    接下来关注下绑定到event的回调函数callback_func:传递给它的是一个socket fd、一个event类型及属性bit_field、以及传递给event_new的最后一个参数(去上面几行回顾一下,把event_base给传进来了,实际上更多地是分配一个结构体,把相关的数据都撂进去,然后丢给event_new,在这里就能取得到了)。其原型是:
typedef void(* event_callback_fn)(evutil_socket_t sockfd, short event_type, void *arg)


    对于一个服务器而言,上面的流程大概是这样组合的:
    1. listener = socket(),bind(),listen(),设置nonblocking(POSIX系统中可使用fcntl设置,windows不需要设置,实际上libevent提供了统一的包装evutil_make_socket_nonblocking)
    2. 创建一个event_base
    3. 创建一个event,将该socket托管给event_base,指定要监听的事件类型,并绑定上相应的回调函数(及需要给它的参数)。对于listener socket来说,只需要监听EV_READ|EV_PERSIST
    4. 启用该事件
    5. 进入事件循环
    ---------------
    6. (异步) 当有client发起请求的时候,调用该回调函数,进行处理。

    问题:为什么不在listen完马上调用accept,获得客户端连接以后再丢给event_base呢?这个问题先想想噢。

    回调函数要做什么事情呢?当然是处理client的请求了。首先要accept,获得一个可以与client通信的sockfd,然后……调用recv/send吗?错!大错特错!如果直接调用recv/send的话,这个线程就阻塞在这个地方了,如果这个客户端非常的阴险(比如一直不发消息,或者网络不好,老是丢包),libevent就只能等它,没法处理其他的请求了——所以应该创建一个新的event来托管这个sockfd。

    在老版本libevent上的实现,比较罗嗦[如果不想详细了解的话,看下一部分]。
    对于服务器希望先从client获取数据的情况,大致流程是这样的:
    1. 将这个sockfd设置为nonblocking
    2. 创建2个event:
        event_read,绑上sockfd的EV_READ|EV_PERSIST,设置回调函数和参数(后面提到的struct)
        event_write,绑上sockfd的EV_WRITE|EV_PERSIST,设置回调函数和参数(后面提到的struct)
    3. 启用event_read事件
    ------
    4. (异步) 等待event_read事件的发生, 调用相应的回调函数。这里麻烦来了:回调函数用recv读入的数据,不能直接用send丢给sockfd了事——因为sockfd是nonblocking的,丢给它的话,不能保证正确(为什么呢?)。所以需要一个自己管理的缓存用来保存读入的数据中(在accept以后就创建一个struct,作为第2步回调函数的arg传进来),在合适的时间(比如遇到换行符)启用event_write事件【event_add(event_write, NULL)】,等待EV_WRITE事件的触发
    ------
    5. (异步) 当event_write事件的回调函数被调用的时候,往sockfd写入数据,然后删除event_write事件【event_del(event_write)】,等待event_read事件的下一次执行。
    以上步骤比较晦涩,具体代码可参考官方文档里面的【Example: A low-level ROT13 server with Libevent】


    由于需要自己管理缓冲区,且过程晦涩难懂,并且不兼容于Windows的IOCP,所以libevent2开始,提供了bufferevent这个神器,用来提供更加优雅、易用的API。struct bufferevent内建了两个event(read/write)和对应的缓冲区【struct evbuffer *input, *output】,并提供相应的函数用来操作缓冲区(或者直接操作bufferevent)。每当有数据被读入input的时候,read_cb函数被调用;每当output被输出完的时候,write_cb被调用;在网络IO操作出现错误的情况(连接中断、超时、其他错误),error_cb被调用。于是上一部分的步骤被简化为:
    1. 设置sockfd为nonblocking
    2. 使用bufferevent_socket_new创建一个struct bufferevent *bev,关联该sockfd,托管给event_base
    3. 使用bufferevent_setcb(bev, read_cb, write_cb, error_cb, (void *)arg)将EV_READ/EV_WRITE对应的函数
    4. 使用bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST)来启用read/write事件
    ------
    5. (异步)
        在read_cb里面从input读取数据,处理完毕后塞到output里(会被自动写入到sockfd)
        在write_cb里面(需要做什么吗?对于一个echo server来说,read_cb就足够了)
        在error_cb里面处理遇到的错误
    *. 可以使用bufferevent_set_timeouts(bev, struct timeval *READ, struct timeval *WRITE)来设置读写超时, 在error_cb里面处理超时。
    *. read_cb和write_cb的原型是
        void read_or_write_callback(struct bufferevent *bev, void *arg)
      error_cb的原型是
        void error_cb(struct bufferevent *bev, short error, void *arg) //这个是event的标准回调函数原型
      可以从bev中用libevent的API提取出event_base、sockfd、input/output等相关数据,详情RTFM~
   

    于是代码简化到只需要几行的read_cb和error_cb函数即可:
void read_cb(struct bufferevent *bev, void *arg) {
    char line[256];
    int n;
    evutil_socket_t fd = bufferevent_getfd(bev);
    while (n = bufferevent_read(bev, line, 256), n > 0)
        bufferevent_write(bev, line, n);
}

void error_cb(struct bufferevent *bev, short event, void *arg) {
    bufferevent_free(bev);
}


    于是一个支持大并发量的echo server就成型了!下面附上无注释的echo server源码,110行,多抄几遍,就能完全弄懂啦!更复杂的例子参见官方文档里面的【Example: A simpler ROT13 server with Libevent】
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>

#include <event2/event.h>
#include <event2/bufferevent.h>

#define LISTEN_PORT 9999
#define LISTEN_BACKLOG 32

void do_accept(evutil_socket_t listener, short event, void *arg);
void read_cb(struct bufferevent *bev, void *arg);
void error_cb(struct bufferevent *bev, short event, void *arg);
void write_cb(struct bufferevent *bev, void *arg);

int main(int argc, char *argv[])
{
    int ret;
    evutil_socket_t listener;
    listener = socket(AF_INET, SOCK_STREAM, 0);
    assert(listener > 0);
    evutil_make_listen_socket_reuseable(listener);

    struct sockaddr_in sin;
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = 0;
    sin.sin_port = htons(LISTEN_PORT);

    if (bind(listener, (struct sockaddr *)&sin, sizeof(sin)) < 0) {
        perror("bind");
        return 1;
    }

    if (listen(listener, LISTEN_BACKLOG) < 0) {
        perror("listen");
        return 1;
    }

    printf ("Listening...\n");

    evutil_make_socket_nonblocking(listener);

    struct event_base *base = event_base_new();
    assert(base != NULL);
    struct event *listen_event;
    listen_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
    event_add(listen_event, NULL);
    event_base_dispatch(base);

    printf("The End.");
    return 0;
}

void do_accept(evutil_socket_t listener, short event, void *arg)
{
    struct event_base *base = (struct event_base *)arg;
    evutil_socket_t fd;
    struct sockaddr_in sin;
    socklen_t slen = sizeof(sin);
    fd = accept(listener, (struct sockaddr *)&sin, &slen);
    if (fd < 0) {
        perror("accept");
        return;
    }
    if (fd > FD_SETSIZE) { //这个if是参考了那个ROT13的例子,貌似是官方的疏漏,从select-based例子里抄过来忘了改
        perror("fd > FD_SETSIZE\n");
        return;
    }

    printf("ACCEPT: fd = %u\n", fd);

    struct bufferevent *bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
    bufferevent_setcb(bev, read_cb, NULL, error_cb, arg);
    bufferevent_enable(bev, EV_READ|EV_WRITE|EV_PERSIST);
}

void read_cb(struct bufferevent *bev, void *arg)
{
#define MAX_LINE    256
    char line[MAX_LINE+1];
    int n;
    evutil_socket_t fd = bufferevent_getfd(bev);

    while (n = bufferevent_read(bev, line, MAX_LINE), n > 0) {
        line[n] = '\0';
        printf("fd=%u, read line: %s\n", fd, line);

        bufferevent_write(bev, line, n);
    }
}

void write_cb(struct bufferevent *bev, void *arg) {}

void error_cb(struct bufferevent *bev, short event, void *arg)
{
    evutil_socket_t fd = bufferevent_getfd(bev);
    printf("fd = %u, ", fd);
    if (event & BEV_EVENT_TIMEOUT) {
        printf("Timed out\n"); //if bufferevent_set_timeouts() called
    }
    else if (event & BEV_EVENT_EOF) {
        printf("connection closed\n");
    }
    else if (event & BEV_EVENT_ERROR) {
        printf("some other error\n");
    }
    bufferevent_free(bev);
}
Feb 24
上一篇TCP Port Multiplexing提到,proxy_connect模块貌似不能限制被代理的HOST。也就是说,通过这台服务器可以PROXY到任意机器这样是有风险的。可是mod_proxy模块又没有提供专门的配置来限制可代理的HOST,所以只好舍近求远写了个multiplexer。

实际上要解决这个问题也是很简单的,可以想到,知道找到相应的地方,直接修改源码,只需要几行就行。虽然以前没有搞过apache的module,但是跟php的也比较类似了。看了下代码发现,前文的“貌似”两个字可以去掉了,的确没有什么地方限制了可代理的HOST。

在Ubuntu下尤其简单:(假定已经配置好了mod_proxy/mod_proxy_connect,比如AllowConnect 22、ProxyRequests On什么的)
引用
$ apt-get source apache2
$ cd apache2-2.2.14/modules/proxy
$ vi mod_proxy_connect.c +123    #就是打开123行,这里刚从http请求里分离出HOST

添加如下几行:
    char *allowed_hosts[] = {
        "SOME_HOST_NAME",
        "SOME_IP",
        "127.0.0.1",
        "localhost"
    };
    int hosts_num = sizeof(allowed_hosts) / sizeof(allowed_hosts[0]);
    int k;
    for (k = 0; k < hosts_num; k++) {
        if (strncmp(uri.hostname, allowed_hosts[k], strlen(allowed_hosts[k])) == 0) {
            break;
        }
    }
    if (k == hosts_num) {
        return ap_proxyerror(r, HTTP_BAD_GATEWAY,
              apr_pstrcat(p, "host not allowed for: ", uri.hostname, NULL));
    }

保存后,执行
引用
$ sudo apt-get install apache2-dev
$ apxs2 -c mod_proxy_connect.c proxy_util.c
$ cp .libs/mod_proxy_connect.so /usr/lib/apache2/modules #这里会覆盖现有的,最好备份一下
$ sudo /etc/init.d/apache2 restart

然后再试试看?
Feb 24

TCP Port Multiplexing 不指定

felix021 @ 2012-2-24 17:00 [IT » 网络] 评论(0) , 引用(0) , 阅读(6002) | Via 本站原创
  很久很久以前就想过这个问题,是不是可以写这样一个frontend proxy,通过client的请求来判断需要连接的端口号,实现http/ssh共用80端口呢?

  不久后我发现,通过apache的mod_proxy+mod_proxy_connect可以使用HTTP的CONNECT HOST:PORT命令来代理22端口,也就是tunneling ssh over http, google可以发现很多都用到了proxytunnel,当作openssh client的代理;securecrt内建支持,使用Option->Global里面的firewall,其实就是proxy。

  于是这个想法被搁置了,好几年。为什么又想起了呢…………是因为,proxy_connect模块貌似不能限制被代理的HOST。也就是说,通过这台服务器可以PROXY到任意机器这样是有风险的。

  于是昨天花了半个下午+半个晚上看libevent的文档。其实本来是打算找个速成教程的,但是好像没有,所以还是老老实实看文档。当天晚上写出了第一个版本的multiplexer,各种BUG。无奈,第二天干脆推倒重来,写了第二个版本,还是各种BUG。各种蛋疼之后,终于实现了一个基本可用的multiplexer,250行左右,挺短的代码。

  遇到的主要障碍包括:

1. bufferevent *bev的read_cb函数被调用以后,应该用一个while循环把bev里面所有的数据都读出来(比如使用bufferevent_read);

2. set_nonblocking的操作应该在bind/listen/connect等操作之后完成;否则这些操作也是nonblocking了……

3. 最好加上 |EV_PERSIST ,免得每次callback的时候都要再enable或者add一次;

4. ssh协议对client/server哪个先说话貌似没要求,比如openssh是等server先说话,而securecrt是自己先说话,所以这个multiplexer的实现很蛋疼,需要设置一个timeout,如果client过了一会儿没说话,就forward给sshd。

5. 对于4,更蛋疼的是,(我用的libevent2.0.17-stable)bufferevent_set_timeouts(bev, NULL, NULL)貌似不能清除timeout(或许是个BUG?),所以使用了一个struct timeval tv = {86400*1024, 0} 来绕过timeout的问题。

大概就这些,代码如下
Feb 22

RELAY 不指定

felix021 @ 2012-2-22 12:06 [IT » 网络] 评论(6) , 引用(0) , 阅读(6800) | Via 本站原创
挑战:某Linux机器A有外网访问权限,但其上运行的ssh服务(22端口)仅对内网开放,希望通过外网的某Linux机器B进行RELAY,实现对机器A的ssh登录。特别地,只要能够进行ssh连接,就可以建立socks代理,实现内网其余机器的访问。

原理:(ssh服务器)A:22 <---- 连接 ~ 连接 ---->  监听B:10001 ~ 监听B:10002 <---- 连接(ssh客户端)
    其中的 ~ 表示将两个连接/监听的socket的输入和输出分别连接起来。

简单实现(nc + shell):
1. 在机器B上运行
引用
mkfifo pipe
nc -l -p 10002 < pipe | nc -l -p 10001 > pipe

2. 在机器A上运行
引用
mkfifo pipe
nc localhost 22 < pipe | nc [B.ip] 10002 > pipe

3. 使用ssh客户端连接B:10001即可。

简单实现的主要问题是,一旦ssh客户端断开连接,部分/所有的nc会结束,无法再建立连接。所以需要改进:
1. 写一个死循环脚本来保证nc的运行,例如  for ((;;)); do nc localhost 22 <pipe | nc [B.ip] 10002 >;pipe; done
2. 将该脚本放入 /etc/rc.local ,保证每次开机后自动运行。

还有一个蛋疼的问题是,(在我的测试中)如果ssh客户端被强制断开连接(不是 $exit ),B上面监听10002端口的那个nc不一定会结束。虽然我特意安排了B机器的脚本管道前监听10002,管道后监听10001,希望能利用SIGPIPE来搞定,但是系统似乎抽风。所以还是需要一个机制来保证一旦某个nc结束了,另一个nc也会结束。可能还有一些其他更蛋疼的情况,无法一一列出来。

为了解决nc不结束的蛋疼情况,可以用脚本来实现:记录2个nc的PID,然后定时grep之。如果只剩下1个,就把另一个也kill掉。不过我没有采用这个方案,而是写了一个c程序来处理,pipe出两对fd,fork出两个child,把两对fd dup成两个child的stdin/stdout,child分别exec执行nc,然后wait之,当wait返回以后,就用kill向两个pid送个SIGTERM,结束。然后进入下一轮循环

代码如下(此代码用于B机器,A机器只要稍微修改下exec的参数就行了):
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <assert.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/wait.h>

void error(const char *fmt, ...)
{
    perror("Infomation");
    fprintf(stderr, "  => ");
    va_list ap;
    va_start(ap, fmt);
    vfprintf(stderr, fmt, ap);
    va_end(ap);
}

int main(int argc, char *argv[])
{
    int fd_left[2], fd_right[2];
    if (pipe(fd_left) < 0 ) {
        perror("pipe left failed");
        return 1;
    }
    if (pipe(fd_right) < 0 ) {
        perror("pipe right failed");
        return 1;
    }

    pid_t pid1 = fork();
    if (pid1 < 0) {
        perror("fork1");
        return 1;
    }

    if (pid1 == 0) {
        //child
        if (dup2(fd_left[0], STDIN_FILENO) < 0) {
            error("dup2@1@stdin");
        }
        if (dup2(fd_right[1], STDOUT_FILENO) < 0) {
            error("dup2@1@stdout");
        }
        execlp("nc", "nc", "-l", "-p", "10001", NULL);
        perror("execlp");
        return 1;
    }
    fprintf(stderr, "pid1 = %d\n", pid1);

    //parent
    pid_t pid2 = fork();
    if (pid2 < 0) {
        perror("fork2");
        return 1;
    }

    if (pid2 == 0) {
        //child
        if (dup2(fd_right[0], STDIN_FILENO) < 0) {
            error("dup2@1@stdin");
        }
        if (dup2(fd_left[1], STDOUT_FILENO) < 0) {
            error("dup2@1@stdout");
        }
        execlp("nc", "nc", "-l", "-p", "10002", NULL);
        perror("execlp");
        return 1;
    }
    fprintf(stderr, "pid2 = %d\n", pid2);

    int status;
    pid_t pid = wait(&status);
    error("Process[%d] exits\n", pid);

    kill(pid1, SIGTERM);
    kill(pid2, SIGTERM);

    return 0;
}
分页: 3/26 第一页 上页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]