MITM proxy

This commit is contained in:
Simon Zolin
2020-08-18 19:23:33 +03:00
parent c3123473cf
commit f85de51452
21 changed files with 2116 additions and 491 deletions

99
mitmproxy/mitm_http.go Normal file
View File

@@ -0,0 +1,99 @@
package mitmproxy
import (
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"github.com/AdguardTeam/golibs/jsonutil"
"github.com/AdguardTeam/golibs/log"
)
// Print to log and set HTTP error message
func httpError(r *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) {
text := fmt.Sprintf(format, args...)
log.Info("MITM: %s %s: %s", r.Method, r.URL, text)
http.Error(w, text, code)
}
type mitmConfigJSON struct {
Enabled bool `json:"enabled"`
ListenAddr string `json:"listen_address"`
ListenPort int `json:"listen_port"`
UserName string `json:"auth_username"`
Password string `json:"auth_password"`
CertData string `json:"cert_data"`
PKeyData string `json:"pkey_data"`
}
func (p *MITMProxy) handleGetConfig(w http.ResponseWriter, r *http.Request) {
resp := mitmConfigJSON{}
p.confLock.Lock()
resp.Enabled = p.conf.Enabled
host, port, _ := net.SplitHostPort(p.conf.ListenAddr)
resp.ListenAddr = host
resp.ListenPort, _ = strconv.Atoi(port)
resp.UserName = p.conf.UserName
resp.Password = p.conf.Password
p.confLock.Unlock()
js, err := json.Marshal(resp)
if err != nil {
httpError(r, w, http.StatusInternalServerError, "json.Marshal: %s", err)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(js)
}
func (p *MITMProxy) handleSetConfig(w http.ResponseWriter, r *http.Request) {
req := mitmConfigJSON{}
_, err := jsonutil.DecodeObject(&req, r.Body)
if err != nil {
httpError(r, w, http.StatusBadRequest, "json.Decode: %s", err)
return
}
if !((len(req.CertData) != 0 && len(req.PKeyData) != 0) ||
(len(req.CertData) == 0 && len(req.PKeyData) == 0)) {
httpError(r, w, http.StatusBadRequest, "certificate & private key must be both empty or specified")
return
}
p.confLock.Lock()
if len(req.CertData) != 0 {
err = p.storeCert([]byte(req.CertData), []byte(req.PKeyData))
if err != nil {
httpError(r, w, http.StatusInternalServerError, "%s", err)
p.confLock.Unlock()
return
}
p.conf.RegenCert = false
} else {
p.conf.RegenCert = true
}
p.conf.Enabled = req.Enabled
p.conf.ListenAddr = net.JoinHostPort(req.ListenAddr, strconv.Itoa(req.ListenPort))
p.conf.UserName = req.UserName
p.conf.Password = req.Password
p.confLock.Unlock()
p.conf.ConfigModified()
p.Close()
err = p.Restart()
if err != nil {
httpError(r, w, http.StatusInternalServerError, "%s", err)
return
}
}
// Initialize web handlers
func (p *MITMProxy) initWeb() {
p.conf.HTTPRegister("GET", "/control/proxy_info", p.handleGetConfig)
p.conf.HTTPRegister("POST", "/control/proxy_config", p.handleSetConfig)
}

57
mitmproxy/mitm_test.go Normal file
View File

@@ -0,0 +1,57 @@
package mitmproxy
import (
"net/http"
"net/url"
"os"
"testing"
"github.com/AdguardTeam/AdGuardHome/filters"
"github.com/stretchr/testify/assert"
)
func prepareTestDir() string {
const dir = "./agh-test"
_ = os.RemoveAll(dir)
_ = os.MkdirAll(dir, 0755)
return dir
}
func TestMITM(t *testing.T) {
dir := prepareTestDir()
defer func() { _ = os.RemoveAll(dir) }()
fconf := filters.Conf{}
fconf.FilterDir = dir
fconf.HTTPClient = http.DefaultClient
filters := filters.New(fconf)
conf := Config{}
conf.Enabled = true
conf.CertDir = dir
conf.RegenCert = true
conf.ListenAddr = "127.0.0.1:8081"
conf.Filter = filters
s := New(conf)
assert.NotNil(t, s)
err := s.Start()
assert.Nil(t, err)
proxyURL, _ := url.Parse("http://127.0.0.1:8081")
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
c := http.Client{
Transport: transport,
}
resp, err := c.Get("http://example.com/")
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
resp, err = c.Get("http://adguardhome.api/cert.crt")
assert.Nil(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
s.Close()
}

279
mitmproxy/mitmproxy.go Normal file
View File

@@ -0,0 +1,279 @@
package mitmproxy
import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
"net/http"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/filters"
"github.com/AdguardTeam/golibs/file"
"github.com/AdguardTeam/golibs/log"
"github.com/AdguardTeam/gomitmproxy/mitm"
"github.com/AdguardTeam/urlfilter/proxy"
)
// MITMProxy - MITM proxy structure
type MITMProxy struct {
proxy *proxy.Server
conf Config
confLock sync.Mutex
}
// Config - module configuration
type Config struct {
Enabled bool `yaml:"enabled"`
ListenAddr string `yaml:"listen_address"`
UserName string `yaml:"auth_username"`
Password string `yaml:"auth_password"`
// TLS:
RegenCert bool `yaml:"regenerate_cert"` // Regenerate certificate on cert loading failure
CertDir string `yaml:"-"` // Directory where Root certificate & pkey is stored
certFileName string
pkeyFileName string
certData []byte
pkeyData []byte
Filter filters.Filters `yaml:"-"`
// Called when the configuration is changed by HTTP request
ConfigModified func() `yaml:"-"`
// Register an HTTP handler
HTTPRegister func(string, string, func(http.ResponseWriter, *http.Request)) `yaml:"-"`
}
// New - create a new instance of the query log
func New(conf Config) *MITMProxy {
p := MITMProxy{}
p.conf = conf
p.conf.certFileName = filepath.Join(p.conf.CertDir, "/http_proxy.crt")
p.conf.pkeyFileName = filepath.Join(p.conf.CertDir, "/http_proxy.key")
err := p.create()
if err != nil {
log.Error("MITM: %s", err)
return nil
}
if p.conf.HTTPRegister != nil {
p.initWeb()
}
p.conf.Filter.SetObserver(p.onFiltersChanged)
return &p
}
// Close - close the object
func (p *MITMProxy) Close() {
if p.proxy != nil {
p.proxy.Close()
p.proxy = nil
log.Debug("MITM: Closed proxy")
}
}
// WriteDiskConfig - write configuration on disk
func (p *MITMProxy) WriteDiskConfig(c *Config) {
p.confLock.Lock()
*c = p.conf
p.confLock.Unlock()
}
// Start - start proxy server
func (p *MITMProxy) Start() error {
if !p.conf.Enabled {
return nil
}
err := p.proxy.Start()
if err != nil {
return err
}
log.Debug("MITM: Running...")
return nil
}
// Restart - restart proxy server after Close()
func (p *MITMProxy) Restart() error {
err := p.create()
if err != nil {
return err
}
return p.Start()
}
// Create a gomitmproxy object
func (p *MITMProxy) create() error {
if !p.conf.Enabled {
return nil
}
c := proxy.Config{}
c.ProxyConfig.APIHost = "adguardhome.api"
addr, port, err := net.SplitHostPort(p.conf.ListenAddr)
if err != nil {
return fmt.Errorf("net.SplitHostPort: %s", err)
}
c.CompressContentScript = true
c.ProxyConfig.ListenAddr = &net.TCPAddr{}
c.ProxyConfig.ListenAddr.IP = net.ParseIP(addr)
if c.ProxyConfig.ListenAddr.IP == nil {
return fmt.Errorf("invalid IP: %s", addr)
}
c.ProxyConfig.ListenAddr.Port, err = strconv.Atoi(port)
if c.ProxyConfig.ListenAddr.Port < 0 || c.ProxyConfig.ListenAddr.Port > 0xffff || err != nil {
return fmt.Errorf("invalid port number: %s", port)
}
c.ProxyConfig.Username = p.conf.UserName
c.ProxyConfig.Password = p.conf.Password
err = p.loadCert()
if err != nil {
if !p.conf.RegenCert {
return err
}
log.Debug("%s", err)
// certificate or private key file doesn't exist - generate new
err = p.createRootCert()
if err != nil {
return err
}
}
c.ProxyConfig.MITMConfig, err = p.prepareMITMConfig()
if err != nil {
if !p.conf.RegenCert {
return err
}
// certificate or private key is invalid - generate new
err = p.createRootCert()
if err != nil {
return err
}
c.ProxyConfig.MITMConfig, err = p.prepareMITMConfig()
if err != nil {
return err
}
}
c.FiltersPaths = make(map[int]string)
filtrs := p.conf.Filter.List(0)
i := 0
for _, f := range filtrs {
if !f.Enabled ||
f.RuleCount == 0 { // not loaded
continue
}
c.FiltersPaths[i] = f.Path
i++
}
p.proxy, err = proxy.NewServer(c)
if err != nil {
return fmt.Errorf("proxy.NewServer: %s", err)
}
return nil
}
// Load cert and pkey from file
func (p *MITMProxy) loadCert() error {
var err error
p.conf.certData, err = ioutil.ReadFile(p.conf.certFileName)
if err != nil {
return err
}
p.conf.pkeyData, err = ioutil.ReadFile(p.conf.pkeyFileName)
if err != nil {
return err
}
return nil
}
// Create Root certificate and pkey and store it on disk
func (p *MITMProxy) createRootCert() error {
cert, key, err := mitm.NewAuthority("AdGuardHome Root", "AdGuard", 365*24*time.Hour)
if err != nil {
return err
}
p.conf.certData = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
p.conf.pkeyData = pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
log.Debug("MITM: Created root certificate and key")
err = p.storeCert(p.conf.certData, p.conf.pkeyData)
if err != nil {
return err
}
return nil
}
// Store cert & pkey on disk
func (p *MITMProxy) storeCert(certData []byte, pkeyData []byte) error {
err := file.SafeWrite(p.conf.certFileName, certData)
if err != nil {
return err
}
err = file.SafeWrite(p.conf.pkeyFileName, pkeyData)
if err != nil {
return err
}
log.Debug("MITM: stored root certificate and key: %s, %s", p.conf.certFileName, p.conf.pkeyFileName)
return nil
}
// Fill TLSConfig & MITMConfig objects
func (p *MITMProxy) prepareMITMConfig() (*mitm.Config, error) {
tlsCert, err := tls.X509KeyPair(p.conf.certData, p.conf.pkeyData)
if err != nil {
return nil, fmt.Errorf("failed to load root CA: %v", err)
}
privateKey := tlsCert.PrivateKey.(*rsa.PrivateKey)
x509c, err := x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
return nil, fmt.Errorf("invalid certificate: %v", err)
}
mitmConfig, err := mitm.NewConfig(x509c, privateKey, nil)
if err != nil {
return nil, fmt.Errorf("failed to create MITM config: %v", err)
}
mitmConfig.SetValidity(time.Hour * 24 * 7) // generate certs valid for 7 days
mitmConfig.SetOrganization("AdGuard") // cert organization
return mitmConfig, nil
}
func (p *MITMProxy) onFiltersChanged(flags uint) {
switch flags {
case filters.EventBeforeUpdate:
p.Close()
case filters.EventAfterUpdate:
err := p.Restart()
if err != nil {
log.Error("MITM: %s", err)
}
}
}