mirror of
https://github.com/SunBK201/UA3F.git
synced 2025-12-16 16:57:08 +00:00
feat: add rule system
This commit is contained in:
parent
5dfa30cf34
commit
9b2ca0223d
58
clash/rules.json
Normal file
58
clash/rules.json
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"enabled": false,
|
||||||
|
"type": "DEST-PORT",
|
||||||
|
"action": "DIRECT",
|
||||||
|
"match_value": "443",
|
||||||
|
"description": "",
|
||||||
|
"rewrite_value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"type": "KEYWORD",
|
||||||
|
"action": "DIRECT",
|
||||||
|
"match_value": "MicroMessenger Client",
|
||||||
|
"description": "",
|
||||||
|
"rewrite_value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"type": "KEYWORD",
|
||||||
|
"action": "DIRECT",
|
||||||
|
"match_value": "Bilibili Freedoooooom\/MarkII",
|
||||||
|
"description": "",
|
||||||
|
"rewrite_value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"type": "KEYWORD",
|
||||||
|
"action": "DIRECT",
|
||||||
|
"match_value": "Valve\/Steam HTTP Client 1.0",
|
||||||
|
"description": "",
|
||||||
|
"rewrite_value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"type": "KEYWORD",
|
||||||
|
"action": "REPLACE",
|
||||||
|
"match_value": "Mac",
|
||||||
|
"description": "",
|
||||||
|
"rewrite_value": "FFF"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"type": "REGEX",
|
||||||
|
"action": "REPLACE",
|
||||||
|
"match_value": "(Apple|iPhone|iPad|Macintosh|Mac OS X|Mac|Darwin|Microsoft|Windows|Linux|Android|OpenHarmony|HUAWEI|OPPO|Vivo|XiaoMi|Mobile|Dalvik)",
|
||||||
|
"rewrite_value": "FFF",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"type": "FINAL",
|
||||||
|
"action": "REPLACE",
|
||||||
|
"match_value": "",
|
||||||
|
"description": "Default Fallback Rule",
|
||||||
|
"rewrite_value": "FFF"
|
||||||
|
}
|
||||||
|
]
|
||||||
1
ipkg/CONTROL/conffiles
Normal file
1
ipkg/CONTROL/conffiles
Normal file
@ -0,0 +1 @@
|
|||||||
|
/etc/config/ua3f
|
||||||
@ -1,5 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
rm -rf /etc/config/ua3f >/dev/null 2>&1
|
# rm -rf /etc/config/ua3f >/dev/null 2>&1
|
||||||
rm -rf /var/log/ua3f >/dev/null 2>&1
|
rm -rf /var/log/ua3f >/dev/null 2>&1
|
||||||
rm -rf /usr/lib/lua/luci/model/cbi/ua3f >/dev/null 2>&1
|
rm -rf /usr/lib/lua/luci/model/cbi/ua3f >/dev/null 2>&1
|
||||||
rm -rf /usr/lib/lua/luci/model/cbi/ua3f.lua >/dev/null 2>&1
|
rm -rf /usr/lib/lua/luci/model/cbi/ua3f.lua >/dev/null 2>&1
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
uci set ua3f.enabled.enabled='0' > /dev/null 2>&1
|
||||||
|
uci commit ua3f > /dev/null 2>&1
|
||||||
[ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0
|
[ -s ${IPKG_INSTROOT}/lib/functions.sh ] || exit 0
|
||||||
. ${IPKG_INSTROOT}/lib/functions.sh
|
. ${IPKG_INSTROOT}/lib/functions.sh
|
||||||
default_prerm $0 $@
|
default_prerm $0 $@
|
||||||
|
|||||||
@ -4,6 +4,8 @@ function index()
|
|||||||
entry({ "admin", "services", "ua3f" }, cbi("ua3f"), _("UA3F"), 1)
|
entry({ "admin", "services", "ua3f" }, cbi("ua3f"), _("UA3F"), 1)
|
||||||
entry({ "admin", "services", "ua3f", "download_log" }, call("action_download_log")).leaf = true
|
entry({ "admin", "services", "ua3f", "download_log" }, call("action_download_log")).leaf = true
|
||||||
entry({ "admin", "services", "ua3f", "clear_log" }, call("clear_log")).leaf = true
|
entry({ "admin", "services", "ua3f", "clear_log" }, call("clear_log")).leaf = true
|
||||||
|
entry({ "admin", "services", "ua3f", "get_rules" }, call("get_rules")).leaf = true
|
||||||
|
entry({ "admin", "services", "ua3f", "save_rules" }, call("save_rules")).leaf = true
|
||||||
end
|
end
|
||||||
|
|
||||||
local fs = require("nixio.fs")
|
local fs = require("nixio.fs")
|
||||||
@ -71,3 +73,111 @@ function clear_log()
|
|||||||
http.write("Log file not found")
|
http.write("Log file not found")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function get_rules()
|
||||||
|
local http = require("luci.http")
|
||||||
|
local uci = require("luci.model.uci").cursor()
|
||||||
|
local json = require("luci.jsonc")
|
||||||
|
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
|
||||||
|
local rules = {}
|
||||||
|
local rules_data = uci:get("ua3f", "main", "rewrite_rules")
|
||||||
|
|
||||||
|
if rules_data then
|
||||||
|
-- Parse the rules from UCI config
|
||||||
|
-- Rules are stored as JSON string in UCI
|
||||||
|
local success, parsed_rules = pcall(json.parse, rules_data)
|
||||||
|
if success and parsed_rules then
|
||||||
|
rules = parsed_rules
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
http.write(json.stringify({
|
||||||
|
success = true,
|
||||||
|
rules = rules
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|
||||||
|
function save_rules()
|
||||||
|
local http = require("luci.http")
|
||||||
|
local uci = require("luci.model.uci").cursor()
|
||||||
|
local json = require("luci.jsonc")
|
||||||
|
|
||||||
|
http.prepare_content("application/json")
|
||||||
|
|
||||||
|
-- Read POST data
|
||||||
|
local content_length = tonumber(http.getenv("CONTENT_LENGTH"))
|
||||||
|
if not content_length or content_length == 0 then
|
||||||
|
http.write(json.stringify({
|
||||||
|
success = false,
|
||||||
|
error = "No data provided"
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local post_data = http.content()
|
||||||
|
if not post_data then
|
||||||
|
http.write(json.stringify({
|
||||||
|
success = false,
|
||||||
|
error = "Failed to read request data"
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Parse JSON data
|
||||||
|
local success, data = pcall(json.parse, post_data)
|
||||||
|
if not success or not data or not data.rules then
|
||||||
|
http.write(json.stringify({
|
||||||
|
success = false,
|
||||||
|
error = "Invalid JSON data"
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Ensure FINAL rule exists and is at the end
|
||||||
|
local has_final = false
|
||||||
|
local final_rule_index = nil
|
||||||
|
for i, rule in ipairs(data.rules) do
|
||||||
|
if rule.type == "FINAL" then
|
||||||
|
has_final = true
|
||||||
|
final_rule_index = i
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If FINAL rule exists but not at the end, move it
|
||||||
|
if has_final and final_rule_index ~= #data.rules then
|
||||||
|
local final_rule = table.remove(data.rules, final_rule_index)
|
||||||
|
table.insert(data.rules, final_rule)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- If no FINAL rule, add one
|
||||||
|
if not has_final then
|
||||||
|
table.insert(data.rules, {
|
||||||
|
type = "FINAL",
|
||||||
|
match_value = "",
|
||||||
|
action = "DIRECT",
|
||||||
|
rewrite_value = "",
|
||||||
|
description = "Default fallback rule",
|
||||||
|
enabled = true
|
||||||
|
})
|
||||||
|
else
|
||||||
|
-- Ensure FINAL rule is always enabled
|
||||||
|
for i, rule in ipairs(data.rules) do
|
||||||
|
if rule.type == "FINAL" then
|
||||||
|
rule.enabled = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Save rules to UCI
|
||||||
|
local rules_json = json.stringify(data.rules)
|
||||||
|
uci:set("ua3f", "main", "rewrite_rules", rules_json)
|
||||||
|
|
||||||
|
http.write(json.stringify({
|
||||||
|
success = true,
|
||||||
|
message = "Rules saved successfully"
|
||||||
|
}))
|
||||||
|
end
|
||||||
|
|||||||
@ -22,6 +22,7 @@ function create_sections(map)
|
|||||||
-- General Section with tabs
|
-- General Section with tabs
|
||||||
sections.general = map:section(NamedSection, "main", "ua3f", translate("General"))
|
sections.general = map:section(NamedSection, "main", "ua3f", translate("General"))
|
||||||
sections.general:tab("general", translate("Settings"))
|
sections.general:tab("general", translate("Settings"))
|
||||||
|
sections.general:tab("rewrite", translate("Rewrite Rules"))
|
||||||
sections.general:tab("stats", translate("Statistics"))
|
sections.general:tab("stats", translate("Statistics"))
|
||||||
sections.general:tab("log", translate("Log"))
|
sections.general:tab("log", translate("Log"))
|
||||||
sections.general:tab("others", translate("Others Settings"))
|
sections.general:tab("others", translate("Others Settings"))
|
||||||
@ -33,6 +34,7 @@ local sections = create_sections(ua3f)
|
|||||||
|
|
||||||
fields.add_status_fields(sections.status)
|
fields.add_status_fields(sections.status)
|
||||||
fields.add_general_fields(sections.general)
|
fields.add_general_fields(sections.general)
|
||||||
|
fields.add_rewrite_fields(sections.general)
|
||||||
fields.add_stats_fields(sections.general)
|
fields.add_stats_fields(sections.general)
|
||||||
fields.add_log_fields(sections.general)
|
fields.add_log_fields(sections.general)
|
||||||
fields.add_others_fields(sections.general)
|
fields.add_others_fields(sections.general)
|
||||||
|
|||||||
@ -41,15 +41,22 @@ function M.add_general_fields(section)
|
|||||||
server_mode:value("TPROXY", "TPROXY")
|
server_mode:value("TPROXY", "TPROXY")
|
||||||
server_mode:value("REDIRECT", "REDIRECT")
|
server_mode:value("REDIRECT", "REDIRECT")
|
||||||
server_mode:value("NFQUEUE", "NFQUEUE")
|
server_mode:value("NFQUEUE", "NFQUEUE")
|
||||||
|
server_mode.default = "SOCKS5"
|
||||||
-- Port
|
|
||||||
local port = section:taboption("general", Value, "port", translate("Port"))
|
|
||||||
port.placeholder = "1080"
|
|
||||||
|
|
||||||
-- Bind Address
|
-- Bind Address
|
||||||
local bind = section:taboption("general", Value, "bind", translate("Bind Address"))
|
local bind = section:taboption("general", Value, "bind", translate("Bind Address"))
|
||||||
bind:value("127.0.0.1")
|
bind:value("127.0.0.1")
|
||||||
bind:value("0.0.0.0")
|
bind:value("0.0.0.0")
|
||||||
|
bind:depends("server_mode", "HTTP")
|
||||||
|
bind:depends("server_mode", "SOCKS5")
|
||||||
|
|
||||||
|
-- Port
|
||||||
|
local port = section:taboption("general", Value, "port", translate("Port"))
|
||||||
|
port.placeholder = "1080"
|
||||||
|
port:depends("server_mode", "HTTP")
|
||||||
|
port:depends("server_mode", "SOCKS5")
|
||||||
|
port:depends("server_mode", "TPROXY")
|
||||||
|
port:depends("server_mode", "REDIRECT")
|
||||||
|
|
||||||
-- Log Level
|
-- Log Level
|
||||||
local log_level = section:taboption("general", ListValue, "log_level", translate("Log Level"))
|
local log_level = section:taboption("general", ListValue, "log_level", translate("Log Level"))
|
||||||
@ -62,14 +69,27 @@ function M.add_general_fields(section)
|
|||||||
log_level.description = translate(
|
log_level.description = translate(
|
||||||
"Sets the logging level. Do not keep the log level set to debug/info/warn for an extended period of time.")
|
"Sets the logging level. Do not keep the log level set to debug/info/warn for an extended period of time.")
|
||||||
|
|
||||||
-- User-Agent
|
-- Rewrite Mode
|
||||||
|
local rewrite_mode = section:taboption("general", ListValue, "rewrite_mode", translate("Rewrite Mode"))
|
||||||
|
rewrite_mode:value("DIRECT", translate("Direct Forward"))
|
||||||
|
rewrite_mode:value("GLOBAL", translate("Global Rewrite"))
|
||||||
|
rewrite_mode:value("RULES", translate("Rule Based"))
|
||||||
|
rewrite_mode.default = "GLOBAL"
|
||||||
|
rewrite_mode.description = translate(
|
||||||
|
"Direct Forward: No rewriting. Global Rewrite: Rewrite all User-Agents to the specified value. Rule Based: Use rewrite rules to determine behavior.")
|
||||||
|
|
||||||
|
-- User-Agent (for Global Rewrite)
|
||||||
local ua = section:taboption("general", Value, "ua", translate("User-Agent"))
|
local ua = section:taboption("general", Value, "ua", translate("User-Agent"))
|
||||||
ua.placeholder = "FFF"
|
ua.placeholder = "FFF"
|
||||||
ua.description = translate("User-Agent after rewrite")
|
ua.description = translate("User-Agent after rewrite")
|
||||||
|
ua:depends("rewrite_mode", "GLOBAL")
|
||||||
|
ua:depends("server_mode", "NFQUEUE")
|
||||||
|
|
||||||
-- User-Agent Regex
|
-- User-Agent Regex
|
||||||
local uaRegexPattern = section:taboption("general", Value, "ua_regex", translate("User-Agent Regex"))
|
local regex = section:taboption("general", Value, "ua_regex", translate("User-Agent Regex"))
|
||||||
uaRegexPattern.description = translate("Regular expression pattern for matching User-Agent")
|
regex.description = translate("Regular expression pattern for matching User-Agent")
|
||||||
|
regex:depends("rewrite_mode", "GLOBAL")
|
||||||
|
regex:depends("server_mode", "NFQUEUE")
|
||||||
|
|
||||||
-- Partial Replace
|
-- Partial Replace
|
||||||
local partialReplace = section:taboption("general", Flag, "partial_replace", translate("Partial Replace"))
|
local partialReplace = section:taboption("general", Flag, "partial_replace", translate("Partial Replace"))
|
||||||
@ -77,12 +97,14 @@ function M.add_general_fields(section)
|
|||||||
translate(
|
translate(
|
||||||
"Replace only the matched part of the User-Agent, only works when User-Agent Regex is not empty")
|
"Replace only the matched part of the User-Agent, only works when User-Agent Regex is not empty")
|
||||||
partialReplace.default = "0"
|
partialReplace.default = "0"
|
||||||
|
partialReplace:depends("rewrite_mode", "GLOBAL")
|
||||||
|
partialReplace:depends("server_mode", "NFQUEUE")
|
||||||
|
end
|
||||||
|
|
||||||
-- Direct Forward
|
-- Rewrite Rules Tab Fields
|
||||||
local directForward = section:taboption("general", Flag, "direct_forward", translate("Direct Forward"))
|
function M.add_rewrite_fields(section)
|
||||||
directForward.description =
|
local rules = section:taboption("rewrite", DummyValue, "")
|
||||||
translate("Directly forward packets without rewriting")
|
rules.template = "ua3f/rules"
|
||||||
directForward.default = "0"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Statistics Tab Fields
|
-- Statistics Tab Fields
|
||||||
|
|||||||
653
openwrt/files/luci/view/ua3f/rules.htm
Normal file
653
openwrt/files/luci/view/ua3f/rules.htm
Normal file
@ -0,0 +1,653 @@
|
|||||||
|
<%
|
||||||
|
local uci = require("luci.model.uci").cursor()
|
||||||
|
local json = require("luci.jsonc")
|
||||||
|
|
||||||
|
local rules_data = uci:get("ua3f", "main", "rewrite_rules") or "[]"
|
||||||
|
%>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.rule-actions button { margin: 0 2px; }
|
||||||
|
.empty-message { text-align: center; padding: 20px; font-style: italic; }
|
||||||
|
|
||||||
|
.cbi-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-modal-dialog {
|
||||||
|
background: var(--background-color-high, #fff);
|
||||||
|
color: var(--text-color-primary, #000);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-modal-dialog-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-bottom: 1px solid var(--border-color-medium, #ddd);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-modal-dialog-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cbi-modal-dialog-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Rewrite Rules Section -->
|
||||||
|
<div class="cbi-section-descr" style="font-weight:bold; margin-bottom: 10px;"><%:Rewrite Rules%></div>
|
||||||
|
<div class="cbi-section-descr" style="color: #999; font-size: 90%; margin-bottom: 10px;">
|
||||||
|
<%:Note: Rewrite rules only take effect in rule mode. NFQUEUE mode is temporarily unavailable.%>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="rewrite-rules-table" class="table cbi-section-table">
|
||||||
|
<tr class="tr table-titles">
|
||||||
|
<th class="th" style="width: 80px; text-align: center"><%:Enabled%></th>
|
||||||
|
<th class="th" style="width: 50px"><%:Index%></th>
|
||||||
|
<th class="th" style="width: 120px"><%:Rule Type%></th>
|
||||||
|
<th class="th"><%:Match Value%></th>
|
||||||
|
<th class="th" style="width: 120px"><%:Rewrite Action%></th>
|
||||||
|
<th class="th"><%:Rewrite Value%></th>
|
||||||
|
<th class="th" style="width: 280px"><%:Actions%></th>
|
||||||
|
</tr>
|
||||||
|
<tbody id="rulesTableBody"></tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div style="margin: 10px 0; padding: 5px 0;">
|
||||||
|
<input type="button" class="cbi-button cbi-button-add" onclick="rewriteRules.openAddDialog()" value="<%:Add Rule%>" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
(function() {
|
||||||
|
window.rewriteRules = {
|
||||||
|
rules: <%=rules_data%>,
|
||||||
|
|
||||||
|
init: function() {
|
||||||
|
this.ensureFinalRule();
|
||||||
|
this.renderTable();
|
||||||
|
},
|
||||||
|
|
||||||
|
ensureFinalRule: function() {
|
||||||
|
// Check if FINAL rule exists
|
||||||
|
var hasFinalRule = this.rules.some(function(rule) {
|
||||||
|
return rule.type === 'FINAL';
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no FINAL rule exists, add it
|
||||||
|
if (!hasFinalRule) {
|
||||||
|
this.rules.push({
|
||||||
|
type: 'FINAL',
|
||||||
|
match_value: '',
|
||||||
|
action: 'DIRECT',
|
||||||
|
rewrite_value: '',
|
||||||
|
description: 'Default fallback rule',
|
||||||
|
enabled: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Ensure FINAL rule is at the bottom
|
||||||
|
var finalRuleIndex = -1;
|
||||||
|
for (var i = 0; i < this.rules.length; i++) {
|
||||||
|
if (this.rules[i].type === 'FINAL') {
|
||||||
|
finalRuleIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalRuleIndex !== -1 && finalRuleIndex !== this.rules.length - 1) {
|
||||||
|
var finalRule = this.rules.splice(finalRuleIndex, 1)[0];
|
||||||
|
this.rules.push(finalRule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isFinalRule: function(index) {
|
||||||
|
return this.rules[index] && this.rules[index].type === 'FINAL';
|
||||||
|
},
|
||||||
|
|
||||||
|
renderTable: function() {
|
||||||
|
var tbody = document.getElementById('rulesTableBody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
if (this.rules.length === 0) {
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
tr.className = 'tr';
|
||||||
|
var td = document.createElement('td');
|
||||||
|
td.className = 'td empty-message';
|
||||||
|
td.colSpan = 7;
|
||||||
|
td.textContent = '<%:No rules configured. Click「Add Rule」to create one%>';
|
||||||
|
tr.appendChild(td);
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < this.rules.length; i++) {
|
||||||
|
tbody.appendChild(this.createRuleRow(this.rules[i], i));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createRuleRow: function(rule, index) {
|
||||||
|
var tr = document.createElement('tr');
|
||||||
|
tr.className = 'tr cbi-section-table-row';
|
||||||
|
var isFinal = this.isFinalRule(index);
|
||||||
|
|
||||||
|
// Enabled column
|
||||||
|
var td1 = document.createElement('td');
|
||||||
|
td1.className = 'td';
|
||||||
|
td1.style.textAlign = 'center';
|
||||||
|
var enabledCheckbox = document.createElement('input');
|
||||||
|
enabledCheckbox.type = 'checkbox';
|
||||||
|
enabledCheckbox.className = 'cbi-input-checkbox';
|
||||||
|
enabledCheckbox.checked = rule.enabled;
|
||||||
|
// FINAL rule must always be enabled
|
||||||
|
if (isFinal) {
|
||||||
|
enabledCheckbox.disabled = true;
|
||||||
|
enabledCheckbox.checked = true;
|
||||||
|
} else {
|
||||||
|
enabledCheckbox.onchange = function() { rewriteRules.toggleRuleEnabled(index, this.checked); };
|
||||||
|
}
|
||||||
|
td1.appendChild(enabledCheckbox);
|
||||||
|
tr.appendChild(td1);
|
||||||
|
|
||||||
|
// Index column
|
||||||
|
var td2 = document.createElement('td');
|
||||||
|
td2.className = 'td';
|
||||||
|
td2.textContent = index + 1;
|
||||||
|
tr.appendChild(td2);
|
||||||
|
|
||||||
|
// Rule Type column
|
||||||
|
var td3 = document.createElement('td');
|
||||||
|
td3.className = 'td';
|
||||||
|
td3.textContent = this.getRuleTypeLabel(rule.type);
|
||||||
|
tr.appendChild(td3);
|
||||||
|
|
||||||
|
// Match Value column
|
||||||
|
var td4 = document.createElement('td');
|
||||||
|
td4.className = 'td';
|
||||||
|
td4.style.maxWidth = '200px';
|
||||||
|
td4.style.overflow = 'hidden';
|
||||||
|
td4.style.textOverflow = 'ellipsis';
|
||||||
|
td4.style.whiteSpace = 'nowrap';
|
||||||
|
var span4 = document.createElement('span');
|
||||||
|
span4.textContent = isFinal ? '-' : (rule.match_value || '');
|
||||||
|
span4.title = isFinal ? '' : (rule.match_value || '');
|
||||||
|
td4.appendChild(span4);
|
||||||
|
tr.appendChild(td4);
|
||||||
|
|
||||||
|
// Rewrite Action column
|
||||||
|
var td5 = document.createElement('td');
|
||||||
|
td5.className = 'td';
|
||||||
|
td5.textContent = this.getActionLabel(rule.action);
|
||||||
|
tr.appendChild(td5);
|
||||||
|
|
||||||
|
// Rewrite Value column
|
||||||
|
var td6 = document.createElement('td');
|
||||||
|
td6.className = 'td';
|
||||||
|
td6.style.maxWidth = '200px';
|
||||||
|
td6.style.overflow = 'hidden';
|
||||||
|
td6.style.textOverflow = 'ellipsis';
|
||||||
|
td6.style.whiteSpace = 'nowrap';
|
||||||
|
var span6 = document.createElement('span');
|
||||||
|
span6.textContent = rule.rewrite_value || '-';
|
||||||
|
span6.title = rule.rewrite_value || '';
|
||||||
|
td6.appendChild(span6);
|
||||||
|
tr.appendChild(td6);
|
||||||
|
|
||||||
|
// Actions column
|
||||||
|
var td7 = document.createElement('td');
|
||||||
|
td7.className = 'td rule-actions';
|
||||||
|
|
||||||
|
var editBtn = document.createElement('button');
|
||||||
|
editBtn.type = 'button';
|
||||||
|
editBtn.className = 'cbi-button cbi-button-edit';
|
||||||
|
editBtn.textContent = '<%:Edit%>';
|
||||||
|
editBtn.onclick = function() { rewriteRules.editRule(index); };
|
||||||
|
td7.appendChild(editBtn);
|
||||||
|
|
||||||
|
// Only show move and delete buttons for non-FINAL rules
|
||||||
|
if (!isFinal) {
|
||||||
|
td7.appendChild(document.createTextNode(' '));
|
||||||
|
|
||||||
|
var upBtn = document.createElement('button');
|
||||||
|
upBtn.type = 'button';
|
||||||
|
upBtn.className = 'cbi-button cbi-button-neutral';
|
||||||
|
upBtn.textContent = '<%:Move Up%>';
|
||||||
|
upBtn.disabled = index === 0;
|
||||||
|
upBtn.onclick = function() { rewriteRules.moveRuleUp(index); };
|
||||||
|
td7.appendChild(upBtn);
|
||||||
|
|
||||||
|
td7.appendChild(document.createTextNode(' '));
|
||||||
|
|
||||||
|
var downBtn = document.createElement('button');
|
||||||
|
downBtn.type = 'button';
|
||||||
|
downBtn.className = 'cbi-button cbi-button-neutral';
|
||||||
|
downBtn.textContent = '<%:Move Down%>';
|
||||||
|
// Can't move down if it's the second-to-last (because last is FINAL)
|
||||||
|
downBtn.disabled = index >= this.rules.length - 2;
|
||||||
|
downBtn.onclick = function() { rewriteRules.moveRuleDown(index); };
|
||||||
|
td7.appendChild(downBtn);
|
||||||
|
|
||||||
|
td7.appendChild(document.createTextNode(' '));
|
||||||
|
|
||||||
|
var delBtn = document.createElement('button');
|
||||||
|
delBtn.type = 'button';
|
||||||
|
delBtn.className = 'cbi-button cbi-button-remove';
|
||||||
|
delBtn.textContent = '<%:Delete%>';
|
||||||
|
delBtn.onclick = function() { rewriteRules.deleteRule(index); };
|
||||||
|
td7.appendChild(delBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.appendChild(td7);
|
||||||
|
|
||||||
|
return tr;
|
||||||
|
},
|
||||||
|
|
||||||
|
getRuleTypeLabel: function(type) {
|
||||||
|
var labels = {
|
||||||
|
'KEYWORD': '<%:KEYWORD%>',
|
||||||
|
'REGEX': '<%:REGEX%>',
|
||||||
|
'IP-CIDR': '<%:IP-CIDR%>',
|
||||||
|
'SRC-IP': '<%:SRC-IP%>',
|
||||||
|
'DEST-PORT': '<%:DEST-PORT%>',
|
||||||
|
'FINAL': '<%:FINAL%>'
|
||||||
|
};
|
||||||
|
return labels[type] || type;
|
||||||
|
},
|
||||||
|
|
||||||
|
getActionLabel: function(action) {
|
||||||
|
var labels = {
|
||||||
|
'REPLACE': '<%:REPLACE%>',
|
||||||
|
'REPLACE-PART': '<%:REPLACE-PART%>',
|
||||||
|
'DELETE': '<%:DELETE%>',
|
||||||
|
'DIRECT': '<%:DIRECT%>',
|
||||||
|
'DROP': '<%:DROP%>'
|
||||||
|
};
|
||||||
|
return labels[action] || action;
|
||||||
|
},
|
||||||
|
|
||||||
|
openAddDialog: function() {
|
||||||
|
this.showRuleDialog(null, -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
editRule: function(index) {
|
||||||
|
this.showRuleDialog(this.rules[index], index);
|
||||||
|
},
|
||||||
|
|
||||||
|
showRuleDialog: function(rule, index) {
|
||||||
|
var self = this;
|
||||||
|
var isEdit = rule !== null;
|
||||||
|
var isFinal = isEdit && rule.type === 'FINAL';
|
||||||
|
|
||||||
|
// Create modal structure
|
||||||
|
var modal = document.createElement('div');
|
||||||
|
modal.className = 'cbi-modal';
|
||||||
|
|
||||||
|
var dialog = document.createElement('div');
|
||||||
|
dialog.className = 'cbi-modal-dialog';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
var header = document.createElement('div');
|
||||||
|
header.className = 'cbi-modal-dialog-header';
|
||||||
|
var h3 = document.createElement('h3');
|
||||||
|
h3.textContent = isFinal ? '<%:Edit FINAL Rule%>' : (isEdit ? '<%:Edit Rewrite Rule%>' : '<%:Add Rewrite Rule%>');
|
||||||
|
header.appendChild(h3);
|
||||||
|
dialog.appendChild(header);
|
||||||
|
|
||||||
|
// Body
|
||||||
|
var body = document.createElement('div');
|
||||||
|
body.className = 'cbi-modal-dialog-body';
|
||||||
|
|
||||||
|
var section = document.createElement('div');
|
||||||
|
section.className = 'cbi-section';
|
||||||
|
|
||||||
|
// Rule Type (hidden for FINAL rule)
|
||||||
|
if (!isFinal) {
|
||||||
|
var typeValue = document.createElement('div');
|
||||||
|
typeValue.className = 'cbi-value';
|
||||||
|
var typeLabel = document.createElement('label');
|
||||||
|
typeLabel.className = 'cbi-value-title';
|
||||||
|
typeLabel.textContent = '<%:Rule Type%>';
|
||||||
|
typeValue.appendChild(typeLabel);
|
||||||
|
var typeField = document.createElement('div');
|
||||||
|
typeField.className = 'cbi-value-field';
|
||||||
|
var typeSelect = document.createElement('select');
|
||||||
|
typeSelect.className = 'cbi-input-select';
|
||||||
|
typeSelect.id = 'modal_rule_type';
|
||||||
|
var types = [
|
||||||
|
['KEYWORD', '<%:KEYWORD%>'],
|
||||||
|
['REGEX', '<%:REGEX%>'],
|
||||||
|
['IP-CIDR', '<%:IP-CIDR%>'],
|
||||||
|
['SRC-IP', '<%:SRC-IP%>'],
|
||||||
|
['DEST-PORT', '<%:DEST-PORT%>']
|
||||||
|
];
|
||||||
|
types.forEach(function(t) {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = t[0];
|
||||||
|
option.textContent = t[1];
|
||||||
|
if (rule && rule.type === t[0]) option.selected = true;
|
||||||
|
typeSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
typeSelect.onchange = function() { rewriteRules.updateMatchValuePlaceholder(); };
|
||||||
|
typeField.appendChild(typeSelect);
|
||||||
|
typeValue.appendChild(typeField);
|
||||||
|
section.appendChild(typeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match Value (hidden for FINAL rule)
|
||||||
|
if (!isFinal) {
|
||||||
|
var matchValue = document.createElement('div');
|
||||||
|
matchValue.className = 'cbi-value';
|
||||||
|
var matchLabel = document.createElement('label');
|
||||||
|
matchLabel.className = 'cbi-value-title';
|
||||||
|
matchLabel.textContent = '<%:Match Value%>';
|
||||||
|
matchValue.appendChild(matchLabel);
|
||||||
|
var matchField = document.createElement('div');
|
||||||
|
matchField.className = 'cbi-value-field';
|
||||||
|
var matchInput = document.createElement('input');
|
||||||
|
matchInput.type = 'text';
|
||||||
|
matchInput.className = 'cbi-input-text';
|
||||||
|
matchInput.id = 'modal_match_value';
|
||||||
|
matchInput.placeholder = '<%:Enter match value%>';
|
||||||
|
if (rule) matchInput.value = rule.match_value || '';
|
||||||
|
matchField.appendChild(matchInput);
|
||||||
|
matchValue.appendChild(matchField);
|
||||||
|
section.appendChild(matchValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite Action
|
||||||
|
var actionValue = document.createElement('div');
|
||||||
|
actionValue.className = 'cbi-value';
|
||||||
|
var actionLabel = document.createElement('label');
|
||||||
|
actionLabel.className = 'cbi-value-title';
|
||||||
|
actionLabel.textContent = '<%:Rewrite Action%>';
|
||||||
|
actionValue.appendChild(actionLabel);
|
||||||
|
var actionField = document.createElement('div');
|
||||||
|
actionField.className = 'cbi-value-field';
|
||||||
|
var actionSelect = document.createElement('select');
|
||||||
|
actionSelect.className = 'cbi-input-select';
|
||||||
|
actionSelect.id = 'modal_action';
|
||||||
|
var actions = isFinal ? [
|
||||||
|
['DELETE', '<%:DELETE%>'],
|
||||||
|
['REPLACE', '<%:REPLACE%>'],
|
||||||
|
['DIRECT', '<%:DIRECT%>']
|
||||||
|
] : [
|
||||||
|
['DELETE', '<%:DELETE%>'],
|
||||||
|
['REPLACE', '<%:REPLACE%>'],
|
||||||
|
['REPLACE-PART', '<%:REPLACE-PART%>'],
|
||||||
|
['DIRECT', '<%:DIRECT%>'],
|
||||||
|
['DROP', '<%:DROP%>']
|
||||||
|
];
|
||||||
|
actions.forEach(function(s) {
|
||||||
|
var option = document.createElement('option');
|
||||||
|
option.value = s[0];
|
||||||
|
option.textContent = s[1];
|
||||||
|
if (rule && rule.action === s[0]) option.selected = true;
|
||||||
|
actionSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
actionSelect.onchange = function() { rewriteRules.updateRewriteValueVisibility(); };
|
||||||
|
actionField.appendChild(actionSelect);
|
||||||
|
actionValue.appendChild(actionField);
|
||||||
|
section.appendChild(actionValue);
|
||||||
|
|
||||||
|
// Rewrite Value
|
||||||
|
var rewriteValue = document.createElement('div');
|
||||||
|
rewriteValue.className = 'cbi-value';
|
||||||
|
rewriteValue.id = 'rewrite_value_container';
|
||||||
|
var rewriteLabel = document.createElement('label');
|
||||||
|
rewriteLabel.className = 'cbi-value-title';
|
||||||
|
rewriteLabel.textContent = '<%:Rewrite Value%>';
|
||||||
|
rewriteValue.appendChild(rewriteLabel);
|
||||||
|
var rewriteField = document.createElement('div');
|
||||||
|
rewriteField.className = 'cbi-value-field';
|
||||||
|
var rewriteInput = document.createElement('input');
|
||||||
|
rewriteInput.type = 'text';
|
||||||
|
rewriteInput.className = 'cbi-input-text';
|
||||||
|
rewriteInput.id = 'modal_rewrite_value';
|
||||||
|
rewriteInput.placeholder = '<%:Enter rewrite value%>';
|
||||||
|
if (rule) rewriteInput.value = rule.rewrite_value || '';
|
||||||
|
rewriteField.appendChild(rewriteInput);
|
||||||
|
rewriteValue.appendChild(rewriteField);
|
||||||
|
section.appendChild(rewriteValue);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
var descValue = document.createElement('div');
|
||||||
|
descValue.className = 'cbi-value';
|
||||||
|
var descLabel = document.createElement('label');
|
||||||
|
descLabel.className = 'cbi-value-title';
|
||||||
|
descLabel.textContent = '<%:Description%> (<%:Optional%>)';
|
||||||
|
descValue.appendChild(descLabel);
|
||||||
|
var descField = document.createElement('div');
|
||||||
|
descField.className = 'cbi-value-field';
|
||||||
|
var descInput = document.createElement('input');
|
||||||
|
descInput.type = 'text';
|
||||||
|
descInput.className = 'cbi-input-text';
|
||||||
|
descInput.id = 'modal_rule_description';
|
||||||
|
descInput.placeholder = '<%:Enter rule description%>';
|
||||||
|
if (rule && rule.description) descInput.value = rule.description;
|
||||||
|
descField.appendChild(descInput);
|
||||||
|
descValue.appendChild(descField);
|
||||||
|
section.appendChild(descValue);
|
||||||
|
|
||||||
|
body.appendChild(section);
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
var btnDiv = document.createElement('div');
|
||||||
|
btnDiv.className = 'right';
|
||||||
|
var cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.type = 'button';
|
||||||
|
cancelBtn.className = 'cbi-button cbi-button-neutral';
|
||||||
|
cancelBtn.textContent = '<%:Cancel%>';
|
||||||
|
cancelBtn.onclick = function() { self.closeDialog(); };
|
||||||
|
btnDiv.appendChild(cancelBtn);
|
||||||
|
btnDiv.appendChild(document.createTextNode(' '));
|
||||||
|
var saveBtn = document.createElement('button');
|
||||||
|
saveBtn.type = 'button';
|
||||||
|
saveBtn.className = 'cbi-button cbi-button-positive';
|
||||||
|
saveBtn.textContent = '<%:Save%>';
|
||||||
|
saveBtn.onclick = function() { self.saveFromDialog(index); };
|
||||||
|
btnDiv.appendChild(saveBtn);
|
||||||
|
body.appendChild(btnDiv);
|
||||||
|
|
||||||
|
dialog.appendChild(body);
|
||||||
|
modal.appendChild(dialog);
|
||||||
|
|
||||||
|
modal.onclick = function(e) {
|
||||||
|
if (e.target === modal) self.closeDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
this.currentModal = modal;
|
||||||
|
|
||||||
|
// Initialize visibility
|
||||||
|
this.updateRewriteValueVisibility();
|
||||||
|
this.updateMatchValuePlaceholder();
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMatchValuePlaceholder: function() {
|
||||||
|
var typeSelect = document.getElementById('modal_rule_type');
|
||||||
|
var matchInput = document.getElementById('modal_match_value');
|
||||||
|
if (!typeSelect || !matchInput) return;
|
||||||
|
|
||||||
|
var placeholders = {
|
||||||
|
'KEYWORD': '<%:Mac%>',
|
||||||
|
'REGEX': '<%:Mac.*Chrome%>',
|
||||||
|
'IP-CIDR': '<%:10.0.0.0/8%>',
|
||||||
|
'SRC-IP': '<%:192.168.1.100%>',
|
||||||
|
'DEST-PORT': '<%:443%>'
|
||||||
|
};
|
||||||
|
matchInput.placeholder = placeholders[typeSelect.value] || '<%:Enter match value%>';
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRewriteValueVisibility: function() {
|
||||||
|
var actionSelect = document.getElementById('modal_action');
|
||||||
|
var rewriteValueContainer = document.getElementById('rewrite_value_container');
|
||||||
|
if (!actionSelect || !rewriteValueContainer) return;
|
||||||
|
|
||||||
|
var action = actionSelect.value;
|
||||||
|
// Show rewrite value only for REPLACE and REPLACE-PART actions
|
||||||
|
if (action === 'REPLACE' || action === 'REPLACE-PART') {
|
||||||
|
rewriteValueContainer.style.display = '';
|
||||||
|
} else {
|
||||||
|
rewriteValueContainer.style.display = 'none';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeDialog: function() {
|
||||||
|
if (this.currentModal) {
|
||||||
|
document.body.removeChild(this.currentModal);
|
||||||
|
this.currentModal = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveFromDialog: function(index) {
|
||||||
|
var isFinal = index >= 0 && this.rules[index].type === 'FINAL';
|
||||||
|
|
||||||
|
var newRule;
|
||||||
|
if (isFinal) {
|
||||||
|
// For FINAL rule, only allow changing action and rewrite_value
|
||||||
|
newRule = {
|
||||||
|
type: 'FINAL',
|
||||||
|
match_value: '',
|
||||||
|
action: document.getElementById('modal_action').value,
|
||||||
|
rewrite_value: document.getElementById('modal_rewrite_value').value,
|
||||||
|
description: document.getElementById('modal_rule_description').value,
|
||||||
|
enabled: this.rules[index].enabled
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
newRule = {
|
||||||
|
type: document.getElementById('modal_rule_type').value,
|
||||||
|
match_value: document.getElementById('modal_match_value').value,
|
||||||
|
action: document.getElementById('modal_action').value,
|
||||||
|
rewrite_value: document.getElementById('modal_rewrite_value').value,
|
||||||
|
description: document.getElementById('modal_rule_description').value,
|
||||||
|
enabled: index >= 0 ? this.rules[index].enabled : true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!newRule.match_value) {
|
||||||
|
alert('<%:Match value is required%>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rewrite value for REPLACE and REPLACE-PART actions
|
||||||
|
if ((newRule.action === 'REPLACE' || newRule.action === 'REPLACE-PART') && !newRule.rewrite_value) {
|
||||||
|
alert('<%:Rewrite value is required for REPLACE and REPLACE-PART actions%>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.rules[index] = newRule;
|
||||||
|
} else {
|
||||||
|
// Insert before FINAL rule
|
||||||
|
this.rules.splice(this.rules.length - 1, 0, newRule);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeDialog();
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '<%=luci.dispatcher.build_url("admin/services/ua3f/save_rules")%>');
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
rewriteRules.renderTable();
|
||||||
|
} else {
|
||||||
|
alert('<%:Failed to save rule%>');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify({ rules: this.rules }));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteRule: function(index) {
|
||||||
|
// Prevent deleting FINAL rule
|
||||||
|
if (this.isFinalRule(index)) {
|
||||||
|
alert('<%:FINAL rule cannot be deleted%>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('<%:Are you sure you want to delete this rule?%>')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rules.splice(index, 1);
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '<%=luci.dispatcher.build_url("admin/services/ua3f/save_rules")%>');
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
rewriteRules.renderTable();
|
||||||
|
} else {
|
||||||
|
alert('<%:Failed to delete rule%>');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify({ rules: this.rules }));
|
||||||
|
},
|
||||||
|
|
||||||
|
moveRuleUp: function(index) {
|
||||||
|
if (index > 0 && !this.isFinalRule(index)) {
|
||||||
|
var temp = this.rules[index];
|
||||||
|
this.rules[index] = this.rules[index - 1];
|
||||||
|
this.rules[index - 1] = temp;
|
||||||
|
this.saveAndRender();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
moveRuleDown: function(index) {
|
||||||
|
// Can't move down if it's FINAL or if next is FINAL
|
||||||
|
if (index < this.rules.length - 2 && !this.isFinalRule(index)) {
|
||||||
|
var temp = this.rules[index];
|
||||||
|
this.rules[index] = this.rules[index + 1];
|
||||||
|
this.rules[index + 1] = temp;
|
||||||
|
this.saveAndRender();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveAndRender: function() {
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '<%=luci.dispatcher.build_url("admin/services/ua3f/save_rules")%>');
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
rewriteRules.renderTable();
|
||||||
|
} else {
|
||||||
|
alert('<%:Failed to save rule%>');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify({ rules: this.rules }));
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleRuleEnabled: function(index, enabled) {
|
||||||
|
this.rules[index].enabled = enabled;
|
||||||
|
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '<%=luci.dispatcher.build_url("admin/services/ua3f/save_rules")%>');
|
||||||
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status !== 200) {
|
||||||
|
alert('<%:Failed to save rule%>');
|
||||||
|
// Revert checkbox state on failure
|
||||||
|
rewriteRules.rules[index].enabled = !enabled;
|
||||||
|
rewriteRules.renderTable();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr.send(JSON.stringify({ rules: this.rules }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rewriteRules.init();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -505,6 +505,7 @@ start_service() {
|
|||||||
LOG "Starting $NAME service..."
|
LOG "Starting $NAME service..."
|
||||||
|
|
||||||
local port bind ua log_level ua_regex partial_replace set_ttl direct_forward
|
local port bind ua log_level ua_regex partial_replace set_ttl direct_forward
|
||||||
|
local rewrite_mode rewrite_rules
|
||||||
config_get server_mode "main" "server_mode" "SOCKS5"
|
config_get server_mode "main" "server_mode" "SOCKS5"
|
||||||
config_get port "main" "port" "1080"
|
config_get port "main" "port" "1080"
|
||||||
config_get bind "main" "bind" "127.0.0.1"
|
config_get bind "main" "bind" "127.0.0.1"
|
||||||
@ -514,6 +515,8 @@ start_service() {
|
|||||||
config_get log_level "main" "log_level" "info"
|
config_get log_level "main" "log_level" "info"
|
||||||
config_get_bool set_ttl "main" "set_ttl" 0
|
config_get_bool set_ttl "main" "set_ttl" 0
|
||||||
config_get_bool direct_forward "main" "direct_forward" 0
|
config_get_bool direct_forward "main" "direct_forward" 0
|
||||||
|
config_get rewrite_mode "main" "rewrite_mode" "GLOBAL"
|
||||||
|
config_get rewrite_rules "main" "rewrite_rules" ""
|
||||||
|
|
||||||
SERVER_MODE="$(echo "$server_mode" | tr '[:lower:]' '[:upper:]')"
|
SERVER_MODE="$(echo "$server_mode" | tr '[:lower:]' '[:upper:]')"
|
||||||
SERVER_MODE="$server_mode"
|
SERVER_MODE="$server_mode"
|
||||||
@ -645,6 +648,8 @@ start_service() {
|
|||||||
procd_append_param command -f "$ua"
|
procd_append_param command -f "$ua"
|
||||||
procd_append_param command -r "$ua_regex"
|
procd_append_param command -r "$ua_regex"
|
||||||
procd_append_param command -l "$log_level"
|
procd_append_param command -l "$log_level"
|
||||||
|
procd_append_param command -x "$rewrite_mode"
|
||||||
|
procd_append_param command -z "$rewrite_rules"
|
||||||
[ "$partial_replace" = "1" ] && procd_append_param command -s
|
[ "$partial_replace" = "1" ] && procd_append_param command -s
|
||||||
[ "$direct_forward" = "1" ] && procd_append_param command -d
|
[ "$direct_forward" = "1" ] && procd_append_param command -d
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ config 'ua3f' 'main'
|
|||||||
option server_mode 'SOCKS5'
|
option server_mode 'SOCKS5'
|
||||||
option port '1080'
|
option port '1080'
|
||||||
option bind '0.0.0.0'
|
option bind '0.0.0.0'
|
||||||
|
option rewrite_mode 'GLOBAL'
|
||||||
option ua 'FFF'
|
option ua 'FFF'
|
||||||
option ua_regex '(Apple|iPhone|iPad|Macintosh|Mac OS X|Mac|Darwin|Microsoft|Windows|Linux|Android|OpenHarmony|HUAWEI|OPPO|Vivo|XiaoMi|Mobile|Dalvik)'
|
option ua_regex '(Apple|iPhone|iPad|Macintosh|Mac OS X|Mac|Darwin|Microsoft|Windows|Linux|Android|OpenHarmony|HUAWEI|OPPO|Vivo|XiaoMi|Mobile|Dalvik)'
|
||||||
option partial_replace '0'
|
option partial_replace '0'
|
||||||
@ -12,3 +13,4 @@ config 'ua3f' 'main'
|
|||||||
option log_level 'error'
|
option log_level 'error'
|
||||||
option log_lines '1000'
|
option log_lines '1000'
|
||||||
option set_ttl '0'
|
option set_ttl '0'
|
||||||
|
option rewrite_rules '[{"enabled":false,"type":"DEST-PORT","action":"DIRECT","match_value":"443","description":"","rewrite_value":""},{"enabled":true,"type":"KEYWORD","action":"DIRECT","match_value":"MicroMessenger Client","description":"","rewrite_value":""},{"enabled":true,"type":"KEYWORD","action":"DIRECT","match_value":"Bilibili Freedoooooom\/MarkII","description":"","rewrite_value":""},{"enabled":true,"type":"KEYWORD","action":"DIRECT","match_value":"Valve\/Steam HTTP Client 1.0","description":"","rewrite_value":""},{"enabled":true,"type":"KEYWORD","action":"REPLACE","match_value":"Mac","description":"","rewrite_value":"FFF"},{"enabled":true,"type":"REGEX","action":"REPLACE","match_value":"(Apple|iPhone|iPad|Macintosh|Mac OS X|Mac|Darwin|Microsoft|Windows|Linux|Android|OpenHarmony|HUAWEI|OPPO|Vivo|XiaoMi|Mobile|Dalvik)","rewrite_value":"FFF","description":""},{"enabled":true,"type":"FINAL","action":"REPLACE","match_value":"","description":"Default Fallback Rule","rewrite_value":"FFF"}]'
|
||||||
@ -2,6 +2,9 @@
|
|||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr "状态"
|
msgstr "状态"
|
||||||
|
|
||||||
@ -18,11 +21,14 @@ msgid "Stopped"
|
|||||||
msgstr "未运行"
|
msgstr "未运行"
|
||||||
|
|
||||||
msgid "Settings"
|
msgid "Settings"
|
||||||
msgstr "设置"
|
msgstr "通用设置"
|
||||||
|
|
||||||
msgid "Statistics"
|
msgid "Statistics"
|
||||||
msgstr "统计信息"
|
msgstr "统计信息"
|
||||||
|
|
||||||
|
msgid "Rewrite Rules"
|
||||||
|
msgstr "重写规则"
|
||||||
|
|
||||||
msgid "Log"
|
msgid "Log"
|
||||||
msgstr "运行日志"
|
msgstr "运行日志"
|
||||||
|
|
||||||
@ -38,6 +44,21 @@ msgstr "绑定地址"
|
|||||||
msgid "Log Level"
|
msgid "Log Level"
|
||||||
msgstr "日志级别"
|
msgstr "日志级别"
|
||||||
|
|
||||||
|
msgid "Rewrite Mode"
|
||||||
|
msgstr "重写策略"
|
||||||
|
|
||||||
|
msgid "Direct Forward"
|
||||||
|
msgstr "直接转发"
|
||||||
|
|
||||||
|
msgid "Global Rewrite"
|
||||||
|
msgstr "全局重写"
|
||||||
|
|
||||||
|
msgid "Rule Based"
|
||||||
|
msgstr "规则判定"
|
||||||
|
|
||||||
|
msgid "Direct Forward: No rewriting. Global Rewrite: Rewrite all User-Agents to the specified value. Rule Based: Use rewrite rules to determine behavior."
|
||||||
|
msgstr "直接转发:不进行任何重写。全局重写:将所有 User-Agent 重写为指定值。规则判定:根据重写规则判定行为。"
|
||||||
|
|
||||||
msgid "User-Agent"
|
msgid "User-Agent"
|
||||||
msgstr "User-Agent"
|
msgstr "User-Agent"
|
||||||
|
|
||||||
@ -127,3 +148,105 @@ msgstr "持续时间"
|
|||||||
|
|
||||||
msgid "Total Connections"
|
msgid "Total Connections"
|
||||||
msgstr "连接总数"
|
msgstr "连接总数"
|
||||||
|
|
||||||
|
msgid "Add Rule"
|
||||||
|
msgstr "添加规则"
|
||||||
|
|
||||||
|
msgid "Index"
|
||||||
|
msgstr "序号"
|
||||||
|
|
||||||
|
msgid "Rule Type"
|
||||||
|
msgstr "规则类型"
|
||||||
|
|
||||||
|
msgid "Match Value"
|
||||||
|
msgstr "匹配值"
|
||||||
|
|
||||||
|
msgid "Rewrite Action"
|
||||||
|
msgstr "重写策略"
|
||||||
|
|
||||||
|
msgid "Rewrite Value"
|
||||||
|
msgstr "重写值"
|
||||||
|
|
||||||
|
msgid "Pattern"
|
||||||
|
msgstr "匹配模式"
|
||||||
|
|
||||||
|
msgid "Replacement"
|
||||||
|
msgstr "替换内容"
|
||||||
|
|
||||||
|
msgid "Actions"
|
||||||
|
msgstr "操作"
|
||||||
|
|
||||||
|
msgid "No rules configured. Click「Add Rule」to create one"
|
||||||
|
msgstr "暂无配置规则,点击「添加规则」创建新规则"
|
||||||
|
|
||||||
|
msgid "Add Rewrite Rule"
|
||||||
|
msgstr "添加重写规则"
|
||||||
|
|
||||||
|
msgid "Edit Rewrite Rule"
|
||||||
|
msgstr "编辑重写规则"
|
||||||
|
|
||||||
|
msgid "Enter match value"
|
||||||
|
msgstr "输入匹配值"
|
||||||
|
|
||||||
|
msgid "Enter rewrite value"
|
||||||
|
msgstr "输入重写值"
|
||||||
|
|
||||||
|
msgid "Description"
|
||||||
|
msgstr "描述"
|
||||||
|
|
||||||
|
msgid "Optional"
|
||||||
|
msgstr "可选"
|
||||||
|
|
||||||
|
msgid "Enter rule description"
|
||||||
|
msgstr "输入规则描述"
|
||||||
|
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "取消"
|
||||||
|
|
||||||
|
msgid "Save"
|
||||||
|
msgstr "保存"
|
||||||
|
|
||||||
|
msgid "Edit"
|
||||||
|
msgstr "编辑"
|
||||||
|
|
||||||
|
msgid "Move Up"
|
||||||
|
msgstr "上移"
|
||||||
|
|
||||||
|
msgid "Move Down"
|
||||||
|
msgstr "下移"
|
||||||
|
|
||||||
|
msgid "Delete"
|
||||||
|
msgstr "删除"
|
||||||
|
|
||||||
|
msgid "Are you sure you want to delete this rule?"
|
||||||
|
msgstr "确定要删除此规则吗?"
|
||||||
|
|
||||||
|
msgid "Failed to delete rule"
|
||||||
|
msgstr "删除规则失败"
|
||||||
|
|
||||||
|
msgid "Failed to move rule"
|
||||||
|
msgstr "移动规则失败"
|
||||||
|
|
||||||
|
msgid "Failed to save rule"
|
||||||
|
msgstr "保存规则失败"
|
||||||
|
|
||||||
|
msgid "Match value is required"
|
||||||
|
msgstr "匹配值为必填项"
|
||||||
|
|
||||||
|
msgid "Rewrite value is required for REPLACE and REPLACE-PART strategies"
|
||||||
|
msgstr "REPLACE 和 REPLACE-PART 策略需要填写重写值"
|
||||||
|
|
||||||
|
msgid "Pattern and Replacement are required"
|
||||||
|
msgstr "匹配模式和替换内容为必填项"
|
||||||
|
|
||||||
|
msgid "Rule saved successfully"
|
||||||
|
msgstr "规则保存成功"
|
||||||
|
|
||||||
|
msgid "Rule deleted successfully"
|
||||||
|
msgstr "规则删除成功"
|
||||||
|
|
||||||
|
msgid "Confirm"
|
||||||
|
msgstr "确认"
|
||||||
|
|
||||||
|
msgid "Note: Rewrite rules only take effect in rule mode. NFQUEUE mode is temporarily unavailable."
|
||||||
|
msgstr "注意:重写规则仅在规则判定模式下生效。NFQUEUE 模式暂不可用。"
|
||||||
@ -3,65 +3,78 @@ package config
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ServerMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ServerModeHTTP = "HTTP"
|
ServerModeHTTP ServerMode = "HTTP"
|
||||||
ServerModeSocks5 = "SOCKS5"
|
ServerModeSocks5 ServerMode = "SOCKS5"
|
||||||
ServerModeTProxy = "TPROXY"
|
ServerModeTProxy ServerMode = "TPROXY"
|
||||||
ServerModeRedirect = "REDIRECT"
|
ServerModeRedirect ServerMode = "REDIRECT"
|
||||||
ServerModeNFQueue = "NFQUEUE"
|
ServerModeNFQueue ServerMode = "NFQUEUE"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RewriteMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RewriteModeGlobal RewriteMode = "GLOBAL"
|
||||||
|
RewriteModeDirect RewriteMode = "DIRECT"
|
||||||
|
RewriteModeRules RewriteMode = "RULES"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ServerMode string
|
ServerMode ServerMode
|
||||||
BindAddr string
|
BindAddr string
|
||||||
Port int
|
Port int
|
||||||
ListenAddr string
|
ListenAddr string
|
||||||
LogLevel string
|
LogLevel string
|
||||||
|
RewriteMode RewriteMode
|
||||||
|
Rules string
|
||||||
PayloadUA string
|
PayloadUA string
|
||||||
UARegex string
|
UARegex string
|
||||||
PartialReplace bool
|
PartialReplace bool
|
||||||
DirectForward bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Parse() (*Config, bool) {
|
func Parse() (*Config, bool) {
|
||||||
var (
|
var (
|
||||||
serverMode string
|
serverMode string
|
||||||
bindAddr string
|
bindAddr string
|
||||||
port int
|
port int
|
||||||
loglevel string
|
loglevel string
|
||||||
payloadUA string
|
payloadUA string
|
||||||
uaRegx string
|
uaRegx string
|
||||||
partial bool
|
partial bool
|
||||||
directForward bool
|
rewriteMode string
|
||||||
showVer bool
|
rules string
|
||||||
|
showVer bool
|
||||||
)
|
)
|
||||||
|
|
||||||
flag.StringVar(&serverMode, "m", ServerModeSocks5, "Server mode: HTTP, SOCKS5, TPROXY, REDIRECT, NFQUEUE")
|
flag.StringVar(&serverMode, "m", string(ServerModeSocks5), "Server mode: HTTP, SOCKS5, TPROXY, REDIRECT, NFQUEUE")
|
||||||
flag.StringVar(&bindAddr, "b", "127.0.0.1", "Bind address")
|
flag.StringVar(&bindAddr, "b", "127.0.0.1", "Bind address")
|
||||||
flag.IntVar(&port, "p", 1080, "Port")
|
flag.IntVar(&port, "p", 1080, "Port")
|
||||||
flag.StringVar(&loglevel, "l", "info", "Log level")
|
flag.StringVar(&loglevel, "l", "info", "Log level")
|
||||||
flag.StringVar(&payloadUA, "f", "FFF", "User-Agent")
|
flag.StringVar(&payloadUA, "f", "FFF", "User-Agent")
|
||||||
flag.StringVar(&uaRegx, "r", "", "User-Agent regex")
|
flag.StringVar(&uaRegx, "r", "", "User-Agent regex")
|
||||||
flag.BoolVar(&partial, "s", false, "Enable regex partial replace")
|
flag.BoolVar(&partial, "s", false, "Enable regex partial replace")
|
||||||
flag.BoolVar(&directForward, "d", false, "Pure Forwarding (no User-Agent rewriting)")
|
flag.StringVar(&rewriteMode, "x", string(RewriteModeGlobal), "Rewrite mode: GLOBAL, DIRECT, RULES")
|
||||||
|
flag.StringVar(&rules, "z", "", "Rules JSON string")
|
||||||
flag.BoolVar(&showVer, "v", false, "Show version")
|
flag.BoolVar(&showVer, "v", false, "Show version")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
ServerMode: strings.ToUpper(serverMode),
|
ServerMode: ServerMode(serverMode),
|
||||||
BindAddr: bindAddr,
|
BindAddr: bindAddr,
|
||||||
Port: port,
|
Port: port,
|
||||||
ListenAddr: fmt.Sprintf("%s:%d", bindAddr, port),
|
ListenAddr: fmt.Sprintf("%s:%d", bindAddr, port),
|
||||||
LogLevel: loglevel,
|
LogLevel: loglevel,
|
||||||
PayloadUA: payloadUA,
|
PayloadUA: payloadUA,
|
||||||
UARegex: uaRegx,
|
UARegex: uaRegx,
|
||||||
DirectForward: directForward,
|
|
||||||
PartialReplace: partial,
|
PartialReplace: partial,
|
||||||
|
RewriteMode: RewriteMode(rewriteMode),
|
||||||
|
Rules: rules,
|
||||||
}
|
}
|
||||||
if serverMode == ServerModeRedirect {
|
if cfg.ServerMode == ServerModeRedirect || cfg.ServerMode == ServerModeTProxy {
|
||||||
cfg.BindAddr = "0.0.0.0"
|
cfg.BindAddr = "0.0.0.0"
|
||||||
cfg.ListenAddr = fmt.Sprintf("0.0.0.0:%d", port)
|
cfg.ListenAddr = fmt.Sprintf("0.0.0.0:%d", port)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,10 +72,11 @@ func LogHeader(version string, cfg *config.Config) {
|
|||||||
logrus.Info("UA3F v" + version)
|
logrus.Info("UA3F v" + version)
|
||||||
logrus.Info("Server Mode: " + cfg.ServerMode)
|
logrus.Info("Server Mode: " + cfg.ServerMode)
|
||||||
logrus.Infof("Listen on %s", cfg.ListenAddr)
|
logrus.Infof("Listen on %s", cfg.ListenAddr)
|
||||||
|
logrus.Infof("Rewrite Mode: %s", cfg.RewriteMode)
|
||||||
|
logrus.Infof("Rewrite Rules: %s", cfg.Rules)
|
||||||
logrus.Infof("User-Agent: %s", cfg.PayloadUA)
|
logrus.Infof("User-Agent: %s", cfg.PayloadUA)
|
||||||
logrus.Infof("User-Agent Regex: '%s'", cfg.UARegex)
|
logrus.Infof("User-Agent Regex: '%s'", cfg.UARegex)
|
||||||
logrus.Infof("Partial Replace: %v", cfg.PartialReplace)
|
logrus.Infof("Partial Replace: %v", cfg.PartialReplace)
|
||||||
logrus.Infof("Direct Forward: %v", cfg.DirectForward)
|
|
||||||
logrus.Infof("Log level: %s", cfg.LogLevel)
|
logrus.Infof("Log level: %s", cfg.LogLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/sunbk201/ua3f/internal/config"
|
"github.com/sunbk201/ua3f/internal/config"
|
||||||
"github.com/sunbk201/ua3f/internal/log"
|
"github.com/sunbk201/ua3f/internal/log"
|
||||||
|
"github.com/sunbk201/ua3f/internal/rule"
|
||||||
"github.com/sunbk201/ua3f/internal/sniff"
|
"github.com/sunbk201/ua3f/internal/sniff"
|
||||||
"github.com/sunbk201/ua3f/internal/statistics"
|
"github.com/sunbk201/ua3f/internal/statistics"
|
||||||
)
|
)
|
||||||
@ -26,10 +27,25 @@ type Rewriter struct {
|
|||||||
payloadUA string
|
payloadUA string
|
||||||
pattern string
|
pattern string
|
||||||
partialReplace bool
|
partialReplace bool
|
||||||
|
rewriteMode config.RewriteMode
|
||||||
|
|
||||||
uaRegex *regexp2.Regexp
|
uaRegex *regexp2.Regexp
|
||||||
whitelist []string
|
ruleEngine *rule.Engine
|
||||||
Cache *expirable.LRU[string, struct{}]
|
whitelist []string
|
||||||
|
Cache *expirable.LRU[string, struct{}]
|
||||||
|
}
|
||||||
|
|
||||||
|
// RewriteDecision 重写决策结果
|
||||||
|
type RewriteDecision struct {
|
||||||
|
Action rule.Action
|
||||||
|
MatchedRule *rule.Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldRewrite 判断是否需要重写
|
||||||
|
func (d *RewriteDecision) ShouldRewrite() bool {
|
||||||
|
return d.Action == rule.ActionReplace ||
|
||||||
|
d.Action == rule.ActionReplacePart ||
|
||||||
|
d.Action == rule.ActionDelete
|
||||||
}
|
}
|
||||||
|
|
||||||
// New constructs a Rewriter from config. Compiles regex and allocates cache.
|
// New constructs a Rewriter from config. Compiles regex and allocates cache.
|
||||||
@ -41,11 +57,23 @@ func New(cfg *config.Config) (*Rewriter, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建规则引擎
|
||||||
|
var ruleEngine *rule.Engine
|
||||||
|
if cfg.RewriteMode == config.RewriteModeRules {
|
||||||
|
ruleEngine, err = rule.NewEngine(cfg.Rules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create rule engine: %w", err)
|
||||||
|
}
|
||||||
|
logrus.Info("Rule engine initialized")
|
||||||
|
}
|
||||||
|
|
||||||
return &Rewriter{
|
return &Rewriter{
|
||||||
payloadUA: cfg.PayloadUA,
|
payloadUA: cfg.PayloadUA,
|
||||||
pattern: cfg.UARegex,
|
pattern: cfg.UARegex,
|
||||||
partialReplace: cfg.PartialReplace,
|
partialReplace: cfg.PartialReplace,
|
||||||
|
rewriteMode: cfg.RewriteMode,
|
||||||
uaRegex: uaRegex,
|
uaRegex: uaRegex,
|
||||||
|
ruleEngine: ruleEngine,
|
||||||
Cache: expirable.NewLRU[string, struct{}](1024, nil, 30*time.Minute),
|
Cache: expirable.NewLRU[string, struct{}](1024, nil, 30*time.Minute),
|
||||||
whitelist: []string{
|
whitelist: []string{
|
||||||
"MicroMessenger Client",
|
"MicroMessenger Client",
|
||||||
@ -66,6 +94,11 @@ func (r *Rewriter) inWhitelist(ua string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRuleEngine 获取规则引擎
|
||||||
|
func (r *Rewriter) GetRuleEngine() *rule.Engine {
|
||||||
|
return r.ruleEngine
|
||||||
|
}
|
||||||
|
|
||||||
// buildUserAgent returns either a partial replacement (regex) or full overwrite.
|
// buildUserAgent returns either a partial replacement (regex) or full overwrite.
|
||||||
func (r *Rewriter) buildUserAgent(originUA string) string {
|
func (r *Rewriter) buildUserAgent(originUA string) string {
|
||||||
if r.partialReplace && r.uaRegex != nil && r.pattern != "" {
|
if r.partialReplace && r.uaRegex != nil && r.pattern != "" {
|
||||||
@ -79,13 +112,74 @@ func (r *Rewriter) buildUserAgent(originUA string) string {
|
|||||||
return r.payloadUA
|
return r.payloadUA
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rewriter) ShouldRewrite(req *http.Request, srcAddr, destAddr string) bool {
|
func (r *Rewriter) EvaluateRewriteDecision(req *http.Request, srcAddr, destAddr string) *RewriteDecision {
|
||||||
originalUA := req.Header.Get("User-Agent")
|
originalUA := req.Header.Get("User-Agent")
|
||||||
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("original User-Agent: (%s)", originalUA))
|
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("original User-Agent: (%s)", originalUA))
|
||||||
if originalUA == "" {
|
if originalUA == "" {
|
||||||
req.Header.Set("User-Agent", "")
|
req.Header.Set("User-Agent", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 「直接转发」模式:不进行任何重写
|
||||||
|
if r.rewriteMode == config.RewriteModeDirect {
|
||||||
|
log.LogDebugWithAddr(srcAddr, destAddr, "Direct forward mode, skip rewriting")
|
||||||
|
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
UA: originalUA,
|
||||||
|
})
|
||||||
|
return &RewriteDecision{
|
||||||
|
Action: rule.ActionDirect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 「规则判定」模式:使用规则引擎(只匹配一次)
|
||||||
|
if r.rewriteMode == config.RewriteModeRules && r.ruleEngine != nil {
|
||||||
|
matchedRule := r.ruleEngine.MatchWithRule(req, srcAddr, destAddr)
|
||||||
|
|
||||||
|
// 没有匹配到任何规则,默认直接转发
|
||||||
|
if matchedRule == nil {
|
||||||
|
log.LogDebugWithAddr(srcAddr, destAddr, "No rule matched, direct forward")
|
||||||
|
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
UA: originalUA,
|
||||||
|
})
|
||||||
|
return &RewriteDecision{
|
||||||
|
Action: rule.ActionDirect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DROP 动作:丢弃请求
|
||||||
|
if matchedRule.Action == rule.ActionDrop {
|
||||||
|
log.LogInfoWithAddr(srcAddr, destAddr, "Rule matched: DROP action, request will be dropped")
|
||||||
|
return &RewriteDecision{
|
||||||
|
Action: matchedRule.Action,
|
||||||
|
MatchedRule: matchedRule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DIRECT 动作:直接转发
|
||||||
|
if matchedRule.Action == rule.ActionDirect {
|
||||||
|
log.LogDebugWithAddr(srcAddr, destAddr, "Rule matched: DIRECT action, skip rewriting")
|
||||||
|
statistics.AddPassThroughRecord(&statistics.PassThroughRecord{
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
DestAddr: destAddr,
|
||||||
|
UA: originalUA,
|
||||||
|
})
|
||||||
|
return &RewriteDecision{
|
||||||
|
Action: matchedRule.Action,
|
||||||
|
MatchedRule: matchedRule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// REPLACE、REPLACE-PART、DELETE 动作:需要重写
|
||||||
|
return &RewriteDecision{
|
||||||
|
Action: matchedRule.Action,
|
||||||
|
MatchedRule: matchedRule,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 「全局重写」模式:使用原有逻辑
|
||||||
var err error
|
var err error
|
||||||
matches := false
|
matches := false
|
||||||
isWhitelist := r.inWhitelist(originalUA)
|
isWhitelist := r.inWhitelist(originalUA)
|
||||||
@ -117,13 +211,29 @@ func (r *Rewriter) ShouldRewrite(req *http.Request, srcAddr, destAddr string) bo
|
|||||||
DestAddr: destAddr,
|
DestAddr: destAddr,
|
||||||
UA: originalUA,
|
UA: originalUA,
|
||||||
})
|
})
|
||||||
|
return &RewriteDecision{
|
||||||
|
Action: rule.ActionDirect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &RewriteDecision{
|
||||||
|
Action: rule.ActionReplace,
|
||||||
}
|
}
|
||||||
return hit
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Rewriter) Rewrite(req *http.Request, srcAddr string, destAddr string) *http.Request {
|
func (r *Rewriter) Rewrite(req *http.Request, srcAddr string, destAddr string, decision *RewriteDecision) *http.Request {
|
||||||
originalUA := req.Header.Get("User-Agent")
|
originalUA := req.Header.Get("User-Agent")
|
||||||
rewritedUA := r.buildUserAgent(originalUA)
|
rewriteValue := decision.MatchedRule.RewriteValue
|
||||||
|
action := decision.Action
|
||||||
|
var rewritedUA string
|
||||||
|
|
||||||
|
// 「规则判定」模式:根据规则动作决定如何重写
|
||||||
|
if r.rewriteMode == config.RewriteModeRules && r.ruleEngine != nil {
|
||||||
|
rewritedUA = r.ruleEngine.ApplyAction(action, rewriteValue, originalUA, decision.MatchedRule)
|
||||||
|
} else {
|
||||||
|
// 「全局重写」模式:使用原有逻辑
|
||||||
|
rewritedUA = r.buildUserAgent(originalUA)
|
||||||
|
}
|
||||||
|
|
||||||
req.Header.Set("User-Agent", rewritedUA)
|
req.Header.Set("User-Agent", rewritedUA)
|
||||||
|
|
||||||
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Rewrite User-Agent from (%s) to (%s)", originalUA, rewritedUA))
|
log.LogInfoWithAddr(srcAddr, destAddr, fmt.Sprintf("Rewrite User-Agent from (%s) to (%s)", originalUA, rewritedUA))
|
||||||
@ -217,9 +327,19 @@ func (r *Rewriter) Process(dst net.Conn, src net.Conn, destAddr string, srcAddr
|
|||||||
err = fmt.Errorf("http.ReadRequest: %w", err)
|
err = fmt.Errorf("http.ReadRequest: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if r.ShouldRewrite(req, srcAddr, destAddr) {
|
|
||||||
req = r.Rewrite(req, srcAddr, destAddr)
|
// 获取重写决策(只匹配一次规则)
|
||||||
|
decision := r.EvaluateRewriteDecision(req, srcAddr, destAddr)
|
||||||
|
// 处理 DROP 动作
|
||||||
|
if decision.Action == rule.ActionDrop {
|
||||||
|
log.LogInfoWithAddr(srcAddr, destAddr, "Request dropped by rule")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
// 如果需要重写,执行重写操作
|
||||||
|
if decision.ShouldRewrite() {
|
||||||
|
req = r.Rewrite(req, srcAddr, destAddr, decision)
|
||||||
|
}
|
||||||
|
|
||||||
if err = r.Forward(dst, req); err != nil {
|
if err = r.Forward(dst, req); err != nil {
|
||||||
err = fmt.Errorf("r.Forward: %w", err)
|
err = fmt.Errorf("r.Forward: %w", err)
|
||||||
return
|
return
|
||||||
|
|||||||
246
src/internal/rule/rule.go
Normal file
246
src/internal/rule/rule.go
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dlclark/regexp2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RuleType 规则类型
|
||||||
|
type RuleType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RuleTypeKeyword RuleType = "KEYWORD" // 关键字匹配
|
||||||
|
RuleTypeRegex RuleType = "REGEX" // 正则表达式匹配
|
||||||
|
RuleTypeIPCIDR RuleType = "IP-CIDR" // IP地址段匹配
|
||||||
|
RuleTypeSrcIP RuleType = "SRC-IP" // 源IP地址匹配
|
||||||
|
RuleTypeDestPort RuleType = "DEST-PORT" // 目标端口匹配
|
||||||
|
RuleTypeFinal RuleType = "FINAL" // 兜底规则
|
||||||
|
)
|
||||||
|
|
||||||
|
// Action 重写策略
|
||||||
|
type Action string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionReplace Action = "REPLACE" // 替换整个 User-Agent
|
||||||
|
ActionReplacePart Action = "REPLACE-PART" // 部分替换
|
||||||
|
ActionDelete Action = "DELETE" // 删除 User-Agent
|
||||||
|
ActionDirect Action = "DIRECT" // 直接转发
|
||||||
|
ActionDrop Action = "DROP" // 丢弃请求
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rule 重写规则
|
||||||
|
type Rule struct {
|
||||||
|
Enabled bool `json:"enabled"` // 是否启用
|
||||||
|
Type RuleType `json:"type"` // 规则类型
|
||||||
|
Action Action `json:"action"` // 重写策略
|
||||||
|
MatchValue string `json:"match_value"` // 匹配值
|
||||||
|
RewriteValue string `json:"rewrite_value"` // 重写值
|
||||||
|
Description string `json:"description"` // 描述
|
||||||
|
|
||||||
|
// 编译后的正则表达式(仅用于 REGEX 类型)
|
||||||
|
regex *regexp2.Regexp
|
||||||
|
// 解析后的 IP 网络(仅用于 IP-CIDR 和 SRC-IP 类型)
|
||||||
|
ipNet *net.IPNet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine 规则引擎
|
||||||
|
type Engine struct {
|
||||||
|
rules []*Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEngine 创建规则引擎
|
||||||
|
func NewEngine(rulesJSON string) (*Engine, error) {
|
||||||
|
if rulesJSON == "" {
|
||||||
|
return &Engine{rules: []*Rule{}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rules []*Rule
|
||||||
|
if err := json.Unmarshal([]byte(rulesJSON), &rules); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse rules JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译正则表达式和解析 IP 网络
|
||||||
|
for _, rule := range rules {
|
||||||
|
if !rule.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch rule.Type {
|
||||||
|
case RuleTypeRegex:
|
||||||
|
if rule.MatchValue != "" {
|
||||||
|
pattern := "(?i)" + rule.MatchValue
|
||||||
|
regex, err := regexp2.Compile(pattern, regexp2.None)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("Failed to compile regex for rule: %s, error: %v", rule.Description, err)
|
||||||
|
rule.Enabled = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rule.regex = regex
|
||||||
|
}
|
||||||
|
case RuleTypeIPCIDR, RuleTypeSrcIP:
|
||||||
|
if rule.MatchValue != "" {
|
||||||
|
if !strings.Contains(rule.MatchValue, "/") {
|
||||||
|
rule.MatchValue += "/32"
|
||||||
|
}
|
||||||
|
_, ipNet, err := net.ParseCIDR(rule.MatchValue)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("Failed to parse CIDR for rule: %s, error: %v", rule.Description, err)
|
||||||
|
rule.Enabled = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rule.ipNet = ipNet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Engine{rules: rules}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchWithRule 匹配规则并返回匹配的规则
|
||||||
|
func (e *Engine) MatchWithRule(req *http.Request, srcAddr, destAddr string) *Rule {
|
||||||
|
for _, rule := range e.rules {
|
||||||
|
if !rule.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matched := false
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch rule.Type {
|
||||||
|
case RuleTypeKeyword:
|
||||||
|
matched = e.matchKeyword(req, rule)
|
||||||
|
case RuleTypeRegex:
|
||||||
|
matched, err = e.matchRegex(req, rule)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnf("Regex match error: %v", err)
|
||||||
|
}
|
||||||
|
case RuleTypeIPCIDR:
|
||||||
|
matched = e.matchIPCIDR(destAddr, rule)
|
||||||
|
case RuleTypeSrcIP:
|
||||||
|
matched = e.matchSrcIP(srcAddr, rule)
|
||||||
|
case RuleTypeDestPort:
|
||||||
|
matched = e.matchDestPort(destAddr, rule)
|
||||||
|
case RuleTypeFinal:
|
||||||
|
matched = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
logrus.Debugf("Rule matched: %s (type: %s, action: %s)", rule.Description, rule.Type, rule.Action)
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchKeyword 关键字匹配
|
||||||
|
func (e *Engine) matchKeyword(req *http.Request, rule *Rule) bool {
|
||||||
|
ua := req.Header.Get("User-Agent")
|
||||||
|
return strings.Contains(strings.ToLower(ua), strings.ToLower(rule.MatchValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchRegex 正则表达式匹配
|
||||||
|
func (e *Engine) matchRegex(req *http.Request, rule *Rule) (bool, error) {
|
||||||
|
if rule.regex == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
ua := req.Header.Get("User-Agent")
|
||||||
|
return rule.regex.MatchString(ua)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchIPCIDR 目标IP地址段匹配
|
||||||
|
func (e *Engine) matchIPCIDR(destAddr string, rule *Rule) bool {
|
||||||
|
if rule.ipNet == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(destAddr)
|
||||||
|
if err != nil {
|
||||||
|
host = destAddr
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return rule.ipNet.Contains(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchSrcIP 源IP地址匹配
|
||||||
|
func (e *Engine) matchSrcIP(srcAddr string, rule *Rule) bool {
|
||||||
|
if rule.ipNet == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
host, _, err := net.SplitHostPort(srcAddr)
|
||||||
|
if err != nil {
|
||||||
|
host = srcAddr
|
||||||
|
}
|
||||||
|
ip := net.ParseIP(host)
|
||||||
|
if ip == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return rule.ipNet.Contains(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// matchDestPort 目标端口匹配
|
||||||
|
func (e *Engine) matchDestPort(destAddr string, rule *Rule) bool {
|
||||||
|
_, portStr, err := net.SplitHostPort(destAddr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
matchPort, err := strconv.Atoi(rule.MatchValue)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return port == matchPort
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyAction 应用规则动作
|
||||||
|
func (e *Engine) ApplyAction(action Action, rewriteValue string, originalUA string, rule *Rule) string {
|
||||||
|
switch action {
|
||||||
|
case ActionReplace:
|
||||||
|
return rewriteValue
|
||||||
|
case ActionReplacePart:
|
||||||
|
if rule != nil && rule.Type == RuleTypeRegex && rule.regex != nil {
|
||||||
|
newUA, err := rule.regex.Replace(originalUA, rewriteValue, -1, -1)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Errorf("Failed to apply REPLACE-PART: %v, using full replacement", err)
|
||||||
|
return rewriteValue
|
||||||
|
}
|
||||||
|
return newUA
|
||||||
|
}
|
||||||
|
// 如果不是正则规则,使用简单字符串替换
|
||||||
|
return strings.ReplaceAll(originalUA, rule.MatchValue, rewriteValue)
|
||||||
|
case ActionDelete:
|
||||||
|
return ""
|
||||||
|
case ActionDirect:
|
||||||
|
return originalUA
|
||||||
|
case ActionDrop:
|
||||||
|
return originalUA // DROP 由上层处理
|
||||||
|
default:
|
||||||
|
return originalUA
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasRules 是否有启用的规则
|
||||||
|
func (e *Engine) HasRules() bool {
|
||||||
|
for _, rule := range e.rules {
|
||||||
|
if rule.Enabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRules 获取所有规则
|
||||||
|
func (e *Engine) GetRules() []*Rule {
|
||||||
|
return e.rules
|
||||||
|
}
|
||||||
@ -9,7 +9,9 @@ import (
|
|||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/sunbk201/ua3f/internal/config"
|
"github.com/sunbk201/ua3f/internal/config"
|
||||||
|
"github.com/sunbk201/ua3f/internal/log"
|
||||||
"github.com/sunbk201/ua3f/internal/rewrite"
|
"github.com/sunbk201/ua3f/internal/rewrite"
|
||||||
|
"github.com/sunbk201/ua3f/internal/rule"
|
||||||
"github.com/sunbk201/ua3f/internal/server/utils"
|
"github.com/sunbk201/ua3f/internal/server/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -58,18 +60,10 @@ func (s *Server) handleHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
defer target.Close()
|
defer target.Close()
|
||||||
|
|
||||||
if s.cfg.DirectForward {
|
err = s.rewriteAndForward(target, req, req.Host, req.RemoteAddr)
|
||||||
err = req.Write(target)
|
if err != nil {
|
||||||
if err != nil {
|
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
||||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = s.rewriteAndForward(target, req, req.Host, req.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusServiceUnavailable)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
resp, err := http.ReadResponse(bufio.NewReader(target), req)
|
resp, err := http.ReadResponse(bufio.NewReader(target), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -111,9 +105,21 @@ func (s *Server) handleTunneling(w http.ResponseWriter, req *http.Request) {
|
|||||||
|
|
||||||
func (s *Server) rewriteAndForward(target net.Conn, req *http.Request, dstAddr, srcAddr string) (err error) {
|
func (s *Server) rewriteAndForward(target net.Conn, req *http.Request, dstAddr, srcAddr string) (err error) {
|
||||||
rw := s.rw
|
rw := s.rw
|
||||||
if rw.ShouldRewrite(req, srcAddr, dstAddr) {
|
|
||||||
req = rw.Rewrite(req, srcAddr, dstAddr)
|
// 获取重写决策(只匹配一次规则)
|
||||||
|
decision := rw.EvaluateRewriteDecision(req, srcAddr, dstAddr)
|
||||||
|
|
||||||
|
// Handle DROP action
|
||||||
|
if decision.Action == rule.ActionDrop {
|
||||||
|
log.LogInfoWithAddr(srcAddr, dstAddr, "Request dropped by rule")
|
||||||
|
return fmt.Errorf("request dropped by rule")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果需要重写,执行重写操作
|
||||||
|
if decision.ShouldRewrite() {
|
||||||
|
req = rw.Rewrite(req, srcAddr, dstAddr, decision)
|
||||||
|
}
|
||||||
|
|
||||||
if err = rw.Forward(target, req); err != nil {
|
if err = rw.Forward(target, req); err != nil {
|
||||||
err = fmt.Errorf("r.forward: %w", err)
|
err = fmt.Errorf("r.forward: %w", err)
|
||||||
return
|
return
|
||||||
@ -131,7 +137,7 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
|
|||||||
// Server -> Client (raw)
|
// Server -> Client (raw)
|
||||||
go utils.CopyHalf(client, target)
|
go utils.CopyHalf(client, target)
|
||||||
|
|
||||||
if s.cfg.DirectForward {
|
if s.cfg.RewriteMode == "direct" {
|
||||||
// Client -> Server (raw)
|
// Client -> Server (raw)
|
||||||
go utils.CopyHalf(target, client)
|
go utils.CopyHalf(target, client)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -67,7 +67,7 @@ func (s *Server) worker(ctx context.Context, workerID int, aChan <-chan *nfq.Att
|
|||||||
logrus.Debugf("Worker %d channel closed", workerID)
|
logrus.Debugf("Worker %d channel closed", workerID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.cfg.DirectForward {
|
if s.cfg.RewriteMode == config.RewriteModeDirect {
|
||||||
_ = s.nf.SetVerdict(*a.PacketID, nfq.NfAccept)
|
_ = s.nf.SetVerdict(*a.PacketID, nfq.NfAccept)
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -77,7 +77,7 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
|
|||||||
// Server -> Client (raw)
|
// Server -> Client (raw)
|
||||||
go utils.CopyHalf(client, target)
|
go utils.CopyHalf(client, target)
|
||||||
|
|
||||||
if s.cfg.DirectForward {
|
if s.cfg.RewriteMode == config.RewriteModeDirect {
|
||||||
// Client -> Server (raw)
|
// Client -> Server (raw)
|
||||||
go utils.CopyHalf(target, client)
|
go utils.CopyHalf(target, client)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -12,6 +12,16 @@ import (
|
|||||||
"github.com/sunbk201/ua3f/internal/server/tproxy"
|
"github.com/sunbk201/ua3f/internal/server/tproxy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ServerMode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServerModeHTTP ServerMode = "HTTP"
|
||||||
|
ServerModeSocks5 ServerMode = "SOCKS5"
|
||||||
|
ServerModeTProxy ServerMode = "TPROXY"
|
||||||
|
ServerModeRedirect ServerMode = "REDIRECT"
|
||||||
|
ServerModeNFQueue ServerMode = "NFQUEUE"
|
||||||
|
)
|
||||||
|
|
||||||
type Server interface {
|
type Server interface {
|
||||||
Start() error
|
Start() error
|
||||||
}
|
}
|
||||||
|
|||||||
@ -217,7 +217,7 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
|
|||||||
// Server -> Client (raw)
|
// Server -> Client (raw)
|
||||||
go utils.CopyHalf(client, target)
|
go utils.CopyHalf(client, target)
|
||||||
|
|
||||||
if s.cfg.DirectForward {
|
if s.cfg.RewriteMode == config.RewriteModeDirect {
|
||||||
// Client -> Server (raw)
|
// Client -> Server (raw)
|
||||||
go utils.CopyHalf(target, client)
|
go utils.CopyHalf(target, client)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -97,7 +97,7 @@ func (s *Server) ForwardTCP(client, target net.Conn, destAddr string) {
|
|||||||
// Server -> Client (raw)
|
// Server -> Client (raw)
|
||||||
go utils.CopyHalf(client, target)
|
go utils.CopyHalf(client, target)
|
||||||
|
|
||||||
if s.cfg.DirectForward {
|
if s.cfg.RewriteMode == config.RewriteModeDirect {
|
||||||
// Client -> Server (raw)
|
// Client -> Server (raw)
|
||||||
go utils.CopyHalf(target, client)
|
go utils.CopyHalf(target, client)
|
||||||
return
|
return
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user