Add routing IP rule matcher
Co-authored-by: Jean-Baptiste Doumenjou <925513+jbdoumenjou@users.noreply.github.com> Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
parent
e5024d5d0a
commit
679def0151
4 changed files with 146 additions and 6 deletions
|
@ -239,6 +239,7 @@ The table below lists all the available matchers:
|
||||||
| ```Path(`/path`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`, ...)``` | Match exact request path. It accepts a sequence of literal and regular expression paths. |
|
| ```Path(`/path`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`, ...)``` | Match exact request path. It accepts a sequence of literal and regular expression paths. |
|
||||||
| ```PathPrefix(`/products/`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`)``` | Match request prefix path. It accepts a sequence of literal and regular expression prefix paths. |
|
| ```PathPrefix(`/products/`, `/articles/{cat:[a-z]+}/{id:[0-9]+}`)``` | Match request prefix path. It accepts a sequence of literal and regular expression prefix paths. |
|
||||||
| ```Query(`foo=bar`, `bar=baz`)``` | Match Query String parameters. It accepts a sequence of key=value pairs. |
|
| ```Query(`foo=bar`, `bar=baz`)``` | Match Query String parameters. It accepts a sequence of key=value pairs. |
|
||||||
|
| ```ClientIP(`10.0.0.0/16`, `::1`)``` | Match if the request client IP is one of the given IP/CIDR. It accepts IPv4, IPv6 and CIDR formats. |
|
||||||
|
|
||||||
!!! important "Non-ASCII Domain Names"
|
!!! important "Non-ASCII Domain Names"
|
||||||
|
|
||||||
|
@ -272,6 +273,10 @@ The table below lists all the available matchers:
|
||||||
For instance, `PathPrefix: /products` would match `/products` but also `/products/shoes` and `/products/shirts`.
|
For instance, `PathPrefix: /products` would match `/products` but also `/products/shoes` and `/products/shirts`.
|
||||||
Since the path is forwarded as-is, your service is expected to listen on `/products`.
|
Since the path is forwarded as-is, your service is expected to listen on `/products`.
|
||||||
|
|
||||||
|
!!! info "ClientIP matcher"
|
||||||
|
|
||||||
|
The `ClientIP` matcher will only match the request client IP and does not use the `X-Forwarded-For` header for matching.
|
||||||
|
|
||||||
### Priority
|
### Priority
|
||||||
|
|
||||||
To avoid path overlap, routes are sorted, by default, in descending order using rules length. The priority is directly equal to the length of the rule, and so the longest length has the highest priority.
|
To avoid path overlap, routes are sorted, by default, in descending order using rules length. The priority is directly equal to the length of the rule, and so the longest length has the highest priority.
|
||||||
|
|
|
@ -24,14 +24,15 @@ func NewChecker(trustedIPs []string) (*Checker, error) {
|
||||||
for _, ipMask := range trustedIPs {
|
for _, ipMask := range trustedIPs {
|
||||||
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
|
||||||
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
checker.authorizedIPs = append(checker.authorizedIPs, &ipAddr)
|
||||||
} else {
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
_, ipAddr, err := net.ParseCIDR(ipMask)
|
_, ipAddr, err := net.ParseCIDR(ipMask)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
|
return nil, fmt.Errorf("parsing CIDR trusted IPs %s: %w", ipAddr, err)
|
||||||
}
|
}
|
||||||
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
|
checker.authorizedIPsNet = append(checker.authorizedIPsNet, ipAddr)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return checker, nil
|
return checker, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/traefik/traefik/v2/pkg/ip"
|
||||||
"github.com/traefik/traefik/v2/pkg/log"
|
"github.com/traefik/traefik/v2/pkg/log"
|
||||||
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
"github.com/traefik/traefik/v2/pkg/middlewares/requestdecorator"
|
||||||
"github.com/vulcand/predicate"
|
"github.com/vulcand/predicate"
|
||||||
|
@ -16,6 +17,7 @@ var funcs = map[string]func(*mux.Route, ...string) error{
|
||||||
"Host": host,
|
"Host": host,
|
||||||
"HostHeader": host,
|
"HostHeader": host,
|
||||||
"HostRegexp": hostRegexp,
|
"HostRegexp": hostRegexp,
|
||||||
|
"ClientIP": clientIP,
|
||||||
"Path": path,
|
"Path": path,
|
||||||
"PathPrefix": pathPrefix,
|
"PathPrefix": pathPrefix,
|
||||||
"Method": methods,
|
"Method": methods,
|
||||||
|
@ -155,6 +157,27 @@ func host(route *mux.Route, hosts ...string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clientIP(route *mux.Route, clientIPs ...string) error {
|
||||||
|
checker, err := ip.NewChecker(clientIPs)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not initialize IP Checker for \"ClientIP\" matcher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy := ip.RemoteAddrStrategy{}
|
||||||
|
|
||||||
|
route.MatcherFunc(func(req *http.Request, _ *mux.RouteMatch) bool {
|
||||||
|
ok, err := checker.Contains(strategy.GetIP(req))
|
||||||
|
if err != nil {
|
||||||
|
log.FromContext(req.Context()).Warnf("\"ClientIP\" matcher: could not match remote address : %w", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func hostRegexp(route *mux.Route, hosts ...string) error {
|
func hostRegexp(route *mux.Route, hosts ...string) error {
|
||||||
router := route.Subrouter()
|
router := route.Subrouter()
|
||||||
for _, host := range hosts {
|
for _, host := range hosts {
|
||||||
|
|
|
@ -17,6 +17,7 @@ func Test_addRoute(t *testing.T) {
|
||||||
desc string
|
desc string
|
||||||
rule string
|
rule string
|
||||||
headers map[string]string
|
headers map[string]string
|
||||||
|
remoteAddr string
|
||||||
expected map[string]int
|
expected map[string]int
|
||||||
expectedError bool
|
expectedError bool
|
||||||
}{
|
}{
|
||||||
|
@ -519,6 +520,112 @@ func Test_addRoute(t *testing.T) {
|
||||||
"http://plopi/a": http.StatusOK,
|
"http://plopi/a": http.StatusOK,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "ClientIP empty",
|
||||||
|
rule: "ClientIP(``)",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Invalid ClientIP",
|
||||||
|
rule: "ClientIP(`invalid`)",
|
||||||
|
expectedError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Non matching ClientIP",
|
||||||
|
rule: "ClientIP(`10.10.1.1`)",
|
||||||
|
remoteAddr: "10.0.0.0",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Non matching IPv6",
|
||||||
|
rule: "ClientIP(`10::10`)",
|
||||||
|
remoteAddr: "::1",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IP",
|
||||||
|
rule: "ClientIP(`10.0.0.0`)",
|
||||||
|
remoteAddr: "10.0.0.0:8456",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IPv6",
|
||||||
|
rule: "ClientIP(`10::10`)",
|
||||||
|
remoteAddr: "10::10",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IP among several IP",
|
||||||
|
rule: "ClientIP(`10.0.0.1`, `10.0.0.0`)",
|
||||||
|
remoteAddr: "10.0.0.0",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Non Matching IP with CIDR",
|
||||||
|
rule: "ClientIP(`11.0.0.0/24`)",
|
||||||
|
remoteAddr: "10.0.0.0",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Non Matching IPv6 with CIDR",
|
||||||
|
rule: "ClientIP(`11::/16`)",
|
||||||
|
remoteAddr: "10::",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IP with CIDR",
|
||||||
|
rule: "ClientIP(`10.0.0.0/16`)",
|
||||||
|
remoteAddr: "10.0.0.0",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IPv6 with CIDR",
|
||||||
|
rule: "ClientIP(`10::/16`)",
|
||||||
|
remoteAddr: "10::10",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IP among several CIDR",
|
||||||
|
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0/16`)",
|
||||||
|
remoteAddr: "10.0.0.0",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IP among non matching CIDR and matching IP",
|
||||||
|
rule: "ClientIP(`11.0.0.0/16`, `10.0.0.0`)",
|
||||||
|
remoteAddr: "10.0.0.0",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "Matching IP among matching CIDR and non matching IP",
|
||||||
|
rule: "ClientIP(`11.0.0.0`, `10.0.0.0/16`)",
|
||||||
|
remoteAddr: "10.0.0.0",
|
||||||
|
expected: map[string]int{
|
||||||
|
"http://tchouk/toto": http.StatusOK,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range testCases {
|
for _, test := range testCases {
|
||||||
|
@ -545,6 +652,10 @@ func Test_addRoute(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil)
|
req := testhelpers.MustNewRequest(http.MethodGet, calledURL, nil)
|
||||||
|
|
||||||
|
// Useful for the ClientIP matcher
|
||||||
|
req.RemoteAddr = test.remoteAddr
|
||||||
|
|
||||||
for key, value := range test.headers {
|
for key, value := range test.headers {
|
||||||
req.Header.Set(key, value)
|
req.Header.Set(key, value)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue