技术 · 2024 年 12 月 12 日 0

解决部分苹果iCloud+用户访问网站出现的403错误:iCloud专用代理与搜索引擎IP的自动配置

问题背景

最近,有少量用户反馈无法访问我们的网站,经过排查,发现他们的IP地址被识别为非允许访问的范围,导致出现403错误。进一步分析后,发现用户的IP地址来自于Cloudflare的主机IP段,这引起了我们的疑惑。

Cloudflare的IP通常用于CDN和反向代理,很少有普通用户直接使用。经过研究,我们发现这与Apple的iCloud+订阅服务中的“专用代理”功能有关。该功能类似于VPN,会使用户的流量通过苹果的服务器进行转发。

苹果官方提供了关于此功能的详细信息以及IP地址列表:

我们查看了苹果提供的IP列表,发现其中包含了大量的IP CIDR地址,仅日本(JP)地区的IP CIDR就有6000多行。为了解决这个问题,我们需要自动化地获取并配置这些IP,以及其他搜索引擎的IP地址。

解决方案:使用脚本自动获取和配置IP列表

为了简化管理和减少手动操作,我编写了一个bash脚本,该脚本使用 Python3 来自动获取以下IP列表,并将其配置到我们的服务器中:

  1. 日本原生IP: 从APNIC获取日本地区的IP段信息。
  2. iCloud专用代理IP: 从苹果官方API获取iCloud专用代理的IP列表,并筛选出日本地区的IP。
  3. 搜索引擎IP: 获取Google, Bing, OpenAI和Cloudflare的IP列表,以便于搜索引擎爬虫正常访问。

以下是脚本的具体内容和步骤:

#!/bin/bash

# 配置日本IP列表
configure_japan_ips() {
    echo "正在配置日本IP列表..."
    
    # 安装 Python3 和必要的系统包
    apt install -y python3-full python3-pip python3-requests || handle_error "Python包安装失败"
    
    # 创建日本IP获取脚本
    cat > /root/jpip.py << 'EOF'
import requests
import math
import sys
from pathlib import Path

def download_file(url, filename):
    """下载文件并保存"""
    try:
        response = requests.get(url)
        response.raise_for_status()
        with open(filename, 'w') as f:
            f.write(response.text)
        return True
    except Exception as e:
        print(f"下载 {url} 失败: {str(e)}", file=sys.stderr)
        return False

def download_search_engine_ips():
    """下载并处理搜索引擎和其他服务的IP列表"""
    search_ips = []
    
    # 获取Google IP
    try:
        response = requests.get('https://developers.google.com/search/apis/ipranges/googlebot.json')
        google_data = response.json()
        search_ips.append("#Google的IP")
        for prefix in google_data['prefixes']:
            if 'ipv4Prefix' in prefix:
                search_ips.append(f"Require ip {prefix['ipv4Prefix']}")
            if 'ipv6Prefix' in prefix:
                search_ips.append(f"Require ip {prefix['ipv6Prefix']}")
    except Exception as e:
        print(f"获取Google IP失败: {str(e)}", file=sys.stderr)

    # 获取Apple爬虫IP
    try:
        response = requests.get('https://search.developer.apple.com/applebot.json')
        apple_data = response.json()
        search_ips.append("\n#Apple的IP")
        for prefix in apple_data['prefixes']:
            if 'ipv4Prefix' in prefix:
                search_ips.append(f"Require ip {prefix['ipv4Prefix']}")
            if 'ipv6Prefix' in prefix:
                search_ips.append(f"Require ip {prefix['ipv6Prefix']}")
    except Exception as e:
        print(f"获取Apple IP失败: {str(e)}", file=sys.stderr)

    # 获取Bing IP
    try:
        response = requests.get('https://www.bing.com/toolbox/bingbot.json')
        bing_data = response.json()
        search_ips.append("\n#Bing的IP")
        for prefix in bing_data['prefixes']:
            if 'ipv4Prefix' in prefix:
                search_ips.append(f"Require ip {prefix['ipv4Prefix']}")
            if 'ipv6Prefix' in prefix:
                search_ips.append(f"Require ip {prefix['ipv6Prefix']}")
    except Exception as e:
        print(f"获取Bing IP失败: {str(e)}", file=sys.stderr)

    # 获取OpenAI IP
    try:
        openai_urls = [
            'https://openai.com/searchbot.json',
            'https://openai.com/gptbot.json',
            'https://openai.com/chatgpt-user.json'
        ]
        search_ips.append("\n#OpenAI的IP")
        for url in openai_urls:
            response = requests.get(url)
            openai_data = response.json()
            for prefix in openai_data.get('prefixes', []):
                if 'ipv4Prefix' in prefix:
                    search_ips.append(f"Require ip {prefix['ipv4Prefix']}")
                if 'ipv6Prefix' in prefix:
                    search_ips.append(f"Require ip {prefix['ipv6Prefix']}")
    except Exception as e:
        print(f"获取OpenAI IP失败: {str(e)}", file=sys.stderr)

    # 获取Cloudflare IPv4
    try:
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
        }
        response = requests.get('https://www.cloudflare.com/ips-v4', headers=headers)
        if response.status_code == 200 and not response.text.lower().startswith('= 7 and parts[1] == 'JP' and (parts[2] == 'ipv4' or parts[2] == 'ipv6'):
                    ip = parts[3]
                    if parts[2] == 'ipv4':
                        count = int(parts[4])
                        prefix = 32 - int(math.log2(count))
                    else:  # ipv6
                        prefix = parts[4]
                    japan_ips.add(f"Require ip {ip}/{prefix}")

        # 处理iCloud数据
        with open('/root/egress-ip-ranges.csv', 'r') as f:
            for line in f:
                try:
                    ip_range, country, region, city, *_ = line.strip().split(',')
                    if country == 'JP':
                        japan_ips.add(f"Require ip {ip_range}")
                except:
                    continue

        # 添加搜索引擎IP下载
        download_search_engine_ips()
        
        # 确保输出目录存在
        output_path = Path(f"/root/jpip.conf")
        output_path.parent.mkdir(parents=True, exist_ok=True)

        # 保存到文件
        with open(output_path, 'w') as f:
            f.write('\n'.join(sorted(japan_ips)))

        print(f"成功保存 {len(japan_ips)} 条 IP 记录到 {output_path}")
        return 0

    except Exception as e:
        print(f"错误: {str(e)}", file=sys.stderr)
        return 1

if __name__ == '__main__':
    sys.exit(main())
EOF

    # 设置执行权限
    chmod +x /root/jpip.py || handle_error "设置脚本权限失败"

    echo "开始运行 jpip.py 脚本..."
    python3 /root/jpip.py || handle_error "日本IP列表获取失败"

    echo "删除临时文件"
    rm -rf /root/delegated-apnic-latest
    rm -rf /root/egress-ip-ranges.csv
    rm -rf /root/jpip.py
    sleep 2

    echo "设置每周定时运行"
    if ! grep -q "^0 0 \* \* 0 /root/jpip\.sh" /etc/crontab; then
        echo "0 0 * * 0 /root/jpip.sh" >> /etc/crontab
        systemctl restart cron
        echo "已添加定时任务"
    else
        echo "定时任务已存在,跳过添加"
    fi
    sleep 2

    echo "重启apache2"
    systemctl restart apache2
    sleep 2

    echo "配置完成" 
    sleep 2
}

# 主函数
main() {
    # 检查root权限
    [ "$(id -u)" != "0" ] && handle_error "请使用root权限运行此脚本"
    
    configure_japan_ips
}

main

脚本说明

  1. 错误处理: 使用 handle_error 函数统一处理错误,方便调试。
  2. 安装依赖: 自动安装 python3-full, python3-pippython3-requests
  3. Python脚本 (/root/jpip.py):
    • 使用 requests 库下载 APNIC 数据,iCloud IP列表和搜索引擎的IP列表。
    • 解析 APNIC 数据,筛选出日本地区的 IPv4 和 IPv6 地址,并计算前缀长度。
    • 解析 iCloud IP 列表,筛选出日本地区的 IP 段。
    • 下载并解析Google, Bing, OpenAI和Cloudflare的IP列表。
    • 将所有IP地址保存到 /root/jpip.conf/root/searchip.conf 文件中。
  4. 脚本执行:
    • 设置 Python 脚本的执行权限。
    • 执行 Python 脚本,获取并保存 IP 地址。
    • 删除临时文件。
    • 设置定时任务,每周日凌晨0点自动更新 IP 地址列表。
    • 重启 apache2 使配置生效。

使用方法

  1. 将上述脚本保存为 /root/jpip.sh
  2. 赋予执行权限:chmod +x /root/jpip.sh
  3. 以 root 用户运行脚本:bash /root/jpip.sh
  4. 脚本运行后,会在 /root/ 目录下生成 jpip.conf 文件,该文件包含了所有需要允许访问的IP地址。以及 /root/searchip.conf 包含搜索引擎的IP地址。
  5. 根据实际情况,将/root/jpip.conf/root/searchip.conf文件中的内容配置到你的Web服务器(如Apache或Nginx)的IP访问控制规则中。
  • 例如在Apache中你可以使用Require ip指令。

结论

通过使用该脚本,我们能够自动获取并更新日本地区的原生IP、iCloud专用代理IP以及搜索引擎IP列表。这样,既可以解决由于iCloud专用代理导致的访问问题,同时也能保证搜索引擎爬虫的正常访问。

希望这篇文章能够帮助你解决类似的问题。如有任何疑问,请随时留言。