Skip to content

Instantly share code, notes, and snippets.

@etng
Forked from diyism/dns2https.php
Created September 18, 2016 08:49
Show Gist options
  • Save etng/a40cfeee723673bf10a88a490b99e3be to your computer and use it in GitHub Desktop.
Save etng/a40cfeee723673bf10a88a490b99e3be to your computer and use it in GitHub Desktop.
dns2https.php
<?php
/*
tcp dns client for google dns over https (https://dns.google.com)
ubuntu上使用:
在/etc/rc.local里加/usr/bin/php /home/<your_name>/dns2https.php
执行:
sysv-rc-conf unbound off
sysv-rc-conf dnscrypt-proxy off
sysv-rc-conf dnsmasq off
在/etc/resolv.conf里加:
options use-vc
nameserver 127.0.0.1
在/etc/resolvconf/resolv.conf.d/head里加:
options use-vc
在/etc/network/interfaces正在使用的iface下加:
dns-nameservers 127.0.0.1
*/
//thanks to:
//https://github.com/mikepultz/netdns2/
//https://github.com/yswery/PHP-DNS-SERVER/
/*
开发时录制模仿系统的dns请求:
//mkfifo fifo
//nc -lk localhost 53 <fifo | tee -a a.txt | nc 8.8.4.4 53 | tee -a b.txt >fifo
$data=file_get_contents('a.txt');
var_export(header_parse(substr($data, 2)));
$q=question_parse(substr($data, 14));
var_export($q);
var_export(answer_parse(substr($data, 14+$q['length']), 1, substr($data, 0, 14+$q['length'])));
exit();
*/
/**********************************************主循环************************************************/
//$ipsock=stream_socket_server('tcp://127.0.0.1:53', $errno2, $errstr2);
$ipsock=stream_socket_server('tcp://0.0.0.0:53', $errno2, $errstr2);
stream_set_blocking($ipsock, 0); //means fread() won't wait for data
//echo 'default_socket_timeout:'.ini_get("default_socket_timeout")."\n";
$queue=curl_multi_init();
$is_tcp=1; //udp还不支持
$timeout=10; //每个并发域名查询的超时时间
$force_ttl=3600; //设0则采用真实ttl
$google_domains=array('mail.google.com'=>'mail.google.com\/mail\/', 'www.google.com'=>'/url?');
$cache=array(//'twitter.com'=>array('time'=>time(), 'ip'=>'1.2.3.4', 'ttl'=>3600*24),
//'t.co'=>array('time'=>time(), 'ip'=>'1.2.3.4', 'ttl'=>3600*24),
'*.baidu.com'=>array('time'=>time(), 'ip'=>'255.255.255.255', 'ttl'=>3600*24)
);
$ipsock_cons=array();
$ipsock_con_datas=array();
$running_domain_tasks=array();
$statis=array();
$google_ips=explode('|', '64.233.160.131|64.233.160.129|64.233.161.131|64.233.161.129|64.233.162.81|64.233.162.82|64.233.162.83|64.233.162.84|64.233.162.129|64.233.163.131|64.233.163.129|64.233.164.129|64.233.164.131');
while (true)
{
//echo "sleep 0.005\n";
usleep(1000);//0.001s, to reduce cpu consumption
//处理监听连接
//echo "1".time()."\n";
$ipsock_con=@stream_socket_accept($ipsock, 0); //加参数0避免等待
if ($ipsock_con!==false)
{
//print_r($ipsock_con);echo " connected\n";
$ipsock_cons[$ipsock_con]=$ipsock_con; //加入连接池
$ipsock_con_datas[$ipsock_con]['last_time']=time(); //记录首次活动时间
}
//结束超时的client connection
foreach ($ipsock_cons as $con) //根据末次活动时间判断连接池里的连接是否客户端意外断开(收不到feof信号)
{
if (time()-$ipsock_con_datas[$con]['last_time']>60)
{
echo "connection timeout(".$con.")!\n";
end_ipsock_con($con);
}
}
//处理监听收信, 给向第三方发信创建任务
//echo "2\n";
$cons_read=$ipsock_cons;
$cons_write=$cons_except=array();
if ($cons_read)
{
stream_select($cons_read, $cons_write, $cons_except, 0); //过滤出所有可读任务resource ids, 加参数0避免等待
}
if ($cons_read)
{
//echo 'need to receive:'.strtr(print_r($cons_read, 1), array("\n"=>''))."\n";
foreach ($cons_read as $con)
{
if (feof($con)) //客户端断开连接服务端会收到feof信号, 即使客户端进程意外中断也会收到, 连接都是系统管理
{
echo "client side close(".$con.")!\n";
end_ipsock_con($con);
continue;
}
if ($data=fread($con, 65536)) //由于是dns请求一个请求不会太长, 未做循环读取
{
//echo print_r($con, 1).' received: '.strlen($data).' '.$data." \n";
//file_put_contents('a.txt', $data."\n======================\n", FILE_APPEND);
$ipsock_con_datas[$con]['last_time']=time(); //记录末次活动时间
$reqs=get_domain_and_packet_id($data);
foreach ($reqs as $req)
{
if ($req['packet_id']==='') //包头都不完整时跳过, 不断开连接, 有新请求过来就接着处理
{
continue;
}
$ipsock_con_datas[$con]['domain_packs'][@$req['domain'].':'.@$req['packet_id']]=array('domain'=>@$req['domain'], 'packet_id'=>@$req['packet_id']);
$this_con_packs=array($con.':'.$req['packet_id']=>array('con'=>$con, 'packet_id'=>$req['packet_id']));
if (!preg_match('/[A-Za-z0-9_\-\.]+/', $req['domain']) //不支持含有非英语数字及三符号的域名查询
|| strpos($req['domain'], '.')===false //不支持本地域名查询
|| strpos($req['domain'], 'in-addr')!==false //不支持逆向域名查询inverse address
|| $req['type']!='A' //不支持非type==A的查询
)
{
echo "answer: not support (".$req['domain'].':'.$req['type'].")\n";
//直接返回, 不创建domain task
send_answer($this_con_packs, $req['domain'], array('Answer'=>array(array('type'=>@$req['type'], 'data'=>'', 'TTL'=>''))));
continue;
}
//已有缓存的域名且未过期
if (@$cache[$req['domain']] && time()-$cache[$req['domain']]['time']<$cache[$req['domain']]['ttl'])
{
//echo 'matched ';var_export($cache);
//直接返回, 不创建domain task
send_answer($this_con_packs, $req['domain'], array('Answer'=>array(array('type'=>1, 'data'=>$cache[$req['domain']]['ip'], 'TTL'=>$cache[$req['domain']]['ttl']-time()+$cache[$req['domain']]['time']))));
continue;
}
//支持泛域名默认配置
$tmp='*.'.implode('.', array_slice(explode('.', $req['domain']), -2));
if (@$cache[$tmp])
{
//echo 'matched ';var_export($cache);
//直接返回, 不创建domain task
send_answer($this_con_packs, $req['domain'], array('Answer'=>array(array('type'=>1, 'data'=>$cache[$tmp]['ip'], 'TTL'=>$cache[$tmp]['ttl']-time()+$cache[$tmp]['time']))));
continue;
}
unset($cache[$req['domain']]); //过期的缓存要清除
create_query_tasks($con, $req['packet_id'], $req['domain']);
}
}
}
}
//执行第三方发信任务, 处理第三方收信, 处理监听发信
//echo "3\n";
while (($code=curl_multi_exec($queue, $active))==CURLM_CALL_MULTI_PERFORM); //总之有了这个while循环后, 有新发送任务时接收可以共用发送的5次执行, 没有新发送任务时, 要么有新接受任务共执行2次, 要么没有新接收任务共执行1次
while ($done=curl_multi_info_read($queue)) //把所有已完成的任务都处理掉, curl_multi_info_read执行一次读取一条
{
$url_ori=curl_getinfo($done['handle'], CURLINFO_EFFECTIVE_URL);
$success_google_ip=parse_url($url_ori, 1);
parse_str(parse_url($url_ori, 6), $args_get);
if ($content=curl_multi_getcontent($done['handle']))
{
parse_body_headers($content, $body, $rsp_headers);
if (in_array($args_get['name'], array_keys($google_domains)))
{
//echo $success_google_ip.' '.json_encode($rsp_headers).' '.$body."=========================\n";
if (strpos(json_encode($rsp_headers).' '.$body, $google_domains[$args_get['name']])!==false)
{
$body=array('Answer'=>array(array('type'=>1, 'data'=>$success_google_ip, 'TTL'=>60*5))); //google ip容易被封, 写死5分钟过期
}
else
{
$body='';
}
}
else
{
$body=@json_decode($body, 1);
}
//var_export(/*var_export($rsp_headers)."\n\n".*/$body);
if ($body)
{
echo "===========\nfastest: ".$url_ori."\n";
$statis[$success_google_ip]=@$statis[$success_google_ip]+1;
file_put_contents('statis.txt', var_export($statis, 1));
//过滤掉非A记录答案
$body['Answer']=is_array($body['Answer'])?$body['Answer']:array();
foreach ($body['Answer'] as $k=>$answer)
{
if ($answer['type']===1) //多个answer里第一个type为ip地址的就返回
{
$body['Answer'][$k]['TTL']=$force_ttl?$force_ttl:intval(@$answer['TTL']);
}
else
{
unset($body['Answer'][$k]);
}
}
//发送答案给所有请求该域名的客户端
send_answer($running_domain_tasks[$args_get['name']]['con_packs'], $args_get['name'], $body);
//创建的domain task也要清理
end_domain_task($args_get['name']);
}
}
}
//结束超时的domain task
foreach ($running_domain_tasks as $domain=>$task)
{
if (time()-$task['time']>$timeout+2)
{
echo "domain task timeout(".$domain.")!\n";
send_answer($task['con_packs'], $domain, array('Answer'=>array(array('type'=>1, 'data'=>'', 'TTL'=>''))));
//创建的domain task也要清理
end_domain_task($domain);
}
}
}
/*********************************************后面都是用到的函数*********************************************/
function end_ipsock_con($con)
{
global $ipsock_cons, $ipsock_con_datas, $running_domain_tasks;
foreach ($ipsock_con_datas[$con]['domain_packs'] as $domain_pack)
{
unset($running_domain_tasks[$domain_pack['domain']]['con_packs'][$con.':'.$domain_pack['packet_id']]); //domain task回来后就少一个客户端连接需要回信了
}
@fclose($con); //客户端断掉的连接(有feof信号)或者客户端意外中断(没有feof信号)服务端都要再断一次
unset($ipsock_cons[$con]); //从轮询队列里移除
unset($ipsock_con_datas[$con]); //从连接关联数据里移除
//echo print_r($con, 1)." closed, left: ".print_r($ipsock_cons, 1)."\n";
}
function end_domain_task($domain)
{
global $queue, $running_domain_tasks;
echo date('i:s').'('.(time()-$running_domain_tasks[$domain]['time']).'s) end domain task: '.$domain."\n";
//取消所有兄弟handle任务并关闭所有兄弟handles, 清理该域名的running domain task
foreach ($running_domain_tasks[$domain]['handles'] as $handle)
{
curl_multi_remove_handle($queue, $handle);
curl_close($handle);
}
unset($running_domain_tasks[$domain]);
}
function send_answer($con_packs, $domain, $rtn)
{
global $is_tcp, $cache;
//var_export($rtn);echo "\n";
$answers=@$rtn['Answer'];
$answers=$answers?$answers:array();
$ip='';
$ttl='';
foreach ($answers as $answer)
{
if ($answer['type']===1) //多个answer里第一个type为ip地址的就返回
{
$ip=@$answer['data'];
$ttl=@$answer['TTL'];
break;
}
}
echo date('i:s').' answer: '.$ip."\n";
//记到缓存
if ($ip && !@$cache[$domain])
{
$cache[$domain]=array('time'=>time(), 'ip'=>$ip, 'ttl'=>$ttl);
//echo 'cached '; print_r($cache);
}
//如果还有需要回信的client connections就回信
foreach ($con_packs as $con_pack)
{
//发给客户端
$res=header_data($con_pack['packet_id'], 1, $ip?1:0).question_data($domain).($ip?answer_data_a($ip, $ttl):'');
myfwrite($con_pack['con'], ($is_tcp?pack('n', strlen($res)):'').$res);
//file_put_contents('b.txt', ($is_tcp?pack('n', strlen($res)):'').$res."\n======================\n", FILE_APPEND);
}
}
function create_query_tasks($con, $packet_id, $domain)
{
global $google_ips, $timeout, $queue, $running_domain_tasks, $google_domains;
if (!@$running_domain_tasks[$domain]) //没有正在运行的该domain task才去创建domain task
{
echo 'new domain task: '.$domain.'('.$con.")\n";
$running_domain_tasks[$domain]=array('con_packs'=>array(), 'handles'=>array(), 'time'=>time());
$selected_google_ips=array_rand(array_flip($google_ips), 10);
foreach ($selected_google_ips as $google_ip)
{
if (in_array($domain, array_keys($google_domains)))
{
$url='https://'.$google_ip.'/?name='.$domain;
$host=$domain.':443';
}
else
{
$url='https://'.$google_ip.'/resolve?name='.$domain.'&type=A&dnssec=true#name='.$domain; //也用hash传变量方便请求返回的时候统一解析
$host='dns.google.com:443';
}
$ch=curl_to_host('GET',
$url,
array('Host'=>$host),
array(),
$resp_head,
$timeout
);
curl_multi_add_handle($queue, $ch);
$running_domain_tasks[$domain]['handles'][]=$ch; //记下一个domain task的10个handles, 后面有一个handle返回就要close掉所有兄弟handle
}
}
else
{
echo 'duplicated domain task: '.$domain.'('.$con.':'.$packet_id.")\n";
}
$running_domain_tasks[$domain]['con_packs'][$con.':'.$packet_id]=array('con'=>$con, 'packet_id'=>$packet_id); //往该domain task服务的客户端连接池里加一个客户端连接, task返回时向多个客户端(比如浏览器的多个线程)返回结果
}
function get_domain_and_packet_id($data)
{
global $is_tcp;
$RR_TYPES=array('A'=>1,
'NS'=>2,
'CNAME'=>5,
'MX'=>15,
'TXT'=>16,
'AAAA'=>28
);
$types=array_flip($RR_TYPES);
$rtn=array();
while ($data)
{
$tmp=array('domain'=>'', 'packet_id'=>'', 'type'=>'');
if (strlen($data)<12) //忽略客户端不完整包或非查询包
{
$rtn[]=$tmp;
$data='';
continue;
}
$data=$is_tcp?substr($data, 2):$data;
$data_header=header_parse($data);
//var_export($data_header);echo "\n";
$tmp['packet_id']=$data_header['id'];
$data_question=substr($data, 12);
$data=strstr($data_question, "\0");
$tmp['type']=@$types[hexdec(bin2hex(substr($data, 1, 2)))];
$data_question=@substr(expand(strstr($data_question, "\0", 1)), 1);
echo "===============\n".date('i:s')." query: ".$data_question.':'.$tmp['type']."\n";
$tmp['domain']=strtolower($data_question);
$rtn[]=$tmp;
$data=@substr($data, 5);
}
return $rtn;
}
function myfwrite($fd,$buf) {
$i=0;
while ($buf != "" and is_resource($fd)) {
$i=fwrite ($fd,$buf,strlen($buf));
if ($i==false) {
if (!feof($fd)) continue;
break;
}
$buf=substr($buf,$i);
}
return $i;
}
//header
function header_data($packet_id=1, $qr=0, $c_ans=0)
{$packet_id=$packet_id;
$qr=$qr; //Query or Response
$oc=0; //Op Code
$aa=0; //Authoritative Answer
$tc=0; //TrunCation
$rd=1; //Recursion Desired
$ra=1; //Recursion Available
$z=0; //reserved
$rc=0; //Response Code
$c_quest=1; //items count in question
$c_ans=$c_ans; //items count in answer
$c_auth=0; //items count in authority
$c_add=0; //items count in additional
$data_put=pack('n6',
$packet_id,
($qr&0x1)<<15|($oc&0xf)<<11|($aa&0x1)<<10|($tc&0x1)<<9|($rd&0x1)<<8|($ra&0x1)<<7|($z&0x7)<<4|($rc &0xf),
$c_quest,
$c_ans,
$c_auth,
$c_add
);
return $data_put;
}
//question
function question_data($dn, $type='A')
{$RR_TYPES=array('A'=>1,
'NS'=>2,
'CNAME'=>5,
'MX'=>15,
'TXT'=>16,
'AAAA'=>28
);
$RR_CLASS_IN=1; //IN means internet
$data_put=preg_replace(array("/\.([^.]+)/e"),
array('chr(strlen("\1"))."\1"'),
'.'.$dn
)
."\0"
.pack('n2',
$RR_TYPES[$type],
$RR_CLASS_IN
);
return $data_put;
}
//answer 'A'
function answer_data_a($ip, $ttl)
{$RR_TYPES=array('A'=>1,
'NS'=>2,
'CNAME'=>5,
'MX'=>15,
'TXT'=>16,
'AAAA'=>28
);
$RR_CLASS_IN=1; //IN means internet
return "\xC0"."\x0C".pack('nnNn', $RR_TYPES['A'], $RR_CLASS_IN, $ttl, '4').@inet_pton($ip); //"C00C"表示是针对第一域名的答案
}
//parse header
function header_parse($data)
{$data_get=array();
$offset=0;
$data_get['id']=ord($data[$offset]) <<8 | ord($data[++$offset]);
++$offset;
$data_get['qr']=(ord($data[$offset]) >>7) & 0x1;
$data_get['oc']=(ord($data[$offset]) >>3) & 0xf;
$data_get['aa']=(ord($data[$offset]) >>2) & 0x1;
$data_get['tc']=(ord($data[$offset]) >>1) & 0x1;
$data_get['rd']=ord($data[$offset]) & 0x1;
++$offset;
$data_get['ra']=(ord($data[$offset]) >>7) & 0x1;
$data_get['z']=0;
$data_get['rc']=ord($data[$offset]) & 0xf;
$data_get['c_quest']=ord($data[++$offset]) <<8 | ord($data[++$offset]);
$data_get['c_ans']=ord($data[++$offset]) <<8 | ord($data[++$offset]);
$data_get['c_auth']=ord($data[++$offset]) <<8 | ord($data[++$offset]);
$data_get['c_add']= ord($data[++$offset]) <<8 | ord($data[++$offset]);
return $data_get;
}
//parse question
function question_parse($pkt)
{$name_length=strpos($pkt, "\0");
$qname = substr(expand(substr($pkt, 0, $name_length)), 1);
$tmp = unpack('nqtype/nqclass', substr($pkt, $name_length+1, 4));
$tmp['qname'] = $qname;
$tmp['length']=$name_length+5;
return $tmp;
}
//parse answer
function answer_parse($data, $cnt, $data_before)
{$rtn=array();
$offset=-1;
for ($i=0;$i<$cnt;++$i)
{if (strlen($data)>=16)
{$offset+=2; //前两个字节'CXXX', 表示改答案对应整个response的'XXX'偏移的那个域名, 比如第一域名是"C00C"
$type=ord($data[++$offset]) <<8 | ord($data[++$offset]);
$class=ord($data[++$offset]) <<8 | ord($data[++$offset]);
}
$ttl=ord($data[++$offset]) <<24 | ord($data[++$offset]) <<16 | ord($data[++$offset]) <<8 | ord($data[++$offset]);
$length=ord($data[++$offset]) << 8 | ord($data[++$offset]);
if ($type==1) //'A'
{if ($length>4)
{$offset+=$length;
continue;
}
$rtn[]=array('ttl'=>$ttl,
'ip'=>ord($data[++$offset]).'.'.ord($data[++$offset]).'.'.ord($data[++$offset]).'.'.ord($data[++$offset])
);
}
else if ($type==2) //'NS'
{$ndsname=expand(substr($data, $offset+1, $length));
$ndsname=preg_replace('/>>>(.)<<</e',
'expand(substr($data_before, ord("\1")))',
$ndsname
);
$ndsname=trim($ndsname, '.');
$rtn[]=array('ttl'=>$ttl,
'nsdname'=>$ndsname
);
$offset+=$length;
}
}
return $rtn;
}
function expand($result)
{return preg_replace('/(.)(.*)/se', //加/s因为为长度为10时没匹配到
'($cnt=@ord("\1"))
?($cnt===192?">>>":".").substr(($str=@strtr(\'\2\', array(\'\"\'=>\'"\', \'\\\\0\'=>"\x00"))), 0, $cnt).($cnt===192?"<<<":"").expand(substr($str, $cnt))
:""
',
$result
);
}
function &curl_to_host($method, $url, $headers, $data, &$resp_headers, $total_timeout=20)
{$ch=curl_init($url);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, $total_timeout);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLOPT_NOSIGNAL, true); //skip slow dns resolve
if (stripos($url, 'https')===0)
{curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
}
if ($method=='POST')
{curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
}
foreach ($headers as $k=>$v)
{$headers[$k]=str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $k)))).': '.$v;
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
return $ch;
}
function parse_body_headers($rtn, &$body, &$rsp_headers)
{$rtn=explode("\r\n\r\nHTTP/", $rtn, 2); //to deal with "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK...\r\n\r\n..." header
$rtn=(count($rtn)>1 ? 'HTTP/' : '').array_pop($rtn);
@list($str_resp_headers, $body)=explode("\r\n\r\n", $rtn, 2);
$str_resp_headers=explode("\r\n", $str_resp_headers);
array_shift($str_resp_headers); //get rid of "HTTP/1.1 200 OK"
$rsp_headers=array();
foreach ($str_resp_headers as $k=>$v)
{$v=explode(': ', $v, 2);
$rsp_headers[$v[0]]=$v[1];
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment