Skip to content

Commit

Permalink
feat: new format for sing-box
Browse files Browse the repository at this point in the history
  • Loading branch information
JinnLynn committed Jul 22, 2024
1 parent 703456a commit 245dff8
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 97 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
9 changes: 9 additions & 0 deletions example/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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选项
Expand Down
153 changes: 57 additions & 96 deletions src/genpac/format/ip.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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))
33 changes: 33 additions & 0 deletions src/genpac/format/sing_box.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 245dff8

Please sign in to comment.