feat: rule inheritance, rule groups, rule show/inspect redesign, rule add/update --extends --group, list filters

This commit is contained in:
Nuno Duque Nunes 2026-05-14 02:10:50 +00:00
parent 6ac1a7d3a2
commit 7b32dcfebc
4 changed files with 504 additions and 328 deletions

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,7 @@ function json::rule_resolve() { python3 "$JSON_HELPER" rule_resolve "$
function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_field "$@" </dev/null; } function json::rule_resolve_field() { python3 "$JSON_HELPER" rule_resolve_field "$@" </dev/null; }
function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; } function json::rule_inspect() { python3 "$JSON_HELPER" rule_inspect "$@" </dev/null; }
function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; } function json::find_rule_file() { python3 "$JSON_HELPER" find_rule_file "$@" </dev/null; }
function json::get_raw() { python3 "$JSON_HELPER" get_raw "$@" </dev/null; }
function json::peer_transfer() { function json::peer_transfer() {
ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \ ACTIVITY_TOTAL_LOW="$(config::activity_total_low)" \

View file

@ -748,15 +748,18 @@ def fmt_datetime(iso_str, fmt):
except: except:
print(iso_str) print(iso_str)
def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips, block_ports): def create_rule(file, name, desc, dns_redirect, allow_ips, block_ips,
block_ports, allow_ports='', extends='', group=''):
rule = { rule = {
'name': name, 'name': name,
'desc': desc, 'desc': desc,
'group': group,
'dns_redirect': dns_redirect == 'true', 'dns_redirect': dns_redirect == 'true',
'allow_ips': [x for x in allow_ips.split(',') if x] if allow_ips else [], 'extends': [x for x in extends.split(',') if x] if extends else [],
'block_ips': [x for x in block_ips.split(',') if x] if block_ips else [], 'allow_ips': [x for x in allow_ips.split(',') if x] if allow_ips else [],
'allow_ports': [x for x in allow_ports.split(',') if x] if allow_ports else [],
'block_ips': [x for x in block_ips.split(',') if x] if block_ips else [],
'block_ports': [x for x in block_ports.split(',') if x] if block_ports else [], 'block_ports': [x for x in block_ports.split(',') if x] if block_ports else [],
'allow_ports': []
} }
with open(file, 'w') as f: with open(file, 'w') as f:
json.dump(rule, f, indent=2) json.dump(rule, f, indent=2)
@ -985,8 +988,10 @@ def _rule_resolve_internal(rules_dir, rule_name, visited=None):
rule = json.load(f) rule = json.load(f)
merged = { merged = {
'allow_ips': [], 'allow_ports': [], 'allow_ips': [],
'block_ips': [], 'block_ports': [], 'allow_ports': [],
'block_ips': [],
'block_ports': [],
'dns_redirect': False 'dns_redirect': False
} }
@ -999,15 +1004,21 @@ def _rule_resolve_internal(rules_dir, rule_name, visited=None):
if base.get('dns_redirect'): if base.get('dns_redirect'):
merged['dns_redirect'] = True merged['dns_redirect'] = True
merged['allow_ips'] = list(dict.fromkeys(merged['allow_ips'] + rule.get('allow_ips', []))) # Merge own fields — use .get() with defaults for all fields
merged['allow_ports'] = list(dict.fromkeys(merged['allow_ports'] + rule.get('allow_ports', []))) merged['allow_ips'] = list(dict.fromkeys(
merged['block_ips'] = list(dict.fromkeys(merged['block_ips'] + rule.get('block_ips', []))) merged['allow_ips'] + rule.get('allow_ips', [])))
merged['block_ports'] = list(dict.fromkeys(merged['block_ports'] + rule.get('block_ports', []))) merged['allow_ports'] = list(dict.fromkeys(
if rule.get('dns_redirect'): merged['allow_ports'] + rule.get('allow_ports', [])))
merged['block_ips'] = list(dict.fromkeys(
merged['block_ips'] + rule.get('block_ips', [])))
merged['block_ports'] = list(dict.fromkeys(
merged['block_ports'] + rule.get('block_ports', [])))
if rule.get('dns_redirect', False):
merged['dns_redirect'] = True merged['dns_redirect'] = True
merged['name'] = rule['name'] merged['name'] = rule.get('name', rule_name)
merged['desc'] = rule.get('desc', '') merged['desc'] = rule.get('desc', '')
merged['group'] = rule.get('group', '')
merged['extends'] = rule.get('extends', []) merged['extends'] = rule.get('extends', [])
return merged return merged
@ -1102,7 +1113,24 @@ def find_rule_file(rules_dir, rule_name):
]: ]:
if os.path.exists(path): if os.path.exists(path):
return path return path
raise FileNotFoundError(f"Rule not found: {rule_name}") return ""
def get_raw(file, key):
try:
with open(file) as f:
data = json.load(f)
val = data.get(key) # returns None if missing
if val is None:
pass # print nothing
elif isinstance(val, bool):
print(str(val).lower())
elif isinstance(val, list):
for v in val:
print(v)
else:
print(val)
except:
pass
commands = { commands = {
'get': lambda args: get(args[0], args[1]), 'get': lambda args: get(args[0], args[1]),
@ -1130,7 +1158,12 @@ commands = {
'rule_list_data': lambda args: rule_list_data(args[0], args[1]), 'rule_list_data': lambda args: rule_list_data(args[0], args[1]),
'group_list_data': lambda args: group_list_data(args[0], args[1]), 'group_list_data': lambda args: group_list_data(args[0], args[1]),
'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]), 'fmt_datetime': lambda args: fmt_datetime(args[0], args[1]),
'create_rule': lambda args: create_rule(args[0], args[1], args[2], args[3], args[4], args[5], args[6]), 'create_rule': lambda args: create_rule(
args[0], args[1], args[2], args[3], args[4], args[5], args[6],
args[7] if len(args) > 7 else '',
args[8] if len(args) > 8 else '',
args[9] if len(args) > 9 else ''
),
'cleanup_config': lambda args: cleanup_config(args[0]), 'cleanup_config': lambda args: cleanup_config(args[0]),
'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]), 'remove_peer_block': lambda args: remove_peer_block(args[0], args[1]),
'create_group': lambda args: create_group(args[0], args[1], args[2]), 'create_group': lambda args: create_group(args[0], args[1], args[2]),
@ -1145,6 +1178,7 @@ commands = {
'rule_resolve_field': lambda args: rule_resolve_field(args[0], args[1], args[2]), 'rule_resolve_field': lambda args: rule_resolve_field(args[0], args[1], args[2]),
'rule_inspect': lambda args: rule_inspect(args[0], args[1]), 'rule_inspect': lambda args: rule_inspect(args[0], args[1]),
'find_rule_file': lambda args: print(find_rule_file(args[0], args[1])), 'find_rule_file': lambda args: print(find_rule_file(args[0], args[1])),
'get_raw': lambda args: print(get_raw(args[0], args[1])),
} }
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -11,8 +11,9 @@ function rule::is_base() {
function rule::exists() { function rule::exists() {
local name="${1:-}" local name="${1:-}"
[[ -f "$(rule::path "${name}")" ]] || \ local path
[[ -f "$(ctx::rules::base)/${name}.rule" ]] path=$(json::find_rule_file "$(ctx::rules)" "$name")
[[ -n "$path" ]]
} }
function rule::require_assignable() { function rule::require_assignable() {
@ -36,6 +37,13 @@ function rule::get() {
json::rule_resolve_field "$(ctx::rules)" "$name" "$key" json::rule_resolve_field "$(ctx::rules)" "$name" "$key"
} }
function rule::get_own() {
local name="${1:-}" key="${2:-}"
local file
file=$(rule::path "$name") || return 0
json::get_raw "$file" "$key"
}
function rule::get_resolved() { function rule::get_resolved() {
local name="${1:-}" local name="${1:-}"
json::rule_resolve "$(ctx::rules)" "$name" json::rule_resolve "$(ctx::rules)" "$name"
@ -43,7 +51,9 @@ function rule::get_resolved() {
function rule::path() { function rule::path() {
local name="${1:-}" local name="${1:-}"
json::find_rule_file "$(ctx::rules)" "$name" local path
path=$(json::find_rule_file "$(ctx::rules)" "$name")
[[ -n "$path" ]] && echo "$path" || return 1
} }
function rule::get_all() { function rule::get_all() {