66f502b153
When a TUN listener with `route=::/0` (or any route) creates its fallback @internal router during parseMetadata/Init, the router immediately calls setSysRoutes before the TUN device exists. This causes an ENETUNREACH "no route to host" error logged by the kernel because the gateway is unreachable at that point. The system routes are correctly added later by the listener's own addRoutes() after device creation. The @internal router only needs in-memory routes for GetRoute lookups. Add NoSysRouteOption which disables netlink route management while keeping route data for in-memory queries. Use it in the TUN listener's @internal router. Fixes go-gost/x#48
324 lines
7.6 KiB
Go
324 lines
7.6 KiB
Go
package router
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"io"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/go-gost/core/logger"
|
|
"github.com/go-gost/core/router"
|
|
xlogger "github.com/go-gost/x/logger"
|
|
|
|
"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
|
|
noSysRoute bool
|
|
logger logger.Logger
|
|
}
|
|
|
|
// Option is a functional option for configuring a Router.
|
|
type Option func(opts *options)
|
|
|
|
// RoutesOption sets the static routes for the router.
|
|
func RoutesOption(routes []*router.Route) Option {
|
|
return func(opts *options) {
|
|
opts.routes = routes
|
|
}
|
|
}
|
|
|
|
// ReloadPeriodOption sets the interval for periodic route reloading.
|
|
func ReloadPeriodOption(period time.Duration) Option {
|
|
return func(opts *options) {
|
|
opts.period = period
|
|
}
|
|
}
|
|
|
|
// FileLoaderOption sets the file-based loader for route data.
|
|
func FileLoaderOption(fileLoader loader.Loader) Option {
|
|
return func(opts *options) {
|
|
opts.fileLoader = fileLoader
|
|
}
|
|
}
|
|
|
|
// RedisLoaderOption sets the Redis-based loader for route data.
|
|
func RedisLoaderOption(redisLoader loader.Loader) Option {
|
|
return func(opts *options) {
|
|
opts.redisLoader = redisLoader
|
|
}
|
|
}
|
|
|
|
// HTTPLoaderOption sets the HTTP-based loader for route data.
|
|
func HTTPLoaderOption(httpLoader loader.Loader) Option {
|
|
return func(opts *options) {
|
|
opts.httpLoader = httpLoader
|
|
}
|
|
}
|
|
|
|
// NoSysRouteOption disables automatic system route management via netlink.
|
|
// When set, the router only maintains in-memory routes for GetRoute lookups
|
|
// and does not attempt to add or replace routes in the OS routing table.
|
|
// This is useful when another component (e.g. a TUN listener) manages system
|
|
// routes independently.
|
|
func NoSysRouteOption() Option {
|
|
return func(opts *options) {
|
|
opts.noSysRoute = true
|
|
}
|
|
}
|
|
|
|
// LoggerOption sets the logger for the router.
|
|
func LoggerOption(logger logger.Logger) Option {
|
|
return func(opts *options) {
|
|
opts.logger = logger
|
|
}
|
|
}
|
|
|
|
// localRouter is the built-in Router implementation. It manages static routes
|
|
// loaded from config and dynamic routes loaded from external sources (file,
|
|
// Redis, HTTP), with support for periodic hot-reload.
|
|
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)
|
|
}
|
|
if options.logger == nil {
|
|
options.logger = xlogger.Nop()
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
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
|
|
}
|
|
|
|
// periodReload periodically reloads routes from external sources until the
|
|
// context is cancelled.
|
|
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)
|
|
}
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// reload loads routes from all configured sources and updates the local state.
|
|
func (p *localRouter) reload(ctx context.Context) error {
|
|
routes := p.options.routes
|
|
|
|
v, err := p.load(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
routes = append(routes, v...)
|
|
|
|
p.options.logger.Debugf("load items %d", len(routes))
|
|
|
|
if !p.options.noSysRoute {
|
|
if err := p.setSysRoutes(routes...); err != nil {
|
|
p.options.logger.Error(err)
|
|
}
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
p.routes = routes
|
|
|
|
return nil
|
|
}
|
|
|
|
// load reads routes from all configured external loaders (file, Redis, HTTP).
|
|
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 fr != nil {
|
|
if v, er := p.parseRoutes(fr); er != nil {
|
|
p.options.logger.Warnf("file loader parse: %v", er)
|
|
} else if 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)
|
|
}
|
|
if r != nil {
|
|
if v, er := p.parseRoutes(r); er != nil {
|
|
p.options.logger.Warnf("redis loader parse: %v", er)
|
|
} else {
|
|
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)
|
|
}
|
|
if r != nil {
|
|
if v, er := p.parseRoutes(r); er != nil {
|
|
p.options.logger.Warnf("http loader parse: %v", er)
|
|
} else {
|
|
routes = append(routes, v...)
|
|
}
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// parseRoutes reads routes from an io.Reader, one route per line.
|
|
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
|
|
}
|
|
|
|
// GetRoute returns the route matching the given destination address. It matches
|
|
// by exact destination string or by CIDR network containment.
|
|
func (p *localRouter) GetRoute(ctx context.Context, dst string, opts ...router.Option) *router.Route {
|
|
if dst == "" || p == nil {
|
|
return nil
|
|
}
|
|
|
|
dstIP := net.ParseIP(dst)
|
|
|
|
p.mu.RLock()
|
|
routes := p.routes
|
|
p.mu.RUnlock()
|
|
|
|
for _, route := range routes {
|
|
if route.Dst == dst || route.Net != nil && route.Net.Contains(dstIP) {
|
|
return route
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseLine parses a single route line in "destination gateway" format.
|
|
// Comments (prefixed with #) and blank lines are ignored.
|
|
func (*localRouter) parseLine(s string) (route *router.Route) {
|
|
line := strings.ReplaceAll(s, "\t", " ")
|
|
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
|
|
}
|
|
|
|
return ParseRoute(sp[0], sp[1])
|
|
}
|
|
|
|
// Close stops the periodic reload goroutine and closes all configured loaders.
|
|
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()
|
|
}
|
|
if p.options.httpLoader != nil {
|
|
p.options.httpLoader.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParseRoute parses a destination/gateway pair into a Route. The destination
|
|
// may be a CIDR notation network (e.g. "10.0.0.0/8") or a plain address. If dst
|
|
// is empty, nil is returned.
|
|
func ParseRoute(dst string, gateway string) *router.Route {
|
|
if dst == "" {
|
|
return nil
|
|
}
|
|
_, ipNet, _ := net.ParseCIDR(dst)
|
|
|
|
return &router.Route{
|
|
Net: ipNet,
|
|
Dst: dst,
|
|
Gateway: gateway,
|
|
}
|
|
}
|