luci-app-vpn-policy-routing: bugfix: remove escaped double-quotes from translateable...
[oweals/luci.git] / applications / luci-app-vpn-policy-routing / luasrc / model / cbi / vpn-policy-routing.lua
1 local readmeURL = "https://github.com/openwrt/packages/tree/master/net/vpn-policy-routing/files/README.md"
2
3 local packageName = "vpn-policy-routing"
4 local uci = require "luci.model.uci".cursor()
5 local sys = require "luci.sys"
6 local util = require "luci.util"
7 local ip = require "luci.ip"
8 local fs = require "nixio.fs"
9 local jsonc = require "luci.jsonc"
10 local http = require "luci.http"
11 local nutil = require "nixio.util"
12 local dispatcher = require "luci.dispatcher"
13 local enabledFlag = uci:get(packageName, "config", "enabled")
14 local enc
15
16 local ubusStatus = util.ubus("service", "list", { name = packageName })
17 if ubusStatus and ubusStatus[packageName] and 
18          ubusStatus[packageName]["instances"] and 
19          ubusStatus[packageName]["instances"]["main"] and 
20          ubusStatus[packageName]["instances"]["main"]["data"] and
21          ubusStatus[packageName]["instances"]["main"]["data"]["status"] and 
22          ubusStatus[packageName]["instances"]["main"]["data"]["status"][1] then
23         serviceGateways = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["gateway"]
24         serviceGateways = serviceGateways and serviceGateways:gsub('\\n', '\n')
25         serviceGateways = serviceGateways and serviceGateways:gsub('\\033%[0;32m%[\\xe2\\x9c\\x93%]\\033%[0m', '✓')
26         serviceErrors = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["error"]
27         serviceErrors = serviceErrors and serviceErrors:gsub('\\n', '\n')
28         serviceErrors = serviceErrors and serviceErrors:gsub('\\033%[0;31mERROR\\033%[0m: ', '')
29         serviceWarnings = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["warning"]
30         serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\n', '\n')
31         serviceWarnings = serviceWarnings and serviceWarnings:gsub('\\033%[0;33mWARNING\\033%[0m: ', '')
32         serviceMode = ubusStatus[packageName]["instances"]["main"]["data"]["status"][1]["mode"]
33 end
34
35 local serviceRunning, statusText = false, nil
36 local packageVersion = tostring(util.trim(sys.exec("opkg list-installed " .. packageName .. " | awk '{print $3}'"))) or ""
37 if packageVersion == "" then
38         statusText = translatef("%s is not installed or not found", packageName)
39 end 
40 if sys.call("iptables -t mangle -L | grep -q VPR_PREROUTING") == 0 then
41         serviceRunning = true
42         statusText = translate("Running")
43         if serviceMode and serviceMode == "strict" then
44                 statusText = translatef("%s (strict mode)", statusText)
45         end
46 else
47         statusText = translate("Stopped")
48         if uci:get(packageName, "config", "enabled") ~= "1" then
49                 statusText = translatef("%s (disabled)", statusText)
50         end
51 end
52
53 local t = uci:get("vpn-policy-routing", "config", "supported_interface")
54 if not t then
55         supportedIfaces = ""
56 elseif type(t) == "table" then
57         for key,value in pairs(t) do supportedIfaces = supportedIfaces and supportedIfaces .. ' ' .. value or value end
58 elseif type(t) == "string" then
59         supportedIfaces = t
60 end
61
62 t = uci:get("vpn-policy-routing", "config", "ignored_interface")
63 if not t then
64         ignoredIfaces = ""
65 elseif type(t) == "table" then
66         for key,value in pairs(t) do ignoredIfaces = ignoredIfaces and ignoredIfaces .. ' ' .. value or value end
67 elseif type(t) == "string" then
68         ignoredIfaces = t
69 end
70
71 local lanIPAddr = uci:get("network", "lan", "ipaddr")
72 local lanNetmask = uci:get("network", "lan", "netmask")
73 -- if multiple ip addresses on lan interface, will be returned as table of CIDR notations i.e. {"10.0.0.1/24","10.0.0.2/24"}
74 if (type(lanIPAddr) == "table") then
75                                 first = true
76                                 for i,line in ipairs(lanIPAddr) do
77                                                                 lanIPAddr = lanIPAddr[i]
78                                                                 break
79                                 end
80                                 lanIPAddr = lanIPAddr:match("[0-9.]+")
81 end          
82 if lanIPAddr and lanNetmask then
83         laPlaceholder = ip.new(lanIPAddr .. "/" .. lanNetmask )
84 end
85
86 function is_wan(name)
87         return name:sub(1,3) == "wan" or name:sub(-3) == "wan"
88 end
89
90 function is_supported_interface(arg)
91         local name=arg['.name']
92         local proto=arg['proto']
93         local ifname=arg['ifname']
94
95         if name and is_wan(name) then return true end
96         if name and supportedIfaces:match('%f[%w]' .. name .. '%f[%W]') then return true end
97         if name and not ignoredIfaces:match('%f[%w]' .. name .. '%f[%W]') then
98                 if type(ifname) == "table" then
99                         for key,value in pairs(ifname) do
100                                 if value and value:sub(1,3) == "tun" then return true end
101                                 if value and value:sub(1,3) == "tap" then return true end
102                                 if value and value:sub(1,3) == "tor" then return true end
103                                 if value and fs.access("/sys/devices/virtual/net/" .. value .. "/tun_flags") then return true end
104                         end
105                 elseif type(ifname) == "string" then
106                         if ifname and ifname:sub(1,3) == "tun" then return true end
107                         if ifname and ifname:sub(1,3) == "tap" then return true end
108                         if ifname and ifname:sub(1,3) == "tor" then return true end
109                         if ifname and fs.access("/sys/devices/virtual/net/" .. ifname .. "/tun_flags") then return true end
110                 end
111                 if proto and proto:sub(1,11) == "openconnect" then return true end
112                 if proto and proto:sub(1,4) == "pptp" then return true end
113                 if proto and proto:sub(1,4) == "l2tp" then return true end
114                 if proto and proto:sub(1,9) == "wireguard" then return true end
115         end
116 end
117
118 m = Map("vpn-policy-routing", translate("VPN and WAN Policy-Based Routing"))
119
120 h = m:section(NamedSection, "config", packageName, translatef("Service Status [%s %s]", packageName, packageVersion))
121 status = h:option(DummyValue, "_dummy", translate("Service Status"))
122 status.template = "vpn-policy-routing/status"
123 status.value = statusText
124 if serviceRunning and serviceGateways and serviceGateways ~= "" then
125         gateways = h:option(DummyValue, "_dummy", translate("Service Gateways"))
126         gateways.template = packageName .. "/status-gateways"
127         gateways.value = serviceGateways
128 end
129 if serviceErrors and serviceErrors ~= "" then
130         errors = h:option(DummyValue, "_dummy", translate("Service Errors"))
131         errors.template = packageName .. "/status-textarea"
132         errors.value = serviceErrors
133 end
134 if serviceWarnings and serviceWarnings ~= "" then
135         warnings = h:option(DummyValue, "_dummy", translate("Service Warnings"))
136         warnings.template = packageName .. "/status-textarea"
137         warnings.value = serviceWarnings
138 end
139 if packageVersion ~= "" then
140         buttons = h:option(DummyValue, "_dummy")
141         buttons.template = packageName .. "/buttons"
142 end
143
144 -- General Options
145 config = m:section(NamedSection, "config", "vpn-policy-routing", translate("Configuration"))
146 config.override_values = true
147 config.override_depends = true
148
149 -- Basic Options
150 config:tab("basic", translate("Basic Configuration"))
151
152 verb = config:taboption("basic", ListValue, "verbosity", translate("Output verbosity"), translate("Controls both system log and console output verbosity."))
153 verb:value("0", translate("Suppress/No output"))
154 verb:value("1", translate("Condensed output"))
155 verb:value("2", translate("Verbose output"))
156 verb.default = 2
157
158 se = config:taboption("basic", ListValue, "strict_enforcement", translate("Strict enforcement"),
159         translatef("See the %sREADME%s for details.", "<a href=\"" .. readmeURL .. "#strict-enforcement" .. "\" target=\"_blank\">", "</a>"))
160 se:value("0", translate("Do not enforce policies when their gateway is down"))
161 se:value("1", translate("Strictly enforce policies when their gateway is down"))
162 se.default = 1
163
164 dest_ipset = config:taboption("basic", ListValue, "dest_ipset", translate("The ipset option for remote policies"),
165         translatef("Please check the %sREADME%s before changing this option.", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>"))
166 dest_ipset:value("", translate("Disabled"))
167 dest_ipset:value("ipset", translate("Use ipset command"))
168 dest_ipset:value("dnsmasq.ipset", translate("Use DNSMASQ ipset"))
169 dest_ipset.default = ""
170 dest_ipset.rmempty = true
171
172 src_ipset = config:taboption("basic", ListValue, "src_ipset", translate("The ipset option for local policies"),
173         translatef("Please check the %sREADME%s before changing this option.", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>"))
174 src_ipset:value("0", translate("Disabled"))
175 src_ipset:value("1", translate("Use ipset command"))
176
177 ipv6 = config:taboption("basic", ListValue, "ipv6_enabled", translate("IPv6 Support"))
178 ipv6:value("0", translate("Disabled"))
179 ipv6:value("1", translate("Enabled"))
180
181 -- Advanced Options
182 config:tab("advanced", translate("Advanced Configuration"),
183         translatef("%sWARNING:%s Please make sure to check the %sREADME%s before changing anything in this section! Change any of the settings below with extreme caution!%s" , "<br/>&nbsp;&nbsp;&nbsp;&nbsp;<b>", "</b>", "<a href=\"" .. readmeURL .. "#service-configuration-settings" .. "\" target=\"_blank\">", "</a>", "<br/><br/>"))
184
185 supportedIface = config:taboption("advanced", DynamicList, "supported_interface", translate("Supported Interfaces"), translate("Allows to specify the list of interface names (in lower case) to be explicitly supported by the service. Can be useful if your OpenVPN tunnels have dev option other than tun* or tap*."))
186 supportedIface.optional = false
187
188 ignoredIface = config:taboption("advanced", DynamicList, "ignored_interface", translate("Ignored Interfaces"), translate("Allows to specify the list of interface names (in lower case) to be ignored by the service. Can be useful if running both VPN server and VPN client on the router."))
189 ignoredIface.optional = false
190
191 timeout = config:taboption("advanced", Value, "boot_timeout", translate("Boot Time-out"), translate("Time (in seconds) for service to wait for WAN gateway discovery on boot."))
192 timeout.optional = false
193 timeout.rmempty = true
194
195 insert = config:taboption("advanced", ListValue, "iptables_rule_option", translate("IPTables rule option"), translate("Select Append for -A and Insert for -I."))
196 insert:value("append", translate("Append"))
197 insert:value("insert", translate("Insert"))
198 insert.default = "append"
199
200 iprule = config:taboption("advanced", ListValue, "iprule_enabled", translate("IP Rules Support"), translate("Add an ip rule, not an iptables entry for policies with just the local address. Use with caution to manipulte policies priorities."))
201 iprule:value("0", translate("Disabled"))
202 iprule:value("1", translate("Enabled"))
203
204 icmp = config:taboption("advanced", ListValue, "icmp_interface", translate("Default ICMP Interface"), translate("Force the ICMP protocol interface."))
205 icmp:value("", translate("No Change"))
206 icmp:value("wan", translate("WAN"))
207 uci:foreach("network", "interface", function(s)
208         local name=s['.name']
209         if is_supported_interface(s) then icmp:value(name, name:upper()) end
210 end)
211 icmp.rmempty = true
212
213 append_local = config:taboption("advanced", Value, "append_src_rules", translate("Append local IP Tables rules"), translate("Special instructions to append iptables rules for local IPs/netmasks/devices."))
214 append_local.rmempty = true
215
216 append_remote = config:taboption("advanced", Value, "append_dest_rules", translate("Append remote IP Tables rules"), translate("Special instructions to append iptables rules for remote IPs/netmasks."))
217 append_remote.rmempty = true
218
219 wantid = config:taboption("advanced", Value, "wan_tid", translate("WAN Table ID"), translate("Starting (WAN) Table ID number for tables created by the service."))
220 wantid.rmempty = true
221 wantid.placeholder = "201"
222 wantid.datatype    = 'and(uinteger, min(201))'
223
224 wanmark = config:taboption("advanced", Value, "wan_mark", translate("WAN Table FW Mark"), translate("Starting (WAN) FW Mark for marks used by the service. High starting mark is used to avoid conflict with SQM/QoS. Change with caution together with") .. " " .. translate("Service FW Mask") .. ".")
225 wanmark.rmempty = true
226 wanmark.placeholder = "0x010000"
227 wanmark.datatype    = "hex(8)"
228
229 fwmask = config:taboption("advanced", Value, "fw_mask", translate("Service FW Mask"), translate("FW Mask used by the service. High mask is used to avoid conflict with SQM/QoS. Change with caution together with") .. " " .. translate("WAN Table FW Mark") .. ".")
230 fwmask.rmempty = true
231 fwmask.placeholder = "0xff0000"
232 fwmask.datatype    = "hex(8)"
233
234 config:tab("webui", translate("Web UI Configuration"))
235
236 webui_enable_column = config:taboption("webui", ListValue, "webui_enable_column", translate("Show Enable Column"), translate("Shows the enable checkbox column for policies, allowing you to quickly enable/disable specific policy without deleting it."))
237 webui_enable_column:value("0", translate("Disabled"))
238 webui_enable_column:value("1", translate("Enabled"))
239
240 webui_protocol_column = config:taboption("webui", ListValue, "webui_protocol_column", translate("Show Protocol Column"), translate("Shows the protocol column for policies, allowing you to assign a specific protocol to a policy."))
241 webui_protocol_column:value("0", translate("Disabled"))
242 webui_protocol_column:value("1", translate("Enabled"))
243
244 webui_supported_protocol = config:taboption("webui", DynamicList, "webui_supported_protocol", translate("Supported Protocols"), translate("Display these protocols in protocol column in Web UI."))
245 webui_supported_protocol.optional = false
246
247 webui_chain_column = config:taboption("webui", ListValue, "webui_chain_column", translate("Show Chain Column"), translate("Shows the chain column for policies, allowing you to assign a PREROUTING, FORWARD, INPUT or OUTPUT chain to a policy."))
248 webui_chain_column:value("0", translate("Disabled"))
249 webui_chain_column:value("1", translate("Enabled"))
250
251 webui_sorting = config:taboption("webui", ListValue, "webui_sorting", translate("Show Up/Down Buttons"), translate("Shows the Up/Down buttons for policies, allowing you to move a policy up or down in the list."))
252 webui_sorting:value("0", translate("Disabled"))
253 webui_sorting:value("1", translate("Enabled"))
254 webui_sorting.default = "1"
255
256
257 -- Policies
258 p = m:section(TypedSection, "policy", translate("Policies"), translate("Comment, interface and at least one other field are required. Multiple local and remote addresses/devices/domains and ports can be space separated. Placeholders below represent just the format/syntax and will not be used if fields are left blank."))
259 p.template = "cbi/tblsection"
260 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_sorting"))
261 if not enc or enc ~= 0 then
262         p.sortable  = true
263 end
264 p.anonymous = true
265 p.addremove = true
266
267 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_enable_column"))
268 if enc and enc ~= 0 then
269         le = p:option(Flag, "enabled", translate("Enabled"))
270         le.default = "1"
271 end
272
273 local comment = uci:get_first("vpn-policy-routing", "policy", "comment")
274 if comment then
275         p:option(Value, "comment", translate("Comment"))
276 else
277         p:option(Value, "name", translate("Name"))
278 end
279
280 la = p:option(Value, "src_addr", translate("Local addresses / devices"))
281 if laPlaceholder then
282         la.placeholder = laPlaceholder
283 end
284 la.rmempty = true
285 la.datatype    = 'list(neg(or(host,network,macaddr)))'
286
287 lp = p:option(Value, "src_port", translate("Local ports"))
288 lp.datatype    = 'list(neg(or(portrange, string)))'
289 lp.placeholder = "0-65535"
290 lp.rmempty = true
291
292 ra = p:option(Value, "dest_addr", translate("Remote addresses / domains"))
293 ra.datatype    = 'list(neg(host))'
294 ra.placeholder = "0.0.0.0/0"
295 ra.rmempty = true
296
297 rp = p:option(Value, "dest_port", translate("Remote ports"))
298 rp.datatype    = 'list(neg(or(portrange, string)))'
299 rp.placeholder = "0-65535"
300 rp.rmempty = true
301
302 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_protocol_column"))
303 if enc and enc ~= 0 then
304         proto = p:option(ListValue, "proto", translate("Protocol"))
305         proto:value("", "AUTO")
306         proto.default = ""
307         proto.rmempty = true
308         enc = uci:get_list("vpn-policy-routing", "config", "webui_supported_protocol")
309         local count = 0
310         for key, value in pairs(enc) do
311                 count = count + 1
312                 proto:value(value:lower(), value:gsub(" ", "/"):upper())
313         end
314         if count == 0 then
315                 enc = { "tcp", "udp", "tcp udp", "icmp", "all" }
316                 for key,value in pairs(enc) do
317                         proto:value(value:lower(), value:gsub(" ", "/"):upper())
318                 end
319         end
320 end
321
322 enc = tonumber(uci:get("vpn-policy-routing", "config", "webui_chain_column"))
323 if enc and enc ~= 0 then
324         chain = p:option(ListValue, "chain", translate("Chain"))
325         chain:value("", "PREROUTING")
326         chain:value("FORWARD", "FORWARD")
327         chain:value("INPUT", "INPUT")
328         chain:value("OUTPUT", "OUTPUT")
329         chain.default = ""
330         chain.rmempty = true
331 end
332
333 gw = p:option(ListValue, "interface", translate("Interface"))
334 gw.datatype = "network"
335 gw.rmempty = false
336 uci:foreach("network", "interface", function(s)
337         local name=s['.name']
338         if is_wan(name) then
339                 gw:value(name, name:upper())
340                 if not gw.default then gw.default = name end
341         elseif is_supported_interface(s) then 
342                 gw:value(name, name:upper()) 
343         end
344 end)
345
346 dscp = m:section(NamedSection, "config", "vpn-policy-routing", translate("DSCP Tagging"), 
347         translatef("Set DSCP tags (in range between 1 and 63) for specific interfaces. See the %sREADME%s for details.", "<a href=\"" .. readmeURL .. "#dscp-tag-based-policies" .. "\" target=\"_blank\">", "</a>"))
348 uci:foreach("network", "interface", function(s)
349         local name=s['.name']
350         if is_supported_interface(s) then 
351                 local x = dscp:option(Value, name .. "_dscp", name:upper() .. " " .. translate("DSCP Tag"))
352                 x.rmempty = true
353                 x.datatype = "range(1,63)"
354         end
355 end)
356
357 -- Includes
358 inc = m:section(TypedSection, "include", translate("Custom User File Includes"), 
359         translatef("Run the following user files after setting up but before restarting DNSMASQ. See the %sREADME%s for details.", "<a href=\"" .. readmeURL .. "#custom-user-files" .. "\" target=\"_blank\">", "</a>"))
360 inc.template = "cbi/tblsection"
361 inc.sortable  = true
362 inc.anonymous = true
363 inc.addremove = true
364
365 finc = inc:option(Flag, "enabled", translate("Enabled"))
366 finc.optional = false
367 finc.default = "1"
368 inc:option(Value, "path", translate("Path")).optional = false
369
370 return m