diff --git a/cmd/frpc/sub/http.go b/cmd/frpc/sub/http.go index 2e19fce4f67..824a5e329c7 100644 --- a/cmd/frpc/sub/http.go +++ b/cmd/frpc/sub/http.go @@ -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") @@ -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 { diff --git a/cmd/frpc/sub/root.go b/cmd/frpc/sub/root.go index 3bde61fd9f4..8e56fa7123c 100644 --- a/cmd/frpc/sub/root.go +++ b/cmd/frpc/sub/root.go @@ -64,6 +64,7 @@ var ( subDomain string httpUser string httpPwd string + ipsAllowList string locations string hostHeaderRewrite string role string diff --git a/go.mod b/go.mod index 0a92dfb5a65..da28f531bbc 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 433f03e281a..d13d6241945 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= @@ -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= @@ -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= diff --git a/pkg/config/proxy.go b/pkg/config/proxy.go index e168520d377..9275e649238 100644 --- a/pkg/config/proxy.go +++ b/pkg/config/proxy.go @@ -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"` @@ -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 } @@ -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 } diff --git a/pkg/msg/msg.go b/pkg/msg/msg.go index ba6dd63e450..4f1c8798adb 100644 --- a/pkg/msg/msg.go +++ b/pkg/msg/msg.go @@ -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"` diff --git a/pkg/util/vhost/http.go b/pkg/util/vhost/http.go index bfbd16ea9b6..b9555ba9879 100644 --- a/pkg/util/vhost/http.go +++ b/pkg/util/vhost/http.go @@ -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 } @@ -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) { @@ -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) diff --git a/pkg/util/vhost/router.go b/pkg/util/vhost/router.go index 768420a47e7..f9828689533 100644 --- a/pkg/util/vhost/router.go +++ b/pkg/util/vhost/router.go @@ -2,9 +2,12 @@ package vhost import ( "errors" + frpLog "github.com/fatedier/frp/pkg/util/log" "sort" "strings" "sync" + + "github.com/jpillora/ipfilter" ) var ( @@ -23,6 +26,7 @@ type Router struct { domain string location string httpUser string + ipFilter *ipfilter.IPFilter // store any object here payload interface{} @@ -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() @@ -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) diff --git a/pkg/util/vhost/vhost.go b/pkg/util/vhost/vhost.go index 2957cec4e78..4e0a23b62bc 100644 --- a/pkg/util/vhost/vhost.go +++ b/pkg/util/vhost/vhost.go @@ -84,6 +84,7 @@ type RouteConfig struct { Username string Password string Headers map[string]string + IpsAllowList []string RouteByHTTPUser string CreateConnFn CreateConnFunc @@ -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 } @@ -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 diff --git a/server/group/http.go b/server/group/http.go index fb00f0c7086..82c774c96dd 100644 --- a/server/group/http.go +++ b/server/group/http.go @@ -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 } diff --git a/server/proxy/http.go b/server/proxy/http.go index e0e2f23cb50..2e054349eae 100644 --- a/server/proxy/http.go +++ b/server/proxy/http.go @@ -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, } diff --git a/test/e2e/basic/http.go b/test/e2e/basic/http.go index 8fe73594958..d3b9bce2f85 100644 --- a/test/e2e/basic/http.go +++ b/test/e2e/basic/http.go @@ -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)) + }) })