feat: add ip-restriction wasm-go plugin (#759)

This commit is contained in:
renz7
2024-03-12 16:25:44 +08:00
committed by GitHub
parent cc6043de15
commit 3128df9abd
10 changed files with 689 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
# 功能说明
`ip-restriction `插件可以通过将 IP 地址列入白名单或黑名单来限制对服务或路由的访问.支持对单个 IP 地址、多个 IP 地址和类似
10.10.10.0/24 的 CIDR范围的限制.
# 配置说明
| 配置项 | 类型 | 必填 | 默认值 | 说明 |
|----------------|--------|----|-----------------------------|------------------------------------------|
| ip_source_type | string | 否 | origin-source | 可选值1. 对端socket ip`origin-source`; 2. 通过header获取`header` |
| ip_header_name | string | 否 | x-forwarded-for | 当`ip_source_type``header`指定自定义IP来源头 |
| allow | array | 否 | [] | 白名单列表 |
| deny | array | 否 | [] | 黑名单列表 |
| status | int | 否 | 403 | 拒绝访问时的 HTTP 状态码 |
| message | string | 否 | Your IP address is blocked. | 拒绝访问时的返回信息 |
```yaml
ip_source_type: origin-source
allow:
- 10.0.0.1
- 192.168.0.0/16
```
```yaml
ip_source_type: header
ip_header_name: x-real-iP
deny:
- 10.0.0.1
- 192.169.0.0/16
```

View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,22 @@
module github.com/alibaba/higress/plugins/wasm-go/extensions/ip-restriction
go 1.19
replace github.com/alibaba/higress/plugins/wasm-go => ../..
require (
github.com/alibaba/higress/plugins/wasm-go v0.0.0
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a
github.com/tidwall/gjson v1.14.3
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837
)
require (
github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 // indirect
github.com/magefile/mage v1.14.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/tidwall/resp v0.1.1 // indirect
)

View File

@@ -0,0 +1,24 @@
github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 h1:Wi5Tgn8K+jDcBYL+dIMS1+qXYH2r7tpRAyBgqrWfQtw=
github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56/go.mod h1:8BhOLuqtSuT5NZtZMwfvEibi09RO3u79uqfHZzfDTR4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520 h1:IHDghbGQ2DTIXHBHxWfqCYQW1fKjyJ/I7W1pMyUDeEA=
github.com/higress-group/nottinygc v0.0.0-20231101025119-e93c4c2f8520/go.mod h1:Nz8ORLaFiLWotg6GeKlJMhv8cci8mM43uEnLA5t8iew=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a h1:luYRvxLTE1xYxrXYj7nmjd1U0HHh8pUPiKfdZ0MhCGE=
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240226064518-b3dc4646a35a/go.mod h1:hNFjhrLUIq+kJ9bOcs8QtiplSQ61GZXtd2xHKx4BYRo=
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/resp v0.1.1 h1:Ly20wkhqKTmDUPlyM1S7pWo5kk0tDu8OoC/vFArXmwE=
github.com/tidwall/resp v0.1.1/go.mod h1:3/FrruOBAxPTPtundW0VXgmsQ4ZBA0Aw714lVYgwFa0=
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837 h1:DjHnADS2r2zynZ3WkCFAQ+PNYngMSNceRROi0pO6c3M=
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837/go.mod h1:9vp0bxqozzQwcjBwenEXfKVq8+mYbwHkQ1NF9Ap0DMw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -0,0 +1,157 @@
package main
import (
"encoding/json"
"fmt"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
"github.com/zmap/go-iptree/iptree"
"net"
"strings"
)
const (
DefaultRealIpHeader string = "X-Forwarded-For"
DefaultDenyStatus uint32 = 403
DefaultDenyMessage string = "Your IP address is blocked."
)
const (
OriginSourceType = "origin-source"
HeaderSourceType = "header"
)
type RestrictionConfig struct {
IPSourceType string `json:"ip_source_type"` //IP来源类型
IPHeaderName string `json:"ip_header_name"` //真实IP头
Allow *iptree.IPTree `json:"allow"` //允许的IP
Deny *iptree.IPTree `json:"deny"` //拒绝的IP
Status uint32 `json:"status"` //被拒绝时返回的状态码
Message string `json:"message"` //被拒绝时返回的消息
}
func main() {
wrapper.SetCtx(
"ip-restriction",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders))
}
func parseConfig(json gjson.Result, config *RestrictionConfig, log wrapper.Log) error {
sourceType := json.Get("ip_source_type")
if sourceType.Exists() && sourceType.String() != "" {
switch sourceType.String() {
case HeaderSourceType:
config.IPSourceType = HeaderSourceType
case OriginSourceType:
default:
config.IPSourceType = OriginSourceType
}
} else {
config.IPSourceType = OriginSourceType
}
header := json.Get("ip_header_name")
if header.Exists() && header.String() != "" {
config.IPHeaderName = header.String()
} else {
config.IPHeaderName = DefaultRealIpHeader
}
status := json.Get("status")
if status.Exists() && status.Uint() > 1 {
config.Status = uint32(header.Uint())
} else {
config.Status = DefaultDenyStatus
}
message := json.Get("message")
if message.Exists() && message.String() != "" {
config.Message = message.String()
} else {
config.Message = DefaultDenyMessage
}
allowNets, err := parseIPNets(json.Get("allow").Array())
if err != nil {
log.Error(err.Error())
return err
}
denyNets, err := parseIPNets(json.Get("deny").Array())
if err != nil {
log.Error(err.Error())
return err
}
if allowNets != nil && denyNets != nil {
log.Warn("allow and deny cannot be set at the same time")
return fmt.Errorf("allow and deny cannot be set at the same time")
}
if allowNets == nil && denyNets == nil {
log.Warn("allow and deny cannot be empty at the same time")
return fmt.Errorf("allow and deny cannot be empty at the same time")
}
config.Allow = allowNets
config.Deny = denyNets
return nil
}
func getDownStreamIp(config RestrictionConfig) (net.IP, error) {
var (
s string
err error
)
if config.IPSourceType == HeaderSourceType {
s, err = proxywasm.GetHttpRequestHeader(config.IPHeaderName)
if err == nil {
s = strings.Split(strings.Trim(s, " "), ",")[0]
}
} else {
var bs []byte
bs, err = proxywasm.GetProperty([]string{"source", "address"})
s = string(bs)
}
if err != nil {
return nil, err
}
ip := parseIP(s)
realIP := net.ParseIP(ip)
if realIP == nil {
return nil, fmt.Errorf("invalid ip[%s]", ip)
}
return realIP, nil
}
func onHttpRequestHeaders(context wrapper.HttpContext, config RestrictionConfig, log wrapper.Log) types.Action {
realIp, err := getDownStreamIp(config)
if err != nil {
return deniedUnauthorized(config)
}
allow := config.Allow
deny := config.Deny
if allow != nil {
if realIp == nil {
log.Error("realIp is nil, blocked")
return deniedUnauthorized(config)
}
if _, found, _ := allow.Get(realIp); !found {
return deniedUnauthorized(config)
}
}
if deny != nil {
if realIp == nil {
log.Error("realIp is nil, continue")
return types.ActionContinue
}
if _, found, _ := deny.Get(realIp); found {
return deniedUnauthorized(config)
}
}
return types.ActionContinue
}
func deniedUnauthorized(config RestrictionConfig) types.Action {
body, _ := json.Marshal(map[string]string{
"message": config.Message,
})
_ = proxywasm.SendHttpResponse(config.Status, nil, body, -1)
return types.ActionContinue
}

View File

@@ -0,0 +1,37 @@
package main
import (
"fmt"
"github.com/tidwall/gjson"
"github.com/zmap/go-iptree/iptree"
"strings"
)
// parseIPNets 解析Ip段配置
func parseIPNets(array []gjson.Result) (*iptree.IPTree, error) {
if len(array) == 0 {
return nil, nil
} else {
tree := iptree.New()
for _, result := range array {
err := tree.AddByString(result.String(), 0)
if err != nil {
return nil, fmt.Errorf("invalid IP[%s]", result.String())
}
}
return tree, nil
}
}
// parseIP 解析IP
func parseIP(source string) string {
if strings.Contains(source, ".") {
// parse ipv4
return strings.Split(source, ":")[0]
}
//parse ipv6
if strings.Contains(source, "]") {
return strings.Split(source, "]")[0][1:]
}
return source
}

View File

@@ -0,0 +1,106 @@
package main
import (
"github.com/tidwall/gjson"
"testing"
)
func Test_parseIPNets(t *testing.T) {
type args struct {
array []gjson.Result
}
tests := []struct {
name string
args args
wantVal bool
wantErr bool
}{
{
name: "",
args: args{
array: gjson.Parse(`["127.0.0.1/30","10.0.0.1"]`).Array(),
},
wantVal: true,
wantErr: false,
},
{
name: "",
args: args{
array: gjson.Parse(``).Array(),
},
wantVal: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseIPNets(tt.args.array)
if (err != nil) != tt.wantErr {
t.Errorf("parseIPNets() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantVal && got == nil {
return
}
if _, found, _ := got.GetByString("10.0.0.1"); found != tt.wantVal {
t.Errorf("parseIPNets() got = %v, want %v", found, tt.wantVal)
return
}
})
}
}
func Test_parseIP(t *testing.T) {
type args struct {
source string
}
tests := []struct {
name string
args args
want string
}{
// TODO: Add test cases.
{
name: "case 1",
args: args{
"127.0.0.1",
},
want: "127.0.0.1",
},
{
name: "case 2",
args: args{
"127.0.0.1:12",
},
want: "127.0.0.1",
},
{
name: "case 3",
args: args{
"fe80::14d5:8aff:fed9:2114",
},
want: "fe80::14d5:8aff:fed9:2114",
},
{
name: "case 4",
args: args{
"[fe80::14d5:8aff:fed9:2114]:123",
},
want: "fe80::14d5:8aff:fed9:2114",
},
{
name: "case 5",
args: args{
"127.0.0.1:12,[fe80::14d5:8aff:fed9:2114]:123",
},
want: "127.0.0.1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := parseIP(tt.args.source); got != tt.want {
t.Errorf("parseIP() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -0,0 +1,46 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
name: httproute-app-root
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: ip-restriction-allow
namespace: higress-system
spec:
defaultConfig:
ip_source_type: header
ip_header_name: x-real-ip
allow:
- 192.168.0.1/16
- 10.0.0.1
url: file:///opt/plugins/wasm-go/extensions/ip-restriction/plugin.wasm

View File

@@ -0,0 +1,46 @@
# Copyright (c) 2022 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
name: httproute-app-root
namespace: higress-conformance-infra
spec:
ingressClassName: higress
rules:
- host: "foo.com"
http:
paths:
- pathType: Prefix
path: "/"
backend:
service:
name: infra-backend-v1
port:
number: 8080
---
apiVersion: extensions.higress.io/v1alpha1
kind: WasmPlugin
metadata:
name: ip-restriction-deny
namespace: higress-system
spec:
defaultConfig:
ip_source_type: header
ip_header_name: x-real-ip
deny:
- 192.168.0.1/16
- 10.0.0.1
url: file:///opt/plugins/wasm-go/extensions/ip-restriction/plugin.wasm

View File

@@ -0,0 +1,219 @@
// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package tests
import (
"testing"
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
)
func init() {
Register(WasmPluginsIPRestrictionAllow)
Register(WasmPluginsIPRestrictionDeny)
}
var WasmPluginsIPRestrictionAllow = suite.ConformanceTest{
ShortName: "WasmPluginsIPRestrictionAllow",
Description: "The Ingress in the higress-conformance-infra namespace test the ip-restriction wasmplugins.",
Manifests: []string{"tests/go-wasm-ip-restriction-allow.yaml"},
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "10.0.0.1"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
ExpectedResponseNoRequest: true,
},
},
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "10.0.0.2"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
ExpectedResponseNoRequest: true,
},
},
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "192.168.5.0"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
ExpectedResponseNoRequest: true,
},
},
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "192.169.5.0"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
ExpectedResponseNoRequest: true,
},
},
}
t.Run("WasmPlugins ip-restriction", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}
})
},
}
var WasmPluginsIPRestrictionDeny = suite.ConformanceTest{
ShortName: "WasmPluginsIPRestrictionDeny",
Description: "The Ingress in the higress-conformance-infra namespace test the ip-restriction wasmplugins.",
Manifests: []string{"tests/go-wasm-ip-restriction-deny.yaml"},
Features: []suite.SupportedFeature{suite.WASMGoConformanceFeature},
Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
testcases := []http.Assertion{
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "10.0.0.1"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
ExpectedResponseNoRequest: true,
},
},
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "10.0.0.2"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
ExpectedResponseNoRequest: true,
},
},
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "192.168.5.0"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 403,
},
ExpectedResponseNoRequest: true,
},
},
{
Meta: http.AssertionMeta{
TargetBackend: "infra-backend-v1",
TargetNamespace: "higress-conformance-infra",
},
Request: http.AssertionRequest{
ActualRequest: http.Request{
Host: "foo.com",
Path: "/info",
UnfollowRedirect: true,
Headers: map[string]string{"X-REAL-IP": "192.169.5.0"},
},
},
Response: http.AssertionResponse{
ExpectedResponse: http.Response{
StatusCode: 200,
},
ExpectedResponseNoRequest: true,
},
},
}
t.Run("WasmPlugins ip-restriction", func(t *testing.T) {
for _, testcase := range testcases {
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
}
})
},
}