diff --git a/config/config.go b/config/config.go index 38ab61b..4c4fe62 100644 --- a/config/config.go +++ b/config/config.go @@ -244,6 +244,21 @@ type SDConfig struct { Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"` } +type RouterRouteConfig struct { + Net string `json:"net"` + Gateway string `json:"gateway"` +} + +type RouterConfig struct { + Name string `json:"name"` + Routes []*RouterRouteConfig `yaml:",omitempty" json:"routes,omitempty"` + Reload time.Duration `yaml:",omitempty" json:"reload,omitempty"` + File *FileLoader `yaml:",omitempty" json:"file,omitempty"` + Redis *RedisLoader `yaml:",omitempty" json:"redis,omitempty"` + HTTP *HTTPLoader `yaml:"http,omitempty" json:"http,omitempty"` + Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"` +} + type RecorderConfig struct { Name string `json:"name"` File *FileRecorder `yaml:",omitempty" json:"file,omitempty"` @@ -447,6 +462,7 @@ type Config struct { Resolvers []*ResolverConfig `yaml:",omitempty" json:"resolvers,omitempty"` Hosts []*HostsConfig `yaml:",omitempty" json:"hosts,omitempty"` Ingresses []*IngressConfig `yaml:",omitempty" json:"ingresses,omitempty"` + Routers []*RouterConfig `yaml:",omitempty" json:"routers,omitempty"` SDs []*SDConfig `yaml:"sds,omitempty" json:"sds,omitempty"` Recorders []*RecorderConfig `yaml:",omitempty" json:"recorders,omitempty"` Limiters []*LimiterConfig `yaml:",omitempty" json:"limiters,omitempty"` diff --git a/config/parsing/ingress/parse.go b/config/parsing/ingress/parse.go index 5c95303..f7dafc3 100644 --- a/config/parsing/ingress/parse.go +++ b/config/parsing/ingress/parse.go @@ -41,13 +41,13 @@ func ParseIngress(cfg *config.IngressConfig) ingress.Ingress { } } - var rules []xingress.Rule + var rules []*ingress.Rule for _, rule := range cfg.Rules { if rule.Hostname == "" || rule.Endpoint == "" { continue } - rules = append(rules, xingress.Rule{ + rules = append(rules, &ingress.Rule{ Hostname: rule.Hostname, Endpoint: rule.Endpoint, }) diff --git a/config/parsing/router/parse.go b/config/parsing/router/parse.go new file mode 100644 index 0000000..f3a563d --- /dev/null +++ b/config/parsing/router/parse.go @@ -0,0 +1,104 @@ +package router + +import ( + "crypto/tls" + "net" + "strings" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/router" + "github.com/go-gost/x/config" + "github.com/go-gost/x/internal/loader" + "github.com/go-gost/x/internal/plugin" + xrouter "github.com/go-gost/x/router" +) + +func ParseRouter(cfg *config.RouterConfig) router.Router { + if cfg == nil { + return nil + } + + if cfg.Plugin != nil { + var tlsCfg *tls.Config + if cfg.Plugin.TLS != nil { + tlsCfg = &tls.Config{ + ServerName: cfg.Plugin.TLS.ServerName, + InsecureSkipVerify: !cfg.Plugin.TLS.Secure, + } + } + switch strings.ToLower(cfg.Plugin.Type) { + case "http": + return xrouter.NewHTTPPlugin( + cfg.Name, cfg.Plugin.Addr, + plugin.TLSConfigOption(tlsCfg), + plugin.TimeoutOption(cfg.Plugin.Timeout), + ) + default: + return xrouter.NewGRPCPlugin( + cfg.Name, cfg.Plugin.Addr, + plugin.TokenOption(cfg.Plugin.Token), + plugin.TLSConfigOption(tlsCfg), + ) + } + } + + var routes []*router.Route + for _, route := range cfg.Routes { + _, ipNet, _ := net.ParseCIDR(route.Net) + if ipNet == nil { + continue + } + gw := net.ParseIP(route.Gateway) + if gw == nil { + continue + } + + routes = append(routes, &router.Route{ + Net: ipNet, + Gateway: gw, + }) + } + opts := []xrouter.Option{ + xrouter.RoutesOption(routes), + xrouter.ReloadPeriodOption(cfg.Reload), + xrouter.LoggerOption(logger.Default().WithFields(map[string]any{ + "kind": "router", + "router": cfg.Name, + })), + } + if cfg.File != nil && cfg.File.Path != "" { + opts = append(opts, xrouter.FileLoaderOption(loader.FileLoader(cfg.File.Path))) + } + if cfg.Redis != nil && cfg.Redis.Addr != "" { + switch cfg.Redis.Type { + case "list": // rediss list + opts = append(opts, xrouter.RedisLoaderOption(loader.RedisListLoader( + cfg.Redis.Addr, + loader.DBRedisLoaderOption(cfg.Redis.DB), + loader.PasswordRedisLoaderOption(cfg.Redis.Password), + loader.KeyRedisLoaderOption(cfg.Redis.Key), + ))) + case "set": // redis set + opts = append(opts, xrouter.RedisLoaderOption(loader.RedisSetLoader( + cfg.Redis.Addr, + loader.DBRedisLoaderOption(cfg.Redis.DB), + loader.PasswordRedisLoaderOption(cfg.Redis.Password), + loader.KeyRedisLoaderOption(cfg.Redis.Key), + ))) + default: // redis hash + opts = append(opts, xrouter.RedisLoaderOption(loader.RedisHashLoader( + cfg.Redis.Addr, + loader.DBRedisLoaderOption(cfg.Redis.DB), + loader.PasswordRedisLoaderOption(cfg.Redis.Password), + loader.KeyRedisLoaderOption(cfg.Redis.Key), + ))) + } + } + if cfg.HTTP != nil && cfg.HTTP.URL != "" { + opts = append(opts, xrouter.HTTPLoaderOption(loader.HTTPLoader( + cfg.HTTP.URL, + loader.TimeoutHTTPLoaderOption(cfg.HTTP.Timeout), + ))) + } + return xrouter.NewRouter(opts...) +} diff --git a/go.mod b/go.mod index 0db2020..fd4b58e 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ require ( github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/gin-contrib/cors v1.3.1 github.com/gin-gonic/gin v1.9.1 - github.com/go-gost/core v0.0.0-20231118102540-486f2cee616a + github.com/go-gost/core v0.0.0-20231119062035-6b01698ea9f4 github.com/go-gost/gosocks4 v0.0.1 github.com/go-gost/gosocks5 v0.4.0 - github.com/go-gost/plugin v0.0.0-20231118102615-bfe81cbb44b6 + github.com/go-gost/plugin v0.0.0-20231119062132-d959ab54847f github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 github.com/go-redis/redis/v8 v8.11.5 diff --git a/go.sum b/go.sum index a46131e..76dbb1a 100644 --- a/go.sum +++ b/go.sum @@ -93,14 +93,14 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gost/core v0.0.0-20231118102540-486f2cee616a h1:bGpcollgZpuI0ct6FdJxZ2k7ipu4T6qrQbHc+ZbI29I= -github.com/go-gost/core v0.0.0-20231118102540-486f2cee616a/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= +github.com/go-gost/core v0.0.0-20231119062035-6b01698ea9f4 h1:dWj03ofZottHxRi6+oQ17CRgOr8qw/egg0cSpSSpQsk= +github.com/go-gost/core v0.0.0-20231119062035-6b01698ea9f4/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E= github.com/go-gost/gosocks4 v0.0.1 h1:+k1sec8HlELuQV7rWftIkmy8UijzUt2I6t+iMPlGB2s= github.com/go-gost/gosocks4 v0.0.1/go.mod h1:3B6L47HbU/qugDg4JnoFPHgJXE43Inz8Bah1QaN9qCc= github.com/go-gost/gosocks5 v0.4.0 h1:EIrOEkpJez4gwHrMa33frA+hHXJyevjp47thpMQsJzI= github.com/go-gost/gosocks5 v0.4.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4= -github.com/go-gost/plugin v0.0.0-20231118102615-bfe81cbb44b6 h1:1zFWxk8mmTewdDzzZutO4nremhQ6N93PWdB3FrLfbaQ= -github.com/go-gost/plugin v0.0.0-20231118102615-bfe81cbb44b6/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw= +github.com/go-gost/plugin v0.0.0-20231119062132-d959ab54847f h1:V5m5plmwkIt16B251Eg2cK2UOJ1IRe4fNuWdTCqn8ek= +github.com/go-gost/plugin v0.0.0-20231119062132-d959ab54847f/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw= github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 h1:qAG1OyjvdA5h221CfFSS3J359V3d2E7dJWyP29QoDSI= github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8= github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 h1:xj8gUZGYO3nb5+6Bjw9+tsFkA9sYynrOvDvvC4uDV2I= diff --git a/handler/tun/handler.go b/handler/tun/handler.go index 87a14b3..445190e 100644 --- a/handler/tun/handler.go +++ b/handler/tun/handler.go @@ -9,9 +9,10 @@ import ( "time" "github.com/go-gost/core/chain" - "github.com/go-gost/core/hop" "github.com/go-gost/core/handler" + "github.com/go-gost/core/hop" md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/router" tun_util "github.com/go-gost/x/internal/util/tun" "github.com/go-gost/x/registry" "github.com/songgao/water/waterutil" @@ -108,15 +109,18 @@ func (h *tunHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. return h.handleServer(ctx, conn, config, log) } -func (h *tunHandler) findRouteFor(dst net.IP, routes ...tun_util.Route) net.Addr { +func (h *tunHandler) findRouteFor(ctx context.Context, dst net.IP, router router.Router) net.Addr { if v, ok := h.routes.Load(ipToTunRouteKey(dst)); ok { return v.(net.Addr) } - for _, route := range routes { - if route.Net.Contains(dst) && route.Gateway != nil { - if v, ok := h.routes.Load(ipToTunRouteKey(route.Gateway)); ok { - return v.(net.Addr) - } + + if router == nil { + return nil + } + + if route := router.GetRoute(ctx, dst); route != nil && route.Gateway != nil { + if v, ok := h.routes.Load(ipToTunRouteKey(route.Gateway)); ok { + return v.(net.Addr) } } return nil diff --git a/handler/tun/server.go b/handler/tun/server.go index 3c2784c..64c9b8e 100644 --- a/handler/tun/server.go +++ b/handler/tun/server.go @@ -82,7 +82,7 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con return nil } - addr := h.findRouteFor(dst, config.Routes...) + addr := h.findRouteFor(ctx, dst, config.Router) if addr == nil { log.Debugf("no route for %s -> %s, packet discarded", src, dst) return nil @@ -203,7 +203,7 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con return nil } - if addr := h.findRouteFor(dst, config.Routes...); addr != nil { + if addr := h.findRouteFor(ctx, dst, config.Router); addr != nil { log.Debugf("find route: %s -> %s", dst, addr) _, err := conn.WriteTo(b[:n], addr) diff --git a/handler/tunnel/bind.go b/handler/tunnel/bind.go index 1099e07..a7ddf89 100644 --- a/handler/tunnel/bind.go +++ b/handler/tunnel/bind.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "net" + "github.com/go-gost/core/ingress" "github.com/go-gost/core/logger" "github.com/go-gost/core/sd" "github.com/go-gost/relay" @@ -56,7 +57,10 @@ func (h *tunnelHandler) handleBind(ctx context.Context, conn net.Conn, network, h.pool.Add(tunnelID, NewConnector(connectorID, tunnelID, h.id, session, h.md.sd), h.md.tunnelTTL) if h.md.ingress != nil { - h.md.ingress.Set(ctx, addr, tunnelID.String()) + h.md.ingress.SetRule(ctx, &ingress.Rule{ + Hostname: addr, + Endpoint: tunnelID.String(), + }) } if h.md.sd != nil { err := h.md.sd.Register(ctx, &sd.Service{ diff --git a/handler/tunnel/connect.go b/handler/tunnel/connect.go index a84d9d2..51c329b 100644 --- a/handler/tunnel/connect.go +++ b/handler/tunnel/connect.go @@ -44,7 +44,9 @@ func (h *tunnelHandler) handleConnect(ctx context.Context, req *relay.Request, c if !h.md.directTunnel { var tid relay.TunnelID if ingress := h.md.ingress; ingress != nil && host != "" { - tid = parseTunnelID(ingress.Get(ctx, host)) + if rule := ingress.GetRule(ctx, host); rule != nil { + tid = parseTunnelID(rule.Endpoint) + } } if !tid.Equal(tunnelID) { resp.Status = relay.StatusHostUnreachable diff --git a/handler/tunnel/entrypoint.go b/handler/tunnel/entrypoint.go index d454058..a8f3126 100644 --- a/handler/tunnel/entrypoint.go +++ b/handler/tunnel/entrypoint.go @@ -85,7 +85,9 @@ func (ep *entrypoint) handle(ctx context.Context, conn net.Conn) error { var tunnelID relay.TunnelID if ep.ingress != nil { - tunnelID = parseTunnelID(ep.ingress.Get(ctx, req.Host)) + if rule := ep.ingress.GetRule(ctx, req.Host); rule != nil { + tunnelID = parseTunnelID(rule.Endpoint) + } } if tunnelID.IsZero() { err := fmt.Errorf("no route to host %s", req.Host) diff --git a/handler/tunnel/metadata.go b/handler/tunnel/metadata.go index 62ca919..a02ff59 100644 --- a/handler/tunnel/metadata.go +++ b/handler/tunnel/metadata.go @@ -45,13 +45,13 @@ func (h *tunnelHandler) parseMetadata(md mdata.Metadata) (err error) { h.md.ingress = registry.IngressRegistry().Get(mdutil.GetString(md, "ingress")) if h.md.ingress == nil { - var rules []xingress.Rule + var rules []*ingress.Rule for _, s := range strings.Split(mdutil.GetString(md, "tunnel"), ",") { ss := strings.SplitN(s, ":", 2) if len(ss) != 2 { continue } - rules = append(rules, xingress.Rule{ + rules = append(rules, &ingress.Rule{ Hostname: ss[0], Endpoint: ss[1], }) diff --git a/ingress/ingress.go b/ingress/ingress.go index bc2edf7..6e50350 100644 --- a/ingress/ingress.go +++ b/ingress/ingress.go @@ -14,13 +14,8 @@ import ( "github.com/go-gost/x/internal/loader" ) -type Rule struct { - Hostname string - Endpoint string -} - type options struct { - rules []Rule + rules []*ingress.Rule fileLoader loader.Loader redisLoader loader.Loader httpLoader loader.Loader @@ -30,7 +25,7 @@ type options struct { type Option func(opts *options) -func RulesOption(rules []Rule) Option { +func RulesOption(rules []*ingress.Rule) Option { return func(opts *options) { opts.rules = rules } @@ -67,7 +62,7 @@ func LoggerOption(logger logger.Logger) Option { } type localIngress struct { - rules map[string]Rule + rules map[string]*ingress.Rule cancelFunc context.CancelFunc options options mu sync.RWMutex @@ -119,9 +114,9 @@ func (ing *localIngress) periodReload(ctx context.Context) error { } func (ing *localIngress) reload(ctx context.Context) error { - rules := make(map[string]Rule) + rules := make(map[string]*ingress.Rule) - fn := func(rule Rule) { + fn := func(rule *ingress.Rule) { if rule.Hostname == "" || rule.Endpoint == "" { return } @@ -152,7 +147,7 @@ func (ing *localIngress) reload(ctx context.Context) error { return nil } -func (ing *localIngress) load(ctx context.Context) (rules []Rule, err error) { +func (ing *localIngress) load(ctx context.Context) (rules []*ingress.Rule, err error) { if ing.options.fileLoader != nil { if lister, ok := ing.options.fileLoader.(loader.Lister); ok { list, er := lister.List(ctx) @@ -203,7 +198,7 @@ func (ing *localIngress) load(ctx context.Context) (rules []Rule, err error) { return } -func (ing *localIngress) parseRules(r io.Reader) (rules []Rule, err error) { +func (ing *localIngress) parseRules(r io.Reader) (rules []*ingress.Rule, err error) { if r == nil { return } @@ -219,9 +214,9 @@ func (ing *localIngress) parseRules(r io.Reader) (rules []Rule, err error) { return } -func (ing *localIngress) Get(ctx context.Context, host string, opts ...ingress.GetOption) string { +func (ing *localIngress) GetRule(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule { if host == "" || ing == nil { - return "" + return nil } // try to strip the port @@ -229,22 +224,18 @@ func (ing *localIngress) Get(ctx context.Context, host string, opts ...ingress.G host = v } - if ing == nil || len(ing.rules) == 0 { - return "" - } - ing.options.logger.Debugf("ingress: lookup %s", host) ep := ing.lookup(host) - if ep == "" { + if ep == nil { ep = ing.lookup("." + host) } - if ep == "" { + if ep == nil { s := host for { if index := strings.IndexByte(s, '.'); index > 0 { ep = ing.lookup(s[index:]) s = s[index+1:] - if ep == "" { + if ep == nil { continue } } @@ -252,29 +243,29 @@ func (ing *localIngress) Get(ctx context.Context, host string, opts ...ingress.G } } - if ep != "" { + if ep != nil { ing.options.logger.Debugf("ingress: %s -> %s", host, ep) } return ep } -func (ing *localIngress) Set(ctx context.Context, host, endpoint string, opts ...ingress.SetOption) bool { +func (ing *localIngress) SetRule(ctx context.Context, rule *ingress.Rule, opts ...ingress.Option) bool { return false } -func (ing *localIngress) lookup(host string) string { - if ing == nil || len(ing.rules) == 0 { - return "" +func (ing *localIngress) lookup(host string) *ingress.Rule { + if ing == nil { + return nil } ing.mu.RLock() defer ing.mu.RUnlock() - return ing.rules[host].Endpoint + return ing.rules[host] } -func (ing *localIngress) parseLine(s string) (rule Rule) { +func (ing *localIngress) parseLine(s string) (rule *ingress.Rule) { line := strings.Replace(s, "\t", " ", -1) line = strings.TrimSpace(line) if n := strings.IndexByte(line, '#'); n >= 0 { @@ -290,7 +281,7 @@ func (ing *localIngress) parseLine(s string) (rule Rule) { return // invalid lines are ignored } - return Rule{ + return &ingress.Rule{ Hostname: sp[0], Endpoint: sp[1], } diff --git a/ingress/plugin.go b/ingress/plugin.go index fbbd997..c09a524 100644 --- a/ingress/plugin.go +++ b/ingress/plugin.go @@ -46,30 +46,36 @@ func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) ingress.Ingr return p } -func (p *grpcPlugin) Get(ctx context.Context, host string, opts ...ingress.GetOption) string { +func (p *grpcPlugin) GetRule(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule { if p.client == nil { - return "" + return nil } - r, err := p.client.Get(ctx, - &proto.GetRequest{ + r, err := p.client.GetRule(ctx, + &proto.GetRuleRequest{ Host: host, }) if err != nil { p.log.Error(err) - return "" + return nil + } + if r.Endpoint == "" { + return nil + } + return &ingress.Rule{ + Hostname: host, + Endpoint: r.Endpoint, } - return r.GetEndpoint() } -func (p *grpcPlugin) Set(ctx context.Context, host, endpoint string, opts ...ingress.SetOption) bool { - if p.client == nil { +func (p *grpcPlugin) SetRule(ctx context.Context, rule *ingress.Rule, opts ...ingress.Option) bool { + if p.client == nil || rule == nil { return false } - r, _ := p.client.Set(ctx, &proto.SetRequest{ - Host: host, - Endpoint: endpoint, + r, _ := p.client.SetRule(ctx, &proto.SetRuleRequest{ + Host: rule.Hostname, + Endpoint: rule.Endpoint, }) if r == nil { return false @@ -85,20 +91,20 @@ func (p *grpcPlugin) Close() error { return nil } -type httpPluginGetRequest struct { +type httpPluginGetRuleRequest struct { Host string `json:"host"` } -type httpPluginGetResponse struct { +type httpPluginGetRuleResponse struct { Endpoint string `json:"endpoint"` } -type httpPluginSetRequest struct { +type httpPluginSetRuleRequest struct { Host string `json:"host"` Endpoint string `json:"endpoint"` } -type httpPluginSetResponse struct { +type httpPluginSetRuleResponse struct { OK bool `json:"ok"` } @@ -127,14 +133,14 @@ func NewHTTPPlugin(name string, url string, opts ...plugin.Option) ingress.Ingre } } -func (p *httpPlugin) Get(ctx context.Context, host string, opts ...ingress.GetOption) (endpoint string) { +func (p *httpPlugin) GetRule(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule { if p.client == nil { - return + return nil } req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.url, nil) if err != nil { - return + return nil } if p.header != nil { req.Header = p.header.Clone() @@ -147,29 +153,35 @@ func (p *httpPlugin) Get(ctx context.Context, host string, opts ...ingress.GetOp resp, err := p.client.Do(req) if err != nil { - return + return nil } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return + return nil } - res := httpPluginGetResponse{} + res := httpPluginGetRuleResponse{} if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { - return + return nil + } + if res.Endpoint == "" { + return nil + } + return &ingress.Rule{ + Hostname: host, + Endpoint: res.Endpoint, } - return res.Endpoint } -func (p *httpPlugin) Set(ctx context.Context, host, endpoint string, opts ...ingress.SetOption) bool { - if p.client == nil { +func (p *httpPlugin) SetRule(ctx context.Context, rule *ingress.Rule, opts ...ingress.Option) bool { + if p.client == nil || rule == nil { return false } - rb := httpPluginSetRequest{ - Host: host, - Endpoint: endpoint, + rb := httpPluginSetRuleRequest{ + Host: rule.Hostname, + Endpoint: rule.Endpoint, } v, err := json.Marshal(&rb) if err != nil { @@ -195,7 +207,7 @@ func (p *httpPlugin) Set(ctx context.Context, host, endpoint string, opts ...ing return false } - res := httpPluginSetResponse{ } + res := httpPluginSetRuleResponse{} if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { return false } diff --git a/internal/util/tun/config.go b/internal/util/tun/config.go index 899deb5..1253d58 100644 --- a/internal/util/tun/config.go +++ b/internal/util/tun/config.go @@ -1,12 +1,10 @@ package tun -import "net" +import ( + "net" -// Route is an IP routing entry -type Route struct { - Net net.IPNet - Gateway net.IP -} + "github.com/go-gost/core/router" +) type Config struct { Name string @@ -15,5 +13,5 @@ type Config struct { Peer string MTU int Gateway net.IP - Routes []Route + Router router.Router } diff --git a/listener/tun/listener.go b/listener/tun/listener.go index d0f7fd1..2b2cce8 100644 --- a/listener/tun/listener.go +++ b/listener/tun/listener.go @@ -8,6 +8,7 @@ import ( "github.com/go-gost/core/listener" "github.com/go-gost/core/logger" mdata "github.com/go-gost/core/metadata" + "github.com/go-gost/core/router" xnet "github.com/go-gost/x/internal/net" limiter "github.com/go-gost/x/limiter/traffic/wrapper" mdx "github.com/go-gost/x/metadata" @@ -26,6 +27,7 @@ type tunListener struct { logger logger.Logger md metadata options listener.Options + routes []*router.Route } func NewListener(opts ...listener.Option) listener.Listener { diff --git a/listener/tun/metadata.go b/listener/tun/metadata.go index 8f3f7d1..2dcd1a3 100644 --- a/listener/tun/metadata.go +++ b/listener/tun/metadata.go @@ -6,7 +6,10 @@ import ( mdata "github.com/go-gost/core/metadata" mdutil "github.com/go-gost/core/metadata/util" + "github.com/go-gost/core/router" tun_util "github.com/go-gost/x/internal/util/tun" + "github.com/go-gost/x/registry" + xrouter "github.com/go-gost/x/router" ) const ( @@ -36,9 +39,10 @@ func (l *tunListener) parseMetadata(md mdata.Metadata) (err error) { } config := &tun_util.Config{ - Name: mdutil.GetString(md, name), - Peer: mdutil.GetString(md, peer), - MTU: mdutil.GetInt(md, mtu), + Name: mdutil.GetString(md, name), + Peer: mdutil.GetString(md, peer), + MTU: mdutil.GetInt(md, mtu), + Router: registry.RouterRegistry().Get(mdutil.GetString(md, "router")), } if config.MTU <= 0 { config.MTU = defaultMTU @@ -62,35 +66,42 @@ func (l *tunListener) parseMetadata(md mdata.Metadata) (err error) { } for _, s := range strings.Split(mdutil.GetString(md, route), ",") { - var route tun_util.Route _, ipNet, _ := net.ParseCIDR(strings.TrimSpace(s)) if ipNet == nil { continue } - route.Net = *ipNet - route.Gateway = config.Gateway - config.Routes = append(config.Routes, route) + l.routes = append(l.routes, &router.Route{ + Net: ipNet, + Gateway: config.Gateway, + }) } for _, s := range mdutil.GetStrings(md, routes) { ss := strings.SplitN(s, " ", 2) if len(ss) == 2 { - var route tun_util.Route + var route router.Route _, ipNet, _ := net.ParseCIDR(strings.TrimSpace(ss[0])) if ipNet == nil { continue } - route.Net = *ipNet - route.Gateway = net.ParseIP(ss[1]) - if route.Gateway == nil { - route.Gateway = config.Gateway + route.Net = ipNet + gw := net.ParseIP(ss[1]) + if gw == nil { + gw = config.Gateway } - config.Routes = append(config.Routes, route) + l.routes = append(l.routes, &router.Route{ + Net: ipNet, + Gateway: gw, + }) } } + if config.Router == nil && len(l.routes) > 0 { + config.Router = xrouter.NewRouter(xrouter.RoutesOption(l.routes)) + } + l.md.config = config return diff --git a/listener/tun/tun_darwin.go b/listener/tun/tun_darwin.go index 9cb4eba..2892acf 100644 --- a/listener/tun/tun_darwin.go +++ b/listener/tun/tun_darwin.go @@ -6,8 +6,6 @@ import ( "net" "os/exec" "strings" - - tun_util "github.com/go-gost/x/internal/util/tun" ) const ( @@ -45,8 +43,8 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net. return } -func (l *tunListener) addRoutes(ifName string, routes ...tun_util.Route) error { - for _, route := range routes { +func (l *tunListener) addRoutes(ifName string) error { + for _, route := range l.routes { cmd := fmt.Sprintf("route add -net %s -interface %s", route.Net.String(), ifName) l.logger.Debug(cmd) args := strings.Split(cmd, " ") diff --git a/listener/tun/tun_linux.go b/listener/tun/tun_linux.go index be66f2c..8f6b47b 100644 --- a/listener/tun/tun_linux.go +++ b/listener/tun/tun_linux.go @@ -6,8 +6,6 @@ import ( "net" "github.com/vishvananda/netlink" - - tun_util "github.com/go-gost/x/internal/util/tun" ) func (l *tunListener) createTun() (dev io.ReadWriteCloser, name string, ip net.IP, err error) { @@ -42,17 +40,17 @@ func (l *tunListener) createTun() (dev io.ReadWriteCloser, name string, ip net.I return } - if err = l.addRoutes(ifce, l.md.config.Routes...); err != nil { + if err = l.addRoutes(ifce); err != nil { return } return } -func (l *tunListener) addRoutes(ifce *net.Interface, routes ...tun_util.Route) error { - for _, route := range routes { +func (l *tunListener) addRoutes(ifce *net.Interface) error { + for _, route := range l.routes { r := netlink.Route{ - Dst: &route.Net, + Dst: route.Net, Gw: route.Gateway, } if r.Gw == nil { diff --git a/listener/tun/tun_unix.go b/listener/tun/tun_unix.go index 191607b..2ed6b43 100644 --- a/listener/tun/tun_unix.go +++ b/listener/tun/tun_unix.go @@ -8,8 +8,6 @@ import ( "net" "os/exec" "strings" - - tun_util "github.com/go-gost/x/internal/util/tun" ) const ( @@ -45,8 +43,8 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net. return } -func (l *tunListener) addRoutes(ifName string, routes ...tun_util.Route) error { - for _, route := range routes { +func (l *tunListener) addRoutes(ifName string) error { + for _, route := range l.routes { cmd := fmt.Sprintf("route add -net %s -interface %s", route.Net.String(), ifName) l.logger.Debug(cmd) args := strings.Split(cmd, " ") diff --git a/listener/tun/tun_windows.go b/listener/tun/tun_windows.go index 971dc2c..ddcb82c 100644 --- a/listener/tun/tun_windows.go +++ b/listener/tun/tun_windows.go @@ -6,8 +6,6 @@ import ( "net" "os/exec" "strings" - - tun_util "github.com/go-gost/x/internal/util/tun" ) const ( @@ -45,8 +43,8 @@ func (l *tunListener) createTun() (ifce io.ReadWriteCloser, name string, ip net. return } -func (l *tunListener) addRoutes(ifName string, gw net.IP, routes ...tun_util.Route) error { - for _, route := range routes { +func (l *tunListener) addRoutes(ifName string, gw net.IP) error { + for _, route := range l.routes { l.deleteRoute(ifName, route.Net.String()) cmd := fmt.Sprintf("netsh interface ip add route prefix=%s interface=%s store=active", diff --git a/registry/ingress.go b/registry/ingress.go index 19c91e7..837773b 100644 --- a/registry/ingress.go +++ b/registry/ingress.go @@ -30,19 +30,19 @@ type ingressWrapper struct { r *ingressRegistry } -func (w *ingressWrapper) Get(ctx context.Context, host string, opts ...ingress.GetOption) string { +func (w *ingressWrapper) GetRule(ctx context.Context, host string, opts ...ingress.Option) *ingress.Rule { v := w.r.get(w.name) if v == nil { - return "" + return nil } - return v.Get(ctx, host, opts...) + return v.GetRule(ctx, host, opts...) } -func (w *ingressWrapper) Set(ctx context.Context, host, endpoint string, opts ...ingress.SetOption) bool { +func (w *ingressWrapper) SetRule(ctx context.Context, rule *ingress.Rule, opts ...ingress.Option) bool { v := w.r.get(w.name) if v == nil { return false } - return v.Set(ctx, host, endpoint, opts...) + return v.SetRule(ctx, rule, opts...) } diff --git a/registry/registry.go b/registry/registry.go index 9a3731d..1f4277d 100644 --- a/registry/registry.go +++ b/registry/registry.go @@ -18,6 +18,7 @@ import ( "github.com/go-gost/core/recorder" reg "github.com/go-gost/core/registry" "github.com/go-gost/core/resolver" + "github.com/go-gost/core/router" "github.com/go-gost/core/sd" "github.com/go-gost/core/service" ) @@ -46,6 +47,7 @@ var ( rateLimiterReg reg.Registry[rate.RateLimiter] = new(rateLimiterRegistry) ingressReg reg.Registry[ingress.Ingress] = new(ingressRegistry) + routerReg reg.Registry[router.Router] = new(routerRegistry) sdReg reg.Registry[sd.SD] = new(sdRegistry) ) @@ -166,6 +168,10 @@ func IngressRegistry() reg.Registry[ingress.Ingress] { return ingressReg } +func RouterRegistry() reg.Registry[router.Router] { + return routerReg +} + func SDRegistry() reg.Registry[sd.SD] { return sdReg } diff --git a/registry/router.go b/registry/router.go new file mode 100644 index 0000000..b007106 --- /dev/null +++ b/registry/router.go @@ -0,0 +1,49 @@ +package registry + +import ( + "context" + "net" + + "github.com/go-gost/core/router" +) + +type routerRegistry struct { + registry[router.Router] +} + +func (r *routerRegistry) Register(name string, v router.Router) error { + return r.registry.Register(name, v) +} + +func (r *routerRegistry) Get(name string) router.Router { + if name != "" { + return &routerWrapper{name: name, r: r} + } + return nil +} + +func (r *routerRegistry) get(name string) router.Router { + return r.registry.Get(name) +} + +type routerWrapper struct { + name string + r *routerRegistry +} + +func (w *routerWrapper) GetRoute(ctx context.Context, dst net.IP, opts ...router.Option) *router.Route { + v := w.r.get(w.name) + if v == nil { + return nil + } + return v.GetRoute(ctx, dst, opts...) +} + +func (w *routerWrapper) SetRoute(ctx context.Context, route *router.Route, opts ...router.Option) bool { + v := w.r.get(w.name) + if v == nil { + return false + } + + return v.SetRoute(ctx, route, opts...) +} diff --git a/router/plugin.go b/router/plugin.go new file mode 100644 index 0000000..f5412f4 --- /dev/null +++ b/router/plugin.go @@ -0,0 +1,207 @@ +package router + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net" + "net/http" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/router" + "github.com/go-gost/plugin/router/proto" + "github.com/go-gost/x/internal/plugin" + "google.golang.org/grpc" +) + +type grpcPlugin struct { + conn grpc.ClientConnInterface + client proto.RouterClient + log logger.Logger +} + +// NewGRPCPlugin creates an Router plugin based on gRPC. +func NewGRPCPlugin(name string, addr string, opts ...plugin.Option) router.Router { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + log := logger.Default().WithFields(map[string]any{ + "kind": "router", + "router": name, + }) + conn, err := plugin.NewGRPCConn(addr, &options) + if err != nil { + log.Error(err) + } + + p := &grpcPlugin{ + conn: conn, + log: log, + } + if conn != nil { + p.client = proto.NewRouterClient(conn) + } + return p +} + +func (p *grpcPlugin) GetRoute(ctx context.Context, dst net.IP, opts ...router.Option) *router.Route { + if p.client == nil { + return nil + } + + r, err := p.client.GetRoute(ctx, + &proto.GetRouteRequest{ + Dst: dst.String(), + }) + if err != nil { + p.log.Error(err) + return nil + } + + return ParseRoute(r.Net, r.Gateway) +} + +func (p *grpcPlugin) SetRoute(ctx context.Context, route *router.Route, opts ...router.Option) bool { + if p.client == nil || route == nil { + return false + } + + r, _ := p.client.SetRoute(ctx, &proto.SetRouteRequest{ + Net: route.Net.String(), + Gateway: route.Gateway.String(), + }) + if r == nil { + return false + } + + return r.Ok +} + +func (p *grpcPlugin) Close() error { + if closer, ok := p.conn.(io.Closer); ok { + return closer.Close() + } + return nil +} + +type httpPluginGetRouteRequest struct { + Dst string `json:"dst"` +} + +type httpPluginGetRouteResponse struct { + Net string `json:"net"` + Gateway string `json:"gateway"` +} + +type httpPluginSetRouteRequest struct { + Net string `json:"net"` + Gateway string `json:"gateway"` +} + +type httpPluginSetRouteResponse struct { + OK bool `json:"ok"` +} + +type httpPlugin struct { + url string + client *http.Client + header http.Header + log logger.Logger +} + +// NewHTTPPlugin creates an Router plugin based on HTTP. +func NewHTTPPlugin(name string, url string, opts ...plugin.Option) router.Router { + var options plugin.Options + for _, opt := range opts { + opt(&options) + } + + return &httpPlugin{ + url: url, + client: plugin.NewHTTPClient(&options), + header: options.Header, + log: logger.Default().WithFields(map[string]any{ + "kind": "router", + "router": name, + }), + } +} + +func (p *httpPlugin) GetRoute(ctx context.Context, dst net.IP, opts ...router.Option) *router.Route { + if p.client == nil { + return nil + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.url, nil) + if err != nil { + return nil + } + if p.header != nil { + req.Header = p.header.Clone() + } + req.Header.Set("Content-Type", "application/json") + + q := req.URL.Query() + q.Set("dst", dst.String()) + req.URL.RawQuery = q.Encode() + + resp, err := p.client.Do(req) + if err != nil { + return nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil + } + + res := httpPluginGetRouteResponse{} + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return nil + } + + return ParseRoute(res.Net, res.Gateway) +} + +func (p *httpPlugin) SetRoute(ctx context.Context, route *router.Route, opts ...router.Option) bool { + if p.client == nil || route == nil { + return false + } + + rb := httpPluginSetRouteRequest{ + Net: route.Net.String(), + Gateway: route.Gateway.String(), + } + v, err := json.Marshal(&rb) + if err != nil { + return false + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPut, p.url, bytes.NewReader(v)) + if err != nil { + return false + } + + if p.header != nil { + req.Header = p.header.Clone() + } + req.Header.Set("Content-Type", "application/json") + resp, err := p.client.Do(req) + if err != nil { + return false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return false + } + + res := httpPluginSetRouteResponse{} + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return false + } + return res.OK +} diff --git a/router/router.go b/router/router.go new file mode 100644 index 0000000..a491ad4 --- /dev/null +++ b/router/router.go @@ -0,0 +1,265 @@ +package router + +import ( + "bufio" + "context" + "io" + "net" + "strings" + "sync" + "time" + + "github.com/go-gost/core/logger" + "github.com/go-gost/core/router" + "github.com/go-gost/x/internal/loader" +) + +type options struct { + routes []*router.Route + fileLoader loader.Loader + redisLoader loader.Loader + httpLoader loader.Loader + period time.Duration + logger logger.Logger +} + +type Option func(opts *options) + +func RoutesOption(routes []*router.Route) Option { + return func(opts *options) { + opts.routes = routes + } +} + +func ReloadPeriodOption(period time.Duration) Option { + return func(opts *options) { + opts.period = period + } +} + +func FileLoaderOption(fileLoader loader.Loader) Option { + return func(opts *options) { + opts.fileLoader = fileLoader + } +} + +func RedisLoaderOption(redisLoader loader.Loader) Option { + return func(opts *options) { + opts.redisLoader = redisLoader + } +} + +func HTTPLoaderOption(httpLoader loader.Loader) Option { + return func(opts *options) { + opts.httpLoader = httpLoader + } +} + +func LoggerOption(logger logger.Logger) Option { + return func(opts *options) { + opts.logger = logger + } +} + +type localRouter struct { + routes []*router.Route + cancelFunc context.CancelFunc + options options + mu sync.RWMutex +} + +// NewRouter creates and initializes a new Router. +func NewRouter(opts ...Option) router.Router { + var options options + for _, opt := range opts { + opt(&options) + } + + ctx, cancel := context.WithCancel(context.TODO()) + + r := &localRouter{ + cancelFunc: cancel, + options: options, + } + + if err := r.reload(ctx); err != nil { + options.logger.Warnf("reload: %v", err) + } + if r.options.period > 0 { + go r.periodReload(ctx) + } + + return r +} + +func (p *localRouter) periodReload(ctx context.Context) error { + period := p.options.period + if period < time.Second { + period = time.Second + } + ticker := time.NewTicker(period) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if err := p.reload(ctx); err != nil { + p.options.logger.Warnf("reload: %v", err) + // return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func (r *localRouter) reload(ctx context.Context) error { + routes := r.options.routes + + v, err := r.load(ctx) + if err != nil { + return err + } + routes = append(routes, v...) + + r.mu.Lock() + defer r.mu.Unlock() + + r.routes = routes + + return nil +} + +func (p *localRouter) load(ctx context.Context) (routes []*router.Route, err error) { + if p.options.fileLoader != nil { + if lister, ok := p.options.fileLoader.(loader.Lister); ok { + list, er := lister.List(ctx) + if er != nil { + p.options.logger.Warnf("file loader: %v", er) + } + for _, s := range list { + routes = append(routes, p.parseLine(s)) + } + } else { + fr, er := p.options.fileLoader.Load(ctx) + if er != nil { + p.options.logger.Warnf("file loader: %v", er) + } + if v, _ := p.parseRoutes(fr); v != nil { + routes = append(routes, v...) + } + } + } + if p.options.redisLoader != nil { + if lister, ok := p.options.redisLoader.(loader.Lister); ok { + list, er := lister.List(ctx) + if er != nil { + p.options.logger.Warnf("redis loader: %v", er) + } + for _, v := range list { + routes = append(routes, p.parseLine(v)) + } + } else { + r, er := p.options.redisLoader.Load(ctx) + if er != nil { + p.options.logger.Warnf("redis loader: %v", er) + } + v, _ := p.parseRoutes(r) + routes = append(routes, v...) + } + } + if p.options.httpLoader != nil { + r, er := p.options.httpLoader.Load(ctx) + if er != nil { + p.options.logger.Warnf("http loader: %v", er) + } + v, _ := p.parseRoutes(r) + routes = append(routes, v...) + } + + p.options.logger.Debugf("load items %d", len(routes)) + return +} + +func (p *localRouter) parseRoutes(r io.Reader) (routes []*router.Route, err error) { + if r == nil { + return + } + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + if route := p.parseLine(scanner.Text()); route != nil { + routes = append(routes, route) + } + } + + err = scanner.Err() + return +} + +func (p *localRouter) GetRoute(ctx context.Context, dst net.IP, opts ...router.Option) *router.Route { + if dst == nil || p == nil { + return nil + } + + p.mu.RLock() + routes := p.routes + p.mu.RUnlock() + + for _, route := range routes { + if route.Net != nil && route.Net.Contains(dst) { + return route + } + } + return nil +} + +func (*localRouter) SetRoute(ctx context.Context, route *router.Route, opts ...router.Option) bool { + return false +} + +func (*localRouter) parseLine(s string) (route *router.Route) { + line := strings.Replace(s, "\t", " ", -1) + line = strings.TrimSpace(line) + if n := strings.IndexByte(line, '#'); n >= 0 { + line = line[:n] + } + var sp []string + for _, s := range strings.Split(line, " ") { + if s = strings.TrimSpace(s); s != "" { + sp = append(sp, s) + } + } + if len(sp) < 2 { + return // invalid lines are ignored + } + + return ParseRoute(sp[0], sp[1]) +} + +func (p *localRouter) Close() error { + p.cancelFunc() + if p.options.fileLoader != nil { + p.options.fileLoader.Close() + } + if p.options.redisLoader != nil { + p.options.redisLoader.Close() + } + return nil +} + +func ParseRoute(dst string, gateway string) *router.Route { + _, ipNet, _ := net.ParseCIDR(dst) + if ipNet == nil { + return nil + } + gw := net.ParseIP(gateway) + if gw == nil { + return nil + } + + return &router.Route{ + Net: ipNet, + Gateway: gw, + } +}