Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ PAC scripts for proxies

## 特点

基于 IP 地址白名单设计,位于白名单中的 IP 地址走直连,白名单以外的 IP 地址走代理(暂不支持 IPv6)
基于 IP 地址白名单设计,位于白名单中的 IP 地址走直连,白名单以外的 IP 地址走代理。

另有 GFWList 版本从 [gfwlist/gfwlist](https://github.com/gfwlist/gfwlist) 获取域名及 URL 列表,优先匹配列表中的黑白名单,有效防止 DNS 污染。

每周六 12:00 (UTC) 会自动使用 GitHub Actions 运行[生成脚本](build.py)从数据源获取 IP 地址列表并生成 PAC 文件。

## 使用

获取方式:[本仓库的 Releases](https://github.com/iBug/pac/releases/latest)
获取方式:[本仓库的 Releases](https://github.com/wits-fe/pac/releases/latest)

- `pac-<name>.txt` 包含从数据源 `<name>` 获取的 IP 地址列表(白名单)
- `pac-gfwlist-<name>.txt` 在 IP 白名单的基础上添加了 GFWList 的匹配
Expand Down
146 changes: 129 additions & 17 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@

import gfwlist


SOURCES = {
SOURCES_4 = {
'ipdeny.com': 'http://www.ipdeny.com/ipblocks/data/aggregated/cn-aggregated.zone',
'17mon': 'https://raw.githubusercontent.com/17mon/china_ip_list/master/china_ip_list.txt',
}
SOURCES_6 = {
'gaoyifan': 'https://gaoyifan.github.io/china-operator-ip/china6.txt',
}
SOURCES_46 = {
'maxmind': 'https://github.com/v2fly/geoip/raw/release/text/cn.txt',
}
OUT_DIR = "dist"

# Stub content to disable GFWList check
Expand All @@ -25,6 +30,8 @@ def fetch_and_convert(src):
template = "var CHINA = [\n{}\n];\n"
lines = []
for iprange in response.text.strip().split("\n"):
if iprange.find(":") != -1:
break
ipnet = ipaddress.IPv4Network(iprange)
netaddr = int(ipnet.network_address)
netmask = int(ipnet.netmask)
Expand All @@ -34,6 +41,101 @@ def fetch_and_convert(src):
return template.format("\n".join(lines))


def fetch_and_convert_ip6(src):
response = requests.get(src)
response.raise_for_status()
text = response.text

template = "var CHINA6_F = [\n{}\n];\n\n"
template2 = "var CHINA6_S = [\n{}\n];\n"
lines = []
lines2 = []
lastnum = 0
count = 0
begins = False
ends = False
lower = 0
upper = 0
networkstr_b = ""
iprangestr_b = ""
fixlen = len(f" [0xFFFFFFFF, -1, 0xFFFFFFFF],")

for iprange in text.strip().split("\n"):
if iprange.find(":") == -1:
continue
ipnet = ipaddress.IPv6Network(iprange)
prefixlen = ipnet.prefixlen
fulladdr = str(ipnet.exploded).replace(':', '')
num1 = int(fulladdr[0:8], 16)
num2 = int(fulladdr[8:16], 16)
fullmask = f"{ipnet.netmask:X}"
mask1 = fullmask[0:8]
mask2 = fullmask[8:16]

if lastnum != num1 and begins:
ends = True

if ends:
begins = False
ends = False
upper = count - 1
s2 = networkstr_b
if upper == lower:
s2 = iprangestr_b
s = f" [0x{lastnum:08X}, {lower}, {upper}],"
len1 = len(s)
if len1 < fixlen:
s = s + " " * (fixlen - len1)
s = f"{s} // {s2}"
lines.append(s)

if prefixlen <= 32:
if begins:
raise NameError(f"Invalid list order: \n{iprange}")
s = f" [0x{num1:08X}, -1, 0x{mask1}], // {iprange}"
lines.append(s)
else:
if not begins:
begins = True
lower = count
networkstr_b = str(ipnet.exploded)[0:10]
iprangestr_b = iprange

s = f" [0x{num2:08X}, 0x{mask2}], // {count}, {iprange}"
lines2.append(s)
count = count + 1
lastnum = num1

if begins:
begins = False
ends = False
upper = count - 1
s2 = networkstr_b
if upper == lower:
s2 = iprangestr_b
s = f" [0x{lastnum:08X}, {lower}, {upper}],"
len1 = len(s)
if len1 < fixlen:
s = s + " " * (fixlen - len1)
s = f"{s} // {s2}"
lines.append(s)

lines.append(" [0xFFFFFFFF, -1, 0xFFFFFFFF] // ffff:ffff::/32 placeholder")
lines2.append(f" [0xFFFFFFFF, 0xFFFFFFFF] // {count}, placeholder, not in use")

return template.format("\n".join(lines)) + template2.format("\n".join(lines2))


def write_pac(filename, code, data, data_6, tail):
with open(os.path.join(OUT_DIR, filename), "w") as f:
f.write(code)
f.write(data)
f.write("\n")
f.write(data_6)
f.write("\n")
f.write(tail)


def main():
now = datetime.utcnow()
date = now.strftime("%Y%m%d")
Expand All @@ -45,27 +147,37 @@ def main():
gfwlist_stub = GFWLIST_STUB

os.makedirs(OUT_DIR, mode=0o755, exist_ok=True)
for key in SOURCES:
print(f"Generating PAC script from source {key}")
for key in SOURCES_4:
key_6 = list(SOURCES_6)[0]
print(f"Generating PAC script from source {key}(IPv4) & {key_6}(IPv6)")
try:
data = fetch_and_convert(SOURCES_4[key])
data_6 = fetch_and_convert_ip6(SOURCES_6[key_6])
except RequestException:
continue
except HTTPError:
continue

filename = f"pac-IPv4_{key}--IPv6_{key_6}.txt"
filename_gfwlist = f"pac-gfwlist-IPv4_{key}--IPv6_{key_6}.txt"
write_pac(filename, code, data, data_6, gfwlist_stub)
write_pac(filename_gfwlist, code, data, data_6, gfwlist_part)

for key in SOURCES_46:
print(f"Generating PAC script from source {key}(IPv4v6)")
try:
data = fetch_and_convert(SOURCES[key])
data = fetch_and_convert(SOURCES_46[key])
data_6 = fetch_and_convert_ip6(SOURCES_46[key])
except RequestException:
continue
except HTTPError:
continue

filename = f"pac-{key}.txt"
filename_gfwlist = f"pac-gfwlist-{key}.txt"
with open(os.path.join(OUT_DIR, filename), "w") as f:
f.write(code)
f.write(data)
f.write("\n")
f.write(gfwlist_stub)
with open(os.path.join(OUT_DIR, filename_gfwlist), "w") as f:
f.write(code)
f.write(data)
f.write("\n")
f.write(gfwlist_part)
filename = f"pac-IPv4v6_{key}.txt"
filename_gfwlist = f"pac-gfwlist-IPv4v6_{key}.txt"
write_pac(filename, code, data, data_6, gfwlist_stub)
write_pac(filename_gfwlist, code, data, data_6, gfwlist_part)



if __name__ == '__main__':
Expand Down
142 changes: 138 additions & 4 deletions code.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,100 @@
var proxy = __PROXY__;
var direct = "DIRECT";

// lower: lower_index
// upper: (upper_index + 1) / (array_length if upper_index = last)
function binarySearch(list, num, lower, upper) {
var x = lower, y = upper, middle;
while (y - x > 1) {
middle = Math.floor((x + y) / 2);
if (list[middle][0] > num)
y = middle;
else
x = middle;
}
return x;
}

function convertToUInt6(high, low) {
var num1 = parseInt(high, 16) & 0xFFFF;
var num2 = parseInt(low, 16) & 0xFFFF;
return (((num1 << 16) | num2) >>> 0);
}

function isInNet6(parts, list, list2) {
var num = convertToUInt6(parts[0], parts[1]);
var x = binarySearch(list, num, 0, list.length);

if (list[x][1] == -1)
return (((num & list[x][2]) ^ list[x][0]) === 0);

// not in net (/33 - /64)
if (num !== list[x][0])
return false;

var num2 = convertToUInt6(parts[2], parts[3]);
var x2 = binarySearch(list2, num2, list[x][1], list[x][2] + 1);

return (((num2 & list2[x2][1]) ^ list2[x2][0]) === 0);
}

function isLanOrChina6(host) {
var addr = host;
if (addr.indexOf("[") !== -1) {
addr = addr.substring(1, addr.length - 1);
}

if (addr.indexOf("::") === -1) {
var groups = addr.split(":");
if (groups.length != 8)
return false; // invalid ipv6 format
return isInNet6(groups, CHINA6_F, CHINA6_S) || isInNet6(groups, LAN6_F, LAN6_S);
}

var halfs = addr.split("::");
var left = halfs[0];
var right = halfs[1];
if (left.length < 1) left = "0000";
if (right.length < 1) right = "0000";

var groups1 = left.split(":");
if (groups1.length > 3)
return isInNet6(groups1, CHINA6_F, CHINA6_S) || isInNet6(groups1, LAN6_F, LAN6_S);

var groups2 = right.split(":");
var zeros = 8 - (groups1.length + groups2.length);
if (zeros < 2)
return false; // invalid ipv6 format

var parts = ["0", "0", "0", "0"];
parts[0] = groups1[0];
var i = 1;
for (var j = 1; j < groups1.length; j++) {
parts[i] = groups1[j];
i = i + 1;
if (i == 4) break;
}

if (i < 4) {
for (var k = 0; k < zeros; k++) {
parts[i] = "0000";
i = i + 1;
if (i == 4) break;
}
}
if (i == 3) parts[3] = groups2[0];

return isInNet6(parts, CHINA6_F, CHINA6_S) || isInNet6(parts, LAN6_F, LAN6_S);
}

function convertToUInt(host) {
var bytes = host.split(".");
var result = ((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF);
return (result >>> 0);
}

function belongsToSubnet(host, list) {
var ip = convert_addr(host) >>> 0;
var ip = convertToUInt(host);

if (list.length === 0 || ip < list[0][0])
return false;
Expand Down Expand Up @@ -83,11 +175,35 @@ function FindProxyForURL(url, host) {
}

// Fallback to IP whitelist
var remote = dnsResolve(host);
if (!remote || remote.indexOf(":") !== -1) {
// resolution failed or is IPv6 addr

// if host is IPv6
if (host.indexOf(":") !== -1) {
if (isLanOrChina6(host)) {
return direct;
}
return proxy;
}

var remote;
if(typeof dnsResolveEx == 'function') {
remote = dnsResolveEx(host);
} else {
remote = dnsResolve(host);
}

if (!remote) {
return proxy;
} else {
remote = remote.split(";")[0];
}

if (remote.indexOf(":") !== -1) {
if (isLanOrChina6(remote)) {
return direct;
}
return proxy;
}

if (isLan(remote) || isChina(remote)) {
return direct;
}
Expand All @@ -104,3 +220,21 @@ var LAN = [
[0xC0A80000, 0xFFFF0000] // 192.168.0.0/16
];

// not support /65 - /128
var LAN6_F = [
[0x00000000, 0, 0], // ::/64
[0x0064FF9B, 1, 2], // 64:ff9b:
[0x01000000, 3, 3], // 100::/64
[0x20010000, -1, 0xFFFFFFFF], // 2001::/32 - teredo, may remove
[0xFC000000, -1, 0xFE000000], // fc00::/7
[0xFE800000, -1, 0xFFC00000], // fe80::/10
[0xFF000000, -1, 0xFF000000] // ff00::/8
];

var LAN6_S = [
[0x00000000, 0xFFFFFFFF], // 0, ::/64 - catch {::, ::1, ::ffff:0:0/96, ::ffff:0:0:0/96}, may remove
[0x00000000, 0xFFFFFFFF], // 1, 64:ff9b::/64 - catch {64:ff9b::/96, NAT64}, may remove
[0x00010000, 0xFFFF0000], // 2, 64:ff9b:1::/48 - NAT64, may remove
[0x00000000, 0xFFFFFFFF] // 3, 100::/64
];