pub struct ProxyControl { /* private fields */ }Expand description
A handle onto a running proxy.
Cheap to clone — one Arc bump — and every clone reads and writes the
same state, so a handle may be moved into a task, kept beside the proxy,
or both. It holds no lock across an await and takes no part in the
forwarding path.
Obtained from
TransparentProxy::control,
which can be called at any point after the proxy is constructed —
including before
TransparentProxy::run is
awaited, which is the usual case, because run() does not return until
the proxy is finished.
§What it reports
ProxyControl::local_addr— the address the listener bound, orProxyError::NotBoundbefore there is one.ProxyControl::sessions— the ids of the sessions that are live at this instant.ProxyControl::stats— what this proxy’s shaping has done across every session it has accepted, ended ones included, cleared byProxyControl::reset_stats. The one report here that is cumulative rather than instantaneous, which is why it survives the disconnect that removes a session from the list above.
The first two are snapshots of a proxy that keeps moving. A session named by
sessions() may end before the next line of the caller’s code runs;
that is not a defect in the snapshot but the reason every request that
takes a SessionId can answer ControlError::SessionEnded.
§What it changes, and how far each change reaches
ProxyControl::set_impair/ProxyControl::clear_impair— the next datagram that leg’s socket passes, in either direction. The only verb here that reaches traffic already flowing over a connection that already exists.ProxyControl::set_shaper_enabled— the next release decision each queue makes, on every session already running.ProxyControl::set_shape— the next stream a running session forwards, and every session accepted afterwards.ProxyControl::set_transport— the next connection the leg makes or accepts, and never one that already exists.ProxyControl::close_session,ProxyControl::reset_stream,ProxyControl::inject_control— one named session, or one stream of it, immediately.
Implementations§
Source§impl ProxyControl
impl ProxyControl
Sourcepub fn local_addr(&self) -> Result<SocketAddr, ProxyError>
pub fn local_addr(&self) -> Result<SocketAddr, ProxyError>
The address this proxy’s listener is bound to.
ProxyError::NotBound until
TransparentProxy::run has
bound the endpoint, and again once it has returned and the endpoint
is gone. Between those two points this is the address a client
connects to, which is the reason the method exists: a proxy
configured with port 0 chooses its port inside run(), and before
this there was no way to learn which port that was without binding
the socket in the caller and handing it in.
A bound listener can still fail to report its address — the query
goes to the operating system — and that failure is reported as
ProxyError::Listener, distinct from not being bound at all.
Sourcepub fn sessions(&self) -> Vec<SessionId>
pub fn sessions(&self) -> Vec<SessionId>
The ids of the sessions that are live right now.
An id appears here when its session starts running and disappears
when that session ends, for any reason at all — including the ones
no teardown site could be written for. It is not a log: a
session that has ended leaves nothing behind, so a caller that wants
a history of everything the proxy handled reads
ProxyEvent::SessionStarted
and
ProxyEvent::SessionEnded
from its observer instead.
Sorted ascending, which makes the value comparable between two calls without the caller sorting it first. Ids are minted monotonically per proxy, so the order is also arrival order.
§This list and the event stream do not have to agree
SessionStarted is emitted when a connection is accepted, before the
session has dialled the upstream relay; registration here happens
when the session begins to run. A session whose upstream connect
fails is therefore reported as started, is briefly listed here, and
then disappears — and a session that has been accepted but has not
been polled yet is in the event stream and not in this list. Sessions
driven by constructing a
ProxySession directly, rather than
through a proxy’s accept loop, appear in neither: they belong to no
proxy and so to no control plane.
Sourcepub fn stats(&self) -> ProxyStats
pub fn stats(&self) -> ProxyStats
What this proxy’s shaping has done, across every session it has accepted — including the ones that have already ended.
The complement of Self::sessions, and the difference between them
is the whole reason this exists. That list is what is live now and
shrinks when a client disconnects; these figures are cumulative and
monotone, so a caller polling them across a disconnect sees them hold
rather than fall. A total summed over the live list could not have
that property: session registrations are released by a Drop, and a
statistic that goes down under normal operation cannot be alerted on.
It follows that a session which ended for any of the reasons no teardown site could be written for — its whole future dropped, its task cancelled mid-write — has still contributed everything it moved, because each unit was charged at the instant it was handled.
Defined before the proxy has bound: a proxy that has accepted nothing
answers ProxyStats::default() rather than an error, the same shape
Self::sessions takes.
§An all-zero answer means “no profile”, not “no traffic”
Every counter behind this is written by the shaping path, which a
session with no ShapeProfile never enters. A proxy forwarding
gigabytes with no profile configured reports
ProxyStats::default(), and nothing in the value distinguishes that
from a proxy nothing has connected to. Ask Self::sessions, or an
observer’s event stream, which of the two it is.
§Sessions driven directly are not counted
A ProxySession constructed by a
caller instead of accepted by this proxy belongs to no control plane,
so it reports only its own
ShapeStats — the same rule
Self::sessions follows, and for the same reason: a proxy must not
claim traffic it never accepted.
§Cost
A read of about forty relaxed atomics plus nine per class row, and
one Vec and one String allocated per class row. Cheap, and still
a reader’s call rather than something to poll in a tight loop.
Sourcepub fn reset_stats(&self)
pub fn reset_stats(&self)
Zero every figure Self::stats reports, and start again from here.
For a caller measuring a phase rather than a run: reset, drive the traffic, read. Without it the only way to get a phase figure is to subtract two snapshots, which is correct but leaves every assertion written against a difference rather than a value.
§What it does not do
It does not touch any session’s own
ShapeStats. Those are a session’s for
its whole life, and a proxy-level verb that silently rewrote them
would make a scenario reading both see two different pasts.
It does not clear the class rows themselves, only their counters. The rows are sized once, from the first shaped session this proxy accepts, because a class index means what the scheduler that produced it says it means; dropping them here would let the next session install a different class list and report figures under it.
§It does not stop traffic
The counters are cleared one relaxed store at a time while the forwarding tasks keep charging them, so a reset that races a live stream can land between two increments of the same unit and leave a row part-cleared. There is no atomic form of this — no primitive clears forty counters at once — and pausing the proxy to get one would be a far larger promise than the figures are worth. Reset when the traffic you are about to measure has not started.
Sourcepub fn set_transport(
&self,
leg: Leg,
p: TransportProfile,
) -> Result<(), ControlError>
pub fn set_transport( &self, leg: Leg, p: TransportProfile, ) -> Result<(), ControlError>
Set the QUIC transport parameters one leg uses — for connections it has not made yet.
§This never reaches a connection that already exists
Read that sentence as the whole of what this method does, because every other sentence here is a consequence of it. A QUIC connection takes its transport configuration once, at setup, and holds it for life; quinn exposes four setters on a connection that is already up — the two stream-count limits and the two windows — and no way at all to hand it a different configuration. So a profile set here reaches:
-
on
Leg::Upstream, the next connection this proxy opens, which is the next session it accepts, because a session dials the relay once and then forwards over that connection until it ends; -
on
Leg::Client, the next connection this proxy accepts, which the proxy does not initiate and which may never arrive.
It follows that on a proxy which never dials or accepts again — one
whose clients have all connected, one nothing is connecting to, one
about to be cancelled — this call is a permanent silent no-op that
returns Ok(()). Nothing about the return value distinguishes that
from a setting that reached every connection made afterwards, because
there is nothing to distinguish: the parameters were installed, and
what did or did not happen next is traffic rather than configuration.
A caller that needs the change to bite on a session already running
has one instrument, and it is
ProxyControl::close_session — a leg that reconnects is a leg
that installs this.
§Every call is absolute: None is a default, not “leave alone”
The profile is built into a fresh quinn::TransportConfig — a
TransportConfig::default() with the profile applied over it — and
that config is installed whole. So a field left None is not carried
over from the profile installed before it: it takes quinn’s default.
Two consecutive calls therefore do not compose. A call setting
stream_receive_window followed by a call setting only
max_idle_timeout leaves the leg with the idle timeout and quinn’s
default window, not with both settings. TransportProfile::default()
puts a leg back on quinn’s defaults entirely, which is the only way to
clear a setting and is why this is worth stating: a merge would look
like a convenience and would make None mean two different things —
“unset” when a profile is built and “unchanged” when it is installed —
leaving no profile that could ever clear a field, and no way for a
caller restoring normal conditions to say so.
§What it replaces
Whatever that leg’s transport parameters were, whether they came from
a TransportProfile or from a raw quinn::TransportConfig in the
configuration the proxy was built with. The two cannot be combined —
see
ProxyError::TransportConfigAndProfile
— so a live profile that was merged with a configured raw config
would be a merge that cannot be written; a live profile that sat
beside one would turn every subsequent connect into that refusal.
Replacing is the only answer that leaves the leg usable, and it means
a caller who set a raw config at build time loses it from the first
call to this.
The profile is built through the leg’s own
TransportInstaller when the
proxy was given one, so a leg keeps building its configuration the
way it always did.
§Errors
ControlError::Profile for a profile the leg would not have
started with — the same check, reporting the same error, run here
rather than at the next connect, so a refusal is attached to the call
that caused it instead of to a connection failure minutes later.
ControlError::Unsupported on Leg::Upstream when the upstream
is WebTransport. That endpoint is built inside the WebTransport
library, which takes no quinn::TransportConfig and hands back no
endpoint to install one on, so the profile could only be stored and
ignored.
Sourcepub fn set_shape(&self, p: ShapeProfile) -> Result<(), ControlError>
pub fn set_shape(&self, p: ShapeProfile) -> Result<(), ControlError>
Replace the shaping profile this proxy’s sessions pace with.
§What it reaches, and when
Every session this proxy accepts afterwards shapes with p, whatever
the profile it was configured with.
A session that is already running picks p up at the next stream
it forwards, not at the next object. That granularity is forced: a
stream’s egress queue holds the scheduler its units were admitted
under, and a class is an index into that scheduler’s class list, so a
unit classified under one profile and released under another charges
one profile’s class against the other profile’s buckets. Streams
already forwarding therefore run to their end under the profile they
started with, and on MoQT that is rarely a long wait — media arrives
on a fresh unidirectional stream per subgroup.
A session that started unshaped stays unshaped for its whole
life, and nothing here changes that. Shaping classifies
ObjectMeta, which only the object
framer produces, and whether a session frames at all is settled at
session start from the profile it was configured with. A session that
began as a byte pump has no objects to classify and no queue to pace.
§A session whose class list would change keeps its own profile
A running session takes p only if p’s class names are the same,
in the same order, as the ones the session started with. Otherwise it
keeps the profile it has until it ends, and only sessions accepted
afterwards get the new one.
The reason is the statistics.
ShapeStats has one row per configured
class, pre-sized when the session is constructed and never resized,
and a class is charged to its row by position. A profile with a
different class list would therefore charge its classes to rows still
named after the old profile’s — every number in
ProxySession::shape_stats
correct, and every label on it wrong — or, for a class beyond the end
of the original list, silently to the default row. Refusing the swap
per session is the only answer that keeps a reader’s numbers
attributable. To move a running session onto a different class list,
end it with ProxyControl::close_session; its replacement starts
on the new profile.
§Buckets start full
A session that takes p builds a scheduler for it, and a fresh
scheduler’s buckets are full as of that instant — the same choice a
session’s first scheduler makes, so that a session does not open with
a burst-sized delay in front of its first object. Setting the same
profile repeatedly therefore hands every class a fresh burst each
time, and a caller doing that in a tight loop measures no rate limit
at all. The report-once diagnostics
(ImpairmentKind::ShapeRuleUnmatchable,
ImpairmentKind::ShapeBurstBelowUnit) start again with the new
scheduler too, so a rule that is unmatchable under both profiles is
reported once per profile rather than once per session.
§Errors
ControlError::Shape for a profile ShapeProfile::try_new
rejects. Since that constructor is the only way to build a
ShapeProfile, a profile arriving here has already passed it and the
refusal is unreachable today; the check is re-run rather than assumed
so that the rules live in exactly one place and a later construction
path cannot arrive here unchecked.
Sourcepub fn set_shaper_enabled(&self, on: bool)
pub fn set_shaper_enabled(&self, on: bool)
Turn pacing off, or back on, for every session this proxy is running and every session it accepts afterwards.
Off is not “no profile”. The profile stays exactly where it was, the
classes keep claiming units, the per-stream queue depth keeps
applying and the statistics keep moving — what stops is the token
bucket and the discipline, so every unit is released as soon as the
queue reaches it. set_shaper_enabled(true) resumes with the same
configuration and the same counters, which is what makes this usable
as a switch in a timeline rather than as a way of throwing a profile
away.
§When a stream that is already held resumes
The switch is read on the next release decision a queue makes, and a queue makes one when its head’s wait expires. Switching pacing off therefore does not reach into a wait that is already running; it changes the answer the stream gets when that wait ends. How long that is depends on why the stream was held:
-
held by a bucket that will refill — one unit’s worth of the configured rate, so a stream paced at a real rate resumes within one pacing interval;
-
held behind another class — as soon as that class drains, which is now immediate;
-
held by a bucket configured at zero, or one whose burst cannot cover a unit — there is no refill instant, so the stream’s wait is the queue’s
max_holdclamp and the switch is not read until it fires. On a stopped class, switching pacing off does not promptly release what is already queued. Nothing in this crate can: the wait is a timer a per-stream queue armed, and there is no session-wide wake that reaches one. UseProxyControl::set_shapeto move the session onto a profile with a rate, or end the session, if that is what is wanted.
§It changes nothing on a proxy with no profile
Returns nothing, and cannot fail, so it is silent about a proxy where
no session has a ShapeProfile to pace with — there is nothing to
switch and no consequence to report. That is the one case where
calling this has no observable effect whatever.
Sourcepub fn close_session(
&self,
id: SessionId,
code: u32,
reason: &[u8],
) -> Result<(), ControlError>
pub fn close_session( &self, id: SessionId, code: u32, reason: &[u8], ) -> Result<(), ControlError>
End one session, giving its egress queues a bounded window to flush
first, and close both of its legs with code and reason.
Returns as soon as the request has been taken. The drain and the close happen in the session, which is the only place that can see them through — a method that waited would have to hold the caller for as long as the drain took, and the drain is bounded precisely so that nobody has to.
§The window, and what happens at the end of it
The window is
EgressConfig::drain_timeout
from the session’s configuration, 100 ms by default. Within it, the
session keeps running exactly as it was: units come off the
per-stream queues at their release times, a shaped class keeps
paying its bucket, and a hook that deferred something still gets it
written. The window ends early — and this is the common case — the
moment the session has nothing queued anywhere.
When it ends because the timeout expired, whatever is still queued
is abandoned and reported, once per stream, as
ImpairmentKind::QueuedBytesAtTeardown.
It is not flushed first. A flush at that point would hand the bytes
to a connection that is about to send CONNECTION_CLOSE, which
discards whatever it had buffered — so the bytes would be neither
confirmably delivered nor confirmably lost, and no count of them
would add up. Abandoning them keeps the arithmetic exact: what the
peer received plus what the impairments name is what was queued when
this was called.
Either way the session then closes with code and reason. That
part is unconditional for the call that was accepted — a drain that
ran out of time changes what reached the peer, never what the close
says. A call that is refused starts nothing at all; see Errors,
which covers the one refusal a caller can produce deliberately.
§Control streams are not repaired
Nothing is synthesized on a control stream to tidy up the close. If
a control message was half-written when the window closed, the peer
gets a truncated message and the session reports
ImpairmentKind::ControlStreamTruncated.
Completing the message would mean the proxy inventing control-stream
bytes that neither peer wrote.
§Errors
ControlError::SessionEnded when the session has already ended or
is already ending — the ordinary race, since a session may end
between listing it and closing it — and
ControlError::NoSuchSession for an id this proxy never ran.
“Already ending” covers one case that is not a race at all, and a caller can produce it deliberately: a second call while the first call’s drain window is still open. The first close fixed the code and the reason and nothing revises them, so a second call’s pair would reach neither peer. It is refused here rather than accepted, because the only alternative is to take a request and drop it — the session’s command task is inside the first drain and cancels when it comes out, so it never returns for a second. A caller that wants a different code has to ask before the first close, not after it.
One thing can survive a refusal, and only in the direction that helps: a call that got as far as fixing the pair and then found the session unreachable still closes it with what was asked for. So a caller that sees this error was either late for everything — the two refusals above — or late only for the drain.
§What an observer sees
This is the one verb here that produces events, and it produces them through the session rather than from this call — after the drain, which is what makes them a record of what happened rather than of what was asked for:
- one
ProxyEvent::SessionEndedwhosereasonbegins*control plane closed the session*and quotescodeandreason. That wording is the point: a hook’sAction::CloseSessionreaches the same latch and reports*hook closed the session*, and for a while both said the latter, so an operator ending a session was recorded as the scenario under test ending it. - one
ImpairmentKind::QueuedBytesAtTeardownper stream the window ran out on, and none at all for the ordinary case where everything drained. - at most one
ImpairmentKind::ControlStreamTruncatedper control direction that was mid-message when the window closed.
Nothing is emitted at the moment the request is accepted. The return value is that answer, and an event that duplicated it would be the only event in this enum an observer could receive for something that had not happened yet.
Sourcepub fn reset_stream(
&self,
id: SessionId,
stream: StreamKey,
code: u64,
) -> Result<(), ControlError>
pub fn reset_stream( &self, id: SessionId, stream: StreamKey, code: u64, ) -> Result<(), ControlError>
Reset one live forwarded stream, immediately.
stream names a stream this session is forwarding; the key is the
one carried on the events and hook contexts for that stream. code
becomes the RESET_STREAM application error code on the
destination, and the source is stopped with the same code, so both
peers learn the stream was abandoned rather than finished.
§Bytes already handed to the transport are gone
A reset abandons the destination stream. Whatever the proxy had written but the transport had not yet acknowledged goes with it — QUIC does not retransmit data on a stream that has been reset — and so does everything this stream still had queued. That is what a reset is, and it is why the method exists, but it means the peer’s view of this stream ends at an arbitrary byte and no count of what it received is predictable from what was sent.
§Errors
ControlError::NoSuchStream when no stream with that key is live
— which is the same answer for a key that never existed and for one
whose stream has already ended, because a stream’s registration is
removed as it ends and nothing is left to tell them apart. Plus the
two session-level refusals.
§One case where Ok(()) is delivery rather than a reset
The request is handed to the task that owns the stream’s write half
and is acted on the next time that task comes round its own loop,
which is immediately in every case but one. A control direction on
the forward-first pipe stops reading its request channel once it is
holding COMMAND_QUEUE_DEPTH injections it has not been able
to place — the backpressure that makes a further
ProxyControl::inject_control answer
ControlError::SessionEnded rather than queue without limit — and a
reset handed over in that state waits until an injection can be
written, which that method’s own documentation says can be never.
Below that depth, and on every data stream, the reset is served at
once.
§No observer event, and neither existing one may be borrowed
The stream’s task performs the reset and emits nothing.
ProxyEvent::StreamReset
means a teardown this proxy observed a peer perform, and
ProxyEvent::ActionApplied
means a hook asked for one at a named site. Reusing either would make
it ambiguous for every reader that already relies on it — an observer
counting peer resets would start counting the operator’s, with
nothing in the payload to separate them — and this crate does not
widen the meaning of a shipped event to save adding one.
A variant of its own was considered and declined: it would carry
nothing the caller does not already hold. The session, the stream and
the code came from this call, and the answer to “did it happen” is
the return value. What is worth observing is the consequence, and
that is observed where consequences are — at the peer, as a
RESET_STREAM carrying code, which is what this crate’s own gate
for the method checks.
Anything the stream still had queued goes with it and is not
reported as
ImpairmentKind::QueuedBytesAtTeardown.
That report is about a teardown losing bytes it was trying to
deliver; here the caller asked for the destination to be abandoned,
and the bytes going with it is what the word means rather than a
reduction in what the proxy could do.
Sourcepub fn inject_control(
&self,
id: SessionId,
leg: Leg,
bytes: Vec<u8>,
) -> Result<(), ControlError>
pub fn inject_control( &self, id: SessionId, leg: Leg, bytes: Vec<u8>, ) -> Result<(), ControlError>
Put a control message on one leg of a live session, in sequence.
leg names whose decoder the message is for:
Leg::Client writes toward the client, Leg::Upstream toward
the relay. The bytes are written onto the session’s existing
control stream, interleaved with the messages already flowing on
it, at a point the receiving decoder accepts.
§What “in sequence” costs, and why it is not a fresh stream
A new stream would be far simpler and would look like working code: the write succeeds, the bytes arrive. The peer reads them as whatever a new stream of that kind is — a data stream header on a fresh unidirectional stream, a request on a fresh bidirectional one — and the message is never seen as a control message at all. On the drafts that carry the control plane on unidirectional streams it would be worse than useless: a second stream announcing itself as a control stream is a second SETUP, which a peer is entitled to close the session over. So the write goes to the task that owns the control stream’s write half, and lands between two forwarded messages rather than inside one — a control stream is a single framed byte sequence, and bytes spliced into the middle of a message’s payload desynchronize the peer’s decoder for the rest of the session.
Because of that, a message injected while one is mid-flight is held until the message in flight has been written. That wait is bounded by the peer finishing the write it started, not by anything this proxy does.
§A held message can wait forever, and this returns Ok(()) anyway
The call returns as soon as the request has been taken, before any write
is attempted — it has to, or a caller would be held for as long as the
peer took. So Ok(()) means accepted for writing, and on the
forward-first control pipe — the one a session takes unless a hook
declared Interest::CONTROL — there are two ways for the write never to
happen:
- the direction goes quiet mid-message. That pipe forwards read chunks and finds the boundaries in the bytes it is forwarding, so a direction whose peer stopped writing part-way through a message never reaches one. The injection waits for a peer that may never write again.
- the proxy has lost the framing. The boundary walk is driven by the session’s draft, which for the moq-00 cohort (drafts 07-14) is a configured guess until a SETUP is peeked. A wrong guess makes the lengths wrong; rather than place a message by guesswork the walk latches to the boundaries are unknown, and from then on nothing is injected on that direction at all. That is deliberate and it is the better of the two outcomes — a misplaced injection desynchronizes the peer’s decoder for the rest of the session, while one that never arrives leaves every other message intact.
A message still held when the session ends is discarded, and nothing reports it: no event, no impairment, no error. A caller that needs to know an injection landed observes it at the peer, which is what this crate’s own gate for the method does.
That silence is the weakest point on this page, and it is written
down rather than smoothed over. An accepted request that never
reaches the wire is exactly the failure this crate refuses
everywhere else, and the only thing standing in for a report is the
paragraph above. An impairment for it was considered and not added:
the held messages live in a local deque of the control pipe, the
pipe returns from seven places, and a report wired into some of them
would be a promise that is kept for some teardowns and not others —
which is worse than no promise, because a scenario would then read
the absence of the event as delivery. The report that would be worth
having has to be raised by something whose completeness is
structural, the way SessionGuard is for the session census, and
that is not a wiring change.
A hook declaring Interest::CONTROL puts the stream on the pipe that
decodes each message before forwarding it, where every return to the
select is a boundary by construction and neither case above exists.
§bytes must already be framed, and is not checked
Pass a complete, framed control message for the session’s draft —
what AnyControlMessage::encode produces, which is the type varint,
the length field, and the payload. The proxy writes it verbatim: it
does not frame it, does not decode it, and does not know what it
says. A payload passed without its framing is the second way to get
this wrong that still looks like working code — the write succeeds
and the peer’s decoder reads the first bytes of the payload as a
message type and length, and is lost from there on.
Nothing is validated because there is nothing to answer with: the
refusals this method can give are about reaching a session, and
inventing a those bytes were not a message refusal would mean decoding
every injection on the session’s forwarding path.
§Which stream that is, per draft
Two topologies, and the session picks between them from its draft.
Through draft 16 the control plane is one client-initiated
bidirectional stream and leg names which of its two directions to
write on. From draft 17 it is a pair of unidirectional streams,
one opened by each peer, and bidirectional streams carry requests
instead — draft-17 Section 3.3. There leg names which of the two
unidirectional control streams to write on, and a request stream is
never a candidate: the session routes an injection to the control
leg’s channel, which only a control direction’s pipe ever serves.
The leg mapping is the same on both: the leg is the peer whose
decoder reads what is written. Leg::Upstream puts the message in
front of the relay, Leg::Client in front of the client.
§Errors
The two session-level refusals. A session whose control stream has not been established yet is not an error: the message waits and is written as soon as there is a stream to write it on. On drafts 17 and later that wait covers a little more ground — the control stream is identified from the first varint of a unidirectional stream, so a session whose peer has not opened one yet has no control stream in that direction and the message waits for it exactly as it waits for a message boundary.
§What an observer sees
Nothing for the injection itself — no event on acceptance, none on
the write, none on the discard described above. The bytes are written
verbatim and are never decoded here, so there is no message to put on
a
ProxyEvent::ControlMessage
and no honest way to synthesize one; and the peer’s own reaction, if
it has one, arrives as ordinary forwarded traffic.
Trait Implementations§
Source§impl Clone for ProxyControl
impl Clone for ProxyControl
Source§fn clone(&self) -> ProxyControl
fn clone(&self) -> ProxyControl
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read more