Great Firewalls: Watching Blocking Censoring Disappearing You with Loving Oppression

grarpamp grarpamp at gmail.com
Sun Aug 9 11:09:03 PDT 2020


https://mailarchive.ietf.org/arch/msg/tls/Dae-cukKMqfzmTT4Ksh1Bzlx7ws/

Present in and coming to your own countries soon...

Re: [TLS] Possible blocking of Encrypted SNI extension in China
David Fifield <david at bamsoftware.com> Fri, 07 August 2020 23:56 UTC

On Thu, Jul 30, 2020 at 03:45:48PM +0000, onoketa wrote:
> The Great Firewall of China may have identified and blocked
> Cloudflare's ESNI implementation.
>
> I have found that when using a TLS client hello with ESNI extension to
> connect to servers behind Cloudflare's CDN, the connection will be cut
> off after the whole TLS handshake is done. And then that IP address
> will be blocked at the TCP level for several minutes.

There is now a detailed written report on the new phenomenon of ESNI
blocking in China. It was produced by a collaboration of researchers
from Geneva (https://censorship.ai/), GFW Report (https://gfw.report/),
and iYouPort (https://www.iyouport.org/).

https://geneva.cs.umd.edu/posts/china-censors-esni/esni/    (English)
https://geneva.cs.umd.edu/zh/posts/china-censors-esni/esni/ (Chinese)

Here are some of the points most likely to be of interest to this group:
 * The detector is not merely matching on the lack of plaintext SNI; it
   is specifically looking for the ESNI extension 0xffce.
 * The ESNI detector only matches the ESNI encrypted_server_name
   extension 0xffce (draft-ietf-tls-esni-00 through -06), not the ECH
   extensions encrypted_client_hello 0xff02, ech_nonce 0xff03,
   outer_extension 0xff04 (draft-ietf-tls-esni-07).
 * The encrypted_server_name extension has to be syntactically correct;
   the detector is not just looking for the byte patter ff cc.
 * Once an ESNI-containing ClientHello is detected, the firewall drops
   packets in the client→server direction for 120 or 180 seconds.
 * The detector runs on all TCP ports, not just 443.

This short payload is sufficient to trigger blocking:
	160303003b0100003703035b72616e646f6d72616e646f6d72616e646f6d7261
	6e646f6d72616e646f6d5d0000000100000effce000a53754772000000000000
Python code to generate this payload is appended to this message.

Most of the functions of the Great Firewall work bidirectionally, and
the ESNI detection and blocking are no exception. Sending an
ESNI-containing ClientHello from *outside* of China to a server
*inside* results in temporary blocking, just the same as sending one
from the inside to the outside. This makes it easy to experiment with,
even if you don't control a host in China.

To experience ESNI blocking for yourself, choose a responsive TCP port
in China (doesn't have to be port 443), for example
www.tsinghua.edu.cn:80. Begin a TCP ping to the port, for example using
one of these commands:
	hping3 -S www.tsinghua.edu.cn -p 80
	nping -4 -c 0 --tcp-connect www.tsinghua.edu.cn -p 80
Then send the trigger payload:
	printf '\x16\x03\x03\x00\x3b\x01\x00\x00\x37\x03\x03[randomrandomrandomrandomrandom]\x00\x00\x00\x01\x00\x00\x0e\xff\xce\x00\nSuGr\x00\x00\x00\x00\x00\x00'
| nc -4 -v www.tsinghua.edu.cn 80
The TCP pings will stop receiving replies for 120 or 180 seconds, then
will start back up again.

----

#!/usr/bin/env python3

# Generates a small TLS ClientHello that trigger's the GFW's ESNI detector.
# Writes output to the file minimal.bin.

import struct

from scapy.all import *
load_layer("tls")
from scapy.layers.tls.all import *

# https://tools.ietf.org/html/rfc8446#section-3.4
def var(ceiling, data):
    if ceiling < 256:
        fmt = ">B"
    elif ceiling < 65536:
        fmt = ">H"
    else:
        raise ValueError(ceiling)
    return struct.pack(fmt, len(data)) + data

# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-01#section-5
def encrypted_server_name(suite, group, key_exchange, record_digest,
encrypted_sni):
    return struct.pack(">HH", suite, group) \
        + var(65535, key_exchange) \
        + var(65535, record_digest) \
        + var(65535, encrypted_sni)

clienthello = TLS(
    msg = TLSClientHello(
        gmt_unix_time = 0x5b72616e, # "[ran"
        random_bytes = b"domrandomrandomrandomrandom]",
        ciphers = [],
        ext = [
            # The GFW detector requires a syntactically valid
            # server_name_extension, but the actual values it contains may be
            # nonsense. Here we use a CipherSuite of 0x5375 ("Su"), a NamedGroup
            # of 0x4772 ("Gr"), and zero-length key_exchange, record_digest, and
            # encrypted_sni.
            TLS_Ext_Unknown(type=0xffce,
val=encrypted_server_name(0x5375, 0x4772, b"", b"", b"")),
        ],
    )
)

TLS(bytes(clienthello)).show()
print(bytes(clienthello))

FILENAME = "minimal.bin"
open(FILENAME, "wb").write(bytes(clienthello))
print("output written to {}".format(FILENAME))


More information about the cypherpunks mailing list