动态IP解析
本文介绍两种方便获取主机动态IP的方式(DDNS,IP报告网页),并给出相应的代码实现.
shell脚本获取本机IP,执行上传操作和更新DNS操作.定期执行通过crontab或者systemd等服务.
应用场景
远程访问具有动态IP的公网或内网主机时,如果通过ip进行访问,由于公网IP总是在变化,我们不得不每次去查看新的ip地址,往往这个重复的过程比较麻烦.
远程主机联网的方式有所不同,主要有以下几种情形:
- 远程主机是通过PPPoE拨号上网,通常获取到动态的私有网络(内网)地址
- 远程主机直接获取到的是动态公网ipv4地址.
- 在教育网中通常还能获得动态的公网ipv6地址.
又可以简易地分成两类: 配有公网ip的主机与仅配置内网ip的主机.
内网主机访问方式
-
反向隧道
对于躲在NAT之后的内网主机,比较方便的方式是在内网主机建立到公网主机的反向隧道
,命令行建立反向隧道工具有:ssh,ngrok,tmate等,参考我之前的反向隧道的文章. 这些工具往往都能在ip发生改变后自动重建连接.
缺点是我们需要一台拥有公网ip的主机,并且时刻保持隧道长连接,另外由于远程访问内网主机需要经过这个公网主机中转,速度变慢. -
内网穿透
通过公网服务器得到内网主机在NAT设备的转码地址,然后可以建立p2p的连接.QQ,TeamViewer即是类似原理.前提是内网容易穿透.
-
路由器端口映射
外网IP和端口映射到内网:在路由器的「转发规则」页面添加外网的端口到内网某主机端口的映射.
本着只要有不断重复的麻烦事就用脚本实现的原则,我们通过一些脚本来方便我们的工作.
DDNS (动态域名IP解析)
IP报告/收集脚本
IP报告脚本
通过linux的ip命令获取到本机的公网ip以及通过网站获取本机的外网ip,然后上传到自建的php服务器上.
脚本使用了本地文件记录前一次变更的ip地址,当ip发生变化才执行网络操作.文件保存在内存文件系统或临时文件中.
reportIP.sh
#!/bin/bash
# __author__ = fyk
# get global ipv6 & ipv4 address,
# note that both ipv6 & ipv4 addr may have more than 1.
# grep -v to exclude temporary ipv6 privacy addr.
ip6s=$(ip -6 addr |grep 'global'|grep -v 'tmpaddr'|awk '{print $2}'|sed 's/\/.*//' | uniq)
ip4s_local=$(ip -4 a | grep global |awk '{print $2}' | uniq) # local v4 ips ,public or prive ips that behind NAT
ips_pub=$(curl -s ifconfig.me) # there are lots of websites supplying IP echo services
ip_data=$ips_pub' '${ip6s}' '${ip4s_local}
#echo $ip_data
IP_FILE='/dev/shm/lastip97451' # or in /tmp .etc
ip_data_old=$(cat $IP_FILE 2> /dev/null) # for non-exist file,content is null
if [ "$ip_data" != "$ip_data_old" ];then
echo 'IP changed,push to remote.'
#echo $ip_data > $IP_FILE # update the file
params='k=fyk'
cnt=0
for ip in $ip_data;do
echo $ip
params=$params"&ip$cnt=$ip" # shell will handle & specially,so we first trans & to %26
((cnt=cnt+1))
done
params="n=$cnt&$params"
sever_addr="http://x.makefile.tk/"
#echo ${sever_addr}
curl -G -d "$params" "$sever_addr"
else echo 'IP unchanged.'
fi
# TODO:增加断网重连
php收集脚本
提供的服务地址形式是:http://x.makefile.tk/?k=password&n=2&p0=x.x.x.x&p1=x.x.x.x
,其中n是ip地址个数,p0,p1,...分别是单独的ip,参数k为了简单地防止一些人捣乱.直接访问http://x.makefile.tk/
将能看到上一次保存的ip地址.
下面是index.php代码,通过文件来记录ip地址,不能够并发写入.同shell报告脚本一样可以使用内存文件来加快读写速度.
<html>
<body>
<?php
if ( !function_exists('sys_get_temp_dir')) {
function sys_get_temp_dir() {
if (!empty($_ENV['TMP'])) { return realpath($_ENV['TMP']); }
if (!empty($_ENV['TMPDIR'])) { return realpath( $_ENV['TMPDIR']); }
if (!empty($_ENV['TEMP'])) { return realpath( $_ENV['TEMP']); }
$tempfile=tempnam(uniqid(rand(),TRUE),'');
if (file_exists($tempfile)) {
unlink($tempfile);
return realpath(dirname($tempfile));
}
}
}
?>
<?php
//echo "QUERY_STRING: " . $_SERVER['QUERY_STRING'];
//echo "<br>";
//$temp_file = tempnam(sys_get_temp_dir(), 'Tux');
//$temp_file = sys_get_temp_dir() . 'ip97845';
//$temp_file = '/dev/shm/ip97845';//seems to be frequently erased by cloud host.
$temp_file = 'ip97845';
//if has param of n,then save ip to file
if(isset($_GET['n'])){
$n = $_GET['n'];
if(isset($_GET['k'])){
$key = $_GET['k'];//for simple security
if($key == 'password'){
$myfile = fopen($temp_file, "w") or die("Unable to open file!");
for ($x=0; $x<$n; $x++) {
$ip_idx = 'ip' . $x;
$line = $ip_idx . "=$_GET[$ip_idx]<br>";
echo $line;
fwrite($myfile, $line);
}
fclose($myfile);
echo 'save ip ok!<br>' ;
}else echo 'key error';
}else echo 'no key error';
}else{ // read from file
$myfile = fopen($temp_file, "r") or die("Unable to open file!");
echo fread($myfile,filesize($temp_file));
fclose($myfile);
}
echo "<br><br>";
echo "Your INFO:<br>";
echo "IP: " . $_SERVER['REMOTE_ADDR'];
echo "<br>";
echo "UA: " . $_SERVER['HTTP_USER_AGENT'];
echo "<br>";
?>
</body>
</html>
对于这种web应用,使用网络上各种php建站即可.
公用DNS服务
准备:购买公网域名,域名设置DNS解析服务为Dnspod或CloudFlare.本文的代码使用CloudFlare的API动态修改DNS记录.
思路是修改IP报告脚本,将更新的IP更新的公共的DNS服务上.
ipv6-dns.sh 代码:
#!/bin/bash
# __author__ = fyk
# get global ipv6 & ipv4 address,
# note that both ipv6 & ipv4 addr may have more than 1.
# get variables in dns.conf which includes cloudflare info
# source dns.conf
if [ -z "$1" ] ;then
echo 'please specify conf file'
exit 0
else
source $1
fi
# grep -v to exclude temporary ipv6 privacy addr.
ip6s=$(ip -6 addr |grep 'global'|grep -v 'tmpaddr'|awk '{print $2}'|sed 's/\/.*//')
#for my own needs,i only use ipv6
#ip4s=$(ip -4 a | grep global |awk '{print $2}')
for ip in $ip6s;do
ip_data=$ip
break # only use first one
done
#ip_data=${ip6s}' '${ip4s}
#echo $ip_data
API_URL="https://api.cloudflare.com/client/v4"
CURL="curl -s \
-H Content-Type:application/json \
-H X-Auth-Key:$AUTH_KEY \
-H X-Auth-Email:$AUTH_EMAIL "
update_dns(){
UPDATE_DATA=$(cat << EOF
{ "type": "AAAA",
"name": "$DOMAIN_NAME",
"content": "$2",
"proxied": false }
EOF
)
#"ttl": 1, # let it be Automatic
echo "update dns: $DOMAIN_NAME -> $2"
$CURL -X PUT "$API_URL/zones/$ZONE_ID/dns_records/$1" -d "$UPDATE_DATA" > /tmp/cloudflare-ddns.json
}
# get current IP
get_dns_ip(){
RECS=$($CURL "$API_URL/zones/$ZONE_ID/dns_records?name=$DOMAIN_NAME")
IP=$(echo "$RECS" | sed -e 's/[{}]/\n/g' | sed -e 's/,/\n/g' | grep '"content":"' | cut -d'"' -f4)
echo $IP
}
IP_FILE='/dev/shm/lastip9745' # or in /tmp .etc
ip_data_old=$(cat $IP_FILE 2> /dev/null) # for non-exist file,content is null
if [ "$ip_data" == "$ip_data_old" ];then
echo 'IP unchanged.'
exit 0
fi
echo 'IP changed,push to remote.'
if [ -z "$REC_ID" ] ; then
RECS=$($CURL "$API_URL/zones/$ZONE_ID/dns_records?name=$DOMAIN_NAME")
echo $RECS
REC_ID=$(echo "$RECS" | sed -e 's/[{}]/\n/g' | sed -e 's/,/\n/g' | grep '"id":"' | cut -d'"' -f4)
echo "REC_ID=$REC_ID"
fi
update_dns "$REC_ID" "$ip_data"
cur_ip=$(get_dns_ip)
if [ "$cur_ip"=="$ip_data" ];then
echo $ip_data > $IP_FILE # update the file
else
echo 'update dns failed.'
fi
脚本中通过source dns.conf读取了配置信息:
# this is Cloudflare api info for DDNS
# !!do not leave space around =
AUTH_EMAIL=<cloudflare-auth-email>
#This is your *Global API Key* under Cloudflare account settings
AUTH_KEY=<cloudflare-auth-key>
#Zone ID:can be find out there: <https://www.cloudflare.com/a/overview/>
ZONE_ID=<DNS Zone>
#your sub domain name
DOMAIN_NAME="ip.example.com"
具体API使用方法查阅https://api.cloudflare.com
自建DNS服务
准备:外网服务器B,搭建bind9服务用来提供DNS服务
借助于IP报告/收集脚本,在服务器B上不断更新域名解析.
客户端机器C,手动设置DNS服务地址为B的IP.这种方式的优点是域名想怎么写就怎么写.
示意图:
通过DNS服务可以实现与著名的花生壳
相类似的服务,而且成本低,“自主、可控”:) 。
关于域名解析ttl
TTL是英语Time-To-Live的简称,意思为一条域名解析记录在DNS服务器中的存留时间。当各地的DNS服务器接受到解析请求时,就会向域名指定的NS服务器发出解析请求从而获得解析记录;在获得这个记录之后,记录会在DNS服务器中保存一段时间,这段时间内如果再接到这个域名的解析请求,DNS服务器将不再向NS服务器发出请求,而是直接返回刚才获得的记录;而这个记录在DNS服务器上保留的时间,就是TTL值。
如果域名的IP经常变更,那么减小TTL的值,如果很少改变,调大成几个小时都行.
将TTL设为1,表示'Automatic',如Cloudflare的DNS会在约5分钟内push出去.
IP地址变更事件通知
得到网络变化的方式有多种:
- C语言使用linux下的rtnetlink的NETLINK_ROUTE socket,监听之后会收到消息.要求写的程序一直在运行.参考.
- 如果网络管理器使用的Gnome的NetworkManager,那么会通过D-Bus广播事件(浏览器等常通过这种方式切换在线/离线模式).
- Debian系统中网络接口up或down时会执行/etc/network下的相关脚本.
- ifplugd 当网线被拔掉或接入时会执行相应脚本.
如果网络是使用NetworkManager(Ubuntu等系统默认的网络管理器)进行DHCP获取动态IP,比较方便的方式是将我们的脚本添加到事件响应脚本中. 参考man手册,在/etc/NetworkManager/dispatcher.d中添加脚本.
#!/bin/bash
# put this script in /etc/NetworkManager/dispatcher.d
IF=$1
STATUS=$2
case "$STATUS" in
down)
#logger -s "NM Script down $IF triggered"
;;
dhcp6-change|up) #
#if [ $IP6_NUM_ADDRESSES > 0 ];then
# echo $IP6_ADDRESS_0 //0,1,2,...
#fi
# msg logged to /va/log/syslog
logger "IP6_ADDRESS_0 = $IP6_ADDRESS_0"
/path/to/ipv6-dns.sh /path/to/dns.conf 2>&1 > /dev/null
*)
;;
esac
cron定时执行
执行crontab -e
将会编辑用户的crontab文件,其创建/tmp下的临时文件进行编辑,保存后将会提交到系统目录下(/var/spool/cron),这种设计方式类似visudo,目的是先检查用户的输入,防止错误的输入带来的破坏.系统重启后/tmp下的文件会删除,而crontab不会丢失.
也可以使用自定义的crontab文件导入到系统任务中:`crontab /path/to/cronfile
文件内容如下,注意使用绝对路径:
0 */1 * * * /home/s05/fyk/ip/ipv6-dns.sh /home/s05/fyk/ip/dns.conf
cron job的执行命令情况可以在/var/log/syslog中看到.
使用logger 'msg'可以将msg记录到syslog文件中.
crontab无需重启会立即生效.
邮件通知
发送邮件.适合于ip更新不太频繁的情形,通过代码发送邮件的代码很简便,Python,Java等语言均有方便的实现.
More
本文代码地址: https://github.com/makefile/CharmScript/tree/master/DDNS
其它资源: