Commit 357eb383 authored by Vitaly Lipatov's avatar Vitaly Lipatov

Add route export API, telemt metrics, chat-dns split views, massdns CNAME fallback

route-web-api.py: - /api/export endpoint: filter by group/list/proto, text/mikrotik/json formats - /api/export/groups: list available groups with counts - aggregate=1 (exact) / aggregate=2 (approx via mapcidr) - Multiple groups support (group=gre,egw,zapret) - Resolved data from /var/lib/etersoft-router/state/ with in-memory cache (60s TTL) - Speed check: don't early-stop before checking first 2 gateways route-update.sh: - umask 022 for readable state files - chmod g+r on resolved after write - Per-list duration tracking (duration file in state) functions: - CNAME fallback: parallel dig (xargs -P 20) instead of sequential route-stats-metrics.sh: - New: collect route list counts and push to InfluxDB - Per-list duration metrics - Route-update total duration dns/chat-dns.sh: - Rewritten for split-view subzone chat.eterfund.ru via SSH to ns1 dns/telemt-metrics.sh: - Added upstream_success/fail/slow and handshake_timeout metrics Co-Authored-By: 's avatarClaude Opus 4.6 (1M context) <noreply@anthropic.com>
parent f1216eb0
#!/bin/bash #!/bin/bash
# chat-dns.sh — manage A records for chat.eterfund.ru # chat-dns.sh — manage A records for chat.eterfund.ru
# Run on ns1. Usage: chat-dns.sh on|off|status # Usage: chat-dns.sh on|off|status
# #
# on — add all three A records (round-robin) # on — add all three A records (round-robin) for external view
# off — keep only the primary IP # off — keep only the primary IP for external view
# status — show current A records # status — show current A records
#
# Trusted view (office) always returns only primary IP.
# Zone chat.eterfund.ru is delegated from eterfund.ru with split views on ns1.
DOMAIN="chat.eterfund.ru" DOMAIN="chat.eterfund.ru"
ZONE="eterfund.ru"
TTL=300
NS="ns1.etersoft.ru" NS="ns1.etersoft.ru"
NS_SSH="ssh -p32 root@$NS"
ZONE_FILE="/var/lib/bind/zone/chat.eterfund.ru.all"
PRIMARY="91.232.225.3" PRIMARY="91.232.225.3"
EXTRA_IPS="95.47.184.52 217.12.37.55" DIV="95.47.184.52"
BEGET="217.12.37.55"
usage() { usage() {
echo "Usage: $0 on|off|status" echo "Usage: $0 on|off|status"
echo " on — set all three A records (round-robin)" echo " on — set all three A records (round-robin, external only)"
echo " off — keep only primary ($PRIMARY)" echo " off — keep only primary ($PRIMARY, external)"
echo " status — show current A records" echo " status — show current A records"
exit 1 exit 1
} }
dns_status() { dns_status() {
echo "Current A records for $DOMAIN:" echo "Current A records for $DOMAIN:"
echo " external (view all):"
dig +short "$DOMAIN" A @"$NS" dig +short "$DOMAIN" A @"$NS"
echo " office (view trusted):"
dig +short "$DOMAIN" A
} }
inc_serial='
old=$(grep -oP "\d{10}(?=\s*;\s*serial)" '"$ZONE_FILE"')
today=$(date +%Y%m%d)
if [ "${old:0:8}" = "$today" ]; then
nn=$(printf "%02d" $(( ${old:8:2} + 1 )))
else
nn="01"
fi
new="${today}${nn}"
sed -i "s/$old/$new/" '"$ZONE_FILE"'
echo "Serial: $old -> $new"
'
dns_on() { dns_on() {
echo "Adding all A records for $DOMAIN..." echo "Adding all A records for $DOMAIN (external view)..."
nsupdate -l <<EOF $NS_SSH "
zone $ZONE cat > $ZONE_FILE << 'ZONE'
update delete $DOMAIN. A \\\$TTL 300
update add $DOMAIN. $TTL A $PRIMARY @ IN SOA ns1.etersoft.ru. support.etersoft.ru. (
update add $DOMAIN. $TTL A 95.47.184.52 2026033101 ; serial
update add $DOMAIN. $TTL A 217.12.37.55 1H ; refresh
send 5M ; retry
EOF 1W ; expire
5M ; minimum
)
IN NS ns1.etersoft.ru.
IN A $PRIMARY
IN A $DIV
IN A $BEGET
ZONE
$inc_serial
cd /var/lib/bind/zone && named-checkzone $DOMAIN chat.eterfund.ru.all 2>&1
rndc reload $DOMAIN IN all 2>&1
cd /var/lib/bind && git add zone/chat.eterfund.ru.all && git commit -m 'chat.eterfund.ru: round-robin on (3 IPs)'
"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "Done. All three IPs active." echo "Done. All three IPs active (external)."
else else
echo "nsupdate failed. Check allow-update in zone config." >&2 echo "Failed." >&2
exit 1 exit 1
fi fi
dns_status dns_status
} }
dns_off() { dns_off() {
echo "Removing extra A records, keeping only $PRIMARY..." echo "Removing extra A records, keeping only $PRIMARY (external view)..."
nsupdate -l <<EOF $NS_SSH "
zone $ZONE cat > $ZONE_FILE << 'ZONE'
update delete $DOMAIN. A \\\$TTL 300
update add $DOMAIN. $TTL A $PRIMARY @ IN SOA ns1.etersoft.ru. support.etersoft.ru. (
send 2026033101 ; serial
EOF 1H ; refresh
5M ; retry
1W ; expire
5M ; minimum
)
IN NS ns1.etersoft.ru.
IN A $PRIMARY
ZONE
$inc_serial
cd /var/lib/bind/zone && named-checkzone $DOMAIN chat.eterfund.ru.all 2>&1
rndc reload $DOMAIN IN all 2>&1
cd /var/lib/bind && git add zone/chat.eterfund.ru.all && git commit -m 'chat.eterfund.ru: primary only'
"
if [ $? -eq 0 ]; then if [ $? -eq 0 ]; then
echo "Done. Only primary IP active." echo "Done. Only primary IP active (external)."
else else
echo "nsupdate failed." >&2 echo "Failed." >&2
exit 1 exit 1
fi fi
dns_status dns_status
......
#!/bin/bash
# telemt-metrics.sh — collect telemt metrics and push to InfluxDB
# Install on each telemt server via cron: * * * * * /path/to/telemt-metrics.sh
# Requires: curl
INFLUX_URL="http://telegraf.office.etersoft.ru:8086"
INFLUX_DB="gateways"
METRICS_URL="http://127.0.0.1:9090/metrics"
HOST=$(hostname -s)
raw=$(curl -sf --max-time 5 "$METRICS_URL") || exit 0
connections_total=$(echo "$raw" | awk '/^telemt_connections_total / {print $2}')
connections_bad=$(echo "$raw" | awk '/^telemt_connections_bad_total / {print $2}')
connections_current=$(echo "$raw" | awk '/^telemt_user_connections_current\{/ {print $2}')
octets_in=$(echo "$raw" | awk '/^telemt_user_octets_from_client\{/ {print $2}')
octets_out=$(echo "$raw" | awk '/^telemt_user_octets_to_client\{/ {print $2}')
unique_ips=$(echo "$raw" | awk '/^telemt_user_unique_ips_current\{/ {print $2}')
uptime=$(echo "$raw" | awk '/^telemt_uptime_seconds / {printf "%.0f", $2}')
upstream_success=$(echo "$raw" | awk '/^telemt_upstream_connect_success_total / {print $2}')
upstream_fail=$(echo "$raw" | awk '/^telemt_upstream_connect_fail_total / {print $2}')
upstream_slow=$(echo "$raw" | awk '/^telemt_upstream_connect_duration_success_total\{bucket="gt_1000ms"\}/ {print $2}')
handshake_timeout=$(echo "$raw" | awk '/^telemt_handshake_timeouts_total / {print $2}')
# InfluxDB line protocol
line="telemt,host=$HOST connections_total=${connections_total:-0}i,connections_bad=${connections_bad:-0}i,connections_current=${connections_current:-0}i,octets_in=${octets_in:-0}i,octets_out=${octets_out:-0}i,unique_ips=${unique_ips:-0}i,uptime=${uptime:-0}i,upstream_success=${upstream_success:-0}i,upstream_fail=${upstream_fail:-0}i,upstream_slow=${upstream_slow:-0}i,handshake_timeout=${handshake_timeout:-0}i"
curl -sf --max-time 5 -XPOST "$INFLUX_URL/write?db=$INFLUX_DB" --data-binary "$line"
...@@ -164,14 +164,16 @@ get_ipv4_list_bulk() ...@@ -164,14 +164,16 @@ get_ipv4_list_bulk()
print $2 print $2
}' }'
# Fallback to dig for CNAME domains (skip nxdomain/nodata/timeout) # Fallback for CNAME domains: parallel dig
local cname_domains=$(mktemp)
grep "^;" "$adns_out" | grep -v 'nxdomain\|nodata\|timeout\|querydomaintoolong' | \ grep "^;" "$adns_out" | grep -v 'nxdomain\|nodata\|timeout\|querydomaintoolong' | \
awk '{ for(i=4;i<=NF;i++) if($i ~ /^[a-z0-9].*\.[a-z]/) { print $i; break } }' | \ awk '{ for(i=4;i<=NF;i++) if($i ~ /^[a-z0-9].*\.[a-z]/) { print $i; break } }' | \
sort -u | while read domain ; do sort -u > "$cname_domains"
echo if [ -s "$cname_domains" ] ; then
echo "# $domain (dig fallback)" xargs -P 20 -I{} dig +short {} A < "$cname_domains" 2>/dev/null
get_ipv4_list "$domain" [ -n "$EXTRA_DNS" ] && xargs -P 20 -I{} dig @$EXTRA_DNS +short {} A < "$cname_domains" 2>/dev/null || true
done fi
rm -f "$cname_domains"
} }
# Bulk resolve domains to IPv6 via adnshost (async) with dig fallback for CNAMEs # Bulk resolve domains to IPv6 via adnshost (async) with dig fallback for CNAMEs
...@@ -207,14 +209,20 @@ get_ipv6_list_bulk() ...@@ -207,14 +209,20 @@ get_ipv6_list_bulk()
print $2 print $2
}' }'
# Fallback to dig for CNAME domains (skip nxdomain/nodata/timeout) # Fallback for CNAME domains via massdns (fast parallel resolver)
local cname_domains=$(mktemp)
grep "^;" "$adns_out" | grep -v 'nxdomain\|nodata\|timeout\|querydomaintoolong' | \ grep "^;" "$adns_out" | grep -v 'nxdomain\|nodata\|timeout\|querydomaintoolong' | \
awk '{ for(i=4;i<=NF;i++) if($i ~ /^[a-z0-9].*\.[a-z]/) { print $i; break } }' | \ awk '{ for(i=4;i<=NF;i++) if($i ~ /^[a-z0-9].*\.[a-z]/) { print $i; break } }' | \
sort -u | while read domain ; do sort -u > "$cname_domains"
echo if [ -s "$cname_domains" ] ; then
echo "# $domain (dig fallback)" local resolvers=$(mktemp)
get_ipv6_list "$domain" awk '/^nameserver/ {print $2}' /etc/resolv.conf > "$resolvers"
done [ -n "$EXTRA_DNS" ] && echo "$EXTRA_DNS" >> "$resolvers"
massdns -r "$resolvers" -t AAAA -o S --quiet < "$cname_domains" 2>/dev/null | \
awk '/ AAAA / { sub(/\.$/, "", $3); print $3 }'
rm -f "$resolvers"
fi
rm -f "$cname_domains"
} }
# Compute covering IPv6 subnets from a list of addresses (stdin). # Compute covering IPv6 subnets from a list of addresses (stdin).
......
#!/bin/bash
# route-stats-metrics.sh — collect route list stats and push to InfluxDB
# Install on igw via cron: * * * * * /root/etersoft-admin-essential/router/route-stats-metrics.sh
# Requires: curl
INFLUX_URL="http://telegraf.office.etersoft.ru:8086"
INFLUX_DB="gateways"
BASEDIR="/root/etersoft-admin-essential/router"
HOST=$(hostname -s)
lines=""
for base in routes.d routes6.d; do
basedir="$BASEDIR/$base"
[ -d "$basedir" ] || continue
case "$base" in
routes.d) proto="ipv4" ;;
routes6.d) proto="ipv6" ;;
esac
for dir in "$basedir"/*/; do
[ -d "$dir" ] || continue
group=$(basename "$dir")
group_total=0
for f in "$dir"*.list; do
[ -f "$f" ] || continue
name=$(basename "$f" .list)
count=$(wc -l < "$f")
group_total=$((group_total + count))
lines="${lines}route_lists,host=$HOST,proto=$proto,group=$group,list=$name count=${count}i
"
done
lines="${lines}route_lists,host=$HOST,proto=$proto,group=$group,list=_total count=${group_total}i
"
done
done
[ -z "$lines" ] && exit 0
# route-update total duration
duration_file="$BASEDIR/../route-web-api/update-duration"
if [ -f "$duration_file" ]; then
duration=$(cat "$duration_file" 2>/dev/null)
[ -n "$duration" ] && lines="${lines}route_update,host=$HOST duration=${duration}i
"
fi
# per-list duration
STATE_DIR="/var/lib/etersoft-router/state"
for subdir in routes.d routes6.d; do
state_base="$STATE_DIR/$subdir"
[ -d "$state_base" ] || continue
case "$subdir" in
routes.d) sproto="ipv4" ;;
routes6.d) sproto="ipv6" ;;
esac
for gdir in "$state_base"/*/; do
[ -d "$gdir" ] || continue
sgroup=$(basename "$gdir")
for ldir in "$gdir"*/; do
dur_file="$ldir/duration"
[ -f "$dur_file" ] || continue
slist=$(basename "$ldir")
dur=$(cat "$dur_file" 2>/dev/null)
[ -n "$dur" ] && lines="${lines}route_list_duration,host=$HOST,proto=$sproto,group=$sgroup,list=$slist duration=${dur}i
"
done
done
done
echo -n "$lines" | curl -sf --max-time 5 -XPOST "$INFLUX_URL/write?db=$INFLUX_DB" --data-binary @-
#!/bin/sh #!/bin/sh
# Unified route updater: reads directory-based config from routes.d/ and routes6.d/ # Unified route updater: reads directory-based config from routes.d/ and routes6.d/
umask 022
# Each subdirectory = route group with gateway/table files and .list symlinks # Each subdirectory = route group with gateway/table files and .list symlinks
# #
# Usage: route-update.sh [--force] [--resolve] [--show] [--verbose] # Usage: route-update.sh [--force] [--resolve] [--show] [--verbose]
...@@ -808,6 +809,7 @@ for line in sys.stdin: ...@@ -808,6 +809,7 @@ for line in sys.stdin:
echo "$_pref" > "$STATE_DIR/$_state/pref" echo "$_pref" > "$STATE_DIR/$_state/pref"
cp "$_gwdir/gateway" "$STATE_DIR/$_state/gateway" cp "$_gwdir/gateway" "$STATE_DIR/$_state/gateway"
mv "$_resolved_new" "$STATE_DIR/$_state/resolved" mv "$_resolved_new" "$STATE_DIR/$_state/resolved"
chmod g+r "$STATE_DIR/$_state/resolved" 2>/dev/null
return 0 return 0
} }
...@@ -883,8 +885,12 @@ process_group() ...@@ -883,8 +885,12 @@ process_group()
fi fi
check_list_changed || continue check_list_changed || continue
local _list_start=$(date +%s)
resolve_list_file resolve_list_file
load_list_routes && log "$_tag$_label Done" load_list_routes
local _list_duration=$(( $(date +%s) - _list_start ))
echo "$_list_duration" > "$STATE_DIR/$_state/duration"
log "$_tag$_label Done in ${_list_duration}s"
done done
} }
...@@ -1167,4 +1173,5 @@ generate_web_json ...@@ -1167,4 +1173,5 @@ generate_web_json
_duration=$(( $(date +%s) - _start_time )) _duration=$(( $(date +%s) - _start_time ))
[ -n "$SHOW" ] || echo "$_duration" > "$DURATION_FILE" 2>/dev/null [ -n "$SHOW" ] || echo "$_duration" > "$DURATION_FILE" 2>/dev/null
[ -n "$SHOW" ] || chmod -R g+rX "$STATE_DIR" 2>/dev/null
[ -n "$SHOW" ] || log "All done in ${_duration}s" [ -n "$SHOW" ] || log "All done in ${_duration}s"
...@@ -45,6 +45,92 @@ DURATION_FILE = os.path.join(BASEDIR, "update-duration") ...@@ -45,6 +45,92 @@ DURATION_FILE = os.path.join(BASEDIR, "update-duration")
MAX_ENTRIES = 500 MAX_ENTRIES = 500
UPDATE_INTERVAL = 300 # route-update.sh cycle, seconds UPDATE_INTERVAL = 300 # route-update.sh cycle, seconds
# --- Export resolved route lists ---
STATE_DIR = "/var/lib/etersoft-router/state"
EXPORT_CACHE_TTL = 60 # seconds
_export_cache = {} # {proto: {group: {list: [ips]}}}
_export_cache_time = 0
_export_cache_lock = threading.Lock()
def _load_resolved_lists():
"""Read all resolved files from .state/routes.d and routes6.d."""
cache = {}
for proto, subdir in [("ipv4", "routes.d"), ("ipv6", "routes6.d")]:
cache[proto] = {}
state_base = os.path.join(STATE_DIR, subdir)
if not os.path.isdir(state_base):
continue
for group in sorted(os.listdir(state_base)):
group_dir = os.path.join(state_base, group)
if not os.path.isdir(group_dir):
continue
cache[proto][group] = {}
for listname in sorted(os.listdir(group_dir)):
resolved = os.path.join(group_dir, listname, "resolved")
if not os.path.isfile(resolved):
continue
try:
with open(resolved, "r") as f:
ips = [l.strip() for l in f if l.strip()]
cache[proto][group][listname] = ips
except OSError:
pass
return cache
def get_export_cache():
"""Return cached resolved lists, refreshing if stale."""
global _export_cache, _export_cache_time
now = time.time()
if now - _export_cache_time > EXPORT_CACHE_TTL:
with _export_cache_lock:
if now - _export_cache_time > EXPORT_CACHE_TTL:
_export_cache = _load_resolved_lists()
_export_cache_time = now
return _export_cache
def aggregate_ips(entries, level=1):
"""Aggregate IPs/subnets using mapcidr.
level=1: exact aggregation (merge adjacent only)
level=2: approximate aggregation (smart subnet grouping)
"""
if not entries:
return []
flag = "-aa" if level >= 2 else "-a"
try:
proc = subprocess.run(
["mapcidr", flag, "-silent"],
input="\n".join(entries) + "\n",
capture_output=True, text=True, timeout=30,
)
if proc.returncode == 0 and proc.stdout.strip():
return [l.strip() for l in proc.stdout.strip().split("\n") if l.strip()]
except (subprocess.TimeoutExpired, FileNotFoundError):
pass
# Fallback to stdlib
nets = []
for e in entries:
try:
nets.append(ipaddress.ip_network(e, strict=False))
except ValueError:
continue
return [str(n) for n in ipaddress.collapse_addresses(sorted(nets))]
def format_mikrotik(entries, list_name="vpn-hosts", ipv6=False):
"""Format as MikroTik address-list commands."""
prefix = "/ipv6" if ipv6 else "/ip"
lines = [
f"{prefix} firewall address-list remove [find where list={list_name}]",
f"{prefix} firewall address-list",
]
for ip in entries:
lines.append(f"add list={list_name} address={ip}")
return "\n".join(lines) + "\n"
CHECK_GATEWAYS = [ CHECK_GATEWAYS = [
# (name, proxy_v4, proxy_v6) # (name, proxy_v4, proxy_v6)
("dgw", "socks5://91.232.225.12:1080", "socks5://[2a03:5a00:c:20::12]:1080"), ("dgw", "socks5://91.232.225.12:1080", "socks5://[2a03:5a00:c:20::12]:1080"),
...@@ -220,15 +306,17 @@ def run_speed_test(file_url, callback): ...@@ -220,15 +306,17 @@ def run_speed_test(file_url, callback):
""" """
gw_by_name = {name: proxy for name, proxy, _pv6 in CHECK_GATEWAYS} gw_by_name = {name: proxy for name, proxy, _pv6 in CHECK_GATEWAYS}
consecutive_ok = 0 consecutive_ok = 0
checked = 0
for gw_name in SPEED_CHECK_ORDER: for gw_name in SPEED_CHECK_ORDER:
proxy = gw_by_name.get(gw_name) proxy = gw_by_name.get(gw_name)
if proxy is None: if proxy is None:
continue continue
_name, res = _check_speed(gw_name, proxy, file_url) _name, res = _check_speed(gw_name, proxy, file_url)
callback(gw_name, res) callback(gw_name, res)
checked += 1
if res and not res.get("error") and res.get("time", 99) < SPEED_CHECK_TIMEOUT - 0.5: if res and not res.get("error") and res.get("time", 99) < SPEED_CHECK_TIMEOUT - 0.5:
consecutive_ok += 1 consecutive_ok += 1
if consecutive_ok >= 2: if consecutive_ok >= 2 and checked > 2:
break break
else: else:
consecutive_ok = 0 consecutive_ok = 0
...@@ -462,6 +550,33 @@ OPENAPI_SPEC = { ...@@ -462,6 +550,33 @@ OPENAPI_SPEC = {
}, },
} }
}, },
"/api/export": {
"get": {
"summary": "Export resolved route lists",
"description": "Возвращает resolved IP из списков маршрутизации. Поддерживает фильтрацию по группе, списку, протоколу. Форматы: text, mikrotik, json. Опциональная агрегация в CIDR.",
"parameters": [
{"name": "group", "in": "query", "schema": {"type": "string", "default": "all"}, "description": "Группа: gre, egw, zapret, dgw, all"},
{"name": "list", "in": "query", "schema": {"type": "string"}, "description": "Конкретный список (telegram, youtube, ai...)"},
{"name": "proto", "in": "query", "schema": {"type": "string", "enum": ["ipv4", "ipv6", "all"], "default": "ipv4"}, "description": "Протокол"},
{"name": "format", "in": "query", "schema": {"type": "string", "enum": ["text", "mikrotik", "json"], "default": "text"}, "description": "Формат ответа"},
{"name": "aggregate", "in": "query", "schema": {"type": "string", "enum": ["0", "1", "2"], "default": "0"}, "description": "0=нет, 1=точная (смежные), 2=приближённая (умная)"},
{"name": "mikrotik_list", "in": "query", "schema": {"type": "string", "default": "vpn-hosts"}, "description": "Имя address-list для MikroTik"},
],
"responses": {
"200": {"description": "Список IP/CIDR"},
"404": {"description": "Группа/список не найдены"},
},
}
},
"/api/export/groups": {
"get": {
"summary": "List available route groups",
"description": "Возвращает доступные группы с количеством IP и именами списков.",
"responses": {
"200": {"description": "Список групп"},
},
}
},
}, },
} }
...@@ -1342,9 +1457,117 @@ class RouteHandler(http.server.BaseHTTPRequestHandler): ...@@ -1342,9 +1457,117 @@ class RouteHandler(http.server.BaseHTTPRequestHandler):
except (FileNotFoundError, json.JSONDecodeError): except (FileNotFoundError, json.JSONDecodeError):
self.send_json({"groups": {}}) self.send_json({"groups": {}})
elif path == "/api/export":
self.handle_export()
elif path == "/api/export/groups":
cache = get_export_cache()
groups = {}
for proto in ("ipv4", "ipv6"):
for group, lists in cache.get(proto, {}).items():
if group not in groups:
groups[group] = {"ipv4": 0, "ipv6": 0, "lists": []}
total = sum(len(v) for v in lists.values())
groups[group][proto] = total
if proto == "ipv4":
groups[group]["lists"] = sorted(lists.keys())
self.send_json(groups)
else: else:
self.send_error_json(404, "Not found") self.send_error_json(404, "Not found")
def handle_export(self):
params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
group_param = params.get("group", ["all"])[0]
groups_requested = [g.strip() for g in group_param.split(",")]
listname = params.get("list", [""])[0]
proto = params.get("proto", ["ipv4"])[0]
fmt = params.get("format", ["text"])[0]
agg_level = int(params.get("aggregate", ["0"])[0] or 0)
mikrotik_list = params.get("mikrotik_list", ["vpn-hosts"])[0]
cache = get_export_cache()
def _collect(protos_list):
"""Collect IPs for given protos."""
result = {}
for p in protos_list:
proto_data = cache.get(p, {})
if "all" in groups_requested:
groups_iter = proto_data.items()
else:
groups_iter = [(g, proto_data[g]) for g in groups_requested if g in proto_data]
for gname, lists in groups_iter:
for lname, ips in lists.items():
if listname and lname != listname:
continue
key = f"{gname}/{lname}" if len(groups_requested) != 1 or "all" in groups_requested else lname
result.setdefault(key, []).extend(ips)
return result
protos = ["ipv4", "ipv6"] if proto == "all" else [proto]
collected = _collect(protos)
if not collected:
self.send_error_json(404, f"Group '{group_param}' or list '{listname}' not found")
return
def _flatten(col):
out = []
for ips in col.values():
out.extend(ips)
return out
if fmt == "mikrotik" and proto == "all":
# Separate IPv4 and IPv6 blocks
col4 = _collect(["ipv4"])
col6 = _collect(["ipv6"])
ips4 = _flatten(col4)
ips6 = _flatten(col6)
if agg_level:
ips4 = aggregate_ips(ips4, agg_level)
ips6 = aggregate_ips(ips6, agg_level)
body = ""
if ips4:
body += format_mikrotik(ips4, mikrotik_list, ipv6=False)
if ips6:
body += "\n" + format_mikrotik(ips6, mikrotik_list, ipv6=True)
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(body.encode())
return
all_ips = _flatten(collected)
if agg_level:
all_ips = aggregate_ips(all_ips, agg_level)
is_ipv6 = proto == "ipv6"
if fmt == "json":
result = {"group": group_param, "proto": proto, "total": len(all_ips)}
if agg_level:
result["aggregated"] = True
result["entries"] = all_ips
else:
result["lists"] = {}
for key, ips in sorted(collected.items()):
result["lists"][key] = ips
result["total"] = sum(len(v) for v in collected.values())
self.send_json(result)
elif fmt == "mikrotik":
body = format_mikrotik(all_ips, mikrotik_list, ipv6=is_ipv6)
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(body.encode())
else:
body = "\n".join(all_ips) + "\n" if all_ips else ""
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(body.encode())
def do_POST(self): def do_POST(self):
path = urllib.parse.urlparse(self.path).path path = urllib.parse.urlparse(self.path).path
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment