mirror of
https://github.com/hansjone/netx.git
synced 2026-10-10 04:40:45 +08:00
Support bastion hop hosts as FQDN and pasted OpenSSH destinations.
Use placeholder examples (example.com / RFC5737) in docs and tests. EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
35e7bb4667
commit
b4514c13aa
12 changed files with 414 additions and 36 deletions
|
|
@ -3,6 +3,7 @@ import { useI18n } from "../i18n";
|
|||
import {
|
||||
HOP_VENDORS,
|
||||
defaultHopTemplate,
|
||||
expandBastionHopFields,
|
||||
isAutoHopTemplate,
|
||||
isBastionHopVendor,
|
||||
isLinuxHopVendor,
|
||||
|
|
@ -127,7 +128,24 @@ export function HopProxyFields({
|
|||
</label>
|
||||
<label>
|
||||
<FormLabel required>{t("managedNe.hop.host")}</FormLabel>
|
||||
<input required value={value.hop_host} onChange={(e) => set({ hop_host: e.target.value })} />
|
||||
<input
|
||||
required
|
||||
value={value.hop_host}
|
||||
placeholder={bastion ? t("managedNe.hop.hostPlaceholderBastion") : undefined}
|
||||
onChange={(e) => set({ hop_host: e.target.value })}
|
||||
onBlur={() => {
|
||||
if (!bastion) return;
|
||||
const raw = String(value.hop_host || "").trim();
|
||||
if (!raw.includes("@")) return;
|
||||
const parsed = expandBastionHopFields(raw, value.hop_username);
|
||||
if (!parsed.hop_host || parsed.hop_host === raw) return;
|
||||
set({
|
||||
hop_host: parsed.hop_host,
|
||||
hop_username: parsed.hop_username || value.hop_username,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{bastion ? <span className="form-field-hint">{t("managedNe.hop.hostHintBastion")}</span> : null}
|
||||
</label>
|
||||
<label>
|
||||
<FormLabel>{t("managedNe.hop.port")}</FormLabel>
|
||||
|
|
|
|||
|
|
@ -820,9 +820,9 @@ const en = {
|
|||
"· password may be empty (required for direct login; optional for bastion-managed or batch proxy later).\n" +
|
||||
"· Recommended flow: import NEs first, select rows, then use Batch add proxy.\n\n" +
|
||||
"[Jump / bastion]\n" +
|
||||
"· Bastion SSH username template: {hop_user}@{target_user}@{target_ip} (bastion host is separate); CLI: ssh user@target@ip@bastion-host.\n" +
|
||||
"· Bastion SSH username template: {hop_user}@{target_user}@{target_ip} (bastion host is separate IP or FQDN); CLI: ssh user@target@ip@bastion-host.\n" +
|
||||
"· Bastion-managed: set Jump password (Vault); target password optional. Manual mode needs target password.\n" +
|
||||
"· JumpServer/CBH often use port 2222; some sites use 22.\n\n" +
|
||||
"· Jump host accepts IP (192.0.2.10) or domain (ssh-bastion.example.com). JumpServer/CBH often use port 2222; some sites use 22.\n\n" +
|
||||
"[Connectivity / edit]\n" +
|
||||
"· NETX_CREDENTIAL_SECRET_KEY required to store passwords.\n" +
|
||||
"· Leave password blank on edit to keep unchanged; open Details after failed connect-test for hop context (no secrets).",
|
||||
|
|
@ -859,15 +859,18 @@ const en = {
|
|||
ciscoHint: "Run Cisco ssh -vrf / telnet /vrf jump commands; target credentials use secondary auth.",
|
||||
linuxHint: "SSH to the Linux bastion, then direct-tcpip tunnel to target IP:port (ProxyJump-style).",
|
||||
bastionHint:
|
||||
"SSH with composite username via bastion protocol proxy. Example: bastion-user@target-user@2.2.2.2@1.1.1.1; password is the bastion/Vault password. JumpServer/CBH often use port 2222.",
|
||||
"SSH with composite username via bastion protocol proxy. Hop host may be an IP or FQDN. Examples: bastion-user@target-user@198.51.100.20@192.0.2.10 or bastion-user@target-user@198.51.100.20@ssh-bastion.example.com; password is the bastion/Vault password. JumpServer/CBH often use port 2222.",
|
||||
targetAuthMode: "Target credentials",
|
||||
targetAuthBastionManaged: "Bastion-managed (target password optional)",
|
||||
targetAuthManual: "Manual (secondary auth after connect)",
|
||||
targetAuthHint: "Bastion-managed needs only bastion password; manual mode requires target NE password.",
|
||||
usernameTemplate: "SSH username template",
|
||||
templateHintBastion:
|
||||
"Default {hop_user}@{target_user}@{target_ip}. Bastion address is the hop host field. CLI example: ssh bastion-user@target-user@2.2.2.2@1.1.1.1.",
|
||||
"Default {hop_user}@{target_user}@{target_ip}. Bastion address (IP or FQDN) goes in Jump host. CLI example: ssh bastion-user@target-user@198.51.100.20@ssh-bastion.example.com.",
|
||||
host: "Jump host",
|
||||
hostPlaceholderBastion: "192.0.2.10 or ssh-bastion.example.com",
|
||||
hostHintBastion:
|
||||
"IP or FQDN. You can paste a full SSH destination (e.g. bastion-user@target-user@198.51.100.20@ssh-bastion.example.com); on blur it splits into jump username + host.",
|
||||
port: "Jump port",
|
||||
protocol: "Jump protocol",
|
||||
protocolSshStelnet: "ssh (stelnet)",
|
||||
|
|
|
|||
|
|
@ -852,15 +852,18 @@ const zh = {
|
|||
ciscoHint: "在思科设备上执行 ssh -vrf / telnet /vrf 跳登,目标账号由二次认证输入。",
|
||||
linuxHint: "先 SSH 登录 Linux 跳板,经 direct-tcpip 隧道连接目标 IP:端口(等同 ProxyJump)。",
|
||||
bastionHint:
|
||||
"SSH 复合用户名直连堡垒机,由堡垒机协议代理到目标。示例:bastion-user@target-user@2.2.2.2@1.1.1.1;密码为 Vault/堡垒机密码。JumpServer/CBH 常用端口 2222。",
|
||||
"SSH 复合用户名直连堡垒机(跳板地址可为 IP 或域名),由堡垒机协议代理到目标。示例:bastion-user@target-user@198.51.100.20@192.0.2.10 或 bastion-user@target-user@198.51.100.20@ssh-bastion.example.com;密码为 Vault/堡垒机密码。JumpServer/CBH 常用端口 2222。",
|
||||
targetAuthMode: "目标凭据",
|
||||
targetAuthBastionManaged: "堡垒机托管(目标密码可留空)",
|
||||
targetAuthManual: "手动输入(连接后二次认证)",
|
||||
targetAuthHint: "堡垒机托管时仅需堡垒机密码;手动模式需填写目标网元密码。",
|
||||
usernameTemplate: "SSH 用户名模板",
|
||||
templateHintBastion:
|
||||
"默认 {hop_user}@{target_user}@{target_ip},堡垒机地址填「跳板地址」。命令行示例:ssh bastion-user@target-user@2.2.2.2@1.1.1.1。",
|
||||
"默认 {hop_user}@{target_user}@{target_ip},堡垒机地址(IP 或域名)填「跳板地址」。命令行示例:ssh bastion-user@target-user@198.51.100.20@ssh-bastion.example.com。",
|
||||
host: "跳板地址",
|
||||
hostPlaceholderBastion: "192.0.2.10 或 ssh-bastion.example.com",
|
||||
hostHintBastion:
|
||||
"支持 IP 或 FQDN。也可粘贴完整 SSH 目标串(如 bastion-user@target-user@198.51.100.20@ssh-bastion.example.com),失焦后自动拆成跳板用户名与地址。",
|
||||
port: "跳板端口",
|
||||
protocol: "跳板协议",
|
||||
protocolSshStelnet: "ssh(stelnet)",
|
||||
|
|
|
|||
|
|
@ -27,6 +27,88 @@ export function isCliHopVendor(vendor: string): boolean {
|
|||
return v === "zte" || v === "huawei" || v === "cisco";
|
||||
}
|
||||
|
||||
/** Strip ssh:// / trailing slash / :port from bastion host (IP or FQDN). */
|
||||
export function normalizeHopHost(value: string): string {
|
||||
let host = String(value || "").trim();
|
||||
if (!host) return "";
|
||||
host = host.replace(/^ssh(?:\s+-p\s+\d+)?\s+/i, "").trim();
|
||||
if (host.includes("://")) host = host.split("://", 2)[1] || host;
|
||||
host = host.trim().replace(/\/+$/, "");
|
||||
if (host.startsWith("[") && host.includes("]")) {
|
||||
const inside = host.slice(1, host.indexOf("]"));
|
||||
const rest = host.slice(host.indexOf("]") + 1);
|
||||
if (!rest || rest.startsWith(":")) return inside.trim();
|
||||
return host;
|
||||
}
|
||||
if ((host.match(/:/g) || []).length === 1) {
|
||||
const [left, right] = host.split(":");
|
||||
if (/^\d+$/.test(right || "") && left && !left.includes("@")) return left.trim();
|
||||
}
|
||||
return host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse OpenSSH-style bastion destination.
|
||||
* Example: bastion-user@target-user@198.51.100.20@ssh-bastion.example.com
|
||||
*/
|
||||
export function parseBastionSshDestination(value: string): {
|
||||
hop_host: string;
|
||||
hop_username: string;
|
||||
target_user: string;
|
||||
target_ip: string;
|
||||
ssh_username: string;
|
||||
} {
|
||||
let raw = String(value || "").trim().replace(/^["']|["']$/g, "");
|
||||
raw = raw.replace(/^ssh(?:\s+-p\s+\d+)?\s+/i, "").trim();
|
||||
if (!raw) {
|
||||
return { hop_host: "", hop_username: "", target_user: "", target_ip: "", ssh_username: "" };
|
||||
}
|
||||
if (!raw.includes("@")) {
|
||||
return {
|
||||
hop_host: normalizeHopHost(raw),
|
||||
hop_username: "",
|
||||
target_user: "",
|
||||
target_ip: "",
|
||||
ssh_username: "",
|
||||
};
|
||||
}
|
||||
const at = raw.lastIndexOf("@");
|
||||
const userPart = raw.slice(0, at).trim();
|
||||
const hopHost = normalizeHopHost(raw.slice(at + 1));
|
||||
const parts = userPart.split("@").filter(Boolean);
|
||||
return {
|
||||
hop_host: hopHost,
|
||||
hop_username: parts[0] || "",
|
||||
target_user: parts[1] || "",
|
||||
target_ip: parts[2] || "",
|
||||
ssh_username: userPart,
|
||||
};
|
||||
}
|
||||
|
||||
/** If hop_host is a pasted user@…@bastion string, split into host + hop username. */
|
||||
export function expandBastionHopFields(
|
||||
hopHost: string,
|
||||
hopUsername = "",
|
||||
): {
|
||||
hop_host: string;
|
||||
hop_username: string;
|
||||
target_user: string;
|
||||
target_ip: string;
|
||||
} {
|
||||
const curUser = String(hopUsername || "").trim();
|
||||
const raw = String(hopHost || "").trim();
|
||||
if (!raw.includes("@")) {
|
||||
return { hop_host: normalizeHopHost(raw), hop_username: curUser, target_user: "", target_ip: "" };
|
||||
}
|
||||
const parsed = parseBastionSshDestination(raw);
|
||||
return {
|
||||
hop_host: parsed.hop_host,
|
||||
hop_username: parsed.hop_username || curUser,
|
||||
target_user: parsed.target_user,
|
||||
target_ip: parsed.target_ip,
|
||||
};
|
||||
}
|
||||
|
||||
export function defaultHopTemplate(vendor: string, protocol: string, vrf: string): string {
|
||||
const v = String(vendor || "zte").toLowerCase();
|
||||
if (v === "huawei") return huaweiHopTemplate(protocol, vrf);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue