Skip to content

XHTTP obfuscation bugfixes.#5720

Open
26X23 wants to merge 5 commits intoXTLS:mainfrom
26X23:xhttp_bugfix
Open

XHTTP obfuscation bugfixes.#5720
26X23 wants to merge 5 commits intoXTLS:mainfrom
26X23:xhttp_bugfix

Conversation

@26X23
Copy link

@26X23 26X23 commented Feb 23, 2026

  1. Browser Dialer was broken in XHTTP. I fixed it and added support for the new HTTP methods and cookies.
    Added OPTIONS support and some Access-Control-Allow-* headers for browser dialer on the server side.
    To reduce the number of OPTIONS requests one can access browser dialer from the XHTTP domain (e.g. cdn.example.com). To hide content of the dialer from the CDN one can create simple loader script, e.g. https://cdn.example.com/loader.html, which will load dialer from the specified url and replaces itself by dialer's content.

  2. Nginx by default has very small limits for header buffers (client_header_buffer_size 1k and large_client_header_buffers 4 8k). And "A request header field cannot exceed the size of one buffer". So it is crucial to strictly follow this limits with the help of scMaxEachPostBytes and uplinkChunkSize settings when using packet-up via GET with payload in headers. But pipe.WriteMultiBuffer in Dial can write any amount of data when pipe isn't full, so after merging ReadMultiBuffer can return more data than allowed by scMaxEachPostBytes. So this data must be split into chunks after reading from pipe in Dial.
    Also reduce the default value for the UplinkDataKey from 4 KiB to 4 KB, because 2x(header name + semicolon and space + 4 KiB + CRLF) will not fit into the default 8 KiB buffer.
    Also select uplinkChunkSize randomly from some range to reduce detection.

  3. Allow scMaxEachPostBytes less than 8 KiB and allow to increase MaxHeaderBytes from 8 KiB. Their defaults where contradictive.

  4. Get rid of *-Upstream and *-Length headers and *_upstream cookie which may be used as a signature for the XHTTP detection. Length is not needed, it can be determined from data. Upstream is not needed because it could be determined from the sequence number presence in the packet-up via GET and is true in all other methods like POST, PUT, PATCH, etc.

  5. Added UplinkDataPlacement=auto to support different placements through different CDN's in the single inbound on the server side. (Similar to Mode=auto)

  6. Fix XPadding in cookies on the server side. It was not added at all when xPaddingPlacement=cookie.

  7. Allow the only one of the sessionPlacement or the seqPlacement to be path.

P.S. It is better to view diff without whitespace changes.

Browser Dialer fix and new HTTP methods and cookies support. OPTIONS and Access-Control-Allow-* for it.
Configurable MaxHeaderBytes limit on the server and strict scMaxEachPostBytes chunking.
Get rid of *-Upstream and *-Length headers and *_upstream cookie which may be used as a signature for the XHTTP detection.
UplinkDataPlacement auto to support different placements through different CDN's in the single inbound on the server side.
Fix XPadding in cookies on the server side.
@RPRX
Copy link
Member

RPRX commented Feb 24, 2026

@paqx 看一下

@paqx
Copy link
Contributor

paqx commented Feb 24, 2026

I can also test this tomorrow to see if it fixes the xhttp bugs.

@paqx
Copy link
Contributor

paqx commented Feb 25, 2026

I reviewed the changes. They look good at a first glance, but there are a lot of them, so I need some more time for thorough testing.

@Kapkap5454
Copy link

Would you please research more into this line?
writer.Header().Set("Access-Control-Allow-Methods", "*")

Firefox complains on CORS, but only for OPTIONS method.
And AI says this:
"Access-Control-Allow-Methods: * valid?

The specification technically mentions that * exists as a syntax option on some CORS headers, but in practice browsers do not reliably treat it as a wildcard for allowed methods — and official examples and docs always expect a concrete list of methods."

Possible bug? Better to set explicitly to GET, POST, OPTIONS?

@26X23
Copy link
Author

26X23 commented Feb 26, 2026

@Kapkap5454

Would you please research more into this line? writer.Header().Set("Access-Control-Allow-Methods", "*")

Firefox complains on CORS, but only for OPTIONS method. And AI says this: "Access-Control-Allow-Methods: * valid?

With Firefox 148.0 I can only reproduce this when some of the {padding,data}Placement is cookie and browser dialer is opened on the localhost. But to use cookies you must open dialer on the same origin with the xray server and then there is no OPTIONS. MDN says that * (wildcard) valid for requests without cookies https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Methods

I changed the value of the Access-Control-Allow-Methods to the value of the Access-Control-Request-Method for OPTIONS and to the current method for others. Try it out.

@Kapkap5454
Copy link

Kapkap5454 commented Feb 26, 2026

@Kapkap5454

Would you please research more into this line? writer.Header().Set("Access-Control-Allow-Methods", "*")
Firefox complains on CORS, but only for OPTIONS method. And AI says this: "Access-Control-Allow-Methods: * valid?

With Firefox 148.0 I can only reproduce this when some of the {padding,data}Placement is cookie and browser dialer is opened on the localhost. But to use cookies you must open dialer on the same origin with the xray server and then there is no OPTIONS. MDN says that * (wildcard) valid for requests without cookies https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Methods

I changed the value of the Access-Control-Allow-Methods to the value of the Access-Control-Request-Method for OPTIONS and to the current method for others. Try it out.

I test on XHTTP VLESS TLS + Windows 10 + v2rayN client (TUN mode), Firefox 148.0. I type http://127.0.0.1:8080, not localhost:8080 (don't know if it's important here).

So... I tried adding explicit allow methods (GET, POST, OPTIONS) myself before I saw your new code. It did help with cors complaints. But... I started to get lots of 400 responses.

And it seems the reason is this in firefox console:
Referrer Policy: Ignoring the less restricted referrer policy “unsafe-url” for the cross-site request:

I have the dialer working good in chrome and in edge even with your initial PR. But they set referrer policy to unsafe-url.

In firefox it switches to strict-origin-when-cross-origin. I tried to disable ETP and other security in firefox, but didn't help.
As I understand, it just strips path from all requests in strict mode? So to solve it, the whole logic needs to be changed.

Probably not worth it for now for just firefox?

If you can come up with some idea how to make it work without cross-origin path in referrer, I am ready to test.

Otherwise I suggest you restore your previous version of this PR and just forget about it.

I assume if firefox didn't switch to strict-origin, there would be no problem with allowed methods *

@26X23
Copy link
Author

26X23 commented Feb 26, 2026

@Kapkap5454 I reproduced your problem. It seems that you use the default settings with the XPadding in the Referer. Here Firefox developers says that since version 93 "Firefox will always trim the HTTP referrer for cross-site requests, regardless of the website’s settings". I think we can do nothing with this.
But #5414 in v26.2.6 added settings for XPadding obfuscation (and broke browser dialer). I fixed browser dialer in this PR. So the minimal changes to the config you have to do is "xPaddingObfsMode": true and "xPaddingPlacement": "header" inside "xhttpSettings" object in client and server (I recommend you to change more xPadding related settings for more obfuscation). With this settings Referer is not used for XPadding and there is no problem with Firefox.

If you can come up with some idea how to make it work without cross-origin path in referrer, I am ready to test.

I have two ideas how to not make cross-origin requests at all.

  1. You can access browser dialer from the domain you specify in the XHTTP address setting (e.g. cdn.example.com). To hide content of the dialer from the CDN you can create simple loader script, e.g. https://cdn.example.com/loader.html, which will load dialer from the specified url (e.g. http://127.0.0.1:8080), replaces itself by dialer's content and re-adds dialer's <script> tag to execute it. This way dialer will have origin https://cdn.example.com and will make requests to the xray server on the same origin. Sorry, I can't share my script because if I do so, then it's content will be used for detection. But it's so simple (22 lines) that you can write it yourself.
  2. I did not tested this, but it should work. You can run nginx on loopback for domain localhost.example.com pointing to 127.0.0.1 with two locations:
    1. https://localhost.example.com/dialer/ -> http://127.0.0.1:8080 (xray client with dialer)
    2. https://localhost.example.com/xraypath/ -> https://cdn.example.com/xraypath/ -> xray server
      I recommend to use http/2 over tls for localhost.example.com because with plain http/1.1 you will run into Chrome's 6 simultaneous connections per domain limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants