feat: Add DNS Guard app and Ollama model suggestions
DNS Guard (luci-app-dnsguard): - Privacy-focused DNS manager with KISS UI - DNS provider feed: FDN, Quad9, Cloudflare, Mullvad, AdGuard, etc. - Smart Config auto-detects fastest DNS for location - Category filtering (privacy, security, fast, family, adblock) - One-click provider switching with dnsmasq integration Ollama: - Add suggested models grid when no models installed - Clickable cards to download directly - Models: tinyllama, llama3.2, phi3, gemma2, qwen2.5, mistral, codellama Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5f85e76ac0
commit
72f51623fa
25
package/secubox/luci-app-dnsguard/Makefile
Normal file
25
package/secubox/luci-app-dnsguard/Makefile
Normal file
@ -0,0 +1,25 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=SecuBox DNS Guard - Privacy DNS Manager
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-dnsguard
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.dnsguard $(1)/usr/libexec/rpcd/
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-dnsguard.json $(1)/usr/share/rpcd/acl.d/
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-dnsguard.json $(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/dnsguard
|
||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/dnsguard/*.js $(1)/www/luci-static/resources/view/dnsguard/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@ -0,0 +1,265 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
var api = {
|
||||
status: rpc.declare({ object: 'luci.dnsguard', method: 'status' }),
|
||||
getProviders: rpc.declare({ object: 'luci.dnsguard', method: 'get_providers' }),
|
||||
setProvider: rpc.declare({ object: 'luci.dnsguard', method: 'set_provider', params: ['provider'] }),
|
||||
smartConfig: rpc.declare({ object: 'luci.dnsguard', method: 'smart_config' }),
|
||||
testDns: rpc.declare({ object: 'luci.dnsguard', method: 'test_dns', params: ['server', 'domain'] }),
|
||||
apply: rpc.declare({ object: 'luci.dnsguard', method: 'apply' })
|
||||
};
|
||||
|
||||
var categoryIcons = {
|
||||
privacy: '\uD83D\uDD12', // lock
|
||||
security: '\uD83D\uDEE1\uFE0F', // shield
|
||||
fast: '\u26A1', // lightning
|
||||
family: '\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC67', // family
|
||||
adblock: '\uD83D\uDEAB' // no entry
|
||||
};
|
||||
|
||||
var countryFlags = {
|
||||
FR: '\uD83C\uDDEB\uD83C\uDDF7',
|
||||
CH: '\uD83C\uDDE8\uD83C\uDDED',
|
||||
US: '\uD83C\uDDFA\uD83C\uDDF8',
|
||||
SE: '\uD83C\uDDF8\uD83C\uDDEA',
|
||||
CY: '\uD83C\uDDE8\uD83C\uDDFE',
|
||||
CA: '\uD83C\uDDE8\uD83C\uDDE6'
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
css: `
|
||||
:root { --dg-bg: #0f172a; --dg-card: #1e293b; --dg-border: #334155; --dg-text: #f1f5f9; --dg-muted: #94a3b8; --dg-accent: #3b82f6; --dg-success: #22c55e; --dg-warning: #f59e0b; }
|
||||
.dg-wrap { font-family: system-ui, sans-serif; background: var(--dg-bg); color: var(--dg-text); min-height: 100vh; padding: 1rem; }
|
||||
.dg-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--dg-border); }
|
||||
.dg-title { font-size: 1.5rem; font-weight: 700; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.dg-title span { font-size: 1.75rem; }
|
||||
.dg-badge { padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.75rem; font-weight: 600; }
|
||||
.dg-badge.privacy { background: rgba(59,130,246,0.2); color: var(--dg-accent); }
|
||||
.dg-badge.security { background: rgba(34,197,94,0.2); color: var(--dg-success); }
|
||||
.dg-status { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
|
||||
.dg-stat { background: var(--dg-card); border: 1px solid var(--dg-border); border-radius: 0.5rem; padding: 1rem; min-width: 150px; }
|
||||
.dg-stat-val { font-size: 1.25rem; font-weight: 700; color: var(--dg-accent); }
|
||||
.dg-stat-lbl { font-size: 0.7rem; color: var(--dg-muted); text-transform: uppercase; margin-top: 0.25rem; }
|
||||
.dg-card { background: var(--dg-card); border: 1px solid var(--dg-border); border-radius: 0.5rem; margin-bottom: 1rem; }
|
||||
.dg-card-head { padding: 0.75rem 1rem; background: rgba(0,0,0,0.2); border-bottom: 1px solid var(--dg-border); font-weight: 600; display: flex; justify-content: space-between; align-items: center; }
|
||||
.dg-card-body { padding: 1rem; }
|
||||
.dg-providers { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; }
|
||||
.dg-provider { background: var(--dg-bg); border: 1px solid var(--dg-border); border-radius: 0.5rem; padding: 0.75rem; cursor: pointer; transition: all 0.2s; }
|
||||
.dg-provider:hover { border-color: var(--dg-accent); }
|
||||
.dg-provider.selected { border-color: var(--dg-success); background: rgba(34,197,94,0.1); }
|
||||
.dg-provider-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.5rem; }
|
||||
.dg-provider-name { font-weight: 600; flex: 1; }
|
||||
.dg-provider-flag { font-size: 1.1rem; }
|
||||
.dg-provider-cat { font-size: 0.75rem; padding: 0.15rem 0.5rem; border-radius: 0.25rem; background: rgba(59,130,246,0.2); color: var(--dg-accent); }
|
||||
.dg-provider-desc { font-size: 0.8rem; color: var(--dg-muted); margin-bottom: 0.5rem; }
|
||||
.dg-provider-dns { font-family: monospace; font-size: 0.75rem; color: var(--dg-muted); }
|
||||
.dg-btn { padding: 0.5rem 1rem; border: none; border-radius: 0.375rem; font-size: 0.85rem; font-weight: 500; cursor: pointer; transition: opacity 0.2s; }
|
||||
.dg-btn:hover { opacity: 0.8; }
|
||||
.dg-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.dg-btn-primary { background: var(--dg-accent); color: #fff; }
|
||||
.dg-btn-success { background: var(--dg-success); color: #fff; }
|
||||
.dg-btn-sm { padding: 0.25rem 0.5rem; font-size: 0.75rem; }
|
||||
.dg-btns { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
.dg-filter { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
||||
.dg-filter-btn { padding: 0.25rem 0.75rem; border: 1px solid var(--dg-border); border-radius: 1rem; background: transparent; color: var(--dg-muted); font-size: 0.8rem; cursor: pointer; }
|
||||
.dg-filter-btn:hover, .dg-filter-btn.active { border-color: var(--dg-accent); color: var(--dg-accent); background: rgba(59,130,246,0.1); }
|
||||
.dg-smart { background: linear-gradient(135deg, rgba(59,130,246,0.2), rgba(34,197,94,0.2)); border: 1px dashed var(--dg-accent); border-radius: 0.5rem; padding: 1rem; margin-bottom: 1rem; }
|
||||
.dg-smart-title { font-weight: 600; margin-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem; }
|
||||
.dg-smart-results { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-top: 0.75rem; }
|
||||
.dg-smart-result { font-size: 0.8rem; padding: 0.25rem 0.5rem; background: var(--dg-card); border-radius: 0.25rem; }
|
||||
.dg-smart-result.best { background: var(--dg-success); color: #fff; }
|
||||
.dg-toast { position: fixed; bottom: 1rem; right: 1rem; padding: 0.75rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; z-index: 9999; }
|
||||
.dg-toast.success { background: var(--dg-success); color: #fff; }
|
||||
.dg-toast.error { background: #ef4444; color: #fff; }
|
||||
`,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
api.status().catch(function() { return {}; }),
|
||||
api.getProviders().catch(function() { return { providers: [] }; })
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var status = data[0] || {};
|
||||
var providers = (data[1] && data[1].providers) || [];
|
||||
this.providers = providers;
|
||||
this.selectedProvider = status.provider || 'fdn';
|
||||
this.activeFilter = 'all';
|
||||
|
||||
var view = E('div', { 'class': 'dg-wrap' }, [
|
||||
E('style', {}, this.css),
|
||||
E('div', { 'class': 'dg-header' }, [
|
||||
E('div', { 'class': 'dg-title' }, [
|
||||
E('span', {}, '\uD83D\uDEE1\uFE0F'),
|
||||
'DNS Guard'
|
||||
]),
|
||||
E('div', { 'class': 'dg-badge ' + (status.mode || 'privacy') }, status.mode === 'adguardhome' ? 'AdGuard Home' : 'dnsmasq')
|
||||
]),
|
||||
E('div', { 'class': 'dg-status', 'id': 'dg-status' }, this.renderStatus(status)),
|
||||
E('div', { 'class': 'dg-smart', 'id': 'dg-smart' }, [
|
||||
E('div', { 'class': 'dg-smart-title' }, ['\u26A1 ', 'Smart Config']),
|
||||
E('div', {}, 'Auto-detect the fastest uncensored DNS for your location'),
|
||||
E('div', { 'class': 'dg-btns', 'style': 'margin-top: 0.75rem;' }, [
|
||||
E('button', { 'class': 'dg-btn dg-btn-primary', 'id': 'smart-btn', 'click': function() { self.runSmartConfig(); } }, 'Run Smart Config')
|
||||
]),
|
||||
E('div', { 'class': 'dg-smart-results', 'id': 'smart-results' })
|
||||
]),
|
||||
E('div', { 'class': 'dg-card' }, [
|
||||
E('div', { 'class': 'dg-card-head' }, [
|
||||
'DNS Providers',
|
||||
E('div', { 'class': 'dg-btns' }, [
|
||||
E('button', { 'class': 'dg-btn dg-btn-success dg-btn-sm', 'id': 'apply-btn', 'click': function() { self.applyConfig(); } }, 'Apply')
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'dg-card-body' }, [
|
||||
E('div', { 'class': 'dg-filter', 'id': 'dg-filter' }, this.renderFilters()),
|
||||
E('div', { 'class': 'dg-providers', 'id': 'dg-providers' }, this.renderProviders(providers, 'all'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
renderStatus: function(status) {
|
||||
return [
|
||||
E('div', { 'class': 'dg-stat' }, [
|
||||
E('div', { 'class': 'dg-stat-val' }, status.primary || 'Auto'),
|
||||
E('div', { 'class': 'dg-stat-lbl' }, 'Primary DNS')
|
||||
]),
|
||||
E('div', { 'class': 'dg-stat' }, [
|
||||
E('div', { 'class': 'dg-stat-val' }, status.secondary || '-'),
|
||||
E('div', { 'class': 'dg-stat-lbl' }, 'Secondary DNS')
|
||||
]),
|
||||
E('div', { 'class': 'dg-stat' }, [
|
||||
E('div', { 'class': 'dg-stat-val' }, status.provider || 'custom'),
|
||||
E('div', { 'class': 'dg-stat-lbl' }, 'Provider')
|
||||
])
|
||||
];
|
||||
},
|
||||
|
||||
renderFilters: function() {
|
||||
var self = this;
|
||||
var categories = ['all', 'privacy', 'security', 'fast', 'family', 'adblock'];
|
||||
return categories.map(function(cat) {
|
||||
return E('button', {
|
||||
'class': 'dg-filter-btn' + (self.activeFilter === cat ? ' active' : ''),
|
||||
'click': function() { self.filterProviders(cat); }
|
||||
}, cat === 'all' ? 'All' : (categoryIcons[cat] || '') + ' ' + cat.charAt(0).toUpperCase() + cat.slice(1));
|
||||
});
|
||||
},
|
||||
|
||||
renderProviders: function(providers, filter) {
|
||||
var self = this;
|
||||
return providers.filter(function(p) {
|
||||
return filter === 'all' || p.category === filter;
|
||||
}).map(function(p) {
|
||||
return E('div', {
|
||||
'class': 'dg-provider' + (self.selectedProvider === p.id ? ' selected' : ''),
|
||||
'data-id': p.id,
|
||||
'click': function() { self.selectProvider(p.id); }
|
||||
}, [
|
||||
E('div', { 'class': 'dg-provider-head' }, [
|
||||
E('span', { 'class': 'dg-provider-flag' }, countryFlags[p.country] || ''),
|
||||
E('span', { 'class': 'dg-provider-name' }, p.name),
|
||||
E('span', { 'class': 'dg-provider-cat' }, (categoryIcons[p.category] || '') + ' ' + p.category)
|
||||
]),
|
||||
E('div', { 'class': 'dg-provider-desc' }, p.description),
|
||||
E('div', { 'class': 'dg-provider-dns' }, p.primary + (p.secondary ? ' / ' + p.secondary : ''))
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
filterProviders: function(category) {
|
||||
this.activeFilter = category;
|
||||
var container = document.getElementById('dg-providers');
|
||||
var filterBtns = document.querySelectorAll('.dg-filter-btn');
|
||||
filterBtns.forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.textContent.toLowerCase().includes(category) || (category === 'all' && btn.textContent === 'All'));
|
||||
});
|
||||
if (container) {
|
||||
dom.content(container, this.renderProviders(this.providers, category));
|
||||
}
|
||||
},
|
||||
|
||||
selectProvider: function(id) {
|
||||
this.selectedProvider = id;
|
||||
document.querySelectorAll('.dg-provider').forEach(function(el) {
|
||||
el.classList.toggle('selected', el.dataset.id === id);
|
||||
});
|
||||
},
|
||||
|
||||
runSmartConfig: function() {
|
||||
var self = this;
|
||||
var btn = document.getElementById('smart-btn');
|
||||
var results = document.getElementById('smart-results');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Testing...';
|
||||
dom.content(results, []);
|
||||
|
||||
api.smartConfig().then(function(data) {
|
||||
var items = (data.results || []).map(function(r) {
|
||||
var cls = 'dg-smart-result' + (r.provider === data.recommended ? ' best' : '');
|
||||
var text = r.provider + ': ' + (r.reachable ? r.latency_ms + 'ms' : 'unreachable');
|
||||
return E('span', { 'class': cls }, text);
|
||||
});
|
||||
dom.content(results, items);
|
||||
|
||||
if (data.recommended) {
|
||||
self.selectedProvider = data.recommended;
|
||||
self.filterProviders(self.activeFilter);
|
||||
self.toast('Recommended: ' + data.recommended + ' (' + data.best_latency_ms + 'ms)', true);
|
||||
}
|
||||
}).catch(function(e) {
|
||||
self.toast('Smart config failed: ' + e.message, false);
|
||||
}).finally(function() {
|
||||
btn.disabled = false;
|
||||
btn.textContent = 'Run Smart Config';
|
||||
});
|
||||
},
|
||||
|
||||
applyConfig: function() {
|
||||
var self = this;
|
||||
var btn = document.getElementById('apply-btn');
|
||||
btn.disabled = true;
|
||||
|
||||
api.setProvider(this.selectedProvider).then(function(r) {
|
||||
if (r && r.success) {
|
||||
return api.apply();
|
||||
}
|
||||
throw new Error(r && r.error || 'Failed to set provider');
|
||||
}).then(function() {
|
||||
self.toast('DNS configuration applied: ' + self.selectedProvider, true);
|
||||
return api.status();
|
||||
}).then(function(status) {
|
||||
var container = document.getElementById('dg-status');
|
||||
if (container) {
|
||||
dom.content(container, self.renderStatus(status));
|
||||
}
|
||||
}).catch(function(e) {
|
||||
self.toast('Error: ' + e.message, false);
|
||||
}).finally(function() {
|
||||
btn.disabled = false;
|
||||
});
|
||||
},
|
||||
|
||||
toast: function(msg, success) {
|
||||
var t = document.querySelector('.dg-toast');
|
||||
if (t) t.remove();
|
||||
t = document.createElement('div');
|
||||
t.className = 'dg-toast ' + (success ? 'success' : 'error');
|
||||
t.textContent = msg;
|
||||
document.body.appendChild(t);
|
||||
setTimeout(function() { t.remove(); }, 4000);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
261
package/secubox/luci-app-dnsguard/root/usr/libexec/rpcd/luci.dnsguard
Executable file
261
package/secubox/luci-app-dnsguard/root/usr/libexec/rpcd/luci.dnsguard
Executable file
@ -0,0 +1,261 @@
|
||||
#!/bin/sh
|
||||
# SecuBox DNS Guard - Privacy DNS Manager
|
||||
# Provides uncensored DNS feeds and smart configuration
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
# DNS Provider Feed - Uncensored & Privacy-focused
|
||||
# Format: id|name|primary|secondary|dot_host|doh_url|category|country|description
|
||||
DNS_PROVIDERS='
|
||||
fdn|FDN (French Data Network)|80.67.169.12|80.67.169.40|ns0.fdn.fr|https://ns0.fdn.fr/dns-query|privacy|FR|French non-profit, no logs, uncensored
|
||||
fdn_ipv6|FDN IPv6|2001:910:800::12|2001:910:800::40|ns0.fdn.fr|https://ns0.fdn.fr/dns-query|privacy|FR|FDN IPv6 resolvers
|
||||
quad9|Quad9|9.9.9.9|149.112.112.112|dns.quad9.net|https://dns.quad9.net/dns-query|security|CH|Malware blocking, Swiss privacy
|
||||
quad9_unsecured|Quad9 Unsecured|9.9.9.10|149.112.112.10|dns10.quad9.net|https://dns10.quad9.net/dns-query|privacy|CH|No blocking, Swiss privacy
|
||||
cloudflare|Cloudflare|1.1.1.1|1.0.0.1|cloudflare-dns.com|https://cloudflare-dns.com/dns-query|fast|US|Fast, privacy-focused
|
||||
cloudflare_family|Cloudflare Family|1.1.1.3|1.0.0.3|family.cloudflare-dns.com|https://family.cloudflare-dns.com/dns-query|family|US|Malware + adult content blocking
|
||||
mullvad|Mullvad|194.242.2.2|193.19.108.2|dns.mullvad.net|https://dns.mullvad.net/dns-query|privacy|SE|No logs, Swedish privacy
|
||||
mullvad_adblock|Mullvad Adblock|194.242.2.3|193.19.108.3|adblock.dns.mullvad.net|https://adblock.dns.mullvad.net/dns-query|adblock|SE|Ads + trackers blocking
|
||||
adguard|AdGuard|94.140.14.14|94.140.15.15|dns.adguard-dns.com|https://dns.adguard-dns.com/dns-query|adblock|CY|Ad blocking DNS
|
||||
adguard_family|AdGuard Family|94.140.14.15|94.140.15.16|family.adguard-dns.com|https://dns.adguard-dns.com/dns-query|family|CY|Family protection
|
||||
opendns|OpenDNS|208.67.222.222|208.67.220.220|doh.opendns.com|https://doh.opendns.com/dns-query|security|US|Cisco security DNS
|
||||
opendns_family|OpenDNS FamilyShield|208.67.222.123|208.67.220.123|doh.familyshield.opendns.com|https://doh.familyshield.opendns.com/dns-query|family|US|Family protection
|
||||
google|Google|8.8.8.8|8.8.4.4|dns.google|https://dns.google/dns-query|fast|US|Fast, global anycast
|
||||
cleanbrowsing|CleanBrowsing Security|185.228.168.9|185.228.169.9|security-filter-dns.cleanbrowsing.org|https://doh.cleanbrowsing.org/doh/security-filter|security|US|Security filtering
|
||||
cleanbrowsing_family|CleanBrowsing Family|185.228.168.168|185.228.169.168|family-filter-dns.cleanbrowsing.org|https://doh.cleanbrowsing.org/doh/family-filter|family|US|Family + adult blocking
|
||||
controld|Control D|76.76.2.0|76.76.10.0|freedns.controld.com|https://freedns.controld.com/p0|privacy|CA|Canadian, no logs
|
||||
'
|
||||
|
||||
method_status() {
|
||||
local current_dns1 current_dns2 provider mode
|
||||
|
||||
# Get current DNS from dnsmasq/network config
|
||||
current_dns1=$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null | awk '{print $1}')
|
||||
current_dns2=$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null | awk '{print $2}')
|
||||
|
||||
# Check if using local resolver (AdGuard Home)
|
||||
if netstat -tlnp 2>/dev/null | grep -q ":53.*AdGuard"; then
|
||||
mode="adguardhome"
|
||||
elif netstat -tlnp 2>/dev/null | grep -q ":53.*dnsmasq"; then
|
||||
mode="dnsmasq"
|
||||
else
|
||||
mode="unknown"
|
||||
fi
|
||||
|
||||
# Try to identify provider
|
||||
provider="custom"
|
||||
echo "$DNS_PROVIDERS" | while IFS='|' read id name primary secondary rest; do
|
||||
[ -z "$id" ] && continue
|
||||
if [ "$current_dns1" = "$primary" ] || [ "$current_dns1" = "$secondary" ]; then
|
||||
provider="$id"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
json_init
|
||||
json_add_string "mode" "$mode"
|
||||
json_add_string "provider" "$provider"
|
||||
json_add_string "primary" "${current_dns1:-auto}"
|
||||
json_add_string "secondary" "${current_dns2:-}"
|
||||
json_add_boolean "dot_enabled" "$(uci -q get dhcp.@dnsmasq[0].dnssec 2>/dev/null | grep -q 1 && echo 1 || echo 0)"
|
||||
json_dump
|
||||
}
|
||||
|
||||
method_get_providers() {
|
||||
json_init
|
||||
json_add_array "providers"
|
||||
|
||||
echo "$DNS_PROVIDERS" | while IFS='|' read id name primary secondary dot_host doh_url category country description; do
|
||||
[ -z "$id" ] && continue
|
||||
json_add_object
|
||||
json_add_string "id" "$id"
|
||||
json_add_string "name" "$name"
|
||||
json_add_string "primary" "$primary"
|
||||
json_add_string "secondary" "$secondary"
|
||||
json_add_string "dot_host" "$dot_host"
|
||||
json_add_string "doh_url" "$doh_url"
|
||||
json_add_string "category" "$category"
|
||||
json_add_string "country" "$country"
|
||||
json_add_string "description" "$description"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_dump
|
||||
}
|
||||
|
||||
method_get_config() {
|
||||
json_init
|
||||
|
||||
# dnsmasq config
|
||||
json_add_object "dnsmasq"
|
||||
json_add_string "server1" "$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null | awk '{print $1}')"
|
||||
json_add_string "server2" "$(uci -q get dhcp.@dnsmasq[0].server 2>/dev/null | awk '{print $2}')"
|
||||
json_add_boolean "noresolv" "$(uci -q get dhcp.@dnsmasq[0].noresolv 2>/dev/null | grep -q 1 && echo 1 || echo 0)"
|
||||
json_close_object
|
||||
|
||||
# AdGuard Home config (if present)
|
||||
if [ -f /var/lib/adguardhome/AdGuardHome.yaml ]; then
|
||||
json_add_object "adguardhome"
|
||||
json_add_boolean "installed" 1
|
||||
json_add_boolean "running" "$(pgrep -f AdGuardHome >/dev/null && echo 1 || echo 0)"
|
||||
json_close_object
|
||||
fi
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
method_set_provider() {
|
||||
local provider="$1"
|
||||
local primary secondary
|
||||
|
||||
# Find provider in feed
|
||||
echo "$DNS_PROVIDERS" | while IFS='|' read id name p1 p2 rest; do
|
||||
[ -z "$id" ] && continue
|
||||
if [ "$id" = "$provider" ]; then
|
||||
primary="$p1"
|
||||
secondary="$p2"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$primary" ]; then
|
||||
json_init
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "error" "Unknown provider: $provider"
|
||||
json_dump
|
||||
return
|
||||
fi
|
||||
|
||||
# Configure dnsmasq
|
||||
uci -q delete dhcp.@dnsmasq[0].server
|
||||
uci add_list dhcp.@dnsmasq[0].server="$primary"
|
||||
[ -n "$secondary" ] && uci add_list dhcp.@dnsmasq[0].server="$secondary"
|
||||
uci set dhcp.@dnsmasq[0].noresolv='1'
|
||||
uci commit dhcp
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "provider" "$provider"
|
||||
json_add_string "primary" "$primary"
|
||||
json_add_string "secondary" "$secondary"
|
||||
json_dump
|
||||
}
|
||||
|
||||
method_smart_config() {
|
||||
local best_provider best_latency=9999
|
||||
local test_providers="fdn quad9 cloudflare mullvad"
|
||||
|
||||
json_init
|
||||
json_add_array "results"
|
||||
|
||||
for provider in $test_providers; do
|
||||
local primary
|
||||
primary=$(echo "$DNS_PROVIDERS" | grep "^${provider}|" | cut -d'|' -f3)
|
||||
[ -z "$primary" ] && continue
|
||||
|
||||
# Test latency
|
||||
local start end latency
|
||||
start=$(date +%s%N 2>/dev/null || date +%s)
|
||||
if nslookup -timeout=2 example.com "$primary" >/dev/null 2>&1; then
|
||||
end=$(date +%s%N 2>/dev/null || date +%s)
|
||||
latency=$(( (end - start) / 1000000 ))
|
||||
[ $latency -lt 0 ] && latency=1
|
||||
|
||||
json_add_object
|
||||
json_add_string "provider" "$provider"
|
||||
json_add_string "server" "$primary"
|
||||
json_add_int "latency_ms" "$latency"
|
||||
json_add_boolean "reachable" 1
|
||||
json_close_object
|
||||
|
||||
if [ "$latency" -lt "$best_latency" ]; then
|
||||
best_latency="$latency"
|
||||
best_provider="$provider"
|
||||
fi
|
||||
else
|
||||
json_add_object
|
||||
json_add_string "provider" "$provider"
|
||||
json_add_string "server" "$primary"
|
||||
json_add_int "latency_ms" 0
|
||||
json_add_boolean "reachable" 0
|
||||
json_close_object
|
||||
fi
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_add_string "recommended" "${best_provider:-fdn}"
|
||||
json_add_int "best_latency_ms" "$best_latency"
|
||||
json_dump
|
||||
}
|
||||
|
||||
method_test_dns() {
|
||||
local server="$1"
|
||||
local domain="${2:-example.com}"
|
||||
|
||||
json_init
|
||||
|
||||
local start end latency
|
||||
start=$(date +%s%N 2>/dev/null || date +%s)
|
||||
if result=$(nslookup -timeout=3 "$domain" "$server" 2>&1); then
|
||||
end=$(date +%s%N 2>/dev/null || date +%s)
|
||||
latency=$(( (end - start) / 1000000 ))
|
||||
[ $latency -lt 0 ] && latency=1
|
||||
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "server" "$server"
|
||||
json_add_string "domain" "$domain"
|
||||
json_add_int "latency_ms" "$latency"
|
||||
else
|
||||
json_add_boolean "success" 0
|
||||
json_add_string "server" "$server"
|
||||
json_add_string "error" "DNS query failed"
|
||||
fi
|
||||
|
||||
json_dump
|
||||
}
|
||||
|
||||
method_apply() {
|
||||
/etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||||
|
||||
json_init
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "DNS configuration applied"
|
||||
json_dump
|
||||
}
|
||||
|
||||
# RPC interface
|
||||
case "$1" in
|
||||
list)
|
||||
cat <<'EOF'
|
||||
{
|
||||
"status": {},
|
||||
"get_providers": {},
|
||||
"get_config": {},
|
||||
"set_provider": { "provider": "string" },
|
||||
"smart_config": {},
|
||||
"test_dns": { "server": "string", "domain": "string" },
|
||||
"apply": {}
|
||||
}
|
||||
EOF
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
status) method_status ;;
|
||||
get_providers) method_get_providers ;;
|
||||
get_config) method_get_config ;;
|
||||
set_provider)
|
||||
read -r input
|
||||
provider=$(echo "$input" | jsonfilter -e '@.provider')
|
||||
method_set_provider "$provider"
|
||||
;;
|
||||
smart_config) method_smart_config ;;
|
||||
test_dns)
|
||||
read -r input
|
||||
server=$(echo "$input" | jsonfilter -e '@.server')
|
||||
domain=$(echo "$input" | jsonfilter -e '@.domain')
|
||||
method_test_dns "$server" "$domain"
|
||||
;;
|
||||
apply) method_apply ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/secubox/security/dnsguard": {
|
||||
"title": "DNS Guard",
|
||||
"order": 35,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "dnsguard/dashboard"
|
||||
},
|
||||
"depends": {
|
||||
"acl": ["luci-app-dnsguard"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"luci-app-dnsguard": {
|
||||
"description": "Grant access to SecuBox DNS Guard",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.dnsguard": ["status", "get_providers", "get_config", "test_dns"]
|
||||
},
|
||||
"uci": ["dhcp", "network"]
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.dnsguard": ["set_provider", "smart_config", "apply"]
|
||||
},
|
||||
"uci": ["dhcp", "network"]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user