feat: add --refrag for dummy TLS record injection

This commit is contained in:
Kreato 2026-06-01 01:36:22 +03:00
commit 40052c65fa
No known key found for this signature in database
3 changed files with 52 additions and 0 deletions

View file

@ -22,6 +22,8 @@ ihtc [flags]
--min-chunk int Minimum bytes per fragment (default 3)
--max-chunk int Maximum bytes per fragment (default 8)
--delay-us int Max microsecond delay between fragments (default 500)
--refrag int Dummy TLS records before ClientHello (default 1, disabled)
--regex string Only fragment hosts matching this regex
--verbose Enable debug logging
--auto-proxy Set macOS auto proxy configuration
```
@ -41,8 +43,31 @@ When a browser opens an HTTPS connection through the proxy:
- TCP only (no QUIC/HTTP3)
- Not effective against DPIs that perform TCP stream reassembly
- For reassembling DPIs, see [re-fragmentation](#re-fragmentation)
- No authentication (local-only, trusted environment)
## Advanced
### Fragmenting only blocked domains
Use `--regex` to limit fragmentation to specific hosts. All other traffic passes through unfragmented:
```sh
./ihtc --auto-proxy --regex 'discord\.com|discord\.gg|twitter\.com'
```
### Re-fragmentation
For DPIs that reassemble TCP streams, enable re-fragmentation with `--refrag N`:
```sh
./ihtc --refrag 3
```
This inserts N-1 deliberately invalid TLS records before the real ClientHello, each in its own TCP segment. A reassembling DPI must inspect multiple nearly-identical records and choose the correct one — while the TLS server ignores the invalid records and processes only the final valid one.
Set N based on DPI aggressiveness (2-5 is typical). Higher values add latency with diminishing returns.
## License
MIT — see [LICENSE](LICENSE).

View file

@ -23,6 +23,7 @@ func main() {
verbose := flag.Bool("verbose", false, "Enable debug logging")
autoProxy := flag.Bool("auto-proxy", false, "Set macOS auto proxy configuration")
hostRegex := flag.String("regex", "", "Only proxy hosts matching this regex")
refrag := flag.Int("refrag", 1, "Number of dummy TLS records before ClientHello (1 = disabled)")
flag.Parse()
logger := log.New(*verbose)
@ -35,11 +36,16 @@ func main() {
logger.Error("max-chunk must be >= min-chunk")
os.Exit(1)
}
if *refrag < 1 {
logger.Error("refrag must be >= 1")
os.Exit(1)
}
cfg := obfuscate.Config{
MinChunk: *minChunk,
MaxChunk: *maxChunk,
DelayUs: *delayUs,
Refrag: *refrag,
}
p := proxy.New(*listen, cfg, logger, *autoProxy)

View file

@ -13,6 +13,7 @@ type Config struct {
MinChunk int
MaxChunk int
DelayUs int
Refrag int
}
func Split(data []byte, cfg Config) [][]byte {
@ -81,6 +82,15 @@ func HandleClientHello(dst net.Conn, src io.Reader, cfg Config) (int, error) {
}
fullHello := append(header, body...)
dummyRecords := buildDummyRecords(cfg.Refrag)
for _, d := range dummyRecords {
chunks := Split(d, cfg)
if err := writeFragmented(dst, chunks, cfg); err != nil {
return 0, err
}
}
chunks := Split(fullHello, cfg)
if err := writeFragmented(dst, chunks, cfg); err != nil {
return len(fullHello), err
@ -88,3 +98,14 @@ func HandleClientHello(dst net.Conn, src io.Reader, cfg Config) (int, error) {
return len(fullHello), nil
}
func buildDummyRecords(count int) [][]byte {
if count <= 1 {
return nil
}
records := make([][]byte, count-1)
for i := range records {
records[i] = []byte{0x17, 0x03, 0x03, 0x00, 0x00}
}
return records
}