smoltcp-rs / smoltcp

a smol tcp/ip stack
BSD Zero Clause License
3.8k stars 428 forks source link

Possible to use this for Android VPN Service #581

Closed JonForShort closed 2 years ago

JonForShort commented 2 years ago

This is just a follow up on a previous question that was asked.

https://github.com/smoltcp-rs/smoltcp/issues/326#issuecomment-595005632

I am trying to use this project in my Android local VPN service implementation (https://developer.android.com/guide/topics/connectivity/vpn). Looking through the PRs, it looks like IP support and TUN interface support have been added. Would this mean that it can be now used for my use-case? Thanks in advance!

Dirbaio commented 2 years ago

I haven't personally messed with android VPNs, but afaik it should Just Work now. The RawSocket phy also has IP medium support (medium param in new()).

You'll probably have to add from_raw_fd support, PRs welcome :)

gopakumarce commented 2 years ago

I have succesfully used smoltcp on windows, android, ios and linux - common code running on all these platforms. And everywhere I use Medium::Ip #[cfg(feature = "medium-ip")]

gopakumarce commented 2 years ago

And as for the from_raw_fd, yes thats a seperate layer I have - for my use case I keep it seperate from smoltcp, because I create the sockets myself - I look at the packets myself and filter out what is of interest to me (or not) and then just create a smoltcp packet for that myself, and feed that socket packets using a Device implementation with Medium::Ip in it.

JonForShort commented 2 years ago

Thanks @Dirbaio and @gopakumarce for the help.

@gopakumarce , would you happen to have reference code you can share?

gopakumarce commented 2 years ago

@JonForShort mine is proprietary code still so unfortunately I cant share it yet. But like I mentioned, the thing just worked (smoltcp is awesome!), all I had to do was the below

  1. Get an IP packet from the android fd by reading the fd (very easy) - code outside smoltcp
  2. Then I filter the packet to see what is of interest to me - again code outside smoltcp
  3. Then I create an interface for myself - interface = InterfaceBuilder::new(device) where device is my own structure with an rx and tx queue (a VecDequeue) that implements the trait Device
  4. Then I feed my rx IP packet into the queue in device, and I call interface.poll()
  5. And out comes packet in the device tx queue, I take that and write it back to the fd

So in my implementation - there is an "Interface per socket" - a 1:1 mapping between an interface and a tcp session/socket. Because for me I want to be deciding myself what packets are of interest to smoltcp etc.. so I cant use any of the standard smoltcp devices.

JonForShort commented 2 years ago

Thanks @gopakumarce . That's understandable about the proprietary code. I appreciate the explanation. I'll give this a try myself. Just to confirm, your application didn't need to run as root for sending/receiving packets correct? You were able to run as a regular application.

gopakumarce commented 2 years ago

Hi @JonForShort .. So the android application certainly cant be run as root, android wont allow that unless you root the device of course. And you dont need to do that, the android vpnService APIs are designed to work in regular user mode. You can use the "builder" APIs in android for that. Note that my use of smoltcp was entirely in an event driven non-blocking mode (using mio), so what I did was from java

  1. I create the android vpnService set in non-blocking mode
  2. Compile the rust code as a C library that I can load in Java using JNI interface
  3. Create a new thread in java and from that thread call the JNI/rust API and pass the fd
  4. And once the rust code has the fd it does all the other stuff with it

Java code class MyApp:

        Builder builder = new Builder();
        builder.setBlocking(false);  << == set to non-blocking
        <snip>
        vpnInterface = builder.setSession(getString(R.string.app_name)).setConfigureIntent(pendingIntent).establish();
        vpnFd = vpnInterface.detachFd();

        <snip>
        // Load the rust library
        System.loadLibrary("rust_library");  <<====== Compile the rust code as a C FFI library that can be loaded here
        new Thread(new Runnable() {
            public void run() {
                Thread.currentThread().setName("vpn.worker");
                // Call into the rust code asking to initialize
                initRustLib(vpnFd);   <<===== This is a JNI API defined along with the android code in a .c file
            }
        }).start();

JNI C wrappers


JNIEXPORT jint JNICALL Java_my_app_MyApp_initRustLib(JNIEnv *env, jclass c, jint fd)
{
    rust_init(fd); <<==== This is an API inside rust declared as C FFI
    return 0;
}

Rust code:

#[no_mangle]
pub unsafe extern "C" fn rust_init(fd: uint32_t) {
}

And the further rust code for the tcp itself is like I said,

  1. Create your own structure implementing smoltcp Device trait - my structure has an Rx and Tx VecDequeue as packet queues
  2. The structure also needs some "tokens" and stuff - that you will figure out if you look at a sample Device implementation inside smoltcp code
  3. And like I mentioned, I like to handle the details of what IP 5-tuple I want to send to smoltcp etc.. myself, so I read the fd, get a packet, figure all that and if its of my interest then I create a socket set with ONE socket in it - onesock = SocketSet::new(Vec::with_capacity(1))
  4. I create a interface for this socket interface = InterfaceBuilder::new(my-device)
  5. I feed the packet into the Rx queue and call interface.poll(&mut onesock)
  6. So after this packet "maybe" there is tcp data in onesock - you can find that out by calling onesock.recv()
  7. As long as there is Rx packets matching the same 5-tuple repeat steps 5 and 6 - ie get IP packet, feed it to the device Rx queue, call poll, and for the socket see if there is tcp data available. If its a new 5-tuple of my interest I create a new onesock and a new interface, rest of the logic is the same
  8. Now if you have tcp data to transmit, call onesock.send() and again do interface.poll(&mut onesock) and if the tcp data has IP packets to be transmitted, then you will have those packets in your device Tx queue, take that and send it on the fd

And I use mio for all the event driven work - not tokio or any higher level libs, just direct mio. The thing to note is that android has no memory limits on a vpnservice code, but if you do the same on iOS, the whole darn thing including text and data has to fit within 15mb - iOS will stretch the limits of your patience - so if you include very large libs into your code like tokios and serde for example, your code will bloat so much that it wont run in iOS. So if you plan to make it work in apple platforms, be very careful to write small tiny code with just minimal libraries (which is why I love smoltcp !)

gopakumarce commented 2 years ago

Also @JonForShort I had some more comments explaining the usage of smoltcp etc.. here https://github.com/smoltcp-rs/smoltcp/pull/440/commits if you want to take a look

JonForShort commented 2 years ago

I appreciate the help @gopakumarce . I started the implementation here (https://github.com/JonForShort/android-local-vpn). So far, everything is going fine. I am able to build smoltcp as part of my build process and link it to my android application. To be honest, I do not know rust so I am sure things will go slowly but will be a good learning experience. Thanks again!

Dirbaio commented 2 years ago

Closing due to inactivity. If you have more questions, feel free to open new issues!