Skip to content

Commit

Permalink
Introduce ip allow list support for IPv4/IPv6 (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
ziv-codefresh authored Oct 9, 2022
1 parent 2384484 commit b0811fd
Show file tree
Hide file tree
Showing 12 changed files with 140 additions and 8 deletions.
4 changes: 4 additions & 0 deletions cmd/frpc/sub/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func init() {
httpCmd.PersistentFlags().StringVarP(&locations, "locations", "", "", "locations")
httpCmd.PersistentFlags().StringVarP(&httpUser, "http_user", "", "", "http auth user")
httpCmd.PersistentFlags().StringVarP(&httpPwd, "http_pwd", "", "", "http auth password")
httpCmd.PersistentFlags().StringVarP(&ipsAllowList, "ips_allow_list", "", "", "lists the rules to configure which IP addresses and subnet masks can access your client (e.g \"192.168.0.0/16, 255.255.0.0\")- IPv4/IPv6 support")
httpCmd.PersistentFlags().StringVarP(&hostHeaderRewrite, "host_header_rewrite", "", "", "host header rewrite")
httpCmd.PersistentFlags().BoolVarP(&useEncryption, "ue", "", false, "use encryption")
httpCmd.PersistentFlags().BoolVarP(&useCompression, "uc", "", false, "use compression")
Expand Down Expand Up @@ -70,6 +71,9 @@ var httpCmd = &cobra.Command{
cfg.HostHeaderRewrite = hostHeaderRewrite
cfg.UseEncryption = useEncryption
cfg.UseCompression = useCompression
if ipsAllowList != "" {
cfg.IpsAllowList = strings.Split(ipsAllowList, ",")
}

err = cfg.CheckForCli()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions cmd/frpc/sub/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ var (
subDomain string
httpUser string
httpPwd string
ipsAllowList string
locations string
hostHeaderRewrite string
role string
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.4.2
github.com/hashicorp/yamux v0.0.0-20210707203944-259a57b3608c
github.com/jpillora/ipfilter v1.2.7
github.com/leodido/go-urn v1.2.1 // indirect
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.13.0
Expand All @@ -21,7 +22,7 @@ require (
github.com/prometheus/client_golang v1.11.0
github.com/rodaine/table v1.0.1
github.com/spf13/cobra v1.1.3
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.8.0
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect
Expand Down
15 changes: 12 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/jpillora/ipfilter v1.2.7 h1:fB+fIa/VtgjOrHjkR3Sw47dHYhZGCae/dIWc/Vur++U=
github.com/jpillora/ipfilter v1.2.7/go.mod h1:QS0miOgSqkxAsnTKLADlahASDOExe2K2pdoswGRt+FM=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
Expand Down Expand Up @@ -300,6 +302,8 @@ github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je4
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/phuslu/iploc v1.0.20220730 h1:Ly2Casvb9LVnaDg06RfkET6AwkMCUXrNANKJX40vsoE=
github.com/phuslu/iploc v1.0.20220730/go.mod h1:gsgExGWldwv1AEzZm+Ki9/vGfyjkL33pbSr9HGpt2Xg=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down Expand Up @@ -366,13 +370,15 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
Expand All @@ -381,6 +387,8 @@ github.com/templexxx/xor v0.0.0-20191217153810-f85b25db303b/go.mod h1:5XA7W9S6mn
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
Expand Down Expand Up @@ -665,8 +673,9 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
3 changes: 3 additions & 0 deletions pkg/config/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ type HTTPProxyConf struct {
Locations []string `ini:"locations" json:"locations"`
HTTPUser string `ini:"http_user" json:"http_user"`
HTTPPwd string `ini:"http_pwd" json:"http_pwd"`
IpsAllowList []string `ini:"ips_allow_list" json:"ips_allow_list"`
HostHeaderRewrite string `ini:"host_header_rewrite" json:"host_header_rewrite"`
Headers map[string]string `ini:"-" json:"headers"`
RouteByHTTPUser string `ini:"route_by_http_user" json:"route_by_http_user"`
Expand Down Expand Up @@ -760,6 +761,7 @@ func (cfg *HTTPProxyConf) UnmarshalFromMsg(pMsg *msg.NewProxy) {
cfg.HostHeaderRewrite = pMsg.HostHeaderRewrite
cfg.HTTPUser = pMsg.HTTPUser
cfg.HTTPPwd = pMsg.HTTPPwd
cfg.IpsAllowList = pMsg.IpsAllowList
cfg.Headers = pMsg.Headers
cfg.RouteByHTTPUser = pMsg.RouteByHTTPUser
}
Expand All @@ -774,6 +776,7 @@ func (cfg *HTTPProxyConf) MarshalToMsg(pMsg *msg.NewProxy) {
pMsg.HostHeaderRewrite = cfg.HostHeaderRewrite
pMsg.HTTPUser = cfg.HTTPUser
pMsg.HTTPPwd = cfg.HTTPPwd
pMsg.IpsAllowList = cfg.IpsAllowList
pMsg.Headers = cfg.Headers
pMsg.RouteByHTTPUser = cfg.RouteByHTTPUser
}
Expand Down
1 change: 1 addition & 0 deletions pkg/msg/msg.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ type NewProxy struct {
Locations []string `json:"locations,omitempty"`
HTTPUser string `json:"http_user,omitempty"`
HTTPPwd string `json:"http_pwd,omitempty"`
IpsAllowList []string `json:"ips_allow_list,omitempty"`
HostHeaderRewrite string `json:"host_header_rewrite,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
RouteByHTTPUser string `json:"route_by_http_user,omitempty"`
Expand Down
33 changes: 32 additions & 1 deletion pkg/util/vhost/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func NewHTTPReverseProxy(option HTTPReverseProxyOptions, vhostRouter *Routers) *
// Register register the route config to reverse proxy
// reverse proxy will use CreateConnFn from routeCfg to create a connection to the remote service
func (rp *HTTPReverseProxy) Register(routeCfg RouteConfig) error {
err := rp.vhostRouter.Add(routeCfg.Domain, routeCfg.Location, routeCfg.RouteByHTTPUser, &routeCfg)
err := rp.vhostRouter.Add(routeCfg.Domain, routeCfg.Location, routeCfg.RouteByHTTPUser, routeCfg.IpsAllowList, &routeCfg)
if err != nil {
return err
}
Expand Down Expand Up @@ -185,6 +185,26 @@ func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, p
return true
}

// CheckClientOriginIpAddr to prevent IP spoofing, be sure to delete any pre-existing X-Forwarded-For header coming from the client or an untrusted proxy.
func (rp *HTTPReverseProxy) CheckClientOriginIpAddr(domain, location, routeByHTTPUser, addr string) bool {
if addr != "" {
frpLog.Debug("Received client ip addr: %s", addr)
ips := strings.Split(addr, ", ")
if len(ips) > 1 {
// Selecting the first ip in the list, it's safe to take it once we ensured the first ip cannot be set by untrusted proxies or the client
addr = ips[0]
}
}
vr, ok := rp.getVhost(domain, location, routeByHTTPUser)
if ok {
if vr.ipFilter != nil {
frpLog.Debug("validating client origin ip %s", addr)
return vr.ipFilter.Allowed(addr)
}
}
return true
}

// getVhost trys to get vhost router by route policy.
func (rp *HTTPReverseProxy) getVhost(domain, location, routeByHTTPUser string) (*Router, bool) {
findRouter := func(inDomain, inLocation, inRouteByHTTPUser string) (*Router, bool) {
Expand Down Expand Up @@ -293,6 +313,17 @@ func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request)
return
}

// Identifying the originating IP address of a client connecting to a web server through a proxy server
addr := req.Header.Get("X-Forwarded-For")
if addr == "" {
// For server direct access, remote address is in "IP:port" format
addr, _, _ = net.SplitHostPort(req.RemoteAddr)
}
if !rp.CheckClientOriginIpAddr(domain, location, user, addr) {
http.Error(rw, http.StatusText(http.StatusForbidden), http.StatusForbidden)
return
}

newreq := rp.injectRequestInfoToCtx(req)
if req.Method == http.MethodConnect {
rp.connectHandler(rw, newreq)
Expand Down
16 changes: 15 additions & 1 deletion pkg/util/vhost/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package vhost

import (
"errors"
frpLog "github.com/fatedier/frp/pkg/util/log"
"sort"
"strings"
"sync"

"github.com/jpillora/ipfilter"
)

var (
Expand All @@ -23,6 +26,7 @@ type Router struct {
domain string
location string
httpUser string
ipFilter *ipfilter.IPFilter

// store any object here
payload interface{}
Expand All @@ -34,7 +38,7 @@ func NewRouters() *Routers {
}
}

func (r *Routers) Add(domain, location, httpUser string, payload interface{}) error {
func (r *Routers) Add(domain, location, httpUser string, ipsAllowList []string, payload interface{}) error {
r.mutex.Lock()
defer r.mutex.Unlock()

Expand All @@ -51,10 +55,20 @@ func (r *Routers) Add(domain, location, httpUser string, payload interface{}) er
vrs = make([]*Router, 0, 1)
}

var ipFilter *ipfilter.IPFilter
frpLog.Debug("adding allow list %s", ipsAllowList)
if ipsAllowList != nil {
ipFilter = ipfilter.New(ipfilter.Options{
AllowedIPs: ipsAllowList,
BlockByDefault: true,
})
}

vr := &Router{
domain: domain,
location: location,
httpUser: httpUser,
ipFilter: ipFilter,
payload: payload,
}
vrs = append(vrs, vr)
Expand Down
5 changes: 4 additions & 1 deletion pkg/util/vhost/vhost.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type RouteConfig struct {
Username string
Password string
Headers map[string]string
IpsAllowList []string
RouteByHTTPUser string

CreateConnFn CreateConnFunc
Expand All @@ -98,12 +99,13 @@ func (v *Muxer) Listen(ctx context.Context, cfg *RouteConfig) (l *Listener, err
routeByHTTPUser: cfg.RouteByHTTPUser,
rewriteHost: cfg.RewriteHost,
userName: cfg.Username,
ipsAllowList: cfg.IpsAllowList,
passWord: cfg.Password,
mux: v,
accept: make(chan net.Conn),
ctx: ctx,
}
err = v.registryRouter.Add(cfg.Domain, cfg.Location, cfg.RouteByHTTPUser, l)
err = v.registryRouter.Add(cfg.Domain, cfg.Location, cfg.RouteByHTTPUser, cfg.IpsAllowList, l)
if err != nil {
return
}
Expand Down Expand Up @@ -234,6 +236,7 @@ type Listener struct {
rewriteHost string
userName string
passWord string
ipsAllowList []string
mux *Muxer // for closing Muxer
accept chan net.Conn
ctx context.Context
Expand Down
2 changes: 1 addition & 1 deletion server/group/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (g *HTTPGroup) Register(
// the first proxy in this group
tmp := routeConfig // copy object
tmp.CreateConnFn = g.createConn
err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, routeConfig.RouteByHTTPUser, &tmp)
err = g.ctl.vhostRouter.Add(routeConfig.Domain, routeConfig.Location, routeConfig.RouteByHTTPUser, routeConfig.IpsAllowList, &tmp)
if err != nil {
return
}
Expand Down
1 change: 1 addition & 0 deletions server/proxy/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (pxy *HTTPProxy) Run() (remoteAddr string, err error) {
Headers: pxy.cfg.Headers,
Username: pxy.cfg.HTTPUser,
Password: pxy.cfg.HTTPPwd,
IpsAllowList: pxy.cfg.IpsAllowList,
CreateConnFn: pxy.GetRealConn,
}

Expand Down
64 changes: 64 additions & 0 deletions test/e2e/basic/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,4 +373,68 @@ var _ = Describe("[Feature: HTTP]", func() {
framework.ExpectNoError(err)
framework.ExpectEqualValues(consts.TestString, string(msg))
})

It("Ip allow list", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
serverConf += `
subdomain_host = example.com
`

fooPort := f.AllocPort()
f.RunServer("", newHTTPServer(fooPort, "foo"))

barPort := f.AllocPort()
f.RunServer("", newHTTPServer(barPort, "bar"))

bazPort := f.AllocPort()
f.RunServer("", newHTTPServer(bazPort, "baz"))

clientConf := consts.DefaultClientConfig
clientConf += fmt.Sprintf(`
[foo]
type = http
local_port = %d
subdomain = foo
ips_allow_list = ""
[bar]
type = http
local_port = %d
subdomain = bar
ips_allow_list = "127.0.0.1/16"
[baz]
type = http
local_port = %d
subdomain = baz
ips_allow_list = "127.1.0.1/16"
`, fooPort, barPort, bazPort)

f.RunProcesses([]string{serverConf}, []string{clientConf})

// The request should pass in case in allow list is empty string
framework.NewRequestExpect(f).Explain("foo subdomain").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("foo.example.com")
}).
ExpectResp([]byte("foo")).
Ensure()

// The request should pass and return the expected response in case the ip match to the provided allow list
framework.NewRequestExpect(f).Explain("bar subdomain").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("bar.example.com")
}).
ExpectResp([]byte("bar")).
Ensure()

// The request should fail with 403 status code due to invalid ip
framework.NewRequestExpect(f).Explain("baz subdomain").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("baz.example.com")
}).
Ensure(framework.ExpectResponseCode(403))
})
})

0 comments on commit b0811fd

Please sign in to comment.