mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-14 10:47:36 +00:00
custom SSH port for Borg remote targets
feat(host-backup): custom SSH port for Borg remote targets Add Borg destinations no longer assume port 22. The Monitor form and the shell TUI both take an optional port (default 22, range 1-65535) and embed it in the persisted ssh://user@host:port/path URL. BORG_RSH, the sshpass key installer and the capacity probe all honour it. Reported by @songochain in #236 — NAS-style Borg hosts on non-standard SSH ports are now first-class targets. Existing borg-targets.txt entries without a port keep working; the port is only serialised into the URL when it differs from 22.
This commit is contained in:
@@ -2717,6 +2717,11 @@ interface BorgRepo {
|
||||
name: string
|
||||
repository: string
|
||||
ssh_key_path?: string
|
||||
// Parsed from `ssh://user@host[:port]/path`; 22 for URLs without an
|
||||
// explicit port and 0 for local-mode targets. Newer backends ship
|
||||
// this; older ones don't and the frontend falls back to re-parsing
|
||||
// the URL on the fly.
|
||||
ssh_port?: number
|
||||
// Encryption + saved-passphrase metadata. Newer backends ship these;
|
||||
// older deployments without the fields default to "repokey" (the
|
||||
// shell installer's historical default) and unknown-passphrase.
|
||||
@@ -6092,6 +6097,11 @@ function AddDestinationDialog({
|
||||
const [borgMode, setBorgMode] = useState<"local" | "ssh">("local")
|
||||
const [borgSshUser, setBorgSshUser] = useState("borg")
|
||||
const [borgSshHost, setBorgSshHost] = useState("")
|
||||
// Custom SSH port for NAS-style hosts that expose SSH on non-22.
|
||||
// Kept as string in state so the input can start empty; converted to
|
||||
// integer at submit time. Empty / "22" means "use the default port"
|
||||
// and is omitted from the ssh:// URL by the backend.
|
||||
const [borgSshPort, setBorgSshPort] = useState("22")
|
||||
const [borgSshRemotePath, setBorgSshRemotePath] = useState("")
|
||||
const [borgSshKeyPath, setBorgSshKeyPath] = useState("/root/.ssh/proxmenux_borg")
|
||||
const [generatedKey, setGeneratedKey] = useState<{ public_key: string; authorized_keys_line: string } | null>(null)
|
||||
@@ -6148,26 +6158,32 @@ function AddDestinationDialog({
|
||||
setUsername(editing.username || "root@pam")
|
||||
setFingerprint(editing.fingerprint || "")
|
||||
setBorgRepo(""); setBorgMode("local"); setBorgSshUser("borg")
|
||||
setBorgSshHost(""); setBorgSshRemotePath("")
|
||||
setBorgSshHost(""); setBorgSshPort("22"); setBorgSshRemotePath("")
|
||||
setBorgSshKeyPath("/root/.ssh/proxmenux_borg")
|
||||
setBorgEncryptionEnabled(true); setLocalPath("")
|
||||
return
|
||||
}
|
||||
if (editing && editing.kind === "borg") {
|
||||
const repo = editing.repository || ""
|
||||
const ssh = repo.match(/^ssh:\/\/([^@]+)@([^/]+)\/(.+)$/)
|
||||
// Match ssh://user@host[:port]/path — the port group is optional.
|
||||
// Group 3 captures the numeric port when present, group 4 the path.
|
||||
const ssh = repo.match(/^ssh:\/\/([^@]+)@([^/:]+)(?::(\d+))?\/(.+)$/)
|
||||
setName(editing.name)
|
||||
if (ssh) {
|
||||
setBorgMode("ssh")
|
||||
setBorgSshUser(ssh[1])
|
||||
setBorgSshHost(ssh[2])
|
||||
setBorgSshRemotePath(`/${ssh[3]}`)
|
||||
// Prefer the port already parsed by the backend (ssh_port); fall
|
||||
// back to the URL match; default 22 when neither is set.
|
||||
const port = editing.ssh_port ?? (ssh[3] ? Number(ssh[3]) : 22)
|
||||
setBorgSshPort(String(port || 22))
|
||||
setBorgSshRemotePath(`/${ssh[4]}`)
|
||||
setBorgSshKeyPath(editing.ssh_key_path || "/root/.ssh/proxmenux_borg")
|
||||
setBorgRepo("")
|
||||
} else {
|
||||
setBorgMode("local")
|
||||
setBorgRepo(repo)
|
||||
setBorgSshUser("borg"); setBorgSshHost(""); setBorgSshRemotePath("")
|
||||
setBorgSshUser("borg"); setBorgSshHost(""); setBorgSshPort("22"); setBorgSshRemotePath("")
|
||||
setBorgSshKeyPath("/root/.ssh/proxmenux_borg")
|
||||
}
|
||||
const mode = editing.encrypt_mode || "repokey"
|
||||
@@ -6186,6 +6202,7 @@ function AddDestinationDialog({
|
||||
setBorgMode("local")
|
||||
setBorgSshUser("borg")
|
||||
setBorgSshHost("")
|
||||
setBorgSshPort("22")
|
||||
setBorgSshRemotePath("")
|
||||
setBorgSshKeyPath("/root/.ssh/proxmenux_borg")
|
||||
setBorgEncryptionEnabled(true)
|
||||
@@ -6274,6 +6291,12 @@ function AddDestinationDialog({
|
||||
body.ssh_host = borgSshHost.trim()
|
||||
body.ssh_remote_path = borgSshRemotePath.trim()
|
||||
if (borgSshKeyPath.trim()) body.ssh_key_path = borgSshKeyPath.trim()
|
||||
// Only send port when it's non-default; the backend leaves 22
|
||||
// out of the persisted URL so existing targets stay identical.
|
||||
const portNum = parseInt(borgSshPort.trim(), 10)
|
||||
if (Number.isFinite(portNum) && portNum > 0 && portNum !== 22) {
|
||||
body.ssh_port = portNum
|
||||
}
|
||||
}
|
||||
const resp = await fetchApi<{ repo?: string }>("/api/host-backups/destinations/borg", {
|
||||
method: "POST",
|
||||
@@ -6409,9 +6432,23 @@ function AddDestinationDialog({
|
||||
{t("backup.destinations.sshUserHelpBefore")} <code className="font-mono">borg serve</code>. {t("backup.destinations.sshUserHelpAfter")} <code className="font-mono">borg</code>, {t("backup.destinations.not")} <code className="font-mono">root</code>.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="borgSshHost">{t("backup.fields.sshHostOrIp")}</Label>
|
||||
<Input id="borgSshHost" value={borgSshHost} onChange={(e) => setBorgSshHost(e.target.value)} className="font-mono mt-1" placeholder="backup.example.com" />
|
||||
<div className="grid grid-cols-[1fr_100px] gap-3">
|
||||
<div>
|
||||
<Label htmlFor="borgSshHost">{t("backup.fields.sshHostOrIp")}</Label>
|
||||
<Input id="borgSshHost" value={borgSshHost} onChange={(e) => setBorgSshHost(e.target.value)} className="font-mono mt-1" placeholder="backup.example.com" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="borgSshPort">{t("backup.fields.sshPort")}</Label>
|
||||
<Input
|
||||
id="borgSshPort"
|
||||
value={borgSshPort}
|
||||
onChange={(e) => setBorgSshPort(e.target.value.replace(/[^\d]/g, ""))}
|
||||
className="font-mono mt-1"
|
||||
placeholder="22"
|
||||
inputMode="numeric"
|
||||
maxLength={5}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="borgSshPath">{t("backup.fields.remoteRepositoryPath")}</Label>
|
||||
|
||||
@@ -355,6 +355,15 @@ function suggestPackageName(name: string) {
|
||||
.replace(/^-+|-+$/g, "")
|
||||
}
|
||||
|
||||
// The argv editors use a comma-separated display value, while the API stores
|
||||
// each argument as an array item. Keep this conversion separate from the text
|
||||
// shown in the controlled input: normalising the visible value on every
|
||||
// keystroke would remove a newly typed comma or trailing space before the user
|
||||
// can enter the next argument.
|
||||
function parseArgvInput(value: string): string[] {
|
||||
return value.split(",").map((item) => item.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Props) {
|
||||
const t = useT()
|
||||
const isLightTheme = useIsLightTheme()
|
||||
@@ -371,6 +380,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
const [detectionNotice, setDetectionNotice] = useState<{ found: boolean; text: string } | null>(null)
|
||||
// Editor state
|
||||
const [editing, setEditing] = useState<{ appId: string | null; draft: AppConfig } | null>(null)
|
||||
const [binaryArgsInput, setBinaryArgsInput] = useState("")
|
||||
const [commandArgvInput, setCommandArgvInput] = useState("")
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [testingDetector, setTestingDetector] = useState(false)
|
||||
const [detectorTest, setDetectorTest] = useState<DetectorTestResult | null>(null)
|
||||
@@ -742,6 +753,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
setShowAdvanced(false)
|
||||
}
|
||||
}
|
||||
setBinaryArgsInput((seed.binary_args || []).join(", "))
|
||||
setCommandArgvInput((seed.command_argv || []).join(", "))
|
||||
setEditing({ appId: existing?.id || null, draft: seed })
|
||||
setDetectorTest(null)
|
||||
setError(null)
|
||||
@@ -749,6 +762,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
|
||||
const closeEditor = () => {
|
||||
setEditing(null)
|
||||
setBinaryArgsInput("")
|
||||
setCommandArgvInput("")
|
||||
setDetectorTest(null)
|
||||
setError(null)
|
||||
}
|
||||
@@ -771,6 +786,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
setSidecar(r)
|
||||
setLxcAppsCached(vmid, r, suggestions)
|
||||
setEditing(null)
|
||||
setBinaryArgsInput("")
|
||||
setCommandArgvInput("")
|
||||
onChange?.()
|
||||
} catch (e: any) {
|
||||
setError(e?.message || t("vmLxc.appEditor.saveFailed"))
|
||||
@@ -1241,6 +1258,7 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
patch.distribution = t.distribution || ""
|
||||
patch.container_name = t.container_name || ""
|
||||
patch.label = t.label || ""
|
||||
patch.command_argv = t.command_argv || []
|
||||
patch.installed_regex = t.installed_regex || ""
|
||||
patch.upstream_type = (t as any).upstream_type || (t.repo ? "github" : "")
|
||||
patch.repo = t.repo || ""
|
||||
@@ -1249,6 +1267,8 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
patch.upstream_json_path = (t as any).upstream_json_path || ""
|
||||
patch.docker_image = (t as any).docker_image || ""
|
||||
patch.tag_regex = t.tag_regex || "v?(\\d+\\.\\d+\\.\\d+)"
|
||||
setBinaryArgsInput((t.binary_args || []).join(", "))
|
||||
setCommandArgvInput((t.command_argv || []).join(", "))
|
||||
setShowAdvanced(true)
|
||||
}
|
||||
setEditing((prev) => prev ? { ...prev, draft: { ...prev.draft, ...patch } } : prev)
|
||||
@@ -1713,10 +1733,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
<Label htmlFor="app-de-args">{t("vmLxc.appEditor.binaryArgsLabel")}</Label>
|
||||
<Input
|
||||
id="app-de-args"
|
||||
value={(draft.binary_args || []).join(", ")}
|
||||
onChange={(e) => setField({
|
||||
binary_args: e.target.value.split(",").map(s => s.trim()).filter(Boolean),
|
||||
})}
|
||||
value={binaryArgsInput}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
setBinaryArgsInput(value)
|
||||
setField({ binary_args: parseArgvInput(value) })
|
||||
}}
|
||||
placeholder={t("vmLxc.appEditor.binaryArgsPlaceholder")}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
@@ -1732,10 +1754,12 @@ export function LxcAppPanel({ vmid, ctIp, onChange, managed, initialData }: Prop
|
||||
<Label htmlFor="app-cmd-argv">{t("vmLxc.appEditor.commandLabel")}</Label>
|
||||
<Input
|
||||
id="app-cmd-argv"
|
||||
value={(draft.command_argv || []).join(", ")}
|
||||
onChange={(e) => setField({
|
||||
command_argv: e.target.value.split(",").map(s => s.trim()).filter(Boolean),
|
||||
})}
|
||||
value={commandArgvInput}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value
|
||||
setCommandArgvInput(value)
|
||||
setField({ command_argv: parseArgvInput(value) })
|
||||
}}
|
||||
placeholder={t("vmLxc.appEditor.commandPlaceholder")}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
|
||||
@@ -3962,7 +3962,8 @@
|
||||
"vmsLxcs": "VMs/LXCs",
|
||||
"when": "Wann",
|
||||
"whenLabel": "Wann",
|
||||
"zfsPools": "ZFS-Pools"
|
||||
"zfsPools": "ZFS-Pools",
|
||||
"sshPort": "SSH-Port"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "Backups.",
|
||||
|
||||
@@ -3962,7 +3962,8 @@
|
||||
"vmsLxcs": "VMs/LXCs",
|
||||
"when": "When",
|
||||
"whenLabel": "When",
|
||||
"zfsPools": "ZFS pools"
|
||||
"zfsPools": "ZFS pools",
|
||||
"sshPort": "SSH port"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "backups.",
|
||||
|
||||
@@ -3962,7 +3962,8 @@
|
||||
"vmsLxcs": "VM/LXC",
|
||||
"when": "Cuando",
|
||||
"whenLabel": "Cuando",
|
||||
"zfsPools": "grupos ZFS"
|
||||
"zfsPools": "grupos ZFS",
|
||||
"sshPort": "Puerto SSH"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "copias de seguridad.",
|
||||
|
||||
@@ -3962,7 +3962,8 @@
|
||||
"vmsLxcs": "VM/LXC",
|
||||
"when": "Quand",
|
||||
"whenLabel": "Quand",
|
||||
"zfsPools": "Pools ZFS"
|
||||
"zfsPools": "Pools ZFS",
|
||||
"sshPort": "Port SSH"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "sauvegardes.",
|
||||
|
||||
@@ -3962,7 +3962,8 @@
|
||||
"vmsLxcs": "VM/LXC",
|
||||
"when": "Quando",
|
||||
"whenLabel": "Quando",
|
||||
"zfsPools": "Pool ZFS"
|
||||
"zfsPools": "Pool ZFS",
|
||||
"sshPort": "Porta SSH"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "backup.",
|
||||
|
||||
@@ -3962,7 +3962,8 @@
|
||||
"vmsLxcs": "VMs/LXCs",
|
||||
"when": "Quando",
|
||||
"whenLabel": "Quando",
|
||||
"zfsPools": "Conjuntos ZFS"
|
||||
"zfsPools": "Conjuntos ZFS",
|
||||
"sshPort": "Porta SSH"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "cópias de segurança.",
|
||||
|
||||
@@ -3962,7 +3962,8 @@
|
||||
"vmsLxcs": "VM/LXC",
|
||||
"when": "Kedy",
|
||||
"whenLabel": "Kedy",
|
||||
"zfsPools": "ZFS pooly"
|
||||
"zfsPools": "ZFS pooly",
|
||||
"sshPort": "SSH port"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "zálohy.",
|
||||
|
||||
@@ -3963,7 +3963,8 @@
|
||||
"vmsLxcs": "virtuella datorer/LXC:er",
|
||||
"when": "När",
|
||||
"whenLabel": "När",
|
||||
"zfsPools": "ZFS pooler"
|
||||
"zfsPools": "ZFS pooler",
|
||||
"sshPort": "SSH-port"
|
||||
},
|
||||
"jobs": {
|
||||
"attachDescriptionAfter": "säkerhetskopior.",
|
||||
|
||||
@@ -16380,11 +16380,29 @@ def _list_borg_destinations() -> list:
|
||||
encrypt_mode = (parts[3] if len(parts) > 3 else '').strip() or 'repokey'
|
||||
pass_file = f'{_BACKUP_STATE_DIR}/borg-pass-{name}.txt'
|
||||
has_passphrase = os.path.isfile(pass_file)
|
||||
# Parse ssh://user@host[:port]/path so the frontend can
|
||||
# display the port without having to re-parse the URL.
|
||||
# Non-ssh targets (local paths) leave ssh_port at 0.
|
||||
ssh_port = 0
|
||||
if repo.startswith('ssh://'):
|
||||
after_scheme = repo[len('ssh://'):]
|
||||
at_split = after_scheme.split('@', 1)
|
||||
if len(at_split) == 2:
|
||||
host_and_path = at_split[1]
|
||||
host_part = host_and_path.split('/', 1)[0]
|
||||
if ':' in host_part:
|
||||
try:
|
||||
ssh_port = int(host_part.rsplit(':', 1)[1])
|
||||
except (ValueError, IndexError):
|
||||
ssh_port = 0
|
||||
if ssh_port == 0:
|
||||
ssh_port = 22
|
||||
targets.append({
|
||||
'name': name,
|
||||
'repository': repo,
|
||||
'ssh_key': ssh_key,
|
||||
'ssh_key_path': ssh_key,
|
||||
'ssh_port': ssh_port,
|
||||
'encrypt_mode': encrypt_mode,
|
||||
'has_passphrase': has_passphrase,
|
||||
'jobs_using': _jobs_using_borg(repo),
|
||||
@@ -16649,15 +16667,21 @@ def _capacity_local(path: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _capacity_borg_ssh(host: str, user: str, remote_path: str, key_path: str = '') -> dict:
|
||||
def _capacity_borg_ssh(host: str, user: str, remote_path: str, key_path: str = '',
|
||||
port: int = 22) -> dict:
|
||||
"""Run `df -B1 --output=size,used,avail` over ssh against the
|
||||
remote borg repo path. Times out fast — failure is just rendered
|
||||
as a missing capacity badge in the UI, not a hard error."""
|
||||
as a missing capacity badge in the UI, not a hard error.
|
||||
|
||||
``port`` defaults to 22 (standard SSH); pass a different value for
|
||||
NAS-style hosts that expose SSH on a custom port."""
|
||||
if not host or not user or not remote_path:
|
||||
return {'error': 'incomplete ssh target'}
|
||||
ssh_target = f'{user}@{host}'
|
||||
cmd = ['ssh', '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5',
|
||||
'-o', 'StrictHostKeyChecking=accept-new']
|
||||
if port and port != 22:
|
||||
cmd += ['-p', str(int(port))]
|
||||
if key_path:
|
||||
cmd += ['-i', key_path]
|
||||
cmd += [ssh_target, f'df -B1 --output=size,used,avail {shlex.quote(remote_path)}']
|
||||
@@ -18017,11 +18041,17 @@ def api_host_backups_dest_capacity():
|
||||
if kind == 'local' or kind == 'borg-local':
|
||||
cap = _capacity_local((t.get('path') or '').strip())
|
||||
elif kind == 'borg-ssh':
|
||||
port_raw = t.get('port')
|
||||
try:
|
||||
port = int(port_raw) if port_raw not in (None, '', 0, '0') else 22
|
||||
except (TypeError, ValueError):
|
||||
port = 22
|
||||
cap = _capacity_borg_ssh(
|
||||
(t.get('host') or '').strip(),
|
||||
(t.get('user') or '').strip(),
|
||||
(t.get('remote_path') or '').strip(),
|
||||
(t.get('key_path') or '').strip(),
|
||||
port=port,
|
||||
)
|
||||
elif kind == 'pbs':
|
||||
cap = _capacity_pbs(
|
||||
@@ -19219,7 +19249,23 @@ def api_host_backups_dest_borg_add():
|
||||
rpath = (payload.get('ssh_remote_path') or '').strip().lstrip('/')
|
||||
if not user or not host or not rpath:
|
||||
return jsonify({'error': 'ssh_user, ssh_host and ssh_remote_path are required for ssh mode'}), 400
|
||||
repo = f'ssh://{user}@{host}/{rpath}'
|
||||
# Optional custom SSH port — NAS-style hosts often expose SSH on
|
||||
# a non-standard port to reduce noise from bots. Empty / missing
|
||||
# means default 22, which we leave out of the URL so existing
|
||||
# targets stay byte-identical to how the shell installer writes them.
|
||||
raw_port = payload.get('ssh_port')
|
||||
ssh_port = 22
|
||||
if raw_port not in (None, '', 0, '0'):
|
||||
try:
|
||||
ssh_port = int(raw_port)
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({'error': 'ssh_port must be an integer between 1 and 65535'}), 400
|
||||
if not (1 <= ssh_port <= 65535):
|
||||
return jsonify({'error': 'ssh_port must be an integer between 1 and 65535'}), 400
|
||||
if ssh_port == 22:
|
||||
repo = f'ssh://{user}@{host}/{rpath}'
|
||||
else:
|
||||
repo = f'ssh://{user}@{host}:{ssh_port}/{rpath}'
|
||||
ssh_key = (payload.get('ssh_key_path') or '').strip()
|
||||
elif mode == 'local':
|
||||
repo = (payload.get('repo') or '').strip()
|
||||
|
||||
@@ -2163,6 +2163,12 @@ hb_borg_generate_and_install_key() {
|
||||
local borg_user="$1" host="$2" rpath="$3" mode="$4"
|
||||
local _out_var="$5"
|
||||
local -n _out_ref="$_out_var"
|
||||
# Custom SSH port arrives via env var so the callers (this file
|
||||
# itself, in 7+ places) don't have to change their signature. When
|
||||
# unset or 22, ssh uses its default and no `-p` flag is injected.
|
||||
local _port="${HB_BORG_INSTALL_PORT:-22}"
|
||||
local _p_flag=()
|
||||
[[ "$_port" != "22" ]] && _p_flag=(-p "$_port")
|
||||
|
||||
local key_file="$HOME/.ssh/borg_proxmenux_$(echo "$host" | tr './:' '___')_ed25519"
|
||||
local pub_file="${key_file}.pub"
|
||||
@@ -2346,6 +2352,7 @@ hb_borg_generate_and_install_key() {
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
-o PreferredAuthentications=password -o PubkeyAuthentication=no \
|
||||
-o NumberOfPasswordPrompts=1 -o ConnectTimeout=10 \
|
||||
"${_p_flag[@]}" \
|
||||
"$admin_user@$host" "true" 2>&1) || true
|
||||
if echo "$_probe" | grep -qiE "permission denied[[:space:]]*\(publickey"; then
|
||||
# SSH password auth refused by the server — common when the Borg
|
||||
@@ -2402,6 +2409,7 @@ hb_borg_generate_and_install_key() {
|
||||
local push_rc
|
||||
SSHPASS="$admin_pass" sshpass -e ssh -o StrictHostKeyChecking=accept-new \
|
||||
-o PreferredAuthentications=password -o PubkeyAuthentication=no \
|
||||
"${_p_flag[@]}" \
|
||||
"$admin_user@$host" "$install_cmd" <<<"$authorized_line" >/tmp/proxmenux-borg-keypush.log 2>&1
|
||||
push_rc=$?
|
||||
|
||||
@@ -2509,6 +2517,22 @@ hb_configure_borg_manual() {
|
||||
12 78 "borg" 3>&1 1>&2 2>&3) || return 1
|
||||
host=$(dialog --backtitle "ProxMenux" --inputbox "$(hb_translate "SSH host or IP:")" \
|
||||
"$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "" 3>&1 1>&2 2>&3) || return 1
|
||||
# Custom SSH port (defaults to 22). NAS-style hosts often
|
||||
# move SSH off 22 to keep their intrusion warnings quiet.
|
||||
# Accepted range: 1-65535; anything else re-prompts.
|
||||
local port
|
||||
while :; do
|
||||
port=$(dialog --backtitle "ProxMenux" \
|
||||
--inputbox "$(hb_translate "SSH port (default 22):")" \
|
||||
"$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "22" 3>&1 1>&2 2>&3) || return 1
|
||||
port="${port//[[:space:]]/}"
|
||||
[[ -z "$port" ]] && port=22
|
||||
if [[ "$port" =~ ^[0-9]+$ ]] && (( port >= 1 && port <= 65535 )); then
|
||||
break
|
||||
fi
|
||||
dialog --backtitle "ProxMenux" \
|
||||
--msgbox "$(hb_translate "Port must be a number between 1 and 65535.")" 8 60
|
||||
done
|
||||
rpath=$(dialog --backtitle "ProxMenux" \
|
||||
--inputbox "$(hb_translate "Remote repository path:")" \
|
||||
"$HB_UI_INPUT_H" "$HB_UI_INPUT_W" "/backup/borgbackup" \
|
||||
@@ -2598,7 +2622,12 @@ hb_configure_borg_manual() {
|
||||
fi
|
||||
;;
|
||||
generate-auto|generate-manual|generate-pct)
|
||||
if ! hb_borg_generate_and_install_key "$user" "$host" "$rpath" "$key_mode" ssh_key; then
|
||||
# Thread the custom port to the key-install helper
|
||||
# via env var so the sshpass probe and the actual
|
||||
# push both hit the right port on the Borg server.
|
||||
HB_BORG_INSTALL_PORT="${port:-22}" \
|
||||
hb_borg_generate_and_install_key "$user" "$host" "$rpath" "$key_mode" ssh_key
|
||||
if (( $? != 0 )); then
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
@@ -2606,7 +2635,15 @@ hb_configure_borg_manual() {
|
||||
ssh_key=""
|
||||
;;
|
||||
esac
|
||||
repo="ssh://$user@$host/$rpath"
|
||||
# Custom SSH port is embedded in the URL — Borg's ssh://
|
||||
# scheme natively supports `ssh://user@host:port/path`. Port
|
||||
# 22 is left out for backwards compatibility with existing
|
||||
# borg-targets.txt entries that never carried the port.
|
||||
if [[ "${port:-22}" == "22" ]]; then
|
||||
repo="ssh://$user@$host/$rpath"
|
||||
else
|
||||
repo="ssh://$user@$host:$port/$rpath"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -2635,7 +2672,13 @@ hb_configure_borg_manual() {
|
||||
|
||||
_borg_repo_ref_new="$repo"
|
||||
if [[ -n "$ssh_key" ]]; then
|
||||
export BORG_RSH="ssh -i $ssh_key -o StrictHostKeyChecking=accept-new"
|
||||
local rsh_cmd="ssh -i $ssh_key -o StrictHostKeyChecking=accept-new"
|
||||
[[ -n "${port:-}" && "$port" != "22" ]] && rsh_cmd="$rsh_cmd -p $port"
|
||||
export BORG_RSH="$rsh_cmd"
|
||||
elif [[ -n "${port:-}" && "$port" != "22" ]]; then
|
||||
# No custom key but non-default port — still need to tell ssh
|
||||
# which port to hit so `borg` doesn't fall back to 22.
|
||||
export BORG_RSH="ssh -o StrictHostKeyChecking=accept-new -p $port"
|
||||
else
|
||||
unset BORG_RSH
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user