Pular para conteúdo

Lab: CGNAT, port forward e Portr no VirtualBox

Laboratório para a aula sobre por que port forward não funciona atrás de CGNAT e como o Portr resolve o problema sem IP público.

Topologia

[Rede de casa]──bridged──[CHR-ISP]──intnet "isp-acesso"──[pfSense]──intnet "lan-cliente"──[Linux cliente]
 (papel de internet)      PPPoE server                    WAN PPPoE                         nginx + Portr
                          + CGNAT                         100.64.0.x                        192.168.1.10

[VPS com IP público e domínio]──Portr server (fora do lab)
VM Função vCPU RAM Placas
CHR-ISP Provedor: PPPoE server + CGNAT 1 256 MB nic1 bridged (uplink), nic2 intnet isp-acesso
pfSense Roteador do cliente 1 1 GB nic1 intnet isp-acesso (WAN), nic2 intnet lan-cliente (LAN)
Linux-Cliente Servidor web atrás do CGNAT 1 1 GB nic1 intnet lan-cliente

Endereçamento:

Segmento Rede Observação
Uplink da CHR DHCP da rede de casa Faz papel de "IP público" do provedor
Acesso PPPoE 100.64.0.0/24 (RFC 6598) Gateway 100.64.0.1, assinante 100.64.0.10+
LAN do cliente 192.168.1.0/24 pfSense 192.168.1.1, Linux 192.168.1.10

0. Preparação do host

O lab usa só VirtualBox, sem virtualização aninhada, então roda em qualquer máquina x86_64 com 8 GB de RAM ou mais. Siga a subseção do seu sistema e depois continue no passo 1.

Recursos totais do lab: 3 vCPU e cerca de 2,5 GB de RAM, mais uns 30 GB de disco.

0.1 Linux

Identificar a placa cabeada (usada na bridge da CHR):

ip -br link

Nos comandos do roteiro, troque enp5s0 pelo nome real da placa.

mkdir -p ~/VMs && cd ~/VMs

Só se a VM não ligar com o erro VERR_SVM_IN_USE ou VERR_VMX_IN_USE. Kernels 6.12 em diante ativam a virtualização da CPU no boot quando o módulo KVM carrega, e o VirtualBox não consegue usá-la. A correção faz o KVM ativar só quando uma VM KVM realmente liga:

# Fedora/RHEL
sudo grubby --update-kernel=ALL --args="kvm.enable_virt_at_load=0"
# Ubuntu/Debian: adicionar o mesmo parâmetro em GRUB_CMDLINE_LINUX_DEFAULT
#   no /etc/default/grub e rodar: sudo update-grub
sudo reboot

O KVM continua funcionando (virt-manager, GNOME Boxes, Podman); só não dá para ter VM KVM e VM VirtualBox ligadas ao mesmo tempo. Para desfazer:

sudo grubby --update-kernel=ALL --remove-args="kvm.enable_virt_at_load=0"
sudo reboot

0.2 Windows

VBoxManage no PATH. No PowerShell:

$env:Path += ";C:\Program Files\Oracle\VirtualBox"
VBoxManage --version
mkdir "$HOME\VMs" -Force; cd "$HOME\VMs"

Nome da placa para a bridge. No Windows o nome é longo e tem espaços:

VBoxManage list bridgedifs | Select-String '^Name:'

Use o nome completo entre aspas, por exemplo --bridgeadapter1 "Intel(R) Ethernet Connection I219-V".

Hyper-V e o VirtualBox. Se Hyper-V, WSL2, Docker Desktop ou "Integridade da memória" estiverem ativos, o VirtualBox roda por cima do hipervisor da Microsoft: o ícone de tartaruga verde aparece na barra da VM. Para este lab isso funciona, só fica mais lento. Não desative nada se o aluno usa WSL2 ou Docker no dia a dia. Se quiser desempenho total e aceitar que WSL2 e Docker Desktop param de funcionar até reverter, em PowerShell como administrador:

bcdedit /set hypervisorlaunchtype off
# reiniciar; para reverter: bcdedit /set hypervisorlaunchtype auto

Sintaxe. Os comandos VBoxManage do roteiro funcionam iguais no PowerShell, com duas diferenças:

  • Quebra de linha é crase (`), não barra invertida. Ou cole o comando em uma linha só.
  • Download, descompactação e criação de arquivos têm equivalentes próprios: veja o Apêndice A.

0.3 macOS

Mac com processador Intel. Funciona igual ao Linux: instale o VirtualBox, aprove a extensão da Oracle em Ajustes do Sistema > Privacidade e Segurança quando pedido, e reinicie. A placa para a bridge costuma ser en0:

VBoxManage list bridgedifs | grep '^Name:'
mkdir -p ~/VMs && cd ~/VMs

Os comandos do roteiro rodam no Terminal (zsh) sem alteração, trocando enp5s0 por en0. Se o Mac só tiver Wi-Fi, a bridge pode não funcionar para a CHR; nesse caso use --nic1 nat na CHR e faça os testes de "acesso pela internet" de dentro do host com port forward do NAT do VirtualBox (Apêndice B).

Mac com Apple Silicon (M1 a M4). O VirtualBox nessas máquinas só roda sistemas ARM, e o pfSense não tem versão ARM para VM. Opções, em ordem de preferência:

  1. Fazer o lab numa máquina x86 (Linux ou Windows) ou numa VM x86 em nuvem com virtualização aninhada.
  2. Usar o UTM em modo de emulação x86 para pfSense e Linux: funciona, mas é lento. A CHR tem imagem ARM64 oficial e roda nativa no UTM.
  3. Assistir a aula e reproduzir só a parte do Portr, que roda nativa no macOS.

1. CHR-ISP (MikroTik RouterOS)

1.1 Baixar a imagem VDI oficial

cd ~/VMs
# versão estável atual direto do servidor de update da MikroTik
VER=$(curl -4 -s https://upgrade.mikrotik.com/routeros/NEWESTa7.stable | awk '{print $1}')
echo "Versão: $VER"

wget -4 https://download.mikrotik.com/routeros/$VER/chr-$VER.vdi.zip
unzip -o chr-$VER.vdi.zip

Se a versão vier vazia, defina na mão conforme mikrotik.com/download (ex.: VER=7.20.1).

No Windows, use o bloco equivalente do Apêndice A.

1.2 Criar a VM

VBoxManage createvm --name CHR-ISP --ostype Linux26_64 --register

# nic1: uplink do provedor (bridged na rede de casa)
# nic2: rede de acesso onde roda o PPPoE server
VBoxManage modifyvm CHR-ISP --cpus 1 --memory 256 --audio-enabled off \
  --nic1 bridged --bridgeadapter1 enp5s0 --nictype1 virtio --nicpromisc1 allow-all \
  --nic2 intnet --intnet2 isp-acesso --nictype2 virtio

VBoxManage storagectl CHR-ISP --name SATA --add sata --portcount 1
VBoxManage storageattach CHR-ISP --storagectl SATA --port 0 --device 0 --type hdd \
  --medium ~/VMs/chr-$VER.vdi

VBoxManage startvm CHR-ISP

Login: admin, senha em branco. Defina uma senha no primeiro acesso.

1.3 Configurar PPPoE server e CGNAT

Cole no terminal da CHR:

# Nomes claros para a aula
/interface ethernet
set [find default-name=ether1] name=wan-internet
set [find default-name=ether2] name=acesso-pppoe

# Uplink recebe IP da rede de casa
/ip dhcp-client add interface=wan-internet disabled=no

# Pool de assinantes no range CGNAT (RFC 6598)
/ip pool add name=pool-cgnat ranges=100.64.0.10-100.64.0.250

# local-address = gateway do assinante; only-one impede login duplicado
/ppp profile add name=plano-cgnat local-address=100.64.0.1 remote-address=pool-cgnat \
    dns-server=1.1.1.1,8.8.8.8 only-one=yes

/ppp secret add name=cliente01 password=Lab@Pppoe2026 service=pppoe profile=plano-cgnat

# max-mtu 1480 compensa o overhead do PPPoE
/interface pppoe-server server add service-name=ISP-LAB interface=acesso-pppoe \
    default-profile=plano-cgnat authentication=chap,mschap2 \
    max-mtu=1480 max-mru=1480 one-session-per-host=yes disabled=no

# CGNAT: todos os assinantes saem com o mesmo IP do uplink
/ip firewall nat add chain=srcnat src-address=100.64.0.0/10 out-interface=wan-internet \
    action=masquerade comment="CGNAT assinantes"

# Nada entra para os assinantes a partir da internet, só retorno de conexão
/ip firewall filter
add chain=forward connection-state=established,related action=accept comment="retorno"
add chain=forward connection-state=invalid action=drop
add chain=forward in-interface=wan-internet dst-address=100.64.0.0/10 action=drop \
    comment="sem entrada direta no CGNAT"

1.4 Validar

/ip dhcp-client print            # anote o IP do uplink: será o "IP público" do provedor
/interface pppoe-server server print
/ppp secret print

Masquerade basta para a aula. CGNAT de produção usa netmap com blocos de porta por assinante e log de NAT, o que rende outra aula.


2. pfSense

2.1 Criar a VM

A ISO do Netgate Installer precisa de internet durante a instalação, por isso a WAN começa em NAT.

VBoxManage createvm --name pfSense --ostype FreeBSD_64 --register

VBoxManage modifyvm pfSense --cpus 1 --memory 1024 --audio-enabled off \
  --nic1 nat --nictype1 82540EM \
  --nic2 intnet --intnet2 lan-cliente --nictype2 82540EM

mkdir -p ~/VMs/pfSense
VBoxManage createmedium disk --filename ~/VMs/pfSense/pfSense.vdi --size 16384

# porta 0 = disco, porta 1 = DVD
VBoxManage storagectl pfSense --name SATA --add sata --portcount 2
VBoxManage storageattach pfSense --storagectl SATA --port 0 --device 0 --type hdd \
  --medium ~/VMs/pfSense/pfSense.vdi
VBoxManage storageattach pfSense --storagectl SATA --port 1 --device 0 --type dvddrive \
  --medium ~/Músicas/netgate-installer-v1.2-RELEASE-amd64.iso

# conferir antes de ligar: SATA-0-0 = vdi, SATA-1-0 = iso
VBoxManage showvminfo pfSense --machinereadable | grep '^"SATA'

VBoxManage startvm pfSense

Se aparecer "No bootable medium found" com a ISO conectada, ative EFI (VM desligada):

VBoxManage modifyvm pfSense --firmware efi

2.2 Instalar

No instalador:

  • WAN: em0, DHCP
  • LAN: em1, 192.168.1.1/24
  • Instalar no disco de 16 GB com as opções padrão

2.3 Remover a ISO e mover a WAN para o provedor

# aguardar VMState="poweroff" antes de alterar
VBoxManage controlvm pfSense acpipowerbutton
VBoxManage showvminfo pfSense --machinereadable | grep '^VMState='

VBoxManage storageattach pfSense --storagectl SATA --port 1 --device 0 --medium emptydrive
VBoxManage modifyvm pfSense --nic1 intnet --intnet1 isp-acesso
VBoxManage startvm pfSense

Com a VM ligada, a troca também funciona a quente:

VBoxManage controlvm pfSense nic1 intnet isp-acesso

2.4 Configurar a WAN em PPPoE pelo shell

O menu de console não tem opção de PPPoE. O script abaixo grava a configuração no config.xml via PHP do próprio pfSense e reaplica a WAN. Para não digitar no console, o VirtualBox "digita" o arquivo dentro da VM com keyboardputfile.

No Fedora, crie o arquivo:

cat > ~/VMs/pfsense-pppoe.txt <<'HOST'
sh
cat > /root/wan-pppoe.php <<'EOF'
<?php
require_once("config.inc");
require_once("interfaces.inc");
require_once("filter.inc");
require_once("util.inc");

// em0 = placa física da WAN; pppoe0 = interface lógica sobre ela
$ppp = [
  "ptpid"    => "0",
  "type"     => "pppoe",
  "if"       => "pppoe0",
  "ports"    => "em0",
  "username" => "cliente01",
  "password" => base64_encode("Lab@Pppoe2026"),  // pfSense guarda a senha PPP em base64
  "provider" => "ISP-LAB",
  "mtu"      => "1480",
];
config_set_path("ppps/ppp", [$ppp]);

$wan = config_get_path("interfaces/wan", []);
$wan["if"]     = "pppoe0";
$wan["ipaddr"] = "pppoe";
// libera tráfego do gateway 100.64.0.1 (CGNAT) e desativa IPv6 na WAN
unset($wan["blockpriv"], $wan["blockbogons"], $wan["ipaddrv6"], $wan["dhcphostname"]);
config_set_path("interfaces/wan", $wan);

write_config("WAN PPPoE - lab CGNAT");
interface_configure("wan", true);
filter_configure_sync();
echo "PPPoE aplicado\n";
EOF
php /root/wan-pppoe.php
HOST

No Windows, crie o arquivo como no Apêndice A (seção "Arquivos para o keyboardputfile").

No console do pfSense, escolha a opção 8 (Shell). Depois, no host:

VBoxManage controlvm pfSense keyboardputfile ~/VMs/pfsense-pppoe.txt

Ao final deve aparecer PPPoE aplicado.

2.5 Validar

No shell do pfSense:

ifconfig pppoe0              # inet 100.64.0.x --> 100.64.0.1
netstat -rn -f inet | head   # default via 100.64.0.1
ping -c 3 1.1.1.1
tail -n 30 /var/log/ppp.log  # negociação PPPoE, LCP e IPCP

Na CHR:

/ppp active print
/ip firewall connection print where src-address~"100.64"

3. Linux-Cliente (servidor web atrás do CGNAT)

3.1 Criar a VM

Use Ubuntu Server (ajuste o caminho da ISO):

VBoxManage createvm --name Linux-Cliente --ostype Ubuntu_64 --register
VBoxManage modifyvm Linux-Cliente --cpus 1 --memory 1024 --audio-enabled off \
  --nic1 intnet --intnet1 lan-cliente --nictype1 virtio

mkdir -p ~/VMs/Linux-Cliente
VBoxManage createmedium disk --filename ~/VMs/Linux-Cliente/disk.vdi --size 12288
VBoxManage storagectl Linux-Cliente --name SATA --add sata --portcount 2
VBoxManage storageattach Linux-Cliente --storagectl SATA --port 0 --device 0 --type hdd \
  --medium ~/VMs/Linux-Cliente/disk.vdi
VBoxManage storageattach Linux-Cliente --storagectl SATA --port 1 --device 0 --type dvddrive \
  --medium ~/Downloads/ubuntu-24.04-live-server-amd64.iso

VBoxManage startvm Linux-Cliente

A instalação já sai pela internet através de pfSense e CHR, então o PPPoE precisa estar ativo antes.

3.2 IP fixo

Confira o nome da interface com ip -br link (no VirtualBox costuma ser enp0s3):

sudo mv /etc/netplan/50-cloud-init.yaml /root/50-cloud-init.yaml.bak 2>/dev/null

sudo tee /etc/netplan/01-lab.yaml >/dev/null <<'EOF'
network:
  version: 2
  ethernets:
    enp0s3:
      addresses: [192.168.1.10/24]
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses: [192.168.1.1]
EOF
sudo chmod 600 /etc/netplan/01-lab.yaml
sudo netplan apply

3.3 Servidor web

sudo apt update && sudo apt install -y nginx
echo "<h1>Servidor atrás do CGNAT</h1>" | sudo tee /var/www/html/index.html
curl -s http://127.0.0.1

4. Demonstração 1: port forward falhando

4.1 Criar o port forward no pfSense (shell)

Encaminha WAN TCP 8080 para 192.168.1.10:80. O associated-rule-id = pass cria a regra de NAT já liberando o tráfego, sem regra de filtro separada.

No Fedora:

cat > ~/VMs/pfsense-portfwd.txt <<'HOST'
cat > /root/portfwd.php <<'EOF'
<?php
require_once("config.inc");
require_once("filter.inc");
require_once("util.inc");

$rule = [
  "source"             => ["any" => ""],
  "destination"        => ["network" => "wanip", "port" => "8080"],
  "protocol"           => "tcp",
  "target"             => "192.168.1.10",
  "local-port"         => "80",
  "interface"          => "wan",
  "descr"              => "Aula CGNAT - nginx",
  "associated-rule-id" => "pass",
];
$rules = config_get_path("nat/rule", []);
$rules[] = $rule;
config_set_path("nat/rule", $rules);

write_config("Port forward 8080 -> 192.168.1.10:80");
filter_configure_sync();
echo "Port forward aplicado\n";
EOF
php /root/portfwd.php
HOST

VBoxManage controlvm pfSense keyboardputfile ~/VMs/pfsense-portfwd.txt

Conferir no pfSense:

pfctl -s nat | grep 8080

4.2 Testar "da internet"

Do Fedora (que está na rede de casa, do lado de fora do provedor), use o IP do uplink da CHR anotado no passo 1.4:

curl -m 5 http://IP_UPLINK_DA_CHR:8080
# resultado esperado: timeout

Pontos para mostrar aos alunos:

  • O pfSense tem 100.64.0.10, não o IP "público".
  • O IP público está na CHR do provedor, e o assinante não controla aquele NAT.
  • O port forward no pfSense está correto, mas o pacote nunca chega até ele.

5. Demonstração 2: só o provedor consegue abrir a porta

Na CHR, simule o provedor vendendo "porta aberta" ou IP dedicado:

/ip firewall nat add chain=dstnat in-interface=wan-internet protocol=tcp dst-port=8080 \
    action=dst-nat to-addresses=100.64.0.10 to-ports=8080 comment="porta liberada pelo provedor"

/ip firewall filter add chain=forward connection-nat-state=dstnat action=accept \
    comment="libera dst-nat" place-before=[find comment="sem entrada direta no CGNAT"]

Repita o teste do Fedora:

curl -m 5 http://IP_UPLINK_DA_CHR:8080
# agora responde

Mensagem da aula: funciona, mas depende de o provedor configurar algo para você, e no CGNAT real a porta 8080 do IP compartilhado só pode ser de um assinante.

Remova antes da próxima demonstração:

/ip firewall nat remove [find comment="porta liberada pelo provedor"]
/ip firewall filter remove [find comment="libera dst-nat"]

6. Demonstração 3: Portr

O Portr é um túnel self-hosted, alternativa open source ao ngrok, que expõe serviços HTTP, TCP e WebSocket usando SSH remote port forwarding. O cliente abre a conexão de dentro para fora, então o CGNAT não interfere.

6.1 Servidor (VPS com IP público, fora do lab)

Requisitos:

  • Domínio com registros DNS A @ e A * apontando para a VPS
  • Token de API da Cloudflare para o certificado wildcard (ou Route53, ou certificado próprio)
  • Portas abertas: 2222 (túnel SSH), 8001, 80/443 e 30001 a 40001 (túneis TCP)
mkdir -p /opt/portr && cd /opt/portr
wget https://raw.githubusercontent.com/amalshaji/portr/main/docker-compose.prod.yaml

# chave de host do servidor de túnel
ssh-keygen -t ed25519 -f /tmp/portr_host_key -N ""
cat /tmp/portr_host_key   # copiar para PORTR_SSH_HOST_KEY no .env
rm /tmp/portr_host_key /tmp/portr_host_key.pub

Arquivo .env:

PORTR_DOMAIN=tunel.seudominio.com.br
PORTR_DB_URL=postgres://postgres:SENHA_FORTE@postgres:5432/postgres
PORTR_SERVER_URL=https://tunel.seudominio.com.br
PORTR_SSH_URL=tunel.seudominio.com.br:2222
PORTR_AUTO_MIGRATE=true
PORTR_SSH_HOST_KEY=   # chave PEM gerada acima
CLOUDFLARE_API_TOKEN=
POSTGRES_USER=postgres
POSTGRES_PASSWORD=SENHA_FORTE
POSTGRES_DB=postgres
docker compose -f docker-compose.prod.yaml up -d
docker compose -f docker-compose.prod.yaml ps

O primeiro login no painel vira superusuário. Gere o token de acesso pelo painel.

6.2 Cliente (Linux-Cliente, atrás do CGNAT)

Instale o binário do cliente a partir das releases em github.com/amalshaji/portr e:

portr auth set --token TOKEN_DO_PAINEL --remote tunel.seudominio.com.br
portr http 80 --subdomain aula-cgnat
# publica https://aula-cgnat.tunel.seudominio.com.br

Para SSH:

portr tcp 22
# publica numa porta do range 30001-40001 da VPS

Confira a sintaxe com portr --help, o projeto está em beta e os comandos podem mudar entre versões.

6.3 Mostrar aos alunos

  • Remova o port forward do pfSense: o Portr continua funcionando.
  • Nenhuma regra de entrada no pfSense e nenhuma configuração na CHR.
  • Na CHR, a conexão aparece como saída comum do assinante:
/ip firewall connection print where dst-port=2222

7. Comparativo para fechar a aula

Abordagem Depende do provedor Abre porta de entrada Custo Uso indicado
Port forward no roteador Sim, precisa de IP público Sim IP fixo pago Produção com IP dedicado
Porta liberada pelo provedor Sim Sim Variável Raro em CGNAT
IPv6 Sim, provedor precisa entregar Sim (regra no firewall) Zero Quando o cliente acessa por IPv6
WireGuard até VPS + dst-nat Não Só na VPS VPS Produção
Portr Não Não, conexão de saída VPS + domínio Desenvolvimento, webhooks, demonstrações

Vantagens do Portr:

  • Funciona atrás de CGNAT, NAT duplo e IP dinâmico.
  • Sem porta de entrada no firewall do cliente, menor superfície de ataque.
  • TLS automático com certificado wildcard e subdomínio por serviço.
  • Self-hosted: o tráfego passa pela sua VPS, não por terceiro.
  • Inspetor de requisições com replay, útil para testar webhooks.

Limitação: o projeto se declara em beta e não recomenda uso junto a servidores de produção.


8. Ordem de boot no dia da aula

  1. CHR-ISP
  2. pfSense (conferir /ppp active print na CHR)
  3. Linux-Cliente
  4. Portr server na VPS já rodando

9. Solução de problemas

Sintoma Causa provável Ação
VERR_SVM_IN_USE ao ligar VM (Linux) KVM ocupando a virtualização Correção condicional do passo 0.1
Tartaruga verde na VM (Windows) VirtualBox sobre Hyper-V Funciona, só mais lento; ver passo 0.2
Texto digitado errado pelo keyboardputfile Arquivo com CRLF ou teclado do pfSense não US Gerar com LF (Apêndice A) e manter teclado US no console
Mac Apple Silicon não roda pfSense Sem imagem ARM Passo 0.3
machine is already locked VM ligada ou desligando Aguardar VMState="poweroff" ou usar controlvm
No drive attached to device slot 0 on port 1 Controlador SATA com uma porta só VBoxManage storagectl pfSense --name SATA --portcount 2
No bootable medium found ISO não conectada ou firmware Conferir grep '^"SATA' ou --firmware efi
ppp.log só mostra PADI enviado CHR fora da rede isp-acesso VBoxManage showvminfo CHR-ISP --machinereadable \| grep intnet
Falha de autenticação PPP Usuário ou senha divergentes /ppp secret print na CHR
CHR não pega IP no uplink Promiscuous mode ou Wi-Fi --nicpromisc1 allow-all e placa cabeada
pfSense conecta mas não navega Bloqueio de redes privadas na WAN Conferir que blockpriv e blockbogons saíram do config
Linux sem internet Gateway ou DNS ip route, resolvectl status, ping 192.168.1.1

Apêndice A: equivalentes no Windows (PowerShell)

Baixar a CHR (passo 1.1)

cd "$HOME\VMs"
$VER = (Invoke-RestMethod https://upgrade.mikrotik.com/routeros/NEWESTa7.stable).Split(' ')[0]
"Versão: $VER"

Invoke-WebRequest "https://download.mikrotik.com/routeros/$VER/chr-$VER.vdi.zip" -OutFile "chr-$VER.vdi.zip"
Expand-Archive "chr-$VER.vdi.zip" -DestinationPath . -Force

Criar a VM da CHR (passo 1.2)

VBoxManage createvm --name CHR-ISP --ostype Linux26_64 --register
VBoxManage modifyvm CHR-ISP --cpus 1 --memory 256 --audio-enabled off `
  --nic1 bridged --bridgeadapter1 "NOME DA SUA PLACA" --nictype1 virtio --nicpromisc1 allow-all `
  --nic2 intnet --intnet2 isp-acesso --nictype2 virtio
VBoxManage storagectl CHR-ISP --name SATA --add sata --portcount 1
VBoxManage storageattach CHR-ISP --storagectl SATA --port 0 --device 0 --type hdd `
  --medium "$HOME\VMs\chr-$VER.vdi"
VBoxManage startvm CHR-ISP

Os demais comandos VBoxManage seguem o mesmo padrão: troque \ no fim da linha por ` e caminhos como ~/VMs/pfSense/pfSense.vdi por "$HOME\VMs\pfSense\pfSense.vdi".

Arquivos para o keyboardputfile (passos 2.4 e 4.1)

O pfSense precisa receber o texto com quebras de linha Unix (LF). O bloco abaixo grava o arquivo já convertido. Cole entre @' e '@ exatamente o conteúdo que no roteiro está entre as linhas <<'HOST' e HOST (sem incluir essas duas linhas):

$script = @'
sh
cat > /root/wan-pppoe.php <<'EOF'
... conteúdo do script PHP do passo 2.4 ...
EOF
php /root/wan-pppoe.php
'@

[IO.File]::WriteAllText("$HOME\VMs\pfsense-pppoe.txt", $script.Replace("`r`n", "`n"))

VBoxManage controlvm pfSense keyboardputfile "$HOME\VMs\pfsense-pppoe.txt"

Repita o mesmo procedimento para pfsense-portfwd.txt no passo 4.1.

Testar "da internet" (passos 4.2 e 5)

curl.exe -m 5 http://IP_UPLINK_DA_CHR:8080

Use curl.exe, não curl: no PowerShell, curl é apelido de outro comando.


Apêndice B: CHR em NAT (Wi-Fi ou bridge indisponível)

Quando a bridge não funciona (comum em Wi-Fi, principalmente no macOS), a CHR pode usar NAT do VirtualBox no uplink. O lab de PPPoE e CGNAT continua igual; muda só a forma de simular o acesso "da internet", que passa a ser um port forward do VirtualBox até a CHR.

# com a CHR desligada
VBoxManage modifyvm CHR-ISP --nic1 nat --nictype1 virtio
# porta 8080 do host vai para a porta 8080 da CHR
VBoxManage modifyvm CHR-ISP --natpf1 "aula8080,tcp,,8080,,8080"
VBoxManage startvm CHR-ISP

Nos testes dos passos 4.2 e 5, troque IP_UPLINK_DA_CHR por 127.0.0.1:

curl -m 5 http://127.0.0.1:8080

O resultado é o mesmo da bridge: timeout com só o port forward no pfSense, e resposta depois que o "provedor" faz o dst-nat na CHR.