MITM proxy
This commit is contained in:
99
mitmproxy/mitm_http.go
Normal file
99
mitmproxy/mitm_http.go
Normal 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
57
mitmproxy/mitm_test.go
Normal 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
279
mitmproxy/mitmproxy.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user