VPN это просто

Post Thumbnail

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

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

Наверное, многие пользовались 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 для настройки наших интерфейсов.

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

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

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

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

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

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

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

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

packet := make([]byte, BUFFERSIZE)
for {
    // читаем данные из виртуального интерфейса
    plen, err := iface.Read(packet)
    if err != nil {
        break
    }

    header, _ := ipv4.ParseHeader(packet[:plen])
    fmt.Printf("Sending to remote: %+v (%+v)\n", header, err)
    // отправляем на удаленный адрес
    lstnConn.WriteToUDP(packet[:plen], remoteAddr)
}

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

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

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

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

func run(args ...string) {
    cmd := exec.Command("/sbin/ip", args...)
    cmd.Stderr = os.Stderr
    cmd.Stdout = os.Stdout
    cmd.Stdin = os.Stdin
    err := cmd.Run()
    if nil != err {
        log.Fatalln("error running /sbin/ip:", err)
    }
}

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

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

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

go func() {
    buf := make([]byte, BUFFERSIZE)
    for {
        n, addr, err := lstnConn.ReadFromUDP(buf)
        header, _ := ipv4.ParseHeader(buf[:n])
        fmt.Printf("Received %d bytes from %v: %+v\n", n, addr, header)
        if err != nil || n == 0 {
            fmt.Println("Error: ", err)
            continue
        }
        iface.Write(buf[:n])
    }
}()

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

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

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

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

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

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

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

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

netcat -lu 192.168.9.11 4001
hello

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