Initial implementation of mc-proxy

Layer 4 TLS SNI proxy with global firewall (IP/CIDR/GeoIP blocking),
per-listener route tables, bidirectional TCP relay with half-close
propagation, and a gRPC admin API (routes, firewall, status) with
TLS/mTLS support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 02:56:24 -07:00
commit c7024dcdf0
23 changed files with 2693 additions and 0 deletions

115
internal/config/config.go Normal file
View File

@@ -0,0 +1,115 @@
package config
import (
"fmt"
"os"
"time"
"github.com/pelletier/go-toml/v2"
)
type Config struct {
Listeners []Listener `toml:"listeners"`
GRPC GRPC `toml:"grpc"`
Firewall Firewall `toml:"firewall"`
Proxy Proxy `toml:"proxy"`
Log Log `toml:"log"`
}
type GRPC struct {
Addr string `toml:"addr"`
TLSCert string `toml:"tls_cert"`
TLSKey string `toml:"tls_key"`
ClientCA string `toml:"client_ca"`
}
type Listener struct {
Addr string `toml:"addr"`
Routes []Route `toml:"routes"`
}
type Route struct {
Hostname string `toml:"hostname"`
Backend string `toml:"backend"`
}
type Firewall struct {
GeoIPDB string `toml:"geoip_db"`
BlockedIPs []string `toml:"blocked_ips"`
BlockedCIDRs []string `toml:"blocked_cidrs"`
BlockedCountries []string `toml:"blocked_countries"`
}
type Proxy struct {
ConnectTimeout Duration `toml:"connect_timeout"`
IdleTimeout Duration `toml:"idle_timeout"`
ShutdownTimeout Duration `toml:"shutdown_timeout"`
}
type Log struct {
Level string `toml:"level"`
}
// Duration wraps time.Duration for TOML string unmarshalling.
type Duration struct {
time.Duration
}
func (d *Duration) UnmarshalText(text []byte) error {
var err error
d.Duration, err = time.ParseDuration(string(text))
return err
}
func Load(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config: %w", err)
}
var cfg Config
if err := toml.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
if err := cfg.validate(); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return &cfg, nil
}
func (c *Config) validate() error {
if len(c.Listeners) == 0 {
return fmt.Errorf("at least one listener is required")
}
for i, l := range c.Listeners {
if l.Addr == "" {
return fmt.Errorf("listener %d: addr is required", i)
}
if len(l.Routes) == 0 {
return fmt.Errorf("listener %d (%s): at least one route is required", i, l.Addr)
}
seen := make(map[string]bool)
for j, r := range l.Routes {
if r.Hostname == "" {
return fmt.Errorf("listener %d (%s), route %d: hostname is required", i, l.Addr, j)
}
if r.Backend == "" {
return fmt.Errorf("listener %d (%s), route %d: backend is required", i, l.Addr, j)
}
if seen[r.Hostname] {
return fmt.Errorf("listener %d (%s), route %d: duplicate hostname %q", i, l.Addr, j, r.Hostname)
}
seen[r.Hostname] = true
}
}
if len(c.Firewall.BlockedCountries) > 0 && c.Firewall.GeoIPDB == "" {
return fmt.Errorf("firewall: geoip_db is required when blocked_countries is set")
}
return nil
}