Git Product home page Git Product logo

quiche4j's Introduction

Quiche4j

Java implementation of the QUIC transport protocol and HTTP/3.

The library provides thin Java API layer on top of JNI calls to quiche. Quiche4j provides a low level API for processing QUIC packets and handling connection state. The application is responsible for providing I/O (e.g. sockets handling) as well as timers. The library itself does not make any assumptions on how I/O layer is organized, making it's pluggle into different architectures.

The main goal of the JNI bindings is to ensure high-performance and flexibility for the application developers while maintaining full access to quiche library features. Specifically, the bindings layer tries to ensure zero-copy data trasfer between runtimes where possible and perform minimum allocations on Java side.

Usage

Maven:

<dependencies>
    <dependency>
        <groupId>io.quiche4j</groupId>
        <artifactId>quiche4j-core</artifactId>
        <version>0.2.5</version>
    </dependency>
    <dependency>
        <groupId>io.quiche4j</groupId>
        <artifactId>quiche4j-jni</artifactId>
        <classifier>linux_x64_86</classifier>
        <version>0.2.5</version>
    </dependency>
</dependencies>

Note that quiche4j-jni contains native library and should be installed with proper classifier. os-maven-plugin could be used to simplify classifier detection

<build>
    <extensions>
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.6.1</version>
        </extension>
    </extensions>
</build>
<dependencies>
    <dependency>
        <groupId>io.quiche4j</groupId>
        <artifactId>quiche4j-jni</artifactId>
        <classifier>${os.detected.classifier}</classifier>
        <version>0.2.5</version>
    </dependency>
</dependencies>

Building

Quiche4j requires cargo and Rust 1.39+ to build. The latest stable Rust release can be installed using rustup. Once the Rust build environment is setup,

$ git clone https://github.com/kachayev/quiche4j
$ mvn clean install

Run Examples

Run HTTP3 client example:

$ ./http3-client.sh https://quic.tech:8443
> sending request to https://quic.tech:8443
> handshake size: 1200
> socket.recieve 167 bytes
> conn.recv 167 bytes
...
! conn is closed recv=10 sent=12 lost=0 rtt=95 cwnd=14520 delivery_rate=1436

Run HTTP3 server example:

$ ./http3-server.sh :4433
! listening on localhost:4433

Compile Manually

Maven project is setup to automatically compile JNI library and include the result of the compilation into the quiche4j-jni JAR. Even thought this method is convenient for distribution, it might lack flexibility. To compile JNI manually follow the next steps,

$ git clone https://github.com/kachayev/quiche4j
$ cargo build --release --manifest-path quiche4j-jni/Cargo.toml
$ mvn clean install
$ java \
    -Djava.library.path=quiche4j-jni/target/release/ \
    -cp quiche4j-examples/target/quiche4j-examples-*.jar \
    io.quiche4j.examples.Http3Server

The code would try to load native libraries from java.library.path first, using built-in artifact as a fallback only.

For cross-compilation options, see cargo build documentation.

API

Connection

Before establishing a QUIC connection, you need to create a configuration object:

import io.quiche4j.Config;
import io.quiche4j.ConfigBuilder;

final Config config = new ConfigBuilder(Quiche.PROTOCOL_VERSION).build();

On the client-side the Quiche.connect utility function can be used to create a new connection, while Quiche.accept is for servers:

// client
final byte[] connId = Quiche.newConnectionId();
// note, that "quic.tech" here is not used for establishing network
// connection. it's used only for peer verification (thus, optional)
final Connection conn = Quiche.connect("quic.tech", connId, config);

// server
final Connection conn = Quiche.accept(sourceConnId, originalDestinationId, config);

Incoming packets

Using the connection's recv method the application can process incoming packets that belong to that connection from the network:

final byte[] buf = new byte[1350];
while(true) {
    DatagramPacket packet = new DatagramPacket(buf, buf.length);
    try {
        // read from the socket
        socket.receive(packet);
        final byte[] buffer = Arrays.copyOfRange(packet.getData(), packet.getOffset(), packet.getLength());
        // update the connection state
        final int read = conn.recv(buffer);
        if(read <= 0) break;
    } catch (SocketTimeoutException e) {
        conn.onTimeout();
        break;
    }
}

Outgoing packets

Outgoing packet are generated using the connection's send method instead:

final byte[] buf = new byte[1350];
while(true) {
    // get data that's need to be sent based on the connection state
    final int len = conn.send(buf);
    if (len <= 0) break;
    final DatagramPacket packet = new DatagramPacket(buf, len, address, port);
    // send it to the network
    socket.send(packet);
}

Timers

The application is responsible for maintaining a timer to react to time-based connection events. When a timer expires, the connection's onTimeout method should be called, after which additional packets might need to be sent on the network:

// handle timer
conn.onTimeout();

// sending corresponding packets
final byte[] buf = new byte[1350];
while(true) {
    final int len = conn.send(buf);
    if (len <= 0) break;
    final DatagramPacket packet = new DatagramPacket(buf, len, address, port);
    socket.send(packet);
}

Streams Data

After some back and forth, the connection will complete its handshake and will be ready for sending or receiving application data.

Data can be sent on a stream by using the streamSend method:

if(conn.isEstablished()) {
    // handshake completed, send some data on stream 0
    conn.streamSend(0, "hello".getBytes(), true);
}

The application can check whether there are any readable streams by using the connection's readable method, which returns an iterator over all the streams that have outstanding data to read.

The streamRecv method can then be used to retrieve the application data from the readable stream:

if(conn.isEstablished()) {
    final byte[] buf = new byte[1350]; 
    for(long streamId: conn.readable()) {
        // stream <streamId> is readable, read until there's no more data
        while(true) {
            final int len = conn.streamRecv(streamId, buf);
            if(len <= 0) break;
        }
    }
}

HTTP/3

The library provides a high level API for sending and receiving HTTP/3 requests and responses on top of the QUIC transport protocol.

Connection

HTTP/3 connections require a QUIC transport-layer connection, see "Connection" for a full description of the setup process. To use HTTP/3, the QUIC connection must be configured with a suitable ALPN Protocol ID:

import io.quiche4j.Config;
import io.quiche4j.ConfigBuilder;
import io.quiche4j.http3.Http3Connection;

final Config config = new ConfigBuilder(Quiche.PROTOCOL_VERSION)
    .withApplicationProtos(Http3.APPLICATION_PROTOCOL)
    .build();

The QUIC handshake is driven by sending and receiving QUIC packets. Once the handshake has completed, the first step in establishing an HTTP/3 connection is creating its configuration object:

import io.quiche4j.http3.Http3Config;
import io.quiche4j.http3.Http3ConfigBuilder;

final Http3Config h3Config = new Http3ConfigBuilder().build();

HTTP/3 client and server connections are both created using the Http3Connection.withTransport function:

import io.quiche4j.http3.Http3Connection;

final Http3Connection h3Conn = Http3Connection.withTransport(conn, h3Config);

Sending Request

An HTTP/3 client can send a request by using the connection's sendRequest method to queue request headers; sending QUIC packets causes the requests to get sent to the peer:

import io.quiche4j.http3.Http3Header;

List<Http3Header> req = new ArrayList<>();
req.add(new Http3Header(":method", "GET"));
req.add(new Http3Header(":scheme", "https"));
req.add(new Http3Header(":authority", "quic.tech"));
req.add(new Http3Header(":path", "/"));
req.add(new Http3Header("user-agent", "Quiche4j"));
h3Conn.sendRequest(req, true);

An HTTP/3 client can send a request with additional body data by using the connection's sendBody method:

final long streamId = h3Conn.sendRequest(req, false);
h3Conn.sendBody(streamId, "Hello there!".getBytes(), true);

Handling Responses

After receiving QUIC packets, HTTP/3 data is processed using the connection's poll method.

An HTTP/3 server uses poll to read requests and responds to them, an HTTP/3 client uses poll to read responses. poll method accepts object that implements Http3EventListener interface defining callbacks for different type of events

import io.quiche4j.http3.Http3EventListener;
import io.quiche4j.http3.Http3Header;

final long streamId = h3Conn.poll(new Http3EventListener() {
    public void onHeaders(long streamId, List<Http3Header> headers) {
        // got headers
    }

    public void onData(long streamId) {
        // got body
        final byte[] body = new byte[MAX_DATAGRAM_SIZE];
        final int len = h3Conn.recvBody(streamId, body);
    }

    public void onFinished(long streamId) {
        // done with this stream
        conn.close(true, 0x00, "Bye! :)".getBytes()));
    }
});

if(Quiche.ErrorCode.DONE == streamId) {
    // this means no event was emitted
    // it would take more packets to proceed with new events
}

Note that poll would either execute callbacks and returns immediately. If there's not enough data to fire any of the events, poll immediately returns Quiche.ErrorCode.DONE. The application is responsible for handling incoming packets from the network and feeding packets data into connection before executing next poll.

Examples

Have a look at the quiche4j-examples folder for more complete examples on how to use the Quiche4j API to work with HTTP/3 protocol.

Examples package has Http3NettyClient with a toy implementation of HTTP/3 client to show case the idea of how quiche4j connection state management could be integrated with Netty I/O primitives.

Errors Hanlding

Native JNI code propagates errors using return codes (typically the return code < 0 means either DONE or failed). For example, quiche::Error enum. Quiche4j follows the same convention instead of throwing Java exceptions to ensure good perfomance and compatibility with async runtimes (catching exception in async environemnt might be somewhat problematic). See Quiche.ErrorCode and Http3.ErrorCode for more details.

Unlike other methods, Quiche.connect and Quiche.accept throw ConnectionFailureException if JNI code failed before quiche::Connection struct had been allocated. In this case there's no pointer to carry around, thus Java code does not create Connection object.

Debug

Use QUICHEJ4_JNI_LOG environment variable to tweak JNI log level. Setting variable to trace gives good visibility into the processing. Example

$ QUICHE4J_JNI_LOG=trace ./http3-client.sh https://quic.tech:8443
...
[2020-09-27T20:49:39Z TRACE quiche] 3457285232348874d2bda1ed5add4a0c894dc9f2 rx pkt Handshake version=ff00001d dcid=3457285232348874d2bda1ed5add4a0c894dc9f2 scid=1b48925e8fcf6281be7f5ca472dd44b71a2f2fc1 len=731 pn=2
[2020-09-27T20:49:39Z TRACE quiche] 3457285232348874d2bda1ed5add4a0c894dc9f2 rx frm CRYPTO off=2252 len=709
[2020-09-27T20:49:39Z TRACE quiche::tls] 3457285232348874d2bda1ed5add4a0c894dc9f2 write message lvl=Handshake len=36
[2020-09-27T20:49:39Z TRACE quiche::tls] 3457285232348874d2bda1ed5add4a0c894dc9f2 set write secret lvl=OneRTT
[2020-09-27T20:49:39Z TRACE quiche::tls] 3457285232348874d2bda1ed5add4a0c894dc9f2 set read secret lvl=OneRTT
[2020-09-27T20:49:39Z TRACE quiche] 3457285232348874d2bda1ed5add4a0c894dc9f2 connection established: proto=Ok("h3-29") cipher=Some(AES128_GCM) curve=Some("X25519") sigalg=Some("rsa_pss_rsae_sha256") resumed=false TransportParams { original_destination_connection_id: Some([121, 203, 4, 8, 44, 253, 150, 111, 224, 200, 201, 105, 201, 162, 250, 160]), max_idle_timeout: 30000, stateless_reset_token: None, max_udp_payload_size: 1350, initial_max_data: 10000000, initial_max_stream_data_bidi_local: 1000000, initial_max_stream_data_bidi_remote: 1000000, initial_max_stream_data_uni: 1000000, initial_max_streams_bidi: 100, initial_max_streams_uni: 100, ack_delay_exponent: 3, max_ack_delay: 25, disable_active_migration: true, active_conn_id_limit: 2, initial_source_connection_id: Some([27, 72, 146, 94, 143, 207, 98, 129, 190, 127, 92, 164, 114, 221, 68, 183, 26, 47, 47, 193]), retry_source_connection_id: None }
...

Implementation Details

  • Modules Native.java and Http3Native.java contains definition of all native calls, structurally close to quiche's src/ffi.rs and src/h3/ffi.rs respectively.

  • JNI calls are implmeneted in Rust (see quiche4j-jni for more details) using rust-jni library. The goal was to stick to primitive types as much as possible and avoid Java objects manipulations in native code. There are still a few exceptions from this rule, e.g. operations with connection Stats, management of Http3Header lists, etc.

  • Proxy Java objects maintain a handle (pointer) to the corresponding Rust struct to maximise compatability with all quiche features. A single instance of a Cleaner is statically defined in io.quiche4j.Native class and is used to register all deallocation callback (conventionally called free for each class that maintains a native pointer).

Contribute

  • Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug (also, check out "TODO" section of this document).
  • Fork the repository on Github & fork master to feature-* branch to start making your changes.
  • Write a test which shows that the bug was fixed or that the feature works as expected.

or simply...

  • Use it.
  • Enjoy it.
  • Spread the word.

TODO

There are still a few xxx comments in the code. Both for Java and for Rust. Plus, there are a few methods that are not exposed to Java layer. Notably, operations with stream priorities and HTTP/3 connection configuration (some of those would require to extend quiche library as well).

Other ideas to work on:

  • Propagate Rust panics into Java exceptions (when necessary)
  • Setup integration testing suite against different QUIC implementations out there
  • Qlog support
  • Experiment with in-memory serialization (Arrow?) to deal with (presumably) high overhead of manipulating objects in native code

Copyright

Copyright (C) 2020, Oleksii Kachaiev.

See COPYING for the license.

See cloudflare/quiche/copying for Quiche license.

quiche4j's People

Contributors

gordonchiang avatar kachayev avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

quiche4j's Issues

build failure

can I get an exact list of dependencies?
I keep getting a build failure when attempting to follow the provided instructions

I've attached the build log as well, hopefully someone can point out what my error in this process is
build log

I couldn't send request from quiche client to quiche server

Hi,

First i started Http3Server with this code
./http3-server.sh :8443
and then i started Http3Client with this code
./http3-client.sh https://localhost:8443

server logs:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:/usr/share/maven/lib/guice.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
! listening on localhost:8443

socket.recv 1200 bytes
packet ty=INITIAL version=-16777187 dcid=3c2af28de3793ee493e402943b457dcf scid=7257454bd06521c047c2b66e0b5a76dd44a255ea pkt_num=0 pkt_num_len=0 token= versions=null
stateless retry
retry length 91
socket.recv 1200 bytes
packet ty=INITIAL version=-16777187 dcid=4f77fc533aa9ef38da277f8d872d226a3b66ae64 scid=7257454bd06521c047c2b66e0b5a76dd44a255ea pkt_num=0 pkt_num_len=0 token=517569636865346a7f0000013c2af28de3793ee493e402943b457dcf versions=null
new connection 4f77fc533aa9ef38da277f8d872d226a3b66ae64
! # of clients: 1
conn.recv 1200 bytes
conn.established false
conn.send 167 bytes
conn.send 1196 bytes
conn.send 228 bytes
socket.recv 70 bytes
packet ty=HANDSHAKE version=-16777187 dcid=4f77fc533aa9ef38da277f8d872d226a3b66ae64 scid=7257454bd06521c047c2b66e0b5a76dd44a255ea pkt_num=0 pkt_num_len=0 token= versions=null
conn.recv 70 bytes
conn.established false
cleaning up 4f77fc533aa9ef38da277f8d872d226a3b66ae64
! # of clients: 0

client logs:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by com.google.inject.internal.cglib.core.$ReflectUtils$1 (file:/usr/share/maven/lib/guice.jar) to method java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain)
WARNING: Please consider reporting this to the maintainers of com.google.inject.internal.cglib.core.$ReflectUtils$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

sending request to https://localhost:8443
handshake size: 1200
socket.recieve 91 bytes
conn.recv 91 bytes
conn.send 1200 bytes
socket.recieve 167 bytes
conn.recv 167 bytes
socket.recieve 1196 bytes
conn.recv 1196 bytes
socket.recieve 228 bytes
conn.recv failed -10
conn.send 70 bytes
! conn is closed recv=2 sent=3 lost=0 rtt=149 cwnd=14520 delivery_rate=0

quic

Client is working with https://quic.tech:8443
What is the problem?

Server crashes with ConcurrentModificationException

Hi,

During our tests involving a Quiche4j implementation with the commit version ea5effc, we identified 1 fault:

The example server crashes with ConcurrentModificationException when the client tries to set up multiple connections with the server using a constant IP address and port number. The root cause of this crash is the server tries to remove a client from the HashMap while looping through the HashMap (line 323) in quiche4j/quiche4j-examples/src/main/java/io/quiche4j/examples/Http3Server.java:main().

The server should not execute clients.remove(connId) in the for loop.

Quiche4j server does not set a limit for its active_connection_id_limit transport parameter and able to process up to 1000 NEW_CONNECTION_ID frames.

Out test client tries to send 1000 NEW_CONNECTION_ID frames to the Quiche4j server after a connection is established. In results, the Quiche4j server does not close the connection and seems to process all the NEW_CONNECTION_ID frames.

According to RFC 9000, if the value of active_connection_id_limit is absent, a default limit of 2 is assumed. This means that if the number of active connection IDs exceeds the value advertised in its active_connection_id_limit transport parameter (which is 2 in this case), the server MUST close the connection with an error of type CONNECTION_ID_LIMIT_ERROR.

java.lang.UnsatisfiedLinkError

When I start in my project the QuicServer class (is like https3Server but removed the http part), throws this error. I searched and all the "solutions" given don't work. I found that the problem is in the ConfigBuilder object, but I can't fix it.

Caused by: java.lang.UnsatisfiedLinkError: 'void io.quiche4j.Native.quiche_init_logger()' at io.quiche4j.Native.quiche_init_logger(Native Method) ~[quiche4j-core-0.2.5.jar:?] at io.quiche4j.Native.<clinit>(Native.java:23) ~[quiche4j-core-0.2.5.jar:?] at io.quiche4j.ConfigBuilder.build(ConfigBuilder.java:317) ~[quiche4j-core-0.2.5.jar:?] at xyz.luisch444.uranium.network.QuicServer.<init>(QuicServer.java:23) ~[main/:?]

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.