Skip to content
This repository was archived by the owner on Dec 19, 2025. It is now read-only.

Comments

Gateway opt in mechanism#58

Merged
nothingmuch merged 3 commits intopayjoin:mainfrom
nothingmuch:gateway-opt-in
Mar 17, 2025
Merged

Gateway opt in mechanism#58
nothingmuch merged 3 commits intopayjoin:mainfrom
nothingmuch:gateway-opt-in

Conversation

@nothingmuch
Copy link
Collaborator

@nothingmuch nothingmuch commented Mar 9, 2025

This adds a new gateway_prober module, whose main type Prober allows checking if a GatewayUri is allowed to be queried by the relay.

This is done by performing a GET /.well-known/ohttp-relay?allowed_purposes. If the response is an application/x-ohttp-allowed-purposes, which is a list of values encoded as in TLS ALPN (U16BE for number of elements, U8 length prefix for each binary string value), and this is found to contain the magic string BIP77 454403bb-9f7b-4385-b31f-acd2dae20b7e, then the gateway is considered to have opted in to receiving OHTTP requests for this purpose.

Although appropriate in principle, the Cache-Control and/or Retry-After headers given by the (potential/alleged) gateways are not taken into consideration as no such functionality exists in the payjoin directory's OHTTP gateway implementation, and such functionality would require parsing logic (a crate exists for this FWIW).

When the prober is over capacity (too many cached or inflight gateway probes), a Cache-Control max-age=... header is provided for the 503 Service Unavailable response, with the TTL of the earliest expiring entry.

Other negative responses do not set Cache-Control, doing so would require a second Error::NotFound variant or some other way of setting that, this doesn't really seem useful for clients or service operators, so that is also omitted for simplicity.

This should probably be squashed before merging since the first commit does not past lints due to dead code.

Depends on #47

@nothingmuch nothingmuch requested a review from DanGould March 9, 2025 23:53
@nothingmuch nothingmuch force-pushed the gateway-opt-in branch 2 times, most recently from 9bf7352 to 3d0d876 Compare March 9, 2025 23:59
@nothingmuch nothingmuch marked this pull request as draft March 10, 2025 00:12
@nothingmuch
Copy link
Collaborator Author

converting to draft because mokckito dev dep broke MSRV, i'll downgrade tomorrow this depends on RFC 9540 stuff anyway

and another loose end, I guess the the GATEWAY_ORIGIN environment variable can be optional now, so i'll do that too

@nothingmuch
Copy link
Collaborator Author

mockito 0.32 and up require rust 1.65, that's when the async stuff was introduced

and dev-dependencies can't be optional unfortunately

so i think we have three choices:

  1. add an optional dep on mockito as a project dependency and feature gate it
  2. raise MSRV to 1.65
  3. refactor the hyper based test server to allow varying its responses (i sunk a few hours into this before switching to mockito and kind of got stuck)

@nothingmuch
Copy link
Collaborator Author

Secondly, I screwed up and didn't realize the websockets handler does not work like CONNECT until yesterday, so I suspect the GATEWAY_ORIGIN environment variable can't be made optional after all, but I can't find information about where/how websockets is actually used, what's the motivation for that functionality @DanGould?

@DanGould
Copy link
Collaborator

DanGould commented Mar 10, 2025

The reason we use WebSocket to bootstrap is so that it can be done from a browser, where WebSockets, where as far as my understanding extended when I wrote the ws-bootstrap feature, is the only way to make a tunnel streaming a logical TLS/TCP connection through a proxy because HTTP CONNECT is not available in that way.

So it was built as a secondary mechanism to HTTP CONNECT (and we've come up with a third, to get keys from trusted bip21 pj= fragment input), where HTTP CONNECT is not available in web-first environments.

This is the PR where I came up with it. Probably makes sense to write detailed mod-level docs after we resolve this conversation and you feel like you understand what I'm saying.

edit: see Mutiny's WebSockets bootstrap here: https://github.com/MutinyWallet/mutiny-node/pull/820/files#diff-9bd1c92f85da5773c1cf3b66662d28004dd0c00ab719781af7d7a77aa712fb20R140-R209

Copy link
Collaborator

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beautiful changes.

I sense some unease about the encapsulation of Policy as Known vs InFlight but also a sense that the abstraction is best as implemented.

Other than what you documented in KnownGateways::insert, do you see this being influenced by another change to itself change? If so, what would that change be? It seems ok and well tested as-is.


test_inflight_deduplication was the hardest for me to understand even though step-by-step it was commented well. A docstring summary might have helped that. Curious also if you think unwrap() is the most appropriate to leave in the test there.

@nothingmuch
Copy link
Collaborator Author

Other than what you documented in KnownGateways::insert, do you see this being influenced by another change to itself change? If so, what would that change be? It seems ok and well tested as-is.

It's kinda lame that probe needs to both .send() on the oneshot, and .insert() to overwrite the future containing it from the map, but it seemed simpler to have KnownGateways just be a dumb mutable container, instead of letting it spawn a task from KnownGateways that listens on the shareable future and calls insert() on itself. The benefit of that is that KnownGateways would maintain its own internal state consistently, but seeing as its a private type and just an implementation detail of the prober I figured that's overkill, but I'm not married to this approach.

test_inflight_deduplication was the hardest for me to understand even though step-by-step it was commented well. A docstring summary might have helped that. Curious also if you think unwrap() is the most appropriate to leave in the test there.

It felt a bit awkward adding expect messages, e.g. expect("mutexes should be usable") or something like that, esp. since this code should never end up in an example.

Perhaps a more readable way of doing that is to bundle the condvar & mutex into a simpler synchronization helper that just has .wait() and .release(). The counter could also be an atomic int instead of a locked int, but i'm not sure if that's any more readable.

@nothingmuch
Copy link
Collaborator Author

Force pushed, addressing review comments, removing additional unwrap()s, and hacky pinning colored in the msrv ci job

nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 11, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.
@DanGould
Copy link
Collaborator

It felt a bit awkward adding expect messages, e.g. expect("mutexes should be usable") or something like that, esp. since this code should never end up in an example.

These are the places I really like to bubble up ?. It looks cleaner but is also mere preference.

@nothingmuch
Copy link
Collaborator Author

It felt a bit awkward adding expect messages, e.g. expect("mutexes should be usable") or something like that, esp. since this code should never end up in an example.

These are the places I really like to bubble up ?. It looks cleaner but is also mere preference.

I disagree in this case, since that code makes no sense as an example, so there's no benefit to avoiding unwrap() in case the code gets copied around, but if an error does occur in a test for some reason (afaik only EINVAL should be possible from underlying pthread_cond_wait, etc, which would suggest a bug) then bubbling as an error would make it harder to figure out where it originated.

@nothingmuch
Copy link
Collaborator Author

note that Cache-Control is not correct for service unavailable, it should be Retry-After

nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 11, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.
nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 11, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.
nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 14, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.

Rationale for adding a UUID: although arguably 64 bits of randomness is
more than enough, and even just the string "BIP 77" is likely to be
unique and unambiguous, adding a UUID practically ensures an opt-in is
really an opt-in as 128 random bits would not be repeated except
intentionally.

Rationale for format: we just need a set of opaque identifiers, encoding
the same way as TLS ALPN does makes sense since that's well specified,
binary safe, easy to parse and places readonable limits on string
lengths.
@nothingmuch nothingmuch force-pushed the gateway-opt-in branch 2 times, most recently from 32b4349 to d523a1f Compare March 14, 2025 23:17
nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 14, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.

Rationale for adding a UUID: although arguably 64 bits of randomness is
more than enough, and even just the string "BIP 77" is likely to be
unique and unambiguous, adding a UUID practically ensures an opt-in is
really an opt-in as 128 random bits would not be repeated except
intentionally.

Rationale for format: we just need a set of opaque identifiers, encoding
the same way as TLS ALPN does makes sense since that's well specified,
binary safe, easy to parse and places readonable limits on string
lengths.
nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 14, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.

Rationale for adding a UUID: although arguably 64 bits of randomness is
more than enough, and even just the string "BIP 77" is likely to be
unique and unambiguous, adding a UUID practically ensures an opt-in is
really an opt-in as 128 random bits would not be repeated except
intentionally.

Rationale for format: we just need a set of opaque identifiers, encoding
the same way as TLS ALPN does makes sense since that's well specified,
binary safe, easy to parse and places readonable limits on string
lengths.
nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 14, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.

Rationale for adding a UUID: although arguably 64 bits of randomness is
more than enough, and even just the string "BIP 77" is likely to be
unique and unambiguous, adding a UUID practically ensures an opt-in is
really an opt-in as 128 random bits would not be repeated except
intentionally.

Rationale for format: we just need a set of opaque identifiers, encoding
the same way as TLS ALPN does makes sense since that's well specified,
binary safe, easy to parse and places readonable limits on string
lengths.
@nothingmuch nothingmuch force-pushed the gateway-opt-in branch 2 times, most recently from 34cff4e to 79efb68 Compare March 15, 2025 18:06
@nothingmuch nothingmuch marked this pull request as ready for review March 15, 2025 18:07
nothingmuch added a commit to payjoin/rust-payjoin that referenced this pull request Mar 15, 2025
With this change, the payjoin directory's RFC 9540 compatible OHTTP
gateway additionally opts in to receiving OHTTP requests from arbitrary
relays.

See payjoin/ohttp-relay#58. 

Depends on
- #549
- #572
Copy link
Collaborator

@DanGould DanGould left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 93ed428

@nothingmuch nothingmuch force-pushed the gateway-opt-in branch 3 times, most recently from 37c5d1c to 9322f65 Compare March 17, 2025 02:40
RFC 9458 (OHTTP) specifies a 1:1 mapping between relays and gateways in
order to preclude abuse. That presents an issue for payjoin UX, the
receiver chooses the directory, which is its own OHTTP gateway, but then
the sender must know a relay that will forward requests to that
directory.

In order to eliminate the need for maintaining up to date relay
configurations, this commit adds a mechanism that allows OHTTP gateways
to signal to relays that they will accept OHTTP requests for a specified
purpose (i.e. BIP77), even if the relay is not configured to allow that
specific gateway a priori.

The new opt-in mechanism is extends RFC 9540: when querying the
`/.well-known/ohttp-gateway` endpoint with query string
`?allowed_purposes`, if the gateway responds with an
`application/x-ohttp-allowed-purposes` response whose body is a list of
bytestrings encoded in the same format as TLS ALPN (U16 big endian
element count list, with U8 length prefixed bytestring), and this
response contains the magic string
`BIP77 454403bb-9f7b-4385-b31f-acd2dae20b7e` as one of the elements,
this indicates that the relay may forward BIP77 OHTTP requests to it.

The relay now expects POST requests to include the gateway in the
request path. Only the scheme and authority are to be included (same as
GatewayUri struct), the RFC 9540 path on the gateway's host is implied.

When a gateway does not opt-in, this is treated as 404 Not Found.

When the prober is over capacity, the relay will respond with 503
Service Unavailable whenever requests to unknown gateways are made.
Retry-After will be set for these responses according to the TTL of the
soonest expiring gateway status.
In order to allow the prober to share the same client as the proxy, the
Request<Incoming> type needs to be boxed, as even an empty Incoming body
type can't be created outside of the hyper crate.

When converting the request, two changes in behavior were also
implemented:

No longer set Host header to 0.0.0.0. Instead, let the client set it
from the request URL's authority.

Also specify Content-Type as `message/ohttp-req`, this was previously
omitted from the forwarded request.
@nothingmuch nothingmuch merged commit c1a4bfd into payjoin:main Mar 17, 2025
5 checks passed
nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 17, 2025
payjoin-test-util's Cargo.toml now refers to the merge commit of
payjoin/ohttp-relay#58, so before the payjoin crate can be released
ohttp-relay needs to be released and this pin removed.
nothingmuch added a commit to nothingmuch/rust-payjoin that referenced this pull request Mar 17, 2025
payjoin-test-util's Cargo.toml now refers to the merge commit of
payjoin/ohttp-relay#58, so before the payjoin crate can be released
ohttp-relay needs to be released and this pin removed.
nothingmuch added a commit to payjoin/rust-payjoin that referenced this pull request Mar 17, 2025
shinghim pushed a commit to shinghim/rust-payjoin that referenced this pull request Mar 25, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.

Rationale for adding a UUID: although arguably 64 bits of randomness is
more than enough, and even just the string "BIP 77" is likely to be
unique and unambiguous, adding a UUID practically ensures an opt-in is
really an opt-in as 128 random bits would not be repeated except
intentionally.

Rationale for format: we just need a set of opaque identifiers, encoding
the same way as TLS ALPN does makes sense since that's well specified,
binary safe, easy to parse and places readonable limits on string
lengths.
shinghim pushed a commit to shinghim/rust-payjoin that referenced this pull request Mar 25, 2025
payjoin-test-util's Cargo.toml now refers to the merge commit of
payjoin/ohttp-relay#58, so before the payjoin crate can be released
ohttp-relay needs to be released and this pin removed.
node-smithxby72w added a commit to node-smithxby72w/rust-payjoin that referenced this pull request Sep 28, 2025
This change signals to relays (see payjoin/ohttp-relay#58) that even if
the gateway is not known to the relay, requests are still allowed for
the purposes of BIP 77.

Because receivers choose the directory (which is its own gateway), but
senders choose the relay, before introducing this functionality it this
would have been the responsibility of the sender (or more realistically
their wallet vendor) to choose an appropriate relay, one that accepts
requests on behalf of the receiver's chosen directory.

Rationale for adding a UUID: although arguably 64 bits of randomness is
more than enough, and even just the string "BIP 77" is likely to be
unique and unambiguous, adding a UUID practically ensures an opt-in is
really an opt-in as 128 random bits would not be repeated except
intentionally.

Rationale for format: we just need a set of opaque identifiers, encoding
the same way as TLS ALPN does makes sense since that's well specified,
binary safe, easy to parse and places readonable limits on string
lengths.
node-smithxby72w added a commit to node-smithxby72w/rust-payjoin that referenced this pull request Sep 28, 2025
With this change, the payjoin directory's RFC 9540 compatible OHTTP
gateway additionally opts in to receiving OHTTP requests from arbitrary
relays.

See payjoin/ohttp-relay#58. 

Depends on
- #549
- #572
node-smithxby72w added a commit to node-smithxby72w/rust-payjoin that referenced this pull request Sep 28, 2025
payjoin-test-util's Cargo.toml now refers to the merge commit of
payjoin/ohttp-relay#58, so before the payjoin crate can be released
ohttp-relay needs to be released and this pin removed.
node-smithxby72w added a commit to node-smithxby72w/rust-payjoin that referenced this pull request Sep 28, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants