VPN это просто

17 minute read

В статье использован материал из “Using TUN/TAP in go or how to write VPN

Сейчас очень много говорят о VPN, мешсетях и других технологиях для анонимизации или создания защищенных соединений. К сожалению, я довольно далек от этой темы, но иногда нужно окунаться в неизвестную область - хорошая разминка для мозгов.

Наверное, многие пользовались VPN(виртуальная приватная сеть), но не очень часто задумывались, как они реализованы изнутри. Если верить википедиии, то VPN это:

Виртуальная частная сеть — обобщённое название технологий, позволяющих обеспечить одно или несколько сетевых соединений (логическую сеть) поверх другой сети (например, Интернет).

Во-первых, технологий и способов создания приватных сетей довольно много. Они отличаются по степени защищенности, реализацией и назначением, типами и уровнями используемых протоколов.

Один из самых известных инструментов для создания VPN - это OpenVPN. С его помощью можно настроить защищенные приватные сети.

Но в чем же сам принцип работы виртуальных сетей? Я попытаюсь объяснить это на простом примере.

TUN/TAP

Для создания нашей супер простой виртуальной сети я буду использовать такую штуку, как TUN/TAP. Это виртуальные сетевые драйверы ядра системы. С их помощью можно эмулировать виртуальные сетевые карты.

TAP работает аж на канальном уровне и эмулирует Ethernet устройство. TUN работает на сетевом уровне и с его помощью можно добраться до ip пакетов.

Для наших экспериментов достаточно TUN. Мы создадим виртуальное устройство с которым будем работать.

Для начала нам нужно создать два виртуальных сетевых устройства на компьютерах, которые мы собрались объединить в виртуальную сеть.

У меня есть небольшой виртуальный сервер с ip 95.213.199.250. Вторая машинка - это мой локальный компьютер с ip адресом 109.167.253.115.

При создании виртуального сетевого устройства ему нужно задать ip адрес. На локальном компьютере это будет 192.168.9.11/24, на виртуальном сервере 192.168.9.9/24.

Как все это будет работать? Все довольно просто:

  1. Мы отправляем пакет на локальной машине на TUN интерфейс 192.168.9.11, например echo "hello" > /dev/udp/192.168.9.11/4001
  2. Затем, наша программа, запущенная на той же машине, вычитывает данные из этого интерфейса и отправляет их на удаленные компьютер 95.213.199.250 через интернет.
  3. На удаленной машине программа читает данные, присланные на 95.213.199.250 и записывает их в TUN интерфейс 192.168.9.9 на той же машине.
  4. Теперь мы можем считать данные с 192.168.9.9, например как-то так netcat -lu 192.168.9.11 4001

Внешне все выглядит, как будто мы работаем по локальной сети, хотя наши данные пересылаются через интернет. Данные можно зашифровать и добавить кучу разных плюшек. Но мы попытаемся реализовать только базовые вещи.

Реализация

Начнем с создания виртуальных сетевых интерфейсов. Для этого мы будем использовать пакет github.com/songgao/water который представляет из себя отличную библиотеку для работы с TUN/TAP интерфейсом. Кроме этого, мы будем использовать программу /sbin/ip для настройки наших интерфейсов.

Создаем интерфейс:

1iface, err := water.NewTUN("")
2if err != nil {
3    log.Fatalln("Unable to allocate TUN interface:", err)
4}

Теперь нам нужно настроить наш свежесозданный интерфейс

1/sbin/ip link set dev tun0 mtu 1300
2/sbin/ip addr add 192.168.9.10/24 dev tun0
3/sbin/ip set dev tun0 up

Для того чтобы посылать и получать данные через интернет нам нужно создать UDP сокетов. Тут нет никаких хитростей, все прям как в мануле.

Один цикл используем для чтения из UDP и запись в виртуальный интерфейс

 1buf := make([]byte, BUFFERSIZE)
 2for {
 3    // читаем, что нам прислали из интернета
 4    n, addr, err := lstnConn.ReadFromUDP(buf)
 5    // будем использовать для отладки
 6    header, _ := ipv4.ParseHeader(buf[:n])
 7    fmt.Printf("Received %d bytes from %v: %+v\n", n, addr, header)
 8    if err != nil || n == 0 {
 9        fmt.Println("Error: ", err)
10        continue
11    }
12    // пишем в TUN интерфейс
13    iface.Write(buf[:n])
14}

Второй цикл используется для обратного - чтения из виртуального интерфейса и записи в UDP:

 1packet := make([]byte, BUFFERSIZE)
 2for {
 3    // читаем данные из виртуального интерфейса
 4    plen, err := iface.Read(packet)
 5    if err != nil {
 6        break
 7    }
 8
 9    header, _ := ipv4.ParseHeader(packet[:plen])
10    fmt.Printf("Sending to remote: %+v (%+v)\n", header, err)
11    // отправляем на удаленный адрес
12    lstnConn.WriteToUDP(packet[:plen], remoteAddr)
13}

Тут нужно уточнить, что у нас в переменных remoteAddr. У нас есть два флага:

1var (
2    local  = flag.String("local", "", "Local tun interface IP/MASK like 192.168.3.3/24")
3    remote = flag.String("remote", "", "Remote server (external) IP like 8.8.8.8")
4)

local - это ip адрес виртуального интерфейса на локальном компьютере. remote - внешний ip адрес удаленного компьютера, по которому будет происходит UDP соединение.

Для настройки интерфейса сделаем специальную функцию:

 1func run(args ...string) {
 2    cmd := exec.Command("/sbin/ip", args...)
 3    cmd.Stderr = os.Stderr
 4    cmd.Stdout = os.Stdout
 5    cmd.Stdin = os.Stdin
 6    err := cmd.Run()
 7    if nil != err {
 8        log.Fatalln("error running /sbin/ip:", err)
 9    }
10}

И использовать ее можно вот так:

1run("link", "set", "dev", iface.Name(), "mtu", MTU)

Обратите внимание, что у вас два бесконечных цикла. Чтобы все работало как надо, можно обернуть первый в go-рутину:

 1go func() {
 2    buf := make([]byte, BUFFERSIZE)
 3    for {
 4        n, addr, err := lstnConn.ReadFromUDP(buf)
 5        header, _ := ipv4.ParseHeader(buf[:n])
 6        fmt.Printf("Received %d bytes from %v: %+v\n", n, addr, header)
 7        if err != nil || n == 0 {
 8            fmt.Println("Error: ", err)
 9            continue
10        }
11        iface.Write(buf[:n])
12    }
13}()

Пример запуска нашей программы на локальном компьютере:

1sudo ./vpn -local="192.168.9.9/24" -remote=95.213.199.250

На удаленном компьютере:

1sudo ./vpn -local="192.168.9.11/24" -remote=109.167.253.115

И на этом, в принципе, все. Программка получилась маленькая, но довольно хорошо иллюстрирующая концепцию работы VPN.

Для проверки отправим что ни будь через нашу виртуальную сеть. На локальном компьютере отправлю данные через UDP:

echo "hello" > /dev/udp/192.168.9.11/4001

На удаленном компьютере читаю из UDP:

1netcat -lu 192.168.9.11 4001
2hello

Ура! Данные передались, наша сеть работает. Полный код программы можно посмотреть на github.