mic.git


mic

~ proxy for JA4-TLS fingerprinting evasion
git clone https://github.com/djnnvx/mic.git
log | files | refs | archive | README | LICENSE

commit 3b287107daa0023ab1198601bace600bfd0a25cd
parent d56c55362c3aef81b05596b5b5afd9240b3bfa9d
Author: djnn <[email protected]>
Date: Thu, 19 Feb 2026 19:39:56 +0900

feat(proxy): server / client mode fingerprinting evasion

Diffstat:
MREADME.md | 149++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Mcmd/root.go | 60++++++++++++++++++++++++++++++++++++++++++++----------------
Mcmd/types.go | 22+++-------------------
Aconfig/config.go | 57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig/config_test.go | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afingerprint/fingerprint.go | 24++++++++++++++++++++++++
Afingerprint/tcp.go | 11+++++++++++
Afingerprint/tls.go | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afingerprint/tls_test.go | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mgo.mod | 1+
Mgo.sum | 2++
Amic.example.toml | 26++++++++++++++++++++++++++
Mproxy/https.go | 47+++++------------------------------------------
Aproxy/integration_test.go | 206+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mproxy/proxy.go | 76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Aproxy/proxy_test.go | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aproxy/serverfront.go | 43+++++++++++++++++++++++++++++++++++++++++++
Ascripts/cleanup.sh | 45+++++++++++++++++++++++++++++++++++++++++++++
Ascripts/run_integration_tests.sh | 27+++++++++++++++++++++++++++
Ascripts/setup.sh | 35+++++++++++++++++++++++++++++++++++

20 files changed, 1078 insertions(+), 100 deletions(-)

diff --git a/README.md b/README.md
@@ -1,34 +1,159 @@
 # mic
 
-mic (mina-is-cute) is a modular Golang proxy (HTTPS) to evade network JA4S fingerprinting.
+mic (mina-is-cute) is a modular Go proxy for controlling outbound TLS fingerprints.
+It lets you specify a JA4-TLS hash and the proxy will use the corresponding `uTLS`
+preset when connecting to upstream servers, making your traffic look like a specific
+browser to any fingerprinting system.
 
-It is planned to support HTTP / SOCKS5 in the future as well, but not within the first version.
+Two modes are supported:
 
-## Compiling
+- **client-front** — standard HTTP CONNECT proxy. Your tool (curl, browser, etc.)
+  connects through the proxy; the proxy dials upstream with the configured fingerprint.
+- **server-front** — the proxy terminates incoming TLS (with your cert/key), then
+  re-dials the backend with the configured fingerprint. Useful when the client
+  cannot be configured to use a CONNECT proxy.
 
-You can simply run:
+---
+
+## How it works
+
+```mermaid
+sequenceDiagram
+    participant C as Client
+    participant P as mic proxy
+    participant T as Target
+
+    rect rgb(30, 30, 60)
+        note over C,T: client-front mode
+        C->>P: HTTP CONNECT target:443
+        P-->>C: 200 Connection Established
+        P->>T: TCP + uTLS handshake (configured fingerprint)
+        C->>P: raw HTTP bytes
+        P->>T: bytes encrypted in uTLS tunnel
+        T-->>P: HTTP response (through uTLS)
+        P-->>C: raw HTTP response
+    end
+
+    rect rgb(30, 60, 30)
+        note over C,T: server-front mode
+        C->>P: TLS handshake (proxy cert)
+        P-->>C: TLS established
+        P->>T: TCP + uTLS handshake (configured fingerprint)
+        C->>P: raw HTTP bytes (decrypted by proxy)
+        P->>T: bytes encrypted in uTLS tunnel
+        T-->>P: HTTP response (through uTLS)
+        P-->>C: HTTP response (re-encrypted for client)
+    end
+```
+
+In both modes the target sees a TLS handshake that matches the configured JA4 hash,
+not the default Go TLS fingerprint.
+
+---
+
+## Build
+
+```bash
+go build -o mic .
+```
+
+Or with the helper script (builds and creates `mic.toml` from the example):
 
 ```bash
-make
+scripts/setup.sh
+```
+
+---
+
+## Configuration
+
+Copy the example config and edit it:
+
+```bash
+cp mic.example.toml mic.toml
+```
+
+```toml
+mode = "client-front"   # or "server-front"
+
+[listen]
+addr = ":8080"
+
+[backend]
+addr = "127.0.0.1:443"   # server-front only
+
+[fingerprint]
+[fingerprint.tls]
+ja4 = "t13d1516h2_8daaf6152771_b0da82dd1658"   # Chrome 120
+
+# server-front only
+[fingerprint.tls.termination]
+cert = "/path/to/cert.pem"
+key  = "/path/to/key.pem"
+
+[ca]
+cert = ""   # optional: custom CA for upstream verification
 ```
 
-Or, you can also run this, to run with Docker:
+### Available JA4 fingerprints
+
+| JA4 hash | Browser |
+|---|---|
+| `t13d1516h2_8daaf6152771_b0da82dd1658` | Chrome 120 |
+| `t13d1516h2_8daaf6152771_e5627efa2ab1` | Chrome 120 (post-quantum) |
+| `t13d1517h2_8daaf6152771_b1ff8ab2d16f` | Firefox 120 |
+| `t13d1516h2_8daaf6152771_4aeede8da0ac` | Safari 16.0 |
+| `t13d1516h2_8daaf6152771_f5b4b24de8b1` | Edge 106 |
+
+---
+
+## Running
+
 ```bash
-make docker
+./mic --config mic.toml
 ```
 
-## Usage
+### client-front
 
-To know how to use it, simply run with `--help`
+Configure your client to use an HTTP CONNECT proxy at the listen address:
 
 ```bash
-./mic --help
+curl -x http://localhost:8080 https://tls.peet.ws/api/all
+# check the "ja4" field in the response
 ```
 
-### Using self-signed certificates
+### server-front
 
-You can generate certificates like so:
+Generate a self-signed certificate for the proxy to present to clients:
 
 ```bash
 go run $(go env GOROOT)/src/crypto/tls/generate_cert.go --host="localhost,127.0.0.1"
+# produces cert.pem and key.pem
+```
+
+Set `mode = "server-front"`, point `fingerprint.tls.termination.cert/key` at those
+files, and set `backend.addr` to your upstream. Then connect directly to the proxy
+address with a TLS client.
+
+### Stop
+
+```bash
+scripts/cleanup.sh
+```
+
+---
+
+## Testing
+
+Unit tests:
+
+```bash
+go test ./...
+```
+
+Integration tests (spin up in-process TLS servers, no external dependencies):
+
+```bash
+scripts/run_integration_tests.sh
+# or: go test -tags integration -v ./proxy/...
 ```
diff --git a/cmd/root.go b/cmd/root.go
@@ -6,6 +6,8 @@ import (
 	"log"
 	"os"
 
+	"github.com/djnnvx/mic/config"
+	"github.com/djnnvx/mic/fingerprint"
 	"github.com/djnnvx/mic/proxy"
 	"github.com/spf13/cobra"
 )
@@ -50,35 +52,61 @@ func getParser(opts *MicCliOptions) *cobra.Command {
 		Short: "modular proxy for network-fingerprinting evasion",
 		Long:  "mic (mina-is-cute) is a HTTPS proxy to evade networking fingerprinting detection",
 		Run: func(cmd *cobra.Command, args []string) {
+			cfg, err := config.Load(opts.ConfigPath)
+			if err != nil {
+				log.Fatalf("[!] Failed to load config: %v", err)
+			}
 
-			log.Printf("[+] only HTTPS supported for now, registering default handler.")
 			p := &proxy.Proxy{
-				ListenAddr: opts.Addr,
-				ForwardTo:  opts.ForwardTo,
+				ListenAddr:  cfg.Listen.Addr,
+				BackendAddr: cfg.Backend.Addr,
 			}
 
-			if opts.CAPath != "" {
-				caCert, err := os.ReadFile(opts.CAPath)
+			// Load optional custom CA pool.
+			if cfg.CA.Cert != "" {
+				caCert, err := os.ReadFile(cfg.CA.Cert)
 				if err != nil {
-					log.Fatalf("Failed to read server certificate: %v", err)
+					log.Fatalf("[!] Failed to read CA certificate: %v", err)
 				}
+				pool := x509.NewCertPool()
+				pool.AppendCertsFromPEM(caCert)
+				p.CAPool = pool
+			}
 
-				caCertPool := x509.NewCertPool()
-				caCertPool.AppendCertsFromPEM(caCert)
-				p.AddCertificate(caCertPool)
+			// Load TLS fingerprint if a JA4 hash is configured.
+			if cfg.Fingerprint.TLS.JA4 != "" {
+				fp, err := fingerprint.NewTLS(cfg.Fingerprint.TLS.JA4)
+				if err != nil {
+					log.Fatalf("[!] Failed to load fingerprint: %v", err)
+				}
+				p.Fingerprint = fp
+				log.Printf("[+] Using TLS fingerprint: %s", fp.Name())
 			}
 
-			p.RegisterHandler(proxy.HttpsHandler)
-			p.Run()
+			switch cfg.Mode {
+			case "client-front":
+				log.Printf("[+] Mode: client-front (HTTP CONNECT proxy)")
+				p.RegisterHandler(proxy.HttpsHandler)
+			case "server-front":
+				log.Printf("[+] Mode: server-front (TLS termination → backend %s)", cfg.Backend.Addr)
+				certFile := cfg.Fingerprint.TLS.Termination.Cert
+				keyFile := cfg.Fingerprint.TLS.Termination.Key
+				if certFile == "" || keyFile == "" {
+					log.Fatalf("[!] server-front mode requires fingerprint.tls.termination.cert and .key")
+				}
+				p.RegisterHandler(proxy.ServerFrontHandler(certFile, keyFile))
+			default:
+				log.Fatalf("[!] Unknown mode %q; expected \"client-front\" or \"server-front\"", cfg.Mode)
+			}
+
+			if err := p.Run(); err != nil {
+				log.Fatalf("[!] Proxy error: %v", err)
+			}
 		},
 	}
 
 	defaults := GetDefaultOptions()
-
-	rootCmd.Flags().StringVarP(&opts.Addr, "addr", "a", defaults.Addr, "address to listen to (Golang notation)")
-	rootCmd.Flags().IntVarP(&opts.ForwardTo, "forward-to", "t", defaults.ForwardTo, "local port to forward to")
-	rootCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", defaults.Verbose, "enable verbose mode")
-	rootCmd.Flags().StringVarP(&opts.CAPath, "ca-cert-path", "c", defaults.CAPath, "custom CA certificate path (optional)")
+	rootCmd.Flags().StringVarP(&opts.ConfigPath, "config", "c", defaults.ConfigPath, "path to TOML config file")
 
 	return rootCmd
 }
diff --git a/cmd/types.go b/cmd/types.go
@@ -1,27 +1,11 @@
 package cmd
 
 type MicCliOptions struct {
-
-	/// Addr: address to listen to (Golang notation)
-	Addr string
-
-	/// ForwardTo: local port to forward call to
-	ForwardTo int
-
-	/// Verbose: enable verbose mode
-	Verbose bool
-
-	/// CAPath: Self-signed certificate path (optional)
-	CAPath string
+	ConfigPath string
 }
 
 func GetDefaultOptions() MicCliOptions {
-	opts := MicCliOptions{
-		Addr:      ":8080",
-		ForwardTo: 1337,
-		Verbose:   false,
-		CAPath:    "",
+	return MicCliOptions{
+		ConfigPath: "mic.toml",
 	}
-
-	return opts
 }
diff --git a/config/config.go b/config/config.go
@@ -0,0 +1,57 @@
+package config
+
+import (
+	"fmt"
+	"os"
+
+	"github.com/pelletier/go-toml/v2"
+)
+
+type Config struct {
+	Mode        string  `toml:"mode"`
+	Listen      Listen  `toml:"listen"`
+	Backend     Backend `toml:"backend"`
+	Fingerprint FpConf  `toml:"fingerprint"`
+	CA          CAConf  `toml:"ca"`
+}
+
+type Listen struct {
+	Addr string `toml:"addr"`
+}
+
+type Backend struct {
+	Addr string `toml:"addr"`
+}
+
+type CAConf struct {
+	Cert string `toml:"cert"`
+}
+
+type FpConf struct {
+	TLS TLSConf `toml:"tls"`
+}
+
+type TLSConf struct {
+	JA4         string         `toml:"ja4"`
+	Termination TLSTermination `toml:"termination"`
+}
+
+type TLSTermination struct {
+	Cert string `toml:"cert"`
+	Key  string `toml:"key"`
+}
+
+// Load reads and parses a TOML config file from path.
+func Load(path string) (*Config, error) {
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("reading config file %q: %w", path, err)
+	}
+
+	var cfg Config
+	if err := toml.Unmarshal(data, &cfg); err != nil {
+		return nil, fmt.Errorf("parsing config file %q: %w", path, err)
+	}
+
+	return &cfg, nil
+}
diff --git a/config/config_test.go b/config/config_test.go
@@ -0,0 +1,94 @@
+package config_test
+
+import (
+	"os"
+	"testing"
+
+	"github.com/djnnvx/mic/config"
+)
+
+const validTOML = `
+mode = "client-front"
+
+[listen]
+addr = ":8080"
+
+[backend]
+addr = "127.0.0.1:443"
+
+[fingerprint]
+[fingerprint.tls]
+ja4 = "t13d1516h2_8daaf6152771_b0da82dd1658"
+
+[fingerprint.tls.termination]
+cert = "/path/to/cert.pem"
+key  = "/path/to/key.pem"
+
+[ca]
+cert = "/path/to/ca.pem"
+`
+
+func writeTempFile(t *testing.T, content string) string {
+	t.Helper()
+	f, err := os.CreateTemp("", "mic-config-*.toml")
+	if err != nil {
+		t.Fatalf("creating temp file: %v", err)
+	}
+	defer f.Close()
+	if _, err := f.WriteString(content); err != nil {
+		t.Fatalf("writing temp file: %v", err)
+	}
+	return f.Name()
+}
+
+func TestLoad_Valid(t *testing.T) {
+	path := writeTempFile(t, validTOML)
+	defer os.Remove(path)
+
+	cfg, err := config.Load(path)
+	if err != nil {
+		t.Fatalf("Load returned error: %v", err)
+	}
+
+	if cfg.Mode != "client-front" {
+		t.Errorf("Mode = %q; want %q", cfg.Mode, "client-front")
+	}
+	if cfg.Listen.Addr != ":8080" {
+		t.Errorf("Listen.Addr = %q; want %q", cfg.Listen.Addr, ":8080")
+	}
+	if cfg.Backend.Addr != "127.0.0.1:443" {
+		t.Errorf("Backend.Addr = %q; want %q", cfg.Backend.Addr, "127.0.0.1:443")
+	}
+	if cfg.Fingerprint.TLS.JA4 != "t13d1516h2_8daaf6152771_b0da82dd1658" {
+		t.Errorf("Fingerprint.TLS.JA4 = %q; want %q",
+			cfg.Fingerprint.TLS.JA4, "t13d1516h2_8daaf6152771_b0da82dd1658")
+	}
+	if cfg.Fingerprint.TLS.Termination.Cert != "/path/to/cert.pem" {
+		t.Errorf("Termination.Cert = %q; want %q",
+			cfg.Fingerprint.TLS.Termination.Cert, "/path/to/cert.pem")
+	}
+	if cfg.Fingerprint.TLS.Termination.Key != "/path/to/key.pem" {
+		t.Errorf("Termination.Key = %q; want %q",
+			cfg.Fingerprint.TLS.Termination.Key, "/path/to/key.pem")
+	}
+	if cfg.CA.Cert != "/path/to/ca.pem" {
+		t.Errorf("CA.Cert = %q; want %q", cfg.CA.Cert, "/path/to/ca.pem")
+	}
+}
+
+func TestLoad_MissingFile(t *testing.T) {
+	_, err := config.Load("/nonexistent/path/to/config.toml")
+	if err == nil {
+		t.Fatal("expected error for missing file, got nil")
+	}
+}
+
+func TestLoad_InvalidTOML(t *testing.T) {
+	path := writeTempFile(t, "this is not valid = toml [[[")
+	defer os.Remove(path)
+
+	_, err := config.Load(path)
+	if err == nil {
+		t.Fatal("expected error for invalid TOML, got nil")
+	}
+}
diff --git a/fingerprint/fingerprint.go b/fingerprint/fingerprint.go
@@ -0,0 +1,24 @@
+// Package fingerprint provides interfaces and implementations for controlling
+// outbound connection fingerprints (TLS ClientHello, TCP socket options, etc.).
+package fingerprint
+
+import utls "github.com/refraction-networking/utls"
+
+// Applier is the base interface for all fingerprint types.
+type Applier interface {
+	Name() string
+}
+
+// TLSApplier controls the uTLS ClientHello fingerprint for outbound connections.
+type TLSApplier interface {
+	Applier
+	ClientHelloID() utls.ClientHelloID
+}
+
+// TCPApplier will control TCP socket options (window size, TTL, MSS, etc.).
+// Left for a future implementation.
+//
+// type TCPApplier interface {
+//     Applier
+//     Apply(fd uintptr) error
+// }
diff --git a/fingerprint/tcp.go b/fingerprint/tcp.go
@@ -0,0 +1,11 @@
+package fingerprint
+
+// TCPApplier is reserved for future TCP-level fingerprinting support
+// (TCP window size, TTL, MSS, timestamps, etc.).
+//
+// Future interface outline:
+//
+//	type TCPApplier interface {
+//	    Applier
+//	    Apply(fd uintptr) error
+//	}
diff --git a/fingerprint/tls.go b/fingerprint/tls.go
@@ -0,0 +1,55 @@
+package fingerprint
+
+import (
+	"fmt"
+
+	utls "github.com/refraction-networking/utls"
+)
+
+// Table maps known JA4-TLS fingerprint hashes to utls ClientHelloID presets.
+// JA4 hashes are computed from real traffic captures; verify against current
+// browser versions when adding new entries.
+var Table = map[string]utls.ClientHelloID{
+	// Chrome 120
+	"t13d1516h2_8daaf6152771_b0da82dd1658": utls.HelloChrome_120,
+	// Chrome 120 with post-quantum key exchange
+	"t13d1516h2_8daaf6152771_e5627efa2ab1": utls.HelloChrome_120_PQ,
+	// Firefox 120
+	"t13d1517h2_8daaf6152771_b1ff8ab2d16f": utls.HelloFirefox_120,
+	// Safari 16.0
+	"t13d1516h2_8daaf6152771_4aeede8da0ac": utls.HelloSafari_16_0,
+	// Edge 106
+	"t13d1516h2_8daaf6152771_f5b4b24de8b1": utls.HelloEdge_106,
+}
+
+// Lookup returns the utls.ClientHelloID for the given JA4 hash.
+func Lookup(ja4 string) (utls.ClientHelloID, bool) {
+	id, ok := Table[ja4]
+	return id, ok
+}
+
+// TLSFingerprint implements TLSApplier for a specific JA4 hash.
+type TLSFingerprint struct {
+	ja4 string
+	id  utls.ClientHelloID
+}
+
+// NewTLS creates a TLSFingerprint for the given JA4 hash.
+// Returns an error if the hash is not in the built-in table.
+func NewTLS(ja4 string) (*TLSFingerprint, error) {
+	id, ok := Lookup(ja4)
+	if !ok {
+		return nil, fmt.Errorf("unknown JA4-TLS fingerprint %q", ja4)
+	}
+	return &TLSFingerprint{ja4: ja4, id: id}, nil
+}
+
+// Name returns a human-readable identifier including the JA4 hash.
+func (f *TLSFingerprint) Name() string {
+	return "tls:" + f.ja4
+}
+
+// ClientHelloID returns the utls preset for this fingerprint.
+func (f *TLSFingerprint) ClientHelloID() utls.ClientHelloID {
+	return f.id
+}
diff --git a/fingerprint/tls_test.go b/fingerprint/tls_test.go
@@ -0,0 +1,62 @@
+package fingerprint_test
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/djnnvx/mic/fingerprint"
+	utls "github.com/refraction-networking/utls"
+)
+
+const knownJA4 = "t13d1516h2_8daaf6152771_b0da82dd1658"
+
+func TestLookup_Known(t *testing.T) {
+	id, ok := fingerprint.Lookup(knownJA4)
+	if !ok {
+		t.Fatalf("Lookup(%q) returned false; expected true", knownJA4)
+	}
+	if id != utls.HelloChrome_120 {
+		t.Errorf("Lookup(%q) = %v; want HelloChrome_120", knownJA4, id)
+	}
+}
+
+func TestLookup_Unknown(t *testing.T) {
+	_, ok := fingerprint.Lookup("unknown_hash_that_does_not_exist")
+	if ok {
+		t.Fatal("Lookup with unknown hash returned true; expected false")
+	}
+}
+
+func TestNewTLS_Happy(t *testing.T) {
+	fp, err := fingerprint.NewTLS(knownJA4)
+	if err != nil {
+		t.Fatalf("NewTLS(%q) returned error: %v", knownJA4, err)
+	}
+	if fp == nil {
+		t.Fatal("NewTLS returned nil fingerprint")
+	}
+}
+
+func TestNewTLS_Error(t *testing.T) {
+	_, err := fingerprint.NewTLS("not_a_real_ja4_hash")
+	if err == nil {
+		t.Fatal("NewTLS with unknown hash returned nil error; expected error")
+	}
+}
+
+func TestName_ContainsJA4(t *testing.T) {
+	fp, err := fingerprint.NewTLS(knownJA4)
+	if err != nil {
+		t.Fatalf("NewTLS: %v", err)
+	}
+	name := fp.Name()
+	if !strings.Contains(name, knownJA4) {
+		t.Errorf("Name() = %q; does not contain JA4 hash %q", name, knownJA4)
+	}
+}
+
+func TestTable_MinEntries(t *testing.T) {
+	if len(fingerprint.Table) < 5 {
+		t.Errorf("Table has %d entries; want at least 5", len(fingerprint.Table))
+	}
+}
diff --git a/go.mod b/go.mod
@@ -3,6 +3,7 @@ module github.com/djnnvx/mic
 go 1.24.4
 
 require (
+	github.com/pelletier/go-toml/v2 v2.2.4
 	github.com/refraction-networking/utls v1.8.0
 	github.com/spf13/cobra v1.9.1
 )
diff --git a/go.sum b/go.sum
@@ -5,6 +5,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
 github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
 github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
diff --git a/mic.example.toml b/mic.example.toml
@@ -0,0 +1,26 @@
+mode = "client-front"   # "client-front" | "server-front"
+
+[listen]
+addr = ":8080"
+
+[backend]
+# Used in server-front mode; client-front uses the CONNECT target directly.
+addr = "127.0.0.1:443"
+
+[fingerprint]
+[fingerprint.tls]
+ja4 = "t13d1516h2_8daaf6152771_b0da82dd1658"   # Chrome 120
+
+# server-front only: certificate for terminating incoming TLS
+[fingerprint.tls.termination]
+cert = "/path/to/cert.pem"
+key  = "/path/to/key.pem"
+
+[ca]
+cert = ""   # optional custom CA for upstream verification
+
+# Future:
+# [fingerprint.tcp]
+# window_size = 65535
+# ttl = 64
+# mss = 1460
diff --git a/proxy/https.go b/proxy/https.go
@@ -2,15 +2,10 @@ package proxy
 
 import (
 	"bufio"
-	"fmt"
-	"io"
 	"log"
 	"net"
 	"net/http"
 	"strings"
-	"sync"
-
-	utls "github.com/refraction-networking/utls"
 )
 
 func HttpsHandler(clientConn net.Conn, p *Proxy) {
@@ -31,52 +26,20 @@ func HttpsHandler(clientConn net.Conn, p *Proxy) {
 	}
 
 	// The Host field contains the destination host and port.
-	// If no port is specified by the client, use the `ForwardTo` port from our Proxy struct.
+	// Default to :443 if no port is specified.
 	host := req.URL.Host
 	if !strings.Contains(host, ":") {
-		host = fmt.Sprintf("%s:%d", host, p.ForwardTo)
+		host = host + ":443"
 	}
 
 	log.Printf("Connecting to target: %s", host)
-	targetConn, err := net.Dial("tcp", host)
+	targetConn, err := p.dialTarget(host)
 	if err != nil {
-		log.Printf("Failed to dial target %s: %v", host, err)
+		log.Printf("Failed to connect to target %s: %v", host, err)
 		return
 	}
 	defer targetConn.Close()
 
-	// Create a uTLS client with the randomly selected fingerprint & perform handshake
-	utlsConfig := &utls.Config{
-		ServerName: strings.Split(host, ":")[0],
-	}
-	uclient := utls.UClient(targetConn, utlsConfig, utls.HelloRandomized)
-
-	if err := uclient.Handshake(); err != nil {
-		log.Printf("uTLS handshake failed with %s: %v", host, err)
-		return
-	}
-	defer uclient.Close()
-
 	clientConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
-	var wg sync.WaitGroup
-	wg.Add(2)
-
-	// Pipe data from the client to the target server.
-	go func() {
-		defer wg.Done()
-		_, err := io.Copy(uclient, reader)
-		if err != nil && err != io.EOF {
-			log.Printf("Error copying from client to target: %v", err)
-		}
-	}()
-
-	go func() {
-		defer wg.Done()
-		_, err := io.Copy(clientConn, uclient)
-		if err != nil && err != io.EOF {
-			log.Printf("Error copying from target to client: %v", err)
-		}
-	}()
-
-	wg.Wait()
+	pipe(clientConn, reader, targetConn)
 }
diff --git a/proxy/integration_test.go b/proxy/integration_test.go
@@ -0,0 +1,206 @@
+//go:build integration
+
+package proxy
+
+import (
+	"bufio"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/djnnvx/mic/fingerprint"
+)
+
+const (
+	chrome120JA4 = "t13d1516h2_8daaf6152771_b0da82dd1658"
+	testTimeout  = 10 * time.Second
+)
+
+// serveHandler starts a TCP listener and dispatches accepted connections to h.
+// The listener is closed when the test ends.
+func serveHandler(t *testing.T, p *Proxy, h Handler) string {
+	t.Helper()
+	ln, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("listen: %v", err)
+	}
+	t.Cleanup(func() { ln.Close() })
+
+	go func() {
+		for {
+			conn, err := ln.Accept()
+			if err != nil {
+				return
+			}
+			go h(conn, p)
+		}
+	}()
+
+	return ln.Addr().String()
+}
+
+// writeTempCertKey generates a self-signed cert/key, writes PEM files to disk,
+// and returns the file paths plus a CA pool that trusts the cert.
+// Files are removed when the test ends.
+func writeTempCertKey(t *testing.T) (certFile, keyFile string, pool *x509.CertPool) {
+	t.Helper()
+
+	pool, tlsCert := generateSelfSignedCert(t)
+
+	// Certificate PEM.
+	cf, err := os.CreateTemp("", "mic-cert-*.pem")
+	if err != nil {
+		t.Fatalf("creating cert temp file: %v", err)
+	}
+	pem.Encode(cf, &pem.Block{Type: "CERTIFICATE", Bytes: tlsCert.Certificate[0]})
+	cf.Close()
+	certFile = cf.Name()
+
+	// Private key PEM (PKCS8 — accepted by tls.LoadX509KeyPair as "PRIVATE KEY").
+	keyBytes, err := x509.MarshalPKCS8PrivateKey(tlsCert.PrivateKey)
+	if err != nil {
+		t.Fatalf("marshaling private key: %v", err)
+	}
+	kf, err := os.CreateTemp("", "mic-key-*.pem")
+	if err != nil {
+		t.Fatalf("creating key temp file: %v", err)
+	}
+	pem.Encode(kf, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
+	kf.Close()
+	keyFile = kf.Name()
+
+	t.Cleanup(func() {
+		os.Remove(certFile)
+		os.Remove(keyFile)
+	})
+
+	return certFile, keyFile, pool
+}
+
+// TestClientFront_Integration tests the full client-front (HTTP CONNECT) flow:
+//
+//	client → TCP CONNECT → proxy → uTLS(Chrome120) → httptest.TLSServer
+func TestClientFront_Integration(t *testing.T) {
+	// Start a target HTTPS server.
+	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Connection", "close")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintln(w, "client-front integration OK")
+	}))
+	defer ts.Close()
+
+	// Trust the test server's certificate.
+	caPool := ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs
+
+	fp, err := fingerprint.NewTLS(chrome120JA4)
+	if err != nil {
+		t.Fatalf("fingerprint.NewTLS: %v", err)
+	}
+
+	p := &Proxy{CAPool: caPool, Fingerprint: fp}
+	proxyAddr := serveHandler(t, p, HttpsHandler)
+
+	// Connect to the proxy over plain TCP.
+	conn, err := net.DialTimeout("tcp", proxyAddr, testTimeout)
+	if err != nil {
+		t.Fatalf("dial proxy: %v", err)
+	}
+	defer conn.Close()
+	conn.SetDeadline(time.Now().Add(testTimeout))
+
+	// Send HTTP CONNECT to open a tunnel to the target.
+	targetAddr := ts.Listener.Addr().String()
+	fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetAddr, targetAddr)
+
+	reader := bufio.NewReader(conn)
+	resp, err := http.ReadResponse(reader, nil)
+	if err != nil {
+		t.Fatalf("reading CONNECT response: %v", err)
+	}
+	if resp.StatusCode != http.StatusOK {
+		t.Fatalf("CONNECT status: %d; want 200", resp.StatusCode)
+	}
+
+	// After CONNECT the proxy has already done uTLS to the target.
+	// We send raw HTTP — the proxy wraps it in TLS on our behalf.
+	fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", targetAddr)
+
+	resp, err = http.ReadResponse(reader, nil)
+	if err != nil {
+		t.Fatalf("reading GET response: %v", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("GET status: %d; want 200", resp.StatusCode)
+	}
+	body, _ := io.ReadAll(resp.Body)
+	t.Logf("response body: %s", body)
+}
+
+// TestServerFront_Integration tests the server-front flow:
+//
+//	client → TLS → proxy (terminates TLS) → uTLS(Chrome120) → httptest.TLSServer
+func TestServerFront_Integration(t *testing.T) {
+	// Start the backend HTTPS server (what the proxy connects to upstream).
+	backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Connection", "close")
+		w.WriteHeader(http.StatusOK)
+		fmt.Fprintln(w, "server-front integration OK")
+	}))
+	defer backend.Close()
+
+	backendCAPool := backend.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs
+
+	// Generate a cert/key pair the proxy will use to terminate incoming client TLS.
+	certFile, keyFile, proxyCertPool := writeTempCertKey(t)
+
+	fp, err := fingerprint.NewTLS(chrome120JA4)
+	if err != nil {
+		t.Fatalf("fingerprint.NewTLS: %v", err)
+	}
+
+	p := &Proxy{
+		BackendAddr: backend.Listener.Addr().String(),
+		CAPool:      backendCAPool,
+		Fingerprint: fp,
+	}
+	proxyAddr := serveHandler(t, p, ServerFrontHandler(certFile, keyFile))
+
+	// Connect to the proxy with TLS, trusting the proxy's generated cert.
+	tlsConn, err := tls.DialWithDialer(
+		&net.Dialer{Timeout: testTimeout},
+		"tcp", proxyAddr,
+		&tls.Config{RootCAs: proxyCertPool, ServerName: "localhost"},
+	)
+	if err != nil {
+		t.Fatalf("tls.Dial to proxy: %v", err)
+	}
+	defer tlsConn.Close()
+	tlsConn.SetDeadline(time.Now().Add(testTimeout))
+
+	// Send plain HTTP — the proxy decrypts from us and re-encrypts with the
+	// configured fingerprint before forwarding to the backend.
+	fmt.Fprintf(tlsConn, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")
+
+	reader := bufio.NewReader(tlsConn)
+	resp, err := http.ReadResponse(reader, nil)
+	if err != nil {
+		t.Fatalf("reading response: %v", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		t.Errorf("status: %d; want 200", resp.StatusCode)
+	}
+	body, _ := io.ReadAll(resp.Body)
+	t.Logf("response body: %s", body)
+}
diff --git a/proxy/proxy.go b/proxy/proxy.go
@@ -2,31 +2,87 @@ package proxy
 
 import (
 	"crypto/x509"
+	"io"
 	"log"
 	"net"
+	"strings"
+	"sync"
+
+	"github.com/djnnvx/mic/fingerprint"
+	utls "github.com/refraction-networking/utls"
 )
 
 type Proxy struct {
-	ListenAddr string
-	ForwardTo  int
-	CAPool     *x509.CertPool
+	ListenAddr  string
+	BackendAddr string // server-front: backend host:port
+	CAPool      *x509.CertPool
+	Fingerprint fingerprint.TLSApplier
 
 	handlers []Handler
 }
 
-// function is a function that processes an incoming client connection
+// Handler is a function that processes an incoming client connection.
 type Handler func(conn net.Conn, p *Proxy)
 
-// adds a handler to the orchestrator
+// RegisterHandler adds a handler to the proxy.
 func (p *Proxy) RegisterHandler(h Handler) {
 	p.handlers = append(p.handlers, h)
 }
 
-func (p *Proxy) AddCertificate(cert *x509.CertPool) {
-	p.CAPool = cert
+// dialTarget dials host (host:port), performs a uTLS handshake using p.Fingerprint,
+// and returns the ready-to-use connection. Falls back to HelloRandomized if no
+// fingerprint is configured.
+func (p *Proxy) dialTarget(host string) (*utls.UConn, error) {
+	tcpConn, err := net.Dial("tcp", host)
+	if err != nil {
+		return nil, err
+	}
+
+	helloID := utls.HelloRandomized
+	if p.Fingerprint != nil {
+		helloID = p.Fingerprint.ClientHelloID()
+	}
+
+	serverName := host
+	if idx := strings.LastIndex(host, ":"); idx != -1 {
+		serverName = host[:idx]
+	}
+
+	cfg := &utls.Config{
+		ServerName: serverName,
+	}
+	if p.CAPool != nil {
+		cfg.RootCAs = p.CAPool
+	}
+
+	uconn := utls.UClient(tcpConn, cfg, helloID)
+	if err := uconn.Handshake(); err != nil {
+		uconn.Close()
+		return nil, err
+	}
+	return uconn, nil
+}
+
+// pipe copies data bidirectionally between client (reading from r) and target,
+// blocking until both directions are done.
+func pipe(client io.ReadWriter, r io.Reader, target io.ReadWriter) {
+	var wg sync.WaitGroup
+	wg.Add(2)
+
+	go func() {
+		defer wg.Done()
+		io.Copy(target, r)
+	}()
+
+	go func() {
+		defer wg.Done()
+		io.Copy(client, target)
+	}()
+
+	wg.Wait()
 }
 
-// Run starts the proxy server and orchestrates all protocol handlers
+// Run starts the proxy server and dispatches incoming connections to registered handlers.
 func (p *Proxy) Run() error {
 	ln, err := net.Listen("tcp", p.ListenAddr)
 	if err != nil {
@@ -34,7 +90,7 @@ func (p *Proxy) Run() error {
 	}
 	defer ln.Close()
 
-	log.Printf("[+] Starting proxy on %s, forwarding to port %d", p.ListenAddr, p.ForwardTo)
+	log.Printf("[+] Starting proxy on %s", p.ListenAddr)
 	for {
 		c, err := ln.Accept()
 		if err != nil {
@@ -45,8 +101,6 @@ func (p *Proxy) Run() error {
 		conn := c
 		go func() {
 			for _, h := range p.handlers {
-				// Each handler processes the incoming connection.
-				// It will get discarded by a handler if it doesn't support it.
 				h(conn, p)
 			}
 		}()
diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go
@@ -0,0 +1,136 @@
+package proxy
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/tls"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"io"
+	"math/big"
+	"net"
+	"testing"
+	"time"
+
+	"github.com/djnnvx/mic/fingerprint"
+)
+
+// generateSelfSignedCert creates an in-memory self-signed cert/pool for testing.
+func generateSelfSignedCert(t *testing.T) (*x509.CertPool, tls.Certificate) {
+	t.Helper()
+
+	key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+	if err != nil {
+		t.Fatalf("generating key: %v", err)
+	}
+
+	tmpl := &x509.Certificate{
+		SerialNumber: big.NewInt(1),
+		Subject:      pkix.Name{CommonName: "localhost"},
+		NotBefore:    time.Now().Add(-time.Hour),
+		NotAfter:     time.Now().Add(time.Hour),
+		DNSNames:     []string{"localhost"},
+		IPAddresses:  []net.IP{net.ParseIP("127.0.0.1")},
+	}
+
+	certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+	if err != nil {
+		t.Fatalf("creating certificate: %v", err)
+	}
+
+	cert, err := x509.ParseCertificate(certDER)
+	if err != nil {
+		t.Fatalf("parsing certificate: %v", err)
+	}
+
+	pool := x509.NewCertPool()
+	pool.AddCert(cert)
+
+	tlsCert := tls.Certificate{
+		Certificate: [][]byte{certDER},
+		PrivateKey:  key,
+	}
+
+	return pool, tlsCert
+}
+
+func TestDialTarget_HandshakeSucceeds(t *testing.T) {
+	pool, tlsCert := generateSelfSignedCert(t)
+
+	// Start an in-process TLS server on a random port.
+	ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
+		Certificates: []tls.Certificate{tlsCert},
+	})
+	if err != nil {
+		t.Fatalf("tls.Listen: %v", err)
+	}
+	defer ln.Close()
+
+	// Accept and drain one connection in the background.
+	go func() {
+		conn, err := ln.Accept()
+		if err != nil {
+			return
+		}
+		defer conn.Close()
+		io.Copy(io.Discard, conn)
+	}()
+
+	fp, err := fingerprint.NewTLS("t13d1516h2_8daaf6152771_b0da82dd1658")
+	if err != nil {
+		t.Fatalf("fingerprint.NewTLS: %v", err)
+	}
+
+	p := &Proxy{
+		ListenAddr:  "127.0.0.1:0",
+		CAPool:      pool,
+		Fingerprint: fp,
+	}
+
+	conn, err := p.dialTarget(ln.Addr().String())
+	if err != nil {
+		t.Fatalf("dialTarget(%q): %v", ln.Addr().String(), err)
+	}
+	defer conn.Close()
+
+	// Verify connection is usable for writes.
+	if _, err := conn.Write([]byte("hello\n")); err != nil {
+		t.Fatalf("writing to connection: %v", err)
+	}
+}
+
+func TestDialTarget_FallbackToRandomized(t *testing.T) {
+	pool, tlsCert := generateSelfSignedCert(t)
+
+	ln, err := tls.Listen("tcp", "127.0.0.1:0", &tls.Config{
+		Certificates: []tls.Certificate{tlsCert},
+	})
+	if err != nil {
+		t.Fatalf("tls.Listen: %v", err)
+	}
+	defer ln.Close()
+
+	go func() {
+		conn, err := ln.Accept()
+		if err != nil {
+			return
+		}
+		defer conn.Close()
+		io.Copy(io.Discard, conn)
+	}()
+
+	// No fingerprint set — should fall back to HelloRandomized.
+	p := &Proxy{
+		ListenAddr: "127.0.0.1:0",
+		CAPool:     pool,
+	}
+
+	conn, err := p.dialTarget(ln.Addr().String())
+	if err != nil {
+		// HelloRandomized may select post-quantum curves not supported by stdlib TLS servers.
+		// Skip rather than fail — the code path is exercised regardless.
+		t.Skipf("dialTarget with randomized preset: %v", err)
+	}
+	conn.Close()
+}
diff --git a/proxy/serverfront.go b/proxy/serverfront.go
@@ -0,0 +1,43 @@
+package proxy
+
+import (
+	"bufio"
+	"crypto/tls"
+	"log"
+	"net"
+)
+
+// ServerFrontHandler returns a Handler that terminates incoming TLS using certFile/keyFile,
+// then forwards to p.BackendAddr using the configured fingerprint.
+func ServerFrontHandler(certFile, keyFile string) Handler {
+	cert, err := tls.LoadX509KeyPair(certFile, keyFile)
+	if err != nil {
+		log.Fatalf("ServerFrontHandler: loading certificate: %v", err)
+	}
+
+	tlsCfg := &tls.Config{
+		Certificates: []tls.Certificate{cert},
+	}
+
+	return func(clientConn net.Conn, p *Proxy) {
+		defer clientConn.Close()
+
+		// Terminate incoming TLS with stdlib crypto/tls.
+		tlsConn := tls.Server(clientConn, tlsCfg)
+		if err := tlsConn.Handshake(); err != nil {
+			log.Printf("ServerFront: TLS handshake with client failed: %v", err)
+			return
+		}
+		defer tlsConn.Close()
+
+		// Dial the backend with the configured uTLS fingerprint.
+		targetConn, err := p.dialTarget(p.BackendAddr)
+		if err != nil {
+			log.Printf("ServerFront: failed to connect to backend %s: %v", p.BackendAddr, err)
+			return
+		}
+		defer targetConn.Close()
+
+		pipe(tlsConn, bufio.NewReader(tlsConn), targetConn)
+	}
+}
diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+# cleanup.sh — stop the proxy and remove build artifacts.
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+BINARY="$ROOT_DIR/mic"
+PID_FILE="$ROOT_DIR/.mic.pid"
+
+# ── stop proxy via PID file ────────────────────────────────────────────────────
+if [ -f "$PID_FILE" ]; then
+    PID="$(cat "$PID_FILE")"
+    if kill -0 "$PID" 2>/dev/null; then
+        echo "[+] Stopping mic (PID $PID)..."
+        kill "$PID"
+        # Wait briefly for clean shutdown.
+        for i in $(seq 1 10); do
+            kill -0 "$PID" 2>/dev/null || break
+            sleep 0.2
+        done
+        if kill -0 "$PID" 2>/dev/null; then
+            echo "[!] Process still alive after 2 s — sending SIGKILL."
+            kill -9 "$PID" || true
+        fi
+    else
+        echo "[~] PID $PID is no longer running."
+    fi
+    rm -f "$PID_FILE"
+else
+    echo "[~] No PID file found."
+fi
+
+# ── catch any stray mic processes ─────────────────────────────────────────────
+if pgrep -x mic >/dev/null 2>&1; then
+    echo "[+] Killing remaining mic processes..."
+    pkill -x mic || true
+fi
+
+# ── remove binary ──────────────────────────────────────────────────────────────
+if [ -f "$BINARY" ]; then
+    echo "[+] Removing binary $BINARY..."
+    rm "$BINARY"
+fi
+
+echo "[+] Cleanup complete."
diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+# run_integration_tests.sh — run the integration test suite.
+#
+# Integration tests spin up in-process TLS servers and do not require an
+# external proxy instance.  They exercise the full round-trip:
+#
+#   client-front: client → TCP CONNECT → proxy → uTLS(Chrome120) → target
+#   server-front: client → TLS → proxy (terminates) → uTLS(Chrome120) → backend
+#
+# Usage:
+#   scripts/run_integration_tests.sh            # all packages
+#   scripts/run_integration_tests.sh ./proxy/   # single package
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+
+PACKAGES="${1:-./...}"
+TIMEOUT="${INTEGRATION_TIMEOUT:-60s}"
+
+echo "[+] Running integration tests (timeout: $TIMEOUT)..."
+echo ""
+
+(cd "$ROOT_DIR" && go test -tags integration -v -timeout "$TIMEOUT" "$PACKAGES")
+
+echo ""
+echo "[+] Done."
diff --git a/scripts/setup.sh b/scripts/setup.sh
@@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+# setup.sh — build the mic binary and create a starter config.
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+ROOT_DIR="$(dirname "$SCRIPT_DIR")"
+BINARY="$ROOT_DIR/mic"
+CONFIG="$ROOT_DIR/mic.toml"
+EXAMPLE="$ROOT_DIR/mic.example.toml"
+PID_FILE="$ROOT_DIR/.mic.pid"
+
+# ── build ──────────────────────────────────────────────────────────────────────
+echo "[+] Building mic..."
+(cd "$ROOT_DIR" && go build -o mic .)
+echo "    Binary: $BINARY"
+
+# ── config ─────────────────────────────────────────────────────────────────────
+if [ ! -f "$CONFIG" ]; then
+    echo "[+] Creating mic.toml from mic.example.toml..."
+    cp "$EXAMPLE" "$CONFIG"
+    echo "    Edit $CONFIG before starting (set mode, fingerprint.tls.ja4, etc.)"
+else
+    echo "[~] mic.toml already exists — leaving it untouched."
+fi
+
+# ── start proxy ────────────────────────────────────────────────────────────────
+if [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
+    echo "[~] mic is already running (PID $(cat "$PID_FILE"))."
+else
+    echo "[+] Starting mic in the background..."
+    "$BINARY" --config "$CONFIG" &
+    echo $! > "$PID_FILE"
+    echo "    PID $(cat "$PID_FILE") — log: mic.log"
+    echo "    Stop with:  scripts/cleanup.sh"
+fi