diff --git a/cmd/client.go b/cmd/client.go
@@ -0,0 +1,70 @@
+package cmd
+
+import (
+ "crypto/x509"
+ "log"
+ "os"
+
+ "github.com/djnnvx/mic/fingerprint"
+ "github.com/djnnvx/mic/proxy"
+ "github.com/spf13/cobra"
+)
+
+func newClientCmd() *cobra.Command {
+ var (
+ listen string
+ fpName string
+ caPath string
+ interceptCert string
+ interceptKey string
+ )
+
+ cmd := &cobra.Command{
+ Use: "client",
+ Short: "HTTP CONNECT proxy with optional MitM interception",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ p := &proxy.Proxy{ListenAddr: listen}
+
+ if caPath != "" {
+ caCert, err := os.ReadFile(caPath)
+ if err != nil {
+ log.Fatalf("[!] Failed to read CA certificate: %v", err)
+ }
+ pool := x509.NewCertPool()
+ pool.AppendCertsFromPEM(caCert)
+ p.CAPool = pool
+ }
+
+ if fpName != "" {
+ fp, err := fingerprint.ByName(fpName)
+ if err != nil {
+ log.Fatalf("[!] Failed to load fingerprint: %v", err)
+ }
+ p.Fingerprint = fp
+ log.Printf("[+] Using TLS fingerprint: %s", fp.Name())
+ }
+
+ if interceptCert != "" || interceptKey != "" {
+ ca, err := proxy.LoadOrGenerateCA(interceptCert, interceptKey)
+ 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>",
+ interceptCert, interceptCert, listen)
+ }
+
+ log.Printf("[+] Mode: client (HTTP CONNECT proxy on %s)", listen)
+ p.RegisterHandler(proxy.HttpsHandler)
+ return p.Run()
+ },
+ }
+
+ cmd.Flags().StringVarP(&listen, "listen", "l", ":8080", "listen address")
+ cmd.Flags().StringVarP(&fpName, "fingerprint", "f", "", "TLS fingerprint profile name (e.g. chrome-120)")
+ cmd.Flags().StringVar(&caPath, "ca", "", "path to upstream CA certificate")
+ cmd.Flags().StringVar(&interceptCert, "intercept-cert", "", "MitM CA cert path (enables interception)")
+ cmd.Flags().StringVar(&interceptKey, "intercept-key", "", "MitM CA key path (enables interception)")
+
+ return cmd
+}
diff --git a/cmd/root.go b/cmd/root.go
@@ -1,14 +1,9 @@
package cmd
import (
- "crypto/x509"
"fmt"
- "log"
"os"
- "github.com/djnnvx/mic/config"
- "github.com/djnnvx/mic/fingerprint"
- "github.com/djnnvx/mic/proxy"
"github.com/spf13/cobra"
)
@@ -16,7 +11,6 @@ var SKID_ASCII_ART = `
-
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣤⣤⣤⣤⣤⣤⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⠿⢿⣿⣿⣿⣿⣿⣶⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⣿⣿⣿⣿⣿⣿⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
@@ -24,7 +18,7 @@ var SKID_ASCII_ART = `
-------- ⠀⠀⢸⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⣀⣀⣀⣙⢿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⠀⠻⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀
djnn.sh⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀
- ⠀v0.0.1 ⠀⠀⢠⣿⣿⣿⣿⡟⠹⠿⠟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀
+ ⠀v0.0.2 ⠀⠀⢠⣿⣿⣿⣿⡟⠹⠿⠟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⡆⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡿⠋⡬⢿⣿⣷⣤⣤⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠰⡇⢸⡇⢸⣿⣿⣿⠟⠁⢀⣬⢽⣿⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣧⣈⣛⣿⣿⣿⡇⠀⠀⣾⠁⢀⢻⣿⣿⣿⣿⠇⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀
@@ -35,7 +29,7 @@ var SKID_ASCII_ART = `
⠸⣿⣿⣄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠃⠀
⠀⢹⣿⣿⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀
⠀⠀⠹⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀
- ⠀⠀⠀⠙⣿⣿⣿⣿⣿⣶⣤⣀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿mina is cute⣿⣿⣿⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀
+ ⠀⠀⠀⠙⣿⣿⣿⣿⣿⣶⣤⣀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿mina is cute⣿⣿⣿⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀
⠀⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣷⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿<3:)⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠉⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠋⠅⠀⠀⠀⠀⠀⠀⠀⠀⠀
@@ -46,86 +40,20 @@ var SKID_ASCII_ART = `
`
-func getParser(opts *MicCliOptions) *cobra.Command {
- var rootCmd = &cobra.Command{
+func newRootCmd() *cobra.Command {
+ rootCmd := &cobra.Command{
Use: "mic",
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)
- }
-
- p := &proxy.Proxy{
- ListenAddr: cfg.Listen.Addr,
- BackendAddr: cfg.Backend.Addr,
- }
-
- // Load optional custom CA pool.
- if cfg.CA.Cert != "" {
- caCert, err := os.ReadFile(cfg.CA.Cert)
- if err != nil {
- log.Fatalf("[!] Failed to read CA certificate: %v", err)
- }
- pool := x509.NewCertPool()
- pool.AppendCertsFromPEM(caCert)
- p.CAPool = pool
- }
-
- // 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())
- }
-
- 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)
- 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.ConfigPath, "config", "c", defaults.ConfigPath, "path to TOML config file")
-
+ rootCmd.AddCommand(newClientCmd())
+ rootCmd.AddCommand(newServerCmd())
return rootCmd
}
func Execute() {
fmt.Println(SKID_ASCII_ART)
-
- opts := GetDefaultOptions()
- cliParser := getParser(&opts)
- if err := cliParser.Execute(); err != nil {
+ if err := newRootCmd().Execute(); err != nil {
fmt.Printf("[!] Error: %s\n", err.Error())
os.Exit(1)
}
diff --git a/cmd/server.go b/cmd/server.go
@@ -0,0 +1,72 @@
+package cmd
+
+import (
+ "crypto/x509"
+ "log"
+ "os"
+
+ "github.com/djnnvx/mic/fingerprint"
+ "github.com/djnnvx/mic/proxy"
+ "github.com/spf13/cobra"
+)
+
+func newServerCmd() *cobra.Command {
+ var (
+ listen string
+ backend string
+ fpName string
+ caPath string
+ cert string
+ key string
+ )
+
+ cmd := &cobra.Command{
+ Use: "server",
+ Short: "TLS termination proxy with upstream fingerprinting",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if backend == "" {
+ log.Fatal("[!] --backend is required for server mode")
+ }
+ if cert == "" || key == "" {
+ log.Fatal("[!] --cert and --key are required for server mode")
+ }
+
+ p := &proxy.Proxy{
+ ListenAddr: listen,
+ BackendAddr: backend,
+ }
+
+ if caPath != "" {
+ caCert, err := os.ReadFile(caPath)
+ if err != nil {
+ log.Fatalf("[!] Failed to read CA certificate: %v", err)
+ }
+ pool := x509.NewCertPool()
+ pool.AppendCertsFromPEM(caCert)
+ p.CAPool = pool
+ }
+
+ if fpName != "" {
+ fp, err := fingerprint.ByName(fpName)
+ if err != nil {
+ log.Fatalf("[!] Failed to load fingerprint: %v", err)
+ }
+ p.Fingerprint = fp
+ log.Printf("[+] Using TLS fingerprint: %s", fp.Name())
+ }
+
+ log.Printf("[+] Mode: server (TLS termination → backend %s)", backend)
+ p.RegisterHandler(proxy.ServerFrontHandler(cert, key))
+ return p.Run()
+ },
+ }
+
+ cmd.Flags().StringVarP(&listen, "listen", "l", ":8080", "listen address")
+ cmd.Flags().StringVarP(&backend, "backend", "b", "", "backend host:port (required)")
+ cmd.Flags().StringVarP(&fpName, "fingerprint", "f", "", "TLS fingerprint profile name (e.g. chrome-120)")
+ cmd.Flags().StringVar(&caPath, "ca", "", "path to upstream CA certificate")
+ cmd.Flags().StringVar(&cert, "cert", "", "TLS certificate for incoming termination (required)")
+ cmd.Flags().StringVar(&key, "key", "", "TLS key for incoming termination (required)")
+
+ return cmd
+}
diff --git a/cmd/types.go b/cmd/types.go
@@ -1,11 +0,0 @@
-package cmd
-
-type MicCliOptions struct {
- ConfigPath string
-}
-
-func GetDefaultOptions() MicCliOptions {
- return MicCliOptions{
- ConfigPath: "mic.toml",
- }
-}
diff --git a/config/config.go b/config/config.go
@@ -1,63 +0,0 @@
-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"` // 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 {
- 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
@@ -1,94 +0,0 @@
-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/go.mod b/go.mod
@@ -3,9 +3,9 @@ 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
+ golang.org/x/net v0.38.0
)
require (
@@ -14,7 +14,6 @@ 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
@@ -5,8 +5,6 @@ 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
@@ -1,33 +0,0 @@
-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_02713d6af862" # Chrome 120 (utls.HelloChrome_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
-
-# 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
-# ttl = 64
-# mss = 1460
| | | | | | | | |