Compare commits

...
29 Commits
Author SHA1 Message Date
KroeseandGitHub 51d6f3aeca fix: Reserve internal ports for user-mode forwarding (#1348) 2026-08-02 23:01:52 +02:00
KroeseandGitHub 4544620d48 feat: Increase indentation of printed QEMU arguments (#1347) 2026-08-01 05:49:50 +02:00
KroeseandGitHub de8468161f feat: Refactor negated command conditions (#1346) 2026-07-30 21:22:21 +02:00
renovate[bot]andGitHub 18d6be8210 chore(deps): update hadolint/hadolint-action action to v3.4.0 (#1345) 2026-07-30 21:06:27 +02:00
KroeseandGitHub e5b8cf3bf8 build: Update Passt to v2026_07_28 (#1344) 2026-07-30 10:10:41 +02:00
KroeseandGitHub ec12039f43 feat: Use deadline-based process timeouts (#1343) 2026-07-28 15:22:47 +02:00
KroeseandGitHub 3c7c3ca1b1 fix: Prevent race when reading PID files (#1342) 2026-07-28 14:22:08 +02:00
KroeseandGitHub 2b27f32cd4 fix: Disk options were applied to the controller (#1341) 2026-07-28 03:30:55 +02:00
KroeseandGitHub 55d1d50284 fix: Retry transient gateway errors in web status (#1340) 2026-07-27 23:49:22 +02:00
KroeseandGitHub 784b73b5b5 feat: Add DISK_OPTIONS support to disk devices (#1339) 2026-07-27 23:48:21 +02:00
KroeseandGitHub 85f00bb9cc docs: Added new DISK_OPTIONS variable (#1338) 2026-07-27 12:04:25 +02:00
KroeseandGitHub efe8732e47 fix: Avoid shadowing PID output variables (#1337) 2026-07-27 12:03:24 +02:00
KroeseandGitHub f6871dd61b fix: Clear invalid PID values in shared reader (#1336) 2026-07-26 17:52:14 +02:00
KroeseandGitHub a713b728ff fix: Use shared PID reader for helper processes (#1335) 2026-07-26 17:41:46 +02:00
KroeseandGitHub 0d74c6ac80 build: Update dependabot config (#1334) 2026-07-26 00:58:36 +02:00
KroeseandGitHub 63e4529eff fix: Add cleanup for failed web server startup (#1333) 2026-07-25 13:47:20 +02:00
KroeseandGitHub afd848cebb fix: Make healthcheck succeed during download (#1332) 2026-07-25 12:16:41 +02:00
KroeseandGitHub 695b075130 feat: Provide host access through system.lan (#1331) 2026-07-25 03:38:36 +02:00
KroeseandGitHub 7eff53e722 feat: Improve disk error handling (#1328) 2026-07-24 13:51:59 +02:00
KroeseandGitHub bcaf0e5980 fix: Improve network error handling (#1327) 2026-07-24 13:27:09 +02:00
KroeseandGitHub faa820c2cc feat: Inline local variable declarations (#1326) 2026-07-24 13:11:40 +02:00
KroeseandGitHub 81e477fcc4 feat: Warn when DSM and container share an IP address (#1325) 2026-07-24 12:45:54 +02:00
KroeseandGitHub abd3a1c3df fix: Network mode shown for DHCP mode (#1324) 2026-07-24 12:03:57 +02:00
KroeseandGitHub 3b59bcd284 fix: Ensure progress reaches 100% (#1322) 2026-07-23 23:03:54 +02:00
KroeseandGitHub f1b56f394a build: Create a detailed .gitignore file (#1321) 2026-07-23 15:42:59 +02:00
KroeseandGitHub 1a983ebcd1 feat: Improve error handling for webserver config (#1320) 2026-07-23 04:17:26 +02:00
KroeseandGitHub 41c76198fa feat: Use atomic writes for progress updates (#1319) 2026-07-23 04:10:09 +02:00
KroeseandGitHub 222b5649b0 fix: Prevent stale polling responses for web status (#1318) 2026-07-23 04:00:12 +02:00
KroeseandGitHub e14fd4b711 docs: Environment variables (#1317) 2026-07-21 13:14:32 +02:00
20 changed files with 917 additions and 299 deletions
+7
View File
@@ -1,4 +1,5 @@
version: 2
updates:
- package-ecosystem: docker
directory: /
@@ -6,9 +7,15 @@ updates:
interval: weekly
cooldown:
default-days: 7
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
cooldown:
default-days: 7
ignore:
- dependency-name: "*"
update-types:
- version-update:semver-minor
- version-update:semver-patch
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
-e SC2153
-
name: Lint Dockerfile
uses: hadolint/hadolint-action@v3.3.0
uses: hadolint/hadolint-action@v3.4.0
with:
dockerfile: Dockerfile
ignore: DL3008
+268 -1
View File
@@ -1 +1,268 @@
build.sh
##############################
# Operating System Files
##############################
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
ehthumbs.db
Desktop.ini
Icon?
$RECYCLE.BIN/
.Spotlight-V100/
.Trashes/
.fseventsd
##############################
# IDEs
##############################
.vscode/
.idea/
*.iml
*.ipr
*.iws
##############################
# VS Code
##############################
.history/
*.code-workspace
##############################
# Vim
##############################
*.swp
*.swo
Session.vim
##############################
# Sublime
##############################
*.sublime-workspace
*.sublime-project
##############################
# Temporary Files
##############################
*.tmp
*.temp
*.bak
*.old
*.orig
*.rej
*.save
##############################
# Logs
##############################
*.log
logs/
log/
*.out
*.err
*.trace
##############################
# Runtime
##############################
*.pid
*.seed
*.pid.lock
##############################
# Secrets
##############################
.env
.env.*
!.env.example
*.pem
*.key
*.crt
*.cer
*.p12
*.pfx
*.kdbx
*.secret
*.token
##############################
# SSH
##############################
.ssh/
##############################
# Docker
##############################
docker-compose.override.yml
docker-compose.local.yml
##############################
# VM Storage
##############################
storage/
windows/
downloads/
##############################
# Disk Images
##############################
*.qcow2
*.qcow
*.vhd
*.vhdx
*.vdi
*.raw
*.img
*.iso
*.bin
##############################
# QEMU
##############################
*.nvram
*.fd
*.efi
*.sock
*.monitor
*.serial
##############################
# Samba
##############################
shared/
share/
##############################
# Backups
##############################
backup/
backups/
*.backup
##############################
# Cache
##############################
.cache/
.cache-loader/
.tmp/
temp/
tmp/
##############################
# Python
##############################
__pycache__/
*.py[cod]
.pytest_cache/
.mypy_cache/
.venv/
venv/
##############################
# Node
##############################
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
##############################
# Build
##############################
dist/
build/
out/
release/
##############################
# Coverage
##############################
coverage/
.coverage
coverage.xml
##############################
# Archives
##############################
*.zip
*.tar
*.tar.gz
*.tgz
*.7z
*.rar
##############################
# Generated Config
##############################
config.local.*
settings.local.*
local.env
##############################
# Test Files
##############################
test-output/
playwright-report/
##############################
# macOS
##############################
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
##############################
# Linux
##############################
*~
.nfs*
##############################
# Windows
##############################
*.stackdump
##############################
# Misc
##############################
*.cache
*.lock
*.lock.json
*.bak.*
##############################
# Keep Examples
##############################
!.gitkeep
!.env.example
+1 -1
View File
@@ -8,7 +8,7 @@ ARG TARGETPLATFORM
ARG VERSION_ARG="0.0"
ARG VERSION_CSTRUCT="4.7"
ARG VERSION_PASST="2026_07_16"
ARG VERSION_PASST="2026_07_28"
ARG DEBCONF_NOWARNINGS="yes"
ARG DEBIAN_FRONTEND="noninteractive"
+6 -5
View File
@@ -8,8 +8,8 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description |
|---|---|---|
| `URL` | | URL or local path to the DSM `.pat` installation file. Downloads the default Virtual DSM image automatically when unset. |
| `COUNTRY` | | Country code used to select the Synology download mirror. Detected automatically when unset. |
| `URL` | | URL or local path to a custom `.pat` installation file. |
| `COUNTRY` | | Country code used to select the Synology download mirror. |
| `HOST_MAC` | | MAC address reported to DSM. |
| `HOST_MODEL` | | Synology host model reported to DSM. |
| `HOST_SERIAL` | | Synology host serial number reported to DSM. |
@@ -39,6 +39,7 @@ An empty default means the variable is unset and its value is determined automat
| `DISK_DISCARD` | `unmap` | Discard/TRIM mode for the primary disk. |
| `DISK_ROTATION` | `1` | Rotation rate reported to the guest. Use `1` to identify the disk as an SSD. |
| `DISK_FLAGS` | | Additional options used when creating `qcow2` disks. |
| `DISK_OPTIONS` | | Additional options appended to QEMU disk devices. |
| `ALLOCATE` | `N` | Preallocates space for the data disks. |
| `STORAGE` | `/storage` | Storage directory used for disks, settings, and downloads. |
@@ -47,14 +48,14 @@ An empty default means the variable is unset and its value is determined automat
| Variable | Default | Description |
|---|---|---|
| `NETWORK` | | Network mode, such as `nat`, `user`, or `N` to disable networking. |
| `DHCP` | `N` | Enables macvtap networking so DSM receives an address from the external LAN through DHCP. |
| `HOST` | `VirtualDSM` | Hostname assigned to DSM. |
| `DHCP` | `N` | Enables macvtap networking so DSM receives a DHCP address. |
| `HOST` | | Hostname assigned to the machine on the network. |
| `IP` | | Overrides the automatically selected guest IPv4 address. |
| `MAC` | | Guest network adapter MAC address. |
| `ADAPTER` | `virtio-net-pci` | QEMU network adapter model. |
| `DEV` | `eth0` | Container network interface used as the uplink. |
| `MTU` | | MTU assigned to the guest network interface. |
| `MASK` | `255.255.255.0` | IPv4 netmask. |
| `MASK` | `255.255.255.0` | IPv4 netmask for guest network. |
| `TAP` | `dsm` | TAP or macvtap interface name. |
| `BRIDGE` | `docker` | Bridge name used for NAT networking. |
| `HOST_PORTS` | | Ports excluded from guest forwarding. |
+1 -1
View File
@@ -15,7 +15,7 @@ file="/run/shm/dsm.url"
address="/run/shm/qemu.ip"
gateway="/run/shm/qemu.gw"
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 1
[ ! -s "$file" ] && echo "DSM has not enabled networking yet..." && exit 0
location=$(<"$file")
+36 -22
View File
@@ -7,6 +7,7 @@ set -Eeuo pipefail
: "${DISK_FMT:="raw"}" # Disk file format, 'raw' by default for best performance
: "${DISK_TYPE:=""}" # Device type to be used, "sata", "nvme", "blk" or "scsi"
: "${DISK_FLAGS:=""}" # Specifies the options for use with the qcow2 disk format
: "${DISK_OPTIONS:=""}" # Specifies additional options for the QEMU disk device
: "${DISK_CACHE:="none"}" # Caching mode, can be set to 'writeback' for better performance
: "${DISK_DISCARD:="unmap"}" # Controls whether unmap (TRIM) commands are passed to the host.
: "${DISK_ROTATION:="1"}" # Rotation rate, set to 1 for SSD storage and increase for HDD
@@ -16,6 +17,7 @@ DISK_IO=$(strip "$DISK_IO")
DISK_FMT=$(strip "$DISK_FMT")
DISK_TYPE=$(strip "$DISK_TYPE")
DISK_FLAGS=$(strip "$DISK_FLAGS")
DISK_OPTIONS=$(strip "$DISK_OPTIONS")
DISK_CACHE=$(strip "$DISK_CACHE")
DISK_DISCARD=$(strip "$DISK_DISCARD")
DISK_ROTATION=$(strip "$DISK_ROTATION")
@@ -154,8 +156,8 @@ normalizeSize() {
local diskDesc="$2"
local dir="$3"
local gb free space
local dataSize spare=1073741824
local free dataSize
local spare=1073741824
if [[ "${diskSpace,,}" == "max" || "${diskSpace,,}" == "half" ]]; then
@@ -168,12 +170,12 @@ normalizeSize() {
fi
(( free < spare )) && free="$spare"
gb=$(( free / 1073741825 ))
local gb=$(( free / 1073741825 ))
diskSpace="${gb}G"
fi
space="${diskSpace// /}"
local space="${diskSpace// /}"
[ -z "$space" ] && space="256G"
[ -z "${space//[0-9. ]}" ] && space="${space}G"
space=$(echo "${space^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
@@ -508,6 +510,9 @@ createDevice () {
[[ -z "${PCI_BUS:-}" && ( "${MACHINE,,}" == pc || "${MACHINE,,}" == pc-i440fx* ) ]] && bus="pci.0"
local options=""
[ -n "$DISK_OPTIONS" ] && options=",${DISK_OPTIONS#,}"
local bootIndex=""
local diskId="data$diskIndex"
[ -n "$diskIndex" ] && bootIndex=",bootindex=$diskIndex"
@@ -520,29 +525,29 @@ createDevice () {
;;
"usb" )
result+=",if=none \
-device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}"
-device usb-storage,drive=${diskId}${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"nvme" )
result+=",if=none \
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}"
-device nvme,drive=${diskId}${bootIndex},serial=deadbeaf${diskIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"ide" | "sata" )
result+=",if=none \
-device ich9-ahci,id=ahci${diskIndex},addr=$diskAddress \
-device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}"
-device ide-hd,drive=${diskId},bus=ahci$diskIndex.0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"blk" | "virtio-blk" )
result+=",if=none \
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}"
-device virtio-blk-pci,drive=${diskId},bus=$bus,addr=$diskAddress,iothread=io2${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
"scsi" | "virtio-scsi" )
result+=",if=none \
-device virtio-scsi-pci,id=${diskId}b,bus=$bus,addr=$diskAddress,iothread=io2,hotplug=off \
-device scsi-hd,drive=${diskId},bus=${diskId}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}"
-device scsi-hd,drive=${diskId},bus=${diskId}b.0,channel=0,scsi-id=0,lun=0,rotation_rate=$DISK_ROTATION${bootIndex}${diskSerial}${diskSectors}${options}"
echo "$result"
;;
esac
@@ -573,13 +578,12 @@ addDisk () {
local diskCache="$9"
local fs dir used space
local diskExt diskFile
local dataSize missing
local diskExt dataSize
local available currentSize
local previousExt previousFmt
local previousExt
diskExt=$(fmt2ext "$diskFmt")
diskFile="$diskBase.$diskExt"
local diskFile="$diskBase.$diskExt"
dir=$(dirname "$diskFile")
[ ! -d "$dir" ] && return 0
@@ -587,7 +591,11 @@ addDisk () {
space=$(normalizeSize "$diskSpace" "$diskDesc" "$dir")
dataSize=$(numfmt --from=iec "$space")
fs=$(stat -f -c %T "$dir")
if ! fs=$(stat -f -c %T "$dir"); then
error "Failed to determine filesystem type of \"$dir\" !"
return 1
fi
checkFS "$fs" "$diskFile" "$diskDesc" || exit $?
if ! supportsDirect "$fs"; then
@@ -595,23 +603,24 @@ addDisk () {
diskCache="writeback"
fi
if [ ! -s "$diskFile" ] ; then
if [ ! -f "$diskFile" ] || [ ! -s "$diskFile" ]; then
if [[ "${diskFmt,,}" != "raw" ]]; then
previousFmt="raw"
local previousFmt="raw"
else
previousFmt="qcow2"
local previousFmt="qcow2"
fi
previousExt=$(fmt2ext "$previousFmt")
if [ -s "$diskBase.$previousExt" ] ; then
if [ -f "$diskBase.$previousExt" ] &&
[ -s "$diskBase.$previousExt" ]; then
convertDisk "$diskBase.$previousExt" "$previousFmt" "$diskFile" "$diskFmt" "$diskBase" "$diskDesc" "$fs" || exit $?
fi
fi
if [ -s "$diskFile" ]; then
if [ -f "$diskFile" ] && [ -s "$diskFile" ]; then
currentSize=$(getSize "$diskFile") || exit 71
@@ -641,18 +650,18 @@ addDisk () {
currentSize=$(getSize "$diskFile") || exit 73
used=$(du -sB 1 "$diskFile" | cut -f1)
available=$(df --output=avail -B 1 "$dir" | tail -n 1)
missing=$(( currentSize - used - available ))
local missing=$(( currentSize - used - available ))
(( missing < 0 )) && missing=0
if (( missing > 0 )); then
local gb base msg
local gb base
gb=$(formatBytes "$available")
base=$(baseDir "$dir")
missing=$(formatBytes "$missing")
currentSize=$(formatBytes "$currentSize")
msg="The virtual size of the ${diskDesc,,} is $currentSize"
local msg="The virtual size of the ${diskDesc,,} is $currentSize"
if [ -n "$used" ] && [[ "$used" != "0" ]]; then
used=$(formatBytes "$used")
@@ -767,6 +776,11 @@ if [[ "$DISK_FLAGS" =~ [[:space:]] ]]; then
exit 78
fi
if [[ "$DISK_OPTIONS" =~ [[:space:]] ]]; then
error "Invalid DISK_OPTIONS value '$DISK_OPTIONS', spaces are not allowed."
exit 78
fi
if [ -z "$ALLOCATE" ]; then
ALLOCATE="N"
fi
+1 -1
View File
@@ -2,7 +2,7 @@
set -Eeuo pipefail
if enabled "$DEBUG"; then
printf "QEMU arguments:\n\n%s\n\n" "${ARGS// -/$'\n-'}"
printf "QEMU arguments:\n\n %s\n\n" "${ARGS// -/$'\n -'}"
fi
return 0
+4 -4
View File
@@ -254,7 +254,7 @@ if ! touch "$SYSTEM"; then
error "Could not create file $SYSTEM for the system disk." && exit 98
fi
! setOwner "$SYSTEM" && warn "failed to set the owner for \"$SYSTEM\" !"
setOwner "$SYSTEM" || warn "failed to set the owner for \"$SYSTEM\" !"
if [[ "${FS,,}" == "btrfs" ]]; then
{ chattr +C "$SYSTEM"; } || :
@@ -329,7 +329,7 @@ fakeroot -- bash -c "set -Eeu;\
rm -rf "$MOUNT"
echo "$BASE" > "$STORAGE/dsm.ver"
! setOwner "$STORAGE/dsm.ver" && warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
setOwner "$STORAGE/dsm.ver" || warn "failed to set the owner for \"$STORAGE/dsm.ver\" !"
if [[ "$URL" == "file://$STORAGE/$BASE.pat" ]]; then
rm -f "$PAT"
@@ -338,11 +338,11 @@ else
fi
if [ -f "$STORAGE/$BASE.pat" ]; then
! setOwner "$STORAGE/$BASE.pat" && warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
setOwner "$STORAGE/$BASE.pat" || warn "failed to set the owner for \"$STORAGE/$BASE.pat\" !"
fi
mv -f "$BOOT" "$STORAGE/$BASE.boot.img"
! setOwner "$STORAGE/$BASE.boot.img" && warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
setOwner "$STORAGE/$BASE.boot.img" || warn "failed to set the owner for \"$STORAGE/$BASE.boot.img\" !"
rm -rf "$TMP"
+259 -135
View File
@@ -84,8 +84,7 @@ getMTU() {
minMTU() {
local mtu=""
local min=""
local mtu min=""
for mtu in "$@"; do
[[ -z "$mtu" || "$mtu" == "0" ]] && continue
@@ -124,7 +123,7 @@ gatewayMAC() {
maskToCIDR() {
local mask="$1"
local prefix=""
local prefix
if ! command -v ipcalc > /dev/null 2>&1; then
error "Required command 'ipcalc' is not installed!"
@@ -154,7 +153,7 @@ maskToCIDR() {
networkCIDR() {
local ip="$1"
local network=""
local network
network=$(ipcalc -n -b "$ip/$MASK" 2>/dev/null | awk '
/^Network:/ {
@@ -172,6 +171,37 @@ networkCIDR() {
return 0
}
upstreamIP() {
local subnet="$1"
local guest="$2"
local gateway="$3"
local broadcast candidate last
broadcast=$(ipcalc -n -b "$subnet" 2>/dev/null | awk '
/^Broadcast:/ {
print $2
exit
}
')
if [[ ! "$broadcast" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
return 1
fi
last="${broadcast##*.}"
for (( last--; last>=2; last-- )); do
candidate="${broadcast%.*}.$last"
[[ "$candidate" == "$guest" || "$candidate" == "$gateway" ]] && continue
echo "$candidate"
return 0
done
return 1
}
detectInterface() {
if [ -n "$DEV" ]; then
@@ -207,16 +237,23 @@ formatAddress() {
return 0
}
defaultGateway() {
ip -4 route list default dev "$1" 2>/dev/null |
awk '$1 == "default" { for (i = 1; i < NF; i++) if ($i == "via") { print $(i + 1); exit } }' || :
return 0
}
detectAddresses() {
GATEWAY=$(ip route list dev "$DEV" | awk ' /^default/ {print $3}' | head -n 1)
GATEWAY=$(defaultGateway "$DEV")
{ UPLINK=$(ip address show dev "$DEV" | grep inet | awk '/inet / { print $2 }' | cut -f1 -d/ | head -n 1); } 2>/dev/null || :
IP6=""
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
local rc=0
{ IP6=$(ip -6 addr show dev "$DEV" scope global up); rc=$?; } 2>/dev/null || :
{ IP6=$(ip -6 addr show dev "$DEV" scope global up); local rc=$?; } 2>/dev/null || :
(( rc != 0 )) && IP6=""
[ -n "$IP6" ] && IP6=$(echo "$IP6" | sed -e's/^.*inet6 \([^ ]*\)\/.*$/\1/;t;d' | head -n 1)
fi
@@ -226,7 +263,7 @@ detectAddresses() {
detectAdapter() {
local result=""
local result
NIC=""
BUS=""
@@ -252,7 +289,7 @@ detectAdapter() {
containerID() {
local id=""
local id
id=$(hostname -s 2>/dev/null || true)
@@ -260,7 +297,7 @@ containerID() {
id=$(< /etc/machine-id)
fi
if [ -z "$id" ] && [ -s /proc/sys/kernel/random/boot_id ]; then
if [ -z "$id" ] && [ -r /proc/sys/kernel/random/boot_id ]; then
id=$(< /proc/sys/kernel/random/boot_id)
fi
@@ -286,7 +323,7 @@ disableIPv6() {
subnetInUse() {
local subnet="$1"
local broader="" narrower="" routes=""
local broader narrower routes
if ! broader=$(ip -4 route show table all match "$subnet" 2>/dev/null); then
error "Failed to inspect existing routes for subnet $subnet."
@@ -324,15 +361,14 @@ guestIP() {
natGuestIP() {
local ip="$1"
local start="" guest="" subnet=""
local second="" third="" rc=""
local guest subnet second third
third=$(cut -d. -f3 <<< "$ip")
if [[ "$ip" == "172.30."* ]]; then
start="31"
local start="31"
else
start="30"
local start="30"
fi
for (( second=start; second<=254; second++ )); do
@@ -342,7 +378,7 @@ natGuestIP() {
if subnetInUse "$subnet"; then
continue
else
rc=$?
local rc=$?
(( rc == 1 )) || return 1
fi
@@ -357,7 +393,7 @@ natGuestIP() {
if subnetInUse "$subnet"; then
continue
else
rc=$?
local rc=$?
(( rc == 1 )) || return 1
fi
@@ -369,36 +405,6 @@ natGuestIP() {
return 1
}
kernelAtLeast() {
local major="$1"
local minor="${2:-0}"
(( KERNEL > major || (KERNEL == major && MINOR >= minor) ))
}
canBindToDevice() {
local dev="$1"
[ -n "$dev" ] || return 1
kernelAtLeast 5 7 || return 1
[ -d "/sys/class/net/$dev" ] || return 1
command -v python3 > /dev/null 2>&1 || return 0
python3 - "$dev" > /dev/null 2>&1 <<'PY'
import socket
import sys
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(
socket.SOL_SOCKET,
socket.SO_BINDTODEVICE,
sys.argv[1].encode() + b"\0",
)
PY
}
# ######################################
# DNS / port helpers
# ######################################
@@ -411,7 +417,9 @@ configureDNS() {
local host="$4"
local mask="$5"
local gateway="$6"
local arguments="$DNSMASQ_OPTS" rc
local upstream="${7:-}"
local arguments="$DNSMASQ_OPTS"
local pid
if ! echo "$gateway" > /run/shm/qemu.gw; then
error "Failed to write gateway file."
@@ -421,7 +429,10 @@ configureDNS() {
enabled "${DNSMASQ_DISABLE:-}" && return 0
enabled "$DEBUG" && echo "Starting dnsmasq daemon..."
[ -s "$DNSMASQ_PID" ] && pKill "$(<"$DNSMASQ_PID")"
if readPidFile pid "$DNSMASQ_PID"; then
pKill "$pid"
fi
rm -f "$DNSMASQ_PID"
if isNAT; then
@@ -459,6 +470,11 @@ configureDNS() {
# Add DNS entry for container
arguments+=" --address=/host.lan/$gateway"
# Add DNS entry for the upstream gateway.
if isNAT && [ -n "$upstream" ]; then
arguments+=" --address=/system.lan/$upstream"
fi
# Avoid returning IPv6 records when the active network mode is IPv4-only.
if isNAT || [ -z "$IP6" ]; then
arguments+=" --filter-AAAA"
@@ -476,9 +492,9 @@ configureDNS() {
arguments+=" --log-facility=$log"
arguments=$(echo "$arguments" | sed 's/\t/ /g' | tr -s ' ' | sed 's/^ *//')
enabled "$DEBUG" && printf "Dnsmasq arguments:\n\n%s\n\n" "${arguments// -/$'\n-'}"
enabled "$DEBUG" && printf "Dnsmasq arguments:\n\n %s\n\n" "${arguments// -/$'\n -'}"
{ $DNSMASQ ${arguments:+ $arguments}; rc=$?; } || :
{ $DNSMASQ ${arguments:+ $arguments}; local rc=$?; } || :
if (( rc != 0 )); then
@@ -499,11 +515,12 @@ configureDNS() {
return 0
}
getHostPorts() {
normalizePorts() {
local port="" ports=""
local num="" mode="${1:-tcp}"
local list="${HOST_PORTS// /},"
local list="$1"
local mode="${2:-tcp}"
local port num
local ports=""
for port in ${list//,/ }; do
@@ -513,6 +530,7 @@ getHostPorts() {
"tcp" )
[[ "$port" == *"/udp" ]] && continue
num="${port%/tcp}"
[ -n "$num" ] && ports+="$num,"
;;
"all" )
if [[ "$port" == *"/udp" ]]; then
@@ -522,21 +540,67 @@ getHostPorts() {
num="${port%/tcp}"
[ -n "$num" ] && ports+="$num/tcp,"
fi
continue
;;
*)
return 1
;;
esac
[ -n "$num" ] && ports+="$num,"
done
# Remove duplicates
ports=$(echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g')
echo "${ports//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g'
return 0
}
getReservedPorts() {
local list=""
local mode="${1:-tcp}"
# Reserve the DNS port while the internal dnsmasq resolver is active.
if ! enabled "${DNSMASQ_DISABLE:-}" && ! isNAT; then
list+="53/tcp,53/udp,"
fi
# WEB_PORT and WSD_PORT are intentionally not reserved. In non-DHCP modes,
# closeWeb() releases both before NAT or user-mode forwarding is configured,
# allowing WEB_PORT (normally 5000) to be handed over to DSM.
normalizePorts "$list" "$mode"
return $?
}
getCustomHostPorts() {
local mode="${1:-tcp}"
local reserved user port
local ports=""
reserved=$(getReservedPorts "all")
user=$(normalizePorts "$HOST_PORTS" "all")
for port in ${user//,/ }; do
[[ ",$reserved," == *",$port,"* ]] && continue
ports+="$port,"
done
normalizePorts "$ports" "$mode"
return 0
}
getHostPorts() {
local mode="${1:-tcp}"
local reserved custom
# Merge internal reservations with user-defined host ports without mutating HOST_PORTS.
# User entries already covered by an internal reservation are silently ignored.
reserved=$(getReservedPorts "all")
custom=$(getCustomHostPorts "all")
normalizePorts "$reserved,$custom" "$mode"
echo "$ports"
return 0
}
@@ -545,16 +609,16 @@ getUserPorts() {
local defaults="22/tcp,5000/tcp,5001/tcp"
local list="$defaults,${USER_PORTS// /},"
local num="" ports="" proto=""
local userport="" hostport=""
local ports=""
local userport hostport exclude reserved
local exclude=""
reserved=$(getReservedPorts "all")
exclude=$(getHostPorts "all")
for userport in ${list//,/ }; do
proto="tcp"
num="$userport"
local proto="tcp"
local num="$userport"
if [[ "$userport" == *"/udp" ]]; then
proto="udp"
@@ -570,7 +634,9 @@ getUserPorts() {
if [[ "$num/$proto" == "$hostport" ]]; then
if [[ "$hostport" != "${WEB_PORT:-}/tcp" ]]; then
if [[ ",$reserved," == *",$hostport,"* ]]; then
warn "Could not assign port $hostport to \"USER_PORTS\" because it is reserved by the container!"
elif [[ "$hostport" != "${WEB_PORT:-}/tcp" ]]; then
warn "Could not assign port $hostport to \"USER_PORTS\" because it is already in \"HOST_PORTS\"!"
fi
@@ -593,7 +659,7 @@ getUserPorts() {
getSlirp() {
local ip="$1"
local args="" list=""
local args="" list
list=$(getUserPorts)
@@ -617,9 +683,8 @@ getSlirp() {
getPasst() {
local args="" list="" port=""
local num="" tcp="" udp=""
local bind="$UPLINK"
local list port
local tcp="" udp="" args=""
list=$(getUserPorts)
@@ -629,12 +694,12 @@ getPasst() {
if [[ "$port" == *"/udp" ]]; then
num="${port%/udp}"
local num="${port%/udp}"
[ -n "$num" ] && udp+="$num,"
elif [[ "$port" == *"/tcp" ]]; then
num="${port%/tcp}"
local num="${port%/tcp}"
[ -n "$num" ] && tcp+="$num,"
else
@@ -648,12 +713,8 @@ getPasst() {
tcp="${tcp%,}"
udp="${udp%,}"
if canBindToDevice "$DEV"; then
bind="%$DEV"
fi
[ -n "$tcp" ] && args+=" -t $bind/$tcp"
[ -n "$udp" ] && args+=" -u $bind/$udp"
[ -n "$tcp" ] && args+=" -t $tcp"
[ -n "$udp" ] && args+=" -u $udp"
echo "$args"
return 0
@@ -665,8 +726,7 @@ getPasst() {
configureVTAP() {
local msg=""
local rc
local msg dev
enabled "$DEBUG" && echo "Configuring MACVTAP networking..."
@@ -678,7 +738,7 @@ configureVTAP() {
fi
# Create a macvtap network for the VM guest
{ msg=$(ip link add link "$DEV" name "$TAP" address "$MAC" type macvtap mode bridge 2>&1); rc=$?; } || :
{ msg=$(ip link add link "$DEV" name "$TAP" address "$MAC" type macvtap mode bridge 2>&1); local rc=$?; } || :
case "$msg" in
"RTNETLINK answers: File exists"* )
@@ -711,15 +771,35 @@ configureVTAP() {
sleep 2
done
local TAP_NR TAP_PATH MAJOR MINOR
TAP_NR=$(</sys/class/net/"$TAP"/ifindex)
TAP_PATH="/dev/tap${TAP_NR}"
local TAP_NR MAJOR MINOR
if ! dev=$(cat /sys/devices/virtual/net/"$TAP"/tap*/dev); then
error "Failed to determine device numbers for MACVTAP interface \"$TAP\" !"
return 1
fi
IFS=: read -r MAJOR MINOR <<< "$dev"
if [[ ! "$MAJOR" =~ ^[0-9]+$ || ! "$MINOR" =~ ^[0-9]+$ ]]; then
error "Failed to parse device numbers for MACVTAP interface \"$TAP\" !"
return 1
fi
if (( MAJOR < 1 )); then
error "Cannot find: sys/devices/virtual/net/$TAP"
return 1
fi
if ! TAP_NR=$(<"/sys/class/net/$TAP/ifindex"); then
error "Failed to determine interface index of MACVTAP interface \"$TAP\" !"
return 1
fi
# Create dev file (there is no udev in container: need to be done manually)
IFS=: read -r MAJOR MINOR < <(cat /sys/devices/virtual/net/"$TAP"/tap*/dev)
(( MAJOR < 1)) && error "Cannot find: sys/devices/virtual/net/$TAP" && return 1
local TAP_PATH="/dev/tap${TAP_NR}"
[[ ! -e "$TAP_PATH" && -e "/dev0/${TAP_PATH##*/}" ]] && ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH"
[[ ! -e "$TAP_PATH" && -e "/dev0/${TAP_PATH##*/}" ]] &&
ln -s "/dev0/${TAP_PATH##*/}" "$TAP_PATH"
if [[ ! -e "$TAP_PATH" ]]; then
{ mknod "$TAP_PATH" c "$MAJOR" "$MINOR"; rc=$?; } || :
@@ -753,7 +833,7 @@ configureSlirp() {
ip=$(guestIP "$ip" 4)
local gateway="${ip%.*}.1"
local subnet=""
local subnet
subnet=$(networkCIDR "$ip") || return 1
local ipv6="ipv6=off,"
@@ -761,7 +841,7 @@ configureSlirp() {
NET_OPTS="-netdev user,id=hostnet0,ipv4=on,host=$gateway,net=$subnet,dhcpstart=$ip,${ipv6}hostname=$HOST"
local forward=""
local forward
forward=$(getSlirp "$ip")
[ -n "$forward" ] && NET_OPTS+=",$forward"
@@ -818,7 +898,7 @@ configurePasst() {
# Pass an explicit MTU to passt.
PASST_OPTS+=" -m $passt_mtu"
local forward=""
local forward
forward=$(getPasst)
[ -n "$forward" ] && PASST_OPTS+="$forward"
@@ -854,11 +934,10 @@ configurePasst() {
if ! "$PASST" ${PASST_OPTS:+$PASST_OPTS} >/dev/null 2>&1; then
local rc=0
rm -f "$log"
PASST_OPTS="${PASST_OPTS/ -q/}"
{ "$PASST" ${PASST_OPTS:+$PASST_OPTS}; rc=$?; } || :
{ "$PASST" ${PASST_OPTS:+$PASST_OPTS}; local rc=$?; } || :
if (( rc != 0 )); then
[ -f "$log" ] && [ -s "$log" ] && cat "$log"
@@ -889,10 +968,10 @@ configurePasst() {
createBridge() {
local gateway="$1"
local rc msg=""
local msg
# Create a bridge with a static IP for the VM guest
{ msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); rc=$?; } || :
{ msg=$(ip link add dev "$BRIDGE" type bridge 2>&1); local rc=$?; } || :
if (( rc != 0 )); then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
@@ -930,10 +1009,10 @@ createBridge() {
createTap() {
local tuntap="$1"
local rc msg=""
local msg
# Set tap to the bridge created
{ msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); rc=$?; } || :
{ msg=$(ip tuntap add dev "$TAP" mode tap 2>&1); local rc=$?; } || :
if (( rc != 0 )); then
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
@@ -976,7 +1055,7 @@ hasTable() {
getTablesBackend() {
local version=""
local version
version=$(iptables --version 2>/dev/null || true)
case "$version" in
@@ -989,7 +1068,7 @@ getTablesBackend() {
setTables() {
local mode="$1"
local path=""
local path
path=$(command -v "iptables-$mode" 2>/dev/null || true)
[ -z "$path" ] && return 1
@@ -1003,7 +1082,7 @@ showRules() {
local chain="$2"
local label="$3"
local rule_tag="$4"
local rules=""
local rules
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
enabled "$DEBUG" || return 0
@@ -1022,7 +1101,7 @@ showRules() {
checkExistingTables() {
local msg="" rules="" conflicts=""
local rules conflicts
local rule_tag="QEMU_DNAT"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -1040,7 +1119,7 @@ checkExistingTables() {
<<< "$rules" || true)
if [ -n "$conflicts" ]; then
msg="your existing NAT rules may take precedence over VM port forwarding"
local msg="your existing NAT rules may take precedence over VM port forwarding"
if enabled "$DEBUG"; then
warn "${msg}."
@@ -1060,7 +1139,7 @@ checkExistingTables() {
<<< "$rules" || true)
if [ -n "$conflicts" ]; then
msg="your existing firewall rules may block traffic forwarded to or from the VM"
local msg="your existing firewall rules may block traffic forwarded to or from the VM"
if enabled "$DEBUG"; then
warn "${msg}."
@@ -1088,13 +1167,13 @@ runTableRule() {
local silent="$1"
local result="$2"
local rc msg=""
local msg
shift 2
printf -v "$result" '%s' ""
{ msg=$("$@" 2>&1); rc=$?; } || :
{ msg=$("$@" 2>&1); local rc=$?; } || :
(( rc == 0 )) && return 0
printf -v "$result" '%s' "$msg"
@@ -1154,8 +1233,8 @@ applyTables() {
local ip="$1"
local subnet="$2"
local silent="${3:-N}"
local exclude="" port=""
local table_error=""
local exclude port
local table_error
local dnat_chain="QEMU_DNAT"
local rule_tag="$dnat_chain"
@@ -1283,8 +1362,8 @@ applyTables() {
clearTables() {
local table="" line="" chain=""
local rules="" remaining="" message=""
local line
local rules remaining message
local dnat_chain="QEMU_DNAT"
local rule_tag="$dnat_chain"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -1309,15 +1388,15 @@ clearTables() {
while IFS= read -r line; do
case "$line" in
\*nat ) table="nat" ;;
\*filter ) table="filter" ;;
\*mangle ) table="mangle" ;;
\*raw ) table="raw" ;;
\*nat ) local table="nat" ;;
\*filter ) local table="filter" ;;
\*mangle ) local table="mangle" ;;
\*raw ) local table="raw" ;;
esac
if [[ "$line" == -A* ]] && [[ "$line" =~ $own_rule ]]; then
chain="${line#-A }"
local chain="${line#-A }"
chain="${chain%% *}"
# Rules inside this chain are removed together by the flush below.
@@ -1383,7 +1462,7 @@ clearTables() {
hasTaggedRules() {
local save="$1"
local rules=""
local rules
local dnat_chain="QEMU_DNAT"
local rule_tag="$dnat_chain"
local own_rule="--comment[[:space:]]+\"?$rule_tag\"?([[:space:]]|\$)"
@@ -1405,9 +1484,8 @@ configureTables() {
local ip="$1"
local subnet="$2"
local preferred=""
local alternate=""
local alternate_save=""
local preferred
local alternate_save
local preferred_clean="N"
local alternate_dirty="N"
@@ -1418,8 +1496,8 @@ configureTables() {
}
case "$preferred" in
"nft" ) alternate="legacy" ;;
"legacy" ) alternate="nft" ;;
"nft" ) local alternate="legacy" ;;
"legacy" ) local alternate="nft" ;;
* )
enabled "$ROOTLESS" && ! enabled "$DEBUG" && return 1
warn "unsupported IP tables backend: $preferred"
@@ -1605,10 +1683,46 @@ configureTables() {
return 1
}
addUpstream() {
local upstream="$1"
local table_error
local rule_tag="QEMU_DNAT"
[ -n "$upstream" ] || return 1
[ -n "$GATEWAY" ] || return 1
if ! ip address add "$upstream/32" dev "$BRIDGE"; then
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
warn "failed to add the system.lan address; access through that name will be unavailable."
fi
return 1
fi
if ! runTableRule "Y" table_error \
iptables -t nat -A PREROUTING \
-i "$BRIDGE" \
-d "$upstream" \
-m comment --comment "$rule_tag" \
-j DNAT --to-destination "$GATEWAY"; then
ip address del "$upstream/32" dev "$BRIDGE" > /dev/null 2>&1 || :
if ! enabled "$ROOTLESS" || enabled "$DEBUG"; then
[ -n "$table_error" ] && echo "$table_error" >&2
warn "failed to configure system.lan forwarding; access through that name will be unavailable."
fi
return 1
fi
return 0
}
configureNAT() {
local tuntap="TUN device is missing. $ADD_ERR --device /dev/net/tun"
local rc ip subnet msg="" forwarding=""
local ip subnet upstream="" forwarding=""
enabled "$DEBUG" && echo "Configuring NAT networking..."
@@ -1616,7 +1730,8 @@ configureNAT() {
if [ ! -c /dev/net/tun ]; then
[ ! -d /dev/net ] && mkdir -m 755 /dev/net > /dev/null 2>&1 || :
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); rc=$?; } || :
local msg
{ msg=$(mknod /dev/net/tun c 10 200 2>&1); local rc=$?; } || :
if (( rc == 0 )); then
chmod 666 /dev/net/tun
@@ -1635,7 +1750,7 @@ configureNAT() {
forwarding=$(< /proc/sys/net/ipv4/ip_forward)
if [[ "$forwarding" != "1" ]]; then
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; rc=$?; } || :
{ sysctl -w net.ipv4.ip_forward=1 > /dev/null 2>&1; local rc=$?; } || :
forwarding=""
[ -r /proc/sys/net/ipv4/ip_forward ] &&
@@ -1657,11 +1772,15 @@ configureNAT() {
local gateway="${ip%.*}.1"
subnet=$(networkCIDR "$ip") || return 1
if [ -n "$GATEWAY" ]; then
upstream=$(upstreamIP "$subnet" "$ip" "$gateway") || upstream=""
fi
if subnetInUse "$subnet"; then
error "VM subnet $subnet conflicts with an existing route inside the container."
return 1
else
rc=$?
local rc=$?
(( rc == 1 )) || return 1
fi
@@ -1675,16 +1794,20 @@ configureNAT() {
configureTables "$ip" "$subnet" || return 1
if [ -n "$upstream" ] && ! addUpstream "$upstream"; then
upstream=""
fi
NET_OPTS="-netdev tap,id=hostnet0,ifname=$TAP"
if [ -c /dev/vhost-net ]; then
{ exec 40>>/dev/vhost-net; rc=$?; } 2>/dev/null || :
{ exec 40>>/dev/vhost-net; local rc=$?; } 2>/dev/null || :
(( rc == 0 )) && NET_OPTS+=",vhost=on,vhostfd=40"
fi
NET_OPTS+=",script=no,downscript=no"
configureDNS "$BRIDGE" "$ip" "$MAC" "$HOST" "$MASK" "$gateway" || return 1
configureDNS "$BRIDGE" "$ip" "$MAC" "$HOST" "$MASK" "$gateway" "$upstream" || return 1
IP="$ip"
return 0
@@ -1740,7 +1863,7 @@ closeNetwork() {
checkOS() {
local iface="macvlan"
local os="" kernel=""
local os="" kernel
kernel=$(uname -a)
@@ -1798,7 +1921,10 @@ validateHost() {
validateHostPorts() {
if isNAT && [[ "${HOST_PORTS,,}" == *"/udp"* ]]; then
local custom
custom=$(getCustomHostPorts "all")
if isNAT && [[ "$custom" == *"/udp"* ]]; then
warn "UDP ports in \"HOST_PORTS\" are not yet implemented for NAT networking."
fi
@@ -1891,14 +2017,12 @@ configureMTU() {
configureMAC() {
local container=""
local file=""
local container
container=$(containerID)
if [ -z "$MAC" ]; then
file="$STORAGE/dsm.mac"
local file="$STORAGE/dsm.mac"
if [ -s "$file" ]; then
if ! MAC=$(readFile "$file"); then
@@ -1941,7 +2065,7 @@ configureMAC() {
showHostInfo() {
local mtu="" host="" uplink="" prefix=""
local mtu host uplink prefix
prefix=$(ip -4 -o address show dev "$DEV" scope global 2>/dev/null |
awk -v ip="$UPLINK" '
@@ -2028,10 +2152,10 @@ showGuestInfo() {
local mode="${NETWORK,,}"
if isNAT; then
mode="NAT"
elif enabled "$DHCP"; then
if enabled "$DHCP"; then
mode="DHCP"
elif isNAT; then
mode="NAT"
elif isUserMode; then
mode="User ($mode)"
elif [ -z "$mode" ]; then
+15 -25
View File
@@ -67,16 +67,8 @@ displayReason() {
readQemuPid() {
local -n _pid="$1"
local file
for file in "$QEMU_START_PID" "$QEMU_PID"; do
if [ -s "$file" ] && read -r _pid < "$file"; then
return 0
fi
done
return 1
readPidFile "$1" "$QEMU_START_PID" && return 0
readPidFile "$1" "$QEMU_PID"
}
qemuPidFile() {
@@ -92,7 +84,7 @@ qemuPidFile() {
waitQemuExit() {
local timeout="${1:-10}"
local file=""
local file
qemuPidFile file
waitPidFile "$file" "$timeout"
@@ -100,26 +92,24 @@ waitQemuExit() {
waitQemuPid() {
local -n _pid="$1"
local cnt=0 value=""
local cnt=0
while ! readQemuPid value; do
while ! readQemuPid "$1"; do
sleep 0.02
cnt=$((cnt + 1))
(( cnt >= 50 )) && return 1
done
_pid="$value"
return 0
}
forceKillQemu() {
local reason="$1"
local pid="" display
local pid display
! readQemuPid pid && return 0
! isAlive "$pid" && return 0
readQemuPid pid || return 0
isAlive "$pid" || return 0
display=$(displayReason "$reason")
error "Forcefully terminating $(app), reason: $display..."
@@ -145,7 +135,7 @@ cleanupHelpers() {
startConsole() {
local output="${1:-/dev/tty}"
local cnt=0 pid=""
local cnt=0
rm -f -- "$CONSOLE_SOCKET" "$CONSOLE_PID"
@@ -159,7 +149,7 @@ startConsole() {
exec nc -lU "$CONSOLE_SOCKET" </dev/tty >"$output"
) &
pid=$!
local pid="$!"
echo "$pid" > "$CONSOLE_PID"
while [ ! -S "$CONSOLE_SOCKET" ]; do
@@ -246,14 +236,14 @@ finish() {
sendGuestShutdown() {
local pid="$1"
local response url
local response
# Don't send the powerdown signal because vDSM ignores ACPI signals
# nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :
# Send shutdown command to guest agent via serial port
API_TIMEOUT=$(strip "$API_TIMEOUT")
url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
local url="http://localhost/read?command=$API_CMD&timeout=$API_TIMEOUT"
response=$(curl --unix-socket "$HOST_API_SOCKET" -sk -m "$(( API_TIMEOUT+2 ))" -S "$url" 2>&1)
if [[ "$response" =~ "\"success\"" ]]; then
@@ -315,7 +305,7 @@ waitForShutdown() {
local slp=$!
# Stop waiting if the process has exited
! isAlive "$pid" && break
isAlive "$pid" || break
# Workaround for stale/zombie QEMU pid file
[ ! -s "$QEMU_START_PID" ] && [ ! -s "$QEMU_PID" ] && break
@@ -338,7 +328,7 @@ waitForShutdown() {
graceful_shutdown() {
local sig="$1"
local pid="" code=0
local pid code
[[ $BASHPID != "$TRAP_PID" ]] && return
@@ -382,7 +372,7 @@ graceful_shutdown() {
finish "$code"
}
! enabled "$SHUTDOWN" && return 0
enabled "$SHUTDOWN" || return 0
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"
if interactive; then
+27 -13
View File
@@ -9,6 +9,7 @@ cd /run
. utils.sh # Load functions
info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "$1" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;33m " "WARNING: $1" "\E[0m\n" >&2; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: $1" "\E[0m\n" >&2; }
disabled "$NETWORK" && exit 0
@@ -36,9 +37,7 @@ exitIfShuttingDown() {
queryGuest() {
local rc
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); rc=$?; } || :
{ json=$(curl --unix-socket "$socket" -m 20 -sk "$url"); local rc=$?; } || :
exitIfShuttingDown
@@ -54,9 +53,8 @@ readJsonField() {
local query="$1"
local result
local rc
{ result=$(jq -r "$query" <<< "$json"); rc=$?; } || :
{ result=$(jq -r "$query" <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then
error "$jq_err $rc ( $json )"
@@ -74,12 +72,12 @@ readJsonField() {
readGuestStatus() {
local result msg rc
local result msg
result=$(readJsonField '.status') || return 1
if [[ "$result" != "success" ]]; then
{ msg=$(jq -r '.message // empty' <<< "$json"); rc=$?; } || :
{ msg=$(jq -r '.message // empty' <<< "$json"); local rc=$?; } || :
if (( rc != 0 )); then
error "$jq_err $rc ( $json )"
@@ -140,14 +138,29 @@ pollGuestLocation() {
return 0
}
checkAddressConflict() {
local guest_ip="${location%:*}"
local container_ip=""
[ -s "$address" ] && container_ip=$(<"$address")
[ -z "$container_ip" ] && return 0
[[ "$guest_ip" != "$container_ip" ]] && return 0
warn "DSM is using the same IP as the container, this will cause connectivity issues."
warn "change the container's macvlan IP or assign DSM a different address in your router."
return 0
}
writeDhcpPage() {
local title body script html
local html
msg="http://$location"
title="<title>Virtual DSM</title>"
body="The location of DSM is <a href='http://$location'>http://$location</a>"
script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
local title="<title>Virtual DSM</title>"
local body="The location of DSM is <a href='http://$location'>http://$location</a>"
local script="<script>setTimeout(function(){ window.location.assign('http://$location'); }, 3000);</script>"
html=$(<"$template")
html="${html/\[1\]/$title}"
@@ -164,11 +177,11 @@ writeDhcpPage() {
buildStaticMessage() {
local nic ip port
local nic ip
nic=$(<"$driver")
ip=$(<"$address")
port="${location##*:}"
local port="${location##*:}"
if [[ "${nic,,}" != "macvlan" ]]; then
msg="port $port"
@@ -196,6 +209,7 @@ exitIfShuttingDown
location=$(<"$file")
if enabled "$DHCP"; then
checkAddressConflict
writeDhcpPage
else
buildStaticMessage
+1 -1
View File
@@ -47,7 +47,7 @@ checkSse42() {
if ! hasFlag "sse4_2"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88
enabled "$DEBUG" || exit 88
fi
return 0
+82 -7
View File
@@ -2,6 +2,7 @@
set -Eeuo pipefail
info="/run/shm/msg.html"
info_tmp="${info}.${BASHPID}.tmp"
escape() {
@@ -17,6 +18,23 @@ escape() {
return 0
}
writeInfo() {
local content="$1"
if ! printf '%s\n' "$content" > "$info_tmp"; then
rm -f -- "$info_tmp"
return 1
fi
if ! mv -f -- "$info_tmp" "$info"; then
rm -f -- "$info_tmp"
return 1
fi
return 0
}
getBytes() {
local path="$1"
@@ -48,6 +66,24 @@ getBytes() {
return 0
}
getStatus() {
local file="$1"
local bytes total extra=""
[ -r "$file" ] || return 1
read -r bytes total extra < "$file" || return 1
if [[ ! "$bytes" =~ ^[0-9]+$ ||
! "$total" =~ ^[0-9]+$ ||
-n "$extra" ]]; then
return 1
fi
printf '%s %s\n' "$bytes" "$total"
return 0
}
formatSize() {
local bytes="$1"
@@ -117,7 +153,19 @@ printSizeProgress() {
return 0
}
finishLogProgress() {
stopProgress() {
if [ -z "$status_file" ]; then
exit 0
fi
stopping="Y"
return 0
}
finishProgress() {
rm -f -- "$info_tmp"
if [[ "$output" == "log" && "$printed" == "Y" ]]; then
printf '\n'
@@ -132,6 +180,7 @@ body=$(escape "$3")
output="${4:-}"
step_bytes="${5:-536870912}"
mode="${6:-apparent}"
status_file="${7:-}"
if [[ -n "$total" && ! "$total" =~ ^(0|[1-9][0-9]*)$ ]]; then
printf 'Invalid total size: %s\n' "$total" >&2
@@ -163,13 +212,15 @@ printed="N"
next_percent=10
next_bytes="$step_bytes"
log_mode="percent"
stopping="N"
if [ -z "$total" ] || [[ "$total" == "0" ]]; then
log_mode="size"
fi
trap finishLogProgress EXIT
trap 'exit 0' HUP INT QUIT TERM
trap finishProgress EXIT
trap 'exit 0' HUP INT QUIT
trap stopProgress TERM
if [[ "$body" == *"..." ]]; then
body="<p class=\"loading\">${body::-3}</p>"
@@ -177,13 +228,34 @@ fi
while true; do
final_pass="${stopping:-}"
bytes=$(getBytes "$path" "$mode")
effective_total="$total"
if [ -n "$status_file" ] && status=$(getStatus "$status_file"); then
read -r status_bytes status_total <<< "$status"
bytes="$status_bytes"
if (( status_total > 0 )); then
effective_total="$status_total"
fi
fi
# A real total may become available shortly after aria2 starts.
if [[ "$log_mode" == "size" &&
"$printed" == "N" &&
-n "$effective_total" &&
"$effective_total" != "0" ]]; then
log_mode="percent"
fi
if (( bytes > 4096 )); then
write_html="Y"
if [ -z "$total" ] || [[ "$total" == "0" ]] || (( bytes > total )); then
if [ -z "$effective_total" ] ||
[[ "$effective_total" == "0" ]] ||
(( bytes > effective_total )); then
size=$(formatSize "$bytes")
if [[ "$output" == "log" ]]; then
@@ -197,7 +269,7 @@ while true; do
fi
else
# Truncate to one decimal so progress is never reported early.
progress=$((bytes * 1000 / total))
progress=$((bytes * 1000 / effective_total))
(( progress > 1000 )) && progress=1000
percent=$((progress / 10))
@@ -219,9 +291,12 @@ while true; do
fi
if [[ "$write_html" == "Y" ]]; then
printf '%s\n' "${body//(\[P\])/($size)}" > "$info"
writeInfo "${body//(\[P\])/($size)}"
fi
fi
sleep 1 & wait $!
[[ "$final_pass" == "Y" ]] && break
sleep 1 &
wait $! || :
done
+11 -18
View File
@@ -41,7 +41,7 @@ detectEngine() {
detectRootless() {
local uid_map=""
local uid_map
uid_map=$(awk '{$1=$1; print}' /proc/self/uid_map 2>/dev/null || true)
@@ -58,8 +58,6 @@ checkPrivileged() {
local cap_bnd
local last_cap
local max_cap
# Get the capability bounding set
cap_bnd=$(grep '^CapBnd:' /proc/$$/status | awk '{print $2}')
cap_bnd=$(printf "%d" "0x${cap_bnd}")
@@ -68,7 +66,7 @@ checkPrivileged() {
last_cap=$(cat /proc/sys/kernel/cap_last_cap)
# Calculate the maximum capability value
max_cap=$(((1 << (last_cap + 1)) - 1))
local max_cap=$(((1 << (last_cap + 1)) - 1))
if [ "$cap_bnd" -eq "$max_cap" ]; then
PRIVILEGED="Y"
@@ -144,9 +142,7 @@ finiteMemoryLimit() {
local limit="$1"
local sentinel="4611686018427387904"
local i=0
local left=""
local right=""
local i
[[ "$limit" =~ ^[0-9]+$ ]] || return 1
@@ -154,8 +150,8 @@ finiteMemoryLimit() {
(( ${#limit} > ${#sentinel} )) && return 1
for (( i=0; i<${#sentinel}; i++ )); do
left="${limit:i:1}"
right="${sentinel:i:1}"
local left="${limit:i:1}"
local right="${sentinel:i:1}"
(( left < right )) && return 0
(( left > right )) && return 1
@@ -166,11 +162,8 @@ finiteMemoryLimit() {
getMemoryInfo() {
local host_total=""
local host_avail=""
local limit=""
local current=""
local available=""
local limit="" current=""
local host_total host_avail
host_total=$(free -b | awk '/^Mem:/ {print $2; exit}')
host_avail=$(free -b | awk '/^Mem:/ {print $7; exit}')
@@ -189,7 +182,7 @@ getMemoryInfo() {
if finiteMemoryLimit "$limit" && [[ "$current" =~ ^[0-9]+$ ]]; then
(( limit < RAM_TOTAL )) && RAM_TOTAL="$limit"
available=$(( limit - current ))
local available=$(( limit - current ))
(( available < 0 )) && available=0
(( available < RAM_AVAIL )) && RAM_AVAIL="$available"
fi
@@ -216,7 +209,7 @@ normalizeRamSize() {
fi
RAM_SIZE=$(echo "${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
! numfmt --from=iec "$RAM_SIZE" &>/dev/null && error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16
numfmt --from=iec "$RAM_SIZE" &>/dev/null || { error "Invalid RAM_SIZE: $RAM_SIZE" && exit 16; }
wanted=$(numfmt --from=iec "$RAM_SIZE")
[ "$wanted" -lt "$RAM_MINIMUM" ] && error "RAM_SIZE is too low: $RAM_SIZE" && exit 16
@@ -261,7 +254,7 @@ checkKvm() {
fi
if ! grep -qw "sse4_2" <<< "$flags"; then
error "Your CPU does not have the SSE4 instruction set that Virtual DSM requires!"
! enabled "$DEBUG" && exit 88
enabled "$DEBUG" || exit 88
fi
fi
fi
@@ -282,7 +275,7 @@ checkKvm() {
error "KVM acceleration is not available $KVM_ERR, this will cause the machine to run about 10 times slower."
error "See the FAQ for possible causes, or disable acceleration by adding the \"KVM=N\" variable (not recommended)." ;;
esac
! enabled "$DEBUG" && exit 88
enabled "$DEBUG" || exit 88
fi
fi
+6 -7
View File
@@ -73,7 +73,7 @@ startHostBinary() {
pid=$!
fi
echo "$pid" > "$HOST_PID"
printf '%s\n' "$pid" > "$HOST_PID"
return 0
}
@@ -82,23 +82,22 @@ waitForSocket() {
local socket="$1"
local exit_code="$2"
local pid cnt=0
local timeout=5 pid
local deadline=$((SECONDS + timeout))
while [ ! -S "$socket" ]; do
if ! read -r pid < "$HOST_PID" || ! isAlive "$pid"; then
if ! readPidFile pid "$HOST_PID" || ! isAlive "$pid"; then
error "qemu-host exited unexpectedly!"
exit "$exit_code"
fi
sleep 0.1
cnt=$((cnt + 1))
if (( cnt > 50 )); then
if (( SECONDS >= deadline )); then
error "Failed to create qemu-host socket: $socket"
exit "$exit_code"
fi
sleep 0.1
done
return 0
+68 -15
View File
@@ -15,24 +15,35 @@ WSD_PID="$QEMU_DIR/websocketd.pid"
prepareWebFiles() {
cp -r /var/www/* "$QEMU_DIR" || return 1
rm -f "$WSD_PID" "$WEB_PID" "$WSD_LOG" || return 1
rm -f -- "$WSD_PID" "$WEB_PID" "$WSD_LOG" || return 1
return 0
}
configureWebPorts() {
sed -i "s/listen 5000 default_server;/listen $WEB_PORT default_server;/g" /etc/nginx/sites-enabled/web.conf
sed -i "s/proxy_pass http:\/\/127.0.0.1:8004\/;/proxy_pass http:\/\/127.0.0.1:$WSD_PORT\/;/g" /etc/nginx/sites-enabled/web.conf
if ! sed -i \
-e "s|listen 5000 default_server;|listen $WEB_PORT default_server;|g" \
-e "s|proxy_pass http://127.0.0.1:8004/;|proxy_pass http://127.0.0.1:$WSD_PORT/;|g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure webserver ports!"
return 1
fi
return 0
}
configureIpv6Listen() {
# shellcheck disable=SC2143
if [ -f /proc/net/if_inet6 ] && [[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" != "1" ]]; then
sed -i "s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" /etc/nginx/sites-enabled/web.conf
if ! sed -i \
"s/listen $WEB_PORT default_server;/listen [::]:$WEB_PORT default_server ipv6only=off;/g" \
/etc/nginx/sites-enabled/web.conf; then
error "Failed to configure IPv6 webserver listener!"
return 1
fi
fi
return 0
@@ -49,6 +60,22 @@ configureWebServer() {
return 0
}
stopWebServer() {
local pid
if readPidFile pid "$WEB_PID"; then
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
fi
rm -f -- "$WEB_PID"
return 0
}
startWebServer() {
# Start webserver
@@ -57,6 +84,22 @@ startWebServer() {
return 0
}
stopWebsocketServer() {
local pid
if readPidFile pid "$WSD_PID"; then
pKill "$pid" 2
if isAlive "$pid"; then
kill -9 -- "$pid" 2>/dev/null || :
fi
fi
rm -f -- "$WSD_PID"
return 0
}
startWebsocketServer() {
# Start websocket server
@@ -70,17 +113,23 @@ startWebsocketServer() {
if ! echo "$pid" > "$WSD_PID"; then
kill "$pid" 2>/dev/null || :
rm -f -- "$WSD_PID"
return 1
fi
sleep 0.1
local i
for (( i = 1; i <= 5; i++ )); do
if ! isAlive "$pid"; then
rm -f "$WSD_PID"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!"
return 1
fi
if ! isAlive "$pid"; then
rm -f -- "$WSD_PID"
[ -s "$WSD_LOG" ] && cat "$WSD_LOG" >&2
error "Failed to start websocket server!"
return 1
fi
sleep 0.1
done
return 0
}
@@ -93,7 +142,11 @@ disabled "${WEB:-}" && return 0
configureWebServer
startWebServer
startWebsocketServer
if startWebServer && startWebsocketServer; then
return 0
fi
return 0
stopWebsocketServer || :
stopWebServer || :
return 1
+18 -5
View File
@@ -3,6 +3,8 @@ set -Eeuo pipefail
lastmsg=""
path="/run/shm/msg.html"
dir=$(dirname -- "$path")
name=$(basename -- "$path")
refresh() {
@@ -17,15 +19,26 @@ refresh() {
lastmsg="$msg"
echo "s: $msg"
return 0
}
refresh
inotifywait -m "$path" |
while read -r fp event fn; do
inotifywait \
-m -q \
-e close_write,moved_to,delete \
--format '%e %f' \
"$dir" |
while read -r event file; do
[[ "$file" == "$name" ]] || continue
case "${event,,}" in
"modify"* ) refresh ;;
"delete_self" ) echo "c: vnc" ;;
esac
"delete"* )
echo "c: vnc" ;;
"close_write"* | "moved_to"* )
refresh ;;
esac
done
+57 -29
View File
@@ -7,6 +7,24 @@ info () { printf "%b%s%b" "\E[1;34m \E[1;36m" "${1:-}" "\E[0m\n"; }
error () { printf "%b%s%b" "\E[1;31m " "ERROR: ${1:-}" "\E[0m\n" >&2; }
warn () { printf "%b%s%b" "\E[1;31m " "Warning: ${1:-}" "\E[0m\n" >&2; }
readPidFile() {
local -n _pid="$1"
_pid=""
if ! _pid=$(cat -- "$2" 2>/dev/null); then
_pid=""
return 1
fi
if [[ ! "$_pid" =~ ^[1-9][0-9]*$ ]]; then
_pid=""
return 1
fi
return 0
}
hasFlag() {
# Match a whitespace-delimited token in /proc/cpuinfo
@@ -118,14 +136,13 @@ isAlive() {
waitPid() {
local i=0
local pid="$1"
local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
while [ -n "$pid" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done
return 0
@@ -133,19 +150,16 @@ waitPid() {
waitPidFile() {
local i=0
local pid=""
local pid
local file="$1"
local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
[ ! -s "$file" ] && return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
readPidFile pid "$file" || return 0
while [ -s "$file" ] && isAlive "$pid"; do
(( SECONDS >= deadline )) && return 1
sleep 0.2
i=$((i + 1))
(( i >= timeout * 5 )) && return 1
done
rm -f -- "$file"
@@ -168,19 +182,19 @@ pKill() {
fWait() {
local i=0
local name="$1"
local timeout="${2:-10}"
local deadline=$((SECONDS + timeout))
[ -z "$name" ] && return 0
while pgrep -f -l "$name" >/dev/null; do
sleep 0.2
i=$((i + 1))
if (( i >= timeout * 5 )); then
if (( SECONDS >= deadline )); then
warn "Timed out while waiting for process: $name"
break
fi
sleep 0.2
done
return 0
@@ -201,12 +215,10 @@ fKill() {
sKill() {
local pid=""
local pid
local file="$1"
[ ! -s "$file" ] && return 0
! read -r pid <"$file" && return 0
[ -z "$pid" ] && return 0
readPidFile pid "$file" || return 0
if isAlive "$pid"; then
{ kill -15 -- "$pid" || :; } 2>/dev/null
@@ -244,7 +256,7 @@ setOwner() {
uid=$(stat -c '%u' "$dir") || return 1
gid=$(stat -c '%g' "$dir") || return 1
! chown "$uid:$gid" "$file" && return 1
chown "$uid:$gid" "$file" || return 1
return 0
}
@@ -255,7 +267,7 @@ makeDir() {
local dir uid gid
[ -d "$path" ] && return 0
! mkdir -p "$path" && return 1
mkdir -p "$path" || return 1
dir=$(dirname -- "$path")
@@ -300,6 +312,25 @@ writeFile() {
return 0
}
writeAtomic() {
local path="$1"
local content="$2"
local tmp="${path}.${BASHPID}.tmp"
if ! printf '%s\n' "$content" > "$tmp"; then
rm -f -- "$tmp"
return 1
fi
if ! mv -f -- "$tmp" "$path"; then
rm -f -- "$tmp"
return 1
fi
return 0
}
readFile() {
local path="$1"
@@ -362,8 +393,7 @@ restoreState() {
escape () {
local s
s=${1//&/\&amp;}
local s=${1//&/\&amp;}
s=${s//</\&lt;}
s=${s//>/\&gt;}
s=${s//'"'/\&quot;}
@@ -389,7 +419,7 @@ html() {
local title
local body
local script
local script="${2:-}"
local footer
title=$(escape "$APP")
@@ -401,8 +431,6 @@ html() {
body="<p class=\"loading\">${body/.../}</p>"
fi
[ -n "${2:-}" ] && script="$2" || script=""
local HTML
HTML=$(<"$TEMPLATE")
HTML="${HTML/\[1\]/$title}"
@@ -411,8 +439,8 @@ html() {
HTML="${HTML/\[4\]/$footer}"
HTML="${HTML/\[5\]/$FOOTER2}"
echo "$HTML" > "$PAGE" || return 1
echo "$body" > "$INFO" || return 1
writeAtomic "$PAGE" "$HTML" || return 1
writeAtomic "$INFO" "$body" || return 1
return 0
}
@@ -459,9 +487,9 @@ getCountry() {
local url=$1
local query=$2
local rc json result
local json result
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); rc=$?; } || :
{ json=$(curl -m 5 -H "Accept: application/json" -sfk "$url"); local rc=$?; } || :
(( rc != 0 )) && return 0
{ result=$(echo "$json" | jq -r "$query" 2> /dev/null); rc=$?; } || :
+48 -8
View File
@@ -1,15 +1,27 @@
var timer;
var request;
var booting = false;
var interval = 1000;
function abortRequest() {
if (!request) {
return false;
}
request.onreadystatechange = null;
request.abort();
request = null;
return true;
}
function getInfo() {
var url = "msg.html";
try {
if (request) {
request.abort();
}
abortRequest();
if (window.XMLHttpRequest) {
request = new XMLHttpRequest();
@@ -56,7 +68,17 @@ function processInfo() {
return true;
}
var msg = request.responseText;
var response = request;
request = null;
var status = response.status;
if (status == 502 || status == 503 || status == 504) {
schedule();
return true;
}
var msg = response.responseText;
if (msg == null || msg.length == 0) {
if (booting) {
@@ -68,9 +90,9 @@ function processInfo() {
return false;
}
var notFound = (request.status == 404);
var notFound = (status == 404);
if (request.status == 200) {
if (status == 200) {
if (msg.toLowerCase().indexOf("<html>") !== -1) {
notFound = true;
} else {
@@ -88,7 +110,7 @@ function processInfo() {
return true;
}
setError("Error: Received statuscode " + request.status);
setError("Error: Received statuscode " + status);
return false;
} catch (e) {
@@ -157,7 +179,9 @@ function setError(text) {
}
function schedule() {
setTimeout(getInfo, interval);
clearTimeout(timer);
timer = setTimeout(getInfo, interval);
}
function reload() {
@@ -179,11 +203,27 @@ function connect() {
switch (cmd) {
case "s":
var aborted = abortRequest();
processMsg(msg);
if (aborted &&
msg.toLowerCase().indexOf("href=") == -1) {
schedule();
}
break;
case "e":
if (abortRequest()) {
schedule();
}
setError(msg);
break;
default:
console.warn("Unknown event: " + cmd);
break;