Étiquette : openvswitch

TEK Arena >

Installation de VyOS sur KVM/Qemu

VyOS est un système d’exploitation de routeur et firewall opensource. Vous trouverez leur site ici. On le retrouve dans les clouds AWS, Azure… et bien évidemment sur KVM. Très pratique, avec OpenVSwitch, il fait partie des petits blocs d’infra qui vous font économiser beaucoup d’heures de recherche et d’argent… Il s’interconnecte avec par exemple une « Azure VNet Gateway », Wireguard, GRE, BGP over IKEv2/IPsec. L’OS consomme moins de 512M de RAM et prend quelques centaines de Mo de place en stockage.

Installation de l’OS

On va chercher la bonne image sur la bonne page du site de l’éditeur. Je suis sympa, je vous mets l’url. Dans mon infra de test KVM/libvirt, j’ai un switch virtuel (OpenVSwitch) sur une IP WAN (ext-br-network) et un réseau isolé (internal). On va créer une nouvelle VM dans libvirt :

virt-install -n vyos_r1 \
   --ram 4096 \
   --vcpus 2 \
   --cdrom /var/lib/libvirt/images/vyos-1.3-rolling-202012041912-amd64.iso \
   --os-type linux \
   --os-variant debian10 \
   --network network=ext-br-network,model=virtio \ # c'est un réseau sur mon OpenVSwitch sur le WAN 
   --network network=internal,model=virtio \ # c'est un réseau isolé
   --graphics vnc,port=5999 \
   --hvm \
   --virt-type kvm \
   --disk path=/var/lib/libvirt/images/vyos_r1.qcow2,bus=virtio,size=8 \
   --noautoconsole

On entre dans l’OS, on change les locales et on installe l’OS (ou pas, dans ce cas il est chargé en ram et il faut créer une image avec les scripts de configuration)

sudo loadkeys fr
sudo dpkg-reconfigure keyboard-configuration
install image

Configuration simple avec deux cartes réseau

configure
set interfaces ethernet eth0 address dhcp
set interfaces ethernet eth1 address '192.168.200.254/24'
set service ssh port '22' # à partir de là le routeur est accessible en ssh, donc la conf peut-être terminée via SSH, Ansible
set nat source rule 100 outbound-interface 'eth0'
set nat source rule 100 source address '192.168.200.0/24'
set nat source rule 100 translation address masquerade
commit
save
exit

show interface
ping 8.8.8.8 interface 192.168.1.80 (en fonction)
ping 8.8.8.8 interface 192.168.200.254

Configuration du DHCP si besoin

set service dhcp-server disabled 'false' # version <~ 1.1.8
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 default-router '192.168.200.254'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 dns-server '192.168.200.254'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 domain-name 'internal.domain.tld'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 lease '86400'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 start '192.168.200.10' stop '192.168.200.253' # version <~ 1.1.8
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 range 0 start '192.168.200.9'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 range 0 stop '192.168.200.253'

On configure le DNS forwarding

set service dns forwarding cache-size '0'
#set service dns forwarding listen-on 'eth1' # version <~ 1.1.8
set service dns forwarding listen-address '192.168.200.254'
set service dns forwarding allow-from '192.168.200.0/24'
set service dns forwarding name-server '8.8.8.8'
set service dns forwarding name-server '8.8.4.4'

Voilà, à ce niveau le routeur est fonctionnel !

Mise en place des firewalls

set firewall name OUTSIDE-IN default-action 'drop'
set firewall name OUTSIDE-IN rule 10 action 'accept'
set firewall name OUTSIDE-IN rule 10 state established 'enable'
set firewall name OUTSIDE-IN rule 10 state related 'enable'

set firewall name OUTSIDE-LOCAL default-action 'drop'
set firewall name OUTSIDE-LOCAL rule 10 action 'accept'
set firewall name OUTSIDE-LOCAL rule 10 state established 'enable'
set firewall name OUTSIDE-LOCAL rule 10 state related 'enable'
set firewall name OUTSIDE-LOCAL rule 20 action 'accept'
set firewall name OUTSIDE-LOCAL rule 20 icmp type-name 'echo-request'
set firewall name OUTSIDE-LOCAL rule 20 protocol 'icmp'
set firewall name OUTSIDE-LOCAL rule 20 state new 'enable'
set firewall name OUTSIDE-LOCAL rule 30 action 'drop'
set firewall name OUTSIDE-LOCAL rule 30 destination port '22'
set firewall name OUTSIDE-LOCAL rule 30 protocol 'tcp'
set firewall name OUTSIDE-LOCAL rule 30 recent count '4'
set firewall name OUTSIDE-LOCAL rule 30 recent time '60'
set firewall name OUTSIDE-LOCAL rule 30 state new 'enable'
set firewall name OUTSIDE-LOCAL rule 31 action 'accept'
set firewall name OUTSIDE-LOCAL rule 31 destination port '22'
set firewall name OUTSIDE-LOCAL rule 31 protocol 'tcp'
set firewall name OUTSIDE-LOCAL rule 31 state new 'enable'

et on applique la configuration à la carte réseau :

set interfaces ethernet eth0 firewall in name 'OUTSIDE-IN'
set interfaces ethernet eth0 firewall local name 'OUTSIDE-LOCAL'

Automatisation

On peut exécuter un script pour automatiser la configuration, il doit être appelé par le groupe vyattacfg avec la commande switch group sg, ce qui donne :

sg vyattacfg -c ./conf-network.sh

#!/bin/vbash
source /opt/vyatta/etc/functions/script-template
configure
set interfaces ethernet eth0 address dhcp
set interfaces ethernet eth1 address '192.168.200.254/24'
set service ssh port '22'
set nat source rule 100 outbound-interface 'eth0'
set nat source rule 100 source address '192.168.200.0/24'
set nat source rule 100 translation address masquerade
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 default-router '192.168.200.254'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 dns-server '192.168.200.254'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 domain-name 'internal.domain.tld'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 lease '86400'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 range 0 start '192.168.200.9'
set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 range 0 stop '192.168.200.253'
set service dns forwarding cache-size '0'
set service dns forwarding listen-address '192.168.200.254'
set service dns forwarding allow-from '192.168.200.0/24'
set service dns forwarding name-server '8.8.8.8'
commit
save
exit

Autre solution « maison », un vieux script des familles qui permet de tout faire depuis l’hyperviseur :

#!/bin/bash
name="vyos_r1"
user="vyos"
password="vyos"
dns_ip="192.168.1.34"

function main() {
  launch
  sleep 40
  install
  sleep 10
  config
}

function launch() {
  sudo virt-install -n $name \
   --ram 4096 \
   --vcpus 2 \
   --cdrom /var/lib/libvirt/images/vyos-1.3-rolling-202012041912-amd64.iso \
   --os-type linux \
   --os-variant debian10 \
   --network network=ext-br-network,model=virtio \
   --network network=internal,model=virtio \
   --graphics vnc,port=5999 \
   --hvm \
   --virt-type kvm \
   --disk path=/var/lib/libvirt/images/$name.qcow2,bus=virtio,size=8 \
   --noautoconsole
}

function install() {
  sudo expect <<EOF
  set timeout 600
  spawn virsh console $name
  expect "Escape character is ^] (Ctrl + ])" {send "\n"}
  expect "vyos login:" {send "$user\r"}
  expect "Password:" {send "$password\r"}
  expect "vyos:~" {send "install image\r"}   expect "Would you like to continue? (Yes/No): " {send "Yes\n"}
  expect "Partition (Auto/Parted/Skip):" {send "Auto\n"}   expect "Install the image on?:" {send "\n"}
  expect "Continue? (Yes/No):" {send "Yes\n"}   expect "How big of a root partition should I create? (2000MB - 8589MB):" {send "8589MB\n"}
  expect "What would you like to name this image?:" {send "\n"}   expect "Which one should I copy to vda?:" {send "\n"}
  expect "Enter password for user 'vyos':" {send "$password\n"}
  expect "Retype password for user 'vyos':" {send "$password\n"}
  expect "Which drive should GRUB modify the boot partition on?:" {send "\n"}   expect "vyos:~" {send "sudo reboot\r"}
  expect "Are you sure you want to reboot this system?" {send "Yes\r"}
EOF
}

function config() {
  sudo virsh start $name
  sleep 50
  sudo expect <<EOF
  set timeout 600
  spawn virsh console $name
  expect "Escape character is ^] (Ctrl + ])" {send "\n"}
  expect "vyos login:" {send "$user\r"}
  expect "Password:" {send "$password\r"}
  send "sudo loadkeys fr\r"
  send "configure\r"
  send "set interfaces ethernet eth0 address dhcp\r"
  send "set interfaces ethernet eth1 address '192.168.200.254/24'\r"
  send "set service ssh port '22'\r"
  send "set nat source rule 100 outbound-interface 'eth0'\r"
  send "set nat source rule 100 source address '192.168.200.0/24'\r"
  send "set nat source rule 100 translation address masquerade\r"
  send "set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 default-router '192.168.200.254'\r"
  send "set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 dns-server '192.168.200.254'\r"
  send "set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 domain-name 'internal.domain.tld'\r"
  send "set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 lease '86400'\r"
  send "set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 range 0 start '192.168.200.9'\r"
  send "set service dhcp-server shared-network-name LAN subnet 192.168.200.0/24 range 0 stop '192.168.200.253'\r"
  send "set service dns forwarding cache-size '0'\r"
  send "set service dns forwarding listen-address '192.168.200.254'\r"
  send "set service dns forwarding allow-from '192.168.200.0/24'\r"
  send "set service dns forwarding name-server '$dns_ip'\r"
  send "commit\r"
  send "save\r"
  send "exit\r"
EOF
}
main
exit 0

Et voilà, un routeur fonctionnel avec un seul script… Pensez à modifier ce script pour durcir la sécurité d’accès au routeur en changeant le second mot de passe et en modifiant la config ssh pour accepter les clés. Je mets le lien vers les blueprints de VyOS (notamment dans des environnements Cloud) :

https://docs.vyos.io/en/latest/configexamples/index.html

@+

Un seul et même réseau quelque soit le datacenter

Derrière ce titre un poil « putaclic » se cache des heures de recherches et des litres de café. Et oui, le réseau et moi ça fait deux… Vous vous souvenez sans doute de mon dernier article portant sur openvswitch, il n’était pas écrit pour rien… généralement quand je commence à fouiller une techno c’est que j’ai une idée derrière la tête.

Jusqu’à présent pour créer un réseau privé, j’utilisais tinc. Je ne vais pas m’appesantir sur ce sujet, mais c’est tout de même assez puissant. Je ne cache pas que dans une logique d’automatisation et d’industrialisation, ce n’est pas franchement sexy. Il faut installer le binaire, le configurer (en mode switch c’est mieux), créer les interfaces tun/tap, les clés, les partager…  Il existe des rôles Ansible pour faire le job, vous les trouverez facilement sur Github.

On va s’approcher un peu plus d’un fonctionnement type OpenStack et utiliser les possibilités offertes par OpenVSwitch.

Nous allons créer un bridge interne GRE avec tunnel IPsec :

Pré-requis

Pour ce faire il faut au minimum deux hyperviseurs KVM bare metal, ça fonctionne aussi avec Proxmox et MacOS VMWare (ahahah), OpenStack puisque c’est intégré dans la solution, mais aussi sans rien… Il faut deux IPs joignables qui pointent vers les serveurs. Avoir installé openvswitch, sauf si ça ne vous intéresse pas mais dans ce cas, je me demande pourquoi vous lisez ces lignes.

Configuration

On ne va pas faire une usine à gaz, juste créer des certificats auto-signés avec la cli d’openvswitch sur le premier hôte : 

mkdir /etc/keys
cd !$
ovs-pki req -u `hostname`
ovs-pki self-sign `hostname`

La même procédure sur le deuxième hôte :

mkdir /etc/keys
cd !$
ovs-pki req -u `hostname`
ovs-pki self-sign `hostname`

Il faut copier la clé d’un serveur sur l’autre : 

scp `hostname`-cert.pem user@hote2:/etc/keys/`hostname`-cert.pem
scp `hostname`-cert.pem user@hote1:/etc/keys/`hostname`-cert.pem

Ensuite on va dire à openvswitch où se trouvent ces fichiers :

ovs-vsctl set Open_vSwitch . \
other_config:certificate=/etc/keys/<hostname1>-cert.pem \
other_config:private_key=/etc/keys/<hostname1>-privkey.pem

A faire sur chacun des serveurs bien sûr et changeant le nom hostname du .pem. C’est terminé pour les certificats et clés. Maintenant la mise en place des bridges ipsec GRE avec openvswitch. Sur l’hôte 1:

ip addr add 192.0.0.1/24 dev br-ipsec
ip link set br-ipsec up
ovs-vsctl add-port br-ipsec tun -- set interface tun type=gre \
options:remote_ip=<ip_de_hote2> \
options:remote_cert=/etc/keys/<hostname2>-cert.pem
ovs-vsctl show

Sur l’hôte 2 :

ip addr add 192.0.0.2/24 dev br-ipsec
ip link set br-ipsec up
ovs-vsctl add-port br-ipsec tun -- set interface tun type=gre \
options:remote_ip=<ip_de_hote1> \
options:remote_cert=/etc/keys/<hostname1>-cert.pem

Et maintenant sous vos yeux ébahis depuis l’hôte 2 :

ping 192.0.0.1
PING 192.0.0.1 (192.0.0.1) 56(84) bytes of data.
64 bytes from 192.0.0.1: icmp_seq=1 ttl=64 time=88.9 ms
64 bytes from 192.0.0.1: icmp_seq=2 ttl=64 time=87.6 ms
64 bytes from 192.0.0.1: icmp_seq=3 ttl=64 time=87.8 ms
^C
--- 192.0.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2002ms
rtt min/avg/max/mdev = 87.587/88.097/88.926/0.591 ms

Généralisation

Vu que les serveurs sont des hyperviseurs KVM, on va tenter de créer des machines virtuelles qui profiteront du tunnel IPsec.
Comme nous l’avons vu dans le dernier article sur openvswitch, il faut intégrer le xml du bridge pour qu’il soit interprété par libvirt :

# cat /tmp/net-ipsec.xml
<network>
  <name>net-ipsec</name>
  <forward mode='bridge'/>
  <bridge name='br-ipsec'/>
  <virtualport type='openvswitch'/>
</network>
virsh net-define /tmp/net-ipsec.xml
virsh net-start net-ipsec
virsh net-autostart net-ipsec

Faites démarrer une vm sur chaque hyperviseur avec comme réseau net-ipsec, donnez-lui une adresse dans ce réseau et pingez. 

Ping de deux vms entre Paris et Beauharnois (datacenter OVH)… latence cata…

Automatisation

Pour travailler dans un environnement automatisé, avec Terraform et Libvirt KVM, il faudra utiliser un alias pour le provider (j’ai mis Toronto en hommage à Glenn Gould, le joueur de foot) :

# cat multi_hyp.tf
provider "libvirt" {
          uri   = "qemu:///system"
}

provider "libvirt" {
          alias = "toronto"
          uri   = "qemu+ssh://<kvm_user>@<ip_address>/system"
}

resource "libvirt_volume" "local-fedora-qcow2" {
          name = "fedora-qcow2"
          pool = "default"
          source = "https://download.fedoraproject.org/pub/fedora/linux/releases/32/Cloud/x86_64/images/Fedora-Cloud-Base-32-1.6.x86_64.qcow2"
          format = "qcow2"
}

resource "libvirt_volume" "remote-fedora-qcow2" {
          provider = libvirt.toronto
          name = "fedora-qcow2"
          pool = "default"
          source = "https://download.fedoraproject.org/pub/fedora/linux/releases/32/Cloud/x86_64/images/Fedora-Cloud-Base-32-1.6.x86_64.qcow2"
          format = "qcow2"
}

resource "libvirt_cloudinit_disk" "commoninit-local" {
          name = "local-commoninit.iso"
          pool = "default"
          user_data = data.template_file.user_data.rendered
          network_config = data.template_file.network_config_local.rendered
}

resource "libvirt_cloudinit_disk" "commoninit-remote" {
          provider = libvirt.toronto
          name = "remote-commoninit.iso"
          pool = "default"
          user_data = data.template_file.user_data.rendered
          network_config = data.template_file.network_config_remote.rendered
}

data "template_file" "user_data" {
          template = file("${path.module}/cloud_init.cfg")
          vars = {
             hostname       = "fedora"
             fqdn           = "lan"
             user           = "matt"
             ssh_public_key = "ssh-rsa AAAA..."
          }
}


data "template_file" "network_config_local" {
          template = file("${path.module}/network_config_static.cfg")
          vars = {
             prefixIP = "192.0.0"
             octetIP = "6"
          }
}

data "template_file" "network_config_remote" {
          template = file("${path.module}/network_config_static.cfg")
          vars = {
             prefixIP = "192.0.0"
             octetIP = "7"
          }
}

resource "libvirt_domain" "local-domain" {
          name   = "local"
          memory = "2048"
          vcpu   = 2

          disk {
            volume_id = libvirt_volume.local-fedora-qcow2.id
          }

          cloudinit = libvirt_cloudinit_disk.commoninit-local.id

          network_interface {
             network_name   = "net-ipsec"
          }
}

resource "libvirt_domain" "remotehost-domain" {
          provider = libvirt.toronto
          name     = "toronto"
          memory   = "2048"
          vcpu     = 2

          disk {
            volume_id = libvirt_volume.remote-fedora-qcow2.id
          }

          cloudinit = libvirt_cloudinit_disk.commoninit-remote.id

          network_interface {
            network_name = "net-ipsec"
          }
}

terraform {
          required_version = ">= 0.12"
}

Avec les fichiers config de cloudinit qui vont bien :

# cat cloud_init.cfg
#cloud-config
# vim: syntax=yaml
hostname: ${hostname}
fqdn: ${fqdn}
ssh_pwauth: true
manage_etc_hosts: true
chpasswd:
  list: |
     root: StrongPassword
  expire: False
users:
  - name: ${user}
    passwd: $6$rounds=4096$sWnV/iBTck9OVnMl$PcfdG7/Yjq91I7KdGJyfap23K3wvt3OFtnGhC2vXpbeK7zYC1EO4qU3toVinoYHxPPhkA3HmyKURONXv1Ezrq.
    lock-passwd: false
    ssh_authorized_keys:
      - ${ssh_public_key}
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    shell: /bin/bash
    groups: wheel
growpart:
  mode: auto
  devices: ['/']
output:
    init:
        output: "> /var/log/cloud-init.out"
        error: "> /var/log/cloud-init.err"
    config: "tee -a /var/log/cloud-config.log"
    final:
        - ">> /var/log/cloud-final.out"
        - "/var/log/cloud-final.err"
final_message: "The system is up, after $UPTIME seconds"

et pour le réseau statique :

# cat network_config_static.cfg
version: 2
ethernets:
  eth0:
     dhcp4: no
     dhcp6: no
     addresses: [ ${prefixIP}.${octetIP}/24 ]

Et les vms se pingent, on peut également s’y connecter en SSH depuis leurs adresses privées sur 192.0.0.0/24 :

Optimisation

Il peut être franchement intéressant d’optimiser en configurant la valeur du MTU. Par exemple sur ma connexion entre mes deux hyperviseurs kvm (Paris et Beauharnois) en passant d’un MTU 1500 à 1554 j’obtiens : 

[root@hyp ~]# iperf -c 192.0.0.2
------------------------------------------------------------
Client connecting to 192.0.0.2, TCP port 5001
TCP window size:  325 KByte (default)
------------------------------------------------------------
[  3] local 192.0.0.1 port 44128 connected with 192.0.0.2 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.2 sec  10.1 MBytes  8.35 Mbits/sec
[root@hyp ~]# ifconfig eno3 mtu 1554
[root@hyp ~]# iperf -c 192.0.0.2
------------------------------------------------------------
Client connecting to 192.0.0.2, TCP port 5001
TCP window size:  325 KByte (default)
------------------------------------------------------------
[  3] local 192.0.0.1 port 44130 connected with 192.0.0.2 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.1 sec   264 MBytes   220 Mbits/sec

Voilà pour cet article, reste maintenant à isoler à l’intérieur avec des VLANS, mais aussi VXLAN… dans un futur… proche… si j’ai le temps…