diff --git a/README.md b/README.md
@@ -7,11 +7,13 @@ browser to any fingerprinting system.
Two modes are supported:
-- **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.
+- **client-front** — HTTP CONNECT proxy with MitM TLS interception. The proxy
+ generates a local CA, issues per-host leaf certs on the fly, terminates TLS from
+ the client, and re-dials the target with the configured uTLS fingerprint. Standard
+ tools (curl, browsers) work after importing the CA once.
+- **server-front** — the proxy terminates incoming TLS (with your own cert/key), then
+ re-dials the backend with the configured fingerprint. Useful when the client cannot
+ be configured to use a CONNECT proxy.
---
@@ -24,14 +26,16 @@ sequenceDiagram
participant T as Target
rect rgb(30, 30, 60)
- note over C,T: client-front mode
+ note over C,T: client-front mode (MitM TLS)
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
+ P-->>C: 200 Connection Established
+ C->>P: TLS handshake (mic-issued cert for target)
+ P-->>C: TLS established
+ C->>P: HTTP request (decrypted by proxy)
+ P->>T: request bytes (through uTLS tunnel)
T-->>P: HTTP response (through uTLS)
- P-->>C: raw HTTP response
+ P-->>C: HTTP response (re-encrypted for client)
end
rect rgb(30, 60, 30)
@@ -39,8 +43,8 @@ sequenceDiagram
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
+ C->>P: HTTP request (decrypted by proxy)
+ P->>T: request bytes (through uTLS tunnel)
T-->>P: HTTP response (through uTLS)
P-->>C: HTTP response (re-encrypted for client)
end
@@ -57,7 +61,8 @@ not the default Go TLS fingerprint.
go build -o mic .
```
-Or with the helper script (builds and creates `mic.toml` from the example):
+Or with the helper script (builds, creates `mic.toml`, starts the proxy, and prints
+CA trust instructions):
```bash
scripts/setup.sh
@@ -86,24 +91,37 @@ addr = "127.0.0.1:443" # server-front only
[fingerprint.tls]
ja4 = "t13d1516h2_8daaf6152771_b0da82dd1658" # Chrome 120
-# server-front only
+# server-front only: certificate mic presents to clients
[fingerprint.tls.termination]
cert = "/path/to/cert.pem"
key = "/path/to/key.pem"
[ca]
-cert = "" # optional: custom CA for upstream verification
+cert = "" # optional: custom CA for verifying upstream targets
+
+# client-front only: local MitM CA.
+# mic creates these files on first run and reuses them across restarts.
+# Import ca.pem into curl / your browser / the system trust store.
+[ca.intercept]
+cert = "ca.pem"
+key = "ca-key.pem"
```
### Available JA4 fingerprints
-| JA4 hash | Browser |
+Hashes are measured by `cmd/probe` against tlsinfo.me and reflect what each utls
+preset actually emits. Re-run the probe after upgrading the utls dependency.
+
+| JA4 hash | Preset |
|---|---|
-| `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 |
+| `t13d1516h2_8daaf6152771_02713d6af862` | Chrome 120 (`HelloChrome_120`) |
+| `t13d1715h2_5b57614c22b0_5c2c66f702b0` | Firefox 120 (`HelloFirefox_120`) |
+| `t13d2014h2_a09f3c656075_14788d8d241b` | Safari 16.0 (`HelloSafari_16_0`) |
+| `t13d1516h2_8daaf6152771_e5627efa2ab1` | Edge 106 (`HelloEdge_106`) |
+
+> `HelloChrome_120_PQ` (post-quantum) produces the same JA4 as Chrome 120 because
+> the X25519MLKEM768 key share is not distinguished by JA4. It is not separately
+> selectable via config hash.
---
@@ -115,11 +133,28 @@ cert = "" # optional: custom CA for upstream verification
### client-front
-Configure your client to use an HTTP CONNECT proxy at the listen address:
+Set `mode = "client-front"` and configure `[ca.intercept]` with paths for the local
+CA cert and key. mic will create them on first run.
+
+**Trust the CA once** (pick whichever applies):
+
+```bash
+# curl — pass --cacert on every invocation, or set CURL_CA_BUNDLE
+curl --cacert ca.pem -x http://localhost:8080 https://tlsinfo.me/json
+
+# Debian / Ubuntu / Kali — add to system store
+sudo cp ca.pem /usr/local/share/ca-certificates/mic-ca.crt
+sudo update-ca-certificates
+
+# macOS
+sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.pem
+```
+
+After trusting the CA, standard tools work without extra flags:
```bash
-curl -x http://localhost:8080 https://tls.peet.ws/api/all
-# check the "ja4" field in the response
+curl -x http://localhost:8080 https://tlsinfo.me/json
+# check the "ja4" field — it should match your configured fingerprint
```
### server-front
diff --git a/cmd/probe/main.go b/cmd/probe/main.go
@@ -0,0 +1,104 @@
+// probe dials tlsinfo.me/json with each known utls preset and prints the
+// JA4 hash that the server observes. Run once to populate the fingerprint table.
+package main
+
+import (
+ "bufio"
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "net"
+ "net/http"
+
+ utls "github.com/refraction-networking/utls"
+ "golang.org/x/net/http2"
+)
+
+var presets = []struct {
+ name string
+ id utls.ClientHelloID
+}{
+ {"HelloChrome_120", utls.HelloChrome_120},
+ {"HelloChrome_120_PQ", utls.HelloChrome_120_PQ},
+ {"HelloFirefox_120", utls.HelloFirefox_120},
+ {"HelloSafari_16_0", utls.HelloSafari_16_0},
+ {"HelloEdge_106", utls.HelloEdge_106},
+}
+
+func dialUTLS(ctx context.Context, id utls.ClientHelloID) (*utls.UConn, error) {
+ tcpConn, err := (&net.Dialer{}).DialContext(ctx, "tcp", "tlsinfo.me:443")
+ if err != nil {
+ return nil, err
+ }
+ uconn := utls.UClient(tcpConn, &utls.Config{ServerName: "tlsinfo.me"}, id)
+ if err := uconn.Handshake(); err != nil {
+ tcpConn.Close()
+ return nil, err
+ }
+ return uconn, nil
+}
+
+func decodeJA4(body interface{ Read([]byte) (int, error) }) (string, error) {
+ var result struct {
+ JA4 string `json:"ja4"`
+ }
+ if err := json.NewDecoder(body).Decode(&result); err != nil {
+ return "", err
+ }
+ return result.JA4, nil
+}
+
+func probe(id utls.ClientHelloID) (string, error) {
+ ctx := context.Background()
+ uconn, err := dialUTLS(ctx, id)
+ if err != nil {
+ return "", err
+ }
+ defer uconn.Close()
+
+ proto := uconn.ConnectionState().NegotiatedProtocol
+
+ if proto == "h2" {
+ // Use http2.Transport with the already-dialed connection.
+ h2t := &http2.Transport{
+ DialTLSContext: func(ctx context.Context, network, addr string, _ *tls.Config) (net.Conn, error) {
+ return dialUTLS(ctx, id)
+ },
+ }
+ // Discard the first connection — the Transport dials its own.
+ uconn.Close()
+ client := &http.Client{Transport: h2t}
+ resp, err := client.Get("https://tlsinfo.me/json")
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ return decodeJA4(resp.Body)
+ }
+
+ // http/1.1 path: reuse the already-established connection.
+ req, _ := http.NewRequest("GET", "/json", nil)
+ req.Host = "tlsinfo.me"
+ req.Header.Set("Connection", "close")
+ if err := req.Write(uconn); err != nil {
+ return "", err
+ }
+ resp, err := http.ReadResponse(bufio.NewReader(uconn), req)
+ if err != nil {
+ return "", err
+ }
+ defer resp.Body.Close()
+ return decodeJA4(resp.Body)
+}
+
+func main() {
+ for _, p := range presets {
+ ja4, err := probe(p.id)
+ if err != nil {
+ fmt.Printf("%-22s ERROR: %v\n", p.name, err)
+ continue
+ }
+ fmt.Printf("%-22s %s\n", p.name, ja4)
+ }
+}
diff --git a/cmd/root.go b/cmd/root.go
@@ -86,6 +86,15 @@ func getParser(opts *MicCliOptions) *cobra.Command {
switch cfg.Mode {
case "client-front":
log.Printf("[+] Mode: client-front (HTTP CONNECT proxy)")
+ if cfg.CA.Intercept.Cert != "" || cfg.CA.Intercept.Key != "" {
+ ca, err := proxy.LoadOrGenerateCA(cfg.CA.Intercept.Cert, cfg.CA.Intercept.Key)
+ if err != nil {
+ log.Fatalf("[!] Failed to load/generate intercept CA: %v", err)
+ }
+ p.LocalCA = ca
+ log.Printf("[+] MitM CA ready — import %s, then: curl --cacert %s -x http://localhost%s https://<target>",
+ cfg.CA.Intercept.Cert, cfg.CA.Intercept.Cert, cfg.Listen.Addr)
+ }
p.RegisterHandler(proxy.HttpsHandler)
case "server-front":
log.Printf("[+] Mode: server-front (TLS termination → backend %s)", cfg.Backend.Addr)
diff --git a/config/config.go b/config/config.go
@@ -24,7 +24,13 @@ type Backend struct {
}
type CAConf struct {
- Cert string `toml:"cert"`
+ Cert string `toml:"cert"` // upstream CA cert for verifying targets
+ Intercept InterceptCAConf `toml:"intercept"` // client-front: local MitM CA
+}
+
+type InterceptCAConf struct {
+ Cert string `toml:"cert"` // path to local CA cert (created if absent)
+ Key string `toml:"key"` // path to local CA key (created if absent)
}
type FpConf struct {
diff --git a/fingerprint/tls.go b/fingerprint/tls.go
@@ -9,17 +9,21 @@ import (
// 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.
+// Table maps JA4-TLS hashes to utls ClientHelloID presets.
+//
+// Hashes are measured empirically against tlsinfo.me using cmd/probe — they
+// reflect what the utls preset actually emits, not captures from real browsers.
+// Re-run cmd/probe after upgrading the utls dependency to verify they still match.
+//
+// Note: HelloChrome_120_PQ produces the same JA4 as HelloChrome_120 because the
+// post-quantum key share (X25519MLKEM768) falls in a key_share extension entry
+// that JA4 does not distinguish. Use HelloChrome_120_PQ directly in dialTarget
+// if you specifically need the PQ key exchange behaviour.
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,
+ "t13d1516h2_8daaf6152771_02713d6af862": utls.HelloChrome_120,
+ "t13d1715h2_5b57614c22b0_5c2c66f702b0": utls.HelloFirefox_120,
+ "t13d2014h2_a09f3c656075_14788d8d241b": utls.HelloSafari_16_0,
+ "t13d1516h2_8daaf6152771_e5627efa2ab1": utls.HelloEdge_106,
}
// Lookup returns the utls.ClientHelloID for the given JA4 hash.
diff --git a/go.mod b/go.mod
@@ -14,5 +14,7 @@ require (
github.com/klauspost/compress v1.17.4 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/crypto v0.36.0 // indirect
+ golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
+ golang.org/x/text v0.23.0 // indirect
)
diff --git a/go.sum b/go.sum
@@ -16,7 +16,11 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/mic.example.toml b/mic.example.toml
@@ -9,7 +9,7 @@ addr = "127.0.0.1:443"
[fingerprint]
[fingerprint.tls]
-ja4 = "t13d1516h2_8daaf6152771_b0da82dd1658" # Chrome 120
+ja4 = "t13d1516h2_8daaf6152771_02713d6af862" # Chrome 120 (utls.HelloChrome_120)
# server-front only: certificate for terminating incoming TLS
[fingerprint.tls.termination]
@@ -19,6 +19,13 @@ key = "/path/to/key.pem"
[ca]
cert = "" # optional custom CA for upstream verification
+# client-front only: MitM interception CA.
+# mic will create the files on first run, then reuse them across restarts.
+# Import ca.pem into curl / your browser / system trust store.
+[ca.intercept]
+cert = "ca.pem"
+key = "ca-key.pem"
+
# Future:
# [fingerprint.tcp]
# window_size = 65535
diff --git a/proxy/ca.go b/proxy/ca.go
@@ -0,0 +1,188 @@
+package proxy
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "fmt"
+ "math/big"
+ "net"
+ "os"
+ "sync"
+ "time"
+)
+
+// LocalCA is a self-signed CA used to issue per-host certificates for
+// MitM TLS interception in client-front mode.
+type LocalCA struct {
+ cert *x509.Certificate
+ key *ecdsa.PrivateKey
+ derCert []byte // raw DER of CA cert, appended as chain in issued certs
+
+ mu sync.Mutex
+ cache map[string]tls.Certificate
+}
+
+// GenerateCA creates a fresh in-memory self-signed CA.
+func GenerateCA() (*LocalCA, error) {
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return nil, err
+ }
+ return buildCA(key)
+}
+
+// LoadOrGenerateCA loads the CA from certPath + keyPath. If the files do not
+// exist, it generates a new CA and writes it to those paths. Pass empty strings
+// to get an ephemeral in-memory CA.
+func LoadOrGenerateCA(certPath, keyPath string) (*LocalCA, error) {
+ if certPath == "" || keyPath == "" {
+ return GenerateCA()
+ }
+
+ ca, err := loadCA(certPath, keyPath)
+ if err == nil {
+ return ca, nil
+ }
+ if !os.IsNotExist(err) {
+ return nil, fmt.Errorf("loading CA: %w", err)
+ }
+
+ // Files don't exist — generate a new CA and persist it.
+ ca, err = GenerateCA()
+ if err != nil {
+ return nil, err
+ }
+ if err := ca.Save(certPath, keyPath); err != nil {
+ return nil, fmt.Errorf("saving CA: %w", err)
+ }
+ return ca, nil
+}
+
+func loadCA(certPath, keyPath string) (*LocalCA, error) {
+ pair, err := tls.LoadX509KeyPair(certPath, keyPath)
+ if err != nil {
+ return nil, err
+ }
+ cert, err := x509.ParseCertificate(pair.Certificate[0])
+ if err != nil {
+ return nil, err
+ }
+ key, ok := pair.PrivateKey.(*ecdsa.PrivateKey)
+ if !ok {
+ return nil, fmt.Errorf("CA private key must be ECDSA")
+ }
+ return &LocalCA{
+ cert: cert,
+ key: key,
+ derCert: pair.Certificate[0],
+ cache: make(map[string]tls.Certificate),
+ }, nil
+}
+
+func buildCA(key *ecdsa.PrivateKey) (*LocalCA, error) {
+ serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
+ if err != nil {
+ return nil, err
+ }
+ tmpl := &x509.Certificate{
+ SerialNumber: serial,
+ Subject: pkix.Name{CommonName: "mic local CA"},
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
+ IsCA: true,
+ KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
+ BasicConstraintsValid: true,
+ }
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+ if err != nil {
+ return nil, err
+ }
+ cert, err := x509.ParseCertificate(der)
+ if err != nil {
+ return nil, err
+ }
+ return &LocalCA{
+ cert: cert,
+ key: key,
+ derCert: der,
+ cache: make(map[string]tls.Certificate),
+ }, nil
+}
+
+// Save writes the CA certificate and private key to PEM files.
+func (ca *LocalCA) Save(certPath, keyPath string) error {
+ cf, err := os.Create(certPath)
+ if err != nil {
+ return err
+ }
+ defer cf.Close()
+ if err := pem.Encode(cf, &pem.Block{Type: "CERTIFICATE", Bytes: ca.derCert}); err != nil {
+ return err
+ }
+
+ keyBytes, err := x509.MarshalECPrivateKey(ca.key)
+ if err != nil {
+ return err
+ }
+ kf, err := os.Create(keyPath)
+ if err != nil {
+ return err
+ }
+ defer kf.Close()
+ return pem.Encode(kf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})
+}
+
+// CertPEM returns the CA certificate as a PEM block, ready to be imported
+// into a trust store (e.g. curl --cacert, system store).
+func (ca *LocalCA) CertPEM() []byte {
+ return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: ca.derCert})
+}
+
+// issueCert generates (and caches) a leaf certificate for host, signed by the CA.
+func (ca *LocalCA) issueCert(host string) (tls.Certificate, error) {
+ ca.mu.Lock()
+ defer ca.mu.Unlock()
+
+ if c, ok := ca.cache[host]; ok {
+ return c, nil
+ }
+
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ serial, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ tmpl := &x509.Certificate{
+ SerialNumber: serial,
+ Subject: pkix.Name{CommonName: host},
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: time.Now().Add(24 * time.Hour),
+ KeyUsage: x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ }
+ if ip := net.ParseIP(host); ip != nil {
+ tmpl.IPAddresses = []net.IP{ip}
+ } else {
+ tmpl.DNSNames = []string{host}
+ }
+
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, ca.cert, &key.PublicKey, ca.key)
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ cert := tls.Certificate{
+ // Include the CA cert so clients that load the CA can verify the chain.
+ Certificate: [][]byte{der, ca.derCert},
+ PrivateKey: key,
+ }
+ ca.cache[host] = cert
+ return cert, nil
+}
diff --git a/proxy/https.go b/proxy/https.go
@@ -2,6 +2,7 @@ package proxy
import (
"bufio"
+ "crypto/tls"
"log"
"net"
"net/http"
@@ -41,5 +42,48 @@ func HttpsHandler(clientConn net.Conn, p *Proxy) {
defer targetConn.Close()
clientConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
+
+ // MitM path: terminate TLS from the client using a per-host certificate
+ // issued by the local CA, then pipe decrypted traffic over the already-
+ // established uTLS connection to the target.
+ if p.LocalCA != nil {
+ serverName := host
+ if idx := strings.LastIndex(host, ":"); idx != -1 {
+ serverName = host[:idx]
+ }
+
+ cert, err := p.LocalCA.issueCert(serverName)
+ if err != nil {
+ log.Printf("Failed to issue cert for %s: %v", serverName, err)
+ return
+ }
+
+ // Mirror the ALPN negotiated with the target so the client uses the
+ // same application protocol (e.g. h2 vs http/1.1). Without this,
+ // Chrome-fingerprint uTLS often negotiates h2 with the target while
+ // the client sends HTTP/1.1, causing an immediate protocol error.
+ nextProtos := []string{"http/1.1"}
+ if proto := targetConn.ConnectionState().NegotiatedProtocol; proto != "" {
+ nextProtos = []string{proto}
+ }
+
+ tlsClient := tls.Server(clientConn, &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ NextProtos: nextProtos,
+ })
+ if err := tlsClient.Handshake(); err != nil {
+ log.Printf("TLS handshake with client failed for %s: %v", serverName, err)
+ return
+ }
+ defer tlsClient.Close()
+
+ log.Printf("MitM tunnel established for %s (proto: %s)", serverName, tlsClient.ConnectionState().NegotiatedProtocol)
+
+ // Both sides are now on the same protocol — pipe plaintext between them.
+ pipe(tlsClient, tlsClient, targetConn)
+ return
+ }
+
+ // No LocalCA: pipe raw bytes (client must not speak TLS after CONNECT).
pipe(clientConn, reader, targetConn)
}
diff --git a/proxy/proxy.go b/proxy/proxy.go
@@ -17,6 +17,7 @@ type Proxy struct {
BackendAddr string // server-front: backend host:port
CAPool *x509.CertPool
Fingerprint fingerprint.TLSApplier
+ LocalCA *LocalCA // client-front: MitM CA for TLS interception
handlers []Handler
}
@@ -64,19 +65,22 @@ func (p *Proxy) dialTarget(host string) (*utls.UConn, error) {
}
// 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) {
+// blocking until both directions are done. When either direction finishes it
+// closes that side's connection so the other goroutine unblocks and exits.
+func pipe(client io.ReadWriteCloser, r io.Reader, target io.ReadWriteCloser) {
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
io.Copy(target, r)
+ target.Close()
}()
go func() {
defer wg.Done()
io.Copy(client, target)
+ client.Close()
}()
wg.Wait()
diff --git a/scripts/setup.sh b/scripts/setup.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# setup.sh — build the mic binary and create a starter config.
+# setup.sh — build the mic binary, create a starter config, and start the proxy.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -28,8 +28,24 @@ 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" &
+ "$BINARY" --config "$CONFIG" > "$ROOT_DIR/mic.log" 2>&1 &
echo $! > "$PID_FILE"
- echo " PID $(cat "$PID_FILE") — log: mic.log"
+ echo " PID $(cat "$PID_FILE") — log: $ROOT_DIR/mic.log"
echo " Stop with: scripts/cleanup.sh"
fi
+
+# ── client-front: MitM CA hint ─────────────────────────────────────────────────
+# Wait briefly for mic to write the CA files on first run.
+CA_CERT="$ROOT_DIR/ca.pem"
+sleep 0.5
+if [ -f "$CA_CERT" ]; then
+ echo ""
+ echo "[i] MitM CA certificate found: $CA_CERT"
+ echo " Trust it once, then use mic as an HTTPS proxy:"
+ echo ""
+ echo " curl --cacert $CA_CERT -x http://localhost:8080 https://tlsinfo.me/json"
+ echo ""
+ echo " To add it to the system trust store (Debian/Ubuntu/Kali):"
+ echo " sudo cp $CA_CERT /usr/local/share/ca-certificates/mic-ca.crt"
+ echo " sudo update-ca-certificates"
+fi
| | | | | | | | | | | |