📜 ⬆️ ⬇️

Implementation of an asynchronous secure communication system based on TCP sockets and central OpenVPN server

In this article I want to tell you about my implementation of the messenger with double encryption of messages based on tcp sockets, OpenVPN server, PowerDNS server.
The essence of the task is to provide Internet messaging bypassing NAT between clients on different platforms (IOS, Android, Windows, Linux). It is also necessary to ensure the security of transmitted data.

My system consists of:


On client terminals, a tcp socket is raised that listens to a specific port for incoming messages. If the message arrives, the socket outputs it to the terminal. The message itself contains the sender's username. The tcp client socket is also opened for sending messages.
The socket opens directly with the remote client terminal. Therefore, in my case, the interaction is in client-client mode.
The search for customers occurs by username. The username and IP addresses are stored in the PowerDNS server.
PowerDNS is a high-performance DNS server written in C ++ and licensed under the GPL license. Development is carried out in the framework of support for Unix-systems; Windows systems are no longer supported.
The server was developed at the Dutch company PowerDNS.com by Bert Hubert and is maintained by the free software community.
PowerDNS uses a flexible data storage / access architecture that can retrieve DNS information from any data source. This includes files, BIND zone files, relational databases, or LDAP directories.
PowerDNS is configured by default to serve requests from the database.
After the release of version 2.9.20, the software is distributed in the form of two components - (Authoritative Server) (authoritative DNS) and Recursor (recursive DNS).
The choice of this solution is due to the fact that PowerDNS is able to work with the database without connecting additional modules, as well as a higher speed of work in comparison with other freely distributed solutions.
For my purposes, only an authoritative module was enough for me;

Client terminals communicate with all internal components through the SOAP Gateway.
The logic of work is as follows - the client includes the program, the SOAP method is executed on the update zone on the PowerDNS server. If a client wants to contact someone, he enters or selects the corresponding username in the list, performs a SOAP method to obtain an IP address from the DNS database, and connects to the remote client by IP address.
I have written ready-made clients for IOS, Android, Windows. When writing them, I used the xamarin framework. Very convenient, only small code changes were required to transfer the application to another platform.
')
Next, I will present the client and server socket codes that I use on the client terminals. Here are the codes for iOS. For Android and Windows will be almost like that. The difference is only in different types of elements (buttons, text blocks, etc.)

Server tcp socket code
public class GlobalFunction { public static void writeLOG(string loggg) { //        string path = @"bin\logfile.log"; string time = DateTime.Now.ToString("hh:mm:ss"); string date = DateTime.Now.ToString("yyyy.MM.dd"); string logging = date + " " + time + " " + loggg; using (StreamWriter sw = File.AppendText(path)) { sw.WriteLine(logging); } } public static void writeLOGdebug(string loggg) { try { //        string path = @"bin\logfile.log"; string time = DateTime.Now.ToString("hh:mm:ss"); string date = DateTime.Now.ToString("yyyy.MM.dd"); string logging = date + " " + time + " " + loggg; using (StreamWriter sw = File.AppendText(path)) { sw.WriteLine(logging); } } catch (Exception exc) { } } } public class Globals { public static IPAddress localip = "192.168.88.23"; public static int _localServerPort = 19991; public const int _maxMessage = 100; public static string _LocalUserName = "375297770001"; public struct MessBuffer { public string usernameLocal; public string usernamePeer; public string message; } public static List<MessBuffer> MessagesBase = new List<MessBuffer>(); public static List<MessBuffer> MessagesBaseSelected = new List<MessBuffer>(); } public class StateObject { // Client socket. public Socket workSocket = null; // Size of receive buffer. public const int BufferSize = 1024; // Receive buffer. public byte[] buffer = new byte[BufferSize]; // Received data string. public StringBuilder sb = new StringBuilder(); } public partial class ViewController : UIViewController { public static ManualResetEvent allDone = new ManualResetEvent(false); public void startLocalServer() { //IPHostEntry ipHost = Dns.GetHostEntry(_serverHost); //IPAddress ipAddress = ipHost.AddressList[0]; IPAddress ipAddress = Globals.localip; IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, Globals._localServerPort); Socket socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); socket.Bind(ipEndPoint); socket.Listen(1000); GlobalFunction.writeLOGdebug("Local Server has been started on IP: " + ipEndPoint); while (true) { try { // Set the event to nonsignaled state. allDone.Reset(); // Start an asynchronous socket to listen for connections. socket.BeginAccept( new AsyncCallback(AcceptCallback), socket); // Wait until a connection is made before continuing. allDone.WaitOne(); } catch (Exception exp) { GlobalFunction.writeLOGdebug("Error. Failed startLocalServer() method: " + Convert.ToString(exp)); } } } public void AcceptCallback(IAsyncResult ar) { // Signal the main thread to continue. allDone.Set(); // Get the socket that handles the client request. Socket listener = (Socket)ar.AsyncState; Socket handler = listener.EndAccept(ar); // Create the state object. StateObject state = new StateObject(); state.workSocket = handler; handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); } public void ReadCallback(IAsyncResult ar) { String content = String.Empty; // Retrieve the state object and the handler socket // from the asynchronous state object. StateObject state = (StateObject)ar.AsyncState; Socket handler = state.workSocket; // Read data from the client socket. int bytesRead = handler.EndReceive(ar); if (bytesRead > 0) { // There might be more data, so store the data received so far. state.sb.Append(Encoding.UTF8.GetString( state.buffer, 0, bytesRead)); // Check for end-of-file tag. If it is not there, read // more data. content = state.sb.ToString(); if (content.IndexOf("<EOF>") > -1) { // All the data has been read from the // client. Display it on the console. string[] bfd = content.Split(new char[] { '|' }, StringSplitOptions.None); string decrypt = MasterEncryption.MasterDecrypt(bfd[0]); string[] bab = decrypt.Split(new char[] { '~' }, StringSplitOptions.None); Globals.MessBuffer Bf = new Globals.MessBuffer(); Bf.message = bab[2]; Bf.usernamePeer = bab[0]; Bf.usernameLocal = bab[1]; string upchat_m = "[" + bab[1] + "]# " + bab[2]; this.InvokeOnMainThread(delegate { frm.messageField1.InsertText(Environment.NewLine + "[" + bab[1] + "]# " + bab[2]); }); //if (Ok != null) { Ok(this, upchat_m); } Globals.MessagesBase.Add(Bf); //GlobalFunction.writeLOGdebug("Received message: " + content); // Echo the data back to the client. //Send(handler, content); } else { handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback), state); } } } } 




The server is started in a separate thread, for example, like this:

  public static class Program { private static Thread _serverLocalThread; /// <summary> /// The main entry point for the application. /// </summary> /// [STAThread] static void Main() { _serverLocalThread = new Thread(GlobalFunction.startLocalServer); _serverLocalThread.IsBackground = true; _serverLocalThread.Start(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } 


Client's tcp socket code
  public override void DidReceiveMemoryWarning() { base.DidReceiveMemoryWarning(); // Release any cached data, images, etc that aren't in use. } private void connectToRemotePeer(IPAddress ipAddress) { try { IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, Globals._localServerPort); _serverSocketClientRemote = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _serverSocketClientRemote.Connect(ipEndPoint); GlobalFunction.writeLOGdebug("We connected to: " + ipEndPoint); } catch (Exception exc) { GlobalFunction.writeLOGdebug("Error. Failed connectToRemotePeer(string iphost) method: " + Convert.ToString(exc)); } } private void sendDataToPeer(string textMessage) { try { byte[] buffer = Encoding.UTF8.GetBytes(textMessage); int bytesSent = _serverSocketClientRemote.Send(buffer); GlobalFunction.writeLOGdebug("Sended data: " + textMessage); } catch (Exception exc) { GlobalFunction.writeLOGdebug("Error. Failed sendDataToPeer(string testMessage) method: " + Convert.ToString(exc)); } } private void Client_listner() { try { while (_serverSocketClientRemote.Connected) { byte[] buffer = new byte[8196]; int bytesRec = _serverSocketClientRemote.Receive(buffer); string data = Encoding.UTF8.GetString(buffer, 0, bytesRec); //string data1 = encryption.Decrypt(data); if (data.Contains("#updatechat")) { //UpdateChat(data); GlobalFunction.writeLOGdebug("Chat updated with: " + data); continue; } } } catch (Exception exc) { GlobalFunction.writeLOGdebug("Error. Failed Client_listner() method: " + Convert.ToString(exc)); } } private void sendMessage() { try { connectToRemotePeer(Globals._peerRemoteServer); _RemoteclientThread = new Thread(Client_listner); _RemoteclientThread.IsBackground = true; _RemoteclientThread.Start(); string data = inputTextBox.Text; Globals.MessBuffer ba = new Globals.MessBuffer(); ba.usernameLocal = Globals._LocalUserName; ba.usernamePeer = Globals._peerRemoteUsername; ba.message = data; Globals.MessagesBase.Add(ba); if (string.IsNullOrEmpty(data)) return; string datatopeer = Globals._peerRemoteUsername + "~" + Globals._LocalUserName + "~" + data; string datatopeerEncrypted = MasterEncryption.MasterEncrypt(datatopeer); sendDataToPeer(datatopeerEncrypted + "|<EOF>"); addLineToChat(data, Globals._LocalUserName); inputTextBox.Text = string.Empty; } catch (Exception exp) { GlobalFunction.writeLOGdebug(Convert.ToString(exp)); } } private void addLineToChat(string msg, string username) { messageField1.InsertText(Environment.NewLine + "[" + username + "]# " + msg); } public void addFromServer(string msg, string username) { messageField1.InsertText(Environment.NewLine + "[" + username + "]# " + msg); } private void listBox1_Click() { messageField1.Text = ""; Globals.MessagesBaseSelected.Clear(); GlobalFunction.ReloadLocalBufferForSelected(); for (int i = 0; i < Globals.MessagesBaseSelected.Count; i++) { messageField1.InsertText(Environment.NewLine + "[" + Globals.MessagesBaseSelected[i].usernameLocal + "]# " + Globals.MessagesBaseSelected[i].message); } Globals._peerRemoteServer = GlobalFunction.getPEERIPbySOAPRequest(Globals._peerRemoteUsername); string Name = Globals._LocalUserName; GlobalFunction.writeLOGdebug("Local name parameter listBox1_DoubleClick: " + Name); connectToRemotePeer(Globals._peerRemoteServer); _RemoteclientThread = new Thread(Client_listner); _RemoteclientThread.IsBackground = true; _RemoteclientThread.Start(); } public static ViewController Form; 



OpenVPN server is needed to overcome NAT, as well as for additional data protection.
In the case of OpenVPN, we have some kind of addressing inside the tunnel, through which customers can communicate.
I will not describe the installation process of the OpenVPN server, as there is a lot of information on this topic. I will give only my configuration. It is fully optimized for my purposes and fully working (copied from the working server as it is, unchanged, only the IP in the client configuration was changed to fake, instead of 1.1.1.1 you need to specify the IP address of your OpenVPN server).
OpenVPN conducts all network operations via TCP or UDP transport. In general, UDP is preferred because the tunnel passes through network layer traffic and higher on OSI, if a TUN connection is used, or link layer traffic and above, if TAP is used. This means that OpenVPN for the client acts as a channel or even physical layer protocol, which means that data transfer reliability can be provided by OSI higher layers, if necessary. That is why the UDP protocol is closest to OpenVPN in its concept, since he, like the channel and physical layer protocols, does not ensure the reliability of the connection, passing this initiative to higher levels. If you configure the tunnel to work on TCP, the server will typically receive OpenVPN TCP segments that contain other TCP segments from the client. As a result, the chain produces a double check for the integrity of information, which makes no sense at all, because reliability does not increase, and connection and ping speeds decrease.

My OpenVPN Server Configuration
port 8443
proto udp
dev tun

cd / etc / openvpn
persist-key
persist tun
tls-server
tls-timeout 120

#certificates and encryption
dh /etc/openvpn/keys/dh2048.pem
ca /etc/openvpn/keys/ca.crt
cert /etc/openvpn/keys/server.crt
key /etc/openvpn/keys/server.key
tls-auth /etc/openvpn/keys/ta.key 0
auth SHA512
cipher AES-256-CBC
duplicate-cn
client-to-client

#DHCP information
server 10.0.141.0 255.255.255.0
topology subnet
max-clients 250
route 10.0.141.0 255.255.255.0

#any
mssfix
float
comp-lzo
mute 20

#log and security
user nobody
group nogroup
keepalive 10 120
status /var/log/openvpn/openvpn-status.log
log-append /var/log/openvpn/openvpn.log
client-config-dir / etc / openvpn / ccd
verb 3


My OpenVPN Client Configuration
client
dev tun
proto udp
remote 1.1.1.1 8443
tls-client
#ca ca.crt
#cert client1.crt
#key client1.key
key-direction 1
# tls-auth ta.key 1
auth SHA512
cipher AES-256-CBC
remote-cert-tls server
comp-lzo
tun-mtu 1500
mssfix
ping-restart 180
persist-key
persist tun
auth-nocache
verb 3

I am waiting for comments on this OpenVPN configuration.

All the requests from the client terminals are carried out at the tunnel address on the side of the OpenVPN server, where the corresponding port forwarding and necessary access are configured.
Here is an example:
 iptables -A INPUT -i ens160 -p tcp -m tcp --dport 8443 -j ACCEPT iptables -A INPUT -i ens160 -p udp -m udp --dport 8443 -j ACCEPT iptables -A INPUT -m state --state ESTABLISHED -j ACCEPT iptables -A INPUT -i ens192 -j ACCEPT iptables -P INPUT DROP iptables -t nat -A PREROUTING -p tcp -m tcp -d 10.0.141.1 --dport 443 -j DNAT --to-destination 10.24.184.179:443 #iptables -t nat -A PREROUTING -p udp -m udp -d 10.0.141.1 --dport 53 -j DNAT --to-destination 10.24.214.124:53 


DNS server for me serves as a kind of storage of IP-username bindings, and not only.
Also here is the configuration of my PowerDNS server. I can only say that in terms of security, it is not very optimized, it would be possible to resolve only the corresponding addresses, but also fully working. I copied from the working server, only replacing logins / passwords / addresses with fake ones.

Configuration of my PowerDNS authoritative server
launch = gmysql
gmysql-host = 10.24.214.131
gmysql-user = powerdns
gmysql-password = password
gmysql-dbname = powerdns
gmysql-dnssec = yes
allow-axfr-ips = 0.0.0.0 / 0
allow-dnsupdate-from = 0.0.0.0 / 0
allow-notify-from = 0.0.0.0 / 0
api = yes
api-key = 1234567890
# api-logfile = / var / log / pdns.log
api-readonly = no
cache-ttl = 2000
daemon = yes
default-soa-mail = dbuynovskiy.spectrum.by
disable-axfr = yes
guardian = yes
local-address = 0.0.0.0
local-port = 53
log-dns-details = yes
log-dns-queries = yes
logging facility = 0
loglevel = 7
master = yes
dnsupdate = yes
max-tcp-connections = 512
receiver-threads = 4
retrieval-threads = 4
reuseport = yes
setgid = pdns
setuid = pdns
signing-threads = 8
slave = no
slave-cycle-interval = 60
version-string = powerdns
webserver = yes
webserver-address = 0.0.0.0
webserver-allow-from = 0.0.0.0 / 0.10.10.71.0 / 24.10.10.55.4 / 32
webserver-password = 1234567890
webserver-port = 7777
webserver-print-arguments = yes
write-pid = yes



To update the PowerDNS zones I used the built-in REST API. I wrote the following procedure in Ruby:
PowerDNS zone update method via REST
  def updatezone(username,ipaddress) p data = {"rrsets": [ {"name": "#{username}.spectrum.loc.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": ipaddress, "disabled": false } ] } ] } url = 'http://10.24.214.124:7777/api/v1/servers/localhost/zones/spectrum.loc.' uri = URI.parse(url) http = Net::HTTP.new(uri.host, uri.port) req = Net::HTTP::Patch.new(uri.request_uri) req["X-API-Key"]="1234567890" req.body = data.to_json p "fd" p response = http.request(req) p content = response.body end 


I execute this method when starting the client. That is, there is an update of the IP address corresponding to a specific user. I remind you that such requests are performed by SOAP Gateway written in Ruby.

My information transmitted between sockets is encrypted using the AES algorithm, but not by a separate password, but by a randomly selected password from the list, which provides almost absolute protection, even if the attacker has infinite computing resources. Of course, the longer the list, the better.
I have a method for doing this encryption / decryption.

In addition, I want to leave here an example of my c # procedure for performing SOAP requests.
Maybe someone will come in handy. In any case, I have long sought to make SOAP requests run on different platforms. At first I used the Service Reference on Windows, but for Xamarin for other platforms it is not available. And this technique works everywhere. Anyway, I tested it on iOS, Android and Windows

An example of executing a SOAP request on c #
  public static void registerSession2() { try { CallWebServiceUpdateLocation(); writeLOG("Session registered on SoapGW."); } catch (Exception exc) { writeLOG("Error. Failed GlobalFunction.registerSession2() method: " + Convert.ToString(exc)); } } private static HttpWebRequest CreateWebRequest(string url, string action) { HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(url); webRequest.Headers.Add("SOAPAction", action); webRequest.ContentType = "text/xml;charset=\"utf-8\""; webRequest.Accept = "text/xml"; webRequest.Method = "POST"; return webRequest; } private static XmlDocument CreateSoapEnvelope() { XmlDocument soapEnvelopeDocument = new XmlDocument(); string xml = System.String.Format(@" <soapenv:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xmlns:xsd=""http://www.w3.org/2001/XMLSchema"" xmlns:soapenv=""http://schemas.xmlsoap.org/soap/envelope/"" xmlns:spec=""https://spectrum.master""> <soapenv:Header/> <soapenv:Body> <master_update_location soapenv:encodingStyle=""http://schemas.xmlsoap.org/soap/encoding/""> <ipaddress xsi:type=""xsd:string"">{0}</ipaddress> <username xsi:type=""xsd:string"">{1}</username> </master_update_location> </soapenv:Body> </soapenv:Envelope> ", Convert.ToString(Globals.localip), Globals._LocalUserName); soapEnvelopeDocument.LoadXml(xml); return soapEnvelopeDocument; } public static void CallWebServiceUpdateLocation() { var _url = "http://10.0.141.1/master/action"; var _action = "master_update_location"; XmlDocument soapEnvelopeXml = CreateSoapEnvelope(); HttpWebRequest webRequest = CreateWebRequest(_url, _action); InsertSoapEnvelopeIntoWebRequest(soapEnvelopeXml, webRequest); // begin async call to web request. IAsyncResult asyncResult = webRequest.BeginGetResponse(null, null); // suspend this thread until call is complete. You might want to // do something usefull here like update your UI. asyncResult.AsyncWaitHandle.WaitOne(); // get the response from the completed web request. string soapResult; using (WebResponse webResponse = webRequest.EndGetResponse(asyncResult)) { using (StreamReader rd = new StreamReader(webResponse.GetResponseStream())) { soapResult = rd.ReadToEnd(); } //Console.Write(soapResult); } } private static void InsertSoapEnvelopeIntoWebRequest(XmlDocument soapEnvelopeXml, HttpWebRequest webRequest) { using (Stream stream = webRequest.GetRequestStream()) { soapEnvelopeXml.Save(stream); } } 



PS Thank you all for your attention! I hope that someone will be useful if not an idea, then at least examples of procedures. Write in the comments your thoughts about this messaging implementation.

Some literature:
1. Useful article. I used it myself. True, the configuration proposed in it did not fit me, but the installation process is very detailed - Setting up an OpenVPN server on Ubuntu 16.04
2. Install PowerDNS
3. Details on Xamarin

Source: https://habr.com/ru/post/346792/


All Articles