created: lunedì, gen 1, 0001
---
title: "Sperimentare con Varlink: Creare un Servizio IPC Hello World"
date: 2025-12-03
description: >
  Un tutorial pratico che esplora Varlink come meccanismo moderno di IPC. Costruiamo un semplice servizio hello world in Rust e discutiamo perché Varlink potrebbe diventare la spina dorsale della comunicazione interna tra macchine in DownToZero.
---

Se hai mai provato a far comunicare diversi processi su un sistema Linux in modo efficiente, sai che il panorama è... diciamo, frammentato. Abbiamo D-Bus, socket Unix con protocolli personalizzati, REST API su localhost, gRPC e innumerevoli altri approcci. Ognuno con la sua complessità, requisiti di strumenti e curva di apprendimento.

Di recente, mi sono imbattuto in [Varlink](https://varlink.org/) (e nel suo fratello più moderno incentrato su Rust, [Zlink](https://github.com/z-galaxy/zlink)), e ha immediatamente fatto clic con ciò che stiamo cercando di realizzare in DownToZero. Stiamo costruendo un'infrastruttura che necessita di una comunicazione inter-processo affidabile e a basso overhead - dall'orchestrazione di container alla gestione delle impostazioni delle macchine. Più approfondivo Varlink, più mi sono reso conto che potrebbe essere esattamente ciò di cui abbiamo bisogno.

In questo post, ti guiderò attraverso i miei esperimenti con Varlink. Costruiremo insieme un semplice servizio hello world, passo passo, e condividerò le mie riflessioni sul perché questa tecnologia mi entusiasma per il futuro della nostra piattaforma.

## Cos'è Varlink e Perché Dovresti Interessartene?

Varlink è un formato di descrizione dell'interfaccia e un protocollo progettati per definire e implementare interfacce di servizio. Pensalo come un'alternativa più semplice e moderna a D-Bus, o un'alternativa più leggera a gRPC per la comunicazione locale.

Se hai lavorato con D-Bus in passato, probabilmente conosci il dolore: sistemi di tipi complessi, introspezione che richiede strumenti speciali, file di configurazione XML che sembrano fatti per confondere. D-Bus è potente, ma risale a un'epoca in cui "semplice" non era una priorità progettuale. Varlink adotta un approccio diverso - è quello che D-Bus potrebbe sembrare se fosse progettato oggi, con sensibilità moderne sull'esperienza dello sviluppatore.

Ecco cosa rende Varlink interessante:

1. **Interfacce autodocumentate**: Ogni servizio Varlink può descrivere la propria API. Puoi connetterti a qualsiasi servizio e chiedere "cosa puoi fare?" e ottenere una risposta leggibile dalla macchina (e anche dall'umano).

2. **Indipendente dal linguaggio**: Il protocollo è semplice al punto che esistono implementazioni in Rust, Go, Python, C, e altro ancora. Ma soprattutto, le interfacce stesse sono neutrali rispetto al linguaggio.

3. **Basato su socket**: La comunicazione avviene tramite socket Unix (o TCP per connessioni remote), il che significa che si integra bene con l’ecosistema Linux, i container e systemd.

4. **Protocollo basato su JSON**: Il formato di trasmissione è JSON, che rende il debug banale. Puoi letteralmente usare `netcat` per parlare con un servizio Varlink, se vuoi.

5. **Integrazione con systemd**: Questo è enorme. systemd usa già Varlink per alcuni dei suoi servizi interni, il che significa che il protocollo è collaudato e ha supporto nativo per l'attivazione tramite socket e la gestione dei servizi.

## Perché Varlink per DownToZero?

In DTZ, ci occupiamo costantemente della configurazione e orchestrazione a livello macchina. La nostra infrastruttura spazia dall'hardware fisico (potresti ricordare i nostri [nodi alimentati a energia solare](/posts/2024/physical-location)), ai container, ai vari servizi di sistema che devono coordinarsi.

Attualmente, abbiamo un mix di approcci per la comunicazione inter-processo:
- API REST per servizi rivolti all'esterno
- Chiamate dirette di funzione all'interno dei nostri servizi Rust
- Alcuni protocolli basati su socket ad-hoc per casi d'uso specifici

Quello che ci manca è un modo unificato per far comunicare i nostri servizi a livello di sistema. Considera questi scenari:

- Un servizio container deve interrogare i limiti di risorse dell'host
- I nostri agenti di osservabilità devono comunicare con i demoni di monitoraggio di sistema
- Le modifiche di configurazione devono propagarsi tra più servizi locali
- I controlli di salute del sistema devono aggregare dati da vari sottosistemi

Per tutti questi casi, Varlink offre una soluzione convincente. È locale prima di tutto (ottimo per la latenza), autodocumentante (ottimo per il debug), e ha supporto nativo systemd (ottimo per l'affidabilità).

Il fatto che systemd stesso usi Varlink per servizi come `systemd-resolved` e `systemd-hostnamed` significa che potremmo potenzialmente integrarci direttamente con i servizi di sistema usando lo stesso protocollo che usiamo per i nostri. È potente.

## Costruiamo Qualcosa: Un Servizio Hello World

Basta teoria - mettiamoci all'opera. Ho creato una semplice implementazione hello world per testare il terreno, e ti guiderò nella costruzione da zero.

Il codice sorgente completo è disponibile su [https://github.com/DownToZero-Cloud/varlink-helloworld](https://github.com/DownToZero-Cloud/varlink-helloworld).

### Passo 1: Creare il Progetto

Per prima cosa, crea un nuovo progetto Rust:

```bash
cargo new varlink-helloworld
cd varlink-helloworld

Ora dobbiamo aggiungere le dipendenze. Apri Cargo.toml e aggiungi:

[package]
name = "varlink-helloworld"
version = "0.1.0"
edition = "2024"

[dependencies]
futures-util = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
zlink = { version = "0.2" }

Stiamo usando zlink, l’implementazione moderna Rust di Varlink. È async-first e basata su tokio, che si adatta perfettamente al modo in cui costruiamo servizi in DTZ. Abbiamo anche bisogno di serde per la serializzazione JSON (il formato di trasmissione Varlink) e futures-util per la gestione degli stream.

Passo 2: Implementare il Servizio

Ora arriva la parte divertente - implementare il nostro servizio da zero. La bellezza di zlink è che non serve generazione di codice o file di definizione dell’interfaccia separati. Definiamo tutto direttamente in Rust, il che significa supporto completo dell’IDE, controllo dei tipi e niente magie a build-time.

Crea src/main.rs:

use serde::{Deserialize, Serialize};
use zlink::{
    self, Call, Connection, ReplyError, Server, Service, 
    connection::Socket, service::MethodReply,
    unix, varlink_service::Info,
};

const SOCKET_PATH: &str = "/tmp/hello.varlink";

#[tokio::main]
async fn main() {
    println!("starting varlink hello world server");
    run_server().await;
}

pub async fn run_server() {
    // Pulisce eventuali file socket esistenti
    let _ = tokio::fs::remove_file(SOCKET_PATH).await;
    
    // Lega al socket Unix
    let listener = unix::bind(SOCKET_PATH).unwrap();
    
    // Crea il nostro servizio e il server
    let service = HelloWorld {};
    let server = Server::new(listener, service);

    match server.run().await {
        Ok(_) => println!("server done."),
        Err(e) => println!("server error: {:?}", e),
    }
}

Questo è il nostro punto di ingresso - semplice e pulito. Ci leghiamo a un socket Unix in /tmp/hello.varlink, creiamo il nostro servizio e lasciamo che il server gestisca le connessioni in arrivo.

Passo 3: Definire i Tipi di Messaggi

Qui si vede l’eleganza di Varlink. Definiamo il nostro protocollo completamente usando tipi Rust con annotazioni serde. Vediamo ogni parte:

Chiamate di Metodo (Richieste in ingresso)

#[derive(Debug, Deserialize)]
#[serde(tag = "method")]
enum HelloWorldMethod {
    #[serde(rename = "rocks.dtz.HelloWorld.Hello")]
    Hello,
    #[serde(rename = "rocks.dtz.HelloWorld.NamedHello")]
    NamedHello {
        #[serde(default)]
        parameters: NamedHelloParameters,
    },
    #[serde(rename = "org.varlink.service.GetInfo")]
    VarlinkGetInfo,
}

#[derive(Debug, Serialize, Deserialize, Default)]
pub struct NamedHelloParameters {
    name: String,
}

L’enum HelloWorldMethod rappresenta tutti i metodi che il nostro servizio può gestire. L’attributo #[serde(tag = "method")] dice a serde di usare il campo JSON method per determinare quale variante deserializzare. Gli attributi #[serde(rename = "...")] mappano le varianti Rust ai nomi reali dei metodi Varlink.

Nota come NamedHello ha un campo parameters annidato - questo corrisponde al protocollo Varlink dove i parametri metodo sono racchiusi in un oggetto parameters nel JSON.

Risposte (Risposte in uscita)

#[derive(Debug, Serialize)]
#[serde(untagged)]
enum HelloWorldReply {
    Hello(HelloResponse),
    VarlinkInfo(Info<'static>),
}

#[derive(Debug, Serialize)]
pub struct HelloResponse {
    message: String,
}

L’enum delle risposte usa #[serde(untagged)] perché le risposte Varlink non includono un discriminatore di tipo - il tipo di risposta è implicito in base al metodo chiamato. HelloResponse è la nostra semplice struct di risposta che contiene un campo message.

Gestione degli Errori

#[derive(Debug, ReplyError)]
#[zlink(interface = "rocks.dtz.HelloWorld")]
enum HelloWorldError {
    Error { message: String },
}

La macro #[derive(ReplyError)] di zlink genera il codice necessario a serializzare i nostri errori secondo il formato di errore Varlink. L’attributo #[zlink(interface = "...")] specifica a quale interfaccia appartengono questi errori.

Passo 4: Implementare il Trait Service

Ora leghiamo tutto implementando il trait Service:

struct HelloWorld {}

impl Service for HelloWorld {
    type MethodCall<'de> = HelloWorldMethod;
    type ReplyParams<'ser> = HelloWorldReply;
    type ReplyStreamParams = ();
    type ReplyStream = futures_util::stream::Empty<zlink::Reply<()>>;
    type ReplyError<'ser> = HelloWorldError;

    async fn handle<'ser, 'de: 'ser, Sock: Socket>(
        &'ser mut self,
        call: Call<Self::MethodCall<'de>>,
        _conn: &mut Connection<Sock>,
    ) -> MethodReply<Self::ReplyParams<'ser>, Self::ReplyStream, Self::ReplyError<'ser>> {
        println!("handling call: {:?}", call.method());
        match call.method() {
            HelloWorldMethod::Hello => {
                MethodReply::Single(Some(HelloWorldReply::Hello(HelloResponse {
                    message: "Hello, World!".to_string(),
                })))
            }
            HelloWorldMethod::NamedHello { parameters } => {
                MethodReply::Single(Some(HelloWorldReply::Hello(HelloResponse {
                    message: format!("Hello, {}!", parameters.name),
                })))
            }
            HelloWorldMethod::VarlinkGetInfo => {
                MethodReply::Single(Some(HelloWorldReply::VarlinkInfo(Info::<'static> {
                    vendor: "DownToZero",
                    product: "hello-world",
                    url: "https://github.com/DownToZero-Cloud/varlink-helloworld",
                    interfaces: vec!["rocks.dtz.HelloWorld", "org.varlink.service"],
                    version: "1.0.0",
                })))
            }
        }
    }
}

Il trait Service è il cuore di zlink. Ecco cosa succede:

  1. Tipi associati: Dichiariamo quali tipi il nostro servizio usa per le chiamate ai metodi, le risposte, le risposte in streaming e gli errori. Questo ci dà completa sicurezza di tipo.

  2. Il metodo handle: Qui vengono instradate tutte le chiamate in ingresso. Facciamo matching sul metodo deserializzato e ritorniamo la risposta appropriata.

  3. MethodReply::Single: Per risposte non in streaming, incapsuliamo la risposta in MethodReply::Single. Varlink supporta anche risposte in streaming (utili per monitoraggio o sottoscrizioni), ma qui restiamo semplici.

  4. VarlinkGetInfo: Ogni servizio Varlink dovrebbe implementare il metodo org.varlink.service.GetInfo. Restituisce i metadati sul nostro servizio - venditore, nome prodotto, versione, URL e la lista delle interfacce implementate.

Passo 5: Esecuzione e Test

Avvia il server:

cargo run

Dovresti vedere:

starting varlink hello world server

Ora, in un altro terminale, possiamo testarlo usando varlinkctl, che fa parte di systemd. Per prima cosa, vediamo cosa espone il servizio:

varlinkctl info /tmp/hello.varlink

Output:

    Vendor: DownToZero
   Product: hello-world
   Version: 1.0.0
       URL: https://github.com/DownToZero-Cloud/varlink-helloworld
Interfaces: org.varlink.service
            rocks.dtz.HelloWorld

Questa è la natura autodocumentante di Varlink in azione. Il client può scoprire esattamente cosa offre questo servizio.

Ora chiamiamo i nostri metodi:

varlinkctl call /tmp/hello.varlink rocks.dtz.HelloWorld.Hello {}

Output:

{
    "message" : "Hello, World!"
}

E con un parametro:

varlinkctl call /tmp/hello.varlink rocks.dtz.HelloWorld.NamedHello '{"name":"jens"}'

Output:

{
    "message" : "Hello, jens!"
}

Funziona! Abbiamo un servizio Varlink pienamente funzionante.

Debug e Esplorazione

Una cosa che adoro di Varlink è quanto sia facile esplorare e fare debug. Poiché il protocollo è basato su JSON, puoi anche usare strumenti di base come socat o netcat per test manuali:

echo '{"method":"rocks.dtz.HelloWorld.Hello","parameters":{}}' | \
  socat - UNIX-CONNECT:/tmp/hello.varlink

Riceverai una risposta JSON che puoi passare a jq o leggere direttamente. Nessun strumento di debug speciale necessario, nessun protocollo binario da decodificare. Quando alle 2 di notte qualcosa non funziona, questa semplicità è impagabile.

Puoi anche introspezionare la definizione dell’interfaccia stessa:

varlinkctl introspect /tmp/hello.varlink rocks.dtz.HelloWorld

Questo restituisce l’esatta definizione dell’interfaccia che abbiamo scritto prima. Combinato con il comando info, hai completa visibilità su cosa può fare qualsiasi servizio Varlink - anche quelli mai visti prima.

Integrazione con systemd

Una delle funzionalità più potenti di Varlink è la sua integrazione con systemd. Puoi creare servizi socket-activated che partono solo quando qualcuno si connette, e systemd gestisce il ciclo di vita.

Crea un unit socket di systemd (hello-varlink.socket):

[Unit]
Description=Hello World Varlink Socket

[Socket]
ListenStream=/run/hello.varlink

[Install]
WantedBy=sockets.target

E una corrispondente unit service (hello-varlink.service):

[Unit]
Description=Hello World Varlink Service

[Service]
ExecStart=/usr/local/bin/varlink-helloworld

Con l’attivazione tramite socket, systemd ascolta sul socket e, quando arriva una connessione, avvia il servizio e gli passa il socket. Questo significa uso di risorse zero finché qualcuno non ha realmente bisogno del servizio - perfetto per la nostra filosofia scale-to-zero in DTZ.

Ma la storia con systemd non finisce qui. Diversi componenti systemd espongono già interfacce Varlink:

Ciò significa che possiamo usare gli stessi pattern Varlink che stiamo sviluppando per i nostri servizi per interagire con il sistema host. Vuoi interrogare la cache DNS? varlinkctl call /run/systemd/resolve/io.systemd.Resolve io.systemd.Resolve.ResolveHostname '{"name":"example.com"}'. Stesso protocollo, stessi strumenti, stesso modello mentale.

Per DTZ, questo è particolarmente entusiasmante perché significa che il nostro livello di orchestrazione può usare un approccio unificato sia per l’IPC a livello applicativo sia per la gestione a livello di sistema. Niente più cambi di contesto tra API e protocolli diversi.

Cosa C’è Dopo?

Questo esperimento hello world mi ha veramente entusiasmato sul potenziale di Varlink per DTZ. Ecco alcune direzioni che sto considerando:

  1. Servizio di configurazione macchina: Un servizio Varlink che espone impostazioni macchina (configurazione rete, limiti risorse, ecc.) con adeguato controllo degli accessi.

  2. IPC per orchestrazione container: Usare Varlink per comunicare tra runtime container e servizi di gestione.

  3. Aggregazione osservabilità: Un servizio Varlink locale che aggrega metriche da vari componenti di sistema.

  4. Integrazione systemd: Interrogare direttamente le interfacce Varlink di systemd per stato servizio e gestione.

  5. Aggregazione controllo di salute: Un servizio Varlink centrale che raccoglie lo stato di salute da tutti i servizi attivi e espone un endpoint di salute unificato.

Il fatto che possiamo usare lo stesso protocollo per parlare sia con i nostri servizi che con quelli di sistema come systemd-resolved è una grande vittoria per la coerenza e la riduzione della complessità.

Sono anche curioso delle caratteristiche di performance. Sebbene JSON non sia il formato più compatto, per IPC locale il costo di parsing è tipicamente trascurabile rispetto ai benefici della leggibilità umana. Ciò detto, ho in programma qualche benchmark in un esperimento successivo per avere numeri reali su latenza e throughput nei nostri casi d’uso.

Conclusioni

Varlink colpisce un punto di equilibrio tra semplicità e capacità. Non cerca di risolvere ogni problema dei sistemi distribuiti - è focalizzato su fare bene l’IPC locale, con giuste caratteristiche per scoperta e sicurezza di tipo.

Per DownToZero, dove ottimizziamo costantemente per efficienza e semplicità, questo approccio risuona fortemente. Non ci serve la complessità di gRPC per la comunicazione locale. Non vogliamo l’overhead di HTTP per chiamate interne alla macchina. Varlink ci dà un protocollo pulito e ben progettato che si integra bene con l’ecosistema Linux su cui costruiamo.

Se sei interessato a sperimentare, prendi il codice da GitHub, apri il tuo editor e prova. La curva di apprendimento è dolce, e c’è qualcosa di soddisfacente nel vedere quel primo varlinkctl call che restituisce il tuo messaggio.

Buona sperimentazione!

Risorse