package module
Version: v0.0.0-...-47094fb Latest Latest

This package is not in the latest version of its module.

Go to latest
Published: Mar 2, 2021 License: MIT Imports: 21 Imported by: 1



HTTP/HTTPS proxy over SSH.


  • Local machine: go get
  • Remote server: need our old friend sshd


Config file

Default path is $HOME/.config/mallory.json, can be set when start program

mallory -config path/to/config.json


  • id_rsa is the path to our private key file, can be generated by ssh-keygen
  • local_smart is the local address to serve HTTP proxy with smart detection of destination host
  • local_normal is similar to local_smart but send all traffic through remote SSH server without destination host detection
  • remote is the remote address of SSH server
  • blocked is a list of domains that need use proxy, any other domains will connect to their server directly
  "id_rsa": "$HOME/.ssh/id_rsa",
  "local_smart": ":1315",
  "local_normal": ":1316",
  "remote": "ssh://",
  "blocked": [

Blocked list in config file will be reloaded automatically when updated, and you can do it manually:

# send signal to reload
kill -USR2 <pid of mallory>

# or use reload command by sending http request
mallory -reload

System config

  • Set both HTTP and HTTPS proxy to localhost with port 1315 to use with block list
  • Set env var http_proxy and https_proxy to localhost:1316 for terminal usage

Get the right suffix name for a domain

mallory -suffix

A simple command to forward all traffic for the given port

# install it: go get

# all traffic through port 20022 will be forwarded to
forward -network tcp -listen :20022 -forward

# you can ssh to destination:22 through localhost:20022
ssh root@localhost -p 20022


  • return http error when unable to dial
  • add host to list automatically when unable to dial
  • support multiple remote servers

Docker container

Considering the following config file:

$ cat mallory.json
  "id_rsa": "/tmp/id_rsa",
  "local_smart": ":1315",
  "local_normal": ":1316",
  "remote": "ssh://bhenrion@"

You can run the container (zoobab/mallory) my mounting the config file, the SSH key, and mapping the 2 ports:

$ docker run -v $PWD/mallory.json:/root/.config/mallory.json -p 1316:1316 -p 1315:1315 -v $PWD/.ssh/id_rsa:/tmp/id_rsa zoobab/mallory
mallory: 2020/03/30 16:51:10 main.go:22: Starting...
mallory: 2020/03/30 16:51:10 main.go:23: PID: 1
mallory: 2020/03/30 16:51:10 config.go:103: Loading: /root/.config/mallory.json
mallory: 2020/03/30 16:51:10 main.go:30: Connecting remote SSH server: ssh://bhenrion@
mallory: 2020/03/30 16:51:10 main.go:38: Local normal HTTP proxy: :1316
mallory: 2020/03/30 16:51:10 main.go:48: Local smart HTTP proxy: :1315

My use case was to connect to a Kubernetes cluster (Openshift) installed behind an SSH bastion:

$ export http_proxy=http://localhost:1316
$ export https_proxy=https://localhost:1316
$ oc login
Authentication required for (openshift)
Username: bhenrion
Login successful.



Package mallory implements a simple http proxy support direct and GAE remote fetcher

Package singleflight provides a duplicate function call suppression mechanism.



View Source
const (
	SmartSrv = iota


View Source
var (
	ErrShouldProxy = errors.New("should proxy")

global logger


func BeautifyDuration

func BeautifyDuration(d time.Duration) string

Duration to e.g. 432ms or 12s, human readable translation

func BeautifySize

func BeautifySize(s int64) string

func CopyHeader

func CopyHeader(w http.ResponseWriter, r *http.Response)

copy and overwrite headers from r to w

func HostOnly

func HostOnly(addr string) string

HostOnly returns host if has port in addr, or addr if missing port

func RemoveHopHeaders

func RemoveHopHeaders(h http.Header)

func StatusText

func StatusText(c int) string

StatusText returns http status text looks like "200 OK"


type AccessType

type AccessType bool

func (AccessType) String

func (t AccessType) String() string

type Config

type Config struct {
	// file path
	Path string
	// config file content
	File *ConfigFile
	// File wather
	Watcher *fsnotify.Watcher
	// contains filtered or unexported fields

Provide global config for mallory

func NewConfig

func NewConfig(path string) (self *Config, err error)

func (*Config) Blocked

func (self *Config) Blocked(host string) bool

test whether host is in blocked list or not

func (*Config) Load

func (self *Config) Load() (err error)

reload config file

func (*Config) Reload

func (self *Config) Reload() (err error)

type ConfigFile

type ConfigFile struct {
	// private file file
	PrivateKey string `json:"id_rsa"`
	// local addr to listen and serve, default is
	LocalSmartServer string `json:"local_smart"`
	// local addr to listen and serve, default is
	LocalNormalServer string `json:"local_normal"`
	// remote addr to connect, e.g. ssh://
	RemoteServer string `json:"remote"`
	// direct to proxy dial timeout
	ShouldProxyTimeoutMS int `json:"should_proxy_timeout_ms"`
	// blocked host list
	BlockedList []string `json:"blocked"`

Memory representation for mallory.json

func NewConfigFile

func NewConfigFile(path string) (self *ConfigFile, err error)

Load file from path

func (*ConfigFile) Blocked

func (self *ConfigFile) Blocked(host string) bool

test whether host is in blocked list or not

type Direct

type Direct struct {
	Tr *http.Transport

Direct fetcher

func NewDirect

func NewDirect(shouldProxyTimeout time.Duration) *Direct

Create and initialize

func (*Direct) Connect

func (self *Direct) Connect(w http.ResponseWriter, r *http.Request) (err error)

Data flow:

1. Receive CONNECT request from the client
2. Dial the remote server(the one client want to conenct)
3. Send 200 OK to client if the connection is established
4. Exchange data between client and server

func (*Direct) ServeHTTP

func (self *Direct) ServeHTTP(w http.ResponseWriter, r *http.Request) (err error)

Data flow:

1. Receive request R1 from client
2. Re-post request R1 to remote server(the one client want to connect)
3. Receive response P1 from remote server
4. Send response P1 to client

type Group

type Group struct {
	// contains filtered or unexported fields

Group represents a class of work and forms a namespace in which units of work can be executed with duplicate suppression.

func (*Group) Do

func (g *Group) Do(key string, fn func() (interface{}, error)) (interface{}, error)

Do executes and returns the results of the given function, making sure that only one execution is in-flight for a given key at a time. If a duplicate comes in, the duplicate caller waits for the original to complete and receives the same results.

type SSH

type SSH struct {
	// global config file
	Cfg *Config
	// connect URL
	URL *url.URL
	// SSH client
	Client *ssh.Client
	// SSH client config
	CliCfg *ssh.ClientConfig
	// direct fetcher
	Direct *Direct
	// contains filtered or unexported fields

func NewSSH

func NewSSH(c *Config) (self *SSH, err error)

Create and initialize

func (*SSH) Connect

func (self *SSH) Connect(w http.ResponseWriter, r *http.Request)

func (*SSH) ServeHTTP

func (self *SSH) ServeHTTP(w http.ResponseWriter, r *http.Request)

type Server

type Server struct {
	// SmartSrv or NormalSrv
	Mode int
	// config file
	Cfg *Config
	// direct fetcher
	Direct *Direct
	// ssh fetcher, to connect remote proxy server
	// a cache
	BlockedHosts map[string]bool
	// contains filtered or unexported fields

func NewServer

func NewServer(mode int, c *Config) (self *Server, err error)

Create and intialize

func (*Server) Blocked

func (self *Server) Blocked(host string) bool

func (*Server) ServeHTTP

func (self *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP proxy accepts requests with following two types:

  Generally, this method is used when the client want to connect server with HTTPS.
  In fact, the client can do anything he want in this CONNECT way...
  The request is something like:
  Only has the host and port information, and the proxy should not do anything with
  the underlying data. What the proxy can do is just exchange data between client and server.
  After accepting this, the proxy should response
    HTTP/1.1 200 OK
  to the client if the connection to the remote server is established.
  Then client and server start to exchange data...

- non-CONNECT, such as GET, POST, ...
  In this case, the proxy should redo the method to the remote server.
  All of these methods should have the absolute URL that contains the host information.
  A GET request looks like:
    GET HTTP/1.1
  which is different from the normal http request:
    GET /justmao945/... HTTP/1.1
  Because we can be sure that all of them are http request, we can only redo the request
  to the remote server and copy the reponse to client.


Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
t or T : Toggle theme light dark auto
y or Y : Canonical URL