Compare commits

..

7 Commits

Author SHA1 Message Date
BROBIRD
87bcebc124 ci(github workflows): 更新actions/checkout和setup-python版本 2026-06-02 01:43:52 +08:00
BROBIRD
86cea74b26 fix a bug 2026-06-02 01:40:48 +08:00
BROBIRD
5af7247d76 ci(github actions): restrict scheduled workflow to master branch, improve ipv6 cidr check
1. add master branch restriction for update workflow, only allow manual trigger on master
2. rewrite ipv6 cidr validation with proper regex patterns, include ipv4-mapped ipv6 check
2026-06-02 01:37:00 +08:00
BROBIRD
15fbb52452 fix some bugs 2026-06-02 01:33:35 +08:00
BROBIRD
7d7818dc01 fix some bugs 2026-06-02 01:18:12 +08:00
BROBIRD
4444dbbffd fix some bugs 2026-06-01 23:24:00 +08:00
BROBIRD
d49854f267 try add autoupdate 2026-06-01 22:54:47 +08:00
2 changed files with 366 additions and 0 deletions

107
.github/workflows/update.yml vendored Normal file
View File

@@ -0,0 +1,107 @@
name: Update GFWList Rules
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
branches:
- master
jobs:
update:
runs-on: ubuntu-slim
if: github.ref == 'refs/heads/master'
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
persist-credentials: false
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Parse GFWList
run: python scripts/gfwlist_parser.py
- name: Download mihomo
run: |
MAX_RETRIES=5
RETRY_DELAY=10
for ((i=1; i<=MAX_RETRIES; i++)); do
echo "Attempt $i/$MAX_RETRIES to download mihomo..."
if TAG=$(curl -s --fail https://api.github.com/repos/MetaCubeX/mihomo/releases/latest | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/'); then
echo "Latest mihomo version: $TAG"
if curl -L --fail -o mihomo.gz "https://github.com/MetaCubeX/mihomo/releases/download/${TAG}/mihomo-linux-amd64-${TAG}.gz"; then
echo "Download succeeded"
if gzip -d mihomo.gz; then
chmod +x mihomo
rm -f mihomo.gz
echo "mihomo extracted and ready"
exit 0
else
echo "Failed to extract mihomo.gz"
fi
else
echo "Download failed (HTTP error)"
fi
else
echo "Failed to fetch latest release info"
fi
if [ $i -lt $MAX_RETRIES ]; then
echo "Retrying in $RETRY_DELAY seconds..."
sleep $RETRY_DELAY
fi
done
echo "ERROR: All download attempts failed"
exit 1
- name: Convert to mrs format
run: |
mkdir -p Clash/mrs
convert_file() {
local input_file=$1
local output_file=$2
local MAX_RETRIES=3
for ((i=1; i<=MAX_RETRIES; i++)); do
echo "[Attempt $i/$MAX_RETRIES] Converting $input_file"
if ./mihomo -f "$input_file" -in clash -out mrs -o "$output_file"; then
echo "Success: $input_file -> $output_file"
return 0
else
echo "Warning: Conversion failed for $input_file (Attempt $i/$MAX_RETRIES)"
if [ $i -lt $MAX_RETRIES ]; then
sleep 3
fi
fi
done
echo "ERROR: Failed to convert $input_file after $MAX_RETRIES attempts"
return 1
}
for file in Clash/Providers/*.yaml; do
if [ -f "$file" ]; then
filename=$(basename "$file" .yaml)
convert_file "$file" "Clash/mrs/${filename}.mrs"
fi
done
for file in Clash/Ruleset/*.list; do
if [ -f "$file" ]; then
filename=$(basename "$file" .list)
convert_file "$file" "Clash/mrs/${filename}.mrs"
fi
done
echo "Conversion completed"
- name: Commit and push changes
uses: EndBug/add-and-commit@v9
with:
message: '[AutoUpdate] Update GFWList rules and mrs files - $(date -u +"%Y-%m-%dT%H:%M:%SZ")'
push: true
branch: ${{ github.head_ref || github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

259
scripts/gfwlist_parser.py Normal file
View File

@@ -0,0 +1,259 @@
#!/usr/bin/env python3
import base64
import re
import os
import sys
def is_ip(s):
if not s:
return False
if s.endswith('^'):
s = s[:-1]
if '.' in s and ':' not in s:
ipv4_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if re.match(ipv4_pattern, s):
parts = s.split('.')
if all(0 <= int(p) <= 255 for p in parts):
return True
if ':' in s:
ipv6_pattern = r'^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^(::)$|^::1$'
if re.match(ipv6_pattern, s):
return True
if s.startswith('::ffff:'):
ipv4_in_v6_pattern = r'^::ffff:(\d{1,3}\.){3}\d{1,3}$'
if re.match(ipv4_in_v6_pattern, s):
return True
return False
return False
def is_cidr(s):
if '/' not in s:
return False
ip_part = s.split('/')[0]
suffix = s.split('/')[1]
if not suffix.isdigit():
return False
suffix_val = int(suffix)
if '.' in ip_part and ':' not in ip_part:
ipv4_pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if re.match(ipv4_pattern, ip_part):
parts = ip_part.split('.')
if all(0 <= int(p) <= 255 for p in parts):
if 0 <= suffix_val <= 32:
return True
return False
if ':' in ip_part:
ipv6_pattern = r'^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$|^(::)$|^::1$'
if re.match(ipv6_pattern, ip_part):
if 0 <= suffix_val <= 128:
return True
if ip_part.startswith('::ffff:'):
ipv4_in_v6_pattern = r'^::ffff:(\d{1,3}\.){3}\d{1,3}$'
if re.match(ipv4_in_v6_pattern, ip_part):
if 0 <= suffix_val <= 128:
return True
return False
return False
def normalize_ip_rule(ip_rule):
ip_rule = ip_rule.strip()
if not ip_rule:
return None
if ip_rule.endswith('^'):
ip_rule = ip_rule[:-1]
if is_ip(ip_rule) or is_cidr(ip_rule):
return ip_rule
return None
def normalize_ip_for_clash(ip_rule):
if is_cidr(ip_rule):
return ip_rule
if is_ip(ip_rule):
if ':' in ip_rule:
return ip_rule + '/128'
return ip_rule + '/32'
return None
def fetch_gfwlist(url="https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt"):
try:
import urllib.request
with urllib.request.urlopen(url) as response:
return response.read().decode('utf-8')
except Exception as e:
print(f"Error fetching GFWList: {e}")
sys.exit(1)
def extract_domain_from_url(url):
url = url.strip()
if url.endswith('^'):
url = url[:-1]
url = url.rstrip('/')
domain_match = re.search(r'(?:https?://)?(?:www\.)?([^/:]+)', url)
if domain_match:
domain = domain_match.group(1)
if domain and not is_ip(domain) and not is_cidr(domain):
return domain
return None
def parse_gfwlist(content):
domain_blacklist = []
domain_whitelist = []
ip_blacklist = []
ip_whitelist = []
try:
decoded = base64.b64decode(content).decode('utf-8')
except:
decoded = content
for line in decoded.split('\n'):
line = line.strip()
if not line or line.startswith('!') or line.startswith('['):
continue
if line.startswith('@@||'):
rule = line[4:]
if rule.endswith('^'):
rule = rule[:-1]
if is_ip(rule) or is_cidr(rule):
normalized = normalize_ip_rule(rule)
if normalized:
ip_whitelist.append(normalized)
else:
domain_whitelist.append(rule)
elif line.startswith('||'):
rule = line[2:]
if rule.endswith('^'):
rule = rule[:-1]
if is_ip(rule) or is_cidr(rule):
normalized = normalize_ip_rule(rule)
if normalized:
ip_blacklist.append(normalized)
else:
domain_blacklist.append(rule)
elif line.startswith('@@|') and len(line) > 3:
domain = extract_domain_from_url(line[3:])
if domain:
domain_whitelist.append(domain)
elif line.startswith('|') and len(line) > 1:
domain = extract_domain_from_url(line[1:])
if domain:
domain_blacklist.append(domain)
return domain_blacklist, domain_whitelist, ip_blacklist, ip_whitelist
def format_domain_suffix_rules(domains):
rules = []
for domain in sorted(set(domains)):
if domain:
rules.append(f"- DOMAIN-SUFFIX,{domain}")
return rules
def format_acl_rules(domains):
rules = []
for domain in sorted(set(domains)):
if domain:
rules.append(f"(^|\\.){domain}$")
return rules
def write_file(filepath, content):
os.makedirs(os.path.dirname(filepath), exist_ok=True)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
def format_ip_cidr_rules(ip_rules):
rules = []
for ip in sorted(set(ip_rules)):
normalized = normalize_ip_for_clash(ip)
if normalized:
if ':' in normalized:
rules.append(f"IP-CIDR6,{normalized},no-resolve")
else:
rules.append(f"IP-CIDR,{normalized},no-resolve")
return rules
def format_ip_cidr_acl_rules(ip_rules):
rules = []
for ip in sorted(set(ip_rules)):
if ip:
rules.append(ip)
return rules
def generate_acl_file(domain_list, ip_list, filename, title="GFWList Rules"):
header = f"""#**********************************************************************
# {title}
# Generated from GFWList
#
# 更新记录 https://raw.githubusercontent.com/ACL4SSR/ACL4SSR/master/more/New.md
#
#**********************************************************************
[bypass_all]
### 默认直连 自己可以自定义
### [outbound_block_list] 禁止访问列表
### [bypass_list] 直连列表 禁止访问列表
### [proxy_list] 代理列表
#**********************************************************************
[proxy_list]
# GFWList
"""
domain_rules = format_acl_rules(domain_list)
ip_rules = format_ip_cidr_acl_rules(ip_list)
all_rules = domain_rules + ip_rules
content = header + '\n'.join(all_rules) + '\n'
write_file(filename, content)
def generate_clash_provider_yaml(domain_list, ip_list, filename, title="payload"):
unique_domains = sorted(set(domain_list))
content = f"{title}:\n"
for domain in unique_domains:
content += f" - DOMAIN-SUFFIX,{domain}\n"
ip_rules = format_ip_cidr_rules(ip_list)
for rule in ip_rules:
content += f" - {rule}\n"
write_file(filename, content)
def generate_clash_ruleset_list(domain_list, ip_list, filename, title="GFWList"):
unique_domains = sorted(set(domain_list))
ip_rules = format_ip_cidr_rules(ip_list)
total = len(unique_domains) + len(ip_rules)
content = f"# 内容:{title}\n# 数量:{total}\n"
for domain in unique_domains:
content += f"DOMAIN-SUFFIX,{domain}\n"
for rule in ip_rules:
content += f"{rule}\n"
write_file(filename, content)
def main():
print("Fetching GFWList...")
content = fetch_gfwlist()
print("Parsing GFWList...")
domain_blacklist, domain_whitelist, ip_blacklist, ip_whitelist = parse_gfwlist(content)
print(f"Domain blacklist entries: {len(domain_blacklist)}")
print(f"Domain whitelist entries: {len(domain_whitelist)}")
print(f"IP blacklist entries: {len(ip_blacklist)}")
print(f"IP whitelist entries: {len(ip_whitelist)}")
generate_acl_file(domain_blacklist, ip_blacklist, 'Acl/fullgfwlist.acl', "GFWList Blacklist")
print("Generated: Acl/fullgfwlist.acl")
generate_clash_provider_yaml(domain_blacklist, ip_blacklist, 'Clash/Providers/ProxyGFWlist.yaml', 'payload')
print("Generated: Clash/Providers/ProxyGFWlist.yaml")
generate_clash_ruleset_list(domain_blacklist, ip_blacklist, 'Clash/Ruleset/ProxyGFWlist.list', 'GFWList 黑名单')
print("Generated: Clash/Ruleset/ProxyGFWlist.list")
generate_clash_provider_yaml(domain_whitelist, ip_whitelist, 'Clash/Providers/UnBan.yaml', 'payload')
print("Generated: Clash/Providers/UnBan.yaml")
generate_clash_ruleset_list(domain_whitelist, ip_whitelist, 'Clash/Ruleset/UnBan.list', 'GFWList 白名单')
print("Generated: Clash/Ruleset/UnBan.list")
if __name__ == "__main__":
main()