Libtelio
A library providing networking utilities for NordVPN VPN and meshnet functionality
Install / Use
/learn @NordSecurity/LibtelioREADME
libtelio
Overview
Libtelio (pronounced 'lɪbtælɪɔ') is a client-side library for creating encrypted networks (called Meshnet) on the user's nodes. It supports a number of features, including:
- adapting different Wireguard implementations to secure the connections,
- exit nodes which might be either VPN servers or some nodes in the Meshnet,
- "DNS" which allows name resolution in Meshnets for devices which does not support lookup tables,
- adjustable firewall.
Getting started
Prerequisites
In the latter sections of this README we assume (if it's not explicitly assumed otherwise)
that you are working on a Linux machine. To be able to build libtelio
it is enough to have rust installed.
To go through the short TCLI tutorial you need also docker.
Build
You can build the libtelio library using standard cargo build command. Ensure to set the BYPASS_LLT_SECRETS environment variable to skip the LLT scan:
BYPASS_LLT_SECRETS=1 cargo build
Linux toolchain
Verify that GCC (GNU Compiler Collection) has been installed:
gcc --version
Otherwise run following command to install it:
sudo apt update
sudo apt install gcc
Windows msvc toolchain
To build libtelio on Windows with x86_64-pc-windows-msvc toolchain you need to install:
- Visual Studio 2019/2022
- Additional Visual Studio components: a. Desktop development with C++ b. Python 3 64-bit c. C++ Clang tools for Windows
- TDM-GCC 64-bit (https://jmeubank.github.io/tdm-gcc/download/)
- Go 1.19
Before running cargo build you need to set msvc environment. Examples for cmd and powershell in case of Visual Studio 2019 Community:
- In cmd.exe run:
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvars64.bat" x64
<!-- markdownlint-disable-next-line MD029 -->
- In powershell run:
Import-Module "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
Enter-VsDevShell -VsInstallPath "C:\Program Files (x86)\Microsoft Visual Studio\2019\Community" -SkipAutomaticLocation
Dependencies
Cmake
Verify that you have cmake installed and visible in the path, for example:
cmake --version
cmake version 3.22.1
Otherwise, run the following command to install it (or other appropriate for your system):
sudo apt update
sudo apt install cmake
libclang
Verify that you have a libclang installed on your system:
find /usr/lib -name "libclang*\.so"
Otherwise, run the following command to install it (or other appropriate for your system):
sudo apt update
sudo apt install libclang-dev
Protocol buffers compiler
Verify that protoc (Protocol buffers compiler has been installed in correct version (at least 3.21.12):
protoc --version
Otherwise, run the following command to install it (or other appropriate for your system):
sudo apt update
sudo apt install protobuf-compiler-grpc
Setting up Meshnet with tcli
tcli is a simple shell created to test and discover the libtelio library capabilities.
Let's see how to use it to create a simple mesh connection between two docker containers.
First of all, build tcli utility:
cargo build -p tcli
You will need a lightweight Linux docker image with the some networking utilities,
which are missing from the basic Ubuntu image, so let's create a new one.
Make a docker directory in tcli-test:
mkdir -p tcli-test/docker
cd tcli-test/docker
and put there the following simple Dockerfile:
FROM ubuntu
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y iproute2 iputils-ping tcpdump ca-certificates
Then build it and tag it as tcli-test, running the following command from the docker directory:
docker build -t tcli-test .
When the image is built, you need to run two copies of it, one for t1:
docker run -itd -v <path to libtelio top-level-directory>:/hostfs --name=t1 --hostname=t1 --privileged tcli-test bash
and a second one for t2:
docker run -itd -v <path to libtelio top-level-directory>:/hostfs --name=t2 --hostname=t2 --privileged tcli-test bash
You need to prepare four terminals. In two of them (we will refer to them as T1a and T1b)
run the following command to connect to the container t1:
docker exec -it t1 bash
and then run an analogous command for t2 in another two (T2a and T2b):
docker exec -it t2 bash
In the following steps, you will need a token for your NordVPN account
(if you don't have an account, you need to create one),
which you can generate from https://my.nordaccount.com/dashboard/nordvpn/.
In terminals T1a and T2a run tcli:
hostfs/target/debug/tcli
and in both of the opened tclis:
login token <NORDVPN_TOKEN>
mesh on <NAME>
where <NORDVPN_TOKEN> is the token generated for your NordVPN account and
<NAME> is t1 for T1a and t2 for T2a.
There will be a large JSON config printed - the IP address can be found in
the list "ip_addresses":
>>> mesh on t1
- registered new device.
- got config:
{"identifier":"...","public_key":"...","hostname":"...","os":"linux","os_version":"linux tcli",<span style="color:red">"ip_addresses":["..."]</span>,"traffic_routing_supported":false,"endpoints":["..."],...
When you find it set it in your bash terminals
(set the one found in config in T1a in T1b and the one from T2a in T2b):
ip addr add <IP_ADDRESS>/10 dev <NAME>
ip -6 addr add <IPv6_ADDRESS>/64 dev <NAME>
ip link set up dev <NAME>
ip link set dev <NAME> mtu 1420
Note: for meshnet to work, you do not need both IPv4 and IPv6 addresses to be set. Only one of them should be enough.
Currently, there is one more issue to overcome: because node t1 was connected
earlier, it doesn't have the information about node t2.
Run mesh on t1 in T1a to fix it.
The containers should be now connected by the mesh, so to try the connection,
run ping in T1b and tcpdump in T2b and see how the packages are flowing.
Running meshnet on macOS
To run tcli client on native macOS use utun name for interface name instead of t1/t2. Use unique index for utun since there might be some already present.
>>> login token <NORDVPN_TOKEN>
>>> mesh on utun10
Find meshnet ip address from "ip_addresses" field the same as in linux case. Then
ifconfig utun10 add <IP_ADDRESS>/10 <IP_ADDRESS>
ifconfig inet6 utun10 add <IPv6_ADDRESS> prefixlen 64
ifconfig utun10 mtu 1420
route add 100.64/10 <IP_ADDRESS>
route add -inet6 fd74:656c:696f::/64 <IPv6_ADDRESS>
Or simply run:
>>> mesh set-ip
Using the libtelio API
Initializing the telio device
The main component of the Libtelio library is the telio::Device structure and its methods.
Let's go through the most important ones.
The first of them is the expected function ::new.
It takes three arguments, let's describe them briefly:
features- tells Libtelio which of the additional features should be enabled - the description of them is out of the scope of this READMEevent_cb- event handler, takes aBox<Event>, which will be called by Libtelio to handle events of three types:Error- an error occurs, especially urgent are Critical error events, which means that the library is unable to continue running and it requires a call tohard_resetmethodRelay- when the relay (i.e. Derp) server configuration is changed, contains a JSON with a new oneNode- appears when the Meshnet node's configuration is changed, it contains a JSON with a new one
protect- callback for excluding connections from VPN tunnel (currently used only for android).
telio::Device implements Drop trait, so we don't need to worry
about deinitialization in the end.
Let's look at an example initialization of telio::Device with no additional
features, handling only Node events and not using protect callback:
let (sender, receiver) = mpsc::channel::<Box<Event>>();
let mut device = telio::device::Device::new(
Features::default(),
move |e| {
sender.send(e).unwrap();
},
None,
).unwrap();
...
loop {
let event = receiver.recv().unwrap();
match *event {
Event::Node { body: Some(b) } => println!(
"event node: {:?}:{}; Path = {:?}",
b.state.unwrap(),
b.public_key,
b.path
),
_ => {}
};
}
Starting the Telio device
Once the device is initialized we can start it, so it will create a new network
interface in your OS. This might be done using start method.
We need to provide an instance of the DeviceConfig structure:
pub struct DeviceConfig {
pub private_key: SecretKey,
pub adapter: AdapterType,
pub name: Option<String>,
pub tun: Option<Tun>,
}
Let's discuss its fields shortly:
private_keyatelio::crypto::SecretKeyinstance containing a 256-bit key,adapterindicating which Wireguard implementation we want to use,nameis the name of the network interface, when omitted, Telio uses the default one,tuna file descriptor of the already opened tunnel, if it's not provided Telio will open a new one.
The API provides a default config which is almost sufficient for simple cases, the only need that needs to be done is the generation of a privat
