View on GitHub

Methodics of Linux Network Protocols

Practical materials for learning TCP/IP and its implementation on Linux

Туннелирование в общем случае - это использование некоторого потока данных для инкапсуляции некоторого, вообще говоря, произвольного сетевого трафика. Основной целью использования туннелей является создание некоторого закрытого подпространства сети, в которой пользователи объединены в единую структуру (например, для них всё будет выглядеть одной локальной сетью, хотя физически это будет не так), данные в ней могут шифроваться (причём на сетевом или даже интерфейсном уровнях, а не на прикладном) или быть защищены от анализа и фильтрации дополнительными системами (например, межсетевыми экранами).

IP over IP

IP over IP, или IP in IP - простейшая схема туннелирования, основанная на инкапсуляции одного IP-пакета в другой IP-пакет. С точки зрения построения пакета данный протокол наращивает ещё один заголовок сетевого уровня на уже существующий. Для обработки таких мета-пакетов необходимо использовать специальный виртуальный интерфейс, который будет обрабатывать специализированный трафик. Данный вид туннелирования позволяет решить проблему, связанную с представлением абонентов участниками одной локальной сети, когда на самом деле маршрут может, вообще говоря, динамически меняться при передаче сообщений в туннеле.

Простая схема работы

Соберём тестовый стенд для IP over IP.

Сделаем классическую топологию сети из трёх абонентов:

papillon_rouge: vbintnets
srv:  
       eth1: intnet  
router:  
       eth1: intnet  
       eth2: deepnet  
client:  
       eth1: deepnet
papillon_rouge:

Проведём быструю настройку сетей для абонентов:

srv

[root@srv ~]# autonet  
[root@srv ~]# ip route add 10/8 via 10.9.0.27  
[root@srv ~]#

router

[root@router ~]# autonet

client

[root@client ~]# autonet  
[root@client ~]# ip route add default via 10.4.0.27  
[root@client ~]# ping 10.9.0.26  
PING 10.9.0.26 (10.9.0.26) 56(84) bytes of data.  
64 bytes from 10.9.0.26: icmp_seq=1 ttl=63 time=1.26 ms  
64 bytes from 10.9.0.26: icmp_seq=2 ttl=63 time=0.802 ms  
64 bytes from 10.9.0.26: icmp_seq=3 ttl=63 time=0.881 ms  
  
--- 10.9.0.26 ping statistics ---  
3 packets transmitted, 3 received, 0% packet loss, time 2046ms  
rtt min/avg/max/mdev = 0.802/0.982/1.264/0.201 ms  
[root@client ~]#

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

srv

[root@srv ~]# ip link add ipip0 type ipip remote 10.4.0.28 local 10.9.0.26  

[root@srv ~]# ip a  
<...>
6: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000  
   link/ipip 0.0.0.0 brd 0.0.0.0  
7: ipip0@NONE: <POINTOPOINT,NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000  
   link/ipip 10.9.0.26 peer 10.4.0.28  

[root@srv ~]# ip addr add dev ipip0 192.168.0.1/24  
[root@srv ~]# ip link set ipip0 up

client

[root@client ~]# ip link add ipip0 type ipip local 10.4.0.28 remote 10.9.0.26  

[root@client ~]# ip a  
<...>
6: tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000  
   link/ipip 0.0.0.0 brd 0.0.0.0  
7: ipip0@NONE: <POINTOPOINT,NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000  
   link/ipip 10.4.0.28 peer 10.9.0.26  

[root@client ~]# ip addr add dev ipip0 192.168.0.2/24  
[root@client ~]# ip link set ipip0 up    
[root@client ~]#

Заметим, что MTU (размер посылки) на 20 октетов меньше MTU других интерфейсов, поскольку их занимает второй заголовок IP.

Запустим трафик через туннель и посмотрим его вид на router:

srv

[root@srv ~]# ping -fc5 192.168.0.2  
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.  
   
--- 192.168.0.2 ping statistics ---  
5 packets transmitted, 5 received, 0% packet loss, time 2ms  
rtt min/avg/max/mdev = 0.418/0.510/0.736/0.115 ms, ipg/ewma 0.575/0.620 ms  
[root@srv ~]#

router

[root@router ~]# tcpdump  -i eth2  
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode  
listening on eth2, link-type EN10MB (Ethernet), snapshot length 262144 bytes  
10:20:38.405461 IP 10.9.0.26 > 10.4.0.28: IP 192.168.0.1 > 192.168.0.2: ICMP echo request, id 2, seq 1,  
length 64  
10:20:38.405855 IP 10.4.0.28 > 10.9.0.26: IP 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 2, seq 1, l  
ength 64  
10:20:38.406144 IP 10.9.0.26 > 10.4.0.28: IP 192.168.0.1 > 192.168.0.2: ICMP echo request, id 2, seq 2,  
length 64  
10:20:38.406370 IP 10.4.0.28 > 10.9.0.26: IP 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 2, seq 2, l  
ength 64  
10:20:38.406596 IP 10.9.0.26 > 10.4.0.28: IP 192.168.0.1 > 192.168.0.2: ICMP echo request, id 2, seq 3,  
length 64  
10:20:38.406817 IP 10.4.0.28 > 10.9.0.26: IP 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 2, seq 3, l  
ength 64  
10:20:38.407066 IP 10.9.0.26 > 10.4.0.28: IP 192.168.0.1 > 192.168.0.2: ICMP echo request, id 2, seq 4,  
length 64  
10:20:38.407289 IP 10.4.0.28 > 10.9.0.26: IP 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 2, seq 4, l  
ength 64  
10:20:38.407503 IP 10.9.0.26 > 10.4.0.28: IP 192.168.0.1 > 192.168.0.2: ICMP echo request, id 2, seq 5,  
length 64  
10:20:38.407806 IP 10.4.0.28 > 10.9.0.26: IP 192.168.0.2 > 192.168.0.1: ICMP echo reply, id 2, seq 5, l  
ength 64  
10:20:43.659786 ARP, Request who-has router tell 10.4.0.28, length 46  
10:20:43.659806 ARP, Reply router is-at 08:00:27:8a:f4:2b (oui Unknown), length 28  
10:20:43.798298 ARP, Request who-has 10.4.0.28 tell router, length 28  
10:20:43.798831 ARP, Reply 10.4.0.28 is-at 08:00:27:6e:71:da (oui Unknown), length 46  
  
14 packets captured  
14 packets received by filter  
0 packets dropped by kernel  
[root@router ~]#

Заметно, что каждый пакет падержит в себе ещё один с адресами туннеля.

Выход в интернет

Для выхода в интернет необходимо, во-первых, добавить трансляцию адресов. Это можно сделать с помощью добавления дополнительной цепочки в NFTables:

srv

[root@srv ~]# dhcpcd eth0  
dhcpcd-10.2.2 starting  
eth0: waiting for carrier  
eth0: carrier acquired  
eth0: soliciting a DHCP lease  
eth0: offered 10.0.2.15 from 10.0.2.2  
eth0: leased 10.0.2.15 for 86400 seconds  
eth0: adding route to 10.0.2.0/24  
eth0: adding default route via 10.0.2.2  

[root@srv ~]# systemctl enable --now nftables.service    
Created symlink '/etc/systemd/system/multi-user.target.wants/nftables.service' -> '/usr/lib/systemd/sys  
tem/nftables.service'.  

[root@srv ~]# nft add chain inet filter masq "{type nat hook postrouting priority srcnat;}"  
[root@srv ~]# nft add rule inet filter masq oif eth0 masquerade
[root@srv ~]# nft list chain inet filter masq  
table inet filter {  
       chain masq {  
               type nat hook postrouting priority srcnat; policy accept;  
               oif "eth0" masquerade  
       }  
}  
[root@srv ~]#    

Казалось бы, теперь необходимо просто поменять маршрут по умолчанию для client на туннель, однако система резко перестаёт работать:

client

[root@client ~]# ip r  
default via 10.4.0.27 dev eth1    
10.4.0.0/24 dev eth1 proto kernel scope link src 10.4.0.28    
192.168.0.0/24 dev ipip0 proto kernel scope link src 192.168.0.2    

[root@client ~]# ip route del default via 10.4.0.27 dev eth1    
[root@client ~]# ip route add default via 192.168.0.1  

[root@client ~]# ip r  
default via 192.168.0.1 dev ipip0    
10.4.0.0/24 dev eth1 proto kernel scope link src 10.4.0.28    
192.168.0.0/24 dev ipip0 proto kernel scope link src 192.168.0.2    

[root@client ~]# ping -fc3 10.9.0.26  
PING 10.9.0.26 (10.9.0.26) 56(84) bytes of data.  
...  
--- 10.9.0.26 ping statistics ---  
3 packets transmitted, 0 received, 100% packet loss, time 22ms

Поскольку наружным destination IP в туннелированнном пакете является IP абонента, а не туннеля, а правил по его передаче никаких, кроме как заново отправить его в туннель, нет, пакет просто теряется в постоянном зацикливании и наращивании заголовков. Для решения этой проблемы необходимо явно указать маршрут для соответствующих пакетов:

client

[root@client ~]# ip route add 10.9.0.26 via 10.4.0.27  

[root@client ~]# ping -fc3 10.9.0.26  
PING 10.9.0.26 (10.9.0.26) 56(84) bytes of data.  
   
--- 10.9.0.26 ping statistics ---  
3 packets transmitted, 3 received, 0% packet loss, time 1ms  
rtt min/avg/max/mdev = 0.544/0.608/0.699/0.065 ms, ipg/ewma 0.727/0.666 ms  

[root@client ~]# ping 1.1.1.1  # Проверка выхода в интернет
PING 1.1.1.1 (1.1.1.1) 56(84) bytes of data.  
64 bytes from 1.1.1.1: icmp_seq=1 ttl=254 time=11.9 ms  
64 bytes from 1.1.1.1: icmp_seq=2 ttl=254 time=7.70 ms  
  
--- 1.1.1.1 ping statistics ---  
2 packets transmitted, 2 received, 0% packet loss, time 1000ms  
rtt min/avg/max/mdev = 7.698/9.798/11.898/2.100 ms  
[root@client ~]#

Более того, на router теперь возможно явно перекрыть трафик, не идущий через туннель:

router

[root@router ~]# systemctl enable --now nftables.service    
Created symlink '/etc/systemd/system/multi-user.target.wants/nftables.service' -> '/usr/lib/systemd/sys  
tem/nftables.service'.  
[root@router ~]# nft add rule inet filter forward ip protocol tcp reject  
[root@router ~]# nft list ruleset  
table inet filter {  
       chain input {  
               type filter hook input priority filter; policy accept;  
       }  
  
       chain forward {  
               type filter hook forward priority filter; policy accept;  
               ip protocol tcp reject with icmp port-unreachable  
       }  
  
       chain output {  
               type filter hook output priority filter; policy accept;  
       }  
}  
[root@router ~]#

client

[root@client ~]# ip link set ipip0 down  
[root@client ~]# netcat 1.1.1.1 80  # без туннеля не работает
[root@client ~]# ip link set ipip0 up     
[root@client ~]# ip r  # после включения туннеля надо восстановить правила маршрутизации
10.4.0.0/24 dev eth1 proto kernel scope link src 10.4.0.28    
10.9.0.26 via 10.4.0.27 dev eth1    
192.168.0.0/24 dev ipip0 proto kernel scope link src 192.168.0.2    
[root@client ~]# ip route add default via 192.168.0.1  

[root@client ~]# netcat 1.1.1.1 80  # с туннелем работает
qwerty  
HTTP/1.1 400 Bad Request  
Server: cloudflare  
Date: Mon, 02 Jun 2025 08:19:21 GMT  
Content-Type: text/html  
Content-Length: 155  
Connection: close  
CF-RAY: -  
  
<html>  
<head><title>400 Bad Request</title></head>  
<body>  
<center><h1>400 Bad Request</h1></center>  
<hr><center>cloudflare</center>  
</body>  
</html>  
[root@client ~]#

Возникает вопрос, почему же никак не автоматизировано подключение в “большой интернет” для соединения через туннель, и почему необходимо вручную строить маршрут до конца туннеля не через туннель? Дело в том, что изначально туннелирование и VPN - Virtual Private Network - не были задуманы, как инструмент обхода блокировок (в том понимании слова “блокировка”, с которым сталкивается сейчас обычный пользователь интернета). Их основной задачей, как обсуждалось выше, является объединение в единую адресную “локальную” сеть устройств, физически не находящихся в ней (например, при подключении рабочего ноутбука из командировки в локальную сеть компании).

Выводы о IP over IP

По итогу работы с IP over IP получается очень упрощённый вариант почти VPN-соединения. Никаких шифрования, авторизации и проверок в нём нет, также такой способ туннелирования очень сложно масштабируется, поскольку для каждой пары необходимо создавать свой туннель, заводить свою пару адресов; у каждого такого туннеля отсутствует MAC-адрес, что может усложнить работу на более низком уровне и т.д. IP over IP хорошо работает в простых p2p-соединениях без жёстких требований, используемый исключительно для “локализации” разных сетей в одну.

L2TP

Более универсальное решение представляет протокол туннелирования фреймов L2TP. Он позволяет организовывать UDP-потоки между абонентами, при этом у организуемых интерфейсов есть фиксированный MAC-адрес и один серверный IP.

Данные решения появились в протоколе не случайно.
Во-первых, выбор транспортного протокола. Поскольку в “большой” сети TCP-соединение используется для сервисной информации, некоторые маршруты могут быть закрыты для потусторонних TCP-соединений, в отличие от UDP, для которого существование какого-то специализированного протокола странного вида - обычное дело.
Во-вторых, единый IP для всех подключений. Подключения в L2TP определяются сеансами с каждой стороны абонентов, составляя уникальную четвёрку “IP первого абонента, номер сеанса первого абонента, IP второго абонента, номер сеанса второго абонента”. Уже сеансы уложены в тоннель с собственной определяющей шестёркой “ID тоннелей, IP и порты абонентов”.

Применение L2TP

Для настройки для начала перезагрузим все машины, чтобы с нуля настроить все машины. Для создания соединения сначала задаётся туннель (с 6 идентификаторами), затем для туннеля задаётся сеанс (с двумя идентификаторами, поскольку остальные два (IP-адреса устройств) уже связаны с туннелем)

router

[root@router ~]# autonet  
[root@router ~]#

srv

[root@srv ~]# autonet  
[root@srv ~]# ip route add 10.4/16 via 10.9.0.27                                                         
[root@srv ~]# ip l2tp add tunnel tunnel_id 300 peer_tunnel_id 400 encap udp local 10.9.0.26 remote 10.4.0.28 udp_sport 5000 udp_dport 6000  
[root@srv ~]# ip l2tp add session tunnel_id 300 session_id 100 peer_session_id 200  

[root@srv ~]# ip a l2tpeth0  
Command "l2tpeth0" is unknown, try "ip address help".  
[root@srv ~]# ip a            
<...>
6: l2tpeth0: <BROADCAST,MULTICAST> mtu 1446 qdisc noop state DOWN group default qlen 1000  
   link/ether 86:ec:96:f0:f3:1c brd ff:ff:ff:ff:ff:ff  

[root@srv ~]# ip link set l2tpeth0 up  
[root@srv ~]# ip addr add dev l2tpeth0 192.168.0.1/24

Параметры туннеля можно посмотреть с помощью специальных show-команд iproute2

srv

[root@srv ~]# ip l2tp show tunnel  
Tunnel 300, encap UDP  
 From 10.9.0.26 to 10.4.0.28  
 Peer tunnel 400  
 UDP source / dest ports: 5000/6000  
 UDP checksum: disabled  

[root@srv ~]# ip l2tp show session  
Session 100 in tunnel 300  
 Peer session 200, tunnel 400  
 interface name: l2tpeth0  
 offset 0, peer offset 0  
  
[root@srv ~]#

Настроим оставшуюся машину и убедимся в работе сети:

client

[root@client ~]# autonet  
[root@client ~]# ip route add default via 10.4.0.27  

[root@client ~]# ip l2tp add tunnel tunnel_id 400 peer_tunnel_id 300 encap udp local 10.4.0.28 remote 10.9.0.26 udp_sport 6000 udp_dport 5000  
[root@client ~]# ip l2tp add session tunnel_id 400 session_id 200 peer_session_id 100  

[root@client ~]# ip link set l2tpeth0 up  
[root@client ~]# ip addr add 192.168.0.2/24 dev l2tpeth0  

[root@client ~]# ping -fc5 192.168.0.1  
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.  
   
--- 192.168.0.1 ping statistics ---  
5 packets transmitted, 5 received, 0% packet loss, time 3ms  
rtt min/avg/max/mdev = 0.378/0.711/1.754/0.523 ms, ipg/ewma 0.859/1.215 ms  

[root@client ~]# ip l2tp show tunnel  
Tunnel 400, encap UDP  
 From 10.4.0.28 to 10.9.0.26  
 Peer tunnel 300  
 UDP source / dest ports: 6000/5000  
 UDP checksum: disabled  

[root@client ~]# ip l2tp show session  
Session 200 in tunnel 400  
 Peer session 100, tunnel 300  
 interface name: l2tpeth0  
 offset 0, peer offset 0  
  
[root@client ~]

Поскольку работа протокола осуществляется на интерфейсном уровне, создаваемый виртуальный интерфейс можно связать в мост с локальным интерфейсом и, таким образом, сделать общий сегмент сети удалённых пользователей и локальной сети.

Для соединения с “большим интернетом” всё также нужно руками настраивать маршрут до конца туннеля в обход туннеля, а остальной трафик пропускать через туннель, как и для IP over IP.

Wireguard

Говоря непосредственно о VPN-протоколах, в которых кроме “локализации” сети происходит также шифрование данных, выделяются три популярных протокола.

Первый - IPsec - изначально разрабатывался исключительно для IPv6, однако был впоследствии перенесён и на IPv4. Основной особенностью протокола являлось шифрование основной посылки пакета без затрагивания заголовков. Идея заключалась в сохранении логики работы сети при обеспечении защиты данных, однако в обычном случае всё провалилось, поскольку большинство уже существующих протоколов (даже самый обычный NAT) строят свою работу в том числе на данных пакета. Решением проблемы стал туннельный режим, который, как было показано выше, сложен в поддержке.

Хорошей альтернативой рассматриваемому в этой главе протоколу является большой открытый проект OpenVPN. Однако далее речь пойдёт о протоколе WireGuard (и, конечно, про него есть Arch-вики), управление и настройка которого поддерживаются Linux-утилитами и системой systemd-networkd. Идея работы протокола аналогична рассмотренным ранее: UDP-пакеты с данными. Особенностью является схема ассиметричного шифрования. Классическая схема с закрытым и открытым ключом, которые генерируются и передаются администратором сети, позволяет безопасно передавать сообщения. Сами ключи можно дополнительно защищать паролями.

Минимальная настройка

Для начала вновь перезапустим все машины и настроим сеть с помощью systemd-networkd, а также вновь добавим между абонентами “агрессивный интернет”, который будет запрещать весь TCP-трафик:

srv

[root@srv ~]# vim /etc/systemd/network/20-default.network  
[root@srv ~]# vim /etc/systemd/network/50-intnet.network  

[root@srv ~]# cat /etc/systemd/network/20-default.network  
[Match]  
Name = eth0  
  
[Network]  
DHCP = ipv4  
IPv4Forwarding=yes  

[root@srv ~]# cat /etc/systemd/network/50-intnet.network  
[Match]  
Name=eth1  
  
[Network]  
Address=10.9.0.1/24  
  
[Route]  
Gateway=10.9.0.27  
Destination=10.0.0.0/8  
  
[root@srv ~]# systemctl enable --now systemd-networkd  
<...>
[root@srv ~]#

router

[root@router ~]# autonet  
[root@router ~]# ip r  
10.4.0.0/24 dev eth2 proto kernel scope link src 10.4.0.27    
10.9.0.0/24 dev eth1 proto kernel scope link src 10.9.0.27    

[root@router ~]# systemctl enable --now nftables.service  
[root@router ~]# nft add rule inet filter forward ip protocol tcp reject  

[root@router ~]# nft list ruleset  
table inet filter {  
       chain input {  
               type filter hook input priority filter; policy accept;  
       }  
  
       chain forward {  
               type filter hook forward priority filter; policy accept;  
               ip protocol tcp reject with icmp port-unreachable  
       }  
  
       chain output {  
               type filter hook output priority filter; policy accept;  
       }  
}  
[root@router ~]#

client

[root@client ~]# vim /etc/systemd/network/50-intnet.network  
[root@client ~]# cat /etc/systemd/network/50-intnet.network  
[Match]  
Name=eth1  
  
[Network]  
Address=10.4.0.3/24  
Gateway=10.4.0.27  
  
[root@client ~]# systemctl enable --now systemd-networkd  
<...>
[root@client ~]# ping -fc5 10.9.0.1  
PING 10.9.0.1 (10.9.0.1) 56(84) bytes of data.  
   
--- 10.9.0.1 ping statistics ---  
5 packets transmitted, 5 received, 0% packet loss, time 3ms  
rtt min/avg/max/mdev = 0.279/0.517/1.173/0.330 ms, ipg/ewma 0.658/0.831 ms  
[root@client ~]#

Для настройки WireGuard необходимо в начале создать пару открытого и закрытого ключей у абонентов. Для этого существуют специальные команды утилиты wg:

Чтобы сразу получить оба ключа для работы (так делать можно только в нашем тестовом случае и никак не в реальной жизни), можно воспользоваться Ctrl+C - Ctrl+V командой

srv

[root@srv ~]# wg genkey | tee /dev/stderr | wg pubkey  
SKY1B2qiRnIC7/w1bd8BEcYmOPVev8zkIlCOzXAgqlA=  
UsOT59qndg5a4vDrIm8J4jb1zTdnN4mL9SXxQTsTxxY=
[root@srv ~]#

client

[root@client ~]# wg genkey | tee /dev/stderr | wg pubkey  
SBg6sXq8zwVUuw06LY5cvPhbVl8bSd6wfj+LYWiW+UM=  
lDna8mICiC+IWMvhD8l7ChVnSY/eRkY+B1xCo3wuEhI=  
[root@client ~]#

Теперь необходимо создать интерфейс для подключения абонентов и настроить для него сеть:

srv

[root@srv ~]# cat /etc/systemd/network/70-wg.netdev    
[NetDev]  
Name        = wg  
Kind        = wireguard  # Интерфейс типа WireGuard
  
[WireGuard]  
ListenPort  = 51820  # Порт подключения
PrivateKey = SKY1B2qiRnIC7/w1bd8BEcYmOPVev8zkIlCOzXAgqlA=  # Свой приватный ключ
  
[WireGuardPeer]  
AllowedIPs  = 192.168.0.0/24  
PublicKey = lDna8mICiC+IWMvhD8l7ChVnSY/eRkY+B1xCo3wuEhI=  # Чужой открытый ключ
  
[root@srv ~]# cat /etc/systemd/network/70-wg.network  
[Match]  
Name        = wg  
  
[Network]  
Address     = 192.168.0.1/24  
[root@srv ~]#

client

[root@client ~]# cat /etc/systemd/network/70-wg.netdev          
[NetDev]  
Name        = wg  
Kind        = wireguard  
  
[WireGuard]  
PrivateKey  = SBg6sXq8zwVUuw06LY5cvPhbVl8bSd6wfj+LYWiW+UM=  
  
[WireGuardPeer]  
AllowedIPs  = 192.168.111.0/24  
PublicKey   = UsOT59qndg5a4vDrIm8J4jb1zTdnN4mL9SXxQTsTxxY=  
Endpoint = 10.9.0.1:51820  
  
[root@client ~]# cat /etc/systemd/network/70-wg.network  
[Match]  
Name        = wg  
  
[Network]  
Address     = 192.168.0.2/24  
[root@client ~]#

Однако система при попытке обращения к интерфейсу выдаст ошибку:

srv

[root@srv ~]# journalctl | grep '70-wg.netdev'  
Mon DD HH:MM:SS srv systemd-networkd[1570]: /etc/systemd/network/70-wg.netdev has 0644 mode that is too permissive, please adjust the ownership and access mode.  
[root@srv ~]#

Поскольку в файл с описанием интерфейса могут обратиться любые демоны или утилиты (права 0644), хранение там приватного ключа в чистом виде запрещено systemd (FrBrGeorge/MyDict/normancoffepot.png это или нет, каждый решает сам). Для решения проблемы можно создать отдельный файл с приватным ключом и вписать его в netdev-файл через параметр PrivateKeyFile или, как сделаем мы, ограничить права для файла только для группы systemd-network:

srv

[root@srv ~]# chgrp systemd-network /etc/systemd/network/70-wg.netdev 
[root@srv ~]# chmod o-r /etc/systemd/network/70-wg.netdev 
[root@srv ~]# ls -l /etc/systemd/network/70-wg.netdev
-rw-r----- 1 root systemd-network 245 Jun  1 16:51 /etc/systemd/network/70-wg.netdev
[root@srv ~]# systemctl restart systemd-networkd
[root@srv ~]# 

client

[root@client ~]# chgrp systemd-network /etc/systemd/network/70-wg.netdev    
[root@client ~]# chmod o-r /etc/systemd/network/70-wg.netdev    
[root@client ~]# chgrp systemd-network keyfile    
[root@client ~]# ls -l /etc/systemd/network/70-wg.netdev  
-rw-r----- 1 root systemd-network 256 Jun  1 16:53 /etc/systemd/network/70-wg.netdev  
[root@client ~]# systemctl restart systemd-networkd  
[root@client ~]#
[root@client ~]# ping -fc5 192.168.0.1  
PING 192.168.0.1 (192.168.0.1) 56(84) bytes of data.  

--- 192.168.0.1 ping statistics ---  
5 packets transmitted, 5 received, 0% packet loss, time 3ms  
rtt min/avg/max/mdev = 0.279/0.517/1.173/0.330 ms, ipg/ewma 0.658/0.831 ms  
[root@client ~]#

Домашняя работа