vpsAdminOS provozuje VPS v kontejnerech, nikoli ve virtuálních strojích. KVM
uvnitř VPS proto není vnořená virtualizace: virtuální stroje používají
hardwarovou virtualizaci fyzického nodu přímo.
Tento návod používá aktuální šablonu Debian (latest), libvirt a systémové
spojení qemu:///system.
Pro virtualizaci je potřeba funkce KVM. TUN/TAP je potřeba pro síťová rozhraní virtuálních strojů typu TAP. Obě funkce jsou u nových VPS zapnuté ve výchozím nastavení v sekci Funkce v detailu VPS ve vpsAdminu.
Nainstaluj QEMU, libvirt a virt-install z Debianu:
#!/usr/bin/env bash set -euo pipefail export DEBIAN_FRONTEND=noninteractive apt-get update apt-get install --yes \ iptables \ libvirt-clients \ libvirt-daemon-system \ qemu-system-x86 \ qemu-utils \ virtinst virsh --connect qemu:///system version
Používej systémové spojení qemu:///system. Pro vzdálenou správu použij
libvirt přes SSH, například qemu+ssh://root@HOST/system, a chraň SSH
klíčem. Nezveřejňuj nešifrovaný libvirt TCP socket do internetu.
Pro obrazy disků doporučujeme samostatný subdataset. V budoucnu tak můžeš změnit jeho ZFS vlastnosti bez zásahu do kořenového datasetu VPS. Doporučené úložiště připrav takto:
vm-images s uvolněnou kapacitou.vm-images použij Mount a připoj jej do VPS jako /srv/libvirt/images.
Například z běžné kapacity 120 GiB můžeš ponechat 20 GiB kořenovému datasetu
a 100 GiB přidělit datasetu vm-images. Prvnímu disku virtuálního stroje pak
můžeš přidělit například 80 GiB a ponechat 20 GiB volných pro jeho růst a
snapshoty.
Subdataset se snapshotuje odděleně od kořenového datasetu VPS.
Připojený adresář přidej do libvirt jako trvalý storage pool:
#!/usr/bin/env bash set -euo pipefail virsh --connect qemu:///system pool-define-as \ vm-images dir --target /srv/libvirt/images virsh --connect qemu:///system pool-start vm-images virsh --connect qemu:///system pool-autostart vm-images virsh --connect qemu:///system pool-info vm-images
Disky pak vytvářej prostřednictvím libvirt, virt-install nebo
virt-manager v poolu vm-images.
ZFS vlastnosti subdatasetu ponech na výchozích hodnotách, dokud měření
neukáže důvod ke změně. Komprese je ve výchozím nastavení zapnutá. Výchozí
recordsize=128K určuje maximální velikost záznamu souboru; ZFS podle potřeby
používá i menší záznamy. Změna recordsize se projeví pouze u nově
vytvořených souborů.
Pro obrazy na ZFS je vhodnou výchozí volbou formát raw, protože nepřidává
další copy-on-write vrstvu. qcow2 použij, pokud potřebuješ jeho backing
files, interní snapshoty nebo jiné funkce obrazového formátu.
Podrobnosti k ladění podle zátěže popisuje OpenZFS Workload Tuning, formáty obrazů pak dokumentace QEMU.
Síťové rozhraní VPS je k nodu připojené směrovaným spojem na třetí vrstvě. Nepřidávej proto rozhraní VPS do bridge spravovaného libvirt. Pro připojení virtuálních strojů k síti jsou vhodná dvě zapojení:
| Zapojení | Adresy v doméně | Výhody | Nevýhody |
|---|---|---|---|
| NAT v libvirt | Privátní IPv4 a privátní ULA IPv6 | Funguje s běžnými adresami VPS, snadno obslouží více domén. | Příchozí služby potřebují přesměrování portů a provoz prochází NAT44 i NAT66. |
| Směrované veřejné adresy | Veřejná IPv4 /32 a adresy ze samostatné IPv6 /64 | Doména je dostupná přímo, bez NAT a bez překladu portů. | Vyžaduje privátní IPv4 na VPS, další směrovanou IPv6 síť a pečlivě nastavený firewall. |
NAT je obvykle jednodušší pro jednu nebo několik domén s malým počtem zveřejněných služeb. Směrované adresy použij, když má mít doména vlastní veřejné adresy, všechny porty nebo protokoly, které se přes port forwarding obsluhují špatně. Pro čistě vnitřní komunikaci mezi doménami můžeš přidat další izolovanou síť libvirt; přístup ven nadále řeší jedno z uvedených zapojení.
Veřejná IPv4 /32 i IPv6 /64 zůstanou na VPS. Doména dostane
192.168.124.10/24 a fd5f:6d2e:9c4a:124::10/64 ze sítě
dualstack-nat. Libvirt překládá odchozí IPv4 pomocí NAT44 a IPv6 pomocí
NAT66. Výchozí síť default v běžné instalaci libvirt obvykle obsahuje jen
IPv4 a používá 192.168.122.0/24, proto zde definujeme vlastní
dvouprotokolovou síť s odlišnou IPv4 podsítí. NAT66 se v XML zapíná pomocí
<nat ipv6='yes'/>; podrobnosti jsou v
dokumentaci formátu sítí libvirt.
ULA fd5f:6d2e:9c4a::/48 je příklad pro samostatnou instalaci. Pokud budeš
více privátních sítí později propojovat pomocí VPN nebo směrování, vytvoř pro
každou z nich vlastní náhodný ULA /48 a změň HOST_IPV6 i
GUEST_IPV6 na adresy ze stejné /64.
Podstatnou část definice sítě tvoří následující XML:
<network> <name>dualstack-nat</name> <forward mode='nat'> <nat ipv6='yes'/> </forward> <bridge name='virbr-nat'/> <ip address='192.168.124.1' prefix='24'/> <ip family='ipv6' address='fd5f:6d2e:9c4a:124::1' prefix='64'/> </network>
Vytvořenou doménu připoj k dualstack-nat, například volbou
–network network=dualstack-nat,model=virtio ve virt-install. V doméně
nastav stabilní adresy a brány:
auto ens3
iface ens3 inet static
address 192.168.124.10/24
gateway 192.168.124.1
iface ens3 inet6 static
address fd5f:6d2e:9c4a:124::10/64
gateway fd5f:6d2e:9c4a:124::1
Přesměrování portu se skládá z DNAT na adresu domény a povolení odpovídajícího forwardu. Pro web na IPv4 jsou podstatou například tyto dva příkazy:
iptables -t nat -I PREROUTING 1 -p tcp -d VPS_IPV4 --dport 80 \ -j DNAT --to-destination 192.168.124.10:80 iptables -t filter -I FORWARD 1 -p tcp -d 192.168.124.10 --dport 80 \ -j ACCEPT
Stejná pravidla je potřeba vytvořit i pomocí ip6tables pro IPv6. Ručně
zadaná pravidla však libvirt při změnách sítě nezachová. Následující úplný
skript proto vytvoří síť, soubor s přesměrováním a hook, který pravidla obnoví
při spuštění nebo znovupřipojení sítě. Ve výchozím nastavení zveřejní web na
portu 80 a SSH domény na portu 2222 pro IPv4 i IPv6.
Ulož jej například jako /root/configure-nat-port-forwards.sh a spusť
s jednou veřejnou IPv4 a jednou adresou z IPv6 /64 nakonfigurovanou na VPS:
PUBLIC_IPV4=VPS_IPV4 PUBLIC_IPV6=VPS_IPV6 \ /root/configure-nat-port-forwards.sh
Skript se spouští při prvním vytvoření sítě a znovu při změně jeho vstupních hodnot, ne po každém restartu VPS. Síť má zapnutý autostart a pravidla při jejím spuštění načte hook.
#!/usr/bin/env bash set -euo pipefail : "${PUBLIC_IPV4:?set PUBLIC_IPV4 to the VPS public IPv4 address}" : "${PUBLIC_IPV6:?set PUBLIC_IPV6 to a public IPv6 address of the VPS}" guest_ipv4=${GUEST_IPV4:-192.168.124.10} guest_ipv6=${GUEST_IPV6:-fd5f:6d2e:9c4a:124::10} host_ipv4=${HOST_IPV4:-192.168.124.1} host_ipv6=${HOST_IPV6:-fd5f:6d2e:9c4a:124::1} connection=qemu:///system network=dualstack-nat hook=/etc/libvirt/hooks/network.d/50-port-forwards config=/etc/libvirt/port-forwards.conf xml=$(mktemp) trap 'rm -f "$xml"' EXIT network_uuid=$(virsh --connect "$connection" net-uuid "$network" 2>/dev/null || :) uuid_element= if [[ -n $network_uuid ]]; then uuid_element=" <uuid>$network_uuid</uuid>" fi cat >"$xml" <<EOF <network> <name>$network</name> $uuid_element <forward mode='nat'> <nat ipv6='yes'/> </forward> <bridge name='virbr-nat' stp='on' delay='0'/> <ip address='$host_ipv4' prefix='24'> <dhcp> <range start='192.168.124.100' end='192.168.124.254'/> </dhcp> </ip> <ip family='ipv6' address='$host_ipv6' prefix='64'/> </network> EOF if virsh --connect "$connection" net-list --name \ | grep -Fx "$network" >/dev/null; then printf '%s is active. Shut down its attached domains, run ' "$network" >&2 printf 'virsh net-destroy %s, then rerun this script.\n' "$network" >&2 exit 1 fi install -d -m 0755 /etc/libvirt/hooks/network.d install -d -m 0755 /etc/libvirt cat >"$hook" <<'HOOK' #!/usr/bin/env bash set -euo pipefail PATH=/usr/sbin:/usr/bin:/sbin:/bin network=${1-} action=${2-} config=/etc/libvirt/port-forwards.conf nat_chain=VPSFREE_KVM_DNAT filter_chain=VPSFREE_KVM_FWD [[ $network == dualstack-nat ]] || exit 0 exec 9>/run/lock/vpsfree-kvm-port-forwards.lock flock 9 delete_jumps() { local tool for tool in iptables ip6tables; do while "$tool" -w -t nat -C PREROUTING -j "$nat_chain" 2>/dev/null; do "$tool" -w -t nat -D PREROUTING -j "$nat_chain" done while "$tool" -w -t filter -C FORWARD -j "$filter_chain" 2>/dev/null; do "$tool" -w -t filter -D FORWARD -j "$filter_chain" done done } delete_chains() { local tool for tool in iptables ip6tables; do if "$tool" -w -t nat -S "$nat_chain" >/dev/null 2>&1; then "$tool" -w -t nat -F "$nat_chain" "$tool" -w -t nat -X "$nat_chain" fi if "$tool" -w -t filter -S "$filter_chain" >/dev/null 2>&1; then "$tool" -w -t filter -F "$filter_chain" "$tool" -w -t filter -X "$filter_chain" fi done } cleanup() { delete_jumps delete_chains } valid_ip_address() { local family=$1 address=$2 perl -MSocket=AF_INET,AF_INET6,inet_pton -e ' my ($family, $address) = @ARGV; my $af = $family eq "ipv4" ? AF_INET : AF_INET6; exit(defined(inet_pton($af, $address)) ? 0 : 1); ' "$family" "$address" } if [[ $action == stopped ]]; then cleanup exit 0 fi [[ $action == started || $action == reconnect ]] || exit 0 entries=() while read -r family protocol public_ip public_port guest_ip guest_port extra \ || [[ -n ${family:-} ]]; do [[ -n ${family:-} && $family != \#* ]] || continue [[ $family == ipv4 || $family == ipv6 ]] || { printf 'Invalid address family in %s: %s\n' "$config" "$family" >&2 exit 1 } [[ $protocol == tcp || $protocol == udp ]] || { printf 'Invalid protocol in %s: %s\n' "$config" "$protocol" >&2 exit 1 } for address in "$public_ip" "$guest_ip"; do valid_ip_address "$family" "$address" || { printf 'Invalid %s address in %s: %s\n' \ "$family" "$config" "$address" >&2 exit 1 } done [[ $public_port =~ ^[0-9]{1,5}$ && $guest_port =~ ^[0-9]{1,5}$ ]] || { printf 'Invalid port in %s\n' "$config" >&2 exit 1 } public_port=$((10#$public_port)) guest_port=$((10#$guest_port)) ((public_port >= 1 && public_port <= 65535)) || exit 1 ((guest_port >= 1 && guest_port <= 65535)) || exit 1 [[ -z ${extra:-} ]] || { printf 'Too many fields in %s\n' "$config" >&2 exit 1 } entries+=("$family|$protocol|$public_ip|$public_port|$guest_ip|$guest_port") done <"$config" cleanup for tool in iptables ip6tables; do "$tool" -w -t nat -N "$nat_chain" "$tool" -w -t filter -N "$filter_chain" done for entry in "${entries[@]}"; do IFS='|' read -r family protocol public_ip public_port guest_ip guest_port \ <<<"$entry" tool=iptables destination="$guest_ip:$guest_port" if [[ $family == ipv6 ]]; then tool=ip6tables destination="[$guest_ip]:$guest_port" fi "$tool" -w -t nat -A "$nat_chain" -p "$protocol" -d "$public_ip" \ --dport "$public_port" -j DNAT --to-destination "$destination" "$tool" -w -t filter -A "$filter_chain" -p "$protocol" -d "$guest_ip" \ --dport "$guest_port" -j ACCEPT done for tool in iptables ip6tables; do "$tool" -w -t nat -I PREROUTING 1 -j "$nat_chain" "$tool" -w -t filter -I FORWARD 1 -j "$filter_chain" done HOOK chmod 0755 "$hook" cat >"$config" <<EOF # FAMILY PROTOCOL PUBLIC_IP PUBLIC_PORT GUEST_IP GUEST_PORT ipv4 tcp $PUBLIC_IPV4 80 $guest_ipv4 80 ipv4 tcp $PUBLIC_IPV4 2222 $guest_ipv4 22 ipv6 tcp $PUBLIC_IPV6 80 $guest_ipv6 80 ipv6 tcp $PUBLIC_IPV6 2222 $guest_ipv6 22 EOF virsh --connect "$connection" net-define "$xml" virsh --connect "$connection" net-autostart "$network" systemctl restart libvirtd.service if ! virsh --connect "$connection" net-list --name \ | grep -Fx "$network" >/dev/null; then virsh --connect "$connection" net-start "$network" fi "$hook" "$network" started begin - virsh --connect "$connection" net-dumpxml "$network"
Další port přidáš do /etc/libvirt/port-forwards.conf ve tvaru
RODINA PROTOKOL VEŘEJNÁ_IP VEŘEJNÝ_PORT IP_DOMÉNY PORT_DOMÉNY. Například
HTTPS dostupné na portu 8443 přidáš pro oba protokoly takto:
ipv4 tcp VPS_IPV4 8443 192.168.124.10 443 ipv6 tcp VPS_IPV6 8443 fd5f:6d2e:9c4a:124::10 443
Podporované protokoly jsou tcp a udp. Řádek s neplatnou syntaxí se
odmítne dříve, než se změní aktivní pravidla. Po změně souboru pravidla načti:
/etc/libvirt/hooks/network.d/50-port-forwards dualstack-nat started begin -
Port 2222 byl zvolen proto, aby SSH domény nekolidovalo s SSH samotné VPS. Pravidla firewallu VPS dále omez podle toho, kdo má mít ke službám přístup.
V tomto zapojení má VPS privátní IPv4 /32 a svou běžnou veřejnou IPv6
/64. Doména dostane veřejnou IPv4 /32 a adresu ze samostatné IPv6
/64, která je směrovaná přes adresu VPS z její první /64. Privátní IPv4
VPS funguje ve vnitřní síti vpsFree.cz a na internet odchází přes NAT na našich
routerech; veřejné adresy domény se v libvirt nepřekládají.
Pro IPv4 nejprve požádej podporu o přidělení privátní IPv4 /32 k VPS.
V části Síť → Routované adresy pak vyber veřejnou /32 v poli
Adresa, privátní adresu VPS v poli Přes a cílové rozhraní VPS. Veřejná
adresa se tím nenastaví na žádném rozhraní uvnitř VPS; vpsAdminOS ji k VPS
směruje přes privátní adresu.
Pro IPv6 použij další /64, kterou máš v části Síť → Routované adresy
k dispozici, a nastav její směrování přes adresu z běžné /64 VPS. Pokud už
máš směrovanou větší síť, například /48, vyber z ní pro doménu
samostatnou /64. První adresu této druhé /64 použijeme jako bránu na
bridge libvirt; například pro ROUTED_IPV6_PREFIX::/64 to bude
ROUTED_IPV6_PREFIX::1.
Tok IPv4 paketů používá malou tranzitní síť pouze mezi VPS a doménou:
internet → veřejná /32 → VPS → 192.168.123.1/30
↓
192.168.123.2/30 → veřejná /32 v doméně
V XML libvirt je veřejná IPv4 samostatná trasa přes tranzitní adresu domény.
Směrovaná IPv6 /64 je naproti tomu přímo na bridge:
<network> <name>public-routed</name> <forward mode='open'/> <bridge name='virbr-public'/> <ip address='192.168.123.1' prefix='30'/> <ip family='ipv6' address='ROUTED_IPV6_PREFIX::1' prefix='64'/> <route family='ipv4' address='PUBLIC_IPV4' prefix='32' gateway='192.168.123.2'/> </network>
Na VPS musí být povolené směrování obou protokolů:
sysctl -w net.ipv4.ip_forward=1 sysctl -w net.ipv6.conf.all.forwarding=1
Následující skript vytvoří tuto síť a uloží forwarding do
/etc/sysctl.d/90-libvirt-routing.conf. Ulož jej například jako
/root/configure-routed-network.sh a spusť jej s routovanou IPv4 a první
adresou routované IPv6 /64:
PUBLIC_IPV4=PUBLIC_IPV4 \ IPV6_GATEWAY=ROUTED_IPV6_PREFIX::1 \ /root/configure-routed-network.sh
Stejně jako u NAT se skript spouští při vytvoření sítě nebo změně adres.
Autostart sítě a nastavení v /etc/sysctl.d se postarají o restarty VPS.
#!/usr/bin/env bash set -euo pipefail : "${PUBLIC_IPV4:?set PUBLIC_IPV4 to the routed public IPv4 address}" : "${IPV6_GATEWAY:?set IPV6_GATEWAY to the first address of the routed IPv6 /64}" host_transit_ipv4=${HOST_TRANSIT_IPV4:-192.168.123.1} guest_transit_ipv4=${GUEST_TRANSIT_IPV4:-192.168.123.2} connection=qemu:///system network=public-routed forwarding_config=/etc/sysctl.d/90-libvirt-routing.conf xml=$(mktemp) trap 'rm -f "$xml"' EXIT export LC_ALL=C network_uuid=$(virsh --connect "$connection" net-uuid "$network" 2>/dev/null || :) uuid_element= if [[ -n $network_uuid ]]; then uuid_element=" <uuid>$network_uuid</uuid>" fi cat >"$xml" <<EOF <network> <name>$network</name> $uuid_element <forward mode='open'/> <bridge name='virbr-public' stp='on' delay='0'/> <ip address='$host_transit_ipv4' prefix='30'/> <ip family='ipv6' address='$IPV6_GATEWAY' prefix='64'/> <route family='ipv4' address='$PUBLIC_IPV4' prefix='32' gateway='$guest_transit_ipv4'/> </network> EOF if virsh --connect "$connection" net-list --name \ | grep -Fx "$network" >/dev/null; then printf '%s is active. Shut down its attached domains, run ' "$network" >&2 printf 'virsh net-destroy %s, then rerun this script.\n' "$network" >&2 exit 1 fi install -d -m 0755 "$(dirname "$forwarding_config")" cat >"$forwarding_config" <<'EOF' net.ipv4.ip_forward = 1 net.ipv6.conf.all.forwarding = 1 EOF sysctl --quiet --load "$forwarding_config" virsh --connect "$connection" net-define "$xml" virsh --connect "$connection" net-autostart "$network" virsh --connect "$connection" net-start "$network" virsh --connect "$connection" net-dumpxml "$network"
Pokud je síť již aktivní, skript ji nezmění. Vypni připojené domény, zastav
síť příkazem virsh net-destroy public-routed, skript spusť znovu a domény
opět zapni. Skript zachová UUID existující sítě.
Doménu připoj k public-routed. V Debianu v doméně pak například ulož
následující konfiguraci do /etc/network/interfaces.d/public-routed;
ens3, PUBLIC_IPV4, GUEST_IPV6 a IPV6_GATEWAY nahraď
skutečnými hodnotami:
auto ens3
iface ens3 inet static
address 192.168.123.2/30
up ip address add PUBLIC_IPV4/32 dev ens3
up ip route replace default via 192.168.123.1 src PUBLIC_IPV4
down ip address del PUBLIC_IPV4/32 dev ens3
iface ens3 inet6 static
address GUEST_IPV6/64
gateway IPV6_GATEWAY
forward mode='open' nechává filtrování forwardovaného provozu na firewallu
VPS. Povol jen provoz, který má být doméně dostupný, a nezapomeň na pravidla
pro IPv4 i IPv6. Veřejná IPv4 je nakonfigurovaná pouze v doméně. IPv6 brána a
adresy domén jsou z druhé, směrované /64; na vnějším rozhraní VPS zůstává
jen adresa z její běžné /64. Ani jeden protokol v tomto zapojení nepoužívá
NAT.
Instalační ISO je bezpečnější nejprve zkopírovat na lokální dataset. U staršího
NFSv3 exportu bez funkčního síťového zamykání může QEMU při čekání na zámek
uvíznout nebo skončit chybou jako Failed to lock byte 100 či
Failed to get … lock.
Pouze pro jednoho klienta a pouze pro instalační ISO připojené pro čtení lze
použít nolock:
#!/usr/bin/env bash set -euo pipefail : "${NFS_SERVER:?set NFS_SERVER to the NAS server address}" : "${NFS_EXPORT:?set NFS_EXPORT to the exported dataset path}" mountpoint=${ISO_MOUNTPOINT:-/mnt/installer-iso} install -d -m 0755 "$mountpoint" mount -t nfs -o ro,vers=3,nolock \ "${NFS_SERVER}:${NFS_EXPORT}" "$mountpoint" findmnt --noheadings --output FSTYPE,OPTIONS --target "$mountpoint"
nolock vypíná síťové zamykání NLM a ostatní klienti pak nevidí lokální
zámky. Nikdy jej nepoužívej pro zapisovatelný nebo sdílený disk virtuálního
stroje. U NFSv4 je zamykání součástí protokolu a tato volba není řešením. Viz
nfs(5).