From df8373f593bfe3eb811ceb525c3ca5b870a26ebb Mon Sep 17 00:00:00 2001 From: xiaorouji <60100640+xiaorouji@users.noreply.github.com> Date: Wed, 6 Sep 2023 16:02:01 +0800 Subject: [PATCH] luci: add sing-box server support --- .../model/cbi/passwall/server/index.lua | 9 +- .../cbi/passwall/server/type/sing-box.lua | 284 ++++++++++++++++ .../luasrc/passwall/server_app.lua | 3 + .../luasrc/passwall/util_sing-box.lua | 313 +++++++++--------- 4 files changed, 438 insertions(+), 171 deletions(-) create mode 100644 luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/index.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/index.lua index a8e31454a..45bb3a834 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/server/index.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/index.lua @@ -45,15 +45,8 @@ e = t:option(DummyValue, "type", translate("Type")) e.cfgvalue = function(t, n) local v = Value.cfgvalue(t, n) if v then - if v == "V2ray" or v == "Xray" then + if v == "sing-box" or v == "V2ray" or v == "Xray" then local protocol = m:get(n, "protocol") - if protocol == "vmess" then - protocol = "VMess" - elseif protocol == "vless" then - protocol = "VLESS" - else - protocol = protocol:gsub("^%l",string.upper) - end return v .. " -> " .. protocol end return v diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua new file mode 100644 index 000000000..ba356c406 --- /dev/null +++ b/luci-app-passwall/luasrc/model/cbi/passwall/server/type/sing-box.lua @@ -0,0 +1,284 @@ +local m, s = ... + +local api = require "luci.passwall.api" + +local singbox_bin = api.finded_com("singbox") + +if not singbox_bin then + return +end + +local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") + +local type_name = "sing-box" + +local option_prefix = "singbox_" + +local function option_name(name) + return option_prefix .. name +end + +local function rm_prefix_cfgvalue(self, section) + if self.option:find(option_prefix) == 1 then + return m:get(section, self.option:sub(1 + #option_prefix)) + end +end +local function rm_prefix_write(self, section, value) + if s.fields["type"]:formvalue(arg[1]) == type_name then + if self.option:find(option_prefix) == 1 then + m:set(section, self.option:sub(1 + #option_prefix), value) + end + end +end +local function rm_prefix_remove(self, section, value) + if s.fields["type"]:formvalue(arg[1]) == type_name then + if self.option:find(option_prefix) == 1 then + m:del(section, self.option:sub(1 + #option_prefix)) + end + end +end + +local ss_method_list = { + "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", + "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +-- [[ Sing-Box ]] + +s.fields["type"]:value(type_name, "Sing-Box") + +o = s:option(ListValue, option_name("protocol"), translate("Protocol")) +o:value("socks", "Socks") +o:value("http", "HTTP") +o:value("shadowsocks", "Shadowsocks") +o:value("vmess", "Vmess") +o:value("vless", "VLESS") +o:value("trojan", "Trojan") +o:value("direct", "Direct") + +o = s:option(Value, option_name("port"), translate("Listen Port")) +o.datatype = "port" + +o = s:option(Flag, option_name("auth"), translate("Auth")) +o.validate = function(self, value, t) + if value and value == "1" then + local user_v = s.fields[option_name("username")]:formvalue(t) or "" + local pass_v = s.fields[option_name("password")]:formvalue(t) or "" + if user_v == "" or pass_v == "" then + return nil, translate("Username and Password must be used together!") + end + end + return value +end +o:depends({ [option_name("protocol")] = "socks" }) +o:depends({ [option_name("protocol")] = "http" }) + +o = s:option(Value, option_name("username"), translate("Username")) +o:depends({ [option_name("auth")] = true }) + +o = s:option(Value, option_name("password"), translate("Password")) +o.password = true +o:depends({ [option_name("auth")] = true }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) + +o = s:option(ListValue, option_name("d_protocol"), translate("Destination protocol")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("tcp,udp", "TCP,UDP") +o:depends({ [option_name("protocol")] = "direct" }) + +o = s:option(Value, option_name("d_address"), translate("Destination address")) +o:depends({ [option_name("protocol")] = "direct" }) + +o = s:option(Value, option_name("d_port"), translate("Destination port")) +o.datatype = "port" +o:depends({ [option_name("protocol")] = "direct" }) + +o = s:option(Value, option_name("decryption"), translate("Encrypt Method")) +o.default = "none" +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(ListValue, option_name("ss_method"), translate("Encrypt Method")) +o.not_rewrite = true +for a, t in ipairs(ss_method_list) do o:value(t) end +o:depends({ [option_name("protocol")] = "shadowsocks" }) +function o.cfgvalue(self, section) + return m:get(section, "method") +end +function o.write(self, section, value) + if s.fields["type"]:formvalue(arg[1]) == type_name then + m:set(section, "method", value) + end +end + +o = s:option(DynamicList, option_name("uuid"), translate("ID") .. "/" .. translate("Password")) +for i = 1, 3 do + o:value(api.gen_uuid(1)) +end +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +o = s:option(ListValue, option_name("flow"), translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ [option_name("protocol")] = "vless" }) + +o = s:option(Flag, option_name("tls"), translate("TLS")) +o.default = 0 +o.validate = function(self, value, t) + if value then + if value == "1" then + local ca = s.fields[option_name("tls_certificateFile")]:formvalue(t) or "" + local key = s.fields[option_name("tls_keyFile")]:formvalue(t) or "" + if ca == "" or key == "" then + return nil, translate("Public key and Private key path can not be empty!") + end + end + return value + end +end +o:depends({ [option_name("protocol")] = "http" }) +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +-- [[ TLS部分 ]] -- + +o = s:option(FileUpload, option_name("tls_certificateFile"), translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" +o:depends({ [option_name("tls")] = true }) +o.validate = function(self, value, t) + if value and value ~= "" then + if not nixio.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end + +o = s:option(FileUpload, option_name("tls_keyFile"), translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" +o:depends({ [option_name("tls")] = true }) +o.validate = function(self, value, t) + if value and value ~= "" then + if not nixio.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end + +o = s:option(ListValue, option_name("transport"), translate("Transport")) +o:value("tcp", "TCP") +o:value("http", "HTTP") +o:value("ws", "WebSocket") +o:value("quic", "QUIC") +o:value("grpc", "gRPC") +o:depends({ [option_name("protocol")] = "shadowsocks" }) +o:depends({ [option_name("protocol")] = "vmess" }) +o:depends({ [option_name("protocol")] = "vless" }) +o:depends({ [option_name("protocol")] = "trojan" }) + +-- [[ HTTP部分 ]]-- + +o = s:option(Value, option_name("http_host"), translate("HTTP Host")) +o:depends({ [option_name("transport")] = "http" }) + +o = s:option(Value, option_name("http_path"), translate("HTTP Path")) +o:depends({ [option_name("transport")] = "http" }) + +-- [[ WebSocket部分 ]]-- + +o = s:option(Value, option_name("ws_host"), translate("WebSocket Host")) +o:depends({ [option_name("transport")] = "ws" }) + +o = s:option(Value, option_name("ws_path"), translate("WebSocket Path")) +o:depends({ [option_name("transport")] = "ws" }) + +-- [[ gRPC部分 ]]-- +o = s:option(Value, option_name("grpc_serviceName"), "ServiceName") +o:depends({ [option_name("transport")] = "grpc" }) + +o = s:option(Flag, option_name("bind_local"), translate("Bind Local"), translate("When selected, it can only be accessed locally, It is recommended to turn on when using reverse proxies or be fallback.")) +o.default = "0" + +o = s:option(Flag, option_name("accept_lan"), translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o.default = "0" + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" and e.type == type_name then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remarks = e["remark"] + } + end +end + +o = s:option(ListValue, option_name("outbound_node"), translate("outbound node")) +o:value("nil", translate("Close")) +o:value("_socks", translate("Custom Socks")) +o:value("_http", translate("Custom HTTP")) +o:value("_iface", translate("Custom Interface")) +for k, v in pairs(nodes_table) do o:value(v.id, v.remarks) end +o.default = "nil" + +o = s:option(Value, option_name("outbound_node_address"), translate("Address (Support Domain Name)")) +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_port"), translate("Port")) +o.datatype = "port" +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_username"), translate("Username")) +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_password"), translate("Password")) +o.password = true +o:depends({ [option_name("outbound_node")] = "_socks" }) +o:depends({ [option_name("outbound_node")] = "_http" }) + +o = s:option(Value, option_name("outbound_node_iface"), translate("Interface")) +o.default = "eth1" +o:depends({ [option_name("outbound_node")] = "_iface" }) + +o = s:option(Flag, option_name("log"), translate("Log")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, option_name("loglevel"), translate("Log Level")) +o.default = "info" +o:value("debug") +o:value("info") +o:value("warn") +o:value("error") +o:depends({ [option_name("log")] = true }) + +for key, value in pairs(s.fields) do + if key:find(option_prefix) == 1 then + if not s.fields[key].not_rewrite then + s.fields[key].cfgvalue = rm_prefix_cfgvalue + s.fields[key].write = rm_prefix_write + s.fields[key].remove = rm_prefix_remove + end + + local deps = s.fields[key].deps + if #deps > 0 then + for index, value in ipairs(deps) do + deps[index]["type"] = type_name + end + else + s.fields[key]:depends({ type = type_name }) + end + end +end diff --git a/luci-app-passwall/luasrc/passwall/server_app.lua b/luci-app-passwall/luasrc/passwall/server_app.lua index f574e9db9..4b63f6a18 100644 --- a/luci-app-passwall/luasrc/passwall/server_app.lua +++ b/luci-app-passwall/luasrc/passwall/server_app.lua @@ -140,6 +140,9 @@ local function start() elseif type == "SS-Rust" then config = require(require_dir .. "util_shadowsocks").gen_config_server(user) bin = ln_run("/usr/bin/ssserver", "ssserver", "-c " .. config_file, log_path) + elseif type == "sing-box" then + config = require(require_dir .. "util_sing-box").gen_config_server(user) + bin = ln_run(api.get_app_path("singbox"), "sing-box", "run -c " .. config_file, log_path) elseif type == "V2ray" then config = require(require_dir .. "util_xray").gen_config_server(user) bin = ln_run(api.get_app_path("v2ray"), "v2ray", "run -c " .. config_file, log_path) diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 1e5f41a8d..6bfbb2e0b 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -261,107 +261,167 @@ function gen_outbound(flag, node, tag, proxy_table) end function gen_config_server(node) - local settings = nil - local routing = nil local outbounds = { - {protocol = "freedom", tag = "direct"}, {protocol = "block", tag = "block"} + { type = "direct", tag = "direct" }, + { type = "block", tag = "block" } } - if node.protocol == "vmess" or node.protocol == "vless" then - if node.uuid then - local clients = {} - for i = 1, #node.uuid do - clients[i] = { - id = node.uuid[i], - flow = ("vless" == node.protocol and "1" == node.tls and node.tlsflow) and node.tlsflow or nil - } - end - settings = { - clients = clients, - decryption = node.decryption or "none" - } - end - elseif node.protocol == "socks" then - settings = { - udp = ("1" == node.udp_forward) and true or false, - auth = ("1" == node.auth) and "password" or "noauth", - accounts = ("1" == node.auth) and { + local tls = nil + + if node.tls == "1" then + tls = { + enabled = true, + certificate_path = node.tls_certificateFile, + key_path = node.tls_keyFile, + } + end + + local v2ray_transport = nil + + if node.transport == "http" then + v2ray_transport = { + type = "http", + host = node.http_host, + path = node.http_path or "/", + } + end + + if node.transport == "ws" then + v2ray_transport = { + type = "ws", + path = node.ws_path or "/", + headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil, + early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil --要与 Xray-core 兼容,请将其设置为 Sec-WebSocket-Protocol。它需要与服务器保持一致。 + } + end + + if node.transport == "quic" then + v2ray_transport = { + type = "quic" + } + --没有额外的加密支持: 它基本上是重复加密。 并且 Xray-core 在这里与 v2ray-core 不兼容。 + end + + if node.transport == "grpc" then + v2ray_transport = { + type = "grpc", + serviceName = node.grpc_serviceName, + } + end + + local inbound = { + type = node.protocol, + tag = "inbound", + listen = (node.bind_local == "1") and "127.0.0.1" or "::", + listen_port = tonumber(node.port), + } + + local protocol_table = nil + + if node.protocol == "socks" then + protocol_table = { + users = (node.auth == "1") and { { - user = node.username, - pass = node.password + username = node.username, + password = node.password } } or nil } - elseif node.protocol == "http" then - settings = { - allowTransparent = false, - accounts = ("1" == node.auth) and { + end + + if node.protocol == "http" then + protocol_table = { + users = (node.auth == "1") and { { - user = node.username, - pass = node.password + username = node.username, + password = node.password } - } or nil + } or nil, + tls = tls, } - node.transport = "tcp" - node.tcp_guise = "none" - elseif node.protocol == "shadowsocks" then - settings = { + end + + if node.protocol == "shadowsocks" then + protocol_table = { method = node.method, password = node.password, - ivCheck = ("1" == node.iv_check) and true or false, - network = node.ss_network or "TCP,UDP" } - elseif node.protocol == "trojan" then + end + + if node.protocol == "vmess" then if node.uuid then - local clients = {} + local users = {} for i = 1, #node.uuid do - clients[i] = { - password = node.uuid[i] + users[i] = { + name = node.uuid[i], + uuid = node.uuid[i], + alterId = 0, } end - settings = { - clients = clients + protocol_table = { + users = users, + tls = tls, + transport = v2ray_transport, } end - elseif node.protocol == "dokodemo-door" then - settings = { - network = node.d_protocol, - address = node.d_address, - port = tonumber(node.d_port) + end + + if node.protocol == "vless" then + if node.uuid then + local users = {} + for i = 1, #node.uuid do + users[i] = { + name = node.uuid[i], + uuid = node.uuid[i], + flow = node.flow, + } + end + protocol_table = { + users = users, + tls = tls, + transport = v2ray_transport, + } + end + end + + if node.protocol == "trojan" then + if node.uuid then + local users = {} + for i = 1, #node.uuid do + users[i] = { + name = node.uuid[i], + uuid = node.uuid[i], + } + end + protocol_table = { + users = users, + tls = tls, + fallback = nil, + fallback_for_alpn = nil, + transport = v2ray_transport, + } + end + end + + if node.protocol == "direct" then + protocol_table = { + network = (node.d_protocol ~= "TCP,UDP") and node.d_protocol or nil, + override_address = node.d_address, + override_port = tonumber(node.d_port) } end - if node.fallback and node.fallback == "1" then - local fallbacks = {} - for i = 1, #node.fallback_list do - local fallbackStr = node.fallback_list[i] - if fallbackStr then - local tmp = {} - string.gsub(fallbackStr, '[^' .. "," .. ']+', function(w) - table.insert(tmp, w) - end) - local dest = tmp[1] or "" - local path = tmp[2] - if dest:find("%.") then - else - dest = tonumber(dest) - end - fallbacks[i] = { - path = path, - dest = dest, - xver = 1 - } - end + if protocol_table then + for key, value in pairs(protocol_table) do + inbound[key] = value end - settings.fallbacks = fallbacks end - routing = { + local route = { rules = { { - type = "field", - ip = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"}, - outboundTag = (node.accept_lan == nil or node.accept_lan == "0") and "block" or "direct" + ip_cidr = { "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" }, + outbound = (node.accept_lan == nil or node.accept_lan == "0") and "block" or "direct" } } } @@ -370,14 +430,10 @@ function gen_config_server(node) local outbound = nil if node.outbound_node == "_iface" and node.outbound_node_iface then outbound = { - protocol = "freedom", + type = "direct", tag = "outbound", - streamSettings = { - sockopt = { - mark = 255, - interface = node.outbound_node_iface - } - } + bind_interface = node.outbound_node_iface, + routing_mark = 255, } sys.call("mkdir -p /tmp/etc/passwall/iface && touch /tmp/etc/passwall/iface/" .. node.outbound_node_iface) else @@ -386,9 +442,8 @@ function gen_config_server(node) outbound_node_t = { type = node.type, protocol = node.outbound_node:gsub("_", ""), - transport = "tcp", address = node.outbound_node_address, - port = node.outbound_node_port, + port = tonumber(node.outbound_node_port), username = (node.outbound_node_username and node.outbound_node_username ~= "") and node.outbound_node_username or nil, password = (node.outbound_node_password and node.outbound_node_password ~= "") and node.outbound_node_password or nil, } @@ -396,99 +451,31 @@ function gen_config_server(node) outbound = require("luci.passwall.util_sing-box").gen_outbound(nil, outbound_node_t, "outbound") end if outbound then + route.final = "outbound" table.insert(outbounds, 1, outbound) end end local config = { log = { - loglevel = ("1" == node.log) and node.loglevel or "none" + disabled = (not node or node.log == "0") and true or false, + level = node.loglevel or "info", + timestamp = true, + --output = logfile, }, - -- 传入连接 - inbounds = { - { - listen = (node.bind_local == "1") and "127.0.0.1" or nil, - port = tonumber(node.port), - protocol = node.protocol, - settings = settings, - streamSettings = { - network = node.transport, - security = "none", - tlsSettings = ("1" == node.tls) and { - disableSystemRoot = false, - certificates = { - { - certificateFile = node.tls_certificateFile, - keyFile = node.tls_keyFile - } - } - } or nil, - tcpSettings = (node.transport == "tcp") and { - acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false, - header = { - type = node.tcp_guise, - request = (node.tcp_guise == "http") and { - path = node.tcp_guise_http_path or {"/"}, - headers = { - Host = node.tcp_guise_http_host or {} - } - } or nil - } - } or nil, - kcpSettings = (node.transport == "mkcp") and { - mtu = tonumber(node.mkcp_mtu), - tti = tonumber(node.mkcp_tti), - uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), - downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), - congestion = (node.mkcp_congestion == "1") and true or false, - readBufferSize = tonumber(node.mkcp_readBufferSize), - writeBufferSize = tonumber(node.mkcp_writeBufferSize), - seed = (node.mkcp_seed and node.mkcp_seed ~= "") and node.mkcp_seed or nil, - header = {type = node.mkcp_guise} - } or nil, - wsSettings = (node.transport == "ws") and { - acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false, - headers = (node.ws_host) and {Host = node.ws_host} or nil, - path = node.ws_path - } or nil, - httpSettings = (node.transport == "h2") and { - path = node.h2_path, host = node.h2_host - } or nil, - dsSettings = (node.transport == "ds") and { - path = node.ds_path - } or nil, - quicSettings = (node.transport == "quic") and { - security = node.quic_security, - key = node.quic_key, - header = {type = node.quic_guise} - } or nil, - grpcSettings = (node.transport == "grpc") and { - serviceName = node.grpc_serviceName - } or nil - } - } - }, - -- 传出连接 + inbounds = { inbound }, outbounds = outbounds, - routing = routing + route = route } - local alpn = {} - if node.alpn then - string.gsub(node.alpn, '[^' .. "," .. ']+', function(w) - table.insert(alpn, w) - end) - end - if alpn and #alpn > 0 then - if config.inbounds[1].streamSettings.tlsSettings then - config.inbounds[1].streamSettings.tlsSettings.alpn = alpn + for index, value in ipairs(config.outbounds) do + for k, v in pairs(config.outbounds[index]) do + if k:find("_") == 1 then + config.outbounds[index][k] = nil + end end end - if "1" == node.tls then - config.inbounds[1].streamSettings.security = "tls" - end - return config end