feat: add per-IP limit controls

This commit is contained in:
sagitchu
2026-04-27 23:17:02 +08:00
parent edfe2a2372
commit a5a500bc0f
2 changed files with 94 additions and 0 deletions
+5
View File
@@ -71,6 +71,9 @@ export interface ForwardApiItem {
userId?: number;
tunnelId?: number;
speedId?: number | null;
ipMaxConn?: number;
ipSpeedId?: number | null;
ipSpeedLimitName?: string;
maxConn?: number;
proxyProtocol?: number;
inx?: number;
@@ -372,6 +375,8 @@ export interface ForwardMutationPayload {
remoteAddr?: string;
strategy?: string;
speedId?: number | null;
ipMaxConn?: number;
ipSpeedId?: number | null;
maxConn?: number;
proxyProtocol?: number;
}
+89
View File
@@ -125,6 +125,9 @@ interface Forward {
userId?: number;
inx?: number;
speedId?: number | null;
ipMaxConn?: number;
ipSpeedId?: number | null;
ipSpeedLimitName?: string;
proxyProtocol?: number;
}
@@ -160,6 +163,8 @@ interface ForwardForm {
interfaceName?: string;
strategy: string;
speedId: number | null;
ipMaxConn?: number;
ipSpeedId: number | null;
maxConn?: number;
proxyProtocol?: number;
}
@@ -578,6 +583,16 @@ const mapForwardApiItems = (items: ForwardApiItem[]): Forward[] => {
typeof forward.speedId === "number" || forward.speedId === null
? forward.speedId
: undefined,
ipMaxConn:
typeof forward.ipMaxConn === "number" ? forward.ipMaxConn : undefined,
ipSpeedId:
typeof forward.ipSpeedId === "number" || forward.ipSpeedId === null
? forward.ipSpeedId
: undefined,
ipSpeedLimitName:
typeof forward.ipSpeedLimitName === "string"
? forward.ipSpeedLimitName
: undefined,
maxConn: typeof forward.maxConn === "number" ? forward.maxConn : undefined,
proxyProtocol:
typeof forward.proxyProtocol === "number"
@@ -1316,6 +1331,8 @@ export default function ForwardPage() {
interfaceName: "",
strategy: "fifo",
speedId: null,
ipMaxConn: 0,
ipSpeedId: null,
maxConn: 0,
proxyProtocol: 0,
});
@@ -2030,6 +2047,7 @@ export default function ForwardPage() {
};
const selectedSpeedId = normalizeSpeedId(form.speedId);
const selectedIPSpeedId = normalizeSpeedId(form.ipSpeedId);
const validateForm = (): boolean => {
const newErrors: { [key: string]: string } = {};
@@ -2105,6 +2123,8 @@ export default function ForwardPage() {
interfaceName: "",
strategy: "fifo",
speedId: null,
ipMaxConn: 0,
ipSpeedId: null,
proxyProtocol: 0,
});
setErrors({});
@@ -2126,6 +2146,8 @@ export default function ForwardPage() {
interfaceName: forward.interfaceName || "",
strategy: forward.strategy || "fifo",
speedId: normalizeSpeedId(forward.speedId),
ipMaxConn: forward.ipMaxConn ?? 0,
ipSpeedId: normalizeSpeedId(forward.ipSpeedId),
maxConn: forward.maxConn ?? 0,
proxyProtocol: forward.proxyProtocol ?? 0,
});
@@ -2245,6 +2267,8 @@ export default function ForwardPage() {
let res: { code: number; msg: string };
const normalizedSpeedId = normalizeSpeedId(form.speedId);
const speedLimitAutoCleared = isMissingSpeedLimit(form.speedId);
const normalizedIPSpeedId = normalizeSpeedId(form.ipSpeedId);
const ipSpeedLimitAutoCleared = isMissingSpeedLimit(form.ipSpeedId);
if (isEdit) {
const updateData = {
@@ -2256,6 +2280,8 @@ export default function ForwardPage() {
remoteAddr: processedRemoteAddr,
strategy: addressCount > 1 ? form.strategy : "fifo",
speedId: normalizedSpeedId,
ipMaxConn: form.ipMaxConn,
ipSpeedId: normalizedIPSpeedId,
maxConn: form.maxConn,
proxyProtocol: form.proxyProtocol,
};
@@ -2270,6 +2296,8 @@ export default function ForwardPage() {
remoteAddr: processedRemoteAddr,
strategy: addressCount > 1 ? form.strategy : "fifo",
speedId: normalizedSpeedId,
ipMaxConn: form.ipMaxConn,
ipSpeedId: normalizedIPSpeedId,
maxConn: form.maxConn,
proxyProtocol: form.proxyProtocol,
};
@@ -2297,6 +2325,12 @@ export default function ForwardPage() {
duration: 5000,
});
}
if (ipSpeedLimitAutoCleared) {
toast("所选每 IP 限速规则不存在,已自动清除为不限速", {
icon: "⚠️",
duration: 5000,
});
}
toast.success(isEdit ? "修改成功" : "创建成功");
setModalOpen(false);
await refreshForwardList(false);
@@ -4911,6 +4945,27 @@ export default function ForwardPage() {
setForm((prev) => ({ ...prev, maxConn: value }));
}}
/>
<Input
description="每个客户端 IP 可同时建立的最大连接数;0 或空表示不限制。"
label="每 IP 最大连接数"
min="0"
placeholder="0 或空表示不限制"
type="number"
value={
form.ipMaxConn === 0
? ""
: String(form.ipMaxConn || "")
}
variant="bordered"
onChange={(e) => {
const value = Math.max(
Number(e.target.value) || 0,
0,
);
setForm((prev) => ({ ...prev, ipMaxConn: value }));
}}
/>
<Select
description="启用 PROXY protocol,用于透传客户端真实 IP"
label="Proxy Protocol"
@@ -4963,6 +5018,40 @@ export default function ForwardPage() {
))}
</Select>
)}
{isAdmin && (
<Select
description="每个客户端 IP 独享该限速规则;不选择表示不限制。"
label="每 IP 限速"
placeholder="不限速"
selectedKeys={
selectedIPSpeedId !== null
? [selectedIPSpeedId.toString()]
: []
}
variant="bordered"
onSelectionChange={(keys) => {
const selectedKey = Array.from(keys)[0] as
| string
| undefined;
setForm((prev) => ({
...prev,
ipSpeedId: selectedKey
? Number(selectedKey)
: null,
}));
}}
>
{availableSpeedLimits.map((speedLimit) => (
<SelectItem
key={speedLimit.id.toString()}
textValue={speedLimit.name}
>
{speedLimit.name}
</SelectItem>
))}
</Select>
)}
</div>
</AccordionItem>
</Accordion>