diff --git a/README.md b/README.md index eee6b4ff..62b1a087 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 基于[gfwlist][]的多种代理软件配置文件生成工具,支持自定义规则 -目前支持的格式有: **PAC, Dnsmasq, V2Ray, Shadowsocks, Quantumult X, Shadowrocket, Surge, Wingy, Potatso** 和 **IP** (国别IP列表), **List** (gfwlist格式的列表), **Copy** (复制源)。[示例](https://github.com/JinnLynn/genpac/tree/cooked) +目前支持的格式有: **PAC, Dnsmasq, V2Ray, Shadowsocks, Quantumult X, Shadowrocket, Surge, Sing-Box, Clash, Wingy, Potatso** 和 **IP** (国别IP列表), **List** (gfwlist格式的列表), **Copy** (复制源)。[示例](https://github.com/JinnLynn/genpac/tree/cooked) **注意**: 生成后的规则不会匹配网址路径,只会检查域名(包括子域名),如`|http://sub2.sub1.domain.com/path/to/file.ext` => `sub2.sub1.domain.com` diff --git a/example/config.ini b/example/config.ini index f1243551..cfa759d6 100644 --- a/example/config.ini +++ b/example/config.ini @@ -232,6 +232,15 @@ output = ./surge-set.conf [job:clash] output = ./clash.yaml +[job:sing] +output = ./sing-domain.json + +[job:sing] +sing-ip = true +sing-ip-cc = cn +sing-ip-family = 4 +output = ./sing-cnip.json + [job:wingy] output = ./wingy.yaml # adapter选项 diff --git a/src/genpac/format/ip.py b/src/genpac/format/ip.py index 6798b0d9..37bd24df 100644 --- a/src/genpac/format/ip.py +++ b/src/genpac/format/ip.py @@ -1,6 +1,6 @@ import re -from netaddr import IPNetwork, IPRange +from netaddr import IPNetwork, IPRange, IPAddress from ..util import FatalError from ..util import conv_lower @@ -23,25 +23,66 @@ } -class IPList(list): - def add(self, item): - if isinstance(item, IPNetwork): - self.append(item) - elif isinstance(item, IPRange): - self.extend(item.cidrs()) - else: - raise ValueError('ONLY IPNetwork or IPRange') +class IPInterface(FmtBase): + def iter_ip_cidr(self, family, cc): + family = str(family) + if family not in ['4', '6', 'all']: + raise ValueError('IP family MUST BE: 4, 6, all') + if family in ['4', 'all']: + yield from self._fetch_data(4, cc) - @property - def size(self): - return sum(item.size for item in self) + if family in ['6', 'all']: + yield from self._fetch_data(6, cc) - def iter_cidrs(self): - return self + def iter_ip_range(self, family, cc): + for d in self.iter_ip_cidr(family, cc): + yield str(IPAddress(d.first)), str(IPAddress(d.last)) + + def _fetch_data(self, family, cc): + if cc.lower() == 'cn': + yield from self._fetch_data_cn(family) + return + + expr = re.compile(f'^[0-9a-f:,]+,{cc}' if family == 6 else f'^[0-9\\.,]+,{cc}', + flags=re.IGNORECASE) + url = _IP_DATA_GEOLITE2[int(family)] + content = self.fetch(url) + if not content: + raise FatalError('获取IP数据失败') + count = 0 + size = 0 + for d in content.splitlines(): + d = d.strip() + if not d or not expr.fullmatch(d): + continue + first, last, _ = d.split(',') + d = IPRange(first, last) + for n in d.cidrs(): + count = count + 1 + size = size + n.size + yield n + logger.debug(f'IPv{family}[{cc}]: {count} => {size:.2e}') + + def _fetch_data_cn(self, family): + url = _IP_DATA_ASN[int(family)] + content = self.fetch(url) + if not content: + raise FatalError('获取IP数据失败') + count = 0 + size = 0 + for ip in content.splitlines(): + ip = ip.strip() + if not ip: + continue + net = IPNetwork(ip) + count = count + 1 + size = size + net.size + yield net + logger.debug(f'IPv{family}[cn]: {count} => {size:.2e}') @formater('ip', desc="国别IP地址列表") -class FmtIP(FmtBase): +class FmtIP(IPInterface): _FORCE_IGNORE_GFWLIST = True def __init__(self, *args, **kwargs): @@ -60,84 +101,4 @@ def prepare(cls, parser): help=f'IP类型 可选: {families} 默认: 4') def generate(self, replacements): - ip4s, ip6s = self._generate_by_cc(self.options.cc) - output = ip4s + ip6s - return '\n'.join([str(i) for i in output]) - - @property - def _ipv4(self): - return self.options.family in [4, '4', 'all'] - - @property - def _ipv6(self): - return self.options.family in [6, '6', 'all'] - - def _ip_network(self, data): - try: - if isinstance(data, str): - return IPNetwork(data) - elif isinstance(data, tuple): - first, last = data - return IPRange(first, last) - raise ValueError('IP数据类型错误') - except Exception as e: - logger.warning(f'解析IP地址错误: {data} {e} {type(e)}') - return None - - def _generate_by_cc(self, cc): - ip4s = IPList() - ip6s = IPList() - - record = 0 - - if self._ipv4: - for d in self._fetch_data(4, cc): - ip_net = self._ip_network(d) - if ip_net: - ip4s.add(ip_net) - record = record + 1 - logger.debug(f'IPv4[{cc}]: Nums: {ip4s.size:.2e} ' - f'Record: {record} => {len(ip4s.iter_cidrs())}') - - record = 0 - if self._ipv6: - record = 0 - for d in self._fetch_data(6, cc): - ip_net = self._ip_network(d) - if ip_net: - ip6s.add(ip_net) - record = record + 1 - - logger.debug(f'IPv6[{cc}]: Nums: {ip6s.size:.2e} ' - f'Record: {record} => {len(ip6s.iter_cidrs())}') - - return ip4s, ip6s - - def _fetch_data_cn(self, family): - url = _IP_DATA_ASN[int(family)] - content = self.fetch(url) - if not content: - raise FatalError('获取IP数据失败') - for ip in content.splitlines(): - ip = ip.strip() - if not ip: - continue - yield ip - - def _fetch_data(self, family, cc): - if cc.lower() == 'cn': - yield from self._fetch_data_cn(family) - return - - expr = re.compile(f'^[0-9a-f:,]+,{cc}' if family == 6 else f'^[0-9\\.,]+,{cc}', - flags=re.IGNORECASE) - url = _IP_DATA_GEOLITE2[int(family)] - content = self.fetch(url) - if not content: - raise FatalError('获取IP数据失败') - for d in content.splitlines(): - d = d.strip() - if not d or not expr.fullmatch(d): - continue - first, last, _ = d.split(',') - yield (first, last) + return '\n'.join(str(i) for i in self.iter_ip_cidr(self.options.family, self.options.cc)) diff --git a/src/genpac/format/sing_box.py b/src/genpac/format/sing_box.py new file mode 100644 index 00000000..1afb8ffb --- /dev/null +++ b/src/genpac/format/sing_box.py @@ -0,0 +1,33 @@ +import json + +from ..util import conv_lower, Namespace +from .base import formater +from .ip import IPInterface, _IP_FAMILIES, _CC_DEF + + +@formater('sing', desc='Sing-Box路由规则集(Rule-Set)') +class FmtSingBox(IPInterface): + @classmethod + def prepare(cls, parser): + super().prepare(parser) + cls.register_option('ip', default=False, + action='store_true', help='默认输出规则基于域名,当指定该选项时输出基于IP的规则') + cls.register_option('ip-cc', conv=conv_lower, default=_CC_DEF, + metavar='CC', + help=f'当输出基于IP规则时的国家代码(ISO 3166-1) 默认: {_CC_DEF}') + cls.register_option('ip-family', conv=conv_lower, default='4', + type=lambda s: s.lower(), + choices=_IP_FAMILIES, + help=f'当输出基于IP规则时的类型 可选: {', '.join(_IP_FAMILIES)} 默认: 4') + + def generate(self, replacements): + data = Namespace(version=1, rules=[]) + if not self.options.ip: + data.rules.append({ + 'domain_suffix': self.gfwed_domains + }) + else: + data.rules.append({ + 'ip_cidr': [str(d) for d in self.iter_ip_cidr(self.options.ip_family, self.options.ip_cc)] + }) + return json.dumps(dict(data), indent=2)