WireGuard and QUIC

When we want to protect transmitted data, we use a VPN. But sometimes we want the fact of using a VPN to be hidden as well. We care so much about our data:)

When it is necessary to protect transmitted data, we resort to the help of a VPN. However, sometimes there is a desire to hide even the fact of its use. We care so much about our security!😊

Since WireGuard is now at the peak of popularity, and it works based on the UDP protocol, the idea arose to disguise its traffic as another popular protocol based on UDP — QUIC. QUIC combines the functions of TCP and TLS, so it was logical to try to make WireGuard traffic look like QUIC traffic. To implement this idea, it was necessary to study the source code of WireGuard in the Linux kernel in detail. The patch for the Linux kernel is available on QUICWireGuard.

Port selection

QUIC traffic uses the same port as HTTPS — 443. Therefore, WireGuard will also be raised on this port. However, since this solution is intended exclusively for operation between two devices on Linux, we leave the possibility of connecting other devices on alternative ports. Mimicry will be activated only when using port 443 for WireGuard or when an incoming connection to this port. In all other cases, WireGuard will remain unchanged.

Installation on Arch, Ubuntu, OpenWRT

Two scripts were made for assembly and installation:

linux_auto_install.sh - suitable for building the wireguard.ko module for this system and installing it.

openwrt_auto_install.sh - suitable for building the wireguard.ko module for OpenWRT and installing it, as an argument to the script you need to specify either the ssh name of the router or [email protected]

Both scripts do not remove the previous wireguard.ko module from the system. Removing the old module, loading the updated one, and subsequently rebooting the WireGuard device must be done manually. To make sure that the new version is loaded, check the output of the command dmesg | grep -i wireguard.

[    8.975692] wireguard: QUICWireGuard 1.0.0 loaded. See https://github.com/karen07/QUICWireGuard for information.
[    8.975694] wireguard: Copyright (C) 2015-2019 Jason A. Donenfeld . All Rights Reserved.
[    8.975695] wireguard: Copyright (C) 2024 Karen.

You should see the message QUICWireGuard. If you only have access to the machine through WireGuard, be especially careful when checking: there is always a risk of kernel failure or incorrect installation. However, in my practice, such cases have not occurred.

For systems other than Arch, Ubuntu, and OpenWRT, the script is also applicable, but you will need to add the installation of packages required to build the kernel module for your specific distribution.

Forming the WireGuard package

If you open the code of WireGuard in the Linux kernel. There is a file messages.h, which describes two structures:

struct message_handshake_initiation {
	struct message_header header;
	__le32 sender_index;
	u8 unencrypted_ephemeral[NOISE_PUBLIC_KEY_LEN];
	u8 encrypted_static[noise_encrypted_len(NOISE_PUBLIC_KEY_LEN)];
	u8 encrypted_timestamp[noise_encrypted_len(NOISE_TIMESTAMP_LEN)];
	struct message_macs macs;
};
struct message_handshake_response {
	struct message_header header;
	__le32 sender_index;
	__le32 receiver_index;
	u8 unencrypted_ephemeral[NOISE_PUBLIC_KEY_LEN];
	u8 encrypted_nothing[noise_encrypted_len(0)];
	struct message_macs macs;
};

The variables of these structures are sent during the WireGuard handshake in the file send.c in the functions wg_packet_send_handshake_initiation and wg_packet_send_handshake_response. Therefore, if you send not only the packet but also the QUIC header, the WireGuard handshake will resemble the QUIC handshake.

struct message_handshake_initiation packet;
wg_socket_send_buffer_to_peer(peer, &packet, sizeof(packet),
					      HANDSHAKE_DSCP);
struct message_handshake_response packet;
wg_socket_send_buffer_to_peer(peer, &packet,
						      sizeof(packet),
						      HANDSHAKE_DSCP);

Description of the QUIC header

If you look in WireShark, the QUIC init header consists of seven fields.

Field

Description, value

Flags

Bit field, fill with value 0xC0

Version

Version, now always 0x1 in BE order

DCID len

DCID length in bytes, for init header value 0x8

DCID

Receiving number

SCID len

SCID length in bytes, for init header value 0x0

Token len

SCID length in bytes, for init header value 0x0

Data len

Consists of two parts, the first two bits of the first byte are the indicator of the total length of Data len, in my case 0b01(0x4), and to 0x4 we add the length of init header

If you look at WireShark, the QUIC response header consists of seven fields.

Field

Description, value

Flags

Bit field, fill with value 0xC0

Version

Version, now always 0x1 in BE order

DCID len

DCID length in bytes, for response header value 0x0

SCID len

SCID length in bytes, for response header value 0x8

SCID

Sender number

Token len

SCID length in bytes, for response header value 0x0

Data len

Consists of two parts, the first two bits of the first byte are the indicator of the total length of Data len, in my case 0b01(0x4), and to 0x4 we add the length of response header

Accordingly, if you add QUIC init header in front of struct message_handshake_initiation packet, the start of the WireGuard session will look like the start of the QUIC session. Similarly, if you add QUIC response header in front of struct message_handshake_response packet, the start of the WireGuard session will look like the start of the QUIC session.

Receiving WireGuard Packet

Opening the WireGuard code in the Linux kernel, you will find the file receive.c, where the reception of incoming packets is processed. In the prepare_skb_header function, the UDP header is removed from the skb buffer. In some cases, we will also need to remove the QUIC header. We will add a port number check and also take into account that all WireGuard packets start with a number not exceeding four, whereas the QUIC header starts with 0xC0. Thus, by adding a condition to check the first byte of the packet, we will be able to distinguish between initiation response and data packets.

Result

Here is an example of how Wireshark interprets the WireGuard handshake. Wireshark identifies the connection as QUIC. We have achieved the desired result.

Comments