mirror of
https://github.com/alibaba/higress.git
synced 2026-02-06 23:21:08 +08:00
Feat: Add ai-geoip wasm plugin to search the client's geographic information depending on the client ip address (#1172)
Co-authored-by: Kent Dong <ch3cho@qq.com> Co-authored-by: 澄潭 <zty98751@alibaba-inc.com>
This commit is contained in:
38
plugins/wasm-go/extensions/geo-ip/README.md
Normal file
38
plugins/wasm-go/extensions/geo-ip/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 功能说明
|
||||
|
||||
`geo-ip`本插件实现了通过用户ip查询出地理位置信息,然后通过请求属性和新添加的请求头把地理位置信息传递给后续插件。
|
||||
|
||||
# 配置字段
|
||||
| 名称 | 数据类型 | 填写要求 | 默认值 | 描述 |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| ip_protocol | string | 否 | ipv4 | 可选值:1. ipv4:只对ipv4用户请求查找地理位置信息,传递给后续插件。而ipv6用户的请求会跳过该插件,继续由后续插件处理。 2. ipv6:(未来实现后)只对ipv6用户查找地理位置信息,传递给后续插件。而ipv4用户的请求会跳过该插件,继续由后续插件处理。(目前是跳过插件,请求由后续插件处理。)
|
||||
| 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来源头 |
|
||||
|
||||
|
||||
# 配置示例
|
||||
|
||||
```yaml
|
||||
ip_protocol: ipv4
|
||||
ip_source_type: header
|
||||
ip_header_name: X-Real-Ip
|
||||
```
|
||||
|
||||
# 生成geoCidr.txt的说明
|
||||
|
||||
在generateCidr目录里包含的ip.merge.txt文件是github上ip2region项目的全世界的ip网段库。 ipRange2Cidr.go 是把ip网段转换成多个cidr的程序。转换出的cidr 和地理位置信息存在 /data/geoCidr.txt文件里。geo-ip插件会在Higress启动读配置阶段读取geoCidr.txt文件并且解析到radixtree数据结构的内存里,以便以后查询用户ip对应的地理位置信息。转换程序运行命令如下:
|
||||
|
||||
```bash
|
||||
go run generateCidr/ipRange2Cidr.go
|
||||
```
|
||||
|
||||
# property 的使用方式
|
||||
在geo-ip插件里调用proxywasm.SetProperty() 分别把country、city、province、isp设置进请求属性里,以便后续插件可以调用proxywasm.GetProperty()获取该请求的用户ip对应的地理信息。
|
||||
|
||||
# ip网段转换成cidr列表的单元测试
|
||||
在 generateCidr 目录里的 ipRange2Cidr_test.go 是ip网段转换成cidr 列表的单元测试程序。在 generateCidr 目录里运行命令 go test 。通过的情况显示如下:
|
||||
|
||||
``bash
|
||||
PASS
|
||||
ok higress/plugins/wasm-go/extensions/geo-ip/generateCidr 0.018s
|
||||
```
|
||||
1
plugins/wasm-go/extensions/geo-ip/VERSION
Normal file
1
plugins/wasm-go/extensions/geo-ip/VERSION
Normal file
@@ -0,0 +1 @@
|
||||
1.0.0-alpha
|
||||
683844
plugins/wasm-go/extensions/geo-ip/generateCidr/ip.merge.txt
Normal file
683844
plugins/wasm-go/extensions/geo-ip/generateCidr/ip.merge.txt
Normal file
File diff suppressed because it is too large
Load Diff
160
plugins/wasm-go/extensions/geo-ip/generateCidr/ipRange2Cidr.go
Normal file
160
plugins/wasm-go/extensions/geo-ip/generateCidr/ipRange2Cidr.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
//"strconv"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed ip.merge.txt
|
||||
var geoipdata string
|
||||
|
||||
var CIDR2MASK = []uint32{
|
||||
0x00000000, 0x80000000, 0xC0000000, 0xE0000000, 0xF0000000, 0xF8000000,
|
||||
0xFC000000, 0xFE000000, 0xFF000000, 0xFF800000, 0xFFC00000, 0xFFE00000,
|
||||
0xFFF00000, 0xFFF80000, 0xFFFC0000, 0xFFFE0000, 0xFFFF0000, 0xFFFF8000,
|
||||
0xFFFFC000, 0xFFFFE000, 0xFFFFF000, 0xFFFFF800, 0xFFFFFC00, 0xFFFFFE00,
|
||||
0xFFFFFF00, 0xFFFFFF80, 0xFFFFFFC0, 0xFFFFFFE0, 0xFFFFFFF0, 0xFFFFFFF8,
|
||||
0xFFFFFFFC, 0xFFFFFFFE, 0xFFFFFFFF,
|
||||
}
|
||||
|
||||
func main() {
|
||||
outFile := "/data/geoCidr.txt"
|
||||
f, err := os.Create(outFile)
|
||||
if err != nil {
|
||||
log.Println("open file failed.", outFile, err)
|
||||
return
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
|
||||
geoIpRows := strings.Split(geoipdata, "\n")
|
||||
for _, row := range geoIpRows {
|
||||
if row == "" {
|
||||
log.Println("this row is empty.")
|
||||
continue
|
||||
}
|
||||
|
||||
log.Println("geoip segment: ", row)
|
||||
tmpArr := strings.Split(row, "|")
|
||||
if len(tmpArr) < 7 {
|
||||
log.Println("geoIp row field number is less than 7 " + row)
|
||||
return
|
||||
}
|
||||
|
||||
sip := tmpArr[0]
|
||||
eip := tmpArr[1]
|
||||
country := tmpArr[2]
|
||||
province := tmpArr[4]
|
||||
city := tmpArr[5]
|
||||
isp := tmpArr[6]
|
||||
|
||||
if country == "0" {
|
||||
country = ""
|
||||
}
|
||||
if province == "0" {
|
||||
province = ""
|
||||
}
|
||||
if city == "0" {
|
||||
city = ""
|
||||
}
|
||||
if isp == "0" {
|
||||
isp = ""
|
||||
}
|
||||
|
||||
if err := parseGeoIpFile(sip, eip, country, province, city, isp, f); err != nil {
|
||||
log.Printf("parse geo ip file failed, error:%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func range2cidrList(startIp string, endIp string) []string {
|
||||
cidrList := []string{}
|
||||
|
||||
start := uint32(ipToInt(startIp))
|
||||
beginStart := start
|
||||
end := uint32(ipToInt(endIp))
|
||||
for end >= start {
|
||||
maxSize := 32
|
||||
for maxSize > 0 {
|
||||
mask := CIDR2MASK[maxSize-1]
|
||||
maskedBase := start & mask
|
||||
|
||||
if maskedBase != start {
|
||||
break
|
||||
}
|
||||
|
||||
maxSize--
|
||||
}
|
||||
|
||||
x := math.Log2(float64(end - start + 1))
|
||||
maxDiff := 32 - int(math.Floor(x))
|
||||
if maxSize < maxDiff {
|
||||
maxSize = maxDiff
|
||||
}
|
||||
ipStr := intToIP(int(start))
|
||||
cidr := fmt.Sprintf("%s/%d", ipStr, maxSize)
|
||||
cidrList = append(cidrList, cidr)
|
||||
|
||||
start += uint32(math.Pow(2, float64(32-maxSize)))
|
||||
//avoid dead loop for 255.255.255.255
|
||||
if start < beginStart {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return cidrList
|
||||
}
|
||||
|
||||
func parseGeoIpFile(startIp string, endIp string, country string, province string, city string, isp string, f *os.File) error {
|
||||
cidrList := range2cidrList(startIp, endIp)
|
||||
for _, cidr := range cidrList {
|
||||
outRow := fmt.Sprintf("%s|%s|%s|%s|%s", cidr, country, province, city, isp)
|
||||
_, err := f.WriteString(outRow + "\n")
|
||||
if err != nil {
|
||||
log.Println("write string failed.", outRow, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipToInt(ipStr string) int {
|
||||
ipSegs := strings.Split(ipStr, ".")
|
||||
var ipInt int = 0
|
||||
var pos uint = 24
|
||||
for _, ipSeg := range ipSegs {
|
||||
tempInt, _ := strconv.Atoi(ipSeg)
|
||||
tempInt = tempInt << pos
|
||||
ipInt = ipInt | tempInt
|
||||
pos -= 8
|
||||
}
|
||||
return ipInt
|
||||
}
|
||||
|
||||
func intToIP(ipInt int) string {
|
||||
ipSegs := make([]string, 4)
|
||||
var len int = len(ipSegs)
|
||||
buffer := bytes.NewBufferString("")
|
||||
for i := 0; i < len; i++ {
|
||||
tempInt := ipInt & 0xFF
|
||||
ipSegs[len-i-1] = strconv.Itoa(tempInt)
|
||||
ipInt = ipInt >> 8
|
||||
}
|
||||
for i := 0; i < len; i++ {
|
||||
buffer.WriteString(ipSegs[i])
|
||||
if i < len-1 {
|
||||
buffer.WriteString(".")
|
||||
}
|
||||
}
|
||||
return buffer.String()
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
//"strconv"
|
||||
_ "embed"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRange2CidrList(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
startIp string
|
||||
endIp string
|
||||
want map[string]int
|
||||
}{
|
||||
{
|
||||
"test start ip with 0.0.0.0",
|
||||
"0.0.0.0",
|
||||
"1.0.0.255",
|
||||
map[string]int{
|
||||
"0.0.0.0/8": 1,
|
||||
"1.0.0.0/24": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"test the same network segment",
|
||||
"1.0.1.0",
|
||||
"1.0.1.255",
|
||||
map[string]int{"1.0.1.0/24": 1},
|
||||
},
|
||||
{
|
||||
"test cross network segment",
|
||||
"1.0.1.0",
|
||||
"2.0.1.112",
|
||||
map[string]int{
|
||||
"1.0.1.0/24": 1,
|
||||
"1.0.2.0/23": 1,
|
||||
"1.0.4.0/22": 1,
|
||||
"1.0.8.0/21": 1,
|
||||
"1.0.16.0/20": 1,
|
||||
"1.0.32.0/19": 1,
|
||||
"1.0.64.0/18": 1,
|
||||
"1.0.128.0/17": 1,
|
||||
"1.1.0.0/16": 1,
|
||||
"1.2.0.0/15": 1,
|
||||
"1.4.0.0/14": 1,
|
||||
"1.8.0.0/13": 1,
|
||||
"1.16.0.0/12": 1,
|
||||
"1.32.0.0/11": 1,
|
||||
"1.64.0.0/10": 1,
|
||||
"1.128.0.0/9": 1,
|
||||
"2.0.0.0/24": 1,
|
||||
"2.0.1.0/26": 1,
|
||||
"2.0.1.64/27": 1,
|
||||
"2.0.1.96/28": 1,
|
||||
"2.0.1.112/32": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"test end ip with 255.255.255.255",
|
||||
"224.0.0.0",
|
||||
"255.255.255.255",
|
||||
map[string]int{"224.0.0.0/3": 1},
|
||||
},
|
||||
{
|
||||
"test start ip is greater than end ip",
|
||||
"1.0.0.255",
|
||||
"1.0.0.0",
|
||||
map[string]int{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := range2cidrList(test.startIp, test.endIp)
|
||||
assert.Equal(t, len(test.want), len(actual), "")
|
||||
for _, v := range actual {
|
||||
if _, ok := test.want[v]; !ok {
|
||||
assert.Error(t, errors.New("not match"), "")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
1023966
plugins/wasm-go/extensions/geo-ip/geoCidr.txt
Normal file
1023966
plugins/wasm-go/extensions/geo-ip/geoCidr.txt
Normal file
File diff suppressed because it is too large
Load Diff
24
plugins/wasm-go/extensions/geo-ip/go.mod
Normal file
24
plugins/wasm-go/extensions/geo-ip/go.mod
Normal file
@@ -0,0 +1,24 @@
|
||||
module higress/plugins/wasm-go/extensions/geo-ip
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/zmap/go-iptree v0.0.0-20210731043055-d4e632617837
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/asergeyev/nradix v0.0.0-20170505151046-3872ab85bb56 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // 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/pmezard/go-difflib v1.0.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
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
32
plugins/wasm-go/extensions/geo-ip/go.sum
Normal file
32
plugins/wasm-go/extensions/geo-ip/go.sum
Normal file
@@ -0,0 +1,32 @@
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2 h1:gH7OIGXm4wtW5Vo7L2deMPqF7OVWNESDHv1CaaTGu6s=
|
||||
github.com/alibaba/higress/plugins/wasm-go v1.4.2/go.mod h1:359don/ahMxpfeLMzr29Cjwcu8IywTTDUzWlBPRNLHw=
|
||||
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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-20240711023527-ba358c48772f h1:ZIiIBRvIw62gA5MJhuwp1+2wWbqL9IGElQ499rUsYYg=
|
||||
github.com/higress-group/proxy-wasm-go-sdk v0.0.0-20240711023527-ba358c48772f/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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.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/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
265
plugins/wasm-go/extensions/geo-ip/main.go
Normal file
265
plugins/wasm-go/extensions/geo-ip/main.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
//go:embed geoCidr.txt
|
||||
var geoipdata string
|
||||
|
||||
var GeoIpRdxTree *iptree.IPTree
|
||||
var HaveInitGeoIpDb bool = false
|
||||
|
||||
const (
|
||||
DefaultRealIpHeader = "X-Forwarded-For"
|
||||
OriginSourceType = "origin-source"
|
||||
HeaderSourceType = "header"
|
||||
)
|
||||
|
||||
// 根据ip2region 项目里的ip地理位置数据库ip.merge.txt的内网ip网段,经过ip网段转换多个cidr的程序 geo-ip/generateCidr/ipRange2Cidr.go 转换成的cidr地址。
|
||||
var internalIpCidr []string = []string{"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/11", "100.96.0.0/12",
|
||||
"100.112.0.0/13", "100.120.0.0/15", "100.122.0.0/16", "100.123.0.0/16", "100.124.0.0/14",
|
||||
"127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24",
|
||||
"192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/3",
|
||||
}
|
||||
|
||||
func main() {
|
||||
wrapper.SetCtx(
|
||||
"geo-ip",
|
||||
wrapper.ParseConfigBy(parseConfig),
|
||||
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
|
||||
)
|
||||
}
|
||||
|
||||
type GeoIpConfig struct {
|
||||
IpProtocol string `json:"ip_protocol"`
|
||||
IPSourceType string `json:"ip_source_type"`
|
||||
IPHeaderName string `json:"ip_header_name"`
|
||||
}
|
||||
|
||||
type GeoIpData struct {
|
||||
Cidr string `json:"cidr"`
|
||||
Country string `json:"country"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Isp string `json:"isp"`
|
||||
}
|
||||
|
||||
func parseConfig(json gjson.Result, config *GeoIpConfig, 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
|
||||
}
|
||||
|
||||
ipProtocol := json.Get("ip_protocol")
|
||||
if !ipProtocol.Exists() {
|
||||
config.IpProtocol = "ipv4"
|
||||
} else {
|
||||
switch ipProtocol.String() {
|
||||
case "ipv6":
|
||||
config.IpProtocol = "ipv6"
|
||||
case "ipv4":
|
||||
default:
|
||||
config.IpProtocol = "ipv4"
|
||||
}
|
||||
}
|
||||
|
||||
if HaveInitGeoIpDb {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := ReadGeoIpDataToRdxtree(log); err != nil {
|
||||
log.Errorf("read geoip data failed.%v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
HaveInitGeoIpDb = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadGeoIpDataToRdxtree(log wrapper.Log) error {
|
||||
GeoIpRdxTree = iptree.New()
|
||||
|
||||
//eg., cidr country province city isp
|
||||
geoIpRows := strings.Split(geoipdata, "\n")
|
||||
for _, row := range geoIpRows {
|
||||
if row == "" {
|
||||
log.Infof("parsed empty line.")
|
||||
continue
|
||||
}
|
||||
|
||||
pureRow := strings.Trim(row, " ")
|
||||
tmpArr := strings.Split(pureRow, "|")
|
||||
if len(tmpArr) < 5 {
|
||||
return errors.New("geoIp row field number is less than 5 " + row)
|
||||
}
|
||||
|
||||
cidr := strings.Trim(tmpArr[0], " ")
|
||||
geoIpData := &GeoIpData{
|
||||
Cidr: cidr,
|
||||
Country: strings.Trim(tmpArr[1], " "),
|
||||
Province: strings.Trim(tmpArr[2], " "),
|
||||
City: strings.Trim(tmpArr[3], " "),
|
||||
Isp: strings.Trim(tmpArr[4], " "),
|
||||
}
|
||||
|
||||
if err := GeoIpRdxTree.AddByString(cidr, geoIpData); err != nil {
|
||||
return errors.New("add geoipdata into radix treefailed " + err.Error())
|
||||
}
|
||||
|
||||
log.Debugf("added geoip data into radixtree: %v", *geoIpData)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// search geodata using client ip in radixtree.
|
||||
func SearchGeoIpDataInRdxtree(ip string, log wrapper.Log) (*GeoIpData, error) {
|
||||
val, found, err := GeoIpRdxTree.GetByString(ip)
|
||||
if err != nil {
|
||||
log.Errorf("search geo ip data in raditree failed. %v %s", err, ip)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if found {
|
||||
return val.(*GeoIpData), nil
|
||||
}
|
||||
|
||||
return nil, errors.New("geo ip data not found")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func isInternalIp(ip string) (string, error) {
|
||||
if ip == "" {
|
||||
return "", errors.New("empty ip")
|
||||
}
|
||||
|
||||
ipBt := net.ParseIP(ip)
|
||||
if ipBt == nil {
|
||||
return "", errors.New("invalid ip format")
|
||||
}
|
||||
|
||||
ip4B := ipBt.To4()
|
||||
if ip4B == nil {
|
||||
return "", errors.New("not ipv4 format")
|
||||
}
|
||||
|
||||
for _, cidr := range internalIpCidr {
|
||||
_, networkIp, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if networkIp.Contains(ip4B) {
|
||||
return cidr, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func onHttpRequestHeaders(ctx wrapper.HttpContext, config GeoIpConfig, log wrapper.Log) types.Action {
|
||||
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 {
|
||||
log.Errorf("get client ip failed. %s %v", config.IPSourceType, err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
clientIp := parseIP(s)
|
||||
|
||||
//ipv6 will be implemented in the future.
|
||||
if config.IpProtocol == "ipv6" || strings.Contains(clientIp, ":") {
|
||||
log.Warnf("ipv6 will be implemented in the future.%s %s", clientIp, config.IpProtocol)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
internalCidr, err := isInternalIp(clientIp)
|
||||
if err != nil {
|
||||
log.Errorf("check internal ip failed. error: %v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
|
||||
var geoIpData *GeoIpData
|
||||
if internalCidr != "" {
|
||||
geoIpData = &GeoIpData{
|
||||
Cidr: internalCidr,
|
||||
City: "内网IP",
|
||||
Province: "内网IP",
|
||||
Country: "内网IP",
|
||||
Isp: "内网IP",
|
||||
}
|
||||
} else {
|
||||
geoIpData, err = SearchGeoIpDataInRdxtree(clientIp, log)
|
||||
if err != nil {
|
||||
log.Errorf("search geo info failed.%v", err)
|
||||
return types.ActionContinue
|
||||
}
|
||||
}
|
||||
|
||||
proxywasm.SetProperty([]string{"geo-city"}, []byte(geoIpData.City))
|
||||
proxywasm.SetProperty([]string{"geo-province"}, []byte(geoIpData.Province))
|
||||
proxywasm.SetProperty([]string{"geo-country"}, []byte(geoIpData.Country))
|
||||
proxywasm.SetProperty([]string{"geo-isp"}, []byte(geoIpData.Isp))
|
||||
|
||||
countryEnc := url.QueryEscape(geoIpData.Country)
|
||||
provinceEnc := url.QueryEscape(geoIpData.Province)
|
||||
cityEnc := url.QueryEscape(geoIpData.City)
|
||||
ispEnc := url.QueryEscape(geoIpData.Isp)
|
||||
|
||||
proxywasm.AddHttpRequestHeader("X-Higress-Geo-Country", countryEnc)
|
||||
proxywasm.AddHttpRequestHeader("X-Higress-Geo-Province", provinceEnc)
|
||||
proxywasm.AddHttpRequestHeader("X-Higress-Geo-City", cityEnc)
|
||||
proxywasm.AddHttpRequestHeader("X-Higress-Geo-Isp", ispEnc)
|
||||
|
||||
return types.ActionContinue
|
||||
}
|
||||
111
test/e2e/conformance/tests/go-wasm-geo-ip.go
Normal file
111
test/e2e/conformance/tests/go-wasm-geo-ip.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/http"
|
||||
"github.com/alibaba/higress/test/e2e/conformance/utils/suite"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register(WasmPluginsGeoIPPlugin)
|
||||
}
|
||||
|
||||
var WasmPluginsGeoIPPlugin = suite.ConformanceTest{
|
||||
ShortName: "WasmPluginsGeoIPPlugin",
|
||||
Description: "The geo-ip wasm pluin finds the client's geographic information according to the client's ip address.",
|
||||
Manifests: []string{"tests/go-wasm-geo-ip.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: "/get",
|
||||
UnfollowRedirect: true,
|
||||
Headers: map[string]string{
|
||||
"X-Forwarded-For": "70.155.208.224,10.1.1.1",
|
||||
},
|
||||
},
|
||||
ExpectedRequest: &http.ExpectedRequest{
|
||||
Request: http.Request{
|
||||
Path: "/get",
|
||||
Host: "foo.com",
|
||||
Headers: map[string]string{
|
||||
"X-Higress-Geo-Isp": url.QueryEscape("美国电话电报"),
|
||||
"X-Higress-Geo-City": "",
|
||||
"X-Higress-Geo-Province": url.QueryEscape("密西西比"),
|
||||
"X-Higress-Geo-Country": url.QueryEscape("美国"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
},
|
||||
ExpectedResponseNoRequest: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/get",
|
||||
UnfollowRedirect: true,
|
||||
Headers: map[string]string{
|
||||
"X-Forwarded-For": "2.2.128.100,10.1.1.2",
|
||||
},
|
||||
},
|
||||
ExpectedRequest: &http.ExpectedRequest{
|
||||
Request: http.Request{
|
||||
Path: "/get",
|
||||
Host: "foo.com",
|
||||
Headers: map[string]string{
|
||||
"X-Higress-Geo-Isp": url.QueryEscape("橘子电信"),
|
||||
"X-Higress-Geo-City": "",
|
||||
"X-Higress-Geo-Province": url.QueryEscape("Var"),
|
||||
"X-Higress-Geo-Country": url.QueryEscape("法国"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
},
|
||||
ExpectedResponseNoRequest: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("WasmPlugins geo-ip", func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, suite.GatewayAddress, testcase)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
46
test/e2e/conformance/tests/go-wasm-geo-ip.yaml
Normal file
46
test/e2e/conformance/tests/go-wasm-geo-ip.yaml
Normal 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: wasmplugin-geo-ip
|
||||
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: geo-ip
|
||||
namespace: higress-system
|
||||
spec:
|
||||
defaultConfigDisable: true
|
||||
matchRules:
|
||||
- config:
|
||||
ip_source_type: header
|
||||
ingress:
|
||||
- higress-conformance-infra/wasmplugin-geo-ip
|
||||
url: file:///opt/plugins/wasm-go/extensions/geo-ip/plugin.wasm
|
||||
Reference in New Issue
Block a user