⛄ Sync 2026-08-16 09:02:12

This commit is contained in:
github-actions[bot] committed 2026-08-16 09:02:12 +08:00
1 parent 570286daf3
commit 4a9528feed
23 files changed
+214 -76

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.8.14
PKG_RELEASE:=84
PKG_RELEASE:=85
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -11,6 +11,10 @@ if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
@@ -29,6 +33,27 @@ local header_type_list = {
o = s:option(Flag, "custom", translate("Use Custom Config"))
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("vmess", "Vmess")
o:value("vless", "VLESS")
@@ -41,6 +66,12 @@ o:value("wireguard", "WireGuard")
o:value("dokodemo-door", "dokodemo-door")
o:depends({ custom = false })
o = s:option(DummyValue, "is_endpoint", "")
o.not_rewrite = true
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends({ custom = false, protocol = "wireguard" })
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
@@ -385,7 +416,7 @@ end
--[[acceptProxyProtocol]]
o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used."))
o.default = "0"
o:depends({ custom = false })
o:depends({ custom = false, is_endpoint = "" })
--[[Fast Open]]
o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"))
@@ -444,8 +475,7 @@ o.datatype = "base64"
o:depends({ protocol = "wireguard" })
o = s:option(DummyValue, "gen_wireguard_key")
o.prefix = option_prefix
o.template = m:template_path("/server/server_wireguard")
o.template = m:template_path("/server/gen_wireguard_key")
o:depends({ protocol = "wireguard" })
o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
@@ -505,27 +535,6 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
@@ -13,6 +13,10 @@ if not s1.fields["type"].default then
s1.fields["type"].default = type_name
end
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
@@ -30,6 +34,27 @@ local ss_method_list = {
o = s:option(Flag, "custom", translate("Use Custom Config"))
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(ListValue, "protocol", translate("Protocol"))
o:value("mixed", "Mixed")
o:value("socks", "Socks")
@@ -55,6 +80,12 @@ end
o:value("direct", "Direct")
o:depends({ custom = false })
o = s:option(DummyValue, "is_endpoint", "")
o.not_rewrite = true
o.template = m:template_path("/cbi/hidevalue")
o.value = "1"
o:depends({ custom = false, protocol = "wireguard" })
o = s:option(Value, "port", translate("Listen Port"))
o.datatype = "port"
o:depends({ custom = false })
@@ -420,14 +451,13 @@ if singbox_tags:find("with_wireguard") then
o:depends({ protocol = "wireguard" })
o = s:option(DummyValue, "gen_wireguard_key")
o.prefix = option_prefix
o.template = m:template_path("/server/server_wireguard")
o.template = m:template_path("/server/gen_wireguard_key")
o:depends({ protocol = "wireguard" })
end
o = s:option(Flag, "bind_local", translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
o:depends({ custom = false })
o:depends({ custom = false, is_endpoint = "" })
o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!"))
o.default = "0"
@@ -482,27 +512,6 @@ for _, d in ipairs(netdev_list) do
o:value(d.name, d.label)
end
o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)")
o.rows = 10
o.wrap = "off"
o:depends({ custom = true })
o.datatype = "json"
local o_validate = o.validate
o.validate = function(self, value)
local v = o_validate(self, value)
if v then return v end
return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!")
end
o.custom_cfgvalue = function(self, section, value)
local config_str = m:get(section, "config_str")
if config_str then
return api.base64Decode(config_str)
end
end
o.custom_write = function(self, section, value)
m:set(section, "config_str", api.base64Encode(value) or "")
end
o = s:option(Flag, "log", translate("Log"))
o.default = "1"
o.rmempty = false
@@ -8,6 +8,10 @@ local type_name = "Hysteria2"
s1.fields["type"]:value(type_name, "Hysteria2")
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
@@ -8,6 +8,10 @@ local type_name = "SS-Rust"
s1.fields["type"]:value(type_name, translate("Shadowsocks Rust"))
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
@@ -8,6 +8,10 @@ local type_name = "SSR"
s1.fields["type"]:value(type_name, translate("ShadowsocksR"))
if not s1.val["type"] then
s1.val["type"] = type_name
end
if s1.val["type"] and s1.val["type"] ~= type_name then
return
end
@@ -1554,6 +1554,7 @@ function luci_types(s, s2)
for i, v in ipairs(s2.children) do
local o = s2.children[i]
o.config_option = o.option
o.option_prefix = option_prefix
o.option = option_prefix .. o.option
if not o.not_rewrite then
o.cfgvalue = function(self, section)
@@ -1,7 +1,7 @@
<%
local map = self.map
local api = map.api
local prefix = self.prefix
local prefix = self.option_prefix or ""
-%>
<div class="cbi-value" id="cbi-<%=self.config.."-"..section.."-"..self.option%>" data-index="<%=self.index%>" data-depends="<%=pcdata(self:deplist2json(section))%>">
<label class="cbi-value-title"><%:Generate key%></label>
@@ -535,6 +535,7 @@ local function parseClashNode(node, add_mode, group, sub_cfg)
result = set_ss_implementation(sub_ss_type, result)
if not result then return nil end
result.method = node.cipher
result.ss_method = node.cipher
result.password = node.password
if node.plugin == "obfs" then
result.plugin = "obfs-local"
@@ -1114,6 +1115,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
(_method == "xchacha20-poly1305" and "xchacha20-ietf-poly1305") or _method
result.method = method
result.ss_method = method
result.password = password
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
@@ -1482,6 +1484,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.port = content.port
result.password = content.password
result.method = content.encryption
result.ss_method = content.encryption
result.plugin = content.plugin
result.plugin_opts = content.plugin_options
result.group = content.airport
@@ -1770,6 +1773,13 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
elseif has_hysteria2 then
result.type = "Hysteria2"
for k1, v1 in pairs(result) do
if k1:find("hysteria2_") == 1 then
local s = split(k1, "hysteria2_")
result[s[2]] = v1
result[k1] = nil
end
end
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "Hysteria2", "Hysteria2"))
return nil
+4 -4
View File
@@ -17,12 +17,12 @@ LUCI_NAME:=luci-theme-footstrap
FOOTSTRAP_VERSION?=
ifneq ($(FOOTSTRAP_VERSION),)
PKG_VERSION:=$(FOOTSTRAP_VERSION)
PKG_RELEASE:=19
PKG_RELEASE:=20
endif
LUCI_TITLE:=Footstrap Theme
# +luci-base is the WHOLE list, and keeping it that way is the constraint. Nothing this package
# ships fetches anything at run time any more (the self-updater is retired), so there is no second
# ships fetches anything at run time (the self-update script is retired), so there is no second
# name to justify — and the rule that got it here stands: the one convenience tool that ever wanted
# a dep was `curl`, which is NOT in OpenWrt's default set, on an 8 MB device, for one API call. Fall
# back to what the base image has (uclient-fetch, jsonfilter, sha256sum) or do without.
@@ -168,8 +168,8 @@ endef
# what Weblate can see (it translates `po/`, and only `po/`).
#
# It was `i18n/` here for a while, with a po2lmo loop in this hook bundling the catalogue into the
# theme package, because a multi-package release was mis-picked by a self-updater that resolved the
# theme by name and took `head -1` (issue #6). That updater is retired and the release is built by
# theme package, because a multi-package release was mis-picked by a self-update script that resolved
# the theme by name and took `head -1` (issue #6). That script is retired and the release is built by
# owfeed, which packages this theme as exactly one artifact per format regardless of what luci.mk
# would do — so the constraint that bought the rename is gone, and the cost of keeping it (a
# catalogue invisible to the project's own translation platform) is not.
@@ -4,6 +4,7 @@
'require fs-fit as fit';
'require fs-prefs as prefs';
'require fs-menutree as tree';
'require fs-version as version';
/* The chrome AROUND the content: the mode menu, the section tabs, the rail toggle, and the
* measurements that decide how much room any of it gets. The MAIN menu is not here — it is injected
@@ -277,6 +278,40 @@ function publishBarHeight(bar) {
root.style.setProperty('--fs-bar-live', `${h}px`);
}
/* ---- the theme names itself in the footer, beside LuCI and the distribution ----
*
* The footer's sentence is about what renders this page: LuCI's version, the distribution's, and —
* since the markup is the theme's — the theme's. Each of the other two is a link to where that
* software lives, so this one is too.
*
* WRITTEN FROM THE CLIENT, and that is the only reason it is here rather than in footer.ut: the
* version is stamped into fs-version.js by file name — the Makefile, tools/stage.sh and dev-sync.sh
* all sed that one literal — so a second copy in a template would be a second stamp site to keep in
* step, and a footer that quietly disagrees with the Appearance tab is worse than no version at all.
* The label is the one fs-version already prints there, dev builds included.
*
* Once per document: the footer survives a client navigation (navigate() sweeps the children of
* .fs-content, and the footer is its sibling), and the guard makes a second call a no-op rather
* than a second copy. No request, no translated string: a proper noun and a number. */
function nameThemeInFooter() {
const host = document.querySelector('footer.fs-footer span');
if (!host || host.querySelector('.fs-footer-self')) return;
const self = E('a', {
class: 'fs-footer-self',
href: version.REPO_URL,
target: '_blank',
rel: 'noreferrer',
}, version.label());
/* AFTER THE DISTRIBUTION, not at the end. The sentence lists the software that renders the page
* — LuCI, the distribution, the theme — and footer.ut appends one more fragment after them that
* is not a component but a state: "Lua compatibility mode active". Landing behind it read as if
* the theme were part of that notice. Two links means the second is the distribution's; with
* anything else there, appending is still correct. */
const links = host.querySelectorAll('a');
if (links.length >= 2) links[1].after(document.createTextNode(' / '), self);
else host.append(document.createTextNode(' / '), self);
}
/* ---- does the right-hand cluster still fit beside the brand? ----
*
* The same question as the menu's, one row up, and it cannot be asked the same way. The cluster is
@@ -459,6 +494,7 @@ function wireIndicatorCounts() {
return baseclass.extend({
setRenderMain,
renderChrome,
nameThemeInFooter,
wireIndicatorCounts,
/* registered with fs-fit by the theme's init(): the bar's "does the menu fit beside the brand"
* measurement rides the same engine as the data tables' */
@@ -125,10 +125,14 @@ function warnPollUnreadable(msg) {
* itself to L's prototype when the FIRST requirer compiles it, so this sees the instance the pages
* actually use, and a router that required it would both bind it to its own prototypal L (the two-L
* trap, docs/spa-router.md) and pull uci.js onto pages that never touch uci. No instance means no
* cache to flush. */
* cache to flush.
*
* Returns the refill below as a promise the caller must wait on, or null when there is nothing to
* wait for. It never rejects — a failed refill is reported and swallowed, because a navigation is
* not the place to lose a page over a config the incoming view may not even read. */
function flushUciCache() {
const uci = window.L ? window.L.uci : null;
if (!uci || typeof uci.unload !== 'function') return;
if (!uci || typeof uci.unload !== 'function') return null;
/* `state.values` and `loaded` are private, so the same rule as L.Poll.timer applies: a shape we
* do not recognise is a reason to do nothing, once, loudly — never to guess. The two hold
* different halves of the cache (a package whose load is still in flight is in `loaded` alone),
@@ -141,14 +145,60 @@ function flushUciCache() {
+ 'will report a missing config on the second SPA visit. fs-router.js needs updating for '
+ 'this luci-base.');
}
return;
return null;
}
const names = Object.keys(uci.state.values).concat(Object.keys(uci.loaded));
if (names.length) uci.unload(names);
if (!names.length) return null;
uci.unload(names);
/* `state.reorder` is the one half unload() does not clear, and it is deliberately left alone
* rather than reset by hand: with `values` gone, reorderSections() finds no sections to order,
* emits no call and clears the map itself on the next save. Writing to that field would be us
* editing another module's private state, for a difference nothing can observe. */
/* …and put back the three packages luci-base's network.js reads but will never load again.
*
* initNetworkState() loads `network`, `wireless` and `luci` ONCE, fills its own `_state`, and
* from then on answers every caller with `return (_state != null ? Promise.resolve(_state) : _init)`
* — no uci call, ever again, for the life of the document. What it answers WITH, though, is the
* uci cache: `getWifiDevices()` is `uci.sections('wireless', 'wifi-device')`, and a view like
* network/switch reads `uci.sections('network', 'switch')` in its own render(). So dropping those
* packages does not make network.js refetch them — it makes every consumer read an EMPTY config
* until the next full load.
*
* Measured on 24.10, one SPA navigation away from Interfaces: `uci.state.values` {}, and
* `network.getWifiDevices()` 2 devices -> 0. That is Status -> Channel Analysis painting its title
* and its button with no band tabs under them, and Network -> Switch painting its description and
* Save/Apply with no VLAN sections — both correct again after F5, which is exactly how it was
* reported upstream ("only work after reloading the page"). tools/spa-parity.mjs reproduces it in
* one command, and tools/upstream-contract.mjs is what notices if that list of three ever moves.
*
* Refilling them is what a full load hands the incoming view, so navigate() WAITS for it (see the
* Promise.all below): a cached module resolves within a microtask, well before this request lands,
* and the view would read the empty cache we just left it. Only when network.js is really in the
* document — no module, no derived state to keep in step, and no request to spend. The ubus half
* of `_state` stays as stale as upstream leaves it; a view that needs it fresh calls
* network.flushCache() itself, and that is not ours to decide.
*
* WHAT IT COSTS, and why the cheaper shape was not taken. Awaiting this puts one uci `get` in
* front of the render on a navigation whose module is already cached — measured on the stand over
* 12 alternating navigations between two warm views: 136 ms median without the wait, 159 ms with
* it. `uci.load()` also dispatches `uci-loaded`, which luci-base wires to the change indicator, so
* a second, unawaited `uci.changes()` rides along that `unload()` alone never triggered.
*
* The free alternative is to leave these three OUT of the unload rather than drop and refetch
* them, and it was rejected on what it does to the pages people edit: `network.flushCache()` — the
* one call a view makes when it wants fresh data — reloads its ubus half but calls `uci.load()`
* for the uci half, which is a no-op while the package is still cached. Interfaces and Wireless
* would then render fresh device state over config values from whenever the document first
* touched them, which a full load never does. Dropping and refilling keeps both halves as fresh as
* a full load, and keeps pending local edits behaving as they do everywhere else: discarded by the
* navigation, exactly like the poll queue and the view's intervals above. */
if (!window.L.network) return null;
const refill = [ 'network', 'wireless', 'luci' ].filter((p) => names.indexOf(p) !== -1);
if (!refill.length) return null;
return uci.load(refill).catch((e) => {
console.error('footstrap: reloading uci ' + refill.join(', ') + ' after a navigation failed', e);
});
}
let _uciCacheWarned = false;
@@ -676,8 +726,9 @@ function navigate(pathname, push, kbd) {
* would have. L.Poll's own tick survives. */
clearViewIntervals();
/* and drop uci's document-scoped config cache, which a full load would not have carried into the
* incoming page either (see flushUciCache) */
flushUciCache();
* incoming page either (see flushUciCache). What it hands back is the refill of the packages
* network.js will never load again — awaited below, before the view renders. */
const uciWarm = flushUciCache();
/* the outgoing page's links are about to become a detached tree — do not hold one of them */
_lastHovered = null;
/* run every registered navigation callback — today the search palette's recent-pages record and
@@ -819,7 +870,10 @@ function navigate(pathname, push, kbd) {
* and the wait introduces a window in which we may never get there. Marking it up front would
* make the NEXT navigation take the cached branch — `new view.constructor()` on a class whose
* require() is what renders it — i.e. two renders and two pollers for one page. */
warmedThen(className).then(() => {
/* `uciWarm` rides alongside the prefetch wait rather than after it: both are in flight already, and
* a view must not be constructed until the uci cache it reads is back (see flushUciCache). It never
* rejects — the refill catches its own failure — so it cannot cost the navigation a full reload. */
Promise.all([ warmedThen(className), uciWarm ]).then(() => {
/* superseded while waiting: never start the require. On a first visit the require IS the
* render, so starting it here would paint a page the user has already left and hand
* repairStaleRender() a mess that only exists because a require in flight cannot be stopped. */
@@ -591,7 +591,7 @@ const HOLDS_CONTROLS = '.cbi-dropdown, .cbi-dynlist, .cbi-tooltip-container, .cb
* `grid`/`treegrid`), so overwriting it with `group` drops every cell to generic — a screen reader
* would then read a flat run of text with no rows and no columns, which is the very structure the
* SC 1.4.10 exception exists to preserve. `aria-label` works on either, so the name and the tab stop
* are unaffected. Reported in review.
* are unaffected.
*
* WHAT IS REMOVED IS WHAT WAS WRITTEN, remembered per element rather than inferred from the value:
* an app that had its own `tabindex="0"` or `role="group"` on that table got them taken away when the
@@ -639,18 +639,12 @@ function fitScrollables() {
if (scroll === was) { if (was) t.classList.add('fs-xscroll'); return; }
if (scroll) {
t.classList.add('fs-xscroll');
/* A SCROLL BOX THE KEYBOARD CANNOT REACH IS CONTENT THE KEYBOARD CANNOT READ.
/* A SCROLL BOX THE KEYBOARD CANNOT REACH IS CONTENT THE KEYBOARD CANNOT READ, and the
* tab stop, the name and the conditional role that say so are all reach()'s business —
* its header above carries the reasoning.
*
* Firefox has made scrollers focusable for years and Chrome since 132, but only when they
* hold no focusable child — a table row full of buttons disqualifies itself — and WebKit
* has not shipped it at all (bug 190870, open since 2018). So say it in the markup: a
* tab stop (SC 2.1.1), a role and a name so what receives focus can be announced
* (SC 4.1.2). `group`, not `region`: a region is a landmark, and a status page with four
* scrolling tables would put four of them in the landmark list.
*
* The name comes from the heading the table sits under, which is what a sighted user
* reads it as; only the fallback is ours to translate. Never overwritten: an app that
* labelled its own table knows better. */
* `group`, not `region`: a region is a landmark, and a status page with four scrolling
* tables would put four of them in the landmark list. */
reach(t);
}
else if (was) unreach(t);
@@ -30,7 +30,7 @@ function isReal() { return ((/^\d+\.\d+/).test(FS_VERSION)) && !((/-dev$/).test(
return baseclass.extend({
VERSION: FS_VERSION,
/* the project's page, for the link at the foot of the Appearance tab. One string, used once —
* it was assembled from a separate FS_REPO constant that the retired updater needed to build
* it was assembled from a separate FS_REPO constant that the retired self-update script needed to build
* API paths from, and nothing has needed the bare `owner/name` since. */
REPO_URL: 'https://github.com/VizzleTF/luci-theme-footstrap',
isReal,
@@ -58,6 +58,8 @@ return baseclass.extend({
fit.add(chrome.fitChrome);
chrome.renderChrome();
/* once per document — the footer outlives every client navigation */
chrome.nameThemeInFooter();
/* Both of these ADD to a stock page rather than owning a route: each watches
* body[data-page] and does nothing anywhere else. A theme may not register a
* dispatcher node — it outlives the theme that registered it. */
@@ -677,6 +677,17 @@
* this rule made them dead.) */
justify-content: center;
text-align: center;
/* AND SPACE THE LINES, because the two links in it become adjacent TARGETS the moment the
* line wraps. The footer is one sentence carrying the LuCI link and the OpenWrt link; on a
* wide screen they sit side by side and nothing is close to anything, but at the width where
* the sentence breaks they land on consecutive lines. With the inherited 1.5 that is a 16px
* line box, i.e. 16px between the two links' centres — inside the 24px envelope WCAG 2.2
* SC 2.5.8 requires around a target smaller than 24x24, which an inline link always is.
* Found by tools/live-audit.mjs on CI's runner and not on the maintainer's stands: the wrap
* width depends on text metrics, so the same 568px viewport breaks the line there and not
* here. 26px, not 24: the criterion is that the two 24px circles must not meet, so 24 is the
* boundary rather than a pass, and 26 leaves the footer visually unchanged on one line. */
line-height: 26px;
}
footer a { color: var(--fs-dim); }
footer a:hover { color: var(--fs-accent); }
+2 -2
View File
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=natflow
PKG_VERSION:=20260531
PKG_RELEASE:=69
PKG_RELEASE:=70
PKG_SOURCE:=$(PKG_VERSION).tar.xz
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
PKG_SOURCE_PROTO:=git
PKG_SOURCE_VERSION:=114c4a602c66cec71fe268455643e95c6df14695
PKG_SOURCE_VERSION:=bbd26afc851575c8c1b326a5d0bdfa3e6c2723c1
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
PKG_LICENSE:=GPL-2.0