diff --git a/dae/Makefile b/dae/Makefile index 4e081d17..6e67f38c 100644 --- a/dae/Makefile +++ b/dae/Makefile @@ -10,7 +10,7 @@ PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_PROTO:=git -PKG_SOURCE_VERSION:=0f23952baf20f8581d9cadd584157e05a1775ca4 +PKG_SOURCE_VERSION:=93a9957e08c14a40e742581e6342450122916294 PKG_SOURCE_URL:=https://github.com/olicesx/dae.git PKG_MIRROR_HASH:=skip diff --git a/doc/lucky1.png b/doc/lucky1.png deleted file mode 100644 index a5b41b6e..00000000 Binary files a/doc/lucky1.png and /dev/null differ diff --git a/doc/lucky2.png b/doc/lucky2.png deleted file mode 100644 index ba4c026f..00000000 Binary files a/doc/lucky2.png and /dev/null differ diff --git a/doc/lucky3.png b/doc/lucky3.png deleted file mode 100644 index 6fa7982f..00000000 Binary files a/doc/lucky3.png and /dev/null differ diff --git a/doc/taskplan1.png b/doc/taskplan1.png new file mode 100644 index 00000000..1f8db7be Binary files /dev/null and b/doc/taskplan1.png differ diff --git a/doc/taskplan2.png b/doc/taskplan2.png new file mode 100644 index 00000000..edb45d6d Binary files /dev/null and b/doc/taskplan2.png differ diff --git a/doc/taskplan3.png b/doc/taskplan3.png new file mode 100644 index 00000000..d4794702 Binary files /dev/null and b/doc/taskplan3.png differ diff --git a/doc/view.png b/doc/view.png new file mode 100644 index 00000000..169e112f Binary files /dev/null and b/doc/view.png differ diff --git a/doc/view2.png b/doc/view2.png new file mode 100644 index 00000000..9058c632 Binary files /dev/null and b/doc/view2.png differ diff --git a/luci-app-mosdns/Makefile b/luci-app-mosdns/Makefile index 462af92e..e8ee0f9e 100644 --- a/luci-app-mosdns/Makefile +++ b/luci-app-mosdns/Makefile @@ -1,7 +1,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-mosdns -PKG_VERSION:=1.7.11 +PKG_VERSION:=1.7.12 PKG_RELEASE:=1 LUCI_TITLE:=LuCI Support for mosdns diff --git a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js index 9b398094..c08bbbda 100644 --- a/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js +++ b/luci-app-mosdns/htdocs/luci-static/resources/view/mosdns/statistics.js @@ -48,6 +48,10 @@ let nodeStats; let nodeTop; let nodeLogs; let autoStatusBadge; +let statsElements = null; +let currentBadgeState = null; +let lastTopJson = ''; +let lastLogsJson = ''; const cleanIP = ip => { if (!ip) return '-'; @@ -66,9 +70,10 @@ const injectStyles = () => { '.mosdns-stat-card .title-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; font-size: 0.85rem; opacity: 0.75; font-weight: 500; }', '.mosdns-stat-card .metric-val { font-size: 1.85rem; font-weight: 700; line-height: 1.1; letter-spacing: -0.02em; }', '.mosdns-stat-card .subtext { font-size: 0.8rem; opacity: 0.6; margin-top: 0.4rem; }', - '.mosdns-sparkline-wrap { margin-top: 0.5rem; height: 44px; position: relative; overflow: visible; display: flex; align-items: flex-end; }', + '.mosdns-sparkline-wrap { margin-top: 0.5rem; height: 44px; position: relative; overflow: visible; display: flex; align-items: flex-end; touch-action: none; -webkit-user-select: none; user-select: none; }', '.mosdns-sparkline { width: 100%; height: 100%; display: block; overflow: visible; }', '.mosdns-sparkline-tooltip { position: absolute; pointer-events: none; z-index: 20; padding: 0.25rem 0.5rem; border-radius: 5px; background: var(--cbi-section-bg, #fff); border: 1px solid rgba(0,0,0,0.12); box-shadow: 0 3px 10px rgba(0,0,0,0.12); line-height: 1.25; text-align: center; white-space: nowrap; transition: opacity 0.15s ease; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }', + '.spark-hover-hitbox { touch-action: none; -webkit-user-select: none; user-select: none; cursor: crosshair; }', '.mosdns-rank-panel { background: var(--cbi-section-bg, #fff); border: 1px solid rgba(0,0,0,0.08); border-radius: 8px; padding: 1rem 1.1rem; box-shadow: 0 2px 6px rgba(0,0,0,0.03); }', '.mosdns-rank-panel h4 { margin: 0 0 0.85rem 0; font-size: 0.95rem; font-weight: 600; display: flex; align-items: center; justify-content: space-between; }', '.mosdns-rank-item { position: relative; overflow: hidden; border-radius: 6px; padding: 0.35rem 0.65rem; display: flex; justify-content: space-between; align-items: center; background: rgba(125,125,125,0.03); border: 1px solid rgba(125,125,125,0.08); margin-bottom: 0.35rem; }', @@ -143,7 +148,15 @@ const getLatencyClass = elapsedMs => { const updateLiveStatusBadge = () => { if (!autoStatusBadge) return; - if (pageIdx === 0 && !searchVal && !isUserPaused) { + const isLive = (pageIdx === 0 && !searchVal && !isUserPaused); + const targetState = isLive ? 'live' : ('paused:' + pageIdx); + + if (currentBadgeState === targetState) + return; + + currentBadgeState = targetState; + + if (isLive) { dom.content(autoStatusBadge, [ E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live Auto-refresh')) ]); @@ -154,47 +167,17 @@ const updateLiveStatusBadge = () => { } }; -const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => { +const createSparklineSVG = (strokeColor, fillGradId) => { const width = 300; const height = 44; const padTop = 4; const padBottom = 2; const drawHeight = height - padTop - padBottom; - let items = (dataItems && dataItems.length > 0) ? dataItems : []; - if (!items.length) { - items = new Array(24).fill(0).map(() => ({ time: '', val: 0 })); - } - if (items.length < 2) { - items = [items[0] || { time: '', val: 0 }, items[0] || { time: '', val: 0 }]; - } - - const vals = items.map(i => i.val || 0); - const maxVal = maxScale || Math.max(...vals, 1); - const len = items.length; - - const coords = items.map((item, idx) => { - const x = (idx / (len - 1)) * width; - const y = height - padBottom - ((item.val || 0) / maxVal) * drawHeight; - return { x, y, time: item.time, val: item.val || 0 }; - }); - - let pathD = 'M ' + coords[0].x.toFixed(1) + ',' + coords[0].y.toFixed(1); - for (let i = 0; i < coords.length - 1; i++) { - const p0 = coords[i === 0 ? 0 : i - 1]; - const p1 = coords[i]; - const p2 = coords[i + 1]; - const p3 = coords[i + 2 < coords.length ? i + 2 : i + 1]; - - const cp1x = p1.x + (p2.x - p0.x) / 6; - const cp1y = p1.y + (p2.y - p0.y) / 6; - const cp2x = p2.x - (p3.x - p1.x) / 6; - const cp2y = p2.y - (p3.y - p1.y) / 6; - - pathD += ' C ' + cp1x.toFixed(1) + ',' + cp1y.toFixed(1) + ' ' + cp2x.toFixed(1) + ',' + cp2y.toFixed(1) + ' ' + p2.x.toFixed(1) + ',' + p2.y.toFixed(1); - } - - const areaD = pathD + ' L ' + width + ',' + height + ' L 0,' + height + ' Z'; + let coords = []; + let isHovered = false; + let currentIdx = -1; + let hideTimer = null; const svgContainer = E('div', { class: 'mosdns-sparkline-wrap' }); svgContainer.innerHTML = @@ -205,27 +188,26 @@ const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => { '' + '' + '' + - '' + - '' + + '' + + '' + '' + - '' + + '' + '' + ''; + const areaPath = svgContainer.querySelector('.spark-area-path'); + const linePath = svgContainer.querySelector('.spark-line-path'); const hoverGroup = svgContainer.querySelector('.spark-hover-group'); const hoverLine = svgContainer.querySelector('.spark-hover-line'); const hoverDot = svgContainer.querySelector('.spark-hover-dot'); const hitbox = svgContainer.querySelector('.spark-hover-hitbox'); const tooltip = svgContainer.querySelector('.mosdns-sparkline-tooltip'); - const onMove = e => { - const rect = svgContainer.getBoundingClientRect(); - const mouseX = e.clientX - rect.left; - const ratio = Math.max(0, Math.min(1, mouseX / rect.width)); - const idx = Math.round(ratio * (len - 1)); + const renderTooltipAndHover = idx => { + if (idx < 0 || idx >= coords.length) return; const coord = coords[idx]; if (!coord) return; @@ -251,10 +233,14 @@ const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => { '
' + timeStr + '
'; tooltip.style.display = 'block'; - const tooltipX = (coord.x / width) * rect.width; + const rect = svgContainer.getBoundingClientRect(); + const containerWidth = rect.width || width; + const tooltipX = (coord.x / width) * containerWidth; + const ratio = coord.x / width; + if (ratio > 0.65) { tooltip.style.left = 'auto'; - tooltip.style.right = (rect.width - tooltipX + 8) + 'px'; + tooltip.style.right = (containerWidth - tooltipX + 8) + 'px'; } else { tooltip.style.left = (tooltipX + 8) + 'px'; tooltip.style.right = 'auto'; @@ -262,21 +248,203 @@ const createSparklineSVG = (dataItems, strokeColor, fillGradId, maxScale) => { tooltip.style.top = '-6px'; }; - const onLeave = () => { - hoverGroup.style.display = 'none'; - tooltip.style.display = 'none'; + const update = (dataItems, maxScale) => { + let items = (dataItems && dataItems.length > 0) ? dataItems : []; + if (!items.length) { + items = new Array(24).fill(0).map(() => ({ time: '', val: 0 })); + } + if (items.length < 2) { + items = [items[0] || { time: '', val: 0 }, items[0] || { time: '', val: 0 }]; + } + + const vals = items.map(i => i.val || 0); + const maxVal = maxScale || Math.max(...vals, 1); + const len = items.length; + + coords = items.map((item, idx) => { + const x = (idx / (len - 1)) * width; + const y = height - padBottom - ((item.val || 0) / maxVal) * drawHeight; + return { x, y, time: item.time, val: item.val || 0 }; + }); + + let pathD = 'M ' + coords[0].x.toFixed(1) + ',' + coords[0].y.toFixed(1); + for (let i = 0; i < coords.length - 1; i++) { + const p0 = coords[i === 0 ? 0 : i - 1]; + const p1 = coords[i]; + const p2 = coords[i + 1]; + const p3 = coords[i + 2 < coords.length ? i + 2 : i + 1]; + + const cp1x = p1.x + (p2.x - p0.x) / 6; + const cp1y = p1.y + (p2.y - p0.y) / 6; + const cp2x = p2.x - (p3.x - p1.x) / 6; + const cp2y = p2.y - (p3.y - p1.y) / 6; + + pathD += ' C ' + cp1x.toFixed(1) + ',' + cp1y.toFixed(1) + ' ' + cp2x.toFixed(1) + ',' + cp2y.toFixed(1) + ' ' + p2.x.toFixed(1) + ',' + p2.y.toFixed(1); + } + + const areaD = pathD + ' L ' + width + ',' + height + ' L 0,' + height + ' Z'; + + areaPath.setAttribute('d', areaD); + linePath.setAttribute('d', pathD); + + if (isHovered && currentIdx >= 0) { + renderTooltipAndHover(currentIdx); + } }; - hitbox.addEventListener('mousemove', onMove); - hitbox.addEventListener('mouseleave', onLeave); + const onPositionMove = clientX => { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + const rect = svgContainer.getBoundingClientRect(); + if (!rect.width || !coords.length) return; + const mouseX = clientX - rect.left; + const ratio = Math.max(0, Math.min(1, mouseX / rect.width)); + const idx = Math.round(ratio * (coords.length - 1)); + currentIdx = idx; + isHovered = true; + renderTooltipAndHover(idx); + }; + const onLeave = (delay = 0) => { + if (delay > 0) { + if (hideTimer) clearTimeout(hideTimer); + hideTimer = setTimeout(() => { + isHovered = false; + currentIdx = -1; + hoverGroup.style.display = 'none'; + tooltip.style.display = 'none'; + hideTimer = null; + }, delay); + } else { + if (hideTimer) { + clearTimeout(hideTimer); + hideTimer = null; + } + isHovered = false; + currentIdx = -1; + hoverGroup.style.display = 'none'; + tooltip.style.display = 'none'; + } + }; + + hitbox.addEventListener('mousemove', e => onPositionMove(e.clientX)); + hitbox.addEventListener('mouseenter', e => onPositionMove(e.clientX)); + hitbox.addEventListener('mouseleave', () => onLeave(0)); + + hitbox.addEventListener('touchstart', e => { + if (e.touches && e.touches.length > 0) { + onPositionMove(e.touches[0].clientX); + } + }, { passive: true }); + + hitbox.addEventListener('touchmove', e => { + if (e.touches && e.touches.length > 0) { + onPositionMove(e.touches[0].clientX); + } + }, { passive: true }); + + hitbox.addEventListener('touchend', () => onLeave(1500), { passive: true }); + hitbox.addEventListener('touchcancel', () => onLeave(0), { passive: true }); + + svgContainer.update = update; return svgContainer; }; -const renderOverviewStats = (stats, historyData) => { +const createOverviewStatsDOM = () => { + const metricTotal = E('div', { class: 'metric-val' }, '-'); + const subtextTotal = E('div', { class: 'subtext' }, '-'); + const sparklineTotal = createSparklineSVG('#3b82f6', 'spark-grad-total'); + + const badgeBlocked = E('span', { class: 'mosdns-badge badge-danger' }, '-'); + const metricBlocked = E('div', { class: 'metric-val', style: 'color: #dc2626;' }, '-'); + const sparklineBlocked = createSparklineSVG('#dc2626', 'spark-grad-blocked'); + + const badgeCached = E('span', { class: 'mosdns-badge badge-teal' }, '-'); + const metricCached = E('div', { class: 'metric-val', style: 'color: #059669;' }, '-'); + const sparklineCached = createSparklineSVG('#059669', 'spark-grad-cached'); + + const metricLatency = E('div', { class: 'metric-val', style: 'color: #2563eb;' }, '-'); + const subtextLatency = E('div', { class: 'subtext' }, _('Per-query speed')); + + const grid = E('div', { class: 'mosdns-grid' }, [ + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('DNS Queries Total')), + E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live')) + ]), + metricTotal, + subtextTotal + ]), + sparklineTotal + ]), + + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('Blocked by Filters')), + badgeBlocked + ]), + metricBlocked + ]), + sparklineBlocked + ]), + + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('Cached Queries')), + badgeCached + ]), + metricCached + ]), + sparklineCached + ]), + + E('div', { class: 'mosdns-stat-card' }, [ + E('div', {}, [ + E('div', { class: 'title-row' }, [ + E('span', {}, _('Average Processing Time')), + E('span', { class: 'mosdns-badge badge-primary' }, _('Latency')) + ]), + metricLatency, + subtextLatency + ]) + ]) + ]); + + statsElements = { + grid, + metricTotal, + subtextTotal, + sparklineTotal, + badgeBlocked, + metricBlocked, + sparklineBlocked, + badgeCached, + metricCached, + sparklineCached, + metricLatency, + subtextLatency + }; + + return grid; +}; + +const updateOverviewStats = (stats, historyData) => { if (!stats || stats.error) { - return E('div', { class: 'alert-message warning' }, - _('MosDNS API is unreachable. Please ensure MosDNS is running and stats_api plugin is enabled.')); + if (nodeStats) { + dom.content(nodeStats, E('div', { class: 'alert-message warning' }, + _('MosDNS API is unreachable. Please ensure MosDNS is running and stats_api plugin is enabled.'))); + statsElements = null; + } + return; + } + + if (!statsElements || !nodeStats.contains(statsElements.grid)) { + dom.content(nodeStats, createOverviewStatsDOM()); } const { @@ -295,52 +463,19 @@ const renderOverviewStats = (stats, historyData) => { const baseMax = Math.max(...totalItems.map(i => i.val), 1); - return E('div', { class: 'mosdns-grid' }, [ - E('div', { class: 'mosdns-stat-card' }, [ - E('div', {}, [ - E('div', { class: 'title-row' }, [ - E('span', {}, _('DNS Queries Total')), - E('span', { class: 'mosdns-badge badge-teal badge-pulse' }, _('● Live')) - ]), - E('div', { class: 'metric-val' }, total.toLocaleString()), - E('div', { class: 'subtext' }, _('Avg Processing') + ': ' + avg_ms + ' ms') - ]), - createSparklineSVG(totalItems, '#3b82f6', 'spark-grad-total', baseMax) - ]), + statsElements.metricTotal.textContent = total.toLocaleString(); + statsElements.subtextTotal.textContent = _('Avg Processing') + ': ' + avg_ms + ' ms'; + statsElements.sparklineTotal.update(totalItems, baseMax); - E('div', { class: 'mosdns-stat-card' }, [ - E('div', {}, [ - E('div', { class: 'title-row' }, [ - E('span', {}, _('Blocked by Filters')), - E('span', { class: 'mosdns-badge badge-danger' }, blocked_pct + '%') - ]), - E('div', { class: 'metric-val', style: 'color: #dc2626;' }, blocked.toLocaleString()) - ]), - createSparklineSVG(blockedItems, '#dc2626', 'spark-grad-blocked', baseMax) - ]), + statsElements.badgeBlocked.textContent = blocked_pct + '%'; + statsElements.metricBlocked.textContent = blocked.toLocaleString(); + statsElements.sparklineBlocked.update(blockedItems, baseMax); - E('div', { class: 'mosdns-stat-card' }, [ - E('div', {}, [ - E('div', { class: 'title-row' }, [ - E('span', {}, _('Cached Queries')), - E('span', { class: 'mosdns-badge badge-teal' }, cached_pct + '%') - ]), - E('div', { class: 'metric-val', style: 'color: #059669;' }, cached.toLocaleString()) - ]), - createSparklineSVG(cachedItems, '#059669', 'spark-grad-cached', baseMax) - ]), + statsElements.badgeCached.textContent = cached_pct + '%'; + statsElements.metricCached.textContent = cached.toLocaleString(); + statsElements.sparklineCached.update(cachedItems, baseMax); - E('div', { class: 'mosdns-stat-card' }, [ - E('div', {}, [ - E('div', { class: 'title-row' }, [ - E('span', {}, _('Average Processing Time')), - E('span', { class: 'mosdns-badge badge-primary' }, _('Latency')) - ]), - E('div', { class: 'metric-val', style: 'color: #2563eb;' }, avg_ms + ' ms'), - E('div', { class: 'subtext' }, _('Per-query speed')) - ]) - ]) - ]); + statsElements.metricLatency.textContent = avg_ms + ' ms'; }; const renderTopRankings = topData => { @@ -398,6 +533,13 @@ const renderTopRankings = topData => { ]); }; +const updateTopRankings = topData => { + const json = JSON.stringify(topData || {}); + if (json === lastTopJson) return; + lastTopJson = json; + dom.content(nodeTop, renderTopRankings(topData)); +}; + const showLogDetailsModal = item => { let statusBadge; if (item.is_blocked) { @@ -579,11 +721,15 @@ const pollScheduler = async () => { const results = await Promise.all(promises); - dom.content(nodeStats, renderOverviewStats(results[0], results[2])); - dom.content(nodeTop, renderTopRankings(results[1])); + updateOverviewStats(results[0], results[2]); + updateTopRankings(results[1]); if (shouldRefreshLogs && results[3]) { - dom.content(nodeLogs, renderLogsTable(results[3])); + const json = JSON.stringify(results[3]); + if (json !== lastLogsJson) { + lastLogsJson = json; + dom.content(nodeLogs, renderLogsTable(results[3])); + } } updateLiveStatusBadge(); } catch (e) { @@ -593,6 +739,7 @@ const pollScheduler = async () => { const refreshLogs = async () => { try { const logs = await callGetLogs(PAGE_SIZE, pageIdx * PAGE_SIZE, searchVal, filterVal); + lastLogsJson = JSON.stringify(logs || {}); dom.content(nodeLogs, renderLogsTable(logs)); updateLiveStatusBadge(); } catch (e) { @@ -613,13 +760,19 @@ return view.extend({ render(data) { injectStyles(); + statsElements = null; + currentBadgeState = null; + lastTopJson = ''; + lastLogsJson = ''; + nodeStats = E('div', { id: 'overview-stats' }); nodeTop = E('div', { id: 'top-rankings' }); nodeLogs = E('div', { id: 'logs-table' }); autoStatusBadge = E('div', { style: 'display: inline-block;' }); - dom.content(nodeStats, renderOverviewStats(data[0], data[3])); - dom.content(nodeTop, renderTopRankings(data[1])); + updateOverviewStats(data[0], data[3]); + updateTopRankings(data[1]); + lastLogsJson = JSON.stringify(data[2] || {}); dom.content(nodeLogs, renderLogsTable(data[2])); updateLiveStatusBadge(); diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index ad55a1da..9cecc8b4 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -1810,18 +1810,7 @@ function gen_config(var) end end - table.insert(route.rules, { - action = "route", - ip_is_private = true, - outbound = "direct" - }) - if COMMON.default_outbound_tag then - table.insert(route.rules, { - action = "route", - port_range = { "0:65535" }, - outbound = COMMON.default_outbound_tag - }) route.final = COMMON.default_outbound_tag end diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index 8162ade4..0bc35e4f 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -1540,11 +1540,6 @@ function gen_config(var) }) end - table.insert(rules, { - outboundTag = "direct", - ip = { "geoip:private" } - }) - if default_outboundTag then local rule = { _flag = "default", diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index 7edf3221..d1a32027 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -1797,11 +1797,6 @@ function gen_config(var) end if COMMON.default_outbound_tag then - table.insert(route.rules, { - action = "route", - port_range = { "0:65535" }, - outbound = COMMON.default_outbound_tag - }) route.final = COMMON.default_outbound_tag end diff --git a/sing-box/Makefile b/sing-box/Makefile index ecae8b44..0da7e3d7 100644 --- a/sing-box/Makefile +++ b/sing-box/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=sing-box -PKG_VERSION:=1.13.21 +PKG_VERSION:=1.14.0 PKG_RELEASE:=1 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)? -PKG_HASH:=c994018d7c2a8fc9fcf6080e4b9cc4f2f915e755342238b77e46fc0806fe96db +PKG_HASH:=87baf6852e37941cbe40bdd94bec81c957c88a56751cecd6bbf0e6108bc69398 PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE_FILES:=LICENSE