One place for hosting & domains

      Creating

      Getting Started with Software-Defined Networking and Creating a VPN with ZeroTier One


      Introdução

      Atualmente, cada vez mais os projetos de software são construídos por equipes cujos membros trabalham em conjunto em localidades geográficas diferentes. Embora este fluxo de trabalho tenha muitas vantagens claras, existem casos onde tais equipes podem querer vincular seus computadores na internet e tratá-los como se estivessem na mesma sala. Por exemplo, você pode estar testando sistemas distribuídos como o Kubernetes ou construindo um aplicativo multisserviço complexo. Às vezes, tratar as máquinas como se elas estivessem uma ao lado da outra ajuda na produtividade, já que não seria necessário arriscar expor seus serviços inacabados na internet. Este paradigma pode ser alcançado através da Rede Definida por Software (SDN), uma tecnologia relativamente nova que fornece uma estrutura de rede dinâmica cuja existência é totalmente composta por software.

      O ZeroTier One é um aplicativo de código aberto que usa alguns dos últimos desenvolvimentos em SDN para permitir que os usuários criem redes seguras, gerenciáveis e tratem dispositivos conectados como se estivessem no mesmo local físico. O ZeroTier fornece um console Web para o gerenciamento de rede e o software de terminal para os clientes. Trata-se de uma tecnologia criptografada ponto a ponto, o que significa que ao contrário de soluções tradicionais VPN, as comunicações não precisam passar por um servidor central ou roteador — as mensagens são enviadas diretamente de host para host. Como resultado, ela é muito eficiente e garante latência mínima. Outros benefícios incluem a implantação e o processo de configuração simples do ZeroTier, manutenção sem complicações, que permite o registro e gerenciamento centralizados de nós autorizados pelo Console Web.

      Ao seguir este tutorial, você irá conectar um cliente e servidor juntos em uma rede simples ponto a ponto. Como a Rede Definida por Software não utiliza o design tradicional cliente/servidor, não há servidor VPN central para instalar e configurar; isso simplifica a implantação da ferramenta e a adição de qualquer nó complementar. Assim que a conectividade for estabelecida, será possível usar a capacidade VPN do ZeroTier usando algumas funcionalidades inteligentes do Linux. Isso permite que o tráfego deixe sua rede do ZeroTier do seu servidor e instrua um cliente para enviar o tráfego naquela direção.

      Pré-requisitos

      Antes de trabalhar neste tutorial, você precisará dos seguintes recursos:

      • Um servidor executando o Ubuntu 16.04. Neste servidor, será necessário um usuário não raiz com privilégios sudo que pode ser configurado usando nosso guia de configuração inicial de servidor para o Ubuntu 16.04.

      • Uma conta com o ZeroTier One, que você pode configurar indo para o My ZeroTier. Para os fins deste tutorial, é possível usar a versão gratuita deste serviço que não tem custos ou compromissos.

      • Um computador local para se juntar ao seu SDN como um cliente. Nos exemplos neste tutorial, tanto o servidor como o computador local estão executando o Linux Ubuntu, mas qualquer sistema operacional listado na página de download do ZeroTier funcionará no cliente.

      Com esses pré-requisitos no lugar, você está pronto para configurar a rede definida por software para seu servidor e máquina local.

      Passo 1 — Criando uma rede definida por software usando o ZeroTier One

      A plataforma ZeroTier fornece o ponto central de controle para sua rede definida por software. Lá, você pode autorizar e desautorizar clientes, escolher um esquema de endereço e criar um ID de rede ao qual você pode dirigir seus clientes ao configurá-los.

      Logue na sua conta do ZeroTier, clique em Networks no topo da tela e, em seguida, clique em Create. Um nome de rede gerado automaticamente aparecerá. Clique nele para ver a tela de configuração da sua rede. Anote o Network ID mostrado em amarelo, já que você precisará usá-lo mais tarde.

      Se preferir alterar o nome da rede para algo mais descritivo, edite o nome no lado esquerdo da tela; você também pode adicionar uma descrição, caso queira. Qualquer alteração que você fizer será salva e aplicada automaticamente.

      Em seguida, escolha em qual intervalo de endereços IPv4 o SDN irá operar. No lado direito da tela, na área intitulada IPv4 Auto-Assign, selecione um intervalo de endereços em que seus nós irão se enquadrar. Para os fins deste tutorial, qualquer intervalo pode ser usado, mas é importante deixar a caixa Auto-Assign from Range marcada.

      Certifique-se de que o Access Control à esquerda permaneça definido como Certificate (Private Network). Isso garante que apenas máquinas aprovadas possam se conectar à sua rede, e não qualquer uma que saiba seu ID de rede!

      Assim que terminar, suas configurações devem ser semelhantes a estas:

      ZeroTier settings configuration

      Neste ponto, você instalou a fundação de uma Rede Definida por Software do ZeroTier com sucesso. Em seguida, você instalará o software ZeroTier no seu servidor e máquinas de cliente para permitir que eles se conectem ao seu SDN.

      Passo 2 — Instalando o cliente ZeroTier One no seu servidor e computador local

      Como o ZeroTier One é um software relativamente novo, ele ainda não foi incluído nos repositórios de software principais do Ubuntu. Por esse motivo, o ZeroTier fornece um script de instalação que vamos usar para instalar o software. Este comando é um script assinado por GPG, o que significa que o código que você baixa será verificado como publicado pelo ZeroTier. Este script tem quatro partes principais, e esta é uma explicação detalhada de cada uma delas:

      • curl -s 'https://pgp.mit.edu/pks/lookup?op=get&search=0x1657198823E52A61' – importa a chave pública do ZeroTier do MIT.
      • gpg --import – esta seção do comando adiciona a chave pública do ZeroTier ao seu conjunto de chaves local de autoridades para confiar em pacotes que tentar instalar. A próxima parte do comando será executada apenas se a importação GPG for concluída com sucesso.
      • if z=$(curl -s 'https://install.zerotier.com/' | gpg); then echo "$z" – existem algumas coisas acontecendo nesta seção, mas é essencialmente: “Se o script de instalação assinado criptografadamente baixado do ZeroTier.com passar pelo GPG e não for rejeitado como não assinado pelo ZeroTier, colar essa informação na tela.”
      • sudo bash; fi – esta seção recebe o script de instalação recentemente validado e executa-o antes de finalizar a rotina.

      Aviso: você nunca deve baixar algo da internet e canalizar em outro programa a menos que tenha certeza de que ele vem de uma fonte confiável. Se quiser, você pode inspecionar o software do ZeroTier revisando o código fonte na página oficial do projeto no GitHub.

      Use um Console SSH para se conectar ao seu servidor recém-criado e execute o seguinte comando como seu usuário normal (uma explicação do comando é fornecida abaixo). Certifique-se de que você não execute ele como raiz, já que o script solicita automaticamente sua senha para aumentar seu nível de privilégios e lembre-se de manter o console do ZeroTier aberto no seu navegador para que possa interagir com ele quando necessário.

      • curl -s 'https://pgp.mit.edu/pks/lookup?op=get&search=0x1657198823E52A61' | gpg --import && if z=$(curl -s 'https://install.zerotier.com/' | gpg); then echo "$z" | sudo bash; fi

      Assim que o script terminar, você verá duas linhas de resultado semelhantes às mostradas abaixo. Anote seu endereço ZeroTier (sem os colchetes) e o nome do sistema que gerou aquele endereço, ambos os quais serão necessários mais tarde:

      Output

      *** Waiting for identity generation... *** Success! You are ZeroTier address [ 916af8664d ].

      Repita este passo no seu computador local se usar o Ubuntu, ou siga os passos relevantes para seu sistema operacional na página de download do site do ZeroTier. Novamente, certifique-se de anotar o endereço do ZeroTier e a máquina que gerou aquele endereço. Você precisará dessas informações no próximo passo deste tutorial quando você ingressar seu servidor e cliente à rede.

      Passo 3 — Ingressando sua rede ZeroTier

      Agora que tanto o servidor como o cliente têm o software do ZeroTier em execução neles, você está pronto para conectá-los à rede que você criou no Web console do ZeroTier.

      Use o comando a seguir para instruir seu cliente a solicitar acesso à rede do ZeroTier através da sua plataforma. O pedido inicial do cliente será rejeitado e suspenso, mas vamos corrigir isso em breve. Certifique-se de substituir o NetworkID pelo ID da rede que você anotou mais cedo da janela de configuração da sua rede.

      • sudo zerotier-cli join NetworkID

      Output

      200 join OK

      Você receberá uma mensagem 200 join OK, confirmando que o serviço do ZeroTier no seu servidor entendeu o comando. Caso contrário, verifique novamente o ID da rede do ZeroTier que você digitou.

      Como não criou uma rede pública que qualquer um no mundo possa ingressar, você precisa agora autorizar seus clientes. Vá até o Console Web do ZeroTier e role para baixo até o fim, onde a seção Members está. Você deve ver duas entradas marcadas como Online, com os mesmos endereços que você mais cedo.

      Na primeira coluna marcada Auth?, marque as caixas para autorizá-los a participar da rede. O Controlador do Zerotier irá atribuir um endereço IP ao servidor e ao cliente do intervalo que você escolheu mais cedo na próxima vez que eles chamarem o SDN.

      A atribuição de endereços IP pode levar tempo. Enquanto espera, você pode fornecer um Short Name e Description para seus nós na seção Members.

      Com isso, você terá conectado dois sistemas à sua rede definida por software.

      Até agora, você ganhou uma familiarização básica com o painel de controle do ZeroTier, usou a interface da linha de comando para baixar e instalar o ZeroTier e, em seguida, anexou tanto o servidor quanto o cliente a essa rede. Em seguida, você irá verificar se tudo foi aplicado corretamente executando um teste de conectividade.

      Passo 4 — Verificando a conectividade

      Nesta fase, é importante validar que os dois hosts possam realmente conversar entre si. Há uma chance de que mesmo que os hosts aleguem ter ingressado na rede, eles não consigam se comunicar. Ao verificar a conectividade neste momento, você não terá que se preocupar com problemas básicos de interconectividade que poderiam causar problemas mais tarde.

      Uma maneira fácil de encontrar o endereço IP do ZeroTier de cada host é olhando na seção Members do Console Web do ZeroTier. Você pode precisar recarregá-lo após autorizar o servidor e cliente antes que seus endereços IP apareçam. De forma alternativa, você pode usar a linha de comando do Linux para encontrar esses endereços. Use o comando a seguir em ambas as máquinas — o primeiro endereço IP mostrado na lista é aquele a ser usado. No exemplo mostrado abaixo, esse endereço é 203.0.113.0.

      • ip addr sh zt0 | grep 'inet'

      Output

      inet 203.0.113.0/24 brd 203.0.255.255 scope global zt0 inet6 fc63:b4a9:3507:6649:9d52::1/40 scope global inet6 fe80::28e4:7eff:fe38:8318/64 scope link

      Para testar a conectividade entre os hosts, execute o comando ping de um host seguido do endereço IP do outro. Por exemplo, no cliente:

      E no servidor:

      Se as respostas estiverem sendo devolvidas do host oposto (como mostrado no resultado abaixo), então os dois nós estão se comunicando com sucesso pelo SDN.

      Output

      PING 203.0.113.0 (203.0.113.0) 56(84) bytes of data. 64 bytes from 203.0.113.0: icmp_seq=1 ttl=64 time=0.054 ms 64 bytes from 203.0.113.0: icmp_seq=2 ttl=64 time=0.046 ms 64 bytes from 203.0.113.0: icmp_seq=3 ttl=64 time=0.043 ms

      Você pode adicionar quantas máquinas quiser nesta configuração repetindo a instalação do ZeroTier e os processos de ingressão evidenciados acima. Lembre-se, essas máquinas não precisam de forma alguma estar próximas.

      Agora que você confirmou que seu servidor e cliente podem se comunicar entre si, continue lendo para aprender como ajustar a rede para fornecer um gateway de saída e construir seu próprio VPN.

      Passo 5 — Habilitando a capacidade VPN do ZeroTier

      Como mencionado na introdução, é possível usar o ZeroTier como uma ferramenta VPN. Se você não planeja usar o usuário ZeroTier como uma solução VPN, não será necessário que siga esse passo, podendo pular para o Passo 6.

      Ao usar um VPN, a fonte de suas comunicações com websites na internet fica escondida. Ele permite que você ignore os filtros e restrições que podem existir na rede que está usando. Para a internet como um todo, irá parecer que você está navegando a partir do endereço IP público do seu servidor. Para usar o ZeroTier como uma ferramenta VPN, será necessário fazer mais algumas alterações nas configurações do seu servidor e cliente.

      Como habilitar a Network Address Translation e o encaminhamento de IP

      O Network Address Translation, mais comumente conhecido como “NAT”, é um método pelo qual um roteador aceita pacotes em uma interface marcada com o endereço IP do remetente e, em seguida, troca aquele endereço pelo do roteador. Um registro desta troca é mantido na memória do roteador para que quando o tráfego de retorno volte na direção oposta, o roteador possa traduzir o IP de volta para seu endereço original. O NAT é normalmente usado para permitir que vários computadores operem por trás de um endereço IP exposto publicamente, o que acaba sendo útil para um serviço VPN. Um exemplo do NAT na prática é o roteador doméstico que seu provedor de serviço de internet deu a você para conectar todos os dispositivos em seu lar à Internet. Seu notebook, telefone, tablets e qualquer outro dispositivo que consiga se conectar à Internet. Todos parecem compartilhar o mesmo endereço IP público para a Internet, porque seu roteador está executando o NAT.

      Embora o NAT seja normalmente conduzido por um roteador, ele também pode ser executado por um servidor. Ao longo deste passo, você irá potencializar essa funcionalidade no seu servidor do ZeroTier para habilitar suas capacidades VPN.

      O encaminhamento de IP é uma função realizada por um roteador ou servidor na qual ele encaminha o tráfego de uma interface para outra como se esses endereços IP estivessem em diferentes zonas. Se um roteador estiver conectado a duas redes, o encaminhamento de IP permite que ele encaminhe o tráfego entre elas. Isso pode parecer simples, mas implementar isso com sucesso pode ser surpreendentemente complicado. No entanto, no caso deste tutorial, é apenas uma questão de editar alguns arquivos de configuração.

      Ao habilitar o encaminhamento de IP, o tráfego VPN do seu cliente na rede do ZeroTier chegará na interface do ZeroTier do servidor. Sem essas configurações, o Kernel Linux irá (por padrão) jogar fora qualquer pacote que não seja destinado para a interface em que eles chegam. Esse comportamento é normal para o Kernel Linux,já que normalmente qualquer pacote que esteja chegando em uma interface que tenha um endereço de destino para outra rede pode ter como causa um erro de configuração do roteamento em outro lugar na rede.

      É útil encaminhar o IP de forma a informar o Kernel Linux de que é aceitável encaminhar pacotes entre as interfaces. A configuração padrão é 0 — equivalente a “Desligado”. Você irá alterar isso para 1 — equivalente a “Ligado”.

      Para ver a configuração atual, execute o seguinte comando:

      • sudo sysctl net.ipv4.ip_forward

      Output

      net.ipv4.ip_forward = 0

      Para habilitar o encaminhamento de IP, modifique o arquivo /etc/sysctl.conf no seu servidor e adicione a linha necessária. Este arquivo de configuração permite que um administrador substitua as configurações padrão do kernel, e ele sempre será aplicado após as reinicializações, então não será necessário se preocupar em configurá-lo novamente. Use o nano ou seu editor de texto favorito para adicionar a seguinte linha ao final do arquivo.

      • sudo nano /etc/sysctl.conf

      /etc/sysctl.conf

      . . .
      net.ipv4.ip_forward = 1
      

      Salve, feche o arquivo e depois execute o próximo comando para fazer com que o kernel adote suas novas configurações.

      O servidor adotará quaisquer novas diretrizes de configuração dentro do arquivo e irá aplicá-las imediatamente, sem a necessidade de uma reinicialização. Execute o mesmo comando que você fez anteriormente e verá que o encaminhamento de IP está ativo.

      • sudo sysctl net.ipv4.ip_forward

      Output

      net.ipv4.ip_forward = 1

      Agora que o encaminhamento de IP está ativo, faça bom uso dele fornecendo algumas regras básicas de roteamento do servidor. Como o Kernel Linux já tem uma capacidade de roteamento de rede incorporada dentro dele, tudo o que terá que fazer é adicionar algumas regras para informar o firewall integrado e o roteador que o novo tráfego que estarão vendo é aceitável e para onde enviá-lo.

      Para adicionar essas regras da linha de comando, será necessário primeiro conhecer os nomes que o Ubuntu atribuiu tanto para sua interface Zerotier como sua interface ethernet comum de acesso a Internet. Normalmente, eles são zt0 e eth0 respectivamente, embora nem sempre esse seja o caso.

      Para encontrar os nomes dessas interfaces, utilize o comando ip link show. Este utilitário de linha de comando faz parte do iproute2, uma coleção de utilitários de userspace que vem instalada no Ubuntu por padrão:

      No resultado deste comando, os nomes das interfaces estão diretamente ao lado dos números que identificam uma interface única na lista. Estes nomes de interface estão destacados no seguinte exemplo de resultado. Se os seus nomes diferem dos nomes mostrados no exemplo, substitua seu nome de interface apropriadamente durante este guia.

      Output

      1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 link/ether 72:2d:7e:6f:5e:08 brd ff:ff:ff:ff:ff:ff 3: zt0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 2800 qdisc pfifo_fast state UNKNOWN mode DEFAULT group default qlen 1000 link/ether be:82:8f:f3:b4:cd brd ff:ff:ff:ff:ff:ff

      Com essas informações em mãos, utilize o iptables para habilitar o Network-Address-Translation e o mascaramento de IP:

      • sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

      Permita o encaminhamento de tráfego e rastreie conexões ativas:

      • sudo iptables -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

      Em seguida, autorize o encaminhamento de tráfego do zt0 para o eth0. Uma regra reversa não é necessária, já que neste tutorial supõe-se que o cliente sempre faça chamadas pelo servidor, e não o contrário:

      • sudo iptables -A FORWARD -i zt0 -o eth0 -j ACCEPT

      É importante lembrar que as regras do iptables que você definiu para o servidor não persistem automaticamente entre as reinicializações. Você precisará salvar essas regras para garantir que elas sejam trazidas de volta, caso o servidor seja alguma vez reinicializado. No seu servidor, execute os comandos abaixo, seguindo as breves instruções na tela para salvar as regras atuais do IPv4. O mesmo não é necessário para o IPv6.

      • sudo apt-get install iptables-persistent
      • sudo netfilter-persistent save

      Após executar o sudo netfilter-persistent save, pode ser vantajoso reiniciar seu servidor para validar o salvamento correto das regras do iptables. Uma maneira fácil de verificar é executando o sudo iptables-save, que irá transferir as configurações atuais carregadas em memória para o seu terminal. Se você ver regras semelhantes às que estão abaixo em relação ao mascaramento, encaminhamento e a interface zt0, então elas foram salvas corretamente.

      Output

      # Generated by iptables-save v1.6.0 on Tue Apr 17 21:43:08 2018 . . . -A POSTROUTING -o eth0 -j MASQUERADE COMMIT . . . -A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT -A FORWARD -i zt0 -o eth0 -j ACCEPT COMMIT . . .

      Agora que essas regras foram aplicadas no seu servidor, ele está pronto para manipular o tráfego entre a rede do ZeroTier e a internet pública. No entanto, o VPN não funcionará a menos que a rede do ZeroTier seja informada de que o servidor está pronto para ser usado como um gateway.

      Como habilitar seu servidor para gerenciar a rota global

      Para que seu servidor possa processar o tráfego de qualquer cliente, é preciso garantir que outros clientes na rede do ZeroTier saibam e enviem seus tráfegos à ele. Pode-se fazer isso configurando uma rota global no Console do ZeroTier. As pessoas que estão familiares com redes de computadores também podem descrever isso como uma Default Route. É onde qualquer cliente envia seu tráfego padrão, ou seja, qualquer tráfego que não deva ir para qualquer outro local específico.

      Vá até o lado direito superior da sua página da rede do ZeroTier e adicione uma nova rota com os seguintes parâmetros. Você pode encontrar o IP do ZeroTier para seu servidor na seção Members da sua página de configuração da rede do ZeroTier. No campo network/bits, digite 0.0.0.0/0, no campo (LAN), digite o endereço IP do seu servidor ZeroTier.

      Quando os detalhes estiverem no lugar, clique no símbolo “+” e você verá uma nova regra aparecer abaixo da existente. Haverá um globo laranja nela para informar que ela é, de fato, uma rota global:

      Global Route Rule

      Com sua rede do ZeroTier pronta para funcionar, há apenas uma configuração a ser feita para que o VPN funcione: a dos clientes.

      Configurando clientes Linux

      Nota: os comandos nesta seção são aplicáveis apenas aos clientes Linux. As instruções para configurar os clientes Windows ou macOS são fornecidas na próxima seção.

      Se seu cliente estiver executando o Linux, será necessário fazer uma alteração manual no seu arquivo /etc/sysctl.conf. Esta mudança de configuração é necessária para alterar a visualização do kernel sobre o qual é um caminho de volta aceitável para o tráfego do seu cliente. Devido ao modo como o VPN do ZeroTier é configurado, o tráfego que volta do seu servidor para seu cliente pode, por vezes, parecer vir de um endereço de rede diferente daquele para o qual foi enviado. Por padrão, o Kernel Linux as vê como inválidas e as elimina, tornando necessário sobrepor esse comportamento.

      Abra o /etc/sysctl.conf na sua máquina do cliente:

      • sudo nano /etc/sysctl.conf

      Então, adicione a seguinte linha:

      Output

      . . . net.ipv4.conf.all.rp_filter=2

      Salve, feche o arquivo e depois execute o sudo sysctl -p para adotar as alterações.

      Em seguida, diga ao software de cliente do ZeroTier que sua rede tem permissão para transportar o tráfego da rota padrão. Isso altera o roteamento do cliente e, portanto, é considerado uma função privilegiada, razão pela qual deve ser habilitada manualmente. O comando imprimirá uma estrutura de configuração na saída. Verifique-a para confirmar que mostra o allowDefault=1 no topo:

      • sudo zerotier-cli set NetworkID allowDefault=1

      Se em qualquer momento você quiser parar de usar o ZeroTier como um VPN com todo seu roteamento de tráfego passando por ele, defina allowDefault de volta para 0:

      • sudo zerotier-cli set NetworkID allowDefault=0

      Cada vez que o serviço do ZeroTier no cliente for reiniciado, o valor allowDefault=1 retorna para 0, portanto lembre-se de executá-lo novamente para ativar a funcionalidade VPN.

      Por padrão, o serviço do ZeroTier é definido para iniciar automaticamente na inicialização para tanto o cliente Linux quanto o servidor. Se você não quiser que isso aconteça, desative a rotina de inicialização com o seguinte comando.

      • sudo systemctl disable zerotier-one

      Se quiser usar outros sistemas operacionais na sua rede do ZeroTier, continue lendo na próxima seção. Caso contrário, pule para a seção Como gerenciar fluxos.

      Como configurar clientes não Linux

      O software de cliente do ZeroTier está disponível para muitos sistemas e não apenas para o SO do Linux — até os smartphones são suportados. Os clientes existem para sistemas Windows, macOS, Android, iOS e até mesmo sistemas operacionais especializados como o QNAP, Synology e sistemas NAS da WesternDigital.

      Para ingressar clientes baseados em macOS e Windows na rede, inicie a ferramenta do ZeroTier (que você instalou no Passo 1) e digite sua NetworkID no campo fornecido antes de clicar em Join. Lembre-se de voltar no console do ZeroTier para marcar o botão Allow para autorizar um novo host na sua rede.

      Certifique-se de marcar a caixa rotulada Route all traffic through ZeroTier. Se não o fizer, seu cliente estará anexado à sua rede do ZeroTier, mas não tentará enviar seu tráfego de internet através dela.

      Use uma ferramenta de verificação de IP como a ICanHazIP para verificar se seu tráfego está aparecendo na internet a partir do IP do seu servidor. Para verificar isso, cole o seguinte URL na barra de endereço do seu navegador. Este site mostrará o endereço IP que seu servidor (e o resto da Internet) vê você usando para acessar o site:

      http://icanhazip.com
      

      Com esses passos concluídos, você pode começar a utilizar seu VPN como quiser. A próxima seção opcional aborda uma tecnologia integrada ao SDN do ZeroTier conhecida como “regras de fluxo”, mas não são de forma alguma necessárias para que a funcionalidade VPN funcione.

      Passo 6 — Gerenciando fluxos (Opcional)

      Um dos benefícios de uma rede definida por software é o controle centralizado. No que diz respeito ao ZeroTier, o controle centralizado é a interface do usuário Web que fica no topo do serviço geral SDN do ZeroTier. A partir dessa interface, é possível escrever regras conhecidas como flow rules, que especificam o tráfego que uma rede pode ou não pode fazer. Por exemplo, você pode especificar uma proibição geral em certas portas de rede que transportam tráfego na rede, limitar os hosts que podem conversar uns com os outros e até mesmo redirecionar o tráfego.

      Esta é uma capacidade extremamente poderosa que entra em vigor quase instantaneamente, já que quaisquer alterações feitas na tabela de fluxo são enviadas aos membros da rede e produzem efeitos após alguns instantes. Para editar as regras de fluxo, volte para a Interface do usuário Web do ZeroTier, clique na aba Networking e role para baixo até ver uma caixa chamada Flow Rules (pode estar recolhida e precisar ser expandida) Isso abre um campo de texto onde você pode digitar as regras que quiser. Um manual completo está disponível dentro do console do ZeroTier em uma caixa abaixo da caixa de entrada Flow Rules, intitulado Rules Engine Help.

      Aqui estão algumas regras para servir de exemplo para ajudar você a explorar essa funcionalidade.

      Para bloquear qualquer tráfego que seja ligado ao servidor DNS do Google 8.8.8.8, adicione essa regra:

      drop
          ipdest 8.8.8.8/32
      ;
      

      Para redirecionar qualquer tráfego que seja ligado ao servidor DNS público do Google para um dos seus nós do ZeroTier, adicione a seguinte regra. Isso pode ser um excelente ponto de captura para substituir pesquisas de DNS:

      redirect NetworkID
          ipdest 8.8.8.8/32
      ;
      

      Se sua rede tiver requisitos especiais de segurança, você pode remover quaisquer atividades nas portas FTP, Telnet e HTTP não criptografada, adicionando essa regra:

      drop
          dport 80,23,21,20
      ;
      

      Quando terminar de adicionar as regras de fluxo, clique no botão Save Changes e o ZeroTier irá gravar suas alterações.

      Conclusão

      Neste tutorial, você deu um primeiro passo no mundo das Redes Definidas por Software. Trabalhar com o ZeroTier fornece alguns insights sobre os benefícios dessa tecnologia. Se seguiu o exemplo VPN, e embora a configuração inicial possa contrastar de outras ferramentas que você possa ter usado no passado, a facilidade em adicionar clientes adicionais pode ser uma razão convincente para usar a tecnologia em outro lugar.

      Para resumir, você aprendeu como usar o ZeroTier como um provedor SDN, além de configurar e anexar nós a essa rede. O elemento VPN fará você compreender mais profundamente como o roteamento dentro de uma rede dessas funciona e ambos os caminhos neste tutorial permitirão que utilize a poderosa tecnologias das regras de fluxo.

      Agora que uma rede ponto a ponto existe, é possível combiná-la com outra funcionalidade, como o Compartilhamento de arquivos. Se você tiver um NAS ou servidor de arquivos em casa, você pode vinculá-lo ao ZeroTier e acessá-lo fora de casa. Se quiser compartilhá-lo com seus amigos, mostre a eles como ingressar na sua rede do ZeroTier. Os funcionários que estão distribuídos em uma grande área poderiam até mesmo vincular-se ao mesmo espaço de armazenamento central. Para começar a construir o compartilhamento de arquivos para qualquer um desses exemplos, consulte Como configurar uma Samba Share para uma organização pequena no Ubuntu 16.04.



      Source link

      Creating, Reading and Writing Files in Go – A Tutorial


      Updated by Linode Contributed by Mihalis Tsoukalos

      Introduction

      This guide provides examples related to performing common file input and output operations in Go.

      Note

      This guide is written for a non-root user. However, some commands might require the help of sudo in order to properly execute. If you are not familiar with the sudo command, see the Users and Groups guide.

      In This Guide

      In this guide guide you will learn how to:

      Before You Begin

      • To follow this guide you need to have Go installed on your computer and access to your preferred text editor.

      • For the purposes of this guide, a text file named data.txt with the following contents will be used:

        cat /tmp/data.txt
        
          
        1 2
        One Two
        
        Three
            
        

      Checking if a Path Exists

      In order to read a file, you will need to open it first. In order to be able to open a file, it must exist at the given path and be an actual file, not a directory. The code of this section will check if the given path exists.

      ./doesItExist.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      
      package main
      
      import (
          "fmt"
          "os"
      )
      
      func main() {
          arguments := os.Args
          if len(arguments) == 1 {
              fmt.Println("Please give one argument.")
              return
          }
          path := arguments[1]
      
          _, err := os.Stat(path)
          if err != nil {
              fmt.Println("Path does not exist!", err)
          }
      }

      All the work here is done by the powerful os.Stat() function. If the call to os.Stat() is successful, then the error value will be nil, which confirms that the given path exists. Notice that if the given path exists, the program generates no output according to the UNIX philosophy.

      Executing doesItExist.go will resemble the following output:

      go run doesItExist.go /bin/What
      
        
      Path does not exist! stat /bin/What: no such file or directory
      
      

      Note

      The fact that a path does exist does not necessarily mean that it is a regular file or a directory. There exist additional tests and functions that will help you determine the kind of file you are dealing with.

      Checking if a Path is a Regular File

      There exist a special function in the Go standard library, IsRegular(), that checks whether a path belongs to a file or not. This function is illustrated in the below example.

      ./isFile.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
      package main
      
      import (
          "fmt"
          "os"
      )
      
      func main() {
          arguments := os.Args
          if len(arguments) == 1 {
              fmt.Println("Please give one argument.")
              return
          }
          path := arguments[1]
      
          fileInfo, err := os.Stat(path)
          if err != nil {
              fmt.Println("Path does not exist!", err)
          }
      
          mode := fileInfo.Mode()
          if mode.IsRegular() {
              fmt.Println(path, "is a regular file!")
          }
      }

      After getting information about the mode of the file using Mode(), you need to call the IsRegular() function to determine whether the given path belongs to a regular file or not. If the path is a regular file, the output of IsRegular() will give you this information.

      Executing isFile.go will resemble the following output:

      go run isFile.go /bin/ls
      
        
      /bin/ls is a regular file!
      
      

      Note

      Most of the examples in this guide will not test whether the file that is going to be read exists in order to minimize the amount of code. The os.Open() function does some of this work, but in a less elegant way. However, on production code all necessary tests should be performed in order to avoid crashes and bugs in your software.

      Reading Files in Go

      Reading files in Go is a simple task. Go treats both text and binary files the same, and it is up to you to interpret the contents of a file. One of the many ways to read a file, ReadFull(), is presented in the readFile.go file below.

      ./readFile.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      
      package main
      
      import (
          "fmt"
          "io"
          "os"
      )
      
      func main() {
          if len(os.Args) != 2 {
              fmt.Println("Please provide a filename")
              return
          }
      
          filename := os.Args[1]
          f, err := os.Open(filename)
          if err != nil {
              fmt.Printf("error opening %s: %s", filename, err)
              return
          }
          defer f.Close()
      
          buf := make([]byte, 8)
          if _, err := io.ReadFull(f, buf); err != nil {
              if err == io.EOF {
                  err = io.ErrUnexpectedEOF
              }
          }
          io.WriteString(os.Stdout, string(buf))
          fmt.Println()
      }

      The io.ReadFull() function reads from the reader of an open file and puts the data into a byte slice with 8 places. The io.WriteString() function is used for sending data to standard output (os.Stdout), which is also a file as far as UNIX is concerned. The read operation is executed only once. If you want to read an entire file, you will need to use a for loop, which is illustrated in other examples of this guide.

      Executing readFile.go will generate the following output:

      go run readFile.go /tmp/data.txt
      
        
      1 2
      One
      
      

      Reading a file line by line

      The following code shows how you can read a text file in Go line by line.

      ./lByL.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      
      package main
      
      import (
          "bufio"
          "flag"
          "fmt"
          "io"
          "os"
      )
      
      func lineByLine(file string) error {
          var err error
          fd, err := os.Open(file)
          if err != nil {
              return err
          }
          defer fd.Close()
      
          reader := bufio.NewReader(fd)
          for {
              line, err := reader.ReadString('n')
              if err == io.EOF {
                  break
              } else if err != nil {
                  fmt.Printf("error reading file %s", err)
                  break
              }
              fmt.Print(line)
          }
          return nil
      }
      
      func main() {
          flag.Parse()
          if len(flag.Args()) == 0 {
              fmt.Printf("usage: lByL <file1> [<file2> ...]n")
              return
          }
      
          for _, file := range flag.Args() {
              err := lineByLine(file)
              if err != nil {
                  fmt.Println(err)
              }
          }
      }

      The core functionality of the program can be found in the lineByLine() function. After ensuring the filename can be opened, the function create a new reader using bufio.NewReader(). Then, the function uses that reader with bufio.ReadString() in order to read the input file line by line. This is accomplished by passing the newline character parameter to bufio.ReadString(). bufio.ReadString() will continue to read the file until that character is found. Constantly calling bufio.ReadString() when that parameter is the newline character results in reading the input file line by line. The for loop in the main() function exists to help to process multiple command line arguments.

      Executing lByL.go will generate the following kind of output:

      go run lByL.go /tmp/data.txt
      
        
      1 2
      One Two
      
      Three
      
      

      Reading a Text File Word by Word

      The following code shows how you can read a text file word by word.

      ./wByW.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      
      package main
      
      import (
          "bufio"
          "flag"
          "fmt"
          "io"
          "os"
          "regexp"
      )
      
      func wordByWord(file string) error {
          var err error
          fd, err := os.Open(file)
          if err != nil {
              return err
          }
          defer fd.Close()
      
          reader := bufio.NewReader(fd)
          for {
              line, err := reader.ReadString('n')
              if err == io.EOF {
                  break
              } else if err != nil {
                  fmt.Printf("error reading file %s", err)
                  return err
              }
      
              r := regexp.MustCompile("[^\s]+")
              words := r.FindAllString(line, -1)
              for i := 0; i < len(words); i++ {
                  fmt.Println(words[i])
              }
          }
          return nil
      }
      
      func main() {
          flag.Parse()
          if len(flag.Args()) == 0 {
              fmt.Printf("usage: wByW <file1> [<file2> ...]n")
              return
          }
      
          for _, file := range flag.Args() {
              err := wordByWord(file)
              if err != nil {
                  fmt.Println(err)
              }
          }
      }

      The core functionality of the program can be found in the wordByWord() function. Initially the text file is read line by line. Then a regular expression, which is stored in the r variable, is used for determining the words in the current line. Those words are stored in the words variable. After that, a for loop is used for iterating over the contents of words and print them on the screen before continuing with the next line of the input file.

      Executing wByW.go will generate the following kind of output:

      go run wByW.go /tmp/data.txt
      
        
      1
      2
      One
      Two
      Three
      
      

      Reading a file character by character

      The following code shows how you can read a text file character by character.

      ./cByC.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      
      package main
      
      import (
          "bufio"
          "flag"
          "fmt"
          "io"
          "os"
      )
      
      func charByChar(file string) error {
          var err error
          fd, err := os.Open(file)
          if err != nil {
              return err
          }
          defer fd.Close()
      
          reader := bufio.NewReader(fd)
          for {
              line, err := reader.ReadString('n')
              if err == io.EOF {
                  break
              } else if err != nil {
                  fmt.Printf("error reading file %s", err)
                  return err
              }
      
              for _, x := range line {
                  fmt.Println(string(x))
              }
          }
          return nil
      }
      
      func main() {
          flag.Parse()
          if len(flag.Args()) == 0 {
              fmt.Printf("usage: cByC <file1> [<file2> ...]n")
              return
          }
      
          for _, file := range flag.Args() {
              err := charByChar(file)
              if err != nil {
                  fmt.Println(err)
              }
          }
      }

      The charByChar() function does all the work. Once again, the input file is ready line by line. Within a for loop, range iterates over the characters of each line.

      Executing cByC.go will generate the following kind of output:

      go run cByC.go /tmp/data.txt
      
        
      1
      
      2
      
      
      O
      n
      e
      
      T
      w
      o
      
      
      
      
      T
      h
      r
      e
      e
      
      
      
      

      Other Examples

      Checking if a Path is a Directory

      In this section you will learn how to differentiate between directories and the other types of UNIX files.

      ./isDirectory.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
      package main
      
      import (
          "fmt"
          "os"
      )
      
      func main() {
          arguments := os.Args
          if len(arguments) == 1 {
              fmt.Println("Please give one argument.")
              return
          }
          path := arguments[1]
      
          fileInfo, err := os.Stat(path)
          if err != nil {
              fmt.Println("Path does not exist!", err)
          }
      
          mode := fileInfo.Mode()
          if mode.IsDir() {
              fmt.Println(path, "is a directory!")
          }
      }

      All the work is done by the IsDir() function. If it is a directory, then it will return true.

      Executing isDirectory.go will generate the following kind of output:

      go run isDirectory.go /tmp
      
        
      /tmp is a directory!
      
      

      Creating a New File

      In this section you will learn how to create a new file in Go.

      ./createFile.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      
      package main
      
      import (
          "fmt"
          "os"
      )
      
      func main() {
          if len(os.Args) != 2 {
              fmt.Println("Please provide a filename")
              return
          }
          filename := os.Args[1]
          var _, err = os.Stat(filename)
      
          if os.IsNotExist(err) {
              file, err := os.Create(filename)
              if err != nil {
                  fmt.Println(err)
                  return
              }
              defer file.Close()
          } else {
              fmt.Println("File already exists!", filename)
              return
          }
      
          fmt.Println("File created successfully", filename)
      }

      It is really important to make sure that the file you are going to create does not already exist, otherwise you might overwrite an existing file and therefore lose its data. os.Create() will truncate the destination file if it already exists. The IsNotExist() function returns true if a file or directory does not exist. This is indicated by the contents of the error variable that is passed as an argument to IsNotExist(). The error variable was returned by a previous call to os.Stat().

      Executing createFile.go will generate the following output:

      go run createFile.go /tmp/newFile.txt
      
        
      File created successfully /tmp/newFile.txt
      
      

      Writing Data to a File

      In this section you will learn how to write data to a new file using fmt.Fprintf().

      ./writeFile.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      
      package main
      
      import (
          "fmt"
          "os"
      )
      
      func main() {
          if len(os.Args) != 2 {
              fmt.Println("Please provide a filename")
              return
          }
      
          filename := os.Args[1]
          destination, err := os.Create(filename)
          if err != nil {
              fmt.Println("os.Create:", err)
              return
          }
          defer destination.Close()
      
          fmt.Fprintf(destination, "[%s]: ", filename)
          fmt.Fprintf(destination, "Using fmt.Fprintf in %sn", filename)
      }

      The use of the fmt.Fprintf() function for writing allows us to write formatted text to files in a way that is similar to the way the fmt.Printf() function works. Notice that fmt.Fprintf() can write to any io.Writer interface. Once again, remember that os.Create() will truncate the destination file if it already exists.

      A successful execution of writeFile.go will generate no output – in this case the executed command will be go run writeFile.go /tmp/aNewFile. However, it would be interesting to see the contents of /tmp/aNewFile.

      cat /tmp/aNewFile
      
        
      [/tmp/aNewFile]: Using fmt.Fprintf in /tmp/aNewFile
      
      

      Appending Data to a File

      You will now learn how to append data to a file, which means adding data to the end of the file without deleting existing data.

      ./append.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
      package main
      
      import (
          "fmt"
          "os"
          "path/filepath"
      )
      
      func main() {
          arguments := os.Args
          if len(arguments) != 3 {
              fmt.Printf("usage: %s message filenamen", filepath.Base(arguments[0]))
              return
          }
          message := arguments[1]
          filename := arguments[2]
      
          file, err := os.OpenFile(filename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660)
          if err != nil {
              fmt.Println(err)
              return
          }
          defer file.Close()
          fmt.Fprintf(file, "%sn", message)
      }

      The actual appending is taken care of by the os.O_APPEND flag of the os.OpenFile() function. This flag tells Go to write at the end of the file. Additionally, the os.O_CREATE flag will make os.OpenFile() create the file if it does not exist, which is pretty handy. Apart from that, the information is written to the file using fmt.Fprintf().

      The append.go program generates no output when executed successfully. In this example, it was executed as go run append.go "123" /tmp/data.txt. However, the contents of /tmp/data.txt will not be the same:

      cat /tmp/data.txt
      
        
      1 2
      One Two
      
      Three
      123
      
      

      Copying Files

      In this section you will learn one way of creating a copy of an existing file.

      ./fileCopy.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      
      package main
      
      import (
          "fmt"
          "io"
          "os"
          "path/filepath"
          "strconv"
      )
      
      var BUFFERSIZE int64
      
      func Copy(src, dst string, BUFFERSIZE int64) error {
          sourceFileStat, err := os.Stat(src)
          if err != nil {
              return err
          }
      
          if !sourceFileStat.Mode().IsRegular() {
              return fmt.Errorf("%s is not a regular file.", src)
          }
      
          source, err := os.Open(src)
          if err != nil {
              return err
          }
          defer source.Close()
      
          _, err = os.Stat(dst)
          if err == nil {
              return fmt.Errorf("File %s already exists.", dst)
          }
      
          destination, err := os.Create(dst)
          if err != nil {
              return err
          }
          defer destination.Close()
      
          buf := make([]byte, BUFFERSIZE)
          for {
              n, err := source.Read(buf)
              if err != nil && err != io.EOF {
                  return err
              }
              if n == 0 {
                  break
              }
      
              if _, err := destination.Write(buf[:n]); err != nil {
                  return err
              }
          }
          return err
      }
      
      func main() {
          if len(os.Args) != 4 {
              fmt.Printf("usage: %s source destination BUFFERSIZEn", filepath.Base(os.Args[0]))
              return
          }
      
          source := os.Args[1]
          destination := os.Args[2]
          BUFFERSIZE, _ = strconv.ParseInt(os.Args[3], 10, 64)
      
          fmt.Printf("Copying %s to %sn", source, destination)
          err := Copy(source, destination, BUFFERSIZE)
          if err != nil {
              fmt.Printf("File copying failed: %qn", err)
          }
      }

      fileCopy.go allows you to set the size of the buffer that will be used during the copy process. In this Go program, the buffer is implemented using a byte slice named buf. The copy takes place in the Copy() function, which keeps reading the input file using the required amount of Read() calls, and writes it using the required amount of Write() calls. The Copy() function performs lots of tests to make sure that the source file exists and is a regular file and that the destination file does not exist.

      The output of fileCopy.go will resemble the following:

      go run fileCopy.go /tmp/data.txt /tmp/newText 16
      
        
      Copying /tmp/data.txt to /tmp/newText
      
      

      Implementing cat in Go

      In this section we will implement the core functionality of the cat(1) command line utility in Go. The cat(1) utility is used to print the contents of a file to a terminal window.

      ./cat.go
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      
      package main
      
      import (
          "bufio"
          "fmt"
          "io"
          "os"
      )
      
      func printFile(filename string) error {
          f, err := os.Open(filename)
          if err != nil {
              return err
          }
          defer f.Close()
          scanner := bufio.NewScanner(f)
          for scanner.Scan() {
              io.WriteString(os.Stdout, scanner.Text())
              io.WriteString(os.Stdout, "n")
          }
          return nil
      }
      
      func main() {
          filename := ""
          arguments := os.Args
          if len(arguments) == 1 {
              io.Copy(os.Stdout, os.Stdin)
              return
          }
      
          for i := 1; i < len(arguments); i++ {
              filename = arguments[i]
              err := printFile(filename)
              if err != nil {
                  fmt.Println(err)
              }
          }
      }

      If you execute cat.go without any command line arguments, then the utility will just copy from standard input to standard output using the io.Copy(os.Stdout, os.Stdin) statement. However, if there are command-line arguments, then the program will process them all in the same order that they were given using the printFile() function.

      Note

      Command Line arguments when using cat.go will only be file paths. cat.go does not support the arguments you’d see with the traditional cat command, only the core functionality.

      The output of cat.go will resemble the following:

      go run cat.go /tmp/data.txt
      
        
      1 2
      One Two
      
      Three
      
      

      Summary

      File I/O is a huge subject that cannot be covered in a single guide. However, now that you know the basics of file input and output in Go, you are free to begin experimenting and writing your own system utilities.

      More Information

      You may wish to consult the following resources for additional information on this topic. While these are provided in the hope that they will be useful, please note that we cannot vouch for the accuracy or timeliness of externally hosted materials.

      Find answers, ask questions, and help others.

      This guide is published under a CC BY-ND 4.0 license.



      Source link

      Creating Custom Errors in Go


      Introduction

      Go provides two methods to create errors in the standard library, errors.New and fmt.Errorf. When communicating more complicated error information to your users, or to your future self when debugging, sometimes these two mechanisms are not enough to adequately capture and report what has happened. To convey this more complex error information and attain more functionality, we can implement the standard library interface type, error.

      The syntax for this would be as follows:

      type error interface {
        Error() string
      }
      

      The builtin package defines error as an interface with a single Error() method that returns an error message as a string. By implementing this method, we can transform any type we define into an error of our own.

      Let’s try running the following example to see an implementation of the error interface:

      package main
      
      import (
          "fmt"
          "os"
      )
      
      type MyError struct{}
      
      func (m *MyError) Error() string {
          return "boom"
      }
      
      func sayHello() (string, error) {
          return "", &MyError{}
      }
      
      func main() {
          s, err := sayHello()
          if err != nil {
              fmt.Println("unexpected error: err:", err)
              os.Exit(1)
          }
          fmt.Println("The string:", s)
      }
      

      We’ll see the following output:

      Output

      unexpected error: err: boom exit status 1

      Here we’ve created a new empty struct type, MyError, and defined the Error() method on it. The Error() method returns the string "boom".

      Within main(), we call the function sayHello that returns an empty string and a new instance of MyError. Since sayHello will always return an error, the fmt.Println invocation within the body of the if statement in main() will always execute. We then use fmt.Println to print the short prefix string "unexpected error:" along with the instance of MyError held within the err variable.

      Notice that we don’t have to directly call Error(), since the fmt package is able to automatically detect that this is an implementation of error. It calls Error() transparently to get the string "boom" and concatenates it with the prefix string "unexpected error: err:".

      Collecting Detailed Information in a Custom Error

      Sometimes a custom error is the cleanest way to capture detailed error information. For example, let’s say we want to capture the status code for errors produced by an HTTP request; run the following program to see an implementation of error that allows us to cleanly capture that information:

      package main
      
      import (
          "errors"
          "fmt"
          "os"
      )
      
      type RequestError struct {
          StatusCode int
      
          Err error
      }
      
      func (r *RequestError) Error() string {
          return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
      }
      
      func doRequest() error {
          return &RequestError{
              StatusCode: 503,
              Err:        errors.New("unavailable"),
          }
      }
      
      func main() {
          err := doRequest()
          if err != nil {
              fmt.Println(err)
              os.Exit(1)
          }
          fmt.Println("success!")
      }
      

      We will see the following output:

      Output

      status 503: err unavailable exit status 1

      In this example, we create a new instance of RequestError and provide the status code and an error using the errors.New function from the standard library. We then print this using fmt.Println as in previous examples.

      Within the Error() method of RequestError, we use the fmt.Sprintf function to construct a string using the information provided when the error was created.

      Type Assertions and Custom Errors

      The error interface exposes only one method, but we may need to access the other methods of error implementations to handle an error properly. For example, we may have several custom implementations of error that are temporary and can be retried—denoted by the presence of a Temporary() method.

      Interfaces provide a narrow view into the wider set of methods provided by types, so we must use a type assertion to change the methods that view is displaying, or to remove it entirely.

      The following example augments the RequestError shown previously to have a Temporary() method which will indicate whether or not callers should retry the request:

      package main
      
      import (
          "errors"
          "fmt"
          "net/http"
          "os"
      )
      
      type RequestError struct {
          StatusCode int
      
          Err error
      }
      
      func (r *RequestError) Error() string {
          return r.Err.Error()
      }
      
      func (r *RequestError) Temporary() bool {
          return r.StatusCode == http.StatusServiceUnavailable // 503
      }
      
      func doRequest() error {
          return &RequestError{
              StatusCode: 503,
              Err:        errors.New("unavailable"),
          }
      }
      
      func main() {
          err := doRequest()
          if err != nil {
              fmt.Println(err)
              re, ok := err.(*RequestError)
              if ok {
                  if re.Temporary() {
                      fmt.Println("This request can be tried again")
                  } else {
                      fmt.Println("This request cannot be tried again")
                  }
              }
              os.Exit(1)
          }
      
          fmt.Println("success!")
      }
      

      We will see the following output:

      Output

      unavailable This request can be tried again exit status 1

      Within main(), we call doRequest() which returns an error interface to us. We first print the error message returned by the Error() method. Next, we attempt to expose all methods from RequestError by using the type assertion re, ok := err.(*RequestError). If the type assertion succeeded, we then use the Temporary() method to see if this error is a temporary error. Since the StatusCode set by doRequest() is 503, which matches http.StatusServiceUnavailable, this returns true and causes "This request can be tried again" to be printed. In practice, we would instead make another request rather than printing a message.

      Wrapping Errors

      Commonly, an error will be generated from something outside of your program such as: a database, a network connection, etc. The error messages provided from these errors don’t help anyone find the origin of the error. Wrapping errors with extra information at the beginning of an error message would provide some needed context for successful debugging.

      The following example demonstrates how we can attach some contextual information to an otherwise cryptic error returned from some other function:

      package main
      
      import (
          "errors"
          "fmt"
      )
      
      type WrappedError struct {
          Context string
          Err     error
      }
      
      func (w *WrappedError) Error() string {
          return fmt.Sprintf("%s: %v", w.Context, w.Err)
      }
      
      func Wrap(err error, info string) *WrappedError {
          return &WrappedError{
              Context: info,
              Err:     err,
          }
      }
      
      func main() {
          err := errors.New("boom!")
          err = Wrap(err, "main")
      
          fmt.Println(err)
      }
      

      We will see the following output:

      Output

      main: boom!

      WrappedError is a struct with two fields: a context message as a string, and an error that this WrappedError is providing more information about. When the Error() method is invoked, we again use fmt.Sprintf to print the context message, then the error (fmt.Sprintf knows to implicitly call the Error() method as well).

      Within main(), we create an error using errors.New, and then we wrap that error using the Wrap function we defined. This allows us to indicate that this error was generated in "main". Also, since our WrappedError is also an error, we could wrap other WrappedErrors—this would allow us to see a chain to help us track down the source of the error. With a little help from the standard library, we can even embed complete stack traces in our errors.

      Conclusion

      Since the error interface is only a single method, we’ve seen that we have great flexibility in providing different types of errors for different situations. This can encompass everything from communicating multiple pieces of information as part of an error all the way to implementing exponential backoff. While the error handling mechanisms in Go might on the surface seem simplistic, we can achieve quite rich handling using these custom errors to handle both common and uncommon situations.

      Go has another mechanism to communicate unexpected behavior, panics. In our next article in the error handling series, we will examine panics—what they are and how to handle them.



      Source link