VPN это просто
В статье использован материал из “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
.
Как все это будет работать? Все довольно просто:
- Мы отправляем пакет на локальной машине на TUN интерфейс
192.168.9.11
, напримерecho "hello" > /dev/udp/192.168.9.11/4001
- Затем, наша программа, запущенная на той же машине, вычитывает данные из этого интерфейса и отправляет их на удаленные компьютер
95.213.199.250
через интернет. - На удаленной машине программа читает данные, присланные на
95.213.199.250
и записывает их в TUN интерфейс192.168.9.9
на той же машине. - Теперь мы можем считать данные с
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.