Repository: Genymobile/gnirehtet
Branch: master
Commit: 1eb2e58bc91e
Files: 119
Total size: 487.8 KB
Directory structure:
gitextract_jsgxj4af/
├── .gitignore
├── DEVELOP.md
├── LICENSE
├── README.md
├── app/
│ ├── build.gradle
│ ├── proguard-rules.pro
│ └── src/
│ ├── main/
│ │ ├── AndroidManifest.xml
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── genymobile/
│ │ │ └── gnirehtet/
│ │ │ ├── Binary.java
│ │ │ ├── CIDR.java
│ │ │ ├── Forwarder.java
│ │ │ ├── GnirehtetActivity.java
│ │ │ ├── GnirehtetService.java
│ │ │ ├── IPPacketOutputStream.java
│ │ │ ├── InvalidCIDRException.java
│ │ │ ├── Net.java
│ │ │ ├── Notifier.java
│ │ │ ├── PersistentRelayTunnel.java
│ │ │ ├── RelayTunnel.java
│ │ │ ├── RelayTunnelListener.java
│ │ │ ├── RelayTunnelProvider.java
│ │ │ ├── Tunnel.java
│ │ │ └── VpnConfiguration.java
│ │ └── res/
│ │ ├── drawable/
│ │ │ ├── ic_close_24dp.xml
│ │ │ ├── ic_report_problem_24dp.xml
│ │ │ └── ic_usb_24dp.xml
│ │ ├── values/
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── values-fr/
│ │ └── strings.xml
│ └── test/
│ └── java/
│ └── com/
│ └── genymobile/
│ └── gnirehtet/
│ └── TestIPPacketOutputSteam.java
├── build.gradle
├── config/
│ ├── android-checkstyle.gradle
│ ├── android-signing.gradle
│ ├── checkstyle/
│ │ └── checkstyle.xml
│ └── java-checkstyle.gradle
├── gradle/
│ └── wrapper/
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── relay-java/
│ ├── build.gradle
│ ├── scripts/
│ │ ├── gnirehtet
│ │ ├── gnirehtet-run.cmd
│ │ └── gnirehtet.cmd
│ └── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── genymobile/
│ │ └── gnirehtet/
│ │ ├── AdbMonitor.java
│ │ ├── CommandLineArguments.java
│ │ ├── Main.java
│ │ └── relay/
│ │ ├── AbstractConnection.java
│ │ ├── Binary.java
│ │ ├── Client.java
│ │ ├── CloseListener.java
│ │ ├── CommandExecutionException.java
│ │ ├── Connection.java
│ │ ├── ConnectionId.java
│ │ ├── DatagramBuffer.java
│ │ ├── IPv4Header.java
│ │ ├── IPv4Packet.java
│ │ ├── IPv4PacketBuffer.java
│ │ ├── Log.java
│ │ ├── Net.java
│ │ ├── PacketSource.java
│ │ ├── Packetizer.java
│ │ ├── Relay.java
│ │ ├── Router.java
│ │ ├── SelectionHandler.java
│ │ ├── StreamBuffer.java
│ │ ├── TCPConnection.java
│ │ ├── TCPHeader.java
│ │ ├── TransportHeader.java
│ │ ├── TunnelServer.java
│ │ ├── UDPConnection.java
│ │ └── UDPHeader.java
│ └── test/
│ └── java/
│ └── com/
│ └── genymobile/
│ └── gnirehtet/
│ ├── AdbMonitorTest.java
│ ├── CommandLineArgumentsTest.java
│ └── relay/
│ ├── DatagramBufferTest.java
│ ├── IPv4HeaderTest.java
│ ├── IPv4PacketBufferTest.java
│ ├── IPv4PacketTest.java
│ ├── InetAddressTest.java
│ ├── PacketizerTest.java
│ ├── StreamBufferTest.java
│ ├── TCPHeaderTest.java
│ └── UDPHeaderTest.java
├── relay-rust/
│ ├── .gitignore
│ ├── Cargo.toml
│ ├── build.gradle
│ ├── scripts/
│ │ └── gnirehtet-run.cmd
│ └── src/
│ ├── adb_monitor.rs
│ ├── cli_args.rs
│ ├── execution_error.rs
│ ├── lib.rs
│ ├── logger.rs
│ ├── main.rs
│ └── relay/
│ ├── binary.rs
│ ├── byte_buffer.rs
│ ├── client.rs
│ ├── close_listener.rs
│ ├── connection.rs
│ ├── datagram.rs
│ ├── datagram_buffer.rs
│ ├── interrupt.rs
│ ├── ipv4_header.rs
│ ├── ipv4_packet.rs
│ ├── ipv4_packet_buffer.rs
│ ├── mod.rs
│ ├── net.rs
│ ├── packet_source.rs
│ ├── packetizer.rs
│ ├── relay.rs
│ ├── router.rs
│ ├── selector.rs
│ ├── stream_buffer.rs
│ ├── tcp_connection.rs
│ ├── tcp_header.rs
│ ├── transport_header.rs
│ ├── tunnel_server.rs
│ ├── udp_connection.rs
│ └── udp_header.rs
├── release
└── settings.gradle
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
build/
.gradle/
.idea/
*.iml
/local.properties
================================================
FILE: DEVELOP.md
================================================
# Gnirehtet for developers
## Getting started
### Requirements
You need the [Android SDK] (_Android Studio_) and the JDK 8 (`openjdk-8-jdk`).
You also need the [Rust] environment to build the Rust version:
```bash
wget https://sh.rustup.rs -O rustup-init
sh rustup-init
```
[Android SDK]: https://developer.android.com/studio/index.html
[Rust]: https://www.rust-lang.org/
### Build
#### Everything
If `gradle` is installed on your computer:
gradle build
Otherwise, you can call the [gradle wrapper]:
./gradlew build
This will build the Android application, the Java and Rust relay servers, both
in debug and release versions.
[gradle wrapper]: https://docs.gradle.org/current/userguide/gradle_wrapper.html
#### Specific parts
Several _gradle_ tasks are exposed in the root project. For instance:
- `debugJava` and `releaseJava` build the Android application and the Java
relay server;
- `debugRust` and `releaseRust` build 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 `cargo` directly.
For instance, to build a release version of the Rust relay server:
cd relay-rust
cargo build --release
It will generate the binary in `target/release/gnirehtet`.
#### Cross-compile the Rust relay server from Linux to Windows
To build `gnirehtet.exe` from Linux, install the cross-compile toolchain (on
Debian):
sudo apt install gcc-mingw-w64-x86-64
rustup target add x86_64-pc-windows-gnu
Add the following lines to `~/.cargo/config`:
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
ar = "x86_64-w64-mingw32-gcc-ar"
Then build:
cargo build --release --target=x86_64-pc-windows-gnu
It will generate `target/x86_64-pc-windows-gnu/release/gnirehtet.exe`.
### Android Studio
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.
## Overview
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][berkeley] (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][portNAT]), 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.
[VPN]: https://developer.android.com/reference/android/net/VpnService.html
[IPv4 packets]: https://en.wikipedia.org/wiki/IPv4#Packet_structure
[OSI model]: https://en.wikipedia.org/wiki/OSI_model
[berkeley]: https://en.wikipedia.org/wiki/Berkeley_sockets
[NAT]: https://en.wikipedia.org/wiki/Network_address_translation
[portNAT]: https://en.wikipedia.org/wiki/Network_address_translation#Methods_of_translation
## Client
The client is an _Android_ project located in [`app/`](app/).
The [`VpnService`] is implemented by [`GnirehtetService`].
We control the application through intents to [`GnirehtetActivity`].
Some configuration options may be passed as extra parameters, converted to a
[`VpnConfiguration`] instance. Currently, the user can configure the DNS servers
to use.
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
of [`AuthorizationActivity`].
[`RelayTunnel`] manages one connection to the relay server.
[`PersistentRelayTunnel`] manages [`RelayTunnel`] instances to handle
reconnections, so that we can stop and start the relay while the client keeps
running.
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
of [`IPPacketOutputStream`].
[`VpnService`]: https://developer.android.com/reference/android/net/VpnService.html
[`GnirehtetService`]: app/src/main/java/com/genymobile/gnirehtet/GnirehtetService.java
[`GnirehtetActivity`]: app/src/main/java/com/genymobile/gnirehtet/GnirehtetActivity.java
[`VpnConfiguration`]: app/src/main/java/com/genymobile/gnirehtet/VpnConfiguration.java
[`startActivityForResult`]: https://developer.android.com/reference/android/app/Activity.html#startActivityForResult%28android.content.Intent,%20int%29
[`Activity`]: https://developer.android.com/reference/android/app/Activity.html
[`AuthorizationActivity`]: app/src/main/java/com/genymobile/gnirehtet/AuthorizationActivity.java
[`RelayTunnel`]: app/src/main/java/com/genymobile/gnirehtet/RelayTunnel.java
[`PersistentRelayTunnel`]: app/src/main/java/com/genymobile/gnirehtet/PersistentRelayTunnel.java
[`IPPacketOutputStream`]: app/src/main/java/com/genymobile/gnirehtet/IPPacketOutputStream.java
## Relay server
The relay server comes in two flavors:
- the **Java** version is a _Java 8_ project located in
[`relay-java/`](relay-java/);
- the **Rust** version is a _Rust_ project located in
[`relay-rust/`](relay-rust/).
It is implemented using [asynchronous I/O] (through [Java NIO] and [Rust mio]).
As a consequence, it is essentially monothreaded, so there is no need for
synchronization to handle packets.
[asynchronous I/O]: https://en.wikipedia.org/wiki/Asynchronous_I/O
[Java NIO]: https://en.wikipedia.org/wiki/New_I/O_%28Java%29
[Rust mio]: https://docs.rs/mio/0.6.10/mio/
### Selector
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_ ([`SelectableChannel`][nio/SelectableChannel]) are
registered to the _selector_ ([`Selector`][nio/Selector]) defined in
[`Relay`][java/Relay], with their [`SelectionHandler`][java/SelectionHandler] as
[attachment][nio/attachment] (for better decoupling). A [`Client`][java/Client]
is created for every accepted _client_.
[nio/Selector]: https://docs.oracle.com/javase/8/docs/api/java/nio/channels/Selector.html
[nio/SelectableChannel]: https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SelectableChannel.html
[java/Relay]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Relay.java
[java/SelectionHandler]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/SelectionHandler.java
[nio/attachment]: https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SelectionKey.html#attachment--
[java/Client]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Client.java
In **Rust**, our own [`Selector`][rust/selector] class wraps the
[`Poll`][mio/Poll] from _mio_ to expose an API accepting event handlers instead
of low-level [tokens][mio/Token]. The _selector_ instance is created in
[`Relay`][rust/relay]. The _channels_ are called _"handles"_ in _mio_; they are
simply the socket instances themselves ([`TcpListener`][mio/TcpListener],
[`TcpStream`][mio/TcpStream] and [`UdpSocket`][mio/UdpSocket]). A
[`Client`][rust/client] is created for every accepted _client_.
[mio/Poll]: https://docs.rs/mio/0.6.10/mio/struct.Poll.html
[mio/Token]: https://docs.rs/mio/0.6.10/mio/struct.Token.html
[mio/TcpListener]: https://docs.rs/mio/0.6.10/mio/net/struct.TcpListener.html
[mio/TcpStream]: https://docs.rs/mio/0.6.10/mio/net/struct.TcpStream.html
[mio/UdpSocket]: https://docs.rs/mio/0.6.10/mio/net/struct.UdpSocket.html
[rust/selector]: relay-rust/src/relay/selector.rs
[rust/relay]: relay-rust/src/relay/relay.rs
[rust/client]: relay-rust/src/relay/client.rs

### 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.
### Packets
A class representing an _IPv4 packet_
([`IPv4Packet`][java/IPv4Packet] | [`Ipv4Packet`][rust/ipv4-packet]) 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.
[java/IPv4Packet]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/IPv4Packet.java
[rust/ipv4-packet]: relay-rust/src/relay/ipv4\_packet.rs
Each packet contains an instance of _IPv4 headers_ and _transport headers_
(which might be _TCP_ or _UDP_ headers).
In **Java**, this is straightforward: [`IPv4Header`][java/IPv4Header],
[`TCPHeader`][java/TCPHeader] and [`UDPHeader`][java/UDPHeader] just share a
slice of the raw packet buffer.
[java/IPv4Header]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/IPv4Header.java
[java/TCPHeader]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/TCPHeader.java
[java/UDPHeader]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/UDPHeader.java
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 (`*Header<'a>` and `*HeaderMut<'a>`) reference both
the raw array and the _header data_:
- [`ipv4_header`][rust/ipv4-header]:
- data: `Ipv4HeaderData`
- view: `Ipv4Header<'a>`
- mutable view: `Ipv4HeaderMut<'a>`
- [`tcp_header`][rust/tcp-header]:
- data: `TcpHeaderData`
- view: `TcpHeader<'a>`
- mutable view: `TcpHeaderMut<'a>`
- [`udp_header`][rust/udp-header]:
- data: `UdpHeaderData`
- view: `UdpHeader<'a>`
- mutable view: `UdpHeaderMut<'a>`
In addition, we use [enums][rust-enums] for _transport headers_ to statically
dispatch calls to _UDP_ and _TCP_ header classes:
- [`transport_header`][rust/transport-header]:
- data: `TransportHeaderData`
- view: `TransportHeader<'a>`
- mutable view: `TransportHeaderMut<'a>`
[rust/ipv4-header]: relay-rust/src/relay/ipv4\_header.rs
[rust/tcp-header]: relay-rust/src/relay/tcp\_header.rs
[rust/udp-header]: relay-rust/src/relay/udp\_header.rs
[rust/transport-header]: relay-rust/src/relay/transport\_header.rs
[rust-enums]: https://doc.rust-lang.org/book/first-edition/enums.html
### Router
Each _client_ holds a _router_
([`Router`][java/Router] | [`Router`][rust/router]), responsible for sending the
packets to the right _connection_, identified by these 5 properties available in
the IP and transport headers:
- protocol
- source address
- source port
- destination address
- destination port
These identifiers are stored in a _connection id_
([`ConnectionId`][java/ConnectionId] | [`ConnectionId`][rust/connection]),
used as a key to find or create the associated _connection_.
[java/Router]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Router.java
[java/ConnectionId]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/ConnectionId.java
[rust/Router]: relay-rust/src/relay/router.rs
[rust/connection]: relay-rust/src/relay/connection.rs
### Connections
A _connection_ ([`Connection`][java/Connection] |
[`Connection`][rust/connection]) is either a _TCP connection_
([`TCPConnection`][java/TCPConnection] | [`TcpConnection`][rust/tcp-connection])
or a _UDP connection_ ([`UDPConnection`][java/UDPConnection] |
[`UdpConnection`][rust/udp-connection]) to the requested destination. It
registers its own _channel_ to the _selector_.
[java/Connection]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Connection.java
[java/TCPConnection]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/TCPConnection.java
[java/UDPConnection]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/UDPConnection.java
[rust/connection]: relay-rust/src/relay/connection.rs
[rust/tcp-connection]: relay-rust/src/relay/tcp\_connection.rs
[rust/udp-connection]: relay-rust/src/relay/udp\_connection.rs
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.
[RFC 793]: https://tools.ietf.org/html/rfc793
A _packetizer_ ([`Packetizer`][java/Packetizer] |
[`Packetizer`][rust/packetizer]) converts from level 5 to level 3 by appending
correct IP and transport headers.
[java/Packetizer]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Packetizer.java
[rust/packetizer]: relay-rust/src/relay/packetizer.rs
#### UDP connection
When the first packet for a specific UDP connection is received from the device,
a new `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.
#### TCP connection
`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:
1. 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.
2. 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][flow control].
[flow control]: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Flow_control
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**.
[contraposition]: https://en.wikipedia.org/wiki/Contraposition
To prevent retrieving a packet while our buffers are full, we indicate that we
are not interested in reading ([`interestOps`][nio/interestOps] |
[`interest`][mio/reregister]) 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 `TcpConnection`s, which are _packet
sources_ ([`PacketSource`][java/PacketSource] |
[`PacketSource`][rust/packet-source]).
[nio/interestOps]: https://developer.android.com/reference/java/nio/channels/SelectionKey.html#interestOps%28int%29
[mio/reregister]: https://docs.rs/mio/0.6.10/mio/struct.Poll.html#method.reregister
[java/PacketSource]: relay-java/src/main/java/com/genymobile/gnirehtet/relay/PacketSource.java
[rust/packet-source]: relay-rust/src/relay/packet\_source.rs
## Hack
For more details, go read the code!
If you find a bug, or have an awesome idea to implement, please discuss and
contribute ;-)
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright (C) 2017 Genymobile
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
# Gnirehtet (v2.5.1)
This project provides **reverse tethering** over `adb` for Android: it
allows devices to use the internet connection of the computer they are plugged
on. It does not require any _root_ access (neither on the device nor on the
computer). It works on _GNU/Linux_, _Windows_ and _Mac OS_.
Currently, it relays [TCP] and [UDP] over [IPv4] traffic, but it does not
support [IPv6] (yet?).
[TCP]: https://en.wikipedia.org/wiki/Transmission_Control_Protocol
[UDP]: https://fr.wikipedia.org/wiki/User_Datagram_Protocol
[IPv4]: https://en.wikipedia.org/wiki/IPv4
[IPv6]: https://en.wikipedia.org/wiki/IPv6
_**This project is not actively maintained anymore, only major blockers (like
build issues) are fixed. It should still work, though.**_
## Flavors
Two implementations of _Gnirehtet_ are available:
- one in **Java**;
- one in **Rust**.
### Which one to choose?
Use the **Rust** implementation. The native binary consumes less CPU and memory,
and does not require a _Java_ runtime environment.
The relay server of _Gnirehtet_ was initially only implemented in Java. As a
benefit, the same "binary" runs on every platform having _Java 8_ runtime
installed. It is still maintained to provide a working alternative in case of
problems with the Rust version.
## Requirements
The Android application requires at least API 21 (Android 5.0).
For the _Java_ version only, _Java 8_ (JRE) is required on your computer. On
Debian-based distros, install the package `openjdk-8-jre`.
### adb
You need a recent version of [adb] (where `adb reverse` is implemented, it
works with 1.0.36).
It is available in the [Android SDK platform tools][platform-tools].
On Debian-based distros, you can alternatively install the package
`android-tools-adb`.
On Windows, if you need `adb` only for this application, just download the
[platform-tools][platform-tools-windows] and extract the following files to the
_gnirehtet_ directory:
- `adb.exe`
- `AdbWinApi.dll`
- `AdbWinUsbApi.dll`
Make sure you [enabled adb debugging][enable-adb] on your device(s).
[adb]: https://developer.android.com/studio/command-line/adb.html
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
## Get the app
### Homebrew
If you use [Homebrew](https://brew.sh/), getting up and running is very quick.
To install the Rust version:
```
brew install gnirehtet
```
### Download
Download the [latest release][latest] in the flavor you want.
[latest]: https://github.com/Genymobile/gnirehtet/releases/latest
#### Rust
- **Linux:** [`gnirehtet-rust-linux64-v2.5.1.zip`][direct-rust-linux64]
(SHA-256: _dee55499ca4fef00ce2559c767d2d8130163736d43fdbce753e923e75309c275_)
- **Windows:** [`gnirehtet-rust-win64-v2.5.1.zip`][direct-rust-win64]
(SHA-256: _7f5b1063e7895182aa60def1437e50363c3758144088dcd079037bb7c3c46a1c_)
- **MacOS:** [`gnirehtet-rust-macos64-v2.2.1.zip`][direct-rust-macos64]
_(old release)_
(SHA-256: _902103e6497f995e1e9b92421be212559950cca4a8b557e1f0403769aee06fc8_)
[direct-rust-linux64]: https://github.com/Genymobile/gnirehtet/releases/download/v2.5.1/gnirehtet-rust-linux64-v2.5.1.zip
[direct-rust-win64]: https://github.com/Genymobile/gnirehtet/releases/download/v2.5.1/gnirehtet-rust-win64-v2.5.1.zip
[direct-rust-macos64]: https://github.com/Genymobile/gnirehtet/releases/download/v2.2.1/gnirehtet-rust-macos64-v2.2.1.zip
Then extract it.
The Linux and MacOS archives contain:
- `gnirehtet.apk`
- `gnirehtet`
The Windows archive contains:
- `gnirehtet.apk`
- `gnirehtet.exe`
- `gnirehtet-run.cmd`
#### Java
- **All platforms:** [`gnirehtet-java-v2.5.1.zip`][direct-java]
(SHA-256: _816748078fa6a304600a294a13338a06ac778bcc0e57b62d88328c7968ad2d3a_)
[direct-java]: https://github.com/Genymobile/gnirehtet/releases/download/v2.5.1/gnirehtet-java-v2.5.1.zip
Then extract it. The archive contains:
- `gnirehtet.apk`
- `gnirehtet.jar`
- `gnirehtet`
- `gnirehtet.cmd`
- `gnirehtet-run.cmd`
## Run (simple)
_Note: On Windows, replace `./gnirehtet` by `gnirehtet` in the following
commands._
The application has no UI, and is intended to be controlled from the computer
only.
If you want to activate reverse tethering for exactly one device, just execute:
./gnirehtet run
Reverse tethering remains active until you press _Ctrl+C_.
On Windows, for convenience, you can double-click on `gnirehtet-run.cmd`
instead (it just executes `gnirehtet run`, without requiring to open a
terminal).
The very first start should open a popup to request permission:

A "key" logo appears in the status bar whenever _Gnirehtet_ is active:

Alternatively, you can enable reverse tethering for all connected devices
(present and future) by calling:
./gnirehtet autorun
## Run
You can execute the actions separately (it may be useful if you want to reverse
tether several devices simultaneously).
Start the relay server and keep it open:
./gnirehtet relay
Install the `apk` on your Android device:
./gnirehtet install [serial]
In another terminal, for each client, execute:
./gnirehtet start [serial]
To stop a client:
./gnirehtet stop [serial]
To reset the tunnel (useful to get the connection back when a device is
unplugged and plugged back while gnirehtet is active):
./gnirehtet tunnel [serial]
The _serial_ parameter is required only if `adb devices` outputs more than one
device.
For advanced options, call `./gnirehtet` without arguments to get more details.
## Run manually
The `gnirehtet` program exposes a simple command-line interface that executes
lower-level commands. You can call them manually instead.
To start the relay server:
./gnirehtet relay
To install the apk:
adb install -r gnirehtet.apk
To start a client:
adb reverse localabstract:gnirehtet tcp:31416
adb shell am start -a com.genymobile.gnirehtet.START \
-n com.genymobile.gnirehtet/.GnirehtetActivity
To stop a client:
adb shell am start -a com.genymobile.gnirehtet.STOP \
-n com.genymobile.gnirehtet/.GnirehtetActivity
## Environment variables
`ADB` defines a custom path to the `adb` executable:
```bash
ADB=/path/to/my/adb ./gnirehtet run
```
`GNIREHTET_APK` defines a custom path to `gnirehtet.apk`:
```bash
GNIREHTET_APK=/usr/share/gnirehtet/gnirehtet.apk ./gnirehtet run
```
## Why _gnirehtet_?
rev <<< tethering
(in _Bash_)
## Developers
Read the [developers page].
[developers page]: DEVELOP.md
## Licence
Copyright (C) 2017 Genymobile
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## Articles
- [Introducing “gnirehtet”, a reverse tethering tool for Android][medium-1] ([French version][blog-1])
- [Gnirehtet 2: our reverse tethering tool for Android now available in Rust][medium-2]
- [Gnirehtet rewritten in Rust][blog-2-en] ([French version][blog-2-fr])
[medium-1]: https://medium.com/@rom1v/gnirehtet-reverse-tethering-android-2afacdbdaec7
[blog-1]: https://blog.rom1v.com/2017/03/gnirehtet/
[medium-2]: https://medium.com/genymobile/gnirehtet-2-our-reverse-tethering-tool-for-android-now-available-in-rust-999960483d5a
[blog-2-en]: https://blog.rom1v.com/2017/09/gnirehtet-rewritten-in-rust/
[blog-2-fr]: https://blog.rom1v.com/2017/09/gnirehtet-reecrit-en-rust/
================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
archivesBaseName = "gnirehtet" // change apk name
applicationId "com.genymobile.gnirehtet"
minSdkVersion 21
targetSdkVersion 29
versionCode 9
versionName "2.5.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
testImplementation 'junit:junit:4.12'
}
apply from: "$project.rootDir/config/android-checkstyle.gradle"
apply from: "$project.rootDir/config/android-signing.gradle"
================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/rom/android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
================================================
FILE: app/src/main/AndroidManifest.xml
================================================
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/Binary.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
@SuppressWarnings("checkstyle:MagicNumber")
public final class Binary {
private static final int MAX_STRING_PACKET_SIZE = 20;
private Binary() {
// not instantiable
}
public static int unsigned(byte value) {
return value & 0xff;
}
public static int unsigned(short value) {
return value & 0xffff;
}
public static long unsigned(int value) {
return value & 0xffffffffL;
}
public static String buildPacketString(byte[] data, int len) {
int limit = Math.min(MAX_STRING_PACKET_SIZE, len);
StringBuilder builder = new StringBuilder();
builder.append('[').append(len).append(" bytes] ");
for (int i = 0; i < limit; ++i) {
if (i != 0) {
String sep = i % 4 == 0 ? " " : " ";
builder.append(sep);
}
builder.append(String.format("%02X", data[i] & 0xff));
}
if (limit < len) {
builder.append(" ... +").append(len - limit).append(" bytes");
}
return builder.toString();
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/CIDR.java
================================================
/*
* Copyright (C) 2018 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class CIDR implements Parcelable {
private InetAddress address;
private int prefixLength;
public CIDR(InetAddress address, int prefixLength) {
this.address = address;
this.prefixLength = prefixLength;
}
private CIDR(Parcel source) {
try {
address = InetAddress.getByAddress(source.createByteArray());
} catch (UnknownHostException e) {
throw new AssertionError("Invalid address", e);
}
prefixLength = source.readInt();
}
@SuppressWarnings("checkstyle:MagicNumber")
public static CIDR parse(String cidr) throws InvalidCIDRException {
int slashIndex = cidr.indexOf('/');
InetAddress address;
int prefix;
try {
if (slashIndex != -1) {
address = Net.toInetAddress(cidr.substring(0, slashIndex));
prefix = Integer.parseInt(cidr.substring(slashIndex + 1));
} else {
address = Net.toInetAddress(cidr);
prefix = 32;
}
return new CIDR(address, prefix);
} catch (IllegalArgumentException e) {
Log.e("Error", e.getMessage(), e);
throw new InvalidCIDRException(cidr, e);
} catch (Throwable e) {
Log.e("Error", e.getMessage(), e);
throw e;
}
}
public InetAddress getAddress() {
return address;
}
public int getPrefixLength() {
return prefixLength;
}
@Override
public String toString() {
return address.getHostAddress() + "/" + prefixLength;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeByteArray(address.getAddress());
dest.writeInt(prefixLength);
}
public static final Creator CREATOR = new Creator() {
@Override
public CIDR createFromParcel(Parcel source) {
return new CIDR(source);
}
@Override
public CIDR[] newArray(int size) {
return new CIDR[size];
}
};
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/Forwarder.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.net.VpnService;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Forwarder {
private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(3);
private static final String TAG = Forwarder.class.getSimpleName();
private static final int BUFSIZE = 0x10000;
private static final byte[] DUMMY_ADDRESS = {42, 42, 42, 42};
private static final int DUMMY_PORT = 4242;
private final FileDescriptor vpnFileDescriptor;
private final PersistentRelayTunnel tunnel;
private Future> deviceToTunnelFuture;
private Future> tunnelToDeviceFuture;
public Forwarder(VpnService vpnService, FileDescriptor vpnFileDescriptor, RelayTunnelListener listener) {
this.vpnFileDescriptor = vpnFileDescriptor;
tunnel = new PersistentRelayTunnel(vpnService, listener);
}
public void forward() {
deviceToTunnelFuture = EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
try {
forwardDeviceToTunnel(tunnel);
} catch (InterruptedIOException e) {
Log.d(TAG, "Device to tunnel interrupted");
} catch (IOException e) {
Log.e(TAG, "Device to tunnel exception", e);
}
}
});
tunnelToDeviceFuture = EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
try {
forwardTunnelToDevice(tunnel);
} catch (InterruptedIOException e) {
Log.d(TAG, "Device to tunnel interrupted");
} catch (IOException e) {
Log.e(TAG, "Tunnel to device exception", e);
}
}
});
}
public void stop() {
tunnel.close();
tunnelToDeviceFuture.cancel(true);
deviceToTunnelFuture.cancel(true);
wakeUpReadWorkaround();
}
@SuppressWarnings("checkstyle:MagicNumber")
private void forwardDeviceToTunnel(Tunnel tunnel) throws IOException {
Log.d(TAG, "Device to tunnel forwarding started");
FileInputStream vpnInput = new FileInputStream(vpnFileDescriptor);
byte[] buffer = new byte[BUFSIZE];
while (true) {
// blocking read
int r = vpnInput.read(buffer);
if (r == -1) {
Log.d(TAG, "VPN closed");
break;
}
if (r > 0) {
int version = buffer[0] >> 4;
if (version == 4) {
// blocking send
tunnel.send(buffer, r);
} else {
// see
Log.w(TAG, "Unexpected packet IP version: " + version);
}
} else {
Log.d(TAG, "Empty read");
}
}
Log.d(TAG, "Device to tunnel forwarding stopped");
}
private void forwardTunnelToDevice(Tunnel tunnel) throws IOException {
Log.d(TAG, "Tunnel to device forwarding started");
FileOutputStream vpnOutput = new FileOutputStream(vpnFileDescriptor);
IPPacketOutputStream packetOutputStream = new IPPacketOutputStream(vpnOutput);
byte[] buffer = new byte[BUFSIZE];
while (true) {
// blocking receive
int w = tunnel.receive(buffer);
if (w == -1) {
Log.d(TAG, "Tunnel closed");
break;
}
if (w > 0) {
// blocking write
packetOutputStream.write(buffer, 0, w);
} else {
Log.d(TAG, "Empty write");
}
}
Log.d(TAG, "Tunnel to device forwarding stopped");
}
/**
* Neither vpnInterface.close() nor vpnInputStream.close() wake up a blocking
* vpnInputStream.read().
*
* Therefore, we need to make Android send a packet to the VPN interface (here by sending a UDP
* packet), so that any blocking read will be woken up.
*
* Since the tunnel is closed at this point, it will never reach the network.
*/
private void wakeUpReadWorkaround() {
// network actions may not be called from the main thread
EXECUTOR_SERVICE.execute(new Runnable() {
@Override
public void run() {
try {
DatagramSocket socket = new DatagramSocket();
InetAddress dummyAddr = InetAddress.getByAddress(DUMMY_ADDRESS);
DatagramPacket packet = new DatagramPacket(new byte[0], 0, dummyAddr, DUMMY_PORT);
socket.send(packet);
} catch (IOException e) {
// ignore
}
}
});
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/GnirehtetActivity.java
================================================
package com.genymobile.gnirehtet;
import android.app.Activity;
import android.content.Intent;
import android.net.VpnService;
import android.os.Bundle;
import android.util.Log;
/**
* This (invisible) activity receives the {@link #ACTION_GNIREHTET_START START} and
* {@link #ACTION_GNIREHTET_STOP} actions from the command line.
*
* Recent versions of Android refuse to directly start a {@link android.app.Service Service} or a
* {@link android.content.BroadcastReceiver BroadcastReceiver}, so actions are always managed by
* this activity.
*/
public class GnirehtetActivity extends Activity {
private static final String TAG = GnirehtetActivity.class.getSimpleName();
public static final String ACTION_GNIREHTET_START = "com.genymobile.gnirehtet.START";
public static final String ACTION_GNIREHTET_STOP = "com.genymobile.gnirehtet.STOP";
public static final String EXTRA_DNS_SERVERS = "dnsServers";
public static final String EXTRA_ROUTES = "routes";
private static final int VPN_REQUEST_CODE = 0;
private VpnConfiguration requestedConfig;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleIntent(getIntent());
}
private void handleIntent(Intent intent) {
String action = intent.getAction();
Log.d(TAG, "Received request " + action);
boolean finish = true;
if (ACTION_GNIREHTET_START.equals(action)) {
VpnConfiguration config = createConfig(intent);
finish = startGnirehtet(config);
} else if (ACTION_GNIREHTET_STOP.equals(action)) {
stopGnirehtet();
}
if (finish) {
finish();
}
}
private static VpnConfiguration createConfig(Intent intent) {
String[] dnsServers = intent.getStringArrayExtra(EXTRA_DNS_SERVERS);
if (dnsServers == null) {
dnsServers = new String[0];
}
String[] routes = intent.getStringArrayExtra(EXTRA_ROUTES);
if (routes == null) {
routes = new String[0];
}
return new VpnConfiguration(Net.toInetAddresses(dnsServers), Net.toCIDRs(routes));
}
private boolean startGnirehtet(VpnConfiguration config) {
Intent vpnIntent = VpnService.prepare(this);
if (vpnIntent == null) {
Log.d(TAG, "VPN was already authorized");
// we got the permission, start the service now
GnirehtetService.start(this, config);
return true;
}
Log.w(TAG, "VPN requires the authorization from the user, requesting...");
requestAuthorization(vpnIntent, config);
return false; // do not finish now
}
private void stopGnirehtet() {
GnirehtetService.stop(this);
}
private void requestAuthorization(Intent vpnIntent, VpnConfiguration config) {
this.requestedConfig = config;
startActivityForResult(vpnIntent, VPN_REQUEST_CODE);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) {
GnirehtetService.start(this, requestedConfig);
}
requestedConfig = null;
finish();
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/GnirehtetService.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.VpnService;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
import java.util.List;
public class GnirehtetService extends VpnService {
public static final boolean VERBOSE = false;
private static final String ACTION_START_VPN = "com.genymobile.gnirehtet.START_VPN";
private static final String ACTION_CLOSE_VPN = "com.genymobile.gnirehtet.CLOSE_VPN";
private static final String EXTRA_VPN_CONFIGURATION = "vpnConfiguration";
private static final String TAG = GnirehtetService.class.getSimpleName();
private static final InetAddress VPN_ADDRESS = Net.toInetAddress(new byte[] {10, 0, 0, 2});
// magic value: higher (like 0x8000 or 0xffff) or lower (like 1500) values show poorer performances
private static final int MTU = 0x4000;
private final Notifier notifier = new Notifier(this);
private final Handler handler = new RelayTunnelConnectionStateHandler(this);
private ParcelFileDescriptor vpnInterface = null;
private Forwarder forwarder;
public static void start(Context context, VpnConfiguration config) {
Intent intent = new Intent(context, GnirehtetService.class);
intent.setAction(ACTION_START_VPN);
intent.putExtra(GnirehtetService.EXTRA_VPN_CONFIGURATION, config);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent);
} else {
context.startService(intent);
}
}
public static void stop(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(createStopIntent(context));
} else {
context.startService(createStopIntent(context));
}
}
static Intent createStopIntent(Context context) {
Intent intent = new Intent(context, GnirehtetService.class);
intent.setAction(ACTION_CLOSE_VPN);
return intent;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
Log.d(TAG, "Received request " + action);
if (ACTION_START_VPN.equals(action)) {
if (isRunning()) {
Log.d(TAG, "VPN already running, ignore START request");
} else {
VpnConfiguration config = intent.getParcelableExtra(EXTRA_VPN_CONFIGURATION);
if (config == null) {
config = new VpnConfiguration();
}
startVpn(config);
}
} else if (ACTION_CLOSE_VPN.equals(action)) {
close();
}
return START_NOT_STICKY;
}
private boolean isRunning() {
return vpnInterface != null;
}
private void startVpn(VpnConfiguration config) {
notifier.start();
if (setupVpn(config)) {
startForwarding();
}
}
@SuppressWarnings("checkstyle:MagicNumber")
private boolean setupVpn(VpnConfiguration config) {
Builder builder = new Builder();
builder.addAddress(VPN_ADDRESS, 32);
builder.setSession(getString(R.string.app_name));
CIDR[] routes = config.getRoutes();
if (routes.length == 0) {
// no routes defined, redirect the whole network traffic
builder.addRoute("0.0.0.0", 0);
} else {
for (CIDR route : routes) {
builder.addRoute(route.getAddress(), route.getPrefixLength());
}
}
InetAddress[] dnsServers = config.getDnsServers();
if (dnsServers.length == 0) {
// no DNS server defined, use Google DNS
builder.addDnsServer("8.8.8.8");
} else {
for (InetAddress dnsServer : dnsServers) {
builder.addDnsServer(dnsServer);
}
}
// non-blocking by default, but FileChannel is not selectable, that's stupid!
// so switch to synchronous I/O to avoid polling
builder.setBlocking(true);
builder.setMtu(MTU);
vpnInterface = builder.establish();
if (vpnInterface == null) {
Log.w(TAG, "VPN starting failed, please retry");
// establish() may return null if the application is not prepared or is revoked
return false;
}
setAsUndernlyingNetwork();
return true;
}
@SuppressWarnings("checkstyle:MagicNumber")
private void setAsUndernlyingNetwork() {
if (Build.VERSION.SDK_INT >= 22) {
Network vpnNetwork = findVpnNetwork();
if (vpnNetwork != null) {
// so that applications knows that network is available
setUnderlyingNetworks(new Network[] {vpnNetwork});
}
} else {
Log.w(TAG, "Cannot set underlying network, API version " + Build.VERSION.SDK_INT + " < 22");
}
}
private Network findVpnNetwork() {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
Network[] networks = cm.getAllNetworks();
for (Network network : networks) {
LinkProperties linkProperties = cm.getLinkProperties(network);
List addresses = linkProperties.getLinkAddresses();
for (LinkAddress addr : addresses) {
if (addr.getAddress().equals(VPN_ADDRESS)) {
return network;
}
}
}
return null;
}
private void startForwarding() {
forwarder = new Forwarder(this, vpnInterface.getFileDescriptor(), new RelayTunnelListener(handler));
forwarder.forward();
}
private void close() {
if (!isRunning()) {
// already closed
return;
}
notifier.stop();
try {
forwarder.stop();
forwarder = null;
vpnInterface.close();
vpnInterface = null;
} catch (IOException e) {
Log.w(TAG, "Cannot close VPN file descriptor", e);
}
}
private static final class RelayTunnelConnectionStateHandler extends Handler {
private final GnirehtetService vpnService;
private RelayTunnelConnectionStateHandler(GnirehtetService vpnService) {
this.vpnService = vpnService;
}
@Override
public void handleMessage(Message message) {
if (!vpnService.isRunning()) {
// if the VPN is not running anymore, ignore obsolete events
return;
}
switch (message.what) {
case RelayTunnelListener.MSG_RELAY_TUNNEL_CONNECTED:
Log.d(TAG, "Relay tunnel connected");
vpnService.notifier.setFailure(false);
break;
case RelayTunnelListener.MSG_RELAY_TUNNEL_DISCONNECTED:
Log.d(TAG, "Relay tunnel disconnected");
vpnService.notifier.setFailure(true);
break;
default:
}
}
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/IPPacketOutputStream.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
/**
* Wrapper for writing one IP packet at a time to an {@link OutputStream}.
*/
@SuppressWarnings("checkstyle:MagicNumber")
public class IPPacketOutputStream extends OutputStream {
private static final String TAG = IPPacketOutputStream.class.getSimpleName();
private static final int MAX_IP_PACKET_LENGTH = 1 << 16; // packet length is stored on 16 bits
private final OutputStream target;
// must always accept 1 full packet + any partial packet
private final ByteBuffer buffer = ByteBuffer.allocate(2 * MAX_IP_PACKET_LENGTH);
public IPPacketOutputStream(OutputStream target) {
this.target = target;
}
@Override
public void close() throws IOException {
target.close();
}
@Override
public void flush() throws IOException {
target.flush();
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
if (len > MAX_IP_PACKET_LENGTH) {
throw new IOException("IPPacketOutputStream does not support writing more than one packet at a time");
}
// by design, the buffer must always have enough space for one packet
if (BuildConfig.DEBUG && len > buffer.remaining()) {
Log.e(TAG, len + " must be <= than " + buffer.remaining());
Log.e(TAG, buffer.toString());
throw new AssertionError("Buffer is unexpectedly full");
}
buffer.put(b, off, len);
buffer.flip();
sink();
buffer.compact();
}
@Override
public void write(int b) throws IOException {
if (!buffer.hasRemaining()) {
throw new IOException("IPPacketOutputStream buffer is full");
}
buffer.put((byte) b);
buffer.flip();
sink();
buffer.compact();
}
private void sink() throws IOException {
// sink all packets
while (sinkPacket()) {
// continue
}
}
private boolean sinkPacket() throws IOException {
int version = readPacketVersion(buffer);
if (version == -1) {
// no packet at all
return false;
}
if (version != 4) {
Log.e(TAG, "Unsupported packet received, IP version is:" + version);
Log.d(TAG, "Clearing buffer");
buffer.clear();
return false;
}
int packetLength = readPacketLength(buffer);
if (packetLength == -1 || packetLength > buffer.remaining()) {
// no packet
return false;
}
target.write(buffer.array(), buffer.arrayOffset() + buffer.position(), packetLength);
buffer.position(buffer.position() + packetLength);
return true;
}
/**
* Read the packet IP version, assuming that an IP packets is stored at absolute position 0.
*
* @param buffer the buffer
* @return the IP version, or {@code -1} if not available
*/
public static int readPacketVersion(ByteBuffer buffer) {
if (!buffer.hasRemaining()) {
// buffer is empty
return -1;
}
// version is stored in the 4 first bits
byte versionAndIHL = buffer.get(buffer.position());
return (versionAndIHL & 0xf0) >> 4;
}
/**
* Read the packet length, assuming thatan IP packet is stored at absolute position 0.
*
* @param buffer the buffer
* @return the packet length, or {@code -1} if not available
*/
public static int readPacketLength(ByteBuffer buffer) {
if (buffer.limit() < buffer.position() + 4) {
// buffer does not even contains the length field
return -1;
}
// packet length is 16 bits starting at offset 2
return Binary.unsigned(buffer.getShort(buffer.position() + 2));
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/InvalidCIDRException.java
================================================
/*
* Copyright (C) 2018 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
public class InvalidCIDRException extends Exception {
private String cidr;
private static String createMessage(String cidr) {
return "Invalid CIDR:" + cidr;
}
public InvalidCIDRException(String cidr, Throwable cause) {
super(createMessage(cidr), cause);
this.cidr = cidr;
}
public InvalidCIDRException(String cidr) {
super(createMessage(cidr));
this.cidr = cidr;
}
public String getCIDR() {
return cidr;
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/Net.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
public final class Net {
private Net() {
// not instantiable
}
public static InetAddress[] toInetAddresses(String... addresses) {
InetAddress[] result = new InetAddress[addresses.length];
for (int i = 0; i < result.length; ++i) {
result[i] = toInetAddress(addresses[i]);
}
return result;
}
public static InetAddress toInetAddress(String address) {
try {
return InetAddress.getByName(address);
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e);
}
}
public static InetAddress toInetAddress(byte[] raw) {
try {
return InetAddress.getByAddress(raw);
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e);
}
}
public static CIDR toCIDR(String cidr) {
try {
return CIDR.parse(cidr);
} catch (InvalidCIDRException e) {
throw new IllegalArgumentException(e);
}
}
public static CIDR[] toCIDRs(String... cidrs) {
CIDR[] result = new CIDR[cidrs.length];
for (int i = 0; i < result.length; ++i) {
result[i] = toCIDR(cidrs[i]);
}
return result;
}
@SuppressWarnings("checkstyle:MagicNumber")
public static Inet4Address getLocalhostIPv4() {
byte[] localhost = {127, 0, 0, 1};
return (Inet4Address) toInetAddress(localhost);
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/Notifier.java
================================================
package com.genymobile.gnirehtet;
import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
/**
* Manage the notification necessary for the foreground service (mandatory since Android O).
*/
public class Notifier {
private static final int NOTIFICATION_ID = 42;
private static final String CHANNEL_ID = "Gnirehtet";
private final Service context;
private boolean failure;
public Notifier(Service context) {
this.context = context;
}
private Notification createNotification(boolean failure) {
Notification.Builder notificationBuilder = createNotificationBuilder();
notificationBuilder.setContentTitle(context.getString(R.string.app_name));
if (failure) {
notificationBuilder.setContentText(context.getString(R.string.relay_disconnected));
notificationBuilder.setSmallIcon(R.drawable.ic_report_problem_24dp);
} else {
notificationBuilder.setContentText(context.getString(R.string.relay_connected));
notificationBuilder.setSmallIcon(R.drawable.ic_usb_24dp);
}
notificationBuilder.addAction(createStopAction());
return notificationBuilder.build();
}
@SuppressWarnings("deprecation")
private Notification.Builder createNotificationBuilder() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return new Notification.Builder(context, CHANNEL_ID);
}
return new Notification.Builder(context);
}
@TargetApi(26)
private void createNotificationChannel() {
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, context.getString(R.string.app_name), NotificationManager
.IMPORTANCE_DEFAULT);
getNotificationManager().createNotificationChannel(channel);
}
@TargetApi(26)
private void deleteNotificationChannel() {
getNotificationManager().deleteNotificationChannel(CHANNEL_ID);
}
public void start() {
failure = false; // reset failure flag
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel();
}
context.startForeground(NOTIFICATION_ID, createNotification(false));
}
public void stop() {
context.stopForeground(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
deleteNotificationChannel();
}
}
public void setFailure(boolean failure) {
if (this.failure != failure) {
this.failure = failure;
Notification notification = createNotification(failure);
getNotificationManager().notify(NOTIFICATION_ID, notification);
}
}
private Notification.Action createStopAction() {
Intent stopIntent = GnirehtetService.createStopIntent(context);
PendingIntent stopPendingIntent = PendingIntent.getService(context, 0, stopIntent, PendingIntent.FLAG_ONE_SHOT);
// the non-deprecated constructor is not available in API 21
@SuppressWarnings("deprecation")
Notification.Action.Builder actionBuilder = new Notification.Action.Builder(R.drawable.ic_close_24dp, context.getString(R.string.stop_vpn),
stopPendingIntent);
return actionBuilder.build();
}
private NotificationManager getNotificationManager() {
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/PersistentRelayTunnel.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.net.VpnService;
import android.util.Log;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Expose a {@link Tunnel} that automatically handles {@link RelayTunnel} reconnections.
*/
public class PersistentRelayTunnel implements Tunnel {
private static final String TAG = PersistentRelayTunnel.class.getSimpleName();
private final RelayTunnelProvider provider;
private final AtomicBoolean stopped = new AtomicBoolean();
public PersistentRelayTunnel(VpnService vpnService, RelayTunnelListener listener) {
provider = new RelayTunnelProvider(vpnService, listener);
}
@Override
public void send(byte[] packet, int len) throws IOException {
while (!stopped.get()) {
Tunnel tunnel = null;
try {
tunnel = provider.getCurrentTunnel();
tunnel.send(packet, len);
return;
} catch (IOException | InterruptedException e) {
Log.e(TAG, "Cannot send to tunnel", e);
if (tunnel != null) {
provider.invalidateTunnel(tunnel);
}
}
}
throw new InterruptedIOException("Persistent tunnel stopped");
}
@Override
public int receive(byte[] packet) throws IOException {
while (!stopped.get()) {
Tunnel tunnel = null;
try {
tunnel = provider.getCurrentTunnel();
int r = tunnel.receive(packet);
if (r == -1) {
Log.d(TAG, "Tunnel read EOF");
provider.invalidateTunnel(tunnel);
continue;
}
return r;
} catch (IOException | InterruptedException e) {
Log.e(TAG, "Cannot receive from tunnel", e);
if (tunnel != null) {
provider.invalidateTunnel(tunnel);
}
}
}
throw new InterruptedIOException("Persistent tunnel stopped");
}
@Override
public void close() {
stopped.set(true);
provider.invalidateTunnel();
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/RelayTunnel.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.net.VpnService;
import android.util.Log;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
public final class RelayTunnel implements Tunnel {
private static final String TAG = RelayTunnel.class.getSimpleName();
private static final String LOCAL_ABSTRACT_NAME = "gnirehtet";
private final LocalSocket localSocket = new LocalSocket();
private RelayTunnel() {
// exposed through open() static method
}
@SuppressWarnings("unused")
public static RelayTunnel open(VpnService vpnService) throws IOException {
Log.d(TAG, "Opening a new relay tunnel...");
// since we use a local socket, we don't need to protect the socket from the vpnService anymore
// but this is an implementation detail, so keep the method signature
return new RelayTunnel();
}
public void connect() throws IOException {
localSocket.connect(new LocalSocketAddress(LOCAL_ABSTRACT_NAME));
readClientId(localSocket.getInputStream());
}
/**
* The relay server is accessible through an "adb reverse" port redirection.
*
* If the port redirection is enabled but the relay server is not started, then the call to
* channel.connect() will succeed, but the first read() will return -1.
*
* As a consequence, the connection state of the relay server would be invalid temporarily (we
* would switch to CONNECTED state then switch back to DISCONNECTED).
*
* To avoid this problem, we must actually read from the server, so that an error occurs
* immediately if the relay server is not accessible.
*
* Therefore, the relay server immediately sends the client id: consume it and log it.
*
* @param inputStream the input stream to receive data from the relay server
* @throws IOException if an I/O error occurs
*/
private static void readClientId(InputStream inputStream) throws IOException {
Log.d(TAG, "Requesting client id");
int clientId = new DataInputStream(inputStream).readInt();
Log.d(TAG, "Connected to the relay server as #" + Binary.unsigned(clientId));
}
@Override
public void send(byte[] packet, int len) throws IOException {
if (GnirehtetService.VERBOSE) {
Log.v(TAG, "Sending packet: " + Binary.buildPacketString(packet, len));
}
localSocket.getOutputStream().write(packet, 0, len);
}
@Override
public int receive(byte[] packet) throws IOException {
int r = localSocket.getInputStream().read(packet);
if (GnirehtetService.VERBOSE) {
Log.v(TAG, "Receiving packet: " + Binary.buildPacketString(packet, r));
}
return r;
}
@Override
public void close() {
try {
if (localSocket.getFileDescriptor() != null) {
// close the streams to interrupt pending read and writes
localSocket.shutdownInput();
localSocket.shutdownOutput();
}
localSocket.close();
} catch (IOException e) {
// what could we do?
throw new RuntimeException(e);
}
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/RelayTunnelListener.java
================================================
package com.genymobile.gnirehtet;
import android.os.Handler;
/**
* Convenient wrapper to dispatch events to the given {@link Handler}.
*/
public class RelayTunnelListener {
static final int MSG_RELAY_TUNNEL_CONNECTED = 0;
static final int MSG_RELAY_TUNNEL_DISCONNECTED = 1;
private final Handler handler;
public RelayTunnelListener(Handler handler) {
this.handler = handler;
}
public void notifyRelayTunnelConnected() {
handler.sendEmptyMessage(MSG_RELAY_TUNNEL_CONNECTED);
}
public void notifyRelayTunnelDisconnected() {
handler.sendEmptyMessage(MSG_RELAY_TUNNEL_DISCONNECTED);
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/RelayTunnelProvider.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.net.VpnService;
import java.io.IOException;
/**
* Provide a valid {@link RelayTunnel}, creating a new one if necessary.
*/
public class RelayTunnelProvider {
private static final int DELAY_BETWEEN_ATTEMPTS_MS = 5000;
private final Object getCurrentTunnelLock = new Object(); // protects getCurrentTunnel()
private final VpnService vpnService;
private final RelayTunnelListener listener;
private RelayTunnel tunnel; // protected both by "this" and "getCurrentTunnelLock"
private boolean first = true; // protected by "getCurrentTunnelLock"
private long lastFailureTimestamp; // protected by "this"
public RelayTunnelProvider(VpnService vpnService, RelayTunnelListener listener) {
this.vpnService = vpnService;
this.listener = listener;
}
public RelayTunnel getCurrentTunnel() throws IOException, InterruptedException {
/*
* To make sure that both the sending and receiving threads use the same tunnel, we must
* guarantee that this method may not be called several times concurrently.
*
* However, since it executes potentially long-running blocking calls, we still want to be
* able to call invalidateTunnel() concurrently, which requires to protect some fields.
*
* Therefore, use one mutex ("getCurrentTunnelLock") to avoid concurrent calls to
* getCurrentTunnel(), and another one ("this") to protect fields shared with
* invalidateTunnel().
*/
synchronized (getCurrentTunnelLock) {
synchronized (this) {
if (tunnel != null) {
return tunnel;
}
waitUntilNextAttemptSlot();
// "tunnel" has not changed during waiting (only getCurrentTunnel() may write it)
tunnel = RelayTunnel.open(vpnService);
}
// the first connection must either notify "connected" or "disconnected"
boolean notifyDisconnectedOnError = first;
first = false;
connectTunnel(notifyDisconnectedOnError);
}
return tunnel;
}
private void connectTunnel(boolean notifyDisconnectedOnError) throws IOException {
try {
tunnel.connect();
notifyConnected();
} catch (IOException e) {
touchFailure();
if (notifyDisconnectedOnError) {
notifyDisconnected();
}
throw e;
}
}
public synchronized void invalidateTunnel() {
if (tunnel != null) {
touchFailure();
tunnel.close();
tunnel = null;
notifyDisconnected();
}
}
/**
* Call {@link #invalidateTunnel()} only if {@code tunnelToInvalidate} is the current tunnel (or
* is {@code null}).
*
* @param tunnelToInvalidate the tunnel to invalidate
*/
public synchronized void invalidateTunnel(Tunnel tunnelToInvalidate) {
if (tunnel == tunnelToInvalidate || tunnelToInvalidate == null) {
invalidateTunnel();
}
}
private synchronized void touchFailure() {
lastFailureTimestamp = System.currentTimeMillis();
}
private void waitUntilNextAttemptSlot() throws InterruptedException {
if (first) {
// do not wait on first attempt
return;
}
long delay = lastFailureTimestamp + DELAY_BETWEEN_ATTEMPTS_MS - System.currentTimeMillis();
while (delay > 0) {
wait(delay);
delay = lastFailureTimestamp + DELAY_BETWEEN_ATTEMPTS_MS - System.currentTimeMillis();
}
}
private void notifyConnected() {
if (listener != null) {
listener.notifyRelayTunnelConnected();
}
}
private void notifyDisconnected() {
if (listener != null) {
listener.notifyRelayTunnelDisconnected();
}
}
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/Tunnel.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import java.io.IOException;
public interface Tunnel {
// blocking
void send(byte[] packet, int len) throws IOException;
// blocking
int receive(byte[] packet) throws IOException;
// blocking
void close();
}
================================================
FILE: app/src/main/java/com/genymobile/gnirehtet/VpnConfiguration.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import android.os.Parcel;
import android.os.Parcelable;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class VpnConfiguration implements Parcelable {
private final InetAddress[] dnsServers;
private final CIDR[] routes;
public VpnConfiguration() {
this.dnsServers = new InetAddress[0];
this.routes = new CIDR[0];
}
public VpnConfiguration(InetAddress[] dnsServers, CIDR[] routes) {
this.dnsServers = dnsServers;
this.routes = routes;
}
private VpnConfiguration(Parcel source) {
int dnsCount = source.readInt();
dnsServers = new InetAddress[dnsCount];
try {
for (int i = 0; i < dnsCount; ++i) {
dnsServers[i] = InetAddress.getByAddress(source.createByteArray());
}
} catch (UnknownHostException e) {
throw new AssertionError("Invalid address", e);
}
routes = source.createTypedArray(CIDR.CREATOR);
}
public InetAddress[] getDnsServers() {
return dnsServers;
}
public CIDR[] getRoutes() {
return routes;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(dnsServers.length);
for (InetAddress addr : dnsServers) {
dest.writeByteArray(addr.getAddress());
}
dest.writeTypedArray(routes, 0);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator CREATOR = new Creator() {
@Override
public VpnConfiguration createFromParcel(Parcel source) {
return new VpnConfiguration(source);
}
@Override
public VpnConfiguration[] newArray(int size) {
return new VpnConfiguration[size];
}
};
}
================================================
FILE: app/src/main/res/drawable/ic_close_24dp.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_report_problem_24dp.xml
================================================
================================================
FILE: app/src/main/res/drawable/ic_usb_24dp.xml
================================================
================================================
FILE: app/src/main/res/values/strings.xml
================================================
GnirehtetReverse tethering enabledDisconnected from the relay serverStop Gnirehtet
================================================
FILE: app/src/main/res/values/styles.xml
================================================
================================================
FILE: app/src/main/res/values-fr/strings.xml
================================================
Reverse tethering activéDéconnecté du serveur relaisArrêter Gnirehtet
================================================
FILE: app/src/test/java/com/genymobile/gnirehtet/TestIPPacketOutputSteam.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@SuppressWarnings("checkstyle:MagicNumber")
public class TestIPPacketOutputSteam {
private ByteBuffer createMockPacket() {
ByteBuffer buffer = ByteBuffer.allocate(32);
writeMockPacketTo(buffer);
buffer.flip();
return buffer;
}
private void writeMockPacketTo(ByteBuffer buffer) {
buffer.put((byte) ((4 << 4) | 5)); // versionAndIHL
buffer.put((byte) 0); // ToS
buffer.putShort((short) 32); // total length 20 + 8 + 4
buffer.putInt(0); // IdFlagsFragmentOffset
buffer.put((byte) 0); // TTL
buffer.put((byte) 17); // protocol (UDP)
buffer.putShort((short) 0); // checksum
buffer.putInt(0x12345678); // source address
buffer.putInt(0x42424242); // destination address
buffer.putShort((short) 1234); // source port
buffer.putShort((short) 5678); // destination port
buffer.putShort((short) 12); // length
buffer.putShort((short) 0); // checksum
buffer.putInt(0x11223344); // payload
}
@Test
public void testSimplePacket() throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
IPPacketOutputStream pos = new IPPacketOutputStream(bos);
byte[] rawPacket = createMockPacket().array();
pos.write(rawPacket, 0, 14);
Assert.assertEquals("Partial packet should not be written", 0, bos.size());
pos.write(rawPacket, 14, 14);
Assert.assertEquals("Partial packet should not be written", 0, bos.size());
pos.write(rawPacket, 28, 4);
Assert.assertEquals("Complete packet should be written", 32, bos.size());
byte[] result = bos.toByteArray();
Assert.assertTrue("Resulting array must be identical", Arrays.equals(rawPacket, result));
}
@Test
public void testSeveralPacketsAtOnce() throws IOException {
class CapturingOutputStream extends ByteArrayOutputStream {
private int packetCount;
@Override
public void write(byte[] b, int off, int len) {
super.write(b, off, len);
++packetCount;
}
}
CapturingOutputStream cos = new CapturingOutputStream();
IPPacketOutputStream pos = new IPPacketOutputStream(cos);
ByteBuffer buffer = ByteBuffer.allocate(3 * 32);
for (int i = 0; i < 3; ++i) {
writeMockPacketTo(buffer);
}
byte[] rawPackets = buffer.array();
pos.write(rawPackets, 0, 70); // 2 packets + 6 bytes
Assert.assertEquals("Exactly 2 packets should have been written", 64, cos.size());
Assert.assertEquals("Packets should be written individually to the target", 2, cos.packetCount);
pos.write(rawPackets, 70, 26);
Assert.assertEquals("Exactly 3 packets should have been written", 96, cos.size());
Assert.assertEquals("Packets should be written individually to the target", 3, cos.packetCount);
}
}
================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.
ext {
compileSdkVersion = 28
buildToolsVersion = "28.0.3"
}
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
google()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
task debugJava(dependsOn: [':app:assembleDebug', ':relay-java:assembleDebug'])
task releaseJava(dependsOn: [':app:assembleRelease', ':relay-java:assembleRelease'])
task debugRust(dependsOn: [':app:assembleDebug', ':relay-rust:debug'])
task releaseRust(dependsOn: [':app:assembleRelease', ':relay-rust:release'])
task releaseRustWindows(dependsOn: [':app:assembleRelease', 'relay-rust:releaseCrossToWindows'])
task debugAll(dependsOn: ['debugJava', 'debugRust'])
task releaseAll(dependsOn: ['releaseJava', 'releaseRust'])
task checkJava(dependsOn: [':app:check', ':relay-java:check'])
task checkRust(dependsOn: ['app:check', ':relay-rust:check'])
task checkAll(dependsOn: ['checkJava', 'checkRust'])
================================================
FILE: config/android-checkstyle.gradle
================================================
apply plugin: 'checkstyle'
check.dependsOn 'checkstyle'
checkstyle {
toolVersion = '6.19'
}
task checkstyle(type: Checkstyle) {
description = "Check Java style with Checkstyle"
configFile = rootProject.file("config/checkstyle/checkstyle.xml")
source = javaSources()
classpath = files()
ignoreFailures = true
}
def javaSources() {
def files = []
android.sourceSets.each { sourceSet ->
sourceSet.java.each { javaSource ->
javaSource.getSrcDirs().each {
if (it.exists()) {
files.add(it)
}
}
}
}
return files
}
================================================
FILE: config/android-signing.gradle
================================================
if (project.hasProperty("RELEASE_STORE_FILE")) {
android.signingConfigs {
release {
// to be defined in gradle.properties
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
android.buildTypes.release.signingConfig = android.signingConfigs.release
}
================================================
FILE: config/checkstyle/checkstyle.xml
================================================
================================================
FILE: config/java-checkstyle.gradle
================================================
apply plugin: 'checkstyle'
checkstyle {
toolVersion = '6.19'
configFile = rootProject.file("config/checkstyle/checkstyle.xml")
}
================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Sat Sep 07 21:43:49 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip
================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
================================================
FILE: gradlew
================================================
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
================================================
FILE: relay-java/build.gradle
================================================
apply plugin: 'application'
mainClassName = 'com.genymobile.gnirehtet.Main'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
}
jar {
manifest {
attributes(
'Main-Class': mainClassName
)
}
baseName 'gnirehtet'
}
task assembleDebug(dependsOn: 'jar')
task assembleRelease(dependsOn: ['build', 'jar'])
apply from: "$project.rootDir/config/java-checkstyle.gradle"
test {
// to log using System.out.println(…) in tests
testLogging.showStandardStreams = true
}
================================================
FILE: relay-java/scripts/gnirehtet
================================================
#!/bin/bash
java -jar gnirehtet.jar "$@"
================================================
FILE: relay-java/scripts/gnirehtet-run.cmd
================================================
@java -jar gnirehtet.jar run
@pause
================================================
FILE: relay-java/scripts/gnirehtet.cmd
================================================
@java -jar gnirehtet.jar %*
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/AdbMonitor.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import com.genymobile.gnirehtet.relay.Log;
import java.io.EOFException;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class AdbMonitor {
public interface AdbDevicesCallback {
void onNewDeviceConnected(String serial);
}
private static final String TAG = AdbMonitor.class.getSimpleName();
private static final int ADBD_PORT = 5037;
private static final String TRACK_DEVICES_REQUEST = "0012host:track-devices";
private static final int BUFFER_SIZE = 1024;
private static final int LENGTH_FIELD_SIZE = 4;
private static final int OKAY_SIZE = 4;
private static final long RETRY_DELAY_ADB_DAEMON_OK = 1000;
private static final long RETRY_DELAY_ADB_DAEMON_KO = 5000;
private List connectedDevices = new ArrayList<>();
private AdbDevicesCallback callback;
private static final byte[] BUFFER = new byte[BUFFER_SIZE]; // used only locally to avoid allocations, so static is ok
private final ByteBuffer socketBuffer = ByteBuffer.allocate(BUFFER_SIZE);
public AdbMonitor(AdbDevicesCallback callback) {
this.callback = callback;
}
public void monitor() {
while (true) {
try {
trackDevices();
} catch (Exception e) {
Log.e(TAG, "Failed to monitor adb devices", e);
repairAdbDaemon();
}
}
}
private void trackDevices() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
try {
socketChannel.connect(new InetSocketAddress(Inet4Address.getLoopbackAddress(), ADBD_PORT));
trackDevicesOnChannel(socketChannel);
} finally {
socketChannel.close();
}
}
private void trackDevicesOnChannel(ByteChannel channel) throws IOException {
socketBuffer.clear();
writeRequest(channel, TRACK_DEVICES_REQUEST);
// the daemon initially sends "OKAY" if it understands the request
if (!consumeOkay(channel)) {
return;
}
while (true) {
String packet = nextPacket(channel);
handlePacket(packet);
}
}
private static void writeRequest(WritableByteChannel channel, String request) throws IOException {
ByteBuffer requestBuffer = ByteBuffer.wrap(request.getBytes(StandardCharsets.US_ASCII));
channel.write(requestBuffer);
}
private boolean consumeOkay(ReadableByteChannel channel) throws IOException {
while (channel.read(socketBuffer) != -1) {
if (socketBuffer.position() < OKAY_SIZE) {
// not enough data
continue;
}
socketBuffer.flip();
socketBuffer.get(BUFFER, 0, OKAY_SIZE);
socketBuffer.compact();
socketBuffer.flip();
String text = new String(BUFFER, 0, OKAY_SIZE, StandardCharsets.US_ASCII);
return "OKAY".equals(text);
}
return false;
}
private String nextPacket(ReadableByteChannel channel) throws IOException {
String packet;
while ((packet = readPacket(socketBuffer)) == null) {
// need more data
fillBufferFrom(channel);
}
return packet;
}
private void fillBufferFrom(ReadableByteChannel channel) throws IOException {
socketBuffer.compact();
int r;
if (channel.read(socketBuffer) == -1) {
throw new EOFException("ADB daemon closed the track-devices connexion");
}
socketBuffer.flip();
}
static String readPacket(ByteBuffer input) {
if (input.remaining() < LENGTH_FIELD_SIZE) {
return null;
}
// each packet contains 4 bytes representing the String length in hexa, followed by a list of device states, one per line;
// each line contains: the device serial, `\t', the state, '\n'
// for example: "00360123456789abcdef\tdevice\nfedcba9876543210\tunauthorized\n":
// - 0036 indicates that the data is 0x36 (54) bytes length
// - the device with serial 0123456789abcdef is connected
// - the device with serial fedcba9876543210 is unauthorized
input.get(BUFFER, 0, LENGTH_FIELD_SIZE);
int length = parseLength(BUFFER);
if (length > BUFFER.length) {
throw new IllegalArgumentException("Packet size should not be that big: " + length);
}
if (input.remaining() < length) {
// not enough data
input.rewind();
return null;
}
input.get(BUFFER, 0, length);
return new String(BUFFER, 0, length, StandardCharsets.UTF_8);
}
void handlePacket(String packet) {
List currentConnectedDevices = parseConnectedDevices(packet);
for (String serial : currentConnectedDevices) {
if (!connectedDevices.contains(serial)) {
callback.onNewDeviceConnected(serial);
}
}
connectedDevices = currentConnectedDevices;
}
private static List parseConnectedDevices(String packet) {
List list = new ArrayList<>();
for (String line : packet.split("\\n")) {
String[] tokens = line.split("\\s+");
if (tokens.length == 2) {
String state = tokens[1];
if ("device".equals(state)) {
String serial = tokens[0];
list.add(serial);
}
}
}
return list;
}
@SuppressWarnings("checkstyle:MagicNumber")
private static int parseLength(byte[] data) {
if (data.length < LENGTH_FIELD_SIZE) {
throw new IllegalArgumentException("Length field must be at least 4 bytes length");
}
int result = 0;
for (int i = 0; i < LENGTH_FIELD_SIZE; ++i) {
char c = (char) data[i];
result = (result << 4) + Character.digit(c, 0x10);
}
return result;
}
private static void repairAdbDaemon() {
if (startAdbDaemon()) {
sleep(RETRY_DELAY_ADB_DAEMON_OK);
} else {
sleep(RETRY_DELAY_ADB_DAEMON_KO);
}
}
private static boolean startAdbDaemon() {
Log.i(TAG, "Restarting adb deamon");
try {
Process process = new ProcessBuilder("adb", "start-server")
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
.redirectError(ProcessBuilder.Redirect.INHERIT).start();
int exitCode = process.waitFor();
if (exitCode != 0) {
Log.e(TAG, "Could not restart adb daemon (exited on error)");
return false;
}
return true;
} catch (InterruptedException | IOException e) {
Log.e(TAG, "Could not restart adb daemon", e);
return false;
}
}
private static void sleep(long delay) {
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
// should never happen
}
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/CommandLineArguments.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
/**
* Simple specific command-line arguments parser.
*/
@SuppressWarnings("checkstyle:MagicNumber")
public class CommandLineArguments {
public static final int PARAM_NONE = 0;
public static final int PARAM_SERIAL = 1;
public static final int PARAM_DNS_SERVER = 1 << 1;
public static final int PARAM_ROUTES = 1 << 2;
public static final int PARAM_PORT = 1 << 3;
public static final int DEFAULT_PORT = 31416;
private int port;
private String serial;
private String dnsServers;
private String routes;
public static CommandLineArguments parse(int acceptedParameters, String... args) {
CommandLineArguments arguments = new CommandLineArguments();
for (int i = 0; i < args.length; ++i) {
String arg = args[i];
if ((acceptedParameters & PARAM_DNS_SERVER) != 0 && "-d".equals(arg)) {
if (arguments.dnsServers != null) {
throw new IllegalArgumentException("DNS servers already set");
}
if (i == args.length - 1) {
throw new IllegalArgumentException("Missing -d parameter");
}
arguments.dnsServers = args[i + 1];
++i; // consume the -d parameter
} else if ((acceptedParameters & PARAM_ROUTES) != 0 && "-r".equals(arg)) {
if (arguments.routes != null) {
throw new IllegalArgumentException("Routes already set");
}
if (i == args.length - 1) {
throw new IllegalArgumentException("Missing -r parameter");
}
arguments.routes = args[i + 1];
++i; // consume the -r parameter
} else if ((acceptedParameters & PARAM_PORT) != 0 && "-p".equals(arg)) {
if (arguments.port != 0) {
throw new IllegalArgumentException("Port already set");
}
if (i == args.length - 1) {
throw new IllegalArgumentException("Missing -p parameter");
}
arguments.port = Integer.parseInt(args[i + 1]);
if (arguments.port <= 0 || arguments.port >= 65536) {
throw new IllegalArgumentException("Invalid port: " + arguments.port);
}
++i;
} else if ((acceptedParameters & PARAM_SERIAL) != 0 && arguments.serial == null) {
arguments.serial = arg;
} else {
throw new IllegalArgumentException("Unexpected argument: \"" + arg + "\"");
}
}
if (arguments.port == 0) {
arguments.port = DEFAULT_PORT;
}
return arguments;
}
public String getSerial() {
return serial;
}
public String getDnsServers() {
return dnsServers;
}
public String getRoutes() {
return routes;
}
public int getPort() {
return port;
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/Main.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet;
import com.genymobile.gnirehtet.relay.CommandExecutionException;
import com.genymobile.gnirehtet.relay.Log;
import com.genymobile.gnirehtet.relay.Relay;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class Main {
private static final String TAG = "Gnirehtet";
private static final String NL = System.lineSeparator();
private static final String REQUIRED_APK_VERSION_CODE = "9";
private Main() {
// not instantiable
}
private static String getAdbPath() {
String adb = System.getenv("ADB");
return adb != null ? adb : "adb";
}
private static String getApkPath() {
String apk = System.getenv("GNIREHTET_APK");
return apk != null ? apk : "gnirehtet.apk";
}
enum Command {
INSTALL("install", CommandLineArguments.PARAM_SERIAL) {
@Override
String getDescription() {
return "Install the client on the Android device and exit.\n"
+ "If several devices are connected via adb, then serial must be\n"
+ "specified.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdInstall(args.getSerial());
}
},
UNINSTALL("uninstall", CommandLineArguments.PARAM_SERIAL) {
@Override
String getDescription() {
return "Uninstall the client from the Android device and exit.\n"
+ "If several devices are connected via adb, then serial must be\n"
+ "specified.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdUninstall(args.getSerial());
}
},
REINSTALL("reinstall", CommandLineArguments.PARAM_SERIAL) {
@Override
String getDescription() {
return "Uninstall then install.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdReinstall(args.getSerial());
}
},
RUN("run", CommandLineArguments.PARAM_SERIAL | CommandLineArguments.PARAM_DNS_SERVER | CommandLineArguments.PARAM_ROUTES
| CommandLineArguments.PARAM_PORT) {
@Override
String getDescription() {
return "Enable reverse tethering for exactly one device:\n"
+ " - install the client if necessary;\n"
+ " - start the client;\n"
+ " - start the relay server;\n"
+ " - on Ctrl+C, stop both the relay server and the client.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdRun(args.getSerial(), args.getDnsServers(), args.getRoutes(), args.getPort());
}
},
AUTORUN("autorun", CommandLineArguments.PARAM_DNS_SERVER | CommandLineArguments.PARAM_ROUTES | CommandLineArguments.PARAM_PORT) {
@Override
String getDescription() {
return "Enable reverse tethering for all devices:\n"
+ " - monitor devices and start clients (autostart);\n"
+ " - start the relay server.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdAutorun(args.getDnsServers(), args.getRoutes(), args.getPort());
}
},
START("start", CommandLineArguments.PARAM_SERIAL | CommandLineArguments.PARAM_DNS_SERVER | CommandLineArguments.PARAM_ROUTES
| CommandLineArguments.PARAM_PORT) {
@Override
String getDescription() {
return "Start a client on the Android device and exit.\n"
+ "If several devices are connected via adb, then serial must be\n"
+ "specified.\n"
+ "If -d is given, then make the Android device use the specified\n"
+ "DNS server(s). Otherwise, use 8.8.8.8 (Google public DNS).\n"
+ "If -r is given, then only reverse tether the specified routes.\n"
+ "If -p is given, then make the relay server listen on the specified\n"
+ "port. Otherwise, use port 31416.\n"
+ "Otherwise, use 0.0.0.0/0 (redirect the whole traffic).\n"
+ "If the client is already started, then do nothing, and ignore\n"
+ "the other parameters.\n"
+ "10.0.2.2 is mapped to the host 'localhost'.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdStart(args.getSerial(), args.getDnsServers(), args.getRoutes(), args.getPort());
}
},
AUTOSTART("autostart", CommandLineArguments.PARAM_DNS_SERVER | CommandLineArguments.PARAM_ROUTES | CommandLineArguments.PARAM_PORT) {
@Override
String getDescription() {
return "Listen for device connexions and start a client on every detected\n"
+ "device.\n"
+ "Accept the same parameters as the start command (excluding the\n"
+ "serial, which will be taken from the detected device).";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdAutostart(args.getDnsServers(), args.getRoutes(), args.getPort());
}
},
STOP("stop", CommandLineArguments.PARAM_SERIAL) {
@Override
String getDescription() {
return "Stop the client on the Android device and exit.\n"
+ "If several devices are connected via adb, then serial must be\n"
+ "specified.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdStop(args.getSerial());
}
},
RESTART("restart", CommandLineArguments.PARAM_SERIAL | CommandLineArguments.PARAM_DNS_SERVER | CommandLineArguments.PARAM_ROUTES
| CommandLineArguments.PARAM_PORT) {
@Override
String getDescription() {
return "Stop then start.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdRestart(args.getSerial(), args.getDnsServers(), args.getRoutes(), args.getPort());
}
},
TUNNEL("tunnel", CommandLineArguments.PARAM_SERIAL | CommandLineArguments.PARAM_PORT) {
@Override
String getDescription() {
return "Set up the 'adb reverse' tunnel.\n"
+ "If a device is unplugged then plugged back while gnirehtet is\n"
+ "active, resetting the tunnel is sufficient to get the\n"
+ "connection back.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdTunnel(args.getSerial(), args.getPort());
}
},
RELAY("relay", CommandLineArguments.PARAM_PORT) {
@Override
String getDescription() {
return "Start the relay server in the current terminal.";
}
@Override
void execute(CommandLineArguments args) throws Exception {
cmdRelay(args.getPort());
}
};
private String command;
private int acceptedParameters;
Command(String command, int acceptedParameters) {
this.command = command;
this.acceptedParameters = acceptedParameters;
}
abstract String getDescription();
abstract void execute(CommandLineArguments args) throws Exception;
}
private static void cmdInstall(String serial) throws InterruptedException, IOException, CommandExecutionException {
Log.i(TAG, "Installing gnirehtet client...");
execAdb(serial, "install", "-r", getApkPath());
}
private static void cmdUninstall(String serial) throws InterruptedException, IOException, CommandExecutionException {
Log.i(TAG, "Uninstalling gnirehtet client...");
execAdb(serial, "uninstall", "com.genymobile.gnirehtet");
}
private static void cmdReinstall(String serial) throws InterruptedException, IOException, CommandExecutionException {
cmdUninstall(serial);
cmdInstall(serial);
}
private static void cmdRun(String serial, String dnsServers, String routes, int port) throws IOException {
// start in parallel so that the relay server is ready when the client connects
asyncStart(serial, dnsServers, routes, port);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// executed on Ctrl+C
try {
cmdStop(serial);
} catch (Exception e) {
Log.e(TAG, "Cannot stop client", e);
}
}));
cmdRelay(port);
}
private static void cmdAutorun(final String dnsServers, final String routes, int port) throws IOException {
new Thread(() -> {
try {
cmdAutostart(dnsServers, routes, port);
} catch (Exception e) {
Log.e(TAG, "Cannot auto start clients", e);
}
}).start();
cmdRelay(port);
}
@SuppressWarnings("checkstyle:MagicNumber")
private static void cmdStart(String serial, String dnsServers, String routes, int port) throws InterruptedException, IOException,
CommandExecutionException {
if (mustInstallClient(serial)) {
cmdInstall(serial);
// wait a bit after the app is installed so that intent actions are correctly registered
Thread.sleep(500); // ms
}
Log.i(TAG, "Starting client...");
cmdTunnel(serial, port);
List cmd = new ArrayList<>();
Collections.addAll(cmd, "shell", "am", "start", "-a", "com.genymobile.gnirehtet.START", "-n",
"com.genymobile.gnirehtet/.GnirehtetActivity");
if (dnsServers != null) {
Collections.addAll(cmd, "--esa", "dnsServers", dnsServers);
}
if (routes != null) {
Collections.addAll(cmd, "--esa", "routes", routes);
}
execAdb(serial, cmd);
}
private static void cmdAutostart(final String dnsServers, final String routes, int port) {
AdbMonitor adbMonitor = new AdbMonitor((serial) -> {
asyncStart(serial, dnsServers, routes, port);
});
adbMonitor.monitor();
}
private static void cmdStop(String serial) throws InterruptedException, IOException, CommandExecutionException {
Log.i(TAG, "Stopping client...");
execAdb(serial, "shell", "am", "start", "-a", "com.genymobile.gnirehtet.STOP", "-n",
"com.genymobile.gnirehtet/.GnirehtetActivity");
}
private static void cmdRestart(String serial, String dnsServers, String routes, int port) throws InterruptedException, IOException,
CommandExecutionException {
cmdStop(serial);
cmdStart(serial, dnsServers, routes, port);
}
private static void cmdTunnel(String serial, int port) throws InterruptedException, IOException, CommandExecutionException {
execAdb(serial, "reverse", "localabstract:gnirehtet", "tcp:" + port);
}
private static void cmdRelay(int port) throws IOException {
Log.i(TAG, "Starting relay server on port " + port + "...");
new Relay(port).run();
}
private static void asyncStart(String serial, String dnsServers, String routes, int port) {
new Thread(() -> {
try {
cmdStart(serial, dnsServers, routes, port);
} catch (Exception e) {
Log.e(TAG, "Cannot start client", e);
}
}).start();
}
private static void execAdb(String serial, String... adbArgs) throws InterruptedException, IOException, CommandExecutionException {
execSync(createAdbCommand(serial, adbArgs));
}
private static List createAdbCommand(String serial, String... adbArgs) {
List command = new ArrayList<>();
command.add(getAdbPath());
if (serial != null) {
command.add("-s");
command.add(serial);
}
Collections.addAll(command, adbArgs);
return command;
}
private static void execAdb(String serial, List adbArgList) throws InterruptedException, IOException, CommandExecutionException {
String[] adbArgs = adbArgList.toArray(new String[adbArgList.size()]);
execAdb(serial, adbArgs);
}
private static void execSync(List command) throws InterruptedException, IOException, CommandExecutionException {
Log.d(TAG, "Execute: " + command);
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT).redirectError(ProcessBuilder.Redirect.INHERIT);
Process process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new CommandExecutionException(command, exitCode);
}
}
private static boolean mustInstallClient(String serial) throws InterruptedException, IOException, CommandExecutionException {
Log.i(TAG, "Checking gnirehtet client...");
List command = createAdbCommand(serial, "shell", "dumpsys", "package", "com.genymobile.gnirehtet");
Log.d(TAG, "Execute: " + command);
Process process = new ProcessBuilder(command).start();
try {
Scanner scanner = new Scanner(process.getInputStream());
// read the versionCode of the installed package
Pattern pattern = Pattern.compile("^ versionCode=(\\p{Digit}+).*");
while (scanner.hasNextLine()) {
Matcher matcher = pattern.matcher(scanner.nextLine());
if (matcher.matches()) {
String installedVersionCode = matcher.group(1);
return !REQUIRED_APK_VERSION_CODE.equals(installedVersionCode);
}
}
} finally {
int exitCode = process.waitFor();
if (exitCode != 0) {
// Overwrite any pending exception, the command just failed
throw new CommandExecutionException(command, exitCode);
}
}
return true;
}
private static void printUsage() {
StringBuilder builder = new StringBuilder("Syntax: gnirehtet (");
Command[] commands = Command.values();
for (int i = 0; i < commands.length; ++i) {
if (i != 0) {
builder.append('|');
}
builder.append(commands[i].command);
}
builder.append(") ...").append(NL);
for (Command command : commands) {
builder.append(NL);
appendCommandUsage(builder, command);
}
System.err.print(builder.toString());
}
private static void appendCommandUsage(StringBuilder builder, Command command) {
builder.append(" gnirehtet ").append(command.command);
if ((command.acceptedParameters & CommandLineArguments.PARAM_SERIAL) != 0) {
builder.append(" [serial]");
}
if ((command.acceptedParameters & CommandLineArguments.PARAM_DNS_SERVER) != 0) {
builder.append(" [-d DNS[,DNS2,...]]");
}
if ((command.acceptedParameters & CommandLineArguments.PARAM_PORT) != 0) {
builder.append(" [-p PORT]");
}
if ((command.acceptedParameters & CommandLineArguments.PARAM_ROUTES) != 0) {
builder.append(" [-r ROUTE[,ROUTE2,...]]");
}
builder.append(NL);
String[] descLines = command.getDescription().split("\n");
for (String descLine : descLines) {
builder.append(" ").append(descLine).append(NL);
}
}
private static void printCommandUsage(Command command) {
StringBuilder builder = new StringBuilder();
appendCommandUsage(builder, command);
System.err.print(builder.toString());
}
public static void main(String... args) throws Exception {
if (args.length == 0) {
printUsage();
return;
}
String cmd = args[0];
for (Command command : Command.values()) {
if (cmd.equals(command.command)) {
// forget args[0] containing the command name
String[] commandArgs = Arrays.copyOfRange(args, 1, args.length);
CommandLineArguments arguments;
try {
arguments = CommandLineArguments.parse(command.acceptedParameters, commandArgs);
} catch (IllegalArgumentException e) {
Log.e(TAG, e.getMessage());
printCommandUsage(command);
return;
}
command.execute(arguments);
return;
}
}
if ("rt".equals(cmd)) {
Log.e(TAG, "The 'rt' command has been renamed to 'run'. Try 'gnirehtet run' instead.");
printCommandUsage(Command.RUN);
} else {
Log.e(TAG, "Unknown command: " + cmd);
printUsage();
}
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/AbstractConnection.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.net.InetAddress;
import java.net.InetSocketAddress;
public abstract class AbstractConnection implements Connection {
private static final int LOCALHOST_FORWARD = 0x0a000202; // 10.0.2.2 must be forwarded to localhost
private final ConnectionId id;
private final Client client;
protected AbstractConnection(ConnectionId id, Client client) {
this.id = id;
this.client = client;
}
@Override
public ConnectionId getId() {
return id;
}
protected void close() {
disconnect();
client.getRouter().remove(this);
}
protected void consume(PacketSource source) {
client.consume(source);
}
protected boolean sendToClient(IPv4Packet packet) {
return client.sendToClient(packet);
}
private static InetAddress getRewrittenAddress(int ip) {
return ip == LOCALHOST_FORWARD ? InetAddress.getLoopbackAddress() : Net.toInetAddress(ip);
}
/**
* Get destination, rewritten to {@code localhost} if it was {@code 10.0.2.2}.
*
* @return Destination to connect to.
*/
protected InetSocketAddress getRewrittenDestination() {
int destIp = id.getDestinationIp();
int port = id.getDestinationPort();
return new InetSocketAddress(getRewrittenAddress(destIp), port);
}
public void logv(String tag, String message, Throwable e) {
Log.v(tag, id + " " + message);
}
public void logv(String tag, String message) {
logv(tag, message, null);
}
public void logd(String tag, String message, Throwable e) {
Log.d(tag, id + " " + message);
}
public void logd(String tag, String message) {
logd(tag, message, null);
}
public void logi(String tag, String message, Throwable e) {
Log.i(tag, id + " " + message);
}
public void logi(String tag, String message) {
logi(tag, message, null);
}
public void logw(String tag, String message, Throwable e) {
Log.w(tag, id + " " + message);
}
public void logw(String tag, String message) {
logw(tag, message, null);
}
public void loge(String tag, String message, Throwable e) {
Log.e(tag, id + " " + message);
}
public void loge(String tag, String message) {
loge(tag, message, null);
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Binary.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.nio.ByteBuffer;
@SuppressWarnings("checkstyle:MagicNumber")
public final class Binary {
private static final int MAX_STRING_PACKET_SIZE = 20;
private Binary() {
// not instantiable
}
public static String buildPacketString(byte[] data, int offset, int len) {
int limit = Math.min(MAX_STRING_PACKET_SIZE, len);
StringBuilder builder = new StringBuilder();
builder.append('[').append(len).append(" bytes] ");
for (int i = 0; i < limit; ++i) {
if (i != 0) {
String sep = i % 4 == 0 ? " " : " ";
builder.append(sep);
}
builder.append(String.format("%02X", data[offset + i] & 0xff));
}
if (limit < len) {
builder.append(" ... +").append(len - limit).append(" bytes");
}
return builder.toString();
}
public static String buildPacketString(ByteBuffer buffer) {
return buildPacketString(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining());
}
public static ByteBuffer copy(ByteBuffer buffer) {
buffer.rewind();
ByteBuffer result = ByteBuffer.allocate(buffer.remaining());
result.put(buffer);
buffer.rewind();
result.flip();
return result;
}
public static ByteBuffer slice(ByteBuffer buffer, int offset, int length) {
// save
int position = buffer.position();
int limit = buffer.limit();
// slice
buffer.limit(offset + length).position(offset);
ByteBuffer result = buffer.slice();
// restore
buffer.limit(limit).position(position);
return result;
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Client.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class Client {
private static final String TAG = Client.class.getSimpleName();
private static int nextId = 0;
private final int id;
private final SocketChannel clientChannel;
private final SelectionKey selectionKey;
private final CloseListener closeListener;
private int interests;
private final IPv4PacketBuffer clientToNetwork = new IPv4PacketBuffer();
private final StreamBuffer networkToClient = new StreamBuffer(16 * IPv4Packet.MAX_PACKET_LENGTH);
private final Router router;
private final List pendingPacketSources = new ArrayList<>();
// store the remaining bytes of "id" to send to the client before relaying any data
private ByteBuffer pendingIdBuffer;
public Client(Selector selector, SocketChannel clientChannel, CloseListener closeListener) throws ClosedChannelException {
id = nextId++;
this.clientChannel = clientChannel;
router = new Router(this, selector);
pendingIdBuffer = createIntBuffer(id);
SelectionHandler selectionHandler = (selectionKey) -> {
if (selectionKey.isValid() && selectionKey.isWritable()) {
processSend();
}
if (selectionKey.isValid() && selectionKey.isReadable()) {
processReceive();
}
if (selectionKey.isValid()) {
updateInterests();
}
};
// on start, we are interested only in writing (we must first send the client id)
interests = SelectionKey.OP_WRITE;
selectionKey = clientChannel.register(selector, interests, selectionHandler);
this.closeListener = closeListener;
}
private static ByteBuffer createIntBuffer(int value) {
final int intSize = 4;
ByteBuffer buffer = ByteBuffer.allocate(intSize);
buffer.putInt(value);
buffer.flip();
return buffer;
}
public int getId() {
return id;
}
public Router getRouter() {
return router;
}
private void processReceive() {
if (!read()) {
close();
return;
}
pushToNetwork();
}
private void processSend() {
if (mustSendId()) {
if (!sendId()) {
close();
}
return;
}
if (!write()) {
close();
return;
}
processPending();
}
private boolean read() {
try {
return clientToNetwork.readFrom(clientChannel) != -1;
} catch (IOException e) {
Log.e(TAG, "Cannot read", e);
return false;
}
}
private boolean write() {
try {
return networkToClient.writeTo(clientChannel) != -1;
} catch (IOException e) {
Log.e(TAG, "Cannot write", e);
return false;
}
}
private boolean mustSendId() {
return pendingIdBuffer != null && pendingIdBuffer.hasRemaining();
}
private boolean sendId() {
assert mustSendId();
try {
if (clientChannel.write(pendingIdBuffer) == -1) {
Log.w(TAG, "Cannot write client id #" + id + " (EOF)");
return false;
}
if (!pendingIdBuffer.hasRemaining()) {
// we don't need this buffer anymore, release it
Log.d(TAG, "Client id #" + id + " sent to client");
pendingIdBuffer = null;
}
return true;
} catch (IOException e) {
Log.e(TAG, "Cannot write client id #" + id, e);
return false;
}
}
private void pushToNetwork() {
IPv4Packet packet;
while ((packet = clientToNetwork.asIPv4Packet()) != null) {
router.sendToNetwork(packet);
clientToNetwork.next();
}
}
private void close() {
selectionKey.cancel();
try {
clientChannel.close();
} catch (IOException e) {
Log.e(TAG, "Cannot close client connection", e);
}
router.clear();
closeListener.onClosed(this);
}
private void updateInterests() {
int interestOps = SelectionKey.OP_READ; // we always want to read
if (!networkToClient.isEmpty()) {
interestOps |= SelectionKey.OP_WRITE;
}
if (interests != interestOps) {
// interests must be changed
interests = interestOps;
selectionKey.interestOps(interestOps);
}
}
public boolean sendToClient(IPv4Packet packet) {
if (networkToClient.remaining() < packet.getRawLength()) {
Log.w(TAG, "Client buffer full");
return false;
}
networkToClient.readFrom(packet.getRaw());
updateInterests();
return true;
}
public void consume(PacketSource source) {
IPv4Packet packet = source.get();
if (sendToClient(packet)) {
source.next();
return;
}
assert !pendingPacketSources.contains(source);
pendingPacketSources.add(source);
}
private void processPending() {
Iterator iterator = pendingPacketSources.iterator();
while (iterator.hasNext()) {
PacketSource packetSource = iterator.next();
IPv4Packet packet = packetSource.get();
if (sendToClient(packet)) {
packetSource.next();
Log.d(TAG, "Pending packet sent to client (" + packet.getRawLength() + ")");
iterator.remove();
} else {
Log.w(TAG, "Pending packet not sent to client (" + packet.getRawLength() + "), client buffer full again");
return;
}
}
}
public void cleanExpiredConnections() {
router.cleanExpiredConnections();
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/CloseListener.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
public interface CloseListener {
void onClosed(T object);
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/CommandExecutionException.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.util.List;
public class CommandExecutionException extends Exception {
private List command;
private int exitCode;
public CommandExecutionException(List command, int exitCode) {
super(createMessage(command, exitCode));
this.command = command;
this.exitCode = exitCode;
}
private static String createMessage(List command, int exitCode) {
return "Command " + command + " returned with value " + exitCode;
}
public int getExitCode() {
return exitCode;
}
public List getCommand() {
return command;
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Connection.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
public interface Connection {
ConnectionId getId();
void sendToNetwork(IPv4Packet packet);
void disconnect();
boolean isExpired();
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/ConnectionId.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
public class ConnectionId {
private final IPv4Header.Protocol protocol;
private final int sourceIp;
private final short sourcePort;
private final int destIp;
private final short destPort;
private final String idString;
public ConnectionId(IPv4Header.Protocol protocol, int sourceIp, short sourcePort, int destIp, short destPort) {
this.protocol = protocol;
this.sourceIp = sourceIp;
this.sourcePort = sourcePort;
this.destIp = destIp;
this.destPort = destPort;
// compute the String representation only once
idString = protocol + " " + Net.toString(sourceIp, sourcePort) + " -> " + Net.toString(destIp, destPort);
}
public IPv4Header.Protocol getProtocol() {
return protocol;
}
public int getSourceIp() {
return sourceIp;
}
public int getSourcePort() {
return Short.toUnsignedInt(sourcePort);
}
public int getDestinationIp() {
return destIp;
}
public int getDestinationPort() {
return Short.toUnsignedInt(destPort);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ConnectionId that = (ConnectionId) o;
return sourceIp == that.sourceIp
&& sourcePort == that.sourcePort
&& destIp == that.destIp
&& destPort == that.destPort
&& protocol == that.protocol;
}
@Override
public int hashCode() {
int result = protocol.hashCode();
result = 31 * result + sourceIp;
result = 31 * result + (int) sourcePort;
result = 31 * result + destIp;
result = 31 * result + (int) destPort;
return result;
}
@Override
public String toString() {
return idString;
}
public static ConnectionId from(IPv4Header ipv4Header, TransportHeader transportHeader) {
IPv4Header.Protocol protocol = ipv4Header.getProtocol();
int sourceAddress = ipv4Header.getSource();
short sourcePort = (short) transportHeader.getSourcePort();
int destinationAddress = ipv4Header.getDestination();
short destinationPort = (short) transportHeader.getDestinationPort();
return new ConnectionId(protocol, sourceAddress, sourcePort, destinationAddress, destinationPort);
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/DatagramBuffer.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
/**
* Circular buffer to store datagrams (preserving their boundaries).
*
*
* circularBufferLength
* |<------------------------->| extra space for storing the last datagram in one block
* +---------------------------+------+
* | | |
* |[D4] [ D1 ][ D2 ][ D3 ] |
* +---------------------------+------+
* ^ ^
* head tail
*
*/
@SuppressWarnings("checkstyle:MagicNumber")
public class DatagramBuffer {
private static final String TAG = DatagramBuffer.class.getSimpleName();
// every datagram is stored along with a header storing its length, on 16 bits
private static final int HEADER_LENGTH = 2;
private static final int MAX_DATAGRAM_LENGTH = 1 << 16;
private static final int MAX_BLOCK_LENGTH = HEADER_LENGTH + MAX_DATAGRAM_LENGTH;
private final byte[] data;
private final ByteBuffer wrapper;
private int head;
private int tail;
private final int circularBufferLength;
public DatagramBuffer(int capacity) {
data = new byte[capacity + MAX_BLOCK_LENGTH];
wrapper = ByteBuffer.wrap(data);
circularBufferLength = capacity + 1;
}
public boolean isEmpty() {
return head == tail;
}
public boolean hasEnoughSpaceFor(int datagramLength) {
if (head >= tail) {
// there is at least the extra space for storing 1 packet
return true;
}
int remaining = tail - head - 1; // 1 extra byte to distinguish empty vs full
return HEADER_LENGTH + datagramLength <= remaining;
}
public int capacity() {
return circularBufferLength - 1;
}
public boolean writeTo(WritableByteChannel channel) throws IOException {
int length = readLength();
wrapper.limit(tail + length).position(tail);
tail += length;
if (tail >= circularBufferLength) {
tail = 0;
}
int w = channel.write(wrapper);
if (w != length) {
Log.e(TAG, "Cannot write the whole datagram to the channel (only " + w + "/" + length + ")");
return false;
}
return true;
}
public boolean readFrom(ByteBuffer buffer) {
int length = buffer.remaining();
if (length > MAX_DATAGRAM_LENGTH) {
throw new IllegalArgumentException("Datagram length (" + buffer.remaining() + ") may not be greater than "
+ MAX_DATAGRAM_LENGTH + " bytes");
}
if (!hasEnoughSpaceFor(length)) {
return false;
}
writeLength(length);
buffer.get(data, head, length);
head += length;
if (head >= circularBufferLength) {
head = 0;
}
return true;
}
private void writeLength(int length) {
assert (length & ~0xffff) == 0 : "Length must be stored on 16 bits";
data[head++] = (byte) ((length >> 8) & 0xff);
data[head++] = (byte) (length & 0xff);
}
private int readLength() {
int length = ((data[tail] & 0xff) << 8) | (data[tail + 1] & 0xff);
tail += 2;
return length;
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/IPv4Header.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.nio.ByteBuffer;
@SuppressWarnings("checkstyle:MagicNumber")
public class IPv4Header {
public enum Protocol {
TCP(6), UDP(17), OTHER(-1);
private final int number;
Protocol(int number) {
this.number = number;
}
int getNumber() {
return number;
}
static Protocol fromNumber(int number) {
if (number == TCP.number) {
return TCP;
}
if (number == UDP.number) {
return UDP;
}
return OTHER;
}
}
private static final int MIN_IPV4_HEADER_LENGTH = 20;
private ByteBuffer raw;
private byte version;
private int headerLength;
private int totalLength;
private Protocol protocol;
private int source;
private int destination;
public IPv4Header(ByteBuffer raw) {
assert raw.limit() >= MIN_IPV4_HEADER_LENGTH : "IPv4 headers length must be at least 20 bytes";
this.raw = raw;
byte versionAndIHL = raw.get(0);
version = (byte) (versionAndIHL >> 4);
byte ihl = (byte) (versionAndIHL & 0xf);
headerLength = ihl << 2;
raw.limit(headerLength);
totalLength = Short.toUnsignedInt(raw.getShort(2));
//raw.limit(); // by design
//assert totalLength == Binary.unsigned(raw.getShort(2)) : "Inconsistent packet length";
int protocolNumber = Short.toUnsignedInt(raw.get(9));
protocol = Protocol.fromNumber(protocolNumber);
source = raw.getInt(12);
destination = raw.getInt(16);
}
public boolean isSupported() {
return version == 4 && protocol != Protocol.OTHER;
}
public Protocol getProtocol() {
return protocol;
}
public int getHeaderLength() {
return headerLength;
}
public int getTotalLength() {
return totalLength;
}
public void setTotalLength(int totalLength) {
this.totalLength = totalLength;
// apply changes to raw
raw.putShort(2, (short) totalLength);
}
public int getSource() {
return source;
}
public int getDestination() {
return destination;
}
public void setSource(int source) {
this.source = source;
raw.putInt(12, source);
}
public void setDestination(int destination) {
this.destination = destination;
raw.putInt(16, destination);
}
public void swapSourceAndDestination() {
int tmp = source;
setSource(destination);
setDestination(tmp);
}
public ByteBuffer getRaw() {
raw.rewind();
return raw.slice();
}
public IPv4Header copyTo(ByteBuffer target) {
raw.rewind();
ByteBuffer slice = Binary.slice(target, target.position(), getHeaderLength());
target.put(raw);
return new IPv4Header(slice);
}
public IPv4Header copy() {
return new IPv4Header(Binary.copy(raw));
}
public void computeChecksum() {
// reset checksum field
setChecksum((short) 0);
// checksum computation is the most CPU-intensive task in gnirehtet
// prefer optimization over readability
byte[] rawArray = raw.array();
int rawArrayOffset = raw.arrayOffset();
int sum = 0;
for (int i = 0; i < headerLength / 2; ++i) {
// compute a 16-bit value from two 8-bit values manually
sum += (rawArray[rawArrayOffset + 2 * i] & 0xff) << 8 | (rawArray[rawArrayOffset + 2 * i + 1] & 0xff);
}
while ((sum & ~0xffff) != 0) {
sum = (sum & 0xffff) + (sum >> 16);
}
setChecksum((short) ~sum);
}
private void setChecksum(short checksum) {
raw.putShort(10, checksum);
}
public short getChecksum() {
return raw.getShort(10);
}
/**
* Read the packet IP version, assuming that an IP packets is stored at absolute position 0.
*
* @param buffer the buffer
* @return the IP version, or {@code -1} if not available
*/
public static int readVersion(ByteBuffer buffer) {
if (buffer.limit() == 0) {
// buffer is empty
return -1;
}
// version is stored in the 4 first bits
byte versionAndIHL = buffer.get(0);
return (versionAndIHL & 0xf0) >> 4;
}
/**
* Read the packet length, assuming thatan IP packet is stored at absolute position 0.
*
* @param buffer the buffer
* @return the packet length, or {@code -1} if not available
*/
public static int readLength(ByteBuffer buffer) {
if (buffer.limit() < 4) {
// buffer does not even contains the length field
return -1;
}
// packet length is 16 bits starting at offset 2
return Short.toUnsignedInt(buffer.getShort(2));
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/IPv4Packet.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.nio.ByteBuffer;
public class IPv4Packet {
private static final String TAG = IPv4Packet.class.getSimpleName();
@SuppressWarnings("checkstyle:MagicNumber")
public static final int MAX_PACKET_LENGTH = 1 << 16; // packet length is stored on 16 bits
private final ByteBuffer raw;
private final IPv4Header ipv4Header;
private final TransportHeader transportHeader;
public IPv4Packet(ByteBuffer raw) {
this.raw = raw;
raw.rewind();
if (Log.isVerboseEnabled()) {
Log.v(TAG, "IPv4Packet: " + Binary.buildPacketString(raw));
}
ipv4Header = new IPv4Header(raw.duplicate());
if (!ipv4Header.isSupported()) {
Log.d(TAG, "Unsupported IPv4 headers");
transportHeader = null;
return;
}
transportHeader = createTransportHeader();
raw.limit(ipv4Header.getTotalLength());
}
public boolean isValid() {
return transportHeader != null;
}
private TransportHeader createTransportHeader() {
IPv4Header.Protocol protocol = ipv4Header.getProtocol();
switch (protocol) {
case UDP:
return new UDPHeader(getRawTransport());
case TCP:
return new TCPHeader(getRawTransport());
default:
throw new AssertionError("Should be unreachable if ipv4Header.isSupported()");
}
}
private ByteBuffer getRawTransport() {
raw.position(ipv4Header.getHeaderLength());
return raw.slice();
}
public IPv4Header getIpv4Header() {
return ipv4Header;
}
public TransportHeader getTransportHeader() {
return transportHeader;
}
public void swapSourceAndDestination() {
ipv4Header.swapSourceAndDestination();
transportHeader.swapSourceAndDestination();
}
public ByteBuffer getRaw() {
raw.rewind();
return raw.duplicate();
}
public int getRawLength() {
return raw.limit();
}
public ByteBuffer getPayload() {
int headersLength = ipv4Header.getHeaderLength() + transportHeader.getHeaderLength();
raw.position(headersLength);
return raw.slice();
}
public int getPayloadLength() {
return raw.limit() - ipv4Header.getHeaderLength() - transportHeader.getHeaderLength();
}
public void computeChecksums() {
ipv4Header.computeChecksum();
transportHeader.computeChecksum(ipv4Header, getPayload());
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/IPv4PacketBuffer.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
public class IPv4PacketBuffer {
private final ByteBuffer buffer = ByteBuffer.allocate(IPv4Packet.MAX_PACKET_LENGTH);
public int readFrom(ReadableByteChannel channel) throws IOException {
return channel.read(buffer);
}
@SuppressWarnings("checkstyle:MagicNumber")
private int getAvailablePacketLength() {
int length = IPv4Header.readLength(buffer);
assert length == -1 || IPv4Header.readVersion(buffer) == 4 : "This function must not be called when the packet is not IPv4";
if (length == -1) {
// no packet
return 0;
}
if (length > buffer.remaining()) {
// no full packet available
return 0;
}
return length;
}
public IPv4Packet asIPv4Packet() {
buffer.flip();
int length = getAvailablePacketLength();
if (length == 0) {
buffer.compact();
return null;
}
int limit = buffer.limit();
buffer.limit(length).position(0);
ByteBuffer packetBuffer = buffer.slice();
buffer.limit(limit).position(length);
// In order to avoid copies, packetBuffer is shared with this IPv4Packet instance that is returned.
// Don't use it after another call to next()!
return new IPv4Packet(packetBuffer);
}
public void next() {
buffer.compact();
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Log.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.PrintStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public final class Log {
enum Level {
VERBOSE("V"),
DEBUG("D"),
INFO("I"),
WARNING("W"),
ERROR("E");
private final String id;
Level(String id) {
this.id = id;
}
}
private static Level threshold = Level.INFO;
private static final DateFormat FORMAT = new SimpleDateFormat("YYYY-MM-dd HH:mm:ss.SSS");
private static final Date DATE = new Date();
private Log() {
// not instantiable
}
public static Level getThreshold() {
return threshold;
}
public static void setThreshold(Level threshold) {
Log.threshold = threshold;
}
public static boolean isEnabled(Level level) {
return level.ordinal() >= threshold.ordinal();
}
public static boolean isVerboseEnabled() {
return isEnabled(Level.VERBOSE);
}
public static boolean isDebugEnabled() {
return isEnabled(Level.DEBUG);
}
public static boolean isInfoEnabled() {
return isEnabled(Level.INFO);
}
public static boolean isWarningEnabled() {
return isEnabled(Level.WARNING);
}
public static boolean isErrorEnabled() {
return isEnabled(Level.ERROR);
}
private static String getDate() {
DATE.setTime(System.currentTimeMillis());
return FORMAT.format(DATE);
}
private static String format(Level level, String tag, String message) {
return getDate() + " " + level.id + " " + tag + ": " + message;
}
private static void l(Level level, PrintStream stream, String tag, String message, Throwable e) {
if (isEnabled(level)) {
stream.println(format(level, tag, message));
if (e != null) {
e.printStackTrace();
}
}
}
public static void v(String tag, String message, Throwable e) {
l(Level.VERBOSE, System.out, tag, message, e);
}
public static void v(String tag, String message) {
v(tag, message, null);
}
public static void d(String tag, String message, Throwable e) {
l(Level.DEBUG, System.out, tag, message, e);
}
public static void d(String tag, String message) {
d(tag, message, null);
}
public static void i(String tag, String message, Throwable e) {
l(Level.INFO, System.out, tag, message, e);
}
public static void i(String tag, String message) {
i(tag, message, null);
}
public static void w(String tag, String message, Throwable e) {
l(Level.WARNING, System.out, tag, message, e);
}
public static void w(String tag, String message) {
w(tag, message, null);
}
public static void e(String tag, String message, Throwable e) {
l(Level.ERROR, System.err, tag, message, e);
}
public static void e(String tag, String message) {
e(tag, message, null);
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Net.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
public final class Net {
private Net() {
// not instantiable
}
public static InetAddress[] toInetAddresses(String... addresses) {
InetAddress[] result = new InetAddress[addresses.length];
for (int i = 0; i < result.length; ++i) {
result[i] = toInetAddress(addresses[i]);
}
return result;
}
public static InetAddress toInetAddress(String address) {
try {
return InetAddress.getByName(address);
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e);
}
}
public static InetAddress toInetAddress(byte[] raw) {
try {
return InetAddress.getByAddress(raw);
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e);
}
}
@SuppressWarnings("checkstyle:MagicNumber")
public static InetAddress toInetAddress(int ipAddr) {
byte[] ip = {
(byte) (ipAddr >>> 24),
(byte) ((ipAddr >> 16) & 0xff),
(byte) ((ipAddr >> 8) & 0xff),
(byte) (ipAddr & 0xff)
};
return toInetAddress(ip);
}
public static String toString(InetSocketAddress address) {
return address.getAddress().getHostAddress() + ":" + address.getPort();
}
public static String toString(int ip, short port) {
return toString(new InetSocketAddress(Net.toInetAddress(ip), Short.toUnsignedInt(port)));
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/PacketSource.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
/**
* Source that may produce packets.
*
* When a {@link TCPConnection} sends a packet to the {@link Client} while its buffers are full,
* then it fails. To recover, once some space becomes available, the {@link Client} must pull the
* available packets.
*
* This interface provides the abstraction of a packet source from which it can pull packets.
*
* It is implemented by {@link TCPConnection}.
*/
public interface PacketSource {
IPv4Packet get();
void next();
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Packetizer.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
/**
* Convert from level 5 to level 3 by appending correct IP and transport headers.
*/
public class Packetizer {
private final ByteBuffer buffer = ByteBuffer.allocate(IPv4Packet.MAX_PACKET_LENGTH);
private final ByteBuffer payloadBuffer;
private final IPv4Header responseIPv4Header;
private final TransportHeader responseTransportHeader;
public Packetizer(IPv4Header ipv4Header, TransportHeader transportHeader) {
responseIPv4Header = ipv4Header.copyTo(buffer);
responseTransportHeader = transportHeader.copyTo(buffer);
payloadBuffer = buffer.slice();
}
public IPv4Header getResponseIPv4Header() {
return responseIPv4Header;
}
public TransportHeader getResponseTransportHeader() {
return responseTransportHeader;
}
public IPv4Packet packetizeEmptyPayload() {
payloadBuffer.limit(0).position(0);
return inflate();
}
public IPv4Packet packetize(ReadableByteChannel channel, int maxChunkSize) throws IOException {
payloadBuffer.limit(maxChunkSize).position(0);
int payloadLength = channel.read(payloadBuffer);
if (payloadLength == -1) {
return null;
}
payloadBuffer.flip();
return inflate();
}
public IPv4Packet packetize(ReadableByteChannel channel) throws IOException {
return packetize(channel, payloadBuffer.capacity());
}
private IPv4Packet inflate() {
int payloadLength = payloadBuffer.remaining();
buffer.limit(payloadBuffer.arrayOffset() + payloadBuffer.limit()).position(0);
int ipv4HeaderLength = responseIPv4Header.getHeaderLength();
int transportHeaderLength = responseTransportHeader.getHeaderLength();
int totalLength = ipv4HeaderLength + transportHeaderLength + payloadLength;
responseIPv4Header.setTotalLength(totalLength);
responseTransportHeader.setPayloadLength(payloadLength);
// In order to avoid copies, buffer is shared with this IPv4Packet instance that is returned.
// Don't use it after another call to packetize()!
IPv4Packet packet = new IPv4Packet(buffer);
packet.computeChecksums();
return packet;
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Relay.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Set;
public class Relay {
private static final String TAG = Relay.class.getSimpleName();
private static final int CLEANING_INTERVAL = 60 * 1000;
private final int port;
public Relay(int port) {
this.port = port;
}
public void run() throws IOException {
Selector selector = Selector.open();
// will register the socket on the selector
TunnelServer tunnelServer = new TunnelServer(port, selector);
Log.i(TAG, "Relay server started");
long nextCleaningDeadline = System.currentTimeMillis() + UDPConnection.IDLE_TIMEOUT;
while (true) {
long timeout = Math.max(0, nextCleaningDeadline - System.currentTimeMillis());
selector.select(timeout);
Set selectedKeys = selector.selectedKeys();
long now = System.currentTimeMillis();
if (now >= nextCleaningDeadline || selectedKeys.isEmpty()) {
tunnelServer.cleanUp();
nextCleaningDeadline = now + CLEANING_INTERVAL;
}
for (SelectionKey selectedKey : selectedKeys) {
SelectionHandler selectionHandler = (SelectionHandler) selectedKey.attachment();
selectionHandler.onReady(selectedKey);
}
// by design, we handled everything
selectedKeys.clear();
}
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/Router.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.IOException;
import java.nio.channels.Selector;
import java.util.ArrayList;
import java.util.List;
public class Router {
private static final String TAG = Router.class.getSimpleName();
private final Client client;
private final Selector selector;
// there are typically only few connections per client, HashMap would be less efficient
private final List connections = new ArrayList<>();
public Router(Client client, Selector selector) {
this.client = client;
this.selector = selector;
}
public void sendToNetwork(IPv4Packet packet) {
if (!packet.isValid()) {
Log.w(TAG, "Dropping invalid packet");
if (Log.isVerboseEnabled()) {
Log.v(TAG, Binary.buildPacketString(packet.getRaw()));
}
return;
}
try {
Connection connection = getConnection(packet.getIpv4Header(), packet.getTransportHeader());
connection.sendToNetwork(packet);
} catch (IOException e) {
Log.e(TAG, "Cannot create connection, dropping packet", e);
}
}
private Connection getConnection(IPv4Header ipv4Header, TransportHeader transportHeader) throws IOException {
ConnectionId id = ConnectionId.from(ipv4Header, transportHeader);
Connection connection = find(id);
if (connection == null) {
connection = createConnection(id, ipv4Header, transportHeader);
connections.add(connection);
}
return connection;
}
private Connection createConnection(ConnectionId id, IPv4Header ipv4Header, TransportHeader transportHeader) throws IOException {
IPv4Header.Protocol protocol = id.getProtocol();
if (protocol == IPv4Header.Protocol.UDP) {
return new UDPConnection(id, client, selector, ipv4Header, (UDPHeader) transportHeader);
}
if (protocol == IPv4Header.Protocol.TCP) {
return new TCPConnection(id, client, selector, ipv4Header, (TCPHeader) transportHeader);
}
throw new UnsupportedOperationException("Unsupported protocol: " + protocol);
}
private Connection find(ConnectionId id) {
for (Connection connection : connections) {
if (id.equals(connection.getId())) {
return connection;
}
}
return null;
}
public void clear() {
for (Connection connection : connections) {
connection.disconnect();
}
connections.clear();
}
public void remove(Connection connection) {
if (!connections.remove(connection)) {
throw new AssertionError("Removed a connection unknown from the router");
}
}
public void cleanExpiredConnections() {
for (int i = connections.size() - 1; i >= 0; --i) {
Connection connection = connections.get(i);
if (connection.isExpired()) {
Log.d(TAG, "Remove expired connection: " + connection.getId());
connection.disconnect();
connections.remove(i);
}
}
}
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/SelectionHandler.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.nio.channels.SelectionKey;
public interface SelectionHandler {
void onReady(SelectionKey selectionKey);
}
================================================
FILE: relay-java/src/main/java/com/genymobile/gnirehtet/relay/StreamBuffer.java
================================================
/*
* Copyright (C) 2017 Genymobile
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.genymobile.gnirehtet.relay;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
/**
* Circular buffer to store a stream. Read/write boundaries are not preserved.
*/
public class StreamBuffer {
private final byte[] data;
private final ByteBuffer wrapper;
private int head;
private int tail;
public StreamBuffer(int capacity) {
data = new byte[capacity + 1];
wrapper = ByteBuffer.wrap(data);
}
public boolean isEmpty() {
return head == tail;
}
public boolean isFull() {
return (head + 1) % data.length == tail;
}
public int size() {
if (head < tail) {
return head + data.length - tail;
}
return head - tail;
}
public int capacity() {
return data.length - 1;
}
public int remaining() {
return capacity() - size();
}
public int writeTo(WritableByteChannel channel) throws IOException {
if (head > tail) {
wrapper.limit(head).position(tail);
int w = channel.write(wrapper);
tail = wrapper.position();
optimize();
return w;
}
if (head < tail) {
wrapper.limit(data.length).position(tail);
int w = channel.write(wrapper);
tail = wrapper.position() % data.length;
optimize();
return w;
}
// else head == tail, which means empty buffer, nothing to do
return 0;
}
public void readFrom(ByteBuffer buffer) {
int requested = Math.min(buffer.remaining(), remaining());
if (requested <= data.length - head) {
buffer.get(data, head, requested);
} else {
buffer.get(data, head, data.length - head);
buffer.get(data, 0, head + requested - data.length);
}
head = (head + requested) % data.length;
}
/**
* To avoid unnecessary copies, StreamBuffer writes at most until the "end" of the circular
* buffer, which is suboptimal (it could have written more data if they have been contiguous).
*