Search code examples
androidopenvpnwireguardics-openvpn

How to throttle internet speed (upload and download) in Android programmatically


How to Limit the Internet Speed of a VPN Connection on Android programmatically.

I have managed to control the upload speed using the OpenVPN "shaper" option, but I want to control both upload and download speeds.

Thank you.


Solution

  • Android does not provide direct APIs for throttling network speed. You may consider using ThrottlingInterceptor and use it like :

    OkHttpClient client = new OkHttpClient.Builder()
      .addInterceptor(new ThrottlingInterceptor(1024 * 100)) // Limit to 100 KBps         
      .build();
    

    TLDR;

    . .. ...

    This is a basic approach for handling bandwidth throttling :


    1. Create the VPN Service Class

    import android.app.PendingIntent;
    import android.content.Intent;
    import android.net.VpnService;
    import android.os.ParcelFileDescriptor;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;
    import java.util.concurrent.TimeUnit;
    
    public class MyVpnService extends VpnService implements Runnable {
        private Thread mThread;
        private ParcelFileDescriptor mInterface;
    
        @Override
        public int onStartCommand(Intent intent, int flags, int startId) {
            if (mThread != null) {
                mThread.interrupt();
            }
            mThread = new Thread(this, "MyVpnThread");
            mThread.start();
            return START_STICKY;
        }
    
        @Override
        public void onDestroy() {
            if (mThread != null) {
                mThread.interrupt();
            }
            try {
                if (mInterface != null) {
                    mInterface.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void run() {
            try {
                // Configure the TUN interface
                Builder builder = new Builder();
                builder.addAddress("10.0.0.2", 24);
                builder.addRoute("0.0.0.0", 0);
                mInterface = builder.setSession("MyVPNService")
                        .setConfigureIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
                        .establish();
    
                // Packets to be sent are queued in this input stream.
                FileInputStream in = new FileInputStream(mInterface.getFileDescriptor());
                // Packets received need to be written to this output stream.
                FileOutputStream out = new FileOutputStream(mInterface.getFileDescriptor());
    
                // Allocate the buffer for a single packet.
                ByteBuffer packet = ByteBuffer.allocate(32767);
    
                while (true) {
                    // Read the outgoing packet from the input stream.
                    int length = in.read(packet.array());
                    if (length > 0) {
                        // Process the packet
                        packet.limit(length);
                        processPacket(packet);
    
                        // Send the packet to the output stream.
                        out.write(packet.array(), 0, length);
                        packet.clear();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private void processPacket(ByteBuffer packet) {
            // Desired bandwidth limit in bytes per second (100 KB/s)
            long bytesPerSecond = 102400; // 100 KBps
    
            // Calculate the time in nanoseconds that should elapse per byte
            long nanosPerByte = TimeUnit.SECONDS.toNanos(1) / bytesPerSecond;
    
            // The number of bytes in the packet
            int packetSize = packet.limit();
    
            // The expected time to send this packet
            long expectedTime = packetSize * nanosPerByte;
    
            // Introduce delay based on the expected time to throttle the speed
            try {
                Thread.sleep(TimeUnit.NANOSECONDS.toMillis(expectedTime));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("Throttling interrupted", e);
            }
        }
    }
    

    2. Declate it in AndroidManifest.xml:

    <service
        android:name=".MyVpnService"
        android:permission="android.permission.BIND_VPN_SERVICE">
        <intent-filter>
            <action android:name="android.net.VpnService" />
        </intent-filter>
    </service>
    

    3. Start and stop the VPN service from activity :

    import android.content.Intent;
    import android.net.VpnService;
    import android.os.Bundle;
    import androidx.appcompat.app.AppCompatActivity;
    
    public class MainActivity extends AppCompatActivity {
        private static final int VPN_REQUEST_CODE = 0x0F;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
    
            Intent intent = VpnService.prepare(this);
            if (intent != null) {
                startActivityForResult(intent, VPN_REQUEST_CODE);
            } else {
                onActivityResult(VPN_REQUEST_CODE, RESULT_OK, null);
            }
        }
    
        @Override
        protected void onActivityResult(int requestCode, int resultCode, Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if (requestCode == VPN_REQUEST_CODE && resultCode == RESULT_OK) {
                Intent intent = new Intent(this, MyVpnService.class);
                startService(intent);
            }
        }
    }
    

    Another Approach

    1. As VPN Service (for deivce level interception)

    a). Using ThrottlingInterceptor as a Service

    class ThrottlingInterceptor(private val maxBytesPerSecond: Long) {
    
        private var bytesTransferred: Long = 0
        private var lastTime: Long = System.currentTimeMillis()
    
        @Synchronized
        fun shouldThrottle(packetSize: Int): Boolean {
            val currentTime = System.currentTimeMillis()
            if (currentTime - lastTime > 1000) {
                lastTime = currentTime
                bytesTransferred = 0
            }
    
            if (bytesTransferred + packetSize > maxBytesPerSecond) {
                return true
            }
    
            bytesTransferred += packetSize
            return false
        }
    
        @Synchronized
        fun reset() {
            lastTime = System.currentTimeMillis()
            bytesTransferred = 0
        }
    }
    

    b). Your service class

    class MyVpnService : VpnService() {
        
        private val interceptor = ThrottlingInterceptor(100 * 1024 / 8) // 100 kbps
    
        override fun onCreate() {
            super.onCreate()
            // Initialize the VPN service here
        }
    
        private fun processPacket(packet: ByteBuffer) {
            val packetSize = packet.remaining()
    
            if (interceptor.shouldThrottle(packetSize)) {
                try {
                    Thread.sleep(1000) // Sleep for 1 second to reset the limit
                } catch (e: InterruptedException) {
                    e.printStackTrace()
                }
                interceptor.reset()
            }
    
            // Forward the packet
            forwardPacket(packet)
        }
    
        private fun forwardPacket(packet: ByteBuffer) {
            // Logic to forward packet to destination
        }
    }
    

    2. If need to restrict speed limit within your app

    a). Interceptor class

    import java.io.IOException;
    import okhttp3.Interceptor;
    import okhttp3.OkHttpClient;
    import okhttp3.Request;
    import okhttp3.Response;
    import okio.Buffer;
    import okio.ForwardingSource;
    import okio.Okio;
    import okio.Source;
    
    public class ThrottlingInterceptor implements Interceptor {
    
        private final long bytesPerSecond;
    
        public ThrottlingInterceptor(long bytesPerSecond) {
            this.bytesPerSecond = bytesPerSecond;
        }
    
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();
            Response originalResponse = chain.proceed(request);
            return originalResponse.newBuilder()
                    .body(new ThrottlingResponseBody(originalResponse.body(), bytesPerSecond))
                    .build();
        }
    
        private static class ThrottlingResponseBody extends okhttp3.ResponseBody {
            private final okhttp3.ResponseBody responseBody;
            private final long bytesPerSecond;
            private BufferedSource bufferedSource;
    
            public ThrottlingResponseBody(okhttp3.ResponseBody responseBody, long bytesPerSecond) {
                this.responseBody = responseBody;
                this.bytesPerSecond = bytesPerSecond;
            }
    
            @Override
            public okhttp3.MediaType contentType() {
                return responseBody.contentType();
            }
    
            @Override
            public long contentLength() {
                return responseBody.contentLength();
            }
    
            @Override
            public BufferedSource source() {
                if (bufferedSource == null) {
                    bufferedSource = Okio.buffer(new ThrottlingSource(responseBody.source(), bytesPerSecond));
                }
                return bufferedSource;
            }
    
            private static class ThrottlingSource extends ForwardingSource {
                private final long bytesPerSecond;
                private long totalBytesRead = 0L;
                private long startTime = System.currentTimeMillis();
    
                ThrottlingSource(Source source, long bytesPerSecond) {
                    super(source);
                    this.bytesPerSecond = bytesPerSecond;
                }
    
                @Override
                public long read(Buffer sink, long byteCount) throws IOException {
                    long bytesRead = super.read(sink, byteCount);
                    if (bytesRead == -1) return -1;
    
                    totalBytesRead += bytesRead;
                    long elapsedTime = System.currentTimeMillis() - startTime;
    
                    long expectedTime = (totalBytesRead * 1000) / bytesPerSecond;
                    if (expectedTime > elapsedTime) {
                        try {
                            Thread.sleep(expectedTime - elapsedTime);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                            throw new IOException("Throttling interrupted", e);
                        }
                    }
    
                    return bytesRead;
                }
            }
        }
    }
    

    b). Do this in Activity OkHttpClient fucntion

    Use OkHttpClient with ThrottlingInterceptor while requesting network request

    OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new ThrottlingInterceptor(1024 * 100)) // Limit to 100 KBps
            .build();
    
    Request request = new Request.Builder()
            .url("https://your.api.endpoint")
            .build();
    
    try (Response response = client.newCall(request).execute()) {
        // Handle the response
    } catch (IOException e) {
        e.printStackTrace();
    }
    

    References