The story of how the pet-project turned into a small passive income (part 2)
First part
DNS Balancing
The previous part ended with unsuccessful balancing, which does not solve almost any problems. In the comments, someone asked why I didn’t use DNS level balancing. So, I used it. It turned out that with the help of DNS records it is possible to organize Round Robin balancing. To do this, in the Wireguard configuration, you just need to use the domain name instead of the IP address. Now the Wireguard configuration will look like this:
[Interface]
PrivateKey = <client_private_key>
Address = <cient_address_on_server>/32
DNS = 8.8.8.8, 1.1.1.1
[Peer]
PublicKey = <server_private_key>
AllowedIPs = 0.0.0.0/0
Endpoint = domainName.com:<server_port>
The query schema would look something like this:

Pros and Cons of DNS Balancing
I have been using this balance for quite some time.
Pros:
There is no need to have your own server for balancing, therefore, you do not need to pay for additional traffic and additional servers.
To add/remove new servers, just add/remove an A record from your DNS provider.
Minuses:
When adding a new server, there can be a large time lag, as it takes some time before the DNS servers pull up updated information.
When deleting a server, you must first delete the DNS record and only after a while you can extinguish the server, since not all DNS servers will have time to remove the record of the old IP address.
If one of our servers goes down, the DNS record will not be updated automatically, and some of the users will try to connect to the server that is not working
Balancing does not occur in an optimal way, since the Round Robin algorithm is used.
About getting the user configuration

actions table:
id | int64 (unique identifier for each entry) |
action | ENUM (1 – connect user; 2 – disconnect user) |
user_id | uuid (unique user ID) |
timestamp | timestamptz (time the record was created) |
users table:
id | uuid (unique user ID) |
chat_id | int64 (unique user id in Telegram) |
public_key | text (Wireguard public key) |
private_key | text (Wireguard private key) |
wireguard_ip | text (unique ip address of each user inside the Wireguard interface) |
subcription_end | timestamptz (time when the user’s subscription expires) |
As I mentioned in the last part, I have Master And slave hosts:
Master includes:
TelegramBot – Responsible for user interaction. Monitors the status of the subscription, accepts payments from users, registers new users, returns the VPN connection configuration to the user.
SlavePingWorker – Responsible for checking the health of the servers. Every few minutes it pings everything slave. If slave does not respond, an ALERT is sent.
PostgreSQL – stores user data and table actions.
server – gives slave record hosts from the table actions.
slave includes:
Consider the steps in Fig. 2
The user makes a request to TelegramBot to get the configuration.
TelegramBot gets chat_id user. Next, it generates public_key, private_key, wireguard_ip and adds all this data to the users table. Also TelegramBot makes the following entry in the actions table:
id | action | user_id | timestamp |
1 | 1 (connect user) |
The user configuration is created and returned to the user as a .conf file.
Slave1Worker and Slave2Worker make a request to the Master every 5 seconds to get fresh records from the actions table. If action = 1, then user keys are added to Wireguard, if action = 2, then user keys are removed.
Safety
Since the master and slave hosts send requests to the IP address (it is inconvenient to use domain names, since we have balancing by the domain name), there is no way to use SSL. Because of this, there are 2 vulnerabilities associated with Man-in-the-middle attack.
Data transmitted between master and slave can be read (an attacker can steal the user’s keys and use the VPN instead).
Requests between master and slave can be intercepted and then resent. Since the master and slave API is not idempotentit is possible to change the internal state of the system and cause errors.
It was decided to encrypt all requests using the RSA key. For this, private/public key pairs are created. It looks like this:

The request is encoded with master_private_key
The request goes to the external network
Request comes to slave host
The request is decoded with master_public_key
Response is formed
The response is encoded with slave_private_key
The response goes to the external network
Master receives a response
The response is decoded with slave_public_key and handled in responseHandler
Encryption of requests between servers makes it impossible to read the transmitted data. In order to protect against re-sending the request, it was decided to add a timestamp to the request. In this case, upon receipt of a request, you can check when the request was signed, and, if the signature is old, then reject such requests. I decided to use a signature deadline of 5 seconds. This turned out to be sufficient to ensure that the requests had time to reach the addressee.
Next, there will be another article in which I will tell you more about the organization of the code in my project, github actions, about the delegation of balancing to clients, and about what plans I have for the future.