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:
ran xuxin
2024-09-01 09:04:55 +08:00
committed by GitHub
parent d82c872c13
commit f26cde3b3b
11 changed files with 1708577 additions and 0 deletions

View 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
}