Interface QuicInterceptor
- Functional Interface:
- This is a functional interface and can therefore be used as the assignment target for a lambda expression or method reference.
TcpInterceptor. Configure one per proxy with HttpProxyServerBootstrap.withQuicInterceptor(QuicInterceptor).
bootstrap.withQuicInterceptor((message, stream) -> message.fromClient()
? message.data() // pass on
: message.text().replace("secret", "******").getBytes(UTF_8));
Which connections
QUIC whose ClientHello offers no HTTP/3 (doq, hq-interop, a protocol of its
own) that the proxy decides to intercept: redirected to the transparent QUIC listener, or
inside a CONNECT-UDP flow with Http3Options.Builder.connectUdpInterceptQuic.
With an interceptor, such clients are intercepted (the host rules, shouldIntercept and
the filters still decide first); without one they are relayed untouched. Hosts matched by
HttpProxyServerBootstrap.withQuicHosts(java.lang.String...) come here even when they offer
HTTP/3, with or without an interceptor.
The proxy finishes a handshake with the server first, offering the client's ALPN protocols and server name, then one with the client, presenting a certificate its CA issues for the server name and choosing the protocol the server chose. Each stream one side opens is opened on the other connection, of the same kind (bidirectional or unidirectional), and its bytes, its end (FIN), a reset (RESET_STREAM) and a request to stop sending (STOP_SENDING) are passed on, with their error codes; datagrams (RFC 9221) go across one for one, when both sides take them. When one connection closes, the other is closed with the same application error code and reason.
Stream data
A stream is a byte stream, as TCP is: a QuicStreamMessage is whatever one read
returned (at most 16 KiB), so a peer's write may be seen in pieces or joined with the next one,
differently from one run to the next. A hook that looks for a message of the protocol keeps the
start of it until its end arrives.
Flow control is end to end: each direction of each stream is read only as fast as the other side takes its data, so a slow reader slows its writer down through QUIC's own flow control, and the proxy holds at most one read per direction (with framing, a message's bytes and one read). Stream limits are the peers' too: a stream one side opens waits until the other side's limit allows it.
Messages
An interceptor that overrides streamMessage(org.microproxy.quic.QuicMessage, org.microproxy.quic.QuicStreamContext) sees whole messages instead, on the
connections that QuicFraming (configured with HttpProxyServerBootstrap.withQuicFraming(org.microproxy.quic.QuicFraming)) gives a MessageFramer: by
default DNS over QUIC (doq), whose messages have a two-byte length prefix. The proxy
keeps each direction's bytes until a message is whole, shows its payload to streamMessage,
and frames what that returns again, computing a length prefix afresh for an edited payload. On
such a stream streamData(org.microproxy.quic.QuicStreamMessage, org.microproxy.quic.QuicStreamContext) is not called: framing replaces it. A message larger than
QuicFraming.maxMessageSize(), or bytes the framer cannot read, reset the stream in both
directions on both connections with QuicFraming.errorCode(). Bytes left over when a side
finishes its stream, not a whole message, are passed on as they are. Streams of connections
without a framer, and every stream when the interceptor does not override streamMessage,
go through streamData as before, and nothing is kept.
bootstrap.withQuicInterceptor(new QuicInterceptor() {
public byte[] streamData(QuicStreamMessage message, QuicStreamContext stream) {
return message.data(); // other protocols' streams
}
public List<QuicMessage> streamMessage(QuicMessage message, QuicStreamContext stream) {
DnsMessage dns = message.dns(); // a DNS over QUIC message
log(dns.questions());
return List.of(message);
}
});
Threads
Each direction of each stream is read by a thread of its own, and so are the datagrams of each direction, so a connection's hooks are called from many threads at once: the interceptor must be thread-safe. Within one direction of one stream, calls come in order, and the direction waits for each one.
Every connection gets quicStart(org.microproxy.quic.QuicContext) first, then its streams' and datagrams' hooks (each
stream's streamOpened(org.microproxy.quic.QuicStreamContext) before its data), then either quicEnd(org.microproxy.quic.QuicContext) or quicError(org.microproxy.quic.QuicContext, java.lang.Throwable), never both. A hook that throws is logged; a throwing streamData(org.microproxy.quic.QuicStreamMessage, org.microproxy.quic.QuicStreamContext), streamMessage(org.microproxy.quic.QuicMessage, org.microproxy.quic.QuicStreamContext) or datagram(org.microproxy.quic.QuicDatagram, org.microproxy.quic.QuicContext) passes the bytes on unchanged.
-
Method Summary
Modifier and TypeMethodDescriptiondefault byte[]datagram(QuicDatagram datagram, QuicContext context) A datagram (RFC 9221) from one side, before it is passed on.default voidquicEnd(QuicContext context) The connection ended: one side closed its connection (the other was closed the same way) without an error, or the proxy closed both.default voidquicError(QuicContext context, Throwable error) The connection failed: one side's connection timed out, was reset (a stateless reset) or closed with a transport error.default voidquicStart(QuicContext context) A connection was intercepted: the handshakes with both sides are complete, nothing has been relayed yet.byte[]streamData(QuicStreamMessage message, QuicStreamContext stream) Bytes read from one side of a stream, before they are passed on.default voidstreamFinished(QuicStreamContext stream, boolean fromClient) One side finished sending on a stream (its FIN): called before the end is passed on, soQuicStreamContext.send(boolean, byte[])with the samefromClientcan still add bytes.default List<QuicMessage> streamMessage(QuicMessage message, QuicStreamContext stream) A whole message from one side of a stream, on a connection thatQuicFraminggives a framer, before it is passed on; on such streams this replacesstreamData(org.microproxy.quic.QuicStreamMessage, org.microproxy.quic.QuicStreamContext).default voidstreamOpened(QuicStreamContext stream) A side opened a stream, which the proxy has opened on the other connection: called before any of its data.default voidstreamReset(QuicStreamContext stream, boolean fromClient, long errorCode) One side reset its sending part of a stream (RESET_STREAM): the proxy resets the other side's with the same code, after this call.default voidstreamStopped(QuicStreamContext stream, boolean fromClient, long errorCode) One side asked the other to stop sending on a stream (STOP_SENDING): the proxy asks the other side the same, with the same code, and resets what it sends to the side that asked.
-
Method Details
-
quicStart
A connection was intercepted: the handshakes with both sides are complete, nothing has been relayed yet.QuicContext.sendDatagram(boolean, byte[])may already send datagrams.- Parameters:
context- the connection
-
streamOpened
A side opened a stream, which the proxy has opened on the other connection: called before any of its data.QuicStreamContext.send(boolean, byte[])may already add bytes.- Parameters:
stream- the stream
-
streamData
Bytes read from one side of a stream, before they are passed on.- Parameters:
message- the bytes, which side sent them and whenstream- the stream, andQuicStreamContext.send(boolean, byte[])to add bytes after these- Returns:
- the bytes to pass on:
message.data()as it is or changed in place, other bytes, or null or an empty array to drop them
-
streamMessage
A whole message from one side of a stream, on a connection thatQuicFraminggives a framer, before it is passed on; on such streams this replacesstreamData(org.microproxy.quic.QuicStreamMessage, org.microproxy.quic.QuicStreamContext). Only interceptors that override this method see messages: the proxy frames streams for no other.Each message returned is framed again (
MessageFramer.frame(byte[])) and sent in the direction itsQuicMessage.fromClient()says, on the same stream: those of the same direction follow each other in place ofmessage, and those of the other direction (made withQuicMessage.withFromClient(boolean)) are sent to the side that sentmessage, as if the other side had answered. A hook that throws, or returns a payload the framer cannot frame, has the message passed on unchanged.- Parameters:
message- the message's payload, which side sent it, on which stream and whenstream- the stream, andQuicStreamContext.send(boolean, byte[])to add bytes as they are (not framed) after these- Returns:
- the messages to send:
List.of(message)(the default) to pass it on with any change made in place, others, or null or an empty list to drop it
-
streamFinished
One side finished sending on a stream (its FIN): called before the end is passed on, soQuicStreamContext.send(boolean, byte[])with the samefromClientcan still add bytes.- Parameters:
stream- the streamfromClient- whether the client finished sending, rather than the server
-
streamReset
One side reset its sending part of a stream (RESET_STREAM): the proxy resets the other side's with the same code, after this call.- Parameters:
stream- the streamfromClient- whether the client reset it, rather than the servererrorCode- the application error code
-
streamStopped
One side asked the other to stop sending on a stream (STOP_SENDING): the proxy asks the other side the same, with the same code, and resets what it sends to the side that asked.- Parameters:
stream- the streamfromClient- whether the client asked the server to stop, rather than the server the clienterrorCode- the application error code
-
datagram
A datagram (RFC 9221) from one side, before it is passed on. Datagrams are unreliable: the proxy drops one that the other side does not take, or that is too large for it.- Parameters:
datagram- the payload, which side sent it and whencontext- the connection, andQuicContext.sendDatagram(boolean, byte[])to add datagrams- Returns:
- the payload to send on as one datagram (
datagram.data()as it is or changed in place, or other bytes), or null to drop it
-
quicEnd
The connection ended: one side closed its connection (the other was closed the same way) without an error, or the proxy closed both.- Parameters:
context- the connection
-
quicError
The connection failed: one side's connection timed out, was reset (a stateless reset) or closed with a transport error. Called instead ofquicEnd(org.microproxy.quic.QuicContext).- Parameters:
context- the connectionerror- what failed
-