看了tcpcopy的源码,php 和 python 都可以操作raw socket,因此,用php 和 python 实现了tcpcopy,代码比较简单(有些地方偷懒了~~)。
tcpcopy关键点,理认上来说,只要得到tcp请求报文中数据部分,在ip层转发到测试服务器,即可实现流量复制。
因此,只需要维护tcp会话即可维护一个假的tcp连接,骗过测试服务器即可。
代码实现,主要以TCP状态机为基础,同时考虑抓包的无序问题,结合tcpdump调试,用php实现tcpcopy,用python实现intercept。
以下代码,经测试,客户机发起5000个http请求(大约50个长连接),流量全部会复制到测试机上。
其实,tcpcopy与lvs、nat、运营商流量劫持工作原理类似,都是通过欺骗tcp协议栈达到目的; 同理,通过在应用层处理原始套接字,也可以实现lvs负载均衡功能(后续可能会尝试)。
Code:
1. tcpcopy.php
两个进程工作,一个进程负责抓包并放入queue中; 另一个进程负责消费抓到的数据包,因此对tcpcopy进程效率要求不高。
<?php
date_default_timezone_set('PRC');
ini_set('memory_limit','512M');
error_reporting(E_ALL);
ini_set( 'display_errors', 'On' );
ini_set( "log_errors", "On" );
ini_set( "error_log", "/tmp/php_error.log" );
$local_ip = "192.168.56.101";
$src_ip = "192.168.56.101";
$dest_ip = "192.168.56.102";
$src_port = 50000+rand(1,10000);
$src_port = 50000;
$local_udp_port = 20000;
$g_remote_test_ip = "192.168.56.102";
$dest_port = 8080;
$seq_num = 1000000000+rand(1,1000000000);
$seq_num = 1;
$ack_num = 0;
$g_fake_port_indx = 20000;
class RawSocket{
public $s;
private $dest_ip;
private $dest_port;
const FIN = 1;
const SYN = 2;
const ACK = 16;
/*
private $seq_num;
private $ack_num
*/
public function create_listen_udp( $local_ip, $local_port ){
// var_dump( func_get_args( ) );
// $local_ip = "0.0.0.0";
// $local_port = 53;
$s = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
// socket_set_nonblock( $s );
$bind_ret = socket_bind($s, $local_ip, $local_port );
if( $s === false
|| $bind_ret === false ){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return false;
}
/*
var_dump( socket_getsockname( $s , $addr, $port ) );
var_dump( $addr );
var_dump( $port );
*/
$this->s = $s;
return $this->s;
}
// listen local ip
public function create_listen_socket( $local_ip, $remote_ip=null ){
$one = 1;
$raw_socket = socket_create( AF_INET, SOCK_RAW, getprotobyname("tcp") );
// $raw_socket = socket_create( AF_INET, SOCK_RAW, getprotobyname("icmp") ); /* no need bind,connect, 不能抓不属于自己的包 */
// $raw_socket = socket_create( AF_INET, SOCK_RAW, 1 );
socket_set_nonblock( $raw_socket );
// trick, 3 is stand for header control
$set_ret = socket_set_option( $raw_socket, getprotobyname("ip"), 3, $one);
$conn_ret = $bind_ret = true;
if( isset( $local_ip) ){
// 设置 dest ip
$bind_ret = socket_bind($raw_socket, $local_ip );
}
if( isset( $remote_ip) ){
// 不限制 source_ip
// $conn_ret = socket_connect( $raw_socket, $remote_ip, 0 );
}
// $bind_ret = socket_connect( $raw_socket, $remote_ip, 0 );
// $bind_ret = socket_bind($raw_socket, $local_ip );
if( $raw_socket === false
|| $set_ret === false
|| $conn_ret === false
|| $bind_ret === false ){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return false;
}
$this->s = $raw_socket;
return $this->s;
}
// send raw socket to ip
public function create_send_socket( $remote_ip ){
$this->dest_ip = $remote_ip;
$dest_ip_fake = "127.0.0.1";
$dest_ip_fake = "192.168.56.101";
$dest_port_fake = 8080;
$one = 1;
$raw_socket = socket_create( AF_INET, SOCK_RAW, getprotobyname("tcp") );
// $raw_socket = socket_create( AF_INET, SOCK_RAW, getprotobyname("ip") );
socket_set_nonblock( $raw_socket );
// trick, 3 is stand for header control
$set_ret = socket_set_option( $raw_socket, getprotobyname("ip"), 3, $one);
// trick, connect is needed anyway. $dest_ip must by right.
$conn_ret = socket_connect( $raw_socket, $remote_ip, 0 );
// $conn_ret = socket_connect( $raw_socket, $dest_ip_fake, $dest_port );
// trick, for read
// socket_bind($raw_socket, $src_ip );
if( $raw_socket === false
|| $set_ret === false
|| $conn_ret === false ){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return false;
}
$this->s = $raw_socket;
return $this->s;
}
public function create_socket( $src_ip, $src_port, $dest_ip=null, $dest_port=null ){
$this->dest_ip = $dest_ip;
$this->dest_port = $dest_port;
$dest_ip_fake = "127.0.0.1";
$dest_ip_fake = "192.168.56.101";
$dest_port_fake = 8080;
$one = 1;
$raw_socket = socket_create( AF_INET, SOCK_RAW, getprotobyname("tcp") );
socket_set_nonblock( $raw_socket );
// trick, 3 is stand for header control
$set_ret = socket_set_option( $raw_socket, getprotobyname("ip"), 3, $one);
// trick, connect is needed anyway. $dest_ip must by right.
// $conn_ret = socket_connect( $raw_socket, $dest_ip, $dest_port );
// $conn_ret = socket_connect( $raw_socket, $dest_ip_fake, $dest_port );
// trick, for read
socket_bind($raw_socket, $src_ip );
if( $raw_socket === false
|| $set_ret === false
|| $conn_ret === false ){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return false;
}
$this->s = $raw_socket;
return $this->s;
}
public function socket_read_raw( ){
$read_ret = socket_read( $this->s, 65535 );
if( $read_ret === false ){
$error = socket_last_error();
if( 11 !== $error ){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return null;
}
return false;
}
if( strlen($read_ret) > 20 ){
// echo "read_data...\n";
return self::IPUnpack( $read_ret );
}
return null;
}
public function socket_read_udp( ){
$read_ret = socket_recvfrom( $this->s, $buf, 65535, 0, $from='', $port=0 );
//echo $read_ret . "\n";
if( $read_ret === false ){
$error = socket_last_error();
if( 11 !== $error ){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return null;
}
return false;
}
if( $read_ret > 20 ){
// echo "udp_read_data...\n";
return self::IPUnpack( $buf );
}
return null;
}
/*
public function socket_recv_raw( ){
$buf = '';
$read_ret = socket_recvfrom( $this->s, $buf, 65535 );
if( $read_ret === false ){
$error = socket_last_error();
if( 11 !== $error ){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return null;
}
return false;
}
if( strlen($buf) > 20 ){
echo "read_data...\n";
return $this->IPUnpack( $buf );
}
return null;
}
*/
public function socket_send(
$src_ip, $src_port, $dest_ip, $dest_port,
$seq_num, $ack_num, $tcp_flag,
$tcp_data,$timestamp, $ts_echo ){
$ip_data = self::TCPPacket( ip2long($src_ip), ip2long($dest_ip), $src_port, $dest_port,
$seq_num, $ack_num, $tcp_flag,
$tcp_data, $timestamp, $ts_echo );
// echo "tcp_data_md5:" . md5( $ip_data ) . "\n";
$ip_data = self::IPPacket("tcp", ip2long($src_ip), ip2long($dest_ip), $ip_data );
$write_ret = socket_write( $this->s, $ip_data );
if( $write_ret === false || $write_ret !== strlen($ip_data)){
echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
return false;
}
// echo "socket_last_error_str:" . socket_strerror(socket_last_error()) . "\n";
// echo "write_ret:" . json_encode( $write_ret ) . "\n";
return $write_ret;
}
/* IP header */
/*
versionAndHeaderlen: 1Byte
service: 1Byte
totalLen: 2Bytes
PacketID: 2Bytes
SliceInfo: 2Bytes
TTL: 1Byte
type: 1Byte
checksum: 2Bytes ==> just ip header, 16bits fan ma sum
srcIP: 4Bytes
destIP: 4Bytes
*/
public static function IPUnpack( $packet ){
$arr = unpack("Cverlen/x/ntotal_len/x4/Cttl/Ctype/ncheck_sum/Nsrc_ip/Ndest_ip/x*", $packet);
$arr['version'] = $arr['verlen'] >> 4;
$arr['header_len'] = ($arr['verlen'] & 0x0f) << 2;
unset( $arr['verlen'] );
$arr['type'] = getprotobynumber( $arr['type'] );
$arr['src_ip'] = long2ip( $arr['src_ip'] );
$arr['dest_ip'] = long2ip( $arr['dest_ip'] );
if( $arr['type'] == 'tcp' ){
$arr['tcp'] = self::TCPUnPack( substr($packet, $arr['header_len'] ) );
$tcp_data_len = $arr['total_len'] - $arr['header_len'] - $arr['tcp']['header_len'];
$arr['tcp']['data_len'] = $tcp_data_len;
}
if( strlen($packet) < 20 ) return false;
return $arr;
}
public static function IPPacket( $proto, $src_ip, $dest_ip, $data ){
$ver_len = 4<<4 | 5;
$service = 0;
$total_len = 20+strlen($data);
$id_flag_offset = 0;
$ttl = 65;
$type = getprotobyname( $proto);
$chk_sum = 0;
$i = 2;
while( $i-- ){
$header = pack("CCn"."N"."CCn"."N"."N",