diff --git a/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js b/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js index 31e178cc..551e54b0 100644 --- a/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js +++ b/package/secubox/luci-app-ollama/htdocs/luci-static/resources/view/ollama/dashboard.js @@ -67,6 +67,14 @@ return view.extend({ .ol-model-name { font-weight: 500; } .ol-model-size { font-size: 0.75rem; color: var(--ol-muted); } .ol-empty { text-align: center; padding: 2rem; color: var(--ol-muted); } + .ol-suggest { margin-top: 1rem; } + .ol-suggest-title { font-size: 0.85rem; margin-bottom: 0.75rem; color: var(--ol-text); } + .ol-suggest-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 0.5rem; } + .ol-suggest-item { background: var(--ol-bg); border: 1px solid var(--ol-border); border-radius: 0.375rem; padding: 0.75rem; cursor: pointer; transition: all 0.2s; } + .ol-suggest-item:hover { border-color: var(--ol-accent); } + .ol-suggest-name { font-weight: 600; font-size: 0.9rem; } + .ol-suggest-desc { font-size: 0.75rem; color: var(--ol-muted); margin-top: 0.25rem; } + .ol-suggest-size { font-size: 0.7rem; color: var(--ol-accent); margin-top: 0.25rem; } .ol-chat-box { height: 200px; overflow-y: auto; background: var(--ol-bg); border: 1px solid var(--ol-border); border-radius: 0.375rem; padding: 0.75rem; margin-bottom: 0.75rem; font-size: 0.875rem; } .ol-chat-msg { margin-bottom: 0.75rem; } .ol-chat-msg.user { color: var(--ol-accent); } @@ -200,10 +208,36 @@ return view.extend({ ]; }, + suggestedModels: [ + { name: 'tinyllama', desc: 'Tiny but capable, fast inference', size: '637 MB' }, + { name: 'llama3.2:1b', desc: 'Meta Llama 3.2 1B - lightweight', size: '1.3 GB' }, + { name: 'llama3.2:3b', desc: 'Meta Llama 3.2 3B - balanced', size: '2.0 GB' }, + { name: 'phi3:mini', desc: 'Microsoft Phi-3 Mini - efficient', size: '2.2 GB' }, + { name: 'gemma2:2b', desc: 'Google Gemma 2 2B - compact', size: '1.6 GB' }, + { name: 'qwen2.5:1.5b', desc: 'Alibaba Qwen 2.5 - multilingual', size: '986 MB' }, + { name: 'mistral', desc: 'Mistral 7B - high quality', size: '4.1 GB' }, + { name: 'codellama:7b', desc: 'Meta CodeLlama - coding tasks', size: '3.8 GB' } + ], + renderModels: function(models) { var self = this; if (!models || models.length === 0) { - return E('div', { 'class': 'ol-empty' }, 'No models installed'); + return E('div', {}, [ + E('div', { 'class': 'ol-empty' }, 'No models installed'), + E('div', { 'class': 'ol-suggest' }, [ + E('div', { 'class': 'ol-suggest-title' }, '\uD83D\uDCE5 Click to download a model:'), + E('div', { 'class': 'ol-suggest-grid' }, this.suggestedModels.map(function(m) { + return E('div', { + 'class': 'ol-suggest-item', + 'click': function() { self.pullModel(m.name); } + }, [ + E('div', { 'class': 'ol-suggest-name' }, m.name), + E('div', { 'class': 'ol-suggest-desc' }, m.desc), + E('div', { 'class': 'ol-suggest-size' }, m.size) + ]); + })) + ]) + ]); } return E('div', {}, models.map(function(m) { return E('div', { 'class': 'ol-model' }, [ @@ -268,17 +302,17 @@ return view.extend({ }).catch(function(e) { self.toast('Error: ' + e.message, false); }); }, - pullModel: function() { + pullModel: function(modelName) { var self = this; var input = document.getElementById('pull-model'); - var name = input ? input.value.trim() : ''; + var name = modelName || (input ? input.value.trim() : ''); if (!name) { self.toast('Enter model name', false); return; } - self.toast('Pulling ' + name + '...', true); + self.toast('Pulling ' + name + '... (this may take a while)', true); api.pull(name).then(function(r) { self.toast(r && r.success ? 'Pulled ' + name : 'Failed: ' + ((r && r.error) || 'Unknown'), r && r.success); if (r && r.success) { - input.value = ''; + if (input) input.value = ''; self.refresh(); } }).catch(function(e) { self.toast('Error: ' + e.message, false); }); diff --git a/package/secubox/luci-secubox-dnsguard/Makefile b/package/secubox/luci-secubox-dnsguard/Makefile new file mode 100644 index 00000000..a7ee71ff --- /dev/null +++ b/package/secubox/luci-secubox-dnsguard/Makefile @@ -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-secubox-dnsguard +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 +PKG_MAINTAINER:=CyberMind + +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-secubox-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-secubox-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))) diff --git a/package/secubox/luci-secubox-dnsguard/htdocs/luci-static/resources/view/dnsguard/dashboard.js b/package/secubox/luci-secubox-dnsguard/htdocs/luci-static/resources/view/dnsguard/dashboard.js new file mode 100644 index 00000000..5096dd34 --- /dev/null +++ b/package/secubox/luci-secubox-dnsguard/htdocs/luci-static/resources/view/dnsguard/dashboard.js @@ -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 +}); diff --git a/package/secubox/luci-secubox-dnsguard/root/usr/libexec/rpcd/luci.dnsguard b/package/secubox/luci-secubox-dnsguard/root/usr/libexec/rpcd/luci.dnsguard new file mode 100755 index 00000000..24dc2287 --- /dev/null +++ b/package/secubox/luci-secubox-dnsguard/root/usr/libexec/rpcd/luci.dnsguard @@ -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 diff --git a/package/secubox/luci-secubox-dnsguard/root/usr/share/luci/menu.d/luci-secubox-dnsguard.json b/package/secubox/luci-secubox-dnsguard/root/usr/share/luci/menu.d/luci-secubox-dnsguard.json new file mode 100644 index 00000000..ed700943 --- /dev/null +++ b/package/secubox/luci-secubox-dnsguard/root/usr/share/luci/menu.d/luci-secubox-dnsguard.json @@ -0,0 +1,13 @@ +{ + "admin/secubox/security/dnsguard": { + "title": "DNS Guard", + "order": 35, + "action": { + "type": "view", + "path": "dnsguard/dashboard" + }, + "depends": { + "acl": ["luci-secubox-dnsguard"] + } + } +} diff --git a/package/secubox/luci-secubox-dnsguard/root/usr/share/rpcd/acl.d/luci-secubox-dnsguard.json b/package/secubox/luci-secubox-dnsguard/root/usr/share/rpcd/acl.d/luci-secubox-dnsguard.json new file mode 100644 index 00000000..39a40948 --- /dev/null +++ b/package/secubox/luci-secubox-dnsguard/root/usr/share/rpcd/acl.d/luci-secubox-dnsguard.json @@ -0,0 +1,17 @@ +{ + "luci-secubox-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"] + } + } +}