mirror of
https://github.com/alibaba/higress.git
synced 2026-06-09 12:47:28 +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:
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
|
||||
}
|
||||
Reference in New Issue
Block a user