LOGI

海外站点生存指南之自动更换 Cloudflare 解析 IP

如今海外站点生存愈发艰难,但我着实需要它跑些任务,再买个国内服有点浪费,所以暂时先套 CF 苟活,其他人建议尽早转国内备案。实际上,之前已用七牛做了动静分离,并通过 CNAME 接入 CF 实现了 DNS 分运营商解析,尽管如此,抽风还是时有发生,几乎每个月都要手动更换一次 A 记录,次数一多难免烦躁,于是搜寻了一下自动扫可用 IP 的方案。大体观察过后发现直接扫可能比较耗资源,且之后还要手动三网测速,总归不是那么尽如人意。想了想,我其实对可用性要求并不高,不如跟随一个长期使用 CF 且站点稳定的大佬,定时扫描他的站点 IP,如不同则自动更换。那么话不多说,现在就贴一下代码。

保存源码

[collapse title="Python Code"]

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import json
import requests
import logging
import traceback


OBJECT = {
    'DOMAIN': 'logi.im', # 你的域名
    'SUB_DOMAINS': ['@', 'www']
}
UPSTREAM = '' # 跟随域名
DNSPOD_LOGIN_TOKEN = '' # DNSPOD TOKEN
IFTTT = {
    'WEBHOOK_KEY': '', # TG 推送
    'EVENT_NAME': ''
}
SERVER_CHAN_SCKEY = '' # 微信推送
LOG_FILE = '/root/cf.log' # 日志文件


LINES = '移动 联通 电信 教育网'.split(' ')
ISPS = {
    LINES[0]: [
        '218.207.255.254',
        '218.205.255.254',
        '221.131.255.254'
    ],
    LINES[1]: [
        '114.255.255.254',
        '163.204.255.254',
        '220.251.255.254'
    ],
    LINES[2]: [
        '180.149.159.254',
        '113.111.255.254',
        '116.239.255.254'
    ],
    LINES[3]: [
        '183.175.255.254',
        '202.199.255.254',
        '219.229.255.254'
    ]
}

logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG,
                    format='%(asctime)s - %(levelname)s - %(message)s')


def post_form_data(url, payload):
    headers = {'Content-Type': 'application/x-www-form-urlencoded'}
    response = requests.request("POST", url, headers=headers, data=payload)
    return json.loads(response.text)


def send_msg(tt, msg=None):
    msg = json.dumps(msg)
    ev = IFTTT['EVENT_NAME']
    wk = IFTTT['WEBHOOK_KEY']
    url = 'https://maker.ifttt.com/trigger/{ev}/with/key/{wk}?value1={tt}&value2={msg}'.format_map(vars())
    requests.get(url)

    scs = SERVER_CHAN_SCKEY
    url = 'http://sc.ftqq.com/{scs}.send?text={tt}'.format_map(vars())
    post_form_data(url, {'text': tt, 'desp': msg})


def get_upstream_ip(ip):
    dn = UPSTREAM
    url = 'http://119.29.29.29/d?dn={dn}&ip={ip}'.format_map(vars())
    return requests.get(url).text


def get_object_ips():
    url = 'https://dnsapi.cn/Record.List'
    payload = {
        'login_token': DNSPOD_LOGIN_TOKEN,
        'format': 'json',
        'domain': OBJECT['DOMAIN'],
        'record_type': 'A'
    }
    records = post_form_data(url, payload)['records']
    result = {}
    for record in records:
        if record['name'] in OBJECT['SUB_DOMAINS'] and record['line'] in LINES:
            if record['line'] not in result:
                result[record['line']] = {}
                result[record['line']]['ids'] = ''
                result[record['line']]['value'] = record['value']
            result[record['line']]['ids'] += record['id'] + ','
    for line in result:
        result[line]['ids'] = result[line]['ids'][:-1]
    return result


def update_object_ips(differences):
    url = 'https://dnsapi.cn/Batch.Record.Modify'
    result = {}
    for line in differences:
        payload = {
            'login_token': DNSPOD_LOGIN_TOKEN,
            'format': 'json',
            'record_id': differences[line]['ids'],
            'change': 'value',
            'change_to': differences[line]['value']
        }
        result[line] = post_form_data(url, payload)
    send_msg('CF IP 更新成功!', json.dumps(result, ensure_ascii=False, encoding='utf-8'))


def compare_upsteam_with_object():
    object_ips = get_object_ips()
    differences = {}
    for line in object_ips:
        count = 0
        upstream_ip = ''
        for IP in ISPS[line]:
            upstream_ip = get_upstream_ip(IP).split(';')[0]
            if upstream_ip != '' and object_ips[line]['value'] != upstream_ip:
                count += 1
        if count == len(ISPS[line]): # 同一运营商所有地点的解析全部不同则跟随上游更新
            differences[line] = object_ips[line]
            differences[line]['value'] = upstream_ip
    if len(differences) != 0:
        update_object_ips(differences)


try:
    compare_upsteam_with_object()
except Exception as e:
    send_msg('CF IP 更新失败,请及时处理!')
    logging.debug(traceback.format_exc())

[/collapse]

定时调用

脚本基于 Python3DNSPod API,搭配 crontab 可定时分运营商比对 自身跟随站点 IP,发现多地不同后会更新解析记录为跟随站点,同时推送通知到 TG 和微信。

# 2 小时执行一次
echo -e "0 */2 * * * /root/cf.py\n`crontab -l`" | crontab -

备用方法

最后一并记录 Hostloc 大佬的 IP 扫描器源码,以备不时之需。

首先安装 php7+zmap

apt -y install zmap php7.0 php7.0-curl

然后,保存验证源码为 cff.php

[collapse title="PHP Code"]

<?PHP
ini_set('memory_limit', '-1');

function partition($list, $p)
{
    $listlen   = count($list);
    $partlen   = floor($listlen / $p);
    $partrem   = $listlen % $p;
    $partition = array();
    $mark      = 0;
    for ($px = 0; $px < $p; $px++) {
        $incr           = ($px < $partrem) ? $partlen + 1 : $partlen;
        $partition[$px] = array_slice($list, $mark, $incr);
        $mark += $incr;
    }
    return $partition;
}

if ($argc < 4) {
    echo "CloudFlare filter\nVersion: 0.4\n";
    echo "Usage: php {$argv[0]} <raw file> <Verification file> <threads> [verify domain] [verify path] [verify value]\n";
    echo "Power by Kagurazaka Shira\n";
    exit;
}

if (!file_exists("$argv[1]")) {
    exit("Invalid input file!\n");
}

if (file_exists("$argv[2]")) {
    unlink($argv[2]);
}

$array      = file($argv[1], FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); //列表
$childcount = $argv[3]; //线程数
$array      = array_unique($array);
$part       = array();
$part       = partition($array, $childcount); //列表分割
$vname      = $argv[2];
$domain     = empty($argv[4]) ? "cfv.virtualizor.com" : $argv[4];
$vpath      = empty($argv[5]) ? "/srk.css" : $argv[5];
$vvalue     = empty($argv[6]) ? "srk_verify" : $argv[6];

for ($i = 0; $i < $childcount; $i++) {
    $pid = pcntl_fork();
    if ($pid == -1) {
        echo "Forking failed on loop $i\n";
        exit;
    } else if ($pid) {
        continue;
    } else {
        foreach ($part[$i] as $ip) {

            $url = "http://{$ip}/cdn-cgi/trace";
            $ch  = curl_init($url);
            curl_setopt_array($ch, [
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_SSL_VERIFYHOST => false,
                CURLOPT_SSL_VERIFYPEER => false,
                // CURLOPT_PROXYTYPE      => CURLPROXY_SOCKS5,
                // CURLOPT_PROXY          => $ip,
                CURLOPT_TIMEOUT        => 10,
                CURLOPT_HTTPHEADER     => [
                    "Host: {$domain}",
                    "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
                ],
                // CURLOPT_USERAGENT      => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36",
            ]);
            $return = curl_exec($ch);
            if (empty($return)) {
                echo "{$ip} down\n";
                curl_close($ch);
                continue;
            }
            // var_dump($return);
            if (strpos($return, "h={$domain}")) {
                curl_setopt($ch, CURLOPT_URL, "http://{$ip}" . $vpath);
                $return = curl_exec($ch);
                // var_dump($return);
                if (strpos($return, $vvalue) !== false) {
                    echo "{$ip} success\n";
                    file_put_contents($vname, $ip . "\n", FILE_APPEND | LOCK_EX);
                    curl_close($ch);
                    continue;
                }
            }
            echo "{$ip} bad\n";

            curl_close($ch);
        }

        exit;
    }
}
for ($j = 0; $j < $childcount; $j++) {
    $pid = pcntl_wait($status);
}

$v_arr = file($vname, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); //列表
$v_arr = array_unique($v_arr);

foreach ($v_arr as &$v) {
    $v = ip2long($v);
}
sort($v_arr);
foreach ($v_arr as &$v) {
    $v = long2ip($v);
}

file_put_contents($vname, implode("\n", $v_arr));

[/collapse]

最后,调用 zmap 预扫,并使用 cff.php 验证。

#!/usr/bin/env bash

curl -sL https://www.cloudflare.com/ips-v4 -o full.txt
zmap -p 443 --whitelist-file=/root/full.txt -o cdn_ip.txt
php cff.php cdn_ip.txt v.txt 100

实际上,你可能还需手动或通过代码验证三网可用性。

参考文章

当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »