klzgrad / naiveproxy

Make a fortune quietly
BSD 3-Clause "New" or "Revised" License
6.68k stars 884 forks source link

Remove Naive server and use padding auto-negotiation #76

Closed klzgrad closed 4 years ago

klzgrad commented 4 years ago

72 discovers Caddy does not pass backend server's headers in 200 OK response, rendering header padding ineffectual. To actually fix this I have to create a Naive fork of Caddy's forwardproxy. Since I'm to fork and maintain a Naive forwardproxy, it's possible to simply subsume all of Naive server into this forwardproxy, i.e. the payload padding encapsulation layer.

This means a major architectural change, as the most users should no longer need to run the naive binary on their server but have to build the Naive fork of Caddy forwardproxy, which though is easy to do. Now it is:

[Browser → Naïve (client)] ⟶ Censor ⟶ [Caddy with forwardproxy Naïve fork] ⟶ Internet

Also the use case of HAProxy still has to be supported, as it doesn't do forward proxying itself. So the old functionality will not be removed.

[Browser → Naïve (client)] ⟶ Censor ⟶ [HAProxy → Naïve (server)] ⟶ Internet


Another medium change I'm testing is turning on payload padding by default through auto-negotiation.

With these I'm removing the option --padding, which proves to be error-prone for some users.

klzgrad commented 4 years ago

Compatibility test cases during the transition:

Verify:

Padding negotiation:

The padding byte format:

The purpose of this padding is to protect the payload with TLS handshakes of known sizes. Therefore only the first 4 reads/writes are padded. The choice of the padding length is based on https://github.com/klzgrad/naiveproxy/issues/1#issuecomment-362941556 and also not to make it too large resulting in detectable entropy. But the padding is currently limited to size of [0, 255], away from the ideal distribution of [200, 800]. Changing this is a wire format breaking change though.

Security issues:

darhwa commented 4 years ago

In fact we have a different approach to do padding. I found it after more reading of h2 specs, and would like to propose it here.

H2's HEADERS and DATA frame types have built-in padding feature. Existing h2 implementations can handle received padded frames, however no one use it (or expose the interface) in sending frames. Since you are planning to maintain both client and server, why not take advantage of this native padding feature?

Using h2 native padding have at least such pros:

and cons:

I've tried and found it not difficult in chrome's h2 stack. I implemented it in a new branch https://github.com/darhwa/naiveproxy/tree/native_padding and tested it for more than 1 day without problem. I can see in wireshark that it works as expected. The patch can even be applied to chrome itself. Please have a look at it when you have time.

But things are not so simple for caddy (golang). I found golang maintainers have explicitly rejected a proposal from sergeyfrolov for supporting padding at higher layer (golang/go#26900). However, it's still doable considering that you are going to maintain your own caddy and users don't need to build it themselves. I'm also going to dive into haproxy source as soon as I have time.

What's your opinion?

emacsenli commented 4 years ago

Does v83.0.4103.61-2 support this RFC ? I found padding is not working

https://github.com/klzgrad/naiveproxy/issues/78

darhwa commented 4 years ago

@emacsenli Did you download the released pkg or compiled from master yourself? The released version changed nothing about padding.

emacsenli commented 4 years ago

@emacsenli Did you download the released pkg or compiled from master yourself? The released version changed nothing about padding.

I am with released binary file .I will try compile later.

darhwa commented 4 years ago

@emacsenli

Does v83.0.4103.61-2 support this RFC ?

No. What in this RFC is not implemented.

I will try compile later.

You should double-check your config, and your server status. v83.0.4103.61-2 is not broken. To compile your own binary won't hopefully help.

klzgrad commented 4 years ago

https://www.mail-archive.com/haproxy@formilux.org/msg28781.html I tried this a while ago. The attitude of Go devs reveals a deeper truth: H2 padding is used by no one and not well tested and it's an uphill battle if you try to use it against server bugs (even with pure client side padding). This project is structured to minimize maintenance work so it's more likely to survive. Forking forwardproxy the plugin is already the limit. Forking Go's H2 stack is not sustainable.

As for the RFC, there's quite some as hoc guesswork accumulated in the paddings all around. It's time to collect some new histograms, update the lit review, and do some big self purging before committing to any particular padding methods. This may take a few months.

klzgrad commented 4 years ago

Resolutions of the above security issues:

Even though # 1 listed many characteristics, most would appear in regular H2 connections. Real issues of H2 tunnels basically boil down to two:

Direction n-gram analysis (poor man's LTSM) - there is no practical countermeasure to this analysis more advanced than simple padding. Thus it remains a pure theoretical exercise useful for evaluating obfuscation strength.

Packet timing (in particular CONNECT and ClientHello back-to-back). This is expensive to remove. Hopefully H2 multiplexing combined length padding to both of the two packets can mitigate this to an extent.


New padding scheme:

[x, y]: a non-uniform, entropy-limited random generator that produce random numbers in [x, y]. (Self-similar distribution is not generated fast enough.) TBD Even it's not very clear how this is better than simply uniform without accidentally introducing some deducible artificiality. "Limited entropy" remains poorly defined.

darhwa commented 4 years ago

To replace RST_STREAM frames with END_STREAM flagged DATA frames is not a good idea. If you begin downloading a large file then cancel it, naiveproxy will still receive the entire file from server. It's also not feasible to make server treat END_STREAM as RST_STREAM, because normal requests also mark the last frame as END_STREAM.

Chrome's h2 implementation shoots exactly one TLS record for one h2 frame. That's not mandatory. I think we can modify it to merge multiple h2 frames in one TLS record in some cases. For each RST_STREAM we can precede it with a pure padding DATA. (Update: I've implemented it in my native_padding branch. It proves to work!) CONNECT HEADER can be merged with the next frame.

klzgrad commented 4 years ago

It's not inherently a bad idea as it merely enters half-closed local state, which is the usual process of a request. Though half-closed states are less clean to manage than resets.

It seems you're putting two frames of content into one SpdySerializedFrame. If this can somehow cheat the existing spdy traffic accounting then it'll be cleaner than END_STREAM.

I accept the general direction of this. You can send a PR. Though I don't like the extra steps to get at this. You don't need all that padding infrastructure or buffer bouncing. Just patch CreateRstStream and put in an even manually crafted byte sequence and it will work.

darhwa commented 4 years ago

I've created a PR (#85). Besides the changes mentioned above, I also padded WINDOW_UPDATE like what I did for RST_STREAM.

klzgrad commented 4 years ago

Except for the camouflage preamble, the padding protocol and other UI are updated:

To test:

I created a build here https://github.com/klzgrad/naiveproxy/releases/tag/v83.0.4103.61-3.

Run with bare options:

./naive --listen=socks://:1081 --proxy=https://user:pass@example.com --log

Build and run Caddy v2 with naivety:

git clone -b naive https://github.com/klzgrad/forwardproxy
go get -u github.com/caddyserver/xcaddy/cmd/xcaddy
~/go/bin/xcaddy build --with github.com/caddyserver/forwardproxy=./forwardproxy
sudo setcap cap_net_bind_service=+ep ./caddy
./caddy run --config caddy.json

caddy.json (static certificate):

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [":443"],
          "routes": [{
            "handle": [{
              "handler": "forward_proxy",
              "hide_ip": true,
              "hide_via": true,
              "auth_user": "user",
              "auth_pass": "pass",
              "probe_resistance": {"domain": "secret.localhost"}
            }]
          }, {
            "match": [{"host": ["example.com"]}],
            "handle": [{
              "handler": "file_server",
              "root": "/var/www/html"
            }],
            "terminal": true
          }],
          "tls_connection_policies": [{
            "match": {"sni": ["example.com"]},
            "certificate_selection": {"any_tag": ["cert0"]}
          }]
        }
      }
    },
    "tls": {
      "certificates": {
        "load_files": [{
          "certificate": "example.com.crt",
          "key": "example.com.key",
          "tags": ["cert0"]
        }]
      }
    }
  }
}

caddy.json (Let's Encrypt):

{
  "apps": {
    "http": {
      "servers": {
        "srv0": {
          "listen": [":443"],
          "routes": [{
            "handle": [{
              "handler": "forward_proxy",
              "hide_ip": true,
              "hide_via": true,
              "auth_user": "user",
              "auth_pass": "pass",
              "probe_resistance": {"domain": "secret.localhost"}
            }]
          }, {
            "match": [{"host": ["example.com", "www.example.com"]}],
            "handle": [{
              "handler": "file_server",
              "root": "/var/www/html"
            }],
            "terminal": true
          }],
          "tls_connection_policies": [{
            "match": {"sni": ["example.com", "www.example.com"]}
          }]
        }
      }
    },
    "tls": {
      "automation": {
        "policies": [{
          "subjects": ["example.com", "www.example.com"],
          "issuer": {
            "email": "admin@example.com",
            "module": "acme"
          }
        }]
      }
    }
  }
}
klzgrad commented 4 years ago

Haven't got any issues since update. Considered fixed.