Usando Cilium no WSL
Published: 2024-06-12 15:54:44

### Criando um ambiente de teste do Cilium no WSL

#### eBPF a base do Cilium

O eBPF é umas das tecnologias mais faladas nos últimos tempos na comunidade de tecnologia,  
isso graças sua capacidade de estender as funções do kernel sem precisar alterar código do kernel ou carregar modulos. Com eBPF você escreve programas em C ou Rust que são compilados em bytecode.

![Ilustração ebpf](https://i.ibb.co/MkqZD8w/cilium-ilustra-o.png)
[Guia Ilustrado do eBPF](https://isovalent.com/books/children-guide-to-ebpf/)

#### Afinal, o que é Cilium?

Cilium é um software de código aberto que aproveita das funcionalidades do eBPF para entregar ao kubernetes soluções para Ingress, gateways api, service mesh, segurança e observabilidade entre outras. Ele consegue atuar de forma transparente sem uso de container sidecar como Envoy.

[Documentação Cilium](https://docs.cilium.io/en/stable/overview/intro/#what-is-cilium)

*"eBPF é uma tecnologia de kernel revolucionária que permite aos desenvolvedores escrever
código que pode ser carregado no kernel dinamicamente, mudando a maneira como o kernel
se comporta. 
Isso permite uma nova geração de redes de alto desempenho, observabilidade e
ferramentas de segurança. E como você verá, se quiser instrumentar um aplicativo com essas ferramentas baseadas em eBPF, você não precisa modificar ou reconfigurar o aplicativo de qualquer forma, graças ao ponto de vista do eBPF dentro do kernel."
Liz Rice, no seu livro gratuíto [Learning eBPF](https://isovalent.com/books/learning-ebpf/)*

A isovalent também contem vários [labs gratuitos](https://isovalent.com/labs/) para aprender usar o Cilium e outras ferramentas da Isovalent, como o hubble, e ainda você ganha badges do Creddly 😍.
#### Compilando um novo Kernel para o WSL ###

Para conseguir carregar os módulos necessários vamos precisar compilar um kernel já com as funcionalidades necessárias ativas,  o WSL padrão vem com o kernel 5.15, mas já que vamos precisar recompilar tudo, vamos colocar logo um mais novo, vamos baixar o kernel 6.8, que é a versão padrão do Ubuntu 24.04, também alguma features do Cilium somente estão disponíveis em versões mais novas do kernel como pode ver na tabela abaixo.


**Meu ambiente**
**Sistema operacional:** Windows 11 23H2
**Distro WSL**: Ubuntu 24.04 LTS
**Versão do WSL:** 2.1.5.0
**Docker Desktop**: 4.30
**Gerenciador de pacotes:** Scoop

|Cilium Feature|Minimum Kernel Version|
|---|---|
|[Bandwidth Manager](https://docs.cilium.io/en/stable/network/kubernetes/bandwidth-manager/#bandwidth-manager)|>= 5.1|
|[Egress Gateway](https://docs.cilium.io/en/stable/network/egress-gateway/#egress-gateway)|>= 5.2|
|VXLAN Tunnel Endpoint (VTEP) Integration|>= 5.2|
|[WireGuard Transparent Encryption](https://docs.cilium.io/en/stable/security/network/encryption-wireguard/#encryption-wg)|>= 5.6|
|Full support for [Session Affinity](https://docs.cilium.io/en/stable/network/kubernetes/kubeproxy-free/#session-affinity)|>= 5.7|
|BPF-based proxy redirection|>= 5.7|
|Socket-level LB bypass in pod netns|>= 5.7|
|L3 devices|>= 5.8|
|BPF-based host routing|>= 5.10|
|IPv6 BIG TCP support|>= 5.19|
|IPv4 BIG TCP support|>= 6.3|

Abra o shell do Ubuntu WSL, no seu gerenciador de terminal, o meu é o [Windows Terminal](https://github.com/microsoft/terminal), 
e siga os passos

1. Instale as ferramentas necessárias para compilação

```bash
sudo apt update && sudo apt install build-essential flex bison libssl-dev libelf-dev bc python3 pahole
```

2. Baixe o kernel no repositório do linux, baixando só a branch que vamos usar no casol linux-6.8.y.

```bash
## baixando do repositorio
git clone --depth 1 --branch linux-6.8.y https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
### Entre na pasta
cd linux
```

3. Estando dentro da pasta, linux,  vamos baixar o arquivo de configuração padrão do kernel do Wsl e salva-lo como .config.

```bash
wget https://raw.githubusercontent.com/microsoft/WSL2-Linux-Kernel/linux-msft-wsl-6.1.y/arch/x86/configs/config-wsl -O .config
```

4. Vamos fazer o replace das entrada LOCALVERVSION para generic
```
sed -i 's/microsoft-standard-WSL2/generic/' ./.config
```

5. Vamos ajustar o arquivo .config para atender a todos os requisitos do Cilium

- Vamos criar um arquivo chamado *cilium_modules* e colocar o conteudo abaixo dentro.
```bash
## linux/cilium_modules
## Base requirements
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_NET_CLS_BPF=y
CONFIG_BPF_JIT=y
CONFIG_NET_CLS_ACT=y
CONFIG_NET_SCH_INGRESS=y
CONFIG_CRYPTO_SHA1=y
CONFIG_CRYPTO_USER_API_HASH=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_BPF=y

## Iptables-based Masquerading
CONFIG_NETFILTER_XT_SET=m
CONFIG_IP_SET=m
CONFIG_IP_SET_HASH_IP=m

## L7 and FQDN Policies
CONFIG_NETFILTER_XT_TARGET_TPROXY=m
CONFIG_NETFILTER_XT_TARGET_CT=m
CONFIG_NETFILTER_XT_MATCH_MARK=m
CONFIG_NETFILTER_XT_MATCH_SOCKET=m

## IPsec
CONFIG_XFRM=y
CONFIG_XFRM_OFFLOAD=y
CONFIG_XFRM_STATISTICS=y
CONFIG_XFRM_ALGO=m
CONFIG_XFRM_USER=m
CONFIG_INET_ESP=m
CONFIG_INET_IPCOMP=m
CONFIG_INET_XFRM_TUNNEL=m
CONFIG_INET_TUNNEL=m
CONFIG_INET6_ESP=m
CONFIG_INET6_IPCOMP=m
CONFIG_INET6_XFRM_TUNNEL=m
CONFIG_INET6_TUNNEL=m
CONFIG_INET_XFRM_MODE_TUNNEL=m
CONFIG_CRYPTO_AEAD=m
CONFIG_CRYPTO_AEAD2=m
CONFIG_CRYPTO_GCM=m
CONFIG_CRYPTO_SEQIV=m
CONFIG_CRYPTO_CBC=m
CONFIG_CRYPTO_HMAC=m
CONFIG_CRYPTO_SHA256=m
CONFIG_CRYPTO_AES=m
```

- Agora, vamos criar um script Python chamado **enable_conf.py** , para obter o conteúdo arquivo cilium_modules e ajustar o .config.

```python
import re
# Lê o conteúdo do arquivo 'cilium_modules
config_replacements = {}
with open('cilium_modules', 'r', encoding='utf-8') as file1:
    for line in file1:
        line = line.strip()
        # Ignora linhas vazias e comentários
        if not line or line.startswith('##'):
            continue
        key, value = line.split('=')
        config_replacements[key] = value
# Lê o conteúdo do arquivo '.config'
with open('.config', 'r') as file2:
    file2_lines = file2.readlines() 
# Mantém um conjunto para controle das chaves que foram atualizadas
updated_keys = set()
# Substitui linhas correspondentes em '.config'
with open('.config', 'w', encoding='utf-8') as file2:
    for line in file2_lines:
        # Verifica se a linha contém alguma chave de 'cilium_modules' usando regex
        for key, value in config_replacements.items():
            if re.search(r'\b' + re.escape(key) + r'\b', line):
                # Se a linha estiver comentada, remove o símbolo de comentário e atualiza
                if line.startswith('# ' + key):
                    line = f"{key}={value}\n"
                # Atualiza o valor da linha
                elif re.search(r'^\s*' + re.escape(key) + r'\b', line):
                    line = f"{key}={value}\n"
                updated_keys.add(key)
                break
        file2.write(line)
    # Adiciona as chaves que não foram encontradas ao '.config'
    for key, value in config_replacements.items():
        if key not in updated_keys:
            file2.write(f"{key}={value}\n")
```

[Gist](https://gist.github.com/cslemes/36ffb29194724cf266d69779b8b5f2f2)

- Execute o script
```
python3 enable_conf.py
```

6. Agora só rodar o Make, pode deixar todas perguntas no padrão.

```bash
make -j $(nproc)
```

7. Finalizando a compilação, instale os moduloes.

```bash
sudo make modules_install
```

8. Vamos criar uma pasta no Windows, para colocar o kernel novo, lembrando que todas as do WSL compartilham o mesmo kernel, então vamos colocar no drive C:.
- No Ubuntu  crie o diretório.
```bash
mkdir /mnt/c/wslkernel
```
- Copie o novo kernel para a pasta vamos renomear para  **kernelcilium**

```bash
cp arch/x86/boot/bzImage /mnt/c/wslkernel/kernelcilium
```

9. Agora vamos alterar o .wslconfig para as distros subirem com o kernel novo,
pode usar o seu editor de texto de preferencia, estando no windows, navegue até a pasta, $env:USERPROFILE e edite o .wslconfig, e adicione a configuração conforme abaixo.

```
[wsl2]
kernel = C:\\wslkernel\\kernelnoble
```

10. Feche as janelas abertas com o wsl e derrube todas as distros.

```
wsl --shutdown
```

11. Abra o Ubuntu novamente e confirme se está usando o kernel novo. 

```
uname -r
```

12. Vamos criar o arquivo de configuração para os modulos necessários carregarem na inicialização.

```bash
awk '(NR>1) { print $2 }' /usr/lib/modules/$(uname -r)/modules.alias | sudo tee /etc/modules-load.d/cilium.conf
```

13. Vamos reiniciar o daemon e o serviço de modulos
```bash
sudo systemctl daemon-reload

sudo systemctl restart systemd-modules-load
```

12. Checando se está tudo certo

```bash
$cris /kind ❱❱ sudo systemctl status systemd-modules-load
● systemd-modules-load.service - Load Kernel Modules
     Loaded: loaded (/usr/lib/systemd/system/systemd-modules-load.service; static)
     Active: active (exited) since Tue 2024-06-11 11:23:40 -03; 4h 59min ago
       Docs: man:systemd-modules-load.service(8)
             man:modules-load.d(5)
    Process: 56 ExecStart=/usr/lib/systemd/systemd-modules-load (code=exited, status=0/SUCCESS)
   Main PID: 56 (code=exited, status=0/SUCCESS)

Notice: journal has been rotated since unit was started, output may be incomplete.
$cris /kind ❱❱ lsmod
Module                  Size  Used by
ipcomp6                12288  0
xfrm6_tunnel           12288  1 ipcomp6
tunnel6                12288  1 xfrm6_tunnel
esp6                   24576  0
xfrm_user              53248  4
xfrm4_tunnel           12288  0
ipcomp                 12288  0
xfrm_ipcomp            12288  2 ipcomp6,ipcomp
esp4                   24576  0
xfrm_algo              16384  4 esp6,esp4,xfrm_ipcomp,xfrm_user
ip_set_hash_netportnet    49152  0
ip_set_hash_netnet     49152  0
ip_set_hash_netiface    45056  0
ip_set_hash_netport    45056  0
ip_set_hash_net        45056  0
ip_set_hash_mac        24576  0
ip_set_hash_ipportnet    45056  0
ip_set_hash_ipportip    40960  0
ip_set_hash_ipport     40960  0
ip_set_hash_ipmark     40960  0
ip_set_hash_ipmac      40960  0
....

```

#### Criando o Cluster kubernetes com Kind

Instalando o client Cilium, você pode fazer todas a instalação também usando Helm.

A partir de agora usaremos somente o powershell para criação dos recursos, primeiramente vamos criar um cluster usando o Kind. 

1. Crie o arquivo de configuração do kind,  vamos desativar a rede padrão e o kubeproxy.
```yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  # localhost.run proxy
  - containerPort: 32042
    hostPort: 32042
  # Hubble relay
  - containerPort: 31234
    hostPort: 31234
  # Hubble UI
  - containerPort: 31235
    hostPort: 31235
- role: worker
- role: worker
networking:
  disableDefaultCNI: true
  kubeProxyMode: "none"
```

2. Agora vamos instalar o Cilium no cluster para isso vamos usar o cliente do cilium, a instalação também pode ser feita via helm.
 - Baixe a ultima release do [Cilium](https://github.com/cilium/cilium-cli/releases) para sua plataforma, e descompacte em uma pasta de sua preferencia, lembrando que para executar em qualquer prompt precisa colocar o local do executavel na varivel de ambiente PATH.

```powershell
## Baixando
aria2c https://github.com/cilium/cilium-cli/releases/download/v0.16.10/cilium-windows-amd64.zip

## descompactando
unzip.exe .\cilium-windows-amd64.zip
```
 - A opção que prefiro é usar o Scoop, o Cilium não está em nenhum bucket official, então vamos precisar criar uma instalação customizada.
 - Crie uma arquivo chamado cilium.json e coloque o conteúdo abaixo.
```json
{
  "bin": "cilium.exe",
  "version": "v0.16.10",
  "url": https://github.com/cilium/ciliumcli/releases/download/v0.16.10/cilium-windows-amd64.zip"
}
``` 
   - Agora é somente instalar com o scoop apontando para o arquivo json.
```powershell
scoop install cilium.json
```

3. Agora é só executar o comando do cilium para instalar ele no cluster ele vai achar seu cluster pelo contexto atual do .kube/config, pode confirmar usando o comando `kubectl config get-contexts` , 

```
cilium install
```

```powershell
$cris /kind ❱❱ cilium install
🔮 Auto-detected Kubernetes kind: kind
✨ Running "kind" validation checks
✅ Detected kind version "0.23.0"
ℹ️  Using Cilium version 1.15.5
🔮 Auto-detected cluster name: kind-kind
ℹ️  Detecting real Kubernetes API server addr and port on Kind
🔮 Auto-detected kube-proxy has not been installed
ℹ️  Cilium will fully replace all functionalities of kube-proxy
```

Depois de alguns minutos o Cilium está pronto, podemos verificar os status do cilium com o cli.

```powerhell
cris /kind ❱❱ cilium status
    /¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    disabled (using embedded mode)
 \__/¯¯\__/    Hubble Relay:       disabled
    \__/       ClusterMesh:        disabled

Deployment             cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1
DaemonSet              cilium             Desired: 3, Ready: 3/3, Available: 3/3
Containers:            cilium             Running: 3
                       cilium-operator    Running: 1
Cluster Pods:          3/3 managed by Cilium
Helm chart version:
Image versions         cilium             quay.io/cilium/cilium:v1.15.5@sha256:4ce1666a73815101ec9a4d360af6c5b7f1193ab00d89b7124f8505dee147ca40: 3
                       cilium-operator    quay.io/cilium/operator-generic:v1.15.5@sha256:f5d3d19754074ca052be6aac5d1ffb1de1eb5f2d947222b5f10f6d97ad4383e8: 1
```

Caso exiba algum erro, você pode dar uma olhada no status do daemont set, e conferir os logs dos pods.

```powershell
$cris /kind ❱❱ k get daemonsets -n kube-system
Events:
  Type    Reason            Age    From                  Message
  ----    ------            ----   ----                  -------
  Normal  SuccessfulCreate  6m47s  daemonset-controller  Created pod: cilium-c74rc
  Normal  SuccessfulCreate  6m47s  daemonset-controller  Created pod: cilium-b7rrn
  Normal  SuccessfulCreate  6m47s  daemonset-controller  Created pod: cilium-wmxlx
```

Verficando o status dos pods
```powershell
k get pods -l k8s-app=cilium -n kube-**system**
k logs -l k8s-app=cilium -n kube-system
```

Se der algum erro em modulo, pode ter faltado algum passo da etapa build do kernel, pode analisar novamente. Pode pegar o nome do modulo que deu erro e tentar carrega-lo, usando modprobe.
Ex:

```bash
sudo modprobe xt_TPROXY
```

Se não der erro e aparecer no lsmod, provalmente só faltar por ele no boot do linux, como foi feito na parte 12 da compilação do kernel.

#### Testando o ambiente.

Para o teste vamos usar o app Star Wars Demo do lab [Getting Started with Cilium](https://isovalent.com/labs/cilium-getting-started/) da Isovalent.
Neste lab, fazemos o deploy de um microserviço simples, temos um deployment chamado DeathStar, que vai receber as requisições POST dos pods  xwing e tiefigher, vamos usar o Cilium para controlar a comunicação entre os pods, baseando-se nos labels configurados.

Os Labels:
- Death Star: `org=empire, class=deathstar`
- Imperio TIE fighter: `org=empire, class=tiefighter`
- Rebel X-Wing: `org=alliance, class=xwing`

Vamos usar criar o app no cluster usando o yaml [`http-sw-app.yaml`](https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/minikube/http-sw-app.yaml):.

```yaml
k apply -f https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/minikube/http-sw-app.yaml
``` 

Checando a criação dos recursos.
```powershell
$cris /kind ❱❱ k get pod,deploy,svc
NAME                             READY   STATUS    RESTARTS   AGE
pod/deathstar-689f66b57d-9c92f   1/1     Running   0          29m
pod/deathstar-689f66b57d-b4ps7   1/1     Running   0          29m
pod/tiefighter                   1/1     Running   0          29m
pod/xwing                        1/1     Running   0          29m

NAME                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/deathstar   2/2     2            2           29m

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/deathstar    ClusterIP   10.96.120.87   <none>        80/TCP    29m
service/kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP   130m
```

O manifesto cria também um serviço para gerenciar a comunicação com a  DeathStar, vamos usar o exec para simular que estamos execuntando o comando a partir dos pods xwing.

```powershell
k exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing

k exec xwing -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
```

No momento sem politicas ativas, as duas naves podem pousar e a api responde "Ship landed"
Vamos criar uma politica usando o cilium, abaixo o manifesto da politica, vamos fazer um bloqueio simples de porta.
Essa politica abaixo atua nas camadas de rede 3 e 4, em suma podemos controlar IP e Porta.

```yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "L3-L4 policy to restrict deathstar access to empire ships only"
  # definindo o pod que vai receber a requisição (No caso a DeathStar)
  endpointSelector:
    matchLabels:
      org: empire
      class: deathstar
  ingress:
  # definindo a origem da conexão, somente permitindo o pod com o label org = empire de acessar na porta 80.
  - fromEndpoints:
    - matchLabels:
        org: empire
    toPorts:  
    - ports:
      - port: "80"
        protocol: TCP
   
```     

Aplicando a política.
```powershell
k apply -f https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/minikube/sw_l3_l4_policy.yaml
```

Testando as politicas
```powershell
k exec xwing  -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing

k exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
```

Agora só o tiefighter recebe o retorno da API, o xwing não consegue conectar, pode dar um CTRL+C para sair.

Agora queremos que o tiefighter somente use a área de pouso, nossa api tem outros endpoints, mas somente queremos que utilize o /request-landing, para isso precisamos criar um regra de HTTP, como nas regras de cama 3 e 4 só trabalhamos com ip e porta, vamos precisar criar uma regra de camada 7, para controlar o tráfego http:

Endpoints:
```powershell
$cris /kind ❱❱ k exec tiefighter -- curl -s -get deathstar.default.svc.cluster.local/v1
{
        "name": "Death Star",
        "hostname": "deathstar-689f66b57d-9c92f",
        "model": "DS-1 Orbital Battle Station",
        "manufacturer": "Imperial Department of Military Research, Sienar Fleet Systems",
        "cost_in_credits": "1000000000000",
        "length": "120000",
        "crew": "342953",
        "passengers": "843342",
        "cargo_capacity": "1000000000000",
        "hyperdrive_rating": "4.0",
        "starship_class": "Deep Space Mobile Battlestation",
        "api": [
                "GET   /v1",
                "GET   /v1/healthz",
                "POST  /v1/request-landing",
                "PUT   /v1/cargobay",
                "GET   /v1/hyper-matter-reactor/status",
                "PUT   /v1/exhaust-port"
        ]
}
```

Para ajustar o yaml para controlar o trafego http, simplesmente adicionamos o campo rules no manifesto, adicionando mais refino da politica.

```yaml
apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: "rule1"
spec:
  description: "L7 policy to restrict access to specific HTTP call"
  endpointSelector:
    matchLabels:
      org: empire
      class: deathstar
  ingress:
  - fromEndpoints:
    - matchLabels:
        org: empire
    toPorts:
    - ports:
      - port: "80"
        protocol: TCP
      rules:
        http:
        - method: "POST"
          path: "/v1/request-landing"

```

Antes de ter politicas, conseguimos facilmente destruir a DeathStar.
```powershell
$cris /kind ❱❱ k exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
Panic: deathstar exploded

goroutine 1 [running]:
main.HandleGarbage(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
        /code/src/github.com/empire/deathstar/
        temp/main.go:9 +0x64
main.main()
        /code/src/github.com/empire/deathstar/
        temp/main.go:5 +0x85
```

Aplicando a politica
```powershell
k apply -f https://raw.githubusercontent.com/cilium/cilium/HEAD/examples/minikube/sw_l3_l4_l7_policy.yaml
```

Testando. 

```powershell
k exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing

k exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
```

Continuamos conseguindo pousar, mas a porta de exaustão está protegida contra Tiefighters.

```powershell
$cris /kind ❱❱ k exec tiefighter -- curl -s -XPOST deathstar.default.svc.cluster.local/v1/request-landing
Ship landed

$cris /kind ❱❱ k exec tiefighter -- curl -s -XPUT deathstar.default.svc.cluster.local/v1/exhaust-port
Access denied
```

#### Referências

[WSL Kernel](https://learn.microsoft.com/en-us/community/content/wsl-user-msft-kernel-v6)
[Falco WSL](https://falco.org/blog/falco-wsl2-custom-kernel/)
[WSL Kernel Cilium](https://wsl.dev/wslcilium/)