Skip to content

Commit

Permalink
Implement Host header bypassing (#155)
Browse files Browse the repository at this point in the history
* Initial implementation of keep-host argument

* Add keep-host parsing to the consulcatalog provider

* Update docs

* update from the current master

---------
by @ffix
  • Loading branch information
ffix authored Jan 25, 2024
1 parent 7d4394f commit fe24cf9
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 16 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ This default can be changed with labels:
- `reproxy.ping` - ping path for the destination container.
- `reproxy.remote` - restrict access to the route with a list of comma-separated subnets or ips
- `reproxy.assets` - set assets mapping as `web-root:location`, for example `reproxy.assets=/web:/var/www`
- `reproxy.keep-host` - keep host header as is (`yes`, `true`, `1`) or replace with destination host (`no`, `false`, `0`)
- `reproxy.enabled` - enable (`yes`, `true`, `1`) or disable (`no`, `false`, `0`) container from reproxy destinations.

Pls note: without `--docker.auto` the destination container has to have at least one of `reproxy.*` labels to be considered as a potential destination.
Expand Down Expand Up @@ -302,6 +303,7 @@ username1:bcrypt(password2)
username2:bcrypt(password2)
...
```
this can be generated with `htpasswd -nbB` command, i.e. `htpasswd -nbB test passwd`

## IP-based access control

Expand Down Expand Up @@ -367,7 +369,8 @@ This is the list of all options supporting multiple elements:
--basic-htpasswd= htpasswd file for basic auth [$BASIC_HTPASSWD]
--lb-type=[random|failover|roundrobin] load balancer type (default: random) [$LB_TYPE]
--signature enable reproxy signature headers [$SIGNATURE]
--remote-lookup-headers enable remote lookup headers [$REMOTE_LOOKUP_HEADERS]
--remote-lookup-headers enable remote lookup headers [$REMOTE_LOOKUP_HEADERS]
--keep-host keep original Host header as default when proxying [$KEEP_HOST]
--insecure skip SSL verification on destination host [$INSECURE]
--dbg debug mode [$DEBUG]

Expand Down
17 changes: 13 additions & 4 deletions app/discovery/discovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type URLMapper struct {
PingURL string
MatchType MatchType
RedirectType RedirectType
KeepHost *bool
OnlyFromIPs []string

AssetsLocation string // local FS root location
Expand Down Expand Up @@ -427,15 +428,23 @@ func (s *Service) extendMapper(m URLMapper) URLMapper {
return m
}

m.Dst = strings.TrimSuffix(m.Dst, "/") + "/$1"

res := URLMapper{
Server: m.Server,
Dst: strings.TrimSuffix(m.Dst, "/") + "/$1",
ProviderID: m.ProviderID,
PingURL: m.PingURL,
MatchType: m.MatchType,
AssetsWebRoot: m.AssetsWebRoot,
AssetsLocation: m.AssetsLocation,
AssetsSPA: m.AssetsSPA,
}
rx, err := regexp.Compile("^" + strings.TrimSuffix(src, "/") + "/(.*)")
if err != nil {
log.Printf("[WARN] can't extend %s, %v", m.SrcMatch.String(), err)
return m
}
m.SrcMatch = *rx
return m
res.SrcMatch = *rx
return res
}

// redirects process @code prefix and sets redirect type, i.e. "@302 /something"
Expand Down
16 changes: 15 additions & 1 deletion app/discovery/provider/consulcatalog/consulcatalog.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func (cc *ConsulCatalog) List() ([]discovery.URLMapper, error) {
destURL := fmt.Sprintf("http://%s:%d/$1", c.ServiceAddress, c.ServicePort)
pingURL := fmt.Sprintf("http://%s:%d/ping", c.ServiceAddress, c.ServicePort)
server := "*"
var keepHost *bool
onlyFrom := []string{}

if v, ok := c.Labels["reproxy.enabled"]; ok && (v == "true" || v == "yes" || v == "1") {
Expand Down Expand Up @@ -170,6 +171,19 @@ func (cc *ConsulCatalog) List() ([]discovery.URLMapper, error) {
pingURL = fmt.Sprintf("http://%s:%d%s", c.ServiceAddress, c.ServicePort, v)
}

if v, ok := c.Labels["reproxy.keep-host"]; ok {
enabled = true
if v == "true" || v == "yes" || v == "1" {
t := true
keepHost = &t
} else if v == "false" || v == "no" || v == "0" {
f := false
keepHost = &f
} else {
log.Printf("[WARN] invalid value for reproxy.keep-host: %s", v)
}
}

if !enabled {
log.Printf("[DEBUG] service %s disabled", c.ServiceID)
continue
Expand All @@ -183,7 +197,7 @@ func (cc *ConsulCatalog) List() ([]discovery.URLMapper, error) {
// server label may have multiple, comma separated servers
for _, srv := range strings.Split(server, ",") {
res = append(res, discovery.URLMapper{Server: strings.TrimSpace(srv), SrcMatch: *srcRegex, Dst: destURL,
OnlyFromIPs: onlyFrom, PingURL: pingURL, ProviderID: discovery.PIConsulCatalog})
PingURL: pingURL, ProviderID: discovery.PIConsulCatalog, KeepHost: keepHost, OnlyFromIPs: onlyFrom})
}
}

Expand Down
36 changes: 34 additions & 2 deletions app/discovery/provider/consulcatalog/consulcatalog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ func TestConsulCatalog_List(t *testing.T) {
ServicePort: 4000,
Labels: map[string]string{"reproxy.enabled": "1"},
},
{
ServiceID: "id5",
ServiceName: "name5",
ServiceAddress: "adr5",
ServicePort: 5000,
Labels: map[string]string{"reproxy.enabled": "true", "reproxy.keep-host": "true"},
},
{
ServiceID: "id6",
ServiceName: "name6",
ServiceAddress: "adr6",
ServicePort: 5001,
Labels: map[string]string{"reproxy.enabled": "true", "reproxy.keep-host": "false"},
},
}, nil
}}

Expand All @@ -83,36 +97,54 @@ func TestConsulCatalog_List(t *testing.T) {

res, err := cc.List()
require.NoError(t, err)
require.Equal(t, 4, len(res))
require.Equal(t, 6, len(res))

// sort slice for exclude random item positions after sorting by SrtMatch in List function
sort.Slice(res, func(i, j int) bool {
return len(res[i].Dst+res[i].Server) > len(res[j].Dst+res[j].Server)
})

assert.Equal(t, "^/api/123/(.*)", res[0].SrcMatch.String())
assert.Equal(t, "http://addr3:3000/blah/$1", res[0].Dst)
assert.Equal(t, "example.com", res[0].Server)
assert.Equal(t, "http://addr3:3000/ping", res[0].PingURL)
assert.Equal(t, (*bool)(nil), res[0].KeepHost)
assert.Equal(t, []string{"127.0.0.1", "192.168.1.0/24"}, res[0].OnlyFromIPs)

assert.Equal(t, "^/api/123/(.*)", res[1].SrcMatch.String())
assert.Equal(t, "http://addr3:3000/blah/$1", res[1].Dst)
assert.Equal(t, "domain.com", res[1].Server)
assert.Equal(t, "http://addr3:3000/ping", res[1].PingURL)
assert.Equal(t, (*bool)(nil), res[1].KeepHost)
assert.Equal(t, []string{"127.0.0.1", "192.168.1.0/24"}, res[1].OnlyFromIPs)

assert.Equal(t, "^/(.*)", res[2].SrcMatch.String())
assert.Equal(t, "http://addr44:4000/$1", res[2].Dst)
assert.Equal(t, "http://addr44:4000/ping", res[2].PingURL)
assert.Equal(t, "*", res[2].Server)
assert.Equal(t, (*bool)(nil), res[2].KeepHost)
assert.Equal(t, []string{}, res[2].OnlyFromIPs)

assert.Equal(t, "^/(.*)", res[3].SrcMatch.String())
assert.Equal(t, "http://addr2:2000/$1", res[3].Dst)
assert.Equal(t, "http://addr2:2000/ping", res[3].PingURL)
assert.Equal(t, "*", res[3].Server)
assert.Equal(t, (*bool)(nil), res[3].KeepHost)
assert.Equal(t, []string{}, res[3].OnlyFromIPs)

tr := true
assert.Equal(t, "^/(.*)", res[4].SrcMatch.String())
assert.Equal(t, "http://adr5:5000/$1", res[4].Dst)
assert.Equal(t, "http://adr5:5000/ping", res[4].PingURL)
assert.Equal(t, "*", res[4].Server)
assert.Equal(t, &tr, res[4].KeepHost)

fa := false
assert.Equal(t, "^/(.*)", res[5].SrcMatch.String())
assert.Equal(t, "http://adr6:5001/$1", res[5].Dst)
assert.Equal(t, "http://adr6:5001/ping", res[5].PingURL)
assert.Equal(t, "*", res[5].Server)
assert.Equal(t, &fa, res[5].KeepHost)

}

func TestConsulCatalog_serviceListWasChanged(t *testing.T) {
Expand Down
25 changes: 24 additions & 1 deletion app/discovery/provider/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func (d *Docker) parseContainerInfo(c containerInfo) (res []discovery.URLMapper)
enabled = true
}

keepHost := d.getKeepHostValue(c.Labels, n)

if !enabled {
continue
}
Expand All @@ -176,7 +178,8 @@ func (d *Docker) parseContainerInfo(c containerInfo) (res []discovery.URLMapper)
// docker server label may have multiple, comma separated servers
for _, srv := range strings.Split(server, ",") {
mp := discovery.URLMapper{Server: strings.TrimSpace(srv), SrcMatch: *srcRegex, Dst: destURL,
PingURL: pingURL, OnlyFromIPs: onlyFrom, ProviderID: discovery.PIDocker, MatchType: discovery.MTProxy}
PingURL: pingURL, ProviderID: discovery.PIDocker, MatchType: discovery.MTProxy,
KeepHost: keepHost, OnlyFromIPs: onlyFrom}

// for assets we add the second proxy mapping only if explicitly requested
if assetsWebRoot != "" && explicit {
Expand Down Expand Up @@ -437,3 +440,23 @@ func (d *dockerClient) ListContainers() ([]containerInfo, error) {

return containers, nil
}

func (d *Docker) getKeepHostValue(labels map[string]string, n int) *bool {
v, ok := d.labelN(labels, n, "keep-host")
if !ok {
return nil
}

if v == "true" || v == "yes" || v == "y" || v == "1" {
k := true
return &k
}

if v == "false" || v == "no" || v == "n" || v == "0" {
k := false
return &k
}

log.Printf("[WARN] keep-host label value %s is not valid, ignoring", v)
return nil
}
17 changes: 16 additions & 1 deletion app/discovery/provider/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,19 +137,28 @@ func TestDocker_ListMulti(t *testing.T) {
Name: "c5", State: "running", IP: "127.0.0.122", Ports: []int{2345}, // not enabled
Labels: map[string]string{"reproxy.enabled": "false"},
},
{
Name: "c6", State: "running", IP: "127.0.0.3", Ports: []int{12346},
Labels: map[string]string{"reproxy.enabled": "y", "reproxy.keep-host": "y", "reproxy.route": "^/ky/"},
},
{
Name: "c7", State: "running", IP: "127.0.0.3", Ports: []int{12346},
Labels: map[string]string{"reproxy.enabled": "y", "reproxy.keep-host": "n", "reproxy.route": "^/kn/"},
},
}, nil
},
}

d := Docker{DockerClient: dclient}
res, err := d.List()
require.NoError(t, err)
require.Equal(t, 6, len(res))
require.Equal(t, 8, len(res))

assert.Equal(t, "^/api/123/(.*)", res[0].SrcMatch.String())
assert.Equal(t, "http://127.0.0.2:12345/blah/$1", res[0].Dst)
assert.Equal(t, "example.com", res[0].Server)
assert.Equal(t, "http://127.0.0.2:12345/ping", res[0].PingURL)
assert.Nil(t, res[0].KeepHost)

assert.Equal(t, "/api/1/(.*)", res[1].SrcMatch.String())
assert.Equal(t, "http://127.0.0.3:7890/blah/1/$1", res[1].Dst)
Expand All @@ -175,6 +184,12 @@ func TestDocker_ListMulti(t *testing.T) {
assert.Equal(t, "http://127.0.0.2:12348/a/$1", res[5].Dst)
assert.Equal(t, "http://127.0.0.2:12348/ping", res[5].PingURL)
assert.Equal(t, "example.com", res[5].Server)

assert.Equal(t, "^/ky/", res[6].SrcMatch.String())
assert.Equal(t, true, *res[6].KeepHost)

assert.Equal(t, "^/kn/", res[7].SrcMatch.String())
assert.Equal(t, false, *res[7].KeepHost)
}

func TestDocker_ListMultiFallBack(t *testing.T) {
Expand Down
2 changes: 2 additions & 0 deletions app/discovery/provider/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func (d *File) List() (res []discovery.URLMapper, err error) {
Ping string `yaml:"ping"`
AssetsEnabled bool `yaml:"assets"`
AssetsSPA bool `yaml:"spa"`
KeepHost *bool `yaml:"keep-host,omitempty"`
OnlyFrom string `yaml:"remote"`
}
fh, err := os.Open(d.FileName)
Expand Down Expand Up @@ -111,6 +112,7 @@ func (d *File) List() (res []discovery.URLMapper, err error) {
SrcMatch: *rx,
Dst: f.Dest,
PingURL: f.Ping,
KeepHost: f.KeepHost,
ProviderID: discovery.PIFile,
MatchType: discovery.MTProxy,
OnlyFromIPs: discovery.ParseOnlyFrom(f.OnlyFrom),
Expand Down
11 changes: 8 additions & 3 deletions app/discovery/provider/file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,24 @@ func TestFile_List(t *testing.T) {
assert.Equal(t, "", res[0].PingURL)
assert.Equal(t, "srv.example.com", res[0].Server)
assert.Equal(t, discovery.MTProxy, res[0].MatchType)
assert.Nil(t, res[0].KeepHost)
assert.Equal(t, []string{}, res[0].OnlyFromIPs)

assert.Equal(t, "^/api/svc1/(.*)", res[1].SrcMatch.String())
assert.Equal(t, "http://127.0.0.1:8080/blah1/$1", res[1].Dst)
assert.Equal(t, "", res[1].PingURL)
assert.Equal(t, "*", res[1].Server)
assert.Equal(t, discovery.MTProxy, res[1].MatchType)
assert.Equal(t, []string{}, res[0].OnlyFromIPs)
assert.Nil(t, res[1].KeepHost)
assert.Equal(t, []string{}, res[1].OnlyFromIPs)

assert.Equal(t, "/api/svc3/xyz", res[2].SrcMatch.String())
assert.Equal(t, "http://127.0.0.3:8080/blah3/xyz", res[2].Dst)
assert.Equal(t, "http://127.0.0.3:8080/ping", res[2].PingURL)
assert.Equal(t, "*", res[2].Server)
assert.Equal(t, discovery.MTProxy, res[2].MatchType)
assert.Equal(t, []string{}, res[0].OnlyFromIPs)
assert.Nil(t, res[2].KeepHost)
assert.Equal(t, []string{}, res[2].OnlyFromIPs)

assert.Equal(t, "/web/", res[3].SrcMatch.String())
assert.Equal(t, "/var/web", res[3].Dst)
Expand All @@ -136,12 +139,14 @@ func TestFile_List(t *testing.T) {
assert.Equal(t, discovery.MTStatic, res[3].MatchType)
assert.Equal(t, false, res[3].AssetsSPA)
assert.Equal(t, []string{"192.168.1.0/24", "124.0.0.1"}, res[3].OnlyFromIPs)
assert.Equal(t, true, *res[3].KeepHost)

assert.Equal(t, "/web2/", res[4].SrcMatch.String())
assert.Equal(t, "/var/web2", res[4].Dst)
assert.Equal(t, "", res[4].PingURL)
assert.Equal(t, "*", res[4].Server)
assert.Equal(t, discovery.MTStatic, res[4].MatchType)
assert.Equal(t, true, res[4].AssetsSPA)
assert.Equal(t, []string{}, res[0].OnlyFromIPs)
assert.Equal(t, []string{}, res[4].OnlyFromIPs)
assert.Equal(t, false, *res[4].KeepHost)
}
4 changes: 2 additions & 2 deletions app/discovery/provider/testdata/config.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
default:
- {route: "^/api/svc1/(.*)", dest: "http://127.0.0.1:8080/blah1/$1"}
- {route: "/api/svc3/xyz", dest: "http://127.0.0.3:8080/blah3/xyz", "ping": "http://127.0.0.3:8080/ping"}
- {route: "/web/", dest: "/var/web", "assets": yes, "remote": "192.168.1.0/24, 124.0.0.1"}
- {route: "/web2/", dest: "/var/web2", "spa": yes}
- {route: "/web/", dest: "/var/web", "assets": yes, "keep-host": yes, "remote": "192.168.1.0/24, 124.0.0.1"}
- {route: "/web2/", dest: "/var/web2", "spa": yes, "keep-host": no}
srv.example.com:
- {route: "^/api/svc2/(.*)", dest: "http://127.0.0.2:8080/blah2/$1/abc"}
2 changes: 2 additions & 0 deletions app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ var opts struct {
RemoteLookupHeaders bool `long:"remote-lookup-headers" env:"REMOTE_LOOKUP_HEADERS" description:"enable remote lookup headers"`
LBType string `long:"lb-type" env:"LB_TYPE" description:"load balancer type" choice:"random" choice:"failover" choice:"roundrobin" default:"random"` // nolint
Insecure bool `long:"insecure" env:"INSECURE" description:"skip SSL certificate verification for the destination host"`
KeepHost bool `long:"keep-host" env:"KEEP_HOST" description:"pass the Host header from the client as-is, instead of rewriting it"`

SSL struct {
Type string `long:"type" env:"TYPE" description:"ssl (auto) support" choice:"none" choice:"static" choice:"auto" default:"none"` // nolint
Expand Down Expand Up @@ -274,6 +275,7 @@ func run() error {
ThrottleUser: opts.Throttle.User,
BasicAuthEnabled: len(basicAuthAllowed) > 0,
BasicAuthAllowed: basicAuthAllowed,
KeepHost: opts.KeepHost,
OnlyFrom: makeOnlyFromMiddleware(),
}

Expand Down
16 changes: 15 additions & 1 deletion app/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ type Http struct { // nolint golint

ThrottleSystem int
ThrottleUser int

KeepHost bool
}

// Matcher source info (server and route) to the destination url
Expand Down Expand Up @@ -199,6 +201,7 @@ const (
ctxURL = contextKey("url")
ctxMatchType = contextKey("type")
ctxMatch = contextKey("match")
ctxKeepHost = contextKey("keepHost")
)

func (h *Http) proxyHandler() http.HandlerFunc {
Expand All @@ -207,11 +210,15 @@ func (h *Http) proxyHandler() http.HandlerFunc {
Director: func(r *http.Request) {
ctx := r.Context()
uu := ctx.Value(ctxURL).(*url.URL)
keepHost := ctx.Value(ctxKeepHost).(bool)
r.Header.Add("X-Forwarded-Host", r.Host)
r.URL.Path = uu.Path
r.URL.Host = uu.Host
r.URL.Scheme = uu.Scheme
r.Host = uu.Host
log.Printf("[DEBUG] keep host is %t", keepHost)
if !keepHost {
r.Host = uu.Host
}
h.setXRealIP(r)
},
Transport: &http.Transport{
Expand Down Expand Up @@ -325,6 +332,13 @@ func (h *Http) matchHandler(next http.Handler) http.Handler {
return
}
ctx = context.WithValue(ctx, ctxURL, uu) // set destination url in request's context
var keepHost bool
if match.Mapper.KeepHost == nil {
keepHost = h.KeepHost
} else {
keepHost = *match.Mapper.KeepHost
}
ctx = context.WithValue(ctx, ctxKeepHost, keepHost) // set keep host in request's context
}
r = r.WithContext(ctx)
}
Expand Down

0 comments on commit fe24cf9

Please sign in to comment.