Présentation de DNSCrypt-Proxy

DNSCrypt-Proxy est proxy DNS, qui prend en charge les communications cryptées des DNS, comme les protocoles DNSCrypt v2, DNS-over-HTTPS et Anonymized DNSCrypt. En plus, on peut alimenter DNSCrypt-Proxy afin de s’en servir comme d’un bloqueur de publicité.

Ainsi, vous évitez les requêtes vers votre FAI et vous êtes préservé des publicités.

Mise en oeuvre

Pour mes besoins, je vais utiliser les outils suivants :

  • Linux kvm pour le système de virtualisation sur Ubuntu
  • Alpine linux pour sa compacité et sa faible empreinte mémoire pour la VM
  • Packer pour préparer une image cloud d’Alpine Linux, qui n’en propose pas
  • Terraform pour déployer la VM
  • Ansible pour installer et configurer DNSCrypt-proxy

Linux KVM

Kvm est une offre de virtualisation intégrée de base dans le noyaux Linux (Kernel Virtual Machine) qui existe depuis plus de 10 ans. On le retrouve dans de nombreuses offres cloud et on l’ulitise sans le savoir.

Packer

Comme je l’ai présenté dans un article précédent sur l’Infra as code, je vais juste parlé ici d’un élément interessant dans son fonctionnement. Packer permets de créer des images cloud d’un OS qui sera déployé par d’autres outils. Certains OS comme Debian, Centos, … Proposent des outils de configuration (debootstrap,kickstart,…). D’autres comme Alpine, OpenBSD, ne proposent pas ce genre de fichier de configuration et nécessitent la saisie de réponses manuelles. Packer gère ça à la perfection !

{
  "variables":
    {
      "cpu": "1",
      "ram": "1024",
      "name": "alpine",
      "disk_size": "1000",
      "iso_checksum": "fe694a34c0e2d30b9e5dea7e2c1a3892c1f14cb474b69cc5c557a52970071da5",
      "iso_checksum_type": "sha256",
      "iso_urls": "http://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/x86_64/alpine-virt-3.12.0-x86_64.iso",
      "version": "3.12.0",
      "headless": "true",
      "ssh_username": "root",
      "ssh_password": "toor"
    },
  "builders": [
    {
      "name": "{{user `name`}}-{{user `version`}}",
      "type": "qemu",
      "format": "qcow2",
      "accelerator": "kvm",
      "qemu_binary": "/usr/bin/qemu-system-x86_64",
      "net_device": "virtio-net",
      "disk_interface": "virtio",
      "disk_cache": "none",
      "qemuargs": [[ "-m", "{{user `ram`}}M" ],[ "-smp", "{{user `cpu`}}" ]],
      "ssh_wait_timeout": "30m",
      "http_directory": ".",
      "http_port_min": 10082,
      "http_port_max": 10089,
      "ssh_host_port_min": 2222,
      "ssh_host_port_max": 2229,
      "ssh_username": "{{user `ssh_username`}}",
      "ssh_password": "{{user `ssh_password`}}",
      "iso_urls": "{{user `iso_urls`}}",
      "iso_checksum": "{{user `iso_checksum`}}",
      "boot_wait": "15s",
      "boot_command": [
        "root<enter>",
        "ifconfig eth0 up \u0026\u0026 udhcpc -i eth0<enter><wait5>",
        "wget -qO answers http://{{.HTTPIP}}:{{.HTTPPort}}/answers<enter><wait>",
        "setup-alpine -f answers<enter><wait5>",
        "toor<enter>",
        "toor<enter>",
        "<wait10>",
        "reboot<enter>"
      ],
      "disk_size": "{{user `disk_size`}}",
      "disk_discard": "unmap",
      "disk_compression": true,
      "headless": "{{user `headless`}}",
      "shutdown_command": "poweroff",
      "output_directory": "{{user `name`}}-{{user `version`}}"
    }
  ],
  "provisioners": [
    {
      "inline": [
        "echo http://dl-cdn.alpinelinux.org/alpine/edge/community >> /etc/apk/repositories",
        "echo http://dl-cdn.alpinelinux.org/alpine/edge/main >> /etc/apk/repositories",
        "apk update",
        "apk add dbus avahi",
        "apk add cloud-init cloud-utils",
        "apk add qemu-guest-agent python3",
        "echo GA_PATH=\"/dev/vport1p1\" >> /etc/conf.d/qemu-guest-agent",
        "rc-update add cloud-init default",
        "rc-update add qemu-guest-agent default",
        "/sbin/setup-cloud-init"
      ],
      "type": "shell"
    }
  ]
}

Dans la section boot_command j’indique à Alpine Linux qui va pouvoir télécharger son fichier de réponses (les commandes qui doivent normalement être saisie manuellement) sur un serveur http automatiquement géré par Packer.

KEYMAPOPTS="us us"
HOSTNAMEOPTS="-n alpine"
INTERFACESOPTS="auto lo
iface lo inet loopback

auto eth0
iface eth0 inet dhcp
"
DNSOPTS=""
TIMEZONEOPTS="-z Europe/Paris"
PROXYOPTS="none"
APKREPOSOPTS="http://dl-cdn.alpinelinux.org/alpine/latest-stable/main/"
SSHDOPTS="-c openssh"
NTPOPTS="-c chrony"
DISKOPTS="-s 0 -m sys /dev/vda"

Une fois les différentes instructions exécutées, dans la section provisioners j’ajoute les repositories edge et installe un certain nombre de composants :

  • cloud-init nécessaire pour le déploiement et la customisation de la VM
  • qemu-guest-agent qui est l’agent du système de virtualisation KVM Linux
  • Python pour le fonctionnement de Ansible
  • Puis je demande que certains de ces utilitaires démarrent automatiquement comme daemon
  • Pour finir il faut lancer la commande de setup pour cloud-init : merci au mainteneur de cloud-init Alpine Linux pour son aide

Puis l’image est générée (dans mon cas au format qcow2).

Terraform

J’utilise Terraform version 0.13.4 et le provider libvirt 0.6.3 (je participe d’ailleurs au projet qui dans cette version intègre un de mes pull-requests par rapport à l’agent Qemu), pour installer ce Terraform Provider il faut télécharger le binaire et l’installer dans votre répertoire .terraform.d/plugins.

Quant à l’utilisation de Terraform regardez mon précédent tuto pour devenir un devops.

Voici le contenu du 1er fichier Terraform : main.tf

# provider

terraform {
 required_version = ">= 0.13"
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.6.3"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

# load data
data "template_file" "user_data" {
  template = file(var.user_data_source)
  vars = {
        hostname = "srv-${random_id.randomId.hex}"
  }
}

data "template_file" "network_config" {
  template = file(var.network_config_source)
}

# create pool
resource "libvirt_pool" "alpine" {
  name = var.pool_name
  type = "dir"
  path = var.pool_dir
}

resource "libvirt_volume" "os_image" {
  source = var.image_source   
  name = "img-${random_id.randomId.hex}.qcow2"
  pool = libvirt_pool.alpine.name
}

# create image
resource "libvirt_volume" "image-qcow2"{
  name = "disk-${random_id.randomId.hex}"
  base_volume_id = libvirt_volume.os_image.id
  pool = libvirt_pool.alpine.name
  format = "qcow2"
  size   = 1 * 1024 * 1024 * 1024
}

# cloudinit
resource "libvirt_cloudinit_disk" "commoninit" {
  name = "commoninit-${random_id.randomId.hex}.iso"
  pool = libvirt_pool.alpine.name
  user_data = data.template_file.user_data.rendered
  network_config = data.template_file.network_config.rendered
}

resource "random_id" "randomId" {
    keepers = {
  store = libvirt_pool.alpine.id
    }
    byte_length = 4
}

# Define KVM domain to create
resource "libvirt_domain" "virt-domain" {
  name = "srv-${random_id.randomId.hex}"
  memory = var.domain_memory
  vcpu   = var.domain_cpu

  cloudinit = libvirt_cloudinit_disk.commoninit.id
  qemu_agent = true
  network_interface {
     bridge = "br0"
     wait_for_lease = true
  }

  provisioner "local-exec" {
    command = <<EOT
        echo "[dnscrypt]\n${self.network_interface.0.addresses.0} ansible_user=devops" > ./hosts.ini 
        sleep 30
             ansible-playbook site.yml -i hosts.ini 
    EOT
  }

  console {
    type = "pty"
    target_type = "serial"
    target_port = "0"
  }

    disk {
    volume_id = libvirt_volume.image-qcow2.id
  }

  graphics {
    type = "spice"
    listen_type = "address"
    autoport = true
  }
}
  1. comme je me moque du nom de la VM (pet vs cattle), le serveur aura un nom du style srv-(randomId)
  2. dans la section provisioner je récupère l’adresse ip de la VM et je crée un fichier hosts.ini pour Ansible, puis je lance l’exécution du playbook d’installation de DNSCrypt-Proxy
  3. La VM aura donc les caractéristiques suivantes :
    • RAM 512Mo
    • 1 vCPU
    • 1Go de disque

Voici le contenu du 2nd fichier Terraform : variables.tf

variable "pool_name"{
  description = "name of to create pool"
  type = string
  default = "alpine-pool-1"
}

variable "pool_dir"{
  description = "dir of the new pool"
  type = string
  default = "/tmp/terraform_libvirt_provider_images_1/"
}

variable "domain_name"{
  description = "name of the image including the format"
  type = string
  default = "alpine-server-1"
}

variable "image_name"{
  description = "name of the image including the format"
  type = string
  default = "alpine-3.12.0-amd64-libvirt.qcow2"
}

variable "common_name"{
  description = "name of cloud init disk"
  type = string
  default = "alpine-s1"
}

variable "image_source"{
  description = "path to image source"
  type = string
  default = "./alpine-3.12.0-amd64-libvirt.qcow2"
}

variable "user_data_source"{
  description = "path to use data"
  type = string
  default = "./user_data.cfg"
}

variable "network_config_source"{
  description = ""
  default = "./network_config.cfg"
}

variable "domain_memory"{
  description = "name of the volume"
  type = string
  default = 512 
}

variable "domain_cpu"{
  description = "name of the volume"
  type = string
  default = 1
}

variable "network_name"{
  description = "name of virtual network"
  type = string
  default = "default"
}

Et le dernier fichier, celui pour afficher l’ip de la VM en fin de process :

output "ip" {
  value = libvirt_domain.virt-domain.network_interface[0].addresses[0]
}

Pour finir mon Terraform a besoin de 2 autres fichiers pour le réseau et les informations de configuration cloud-init :

#cloud-config
preserve_hostname: False
manage_etc_hosts: True
hostname: ${hostname}
fqdn: "${hostname}.local"

users:
  - name: devops
    lock-passwd: false
    ssh_pwauth: True
    chpasswd: { expire: False }
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    home: /home/devops
    shell: /bin/ash
    ssh_authorized_keys:
      - ssh-rsa AAAAB3N....kshdiuoJ8

disable_root: False

# Growpart resizes partitions to fill the available disk space
growpart:
  mode: auto
  devices: ['/']

Ansible

Pour installer les compléments systèmes et DNSCrypt-Proxy, j’ai créé un rôle Ansible simple, qui réalise les actions suivantes :

  • Upgrade de la distro Alpine Linux
  • Installation de DNSCrypt-Proxy
  • Copie de mon fichier de config
  • Démarrage automatique du daemon DNSCrypt-Proxy
  • Installation du script de mise à jour des listes noirs (Adblock)
  • Ajout de ce script dans crontab
  • Exécution du script pour télécharger la list
  • Redémarrage du daemon DNSCrypt-Proxy
---
# tasks file for dnscrypt

- name: Update cache  
  shell:
    cmd: apk update

- name: Upgrade Alpine
  shell:
    cmd: apk upgrade

- name: Install dnscrypt
  package:
    name: dnscrypt-proxy
    state: latest
    update_cache: yes

- name: Copy dnscrypt config file
  copy:
    src: files/dnscrypt-proxy.toml
    dest: /etc/dnscrypt-proxy/
    owner: root
    group: root
    mode: 0644

- name: Enable dnscrypt-proxy service on startup
  service:
    name: dnscrypt-proxy
    enabled: yes

- name: Copy ad-block script 
  copy: 
    src:  files/update-adblocker.sh
    dest: /usr/bin/update-adblocker.sh
    owner: root
    group: root
    mode: u+x,g+x,o+x

- name: Creates a cron file under /etc/cron.d
  cron:
    name: ad-block
    special_time: daily
    user: root
    job: /usr/bin/update-adblocker.sh

- name: Run script to init adblock
  command: /usr/bin/update-adblocker.sh
  notify: 
    - Restart dnscrypt

Dans le fichier de configuration de DNSCrypt-proxy, j’ai volontairement ouvert l’accès à l’ensemble du réseaux, afin que toutes les machines de mon réseau local puissent s’y connecter.

Et voilà, en 3 min vous avez une VM opétationnelle ! L’ensemble des fichiers de configuraiton sont dans le dépot Github du projet.