Gnirehtet for developers
You need the Android SDK (Android Studio) and the JDK 8 (
You also need the Rust environment to build the Rust version:
wget https://sh.rustup.rs -O rustup-init sh rustup-init
gradle is installed on your computer:
Otherwise, you can call the gradle wrapper:
This will build the Android application, the Java and Rust relay servers, both in debug and release versions.
Several gradle tasks are exposed in the root project. For instance:
releaseJavabuild the Android application and the Java relay server;
releaseRustbuild the Android application and the Rust relay server.
Even if the Rust build tasks are exposed through
gradle (which wraps calls to
cargo), it is often more convenient to use
For instance, to build a release version of the Rust relay server:
cd relay-rust cargo build --release
It will generate the binary in
Cross-compile the Rust relay server from Linux to Windows
gnirehtet.exe from Linux, install the cross-compile toolchain (on
sudo apt install gcc-mingw-w64-x86-64 rustup target add x86_64-pc-windows-gnu
Add the following lines to
[target.x86_64-pc-windows-gnu] linker = "x86_64-w64-mingw32-gcc" ar = "x86_64-w64-mingw32-gcc-ar"
cargo build --release --target=x86_64-pc-windows-gnu
It will generate
To import the project in Android Studio: File → Import…
From there, you can develop on the Android application and the Java relay server. You can also execute any gradle tasks, and run the tests with visual results.
The client registers itself as a VPN, in order to intercept the whole device network traffic.
It exchanges raw IPv4 packets as
byte with the device:
- it receives packets from the Android applications or system;
- it must forge response packets.
The client (executed on the Android device) just maintains a TCP connection to the relay server, and sends the raw packets to it.
This TCP connection is established over adb, after we started a reverse port redirection:
adb reverse localabstract:gnirehtet tcp:31416
This means that every connection initiated to
localhost:31416 from the device
will be redirected to the port
31416 on the computer, on which the relay
server is listening.
The relay server does all the hard work. It receives the IP packets from every connected client and opens standard sockets (which, of course, don't require root) accordingly, then relays data in both directions. This requires to translate packets between level 3 (on the device side) and level 5 (on the network side) in the OSI model.
In a sense, the relay server behaves like a NAT (more precisely a port-restricted cone NAT), in that it opens connections on behalf of private peers. However, it differs from a standard NAT in the way it communicates with the clients (the private peers), by using a very specific (though simple) protocol, over a TCP connection.
The client is an Android project located in
Some configuration options may be passed as extra parameters, converted to a
VpnConfiguration instance. Currently, the user can configure the DNS servers
The very first time, Android requests to the user the permission to enable the
VPN. In that case, the API requires to call
startActivityForResult, so we need an
Activity: this is the purpose
RelayTunnel manages one connection to the relay server.
RelayTunnel instances to handle
reconnections, so that we can stop and start the relay while the client keeps
To send response packets to the system, we must write one packet at a time to
the VPN interface. Since we receive packets from the relay server over a TCP
connection, we have to split writes at packet boundaries: this is the purpose
The relay server comes in two flavors:
- the Java version is a Java 8 project located in
- the Rust version is a Rust project located in
There are different socket channels registered to a unique selector:
- one for the server socket, listening on port 31416;
- one for each client, accepted by the server socket;
- one for each TCP connection to the network;
- one for each UDP connection to the network.
Initially, only the server socket channel is registered.
In Java, the channels (
registered to the selector (
Selector) defined in
Relay, with their
attachment (for better decoupling). A
is created for every accepted client.
In Rust, our own
Selector class wraps the
Poll from mio to expose an API accepting event handlers instead
of low-level tokens. The selector instance is created in
Relay. The channels are called "handles" in mio; they are
simply the socket instances themselves (
Client is created for every accepted client.
Each client manages a TCP socket, used to transmit raw IP packets from and to the Gnirehtet Android client. Thus, these IP packets are encapsulated into TCP (they are transmitted as the TCP payload).
When a client connects, the relay server assigns an integer id to it, which it
writes to the TCP socket. The client considers itself connected to the relay
server only once it has received this number. This allows to detect any
end-to-end connection issue immediately. For instance, a TCP connect initiated
by a client succeeds whenever a port redirection is enabled (typically through
adb reverse), even if the relay server is not listening. In that case, the
first read will fail.
A class representing an IPv4 packet
Ipv4Packet) provides a
structured view to read and write packet data, which is physically stored in the
buffers (the little squares on the schema). Since we handle one packet at a time
with asynchronous I/O, there is no need to copy or synchronize access to the
packets data: the packets just point to the buffer where they are stored.
Each packet contains an instance of IPv4 headers and transport headers (which might be TCP or UDP headers).
In Rust, the borrowing rules prevent to share a mutable reference.
Therefore, header data classes (
*HeaderData) are used to store the fields,
and lifetime-bound views (
*HeaderMut<'a>) reference both
the raw array and the header data:
- mutable view:
- mutable view:
- mutable view:
In addition, we use enums for transport headers to statically dispatch calls to UDP and TCP header classes:
- mutable view:
- source address
- source port
- destination address
- destination port
A connection (
Connection) is either a TCP connection
or a UDP connection (
UdpConnection) to the requested destination. It
registers its own channel to the selector.
The connection is responsible for converting data from level 3 to level 5 for device-to-network packets, and from level 5 to level 3 for network-to-device packets. For UDP connections, it consists essentially in removing or adding IP and transport headers. For TCP connections, however, it requires to respond to the client according to the TCP protocol (RFC 793), in such a way as to ensure a correct end-to-end communication.
When the first packet for a specific UDP connection is received from the device,
UdpConnection is created. It keeps a copy of the IP and UDP headers
of this first packet, swapping the source and the destination, in order to use
them as headers for all response packets.
The relaying is simple for UDP: each packet received from one side must be sent to the other side, without any splitting or merging (datagram boundaries must be preserved for UDP).
Since UDP is not a connected protocol, a UDP connection is never "closed". Therefore, the selector wakes up once per minute (using a timeout) to clean expired (in practice, unused for more than 2 minutes) UDP connections.
TcpConnection also keeps, as a reference, a copy of the IP and TCP headers
of the first packet received.
However, contrary to UDP, TCP must provide reliable delivery. In particular, lost packets have to be retransmitted. Nonetheless, we can take advantage of the two TCP we are proxifying, so that we can provide reliability by delegating the retransmission mechanism to them. In fact, it is sufficient to guarantee that we cannot lose packets from network to device.
Indeed, any packet written to a TCP channel is safe, since it will be managed by the TCP implementation from the system. Losing a raw IP packet received from the device is also safe: the device TCP implementation will follow the TCP protocol to retransmit it. Therefore, dropping packets from device to network does not break the connection.
On the other hand, once we retrieved a packet from a TCP channel from the network, we are responsible for it. Would it be dropped, there would be no way to recover the connection.
As far as I know, there are only two possible causes of packet loss for which we are responsible:
When our buffers are full, we won't resize them indefinitely, so we have to drop packets. Typically, this may happen if the data from the network is received at a higher rate than that they can be sent to the device.
When a raw packet is considered invalid by the device, it is rejected. This may happen for example if the checksum is invalid or if the TCP sequence number is out-of-the-window.
Therefore, by contraposition, if we guarantee that we never retrieve a packet that we won't be able to store, and that we provide a valid checksum and respect the client TCP window, then we won't lose any packet without implementing any retransmission mechanism.
To prevent retrieving a packet while our buffers are full, we indicate that we
are not interested in reading (
interest) the TCP channel when some pending data remain to
be written to the client buffer. Once some space becomes available, the client
then pulls the available packets from the
TcpConnections, which are packet
For more details, go read the code!
If you find a bug, or have an awesome idea to implement, please discuss and contribute ;-)