Skip to content

How to create a single hop relay

Karel Donk edited this page Jan 1, 2022 · 10 revisions

Introduction

This tutorial will explain how to create a single hop relayed connection between two peers using QuantumGate. It's important to read the basics about relays first before you continue.

The following image shows the kind of connection we're going to create.

The following table shows the information for both peers.

Peer / Machine IP Address TCP Port
A 3.134.84.222 999
B 13.58.12.74 999

On both peers/machines we're going to be running a single instance of QuantumGate that will be configured to listen on TCP port 999 for incoming connections.

For a short example of how to start an instance of QuantumGate you can refer to the QuantumGateStartup example, and specifically the QuantumGateStartup.cpp file in that project.

Configuration Requirements

By default QuantumGate is configured to not allow relayed connections so we'll need to enable relays on both peers. We do this by adding one line in the above mentioned example as follows:

// Start extenders on startup
params.EnableExtenders = true;

// Add the following line on both machines to enable relays
params.Relays.Enable = true;

Because QuantumGate is configured by default to be completely locked down, we'll have to explicitly allow the peers A and B to accept connections from each other.

First we disable the requirement for authentication to make things simpler for this tutorial. In the above mentioned example you'll find the following lines that do this:

// For testing purposes we disable authentication requirement; when
// authentication is required we would need to add peers to the instance
// via QuantumGate::Local::GetAccessManager().AddPeer() including their
// UUID and public key so that they can be authenticated when connecting
params.RequireAuthentication = false;

Since we disabled authentication requirements, we'll need to configure QuantumGate to allow access to peers by default when they have not explicitly been added via the Access Manager. The following line does this:

// For testing purposes we allow access by default
qg.GetAccessManager().SetPeerAccessDefault(QuantumGate::Access::PeerAccessDefault::Allowed);

Next we need to explicitly allow the peers to accept connections from each other's IP addresses. This can be done, as in the example, by the following lines, which allow ALL IP addresses on both peers:

// For testing purposes we allow all IP addresses to connect;
// by default all IP Addresses are blocked
if (!qg.GetAccessManager().AddIPFilter(L"0.0.0.0/0", QuantumGate::Access::IPFilterType::Allowed) ||
    !qg.GetAccessManager().AddIPFilter(L"::/0", QuantumGate::Access::IPFilterType::Allowed))
{
    std::wcout << L"Failed to add an IP filter\r\n";
    return -1;
}

Depending on your use case this may not be desired, so we can configure QuantumGate on both peers to allow only each other's specific IP addresses. On peer A we would then need to do the following instead of the above:

// Allow only peer B's IP address on peer A
if (!qg.GetAccessManager().AddIPFilter(L"13.58.12.74/32",
                                       QuantumGate::Access::IPFilterType::Allowed))
{
    std::wcout << L"Failed to add an IP filter\r\n";
    return -1;
}

And on peer B we would need to do the following:

// Allow only peer A's IP address on peer B
if (!qg.GetAccessManager().AddIPFilter(L"3.134.84.222/32",
                                       QuantumGate::Access::IPFilterType::Allowed))
{
    std::wcout << L"Failed to add an IP filter\r\n";
    return -1;
}

Apart from configuring QuantumGate's security and access settings as shown above, it's important that you configure any firewall (for example the Windows Firewall) on both machines to allow accepting incoming TCP connections on port 999.

Making the connection

Now we're ready to connect peer A to peer B via a single hop relay. In the above example we'll be modifying the following code:

const auto result = qg.Startup(params);
if (result.Succeeded())
{
    std::wcout << L"Startup successful\r\nPress Enter to shut down";

    std::wcin.ignore();

    if (!qg.Shutdown())
    {
        std::wcout << L"Shutdown failed\r\n";
    }
}
else
{
    std::wcout << L"Startup failed (" << result << L")\r\n";
    return -1;
}

After QuantumGate has successfully started, we'll try to connect to peer B from peer A. In the code on peer A we will add the following lines to try to connect to peer B:

QuantumGate::ConnectParameters cparams;
cparams.PeerEndpoint = QuantumGate::IPEndpoint(QuantumGate::IPEndpoint::Protocol::TCP,
                                               QuantumGate::IPAddress(L"13.58.12.74"), 999);
cparams.Relay.Hops = 1;

const auto cresult = qg.ConnectTo(std::move(cparams),
    [](QuantumGate::PeerLUID pluid,
       QuantumGate::Result<QuantumGate::Peer> presult) mutable noexcept
    {
        if (presult.Succeeded())
        {
            std::wcout << L"Relay connected (peer LUID " << pluid << L")\r\n";
        }
        else std::wcout << L"Relay connection failed (" << presult << L")\r\n";
    }
);

if (!cresult)
{
    std::wcout << L"Failed to connect (" << cresult << L")\r\n";
}

See QuantumGate::Local::ConnectTo for more details about the connect function and the parameters used.

The complete code then becomes:

const auto result = qg.Startup(params);
if (result.Succeeded())
{
    std::wcout << L"Startup successful\r\nPress Enter to shut down";

    QuantumGate::ConnectParameters cparams;
    cparams.PeerEndpoint = QuantumGate::IPEndpoint(QuantumGate::IPEndpoint::Protocol::TCP,
                                                   QuantumGate::IPAddress(L"13.58.12.74"), 999);
    cparams.Relay.Hops = 1;

    const auto cresult = qg.ConnectTo(std::move(cparams),
        [](QuantumGate::PeerLUID pluid,
           QuantumGate::Result<QuantumGate::Peer> presult) mutable noexcept
        {
            if (presult.Succeeded())
            {
                std::wcout << L"Relay connected (peer LUID " << pluid << L")\r\n";
            }
            else std::wcout << L"Relay connection failed (" << presult << L")\r\n";
        }
    );

    if (!cresult)
    {
        std::wcout << L"Failed to connect (" << cresult << L")\r\n";
    }

    std::wcin.ignore();

    if (!qg.Shutdown())
    {
        std::wcout << L"Shutdown failed\r\n";
    }
}
else
{
    std::wcout << L"Startup failed (" << result << L")\r\n";
    return -1;
}

Running the program on peer A you will see output similar to the following in the QuantumGate console:

Startup successful
Press Enter to shut down
[07/01/2020 11:51:22] Connecting to peer 13.58.12.74:999 (Relayed)
[07/01/2020 11:51:22] Connecting to peer 13.58.12.74:999
[07/01/2020 11:51:22] Using peer LUID 6368341040317666277 as gateway for relay connection to peer 13.58.12.74:999
[07/01/2020 11:51:22] Connected to peer 13.58.12.74:999 (LUID 6368341040317666277)
[07/01/2020 11:51:23] No public key found to verify authentication signature from peer 13.58.12.74:999 (LUID 6368341040317666277) (UUID 9bfa9b63-878b-799f-8b0b-a997e8c92617)
[07/01/2020 11:51:23] Peer 13.58.12.74:999 (LUID 6368341040317666277) with UUID 9bfa9b63-878b-799f-8b0b-a997e8c92617 is NOT authenticated
[07/01/2020 11:51:23] Peer 13.58.12.74:999 (LUID 6368341040317666277) is ready
[07/01/2020 11:51:23] Relay link on port 10044460088979607967 connected (local hop 1)
[07/01/2020 11:51:23] Connected to peer 13.58.12.74:999:10044460088979607967:1 (LUID 1218565228574192743)
[07/01/2020 11:51:24] No public key found to verify authentication signature from peer 13.58.12.74:999:10044460088979607967:1 (LUID 1218565228574192743) (UUID 9bfa9b63-878b-799f-8b0b-a997e8c92617)
[07/01/2020 11:51:24] Peer 13.58.12.74:999:10044460088979607967:1 (LUID 1218565228574192743) with UUID 9bfa9b63-878b-799f-8b0b-a997e8c92617 is NOT authenticated
[07/01/2020 11:51:24] Peer 13.58.12.74:999:10044460088979607967:1 (LUID 1218565228574192743) is ready
Relay connected (peer LUID 1218565228574192743)

If you look at the output shown above, you'll see that QuantumGate is actually making 2 connections from peer A to peer B. First a normal connection is made from peer A to peer B and that connection receives the Locally Unique Identifier (LUID) 6368341040317666277. In the image on top of this tutorial it's the green connection from peer A to peer B.

After the first connection succeeds, QuantumGate then uses it to tunnel a second relayed connection. QuantumGate creates a relayed connection on randomly chosen port 10044460088979607967 and then makes the second connection using that port. The second connection receives the LUID 1218565228574192743. In the image on top of this tutorial it's the yellow connection from peer A to peer B. You can also see that the endpoint of the relayed connection is 13.58.12.74:999:10044460088979607967:1. This endpoint consists of the IP address, followed by the TCP port, followed by the relay port, followed by the number of hops (1 in this case).

From now on peer A can use the connection with LUID 1218565228574192743 to communicate with peer B via the single hop relay.

Bonus content

We've seen above that we created a normal connection (the green one) from peer A to B with LUID 6368341040317666277, through which we later tunneled a second connection (the yellow one) with LUID 1218565228574192743. It's actually possible to go further than this and use the second connection as a tunnel for a third relayed connection from peer A to peer B. In fact, there are no limits for how deep the levels of tunneling can go, apart from the resources and latency you are willing to spend.

If we wanted to create a third connection tunneling through the second one, here's the code we would have to use:

QuantumGate::ConnectParameters cparams;
cparams.PeerEndpoint = QuantumGate::IPEndpoint(QuantumGate::IPEndpoint::Protocol::TCP,
                                               QuantumGate::IPAddress(L"13.58.12.74"), 999);
cparams.Relay.Hops = 1;
cparams.ReuseExistingConnection = false;
cparams.Relay.GatewayPeer = QuantumGate::PeerLUID{ 1218565228574192743 };

const auto cresult = qg.ConnectTo(std::move(cparams),
    [](QuantumGate::PeerLUID pluid,
       QuantumGate::Result<QuantumGate::Peer> presult) mutable noexcept
    {
        if (presult.Succeeded())
        {
            std::wcout << L"Relay connected (peer LUID " << pluid << L")\r\n";
        }
        else std::wcout << L"Relay connection failed (" << presult << L")\r\n";
    }
);

if (!cresult)
{
    std::wcout << L"Failed to connect (" << cresult << L")\r\n";
}

Note that we've just added 2 lines to the connection code:

cparams.ReuseExistingConnection = false;

This tells QuantumGate not to use any existing 1 hop connections to the destination endpoint and create a new one. If this was set to true, which is the default, then QuantumGate would simply return the already existing 1 hop connection to peer B.

cparams.Relay.GatewayPeer = QuantumGate::PeerLUID{ 1218565228574192743 };

This line tells QuantumGate to use the second 1 hop relayed connection with LUID 1218565228574192743 as the gateway connection through which to tunnel the third 1 hop relayed connection.

Clone this wiki locally