-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
split out most mitmproxy code, Tap interface
- Loading branch information
Matt Hamilton
committed
May 26, 2020
1 parent
33f1d7b
commit a534c7e
Showing
4 changed files
with
382 additions
and
292 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,3 +21,4 @@ site/ | |
.vim/ | ||
venv/ | ||
coverage.* | ||
krew/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,3 +14,4 @@ linters: | |
- gomnd | ||
- gocognit | ||
- goerr113 | ||
- gochecknoglobals |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,247 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"os" | ||
|
||
k8sappsv1 "k8s.io/api/apps/v1" | ||
v1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/apimachinery/pkg/util/intstr" | ||
"k8s.io/client-go/kubernetes" | ||
corev1 "k8s.io/client-go/kubernetes/typed/core/v1" | ||
) | ||
|
||
var ( | ||
// data volume names much have a "kubetap" prefix to be | ||
// properly removed during untapping. | ||
mitmproxyDataVolName = "kubetap-mitmproxy-data" | ||
mitmproxyConfigFile = "config.yaml" | ||
mitmproxyBaseConfig = `listen_port: 7777 | ||
ssl_insecure: true | ||
web_port: 2244 | ||
web_host: 0.0.0.0 | ||
web_open_browser: false | ||
` | ||
) | ||
|
||
var MitmproxySidecarContainer = v1.Container{ | ||
Name: kubetapContainerName, | ||
//NOTE: Image must be set | ||
//Image: image, | ||
//NOTE Args must be set | ||
//Args: commandArgs, | ||
ImagePullPolicy: v1.PullAlways, | ||
Ports: []v1.ContainerPort{ | ||
{ | ||
Name: kubetapPortName, | ||
ContainerPort: kubetapProxyListenPort, | ||
Protocol: v1.ProtocolTCP, | ||
}, | ||
{ | ||
Name: kubetapWebPortName, | ||
ContainerPort: kubetapProxyWebInterfacePort, | ||
Protocol: v1.ProtocolTCP, | ||
}, | ||
}, | ||
ReadinessProbe: &v1.Probe{ | ||
Handler: v1.Handler{ | ||
HTTPGet: &v1.HTTPGetAction{ | ||
Path: "/", | ||
Port: intstr.FromInt(kubetapProxyWebInterfacePort), | ||
Scheme: v1.URISchemeHTTP, | ||
}, | ||
}, | ||
InitialDelaySeconds: 5, | ||
PeriodSeconds: 5, | ||
SuccessThreshold: 3, | ||
TimeoutSeconds: 5, | ||
}, | ||
VolumeMounts: []v1.VolumeMount{ | ||
{ | ||
//NOTE: Name must be set | ||
//Name: kubetapConfigMapPrefix + dpl.Name, | ||
MountPath: "/home/mitmproxy/config/", // we store outside main dir to prevent RO problems, see below. | ||
// this also means that we need to wrap the official mitmproxy container. | ||
/* | ||
// *sigh* https://github.com/kubernetes/kubernetes/issues/64120 | ||
ReadOnly: false, // mitmproxy container does a chown | ||
MountPath: "/home/mitmproxy/.mitmproxy/config.yaml", | ||
SubPath: "config.yaml", // we only mount the config file | ||
*/ | ||
}, | ||
{ | ||
Name: mitmproxyDataVolName, | ||
MountPath: "/home/mitmproxy/.mitmproxy", | ||
ReadOnly: false, | ||
}, | ||
}, | ||
} | ||
|
||
// Sidecar provides a proxy sidecar container. | ||
func (m *Mitmproxy) Sidecar(deploymentName string) v1.Container { | ||
c := MitmproxySidecarContainer | ||
c.VolumeMounts[0].Name = kubetapConfigMapPrefix + deploymentName | ||
return c | ||
} | ||
|
||
// PatchDeployment provides any necessary tweaks to the deployment after the sidecar is added. | ||
func (m *Mitmproxy) PatchDeployment(deployment *k8sappsv1.Deployment) { | ||
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, v1.Volume{ | ||
Name: kubetapConfigMapPrefix + deployment.Name, | ||
VolumeSource: v1.VolumeSource{ | ||
ConfigMap: &v1.ConfigMapVolumeSource{ | ||
LocalObjectReference: v1.LocalObjectReference{ | ||
Name: kubetapConfigMapPrefix + deployment.Name, | ||
}, | ||
}, | ||
}, | ||
}) | ||
// add emptydir to resolve permission problems, and to down the road export dumps | ||
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, v1.Volume{ | ||
Name: mitmproxyDataVolName, | ||
VolumeSource: v1.VolumeSource{ | ||
EmptyDir: &v1.EmptyDirVolumeSource{}, | ||
}, | ||
}) | ||
} | ||
|
||
// Mitmproxy is a interactive web proxy for intercepting and modifying HTTP requests. | ||
type Mitmproxy struct { | ||
Protos []Protocol | ||
Client kubernetes.Interface | ||
ProxyOpts ProxyOptions | ||
} | ||
|
||
// NewMitmproxy initializes a new mitmproxy tap. | ||
func NewMitmproxy(c kubernetes.Interface, p ProxyOptions) Tap { | ||
// mitmproxy only supports one mode right now. | ||
// How we expose options for other modes may | ||
// be explored in the future. | ||
p.Mode = "reverse" | ||
return &Mitmproxy{ | ||
Protos: []Protocol{protocolHTTP}, | ||
Client: c, | ||
ProxyOpts: p, | ||
} | ||
} | ||
|
||
func (m *Mitmproxy) Protocols() []Protocol { | ||
return m.Protos | ||
} | ||
|
||
func (m *Mitmproxy) String() string { | ||
return "mitmproxy" | ||
} | ||
|
||
// ReadyEnv readies the environment by providing a configmap for the mitmproxy container. | ||
func (m *Mitmproxy) ReadyEnv() error { | ||
configmapsClient := m.Client.CoreV1().ConfigMaps(m.ProxyOpts.Namespace) | ||
// Create the ConfigMap based the options we're configuring mitmproxy with | ||
if err := createConfigMap(configmapsClient, m.ProxyOpts); err != nil { | ||
// If the service hasn't been tapped but still has a configmap from a previous | ||
// run (which can happen if the deployment borks and "tap off" isn't explicitly run, | ||
// delete the configmap and try again. | ||
// This is mostly here to fix development environments that become broken during | ||
// code testing. | ||
_ = destroyConfigMap(configmapsClient, m.ProxyOpts.Target) | ||
rErr := createConfigMap(configmapsClient, m.ProxyOpts) | ||
if rErr != nil { | ||
if errors.Is(os.ErrInvalid, rErr) { | ||
return fmt.Errorf("there was an unexpected problem creating the ConfigMap") | ||
} | ||
return rErr | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// UnreadyEnv removes tap supporting configmap. | ||
func (m *Mitmproxy) UnreadyEnv() error { | ||
configmapsClient := m.Client.CoreV1().ConfigMaps(m.ProxyOpts.Namespace) | ||
return destroyConfigMap(configmapsClient, m.ProxyOpts.Target) | ||
} | ||
|
||
func createConfigMap(configmapClient corev1.ConfigMapInterface, proxyOpts ProxyOptions) error { | ||
// TODO: eventually, we should build a struct and use yaml to marshal this, | ||
// but for now we're just doing string concatenation. | ||
var mitmproxyConfig []byte | ||
switch proxyOpts.Mode { | ||
case "reverse": | ||
if proxyOpts.UpstreamHTTPS { | ||
mitmproxyConfig = append([]byte(mitmproxyBaseConfig), []byte("mode: reverse:https://127.0.0.1:"+proxyOpts.UpstreamPort)...) | ||
} else { | ||
mitmproxyConfig = append([]byte(mitmproxyBaseConfig), []byte("mode: reverse:http://127.0.0.1:"+proxyOpts.UpstreamPort)...) | ||
} | ||
case "regular": | ||
// non-applicable | ||
return errors.New("mitmproxy container only supports \"reverse\" mode") | ||
case "socks5": | ||
// non-applicable | ||
return errors.New("mitmproxy container only supports \"reverse\" mode") | ||
case "upstream": | ||
// non-applicable, unless you really know what you're doing, in which case fork this and connect it to your existing proxy | ||
return errors.New("mitmproxy container only supports \"reverse\" mode") | ||
case "transparent": | ||
// Because transparent mode uses iptables, it's not supported as we cannot guarantee that iptables is available and functioning | ||
return errors.New("mitmproxy container only supports \"reverse\" mode") | ||
default: | ||
return errors.New("invalid proxy mode: \"" + proxyOpts.Mode + "\"") | ||
} | ||
cmData := make(map[string][]byte) | ||
cmData[mitmproxyConfigFile] = mitmproxyConfig | ||
cm := v1.ConfigMap{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: kubetapConfigMapPrefix + proxyOpts.Target, | ||
Namespace: proxyOpts.Namespace, | ||
Annotations: map[string]string{ | ||
annotationConfigMap: configMapAnnotationPrefix + proxyOpts.Target, | ||
}, | ||
}, | ||
BinaryData: cmData, | ||
} | ||
slen := len(cm.BinaryData[mitmproxyConfigFile]) | ||
if slen == 0 { | ||
return os.ErrInvalid | ||
} | ||
ccm, err := configmapClient.Create(context.TODO(), &cm, metav1.CreateOptions{}) | ||
if err != nil { | ||
return err | ||
} | ||
if ccm.BinaryData == nil { | ||
return os.ErrInvalid | ||
} | ||
cdata := ccm.BinaryData[mitmproxyConfigFile] | ||
if len(cdata) != slen { | ||
return ErrCreateResourceMismatch | ||
} | ||
return nil | ||
} | ||
|
||
func destroyConfigMap(configmapClient corev1.ConfigMapInterface, serviceName string) error { | ||
if serviceName == "" { | ||
return os.ErrInvalid | ||
} | ||
cms, err := configmapClient.List(context.TODO(), metav1.ListOptions{}) | ||
if err != nil { | ||
return fmt.Errorf("error getting ConfigMaps: %w", err) | ||
} | ||
var targetConfigMapNames []string | ||
for _, cm := range cms.Items { | ||
anns := cm.GetAnnotations() | ||
if anns == nil { | ||
continue | ||
} | ||
for k, v := range anns { | ||
if k == annotationConfigMap && v == configMapAnnotationPrefix+serviceName { | ||
targetConfigMapNames = append(targetConfigMapNames, cm.Name) | ||
} | ||
} | ||
} | ||
if len(targetConfigMapNames) == 0 { | ||
return ErrConfigMapNoMatch | ||
} | ||
return configmapClient.Delete(context.TODO(), targetConfigMapNames[0], metav1.DeleteOptions{}) | ||
} |
Oops, something went wrong.