diff --git a/config/config.go b/config/config.go index 0e2f4608..42b1eb36 100644 --- a/config/config.go +++ b/config/config.go @@ -252,7 +252,9 @@ type SDConfig struct { } type RouterRouteConfig struct { - Net string `json:"net"` + // Deprecated: use dst instead + Net string `yaml:",omitempty" json:"net,omitempty"` + Dst string `yaml:",omitempty" json:"dst,omitempty"` Gateway string `json:"gateway"` } diff --git a/config/parsing/router/parse.go b/config/parsing/router/parse.go index 513724a1..3c355c02 100644 --- a/config/parsing/router/parse.go +++ b/config/parsing/router/parse.go @@ -46,17 +46,23 @@ func ParseRouter(cfg *config.RouterConfig) router.Router { var routes []*router.Route for _, route := range cfg.Routes { _, ipNet, _ := net.ParseCIDR(route.Net) - if ipNet == nil { - continue + dst := route.Dst + if dst != "" { + _, ipNet, _ = net.ParseCIDR(dst) + } else { + if ipNet != nil { + dst = ipNet.String() + } } - gw := net.ParseIP(route.Gateway) - if gw == nil { + + if dst == "" || route.Gateway == "" { continue } routes = append(routes, &router.Route{ Net: ipNet, - Gateway: gw, + Dst: dst, + Gateway: route.Gateway, }) } opts := []xrouter.Option{ diff --git a/connector/relay/conn.go b/connector/relay/conn.go index e2d16a2b..932df861 100644 --- a/connector/relay/conn.go +++ b/connector/relay/conn.go @@ -192,14 +192,13 @@ func (c *bindUDPConn) Write(b []byte) (n int, err error) { return } - // 2-byte data length header - var bh [2]byte - binary.BigEndian.PutUint16(bh[:], uint16(len(b))) - _, err = c.Conn.Write(bh[:]) - if err != nil { - return - } - return c.Conn.Write(b) + buf := bufpool.Get(len(b) + 2) + defer bufpool.Put(buf) + + binary.BigEndian.PutUint16(buf[:2], uint16(len(b))) + n = copy(buf[2:], b) + + return c.Conn.Write(buf) } func (c *bindUDPConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { diff --git a/connector/router/conn.go b/connector/router/conn.go new file mode 100644 index 00000000..5e38537d --- /dev/null +++ b/connector/router/conn.go @@ -0,0 +1,73 @@ +package tunnel + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "net" + + "github.com/go-gost/core/common/bufpool" + "github.com/go-gost/relay" + xrelay "github.com/go-gost/x/internal/util/relay" +) + +type packetConn struct { + net.Conn +} + +func (c *packetConn) Read(b []byte) (n int, err error) { + var bb [2]byte + _, err = io.ReadFull(c.Conn, bb[:]) + if err != nil { + return + } + + dlen := int(binary.BigEndian.Uint16(bb[:])) + if len(b) >= dlen { + return io.ReadFull(c.Conn, b[:dlen]) + } + + buf := bufpool.Get(dlen) + defer bufpool.Put(buf) + + n, err = io.ReadFull(c.Conn, buf) + copy(b, buf[:n]) + + return +} + +func (c *packetConn) Write(b []byte) (n int, err error) { + if len(b) > math.MaxUint16 { + err = errors.New("write: data maximum exceeded") + return + } + + buf := bufpool.Get(len(b) + 2) + defer bufpool.Put(buf) + + binary.BigEndian.PutUint16(buf[:2], uint16(len(b))) + n = copy(buf[2:], b) + + return c.Conn.Write(buf) +} + +func readResponse(r io.Reader) (err error) { + resp := relay.Response{} + _, err = resp.ReadFrom(r) + if err != nil { + return + } + + if resp.Version != relay.Version1 { + err = relay.ErrBadVersion + return + } + + if resp.Status != relay.StatusOK { + err = fmt.Errorf("%d %s", resp.Status, xrelay.StatusText(resp.Status)) + return + } + return nil +} diff --git a/connector/router/connector.go b/connector/router/connector.go new file mode 100644 index 00000000..ad21fc36 --- /dev/null +++ b/connector/router/connector.go @@ -0,0 +1,117 @@ +package tunnel + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/go-gost/core/connector" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/relay" + ctxvalue "github.com/go-gost/x/ctx" + "github.com/go-gost/x/registry" +) + +func init() { + registry.ConnectorRegistry().Register("router", NewConnector) +} + +type routerConnector struct { + md metadata + options connector.Options +} + +func NewConnector(opts ...connector.Option) connector.Connector { + options := connector.Options{} + for _, opt := range opts { + opt(&options) + } + + return &routerConnector{ + options: options, + } +} + +func (c *routerConnector) Init(md md.Metadata) (err error) { + return c.parseMetadata(md) +} + +func (c *routerConnector) Connect(ctx context.Context, conn net.Conn, network, address string, opts ...connector.ConnectOption) (net.Conn, error) { + log := c.options.Logger.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "network": network, + "address": address, + "sid": string(ctxvalue.SidFromContext(ctx)), + }) + log.Debugf("connect %s/%s", address, network) + + if c.md.connectTimeout > 0 { + conn.SetDeadline(time.Now().Add(c.md.connectTimeout)) + defer conn.SetDeadline(time.Time{}) + } + + req := relay.Request{ + Version: relay.Version1, + Cmd: relay.CmdAssociate, + } + + if c.options.Auth != nil { + pwd, _ := c.options.Auth.Password() + req.Features = append(req.Features, &relay.UserAuthFeature{ + Username: c.options.Auth.Username(), + Password: pwd, + }) + } + + switch network { + case "udp": + req.Features = append(req.Features, &relay.NetworkFeature{ + Network: relay.NetworkUDP, + }) + + case "ip": + req.Features = append(req.Features, &relay.NetworkFeature{ + Network: relay.NetworkIP, + }) + } + + srcAddr := conn.LocalAddr().String() + if v := ctxvalue.ClientAddrFromContext(ctx); v != "" { + srcAddr = string(v) + } + + af := &relay.AddrFeature{} + af.ParseFrom(srcAddr) + req.Features = append(req.Features, af) // src address + + af = &relay.AddrFeature{} + af.ParseFrom(address) + req.Features = append(req.Features, af) // dst address + + req.Features = append(req.Features, &relay.TunnelFeature{ + ID: c.md.routerID, + }) + + if _, err := req.WriteTo(conn); err != nil { + return nil, err + } + // drain the response + if err := readResponse(conn); err != nil { + return nil, err + } + + switch network { + case "udp", "ip": + conn = &packetConn{ + Conn: conn, + } + default: + err := fmt.Errorf("network %s is unsupported", network) + log.Error(err) + return nil, err + } + + return conn, nil +} diff --git a/connector/router/metadata.go b/connector/router/metadata.go new file mode 100644 index 00000000..54d199e0 --- /dev/null +++ b/connector/router/metadata.go @@ -0,0 +1,42 @@ +package tunnel + +import ( + "errors" + "time" + + mdata "github.com/go-gost/core/metadata" + "github.com/go-gost/relay" + mdutil "github.com/go-gost/x/metadata/util" + "github.com/google/uuid" +) + +var ( + ErrInvalidRouterID = errors.New("router: invalid router ID") +) + +type metadata struct { + connectTimeout time.Duration + routerID relay.TunnelID +} + +func (c *routerConnector) parseMetadata(md mdata.Metadata) (err error) { + c.md.connectTimeout = mdutil.GetDuration(md, "connectTimeout") + + if s := mdutil.GetString(md, "router.id"); s != "" { + uuid, err := uuid.Parse(s) + if err != nil { + return err + } + c.md.routerID = relay.NewTunnelID(uuid[:]) + } + + if c.md.routerID.IsZero() { + uuid, err := uuid.NewUUID() + if err != nil { + return err + } + c.md.routerID = relay.NewTunnelID(uuid[:]) + } + + return +} diff --git a/connector/tunnel/conn.go b/connector/tunnel/conn.go index 2a025bce..47313883 100644 --- a/connector/tunnel/conn.go +++ b/connector/tunnel/conn.go @@ -130,14 +130,13 @@ func (c *bindUDPConn) Write(b []byte) (n int, err error) { return } - // 2-byte data length header - var bh [2]byte - binary.BigEndian.PutUint16(bh[:], uint16(len(b))) - _, err = c.Conn.Write(bh[:]) - if err != nil { - return - } - return c.Conn.Write(b) + buf := bufpool.Get(len(b) + 2) + defer bufpool.Put(buf) + + binary.BigEndian.PutUint16(buf[:2], uint16(len(b))) + n = copy(buf[2:], b) + + return c.Conn.Write(buf) } func (c *bindUDPConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) { diff --git a/dialer/quic/dialer.go b/dialer/quic/dialer.go index 8c280978..857ff5e1 100644 --- a/dialer/quic/dialer.go +++ b/dialer/quic/dialer.go @@ -111,6 +111,7 @@ func (d *quicDialer) initSession(ctx context.Context, addr net.Addr, conn net.Pa quic.Version2, }, MaxIncomingStreams: int64(d.md.maxStreams), + EnableDatagrams: d.md.enableDatagram, } tlsCfg := d.options.TLSConfig diff --git a/dialer/quic/metadata.go b/dialer/quic/metadata.go index 9d386fc0..670192fd 100644 --- a/dialer/quic/metadata.go +++ b/dialer/quic/metadata.go @@ -12,6 +12,7 @@ type metadata struct { maxIdleTimeout time.Duration handshakeTimeout time.Duration maxStreams int + enableDatagram bool cipherKey []byte } @@ -40,6 +41,7 @@ func (d *quicDialer) parseMetadata(md mdata.Metadata) (err error) { d.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) d.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout) d.md.maxStreams = mdutil.GetInt(md, maxStreams) + d.md.enableDatagram = mdutil.GetBool(md, "quic.enableDatagram", "enableDatagram") return } diff --git a/handler/router/associate.go b/handler/router/associate.go new file mode 100644 index 00000000..9ba54dc7 --- /dev/null +++ b/handler/router/associate.go @@ -0,0 +1,262 @@ +package router + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "io" + "math" + "net" + "sync" + + "github.com/go-gost/core/common/bufpool" + "github.com/go-gost/core/limiter" + "github.com/go-gost/core/logger" + "github.com/go-gost/core/observer/stats" + "github.com/go-gost/core/router" + "github.com/go-gost/core/sd" + "github.com/go-gost/relay" + xip "github.com/go-gost/x/internal/net/ip" + traffic_wrapper "github.com/go-gost/x/limiter/traffic/wrapper" + stats_wrapper "github.com/go-gost/x/observer/stats/wrapper" + "github.com/go-gost/x/registry" + "github.com/google/uuid" + "github.com/songgao/water/waterutil" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +func (h *routerHandler) handleAssociate(ctx context.Context, conn net.Conn, network, host string, routerID relay.TunnelID, log logger.Logger) (err error) { + log = log.WithFields(map[string]any{ + "dst": fmt.Sprintf("%s/%s", host, network), + "cmd": "associate", + "router": routerID.String(), + "host": host, + }) + + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + + if ingress := h.md.ingress; ingress != nil && host != "" { + var rid relay.TunnelID + if rule := ingress.GetRule(ctx, host); rule != nil { + rid = parseRouterID(rule.Endpoint) + } + + if rid.IsZero() || !rid.Equal(routerID) { + resp.Status = relay.StatusHostUnreachable + resp.WriteTo(conn) + err := fmt.Errorf("no route to host %s", host) + log.Error(err) + return err + } + } + + uuid, err := uuid.NewRandom() + if err != nil { + resp.Status = relay.StatusInternalServerError + resp.WriteTo(conn) + return + } + connectorID := relay.NewConnectorID(uuid[:]) + + resp.Features = append(resp.Features, + &relay.TunnelFeature{ + ID: connectorID, + }, + ) + resp.WriteTo(conn) + + conn = &packetConn{conn} + + clientID := fmt.Sprintf("%s@%s", host, routerID) + var stats stats.Stats + if h.stats != nil { + stats = h.stats.Stats(clientID) + } + conn = stats_wrapper.WrapConn(conn, stats) + conn = traffic_wrapper.WrapConn( + conn, + h.limiter, + clientID, + limiter.ScopeOption(limiter.ScopeClient), + limiter.ServiceOption(h.options.Service), + limiter.ClientOption(clientID), + limiter.NetworkOption(network), + limiter.SrcOption(conn.RemoteAddr().String()), + ) + + h.pool.Add(routerID, NewConnector(routerID, connectorID, host, LockWriter(conn), &ConnectorOptions{})) + defer h.pool.Del(routerID, host, connectorID) + + if h.md.sd != nil { + err := h.md.sd.Register(ctx, &sd.Service{ + ID: connectorID.String(), + Name: clientID, + Node: h.id, + Network: network, + Address: h.md.entryPoint, + }) + if err != nil { + h.log.Error(err) + } + + defer h.md.sd.Deregister(ctx, &sd.Service{ + ID: connectorID.String(), + Name: clientID, + Node: h.id, + }) + } + + log.Debugf("%s/%s: router=%s, connector=%s, weight=%d established", host, network, routerID, connectorID, connectorID.Weight()) + + return h.routing(ctx, conn, routerID, log) +} + +func (h *routerHandler) routing(ctx context.Context, conn net.Conn, routerID relay.TunnelID, log logger.Logger) error { + b := bufpool.Get(h.md.bufferSize) + defer bufpool.Put(b) + + rid := routerID.String() + + for { + err := func() error { + n, err := conn.Read(b) + if err != nil { + return err + } + + var dstIP net.IP + + if waterutil.IsIPv4(b[:n]) { + header, err := ipv4.ParseHeader(b[:n]) + if err != nil { + log.Warn(err) + return nil + } + + dstIP = header.Dst + + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", + header.Src, header.Dst, xip.Protocol(waterutil.IPv4Protocol(b[:n])), + header.Len, header.TotalLen, header.ID, header.Flags) + } + } else if waterutil.IsIPv6(b[:n]) { + header, err := ipv6.ParseHeader(b[:n]) + if err != nil { + log.Warn(err) + return nil + } + + dstIP = header.Dst + + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %s %d %d", + header.Src, header.Dst, + xip.Protocol(waterutil.IPProtocol(header.NextHeader)), + header.PayloadLen, header.TrafficClass) + } + } else { + log.Warnf("unknown packet, discarded(%d)", n) + return nil + } + + var route *router.Route + if r := registry.RouterRegistry().Get(rid); r != nil { + route = r.GetRoute(ctx, dstIP, router.IDOption(rid)) + } + if route == nil && h.md.router != nil { + route = h.md.router.GetRoute(ctx, dstIP, router.IDOption(rid)) + } + if route == nil || route.Gateway == "" { + // no route to host, discard + return nil + } + + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("route for %s: %s -> %s", dstIP, route.Dst, route.Gateway) + } + + if c := h.pool.Get(routerID, route.Gateway); c != nil { + if w := c.Writer(); w != nil { + w.Write(b[:n]) + } + } + + return nil + }() + if err != nil { + if err == io.EOF { + err = nil + } + return err + } + } +} + +type packetConn struct { + net.Conn +} + +func (c *packetConn) Read(b []byte) (n int, err error) { + var bb [2]byte + _, err = io.ReadFull(c.Conn, bb[:]) + if err != nil { + return + } + + dlen := int(binary.BigEndian.Uint16(bb[:])) + if len(b) >= dlen { + return io.ReadFull(c.Conn, b[:dlen]) + } + + buf := bufpool.Get(dlen) + defer bufpool.Put(buf) + + n, err = io.ReadFull(c.Conn, buf) + copy(b, buf[:n]) + + return +} + +func (c *packetConn) Write(b []byte) (n int, err error) { + if len(b) > math.MaxUint16 { + err = errors.New("write: data maximum exceeded") + return + } + + buf := bufpool.Get(len(b) + 2) + defer bufpool.Put(buf) + + binary.BigEndian.PutUint16(buf[:2], uint16(len(b))) + n = copy(buf[2:], b) + + return c.Conn.Write(buf) +} + +type lockWriter struct { + w io.Writer + mu sync.Mutex +} + +func LockWriter(w io.Writer) io.Writer { + return &lockWriter{w: w} +} + +func (w *lockWriter) Write(p []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + + return w.w.Write(p) +} + +func (w *lockWriter) Close() error { + if closer, ok := w.w.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/handler/router/handler.go b/handler/router/handler.go new file mode 100644 index 00000000..5a5ee5a2 --- /dev/null +++ b/handler/router/handler.go @@ -0,0 +1,268 @@ +package router + +import ( + "context" + "errors" + "net" + "strconv" + "time" + + "github.com/go-gost/core/handler" + "github.com/go-gost/core/limiter" + "github.com/go-gost/core/limiter/traffic" + "github.com/go-gost/core/logger" + md "github.com/go-gost/core/metadata" + "github.com/go-gost/core/observer" + "github.com/go-gost/core/service" + "github.com/go-gost/relay" + ctxvalue "github.com/go-gost/x/ctx" + limiter_util "github.com/go-gost/x/internal/util/limiter" + stats_util "github.com/go-gost/x/internal/util/stats" + rate_limiter "github.com/go-gost/x/limiter/rate" + "github.com/go-gost/x/registry" + "github.com/google/uuid" +) + +var ( + ErrBadVersion = errors.New("bad version") + ErrUnknownCmd = errors.New("unknown command") + ErrRouterID = errors.New("invalid router ID") + ErrRouterNotAvailable = errors.New("router not available") + ErrUnauthorized = errors.New("unauthorized") + ErrTunnelRoute = errors.New("no route to host") +) + +func init() { + registry.HandlerRegistry().Register("router", NewHandler) +} + +type routerHandler struct { + id string + options handler.Options + pool *ConnectorPool + epSvc service.Service + md metadata + log logger.Logger + stats *stats_util.HandlerStats + limiter traffic.TrafficLimiter + cancel context.CancelFunc +} + +func NewHandler(opts ...handler.Option) handler.Handler { + options := handler.Options{} + for _, opt := range opts { + opt(&options) + } + + return &routerHandler{ + options: options, + } +} + +func (h *routerHandler) Init(md md.Metadata) (err error) { + if err := h.parseMetadata(md); err != nil { + return err + } + + uuid, err := uuid.NewRandom() + if err != nil { + return err + } + h.id = uuid.String() + + h.log = h.options.Logger.WithFields(map[string]any{ + "node": h.id, + }) + h.pool = NewConnectorPool(h.id) + + /* + for _, ro := range h.options.Recorders { + if ro.Record == xrecorder.RecorderServiceHandler { + h.ep.recorder = ro + break + } + } + */ + + ctx, cancel := context.WithCancel(context.Background()) + h.cancel = cancel + + if h.options.Observer != nil { + h.stats = stats_util.NewHandlerStats(h.options.Service, h.md.observerResetTraffic) + go h.observeStats(ctx) + } + + if h.options.Limiter != nil { + h.limiter = limiter_util.NewCachedTrafficLimiter(h.options.Limiter, + limiter_util.RefreshIntervalOption(h.md.limiterRefreshInterval), + limiter_util.CleanupIntervalOption(h.md.limiterCleanupInterval), + limiter_util.ScopeOption(limiter.ScopeClient), + ) + } + + return nil +} + +func (h *routerHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler.HandleOption) (err error) { + defer conn.Close() + + start := time.Now() + log := h.log.WithFields(map[string]any{ + "remote": conn.RemoteAddr().String(), + "local": conn.LocalAddr().String(), + "sid": ctxvalue.SidFromContext(ctx), + }) + + log.Infof("%s <> %s", conn.RemoteAddr(), conn.LocalAddr()) + + defer func() { + log.WithFields(map[string]any{ + "duration": time.Since(start), + }).Infof("%s >< %s", conn.RemoteAddr(), conn.LocalAddr()) + }() + + if !h.checkRateLimit(conn.RemoteAddr()) { + return rate_limiter.ErrRateLimit + } + + if h.md.readTimeout > 0 { + conn.SetReadDeadline(time.Now().Add(h.md.readTimeout)) + } + + req := relay.Request{} + if _, err := req.ReadFrom(conn); err != nil { + return err + } + + conn.SetReadDeadline(time.Time{}) + + resp := relay.Response{ + Version: relay.Version1, + Status: relay.StatusOK, + } + + if req.Version != relay.Version1 { + resp.Status = relay.StatusBadRequest + resp.WriteTo(conn) + return ErrBadVersion + } + + var user, pass string + var srcAddr, dstAddr string + network := "ip" + var routerID relay.TunnelID + for _, f := range req.Features { + switch f.Type() { + case relay.FeatureUserAuth: + if feature, _ := f.(*relay.UserAuthFeature); feature != nil { + user, pass = feature.Username, feature.Password + } + case relay.FeatureAddr: + if feature, _ := f.(*relay.AddrFeature); feature != nil { + v := net.JoinHostPort(feature.Host, strconv.Itoa(int(feature.Port))) + if srcAddr != "" { + dstAddr = v + } else { + srcAddr = v + } + } + case relay.FeatureTunnel: + if feature, _ := f.(*relay.TunnelFeature); feature != nil { + routerID = feature.ID + } + case relay.FeatureNetwork: + if feature, _ := f.(*relay.NetworkFeature); feature != nil { + network = feature.Network.String() + } + } + } + + if routerID.IsZero() { + resp.Status = relay.StatusBadRequest + resp.WriteTo(conn) + return ErrRouterID + } + + if user != "" { + log = log.WithFields(map[string]any{"user": user}) + } + + if h.options.Auther != nil { + clientID, ok := h.options.Auther.Authenticate(ctx, user, pass) + if !ok { + resp.Status = relay.StatusUnauthorized + resp.WriteTo(conn) + return ErrUnauthorized + } + ctx = ctxvalue.ContextWithClientID(ctx, ctxvalue.ClientID(clientID)) + } + + switch req.Cmd & relay.CmdMask { + case relay.CmdAssociate: + host, _, _ := net.SplitHostPort(dstAddr) + log.Debugf("associate %s/%s -> %s", host, network, routerID) + return h.handleAssociate(ctx, conn, network, host, routerID, log) + + default: + resp.Status = relay.StatusBadRequest + resp.WriteTo(conn) + return ErrUnknownCmd + } +} + +// Close implements io.Closer interface. +func (h *routerHandler) Close() error { + if h.epSvc != nil { + h.epSvc.Close() + } + h.pool.Close() + + if h.cancel != nil { + h.cancel() + } + + return nil +} + +func (h *routerHandler) checkRateLimit(addr net.Addr) bool { + if h.options.RateLimiter == nil { + return true + } + host, _, _ := net.SplitHostPort(addr.String()) + if limiter := h.options.RateLimiter.Limiter(host); limiter != nil { + return limiter.Allow(1) + } + + return true +} + +func (h *routerHandler) observeStats(ctx context.Context) { + if h.options.Observer == nil { + return + } + + var events []observer.Event + + ticker := time.NewTicker(h.md.observerPeriod) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + if len(events) > 0 { + if err := h.options.Observer.Observe(ctx, events); err == nil { + events = nil + } + break + } + + evs := h.stats.Events() + if err := h.options.Observer.Observe(ctx, evs); err != nil { + events = evs + } + + case <-ctx.Done(): + return + } + } +} diff --git a/handler/router/metadata.go b/handler/router/metadata.go new file mode 100644 index 00000000..3bfa1476 --- /dev/null +++ b/handler/router/metadata.go @@ -0,0 +1,60 @@ +package router + +import ( + "time" + + "github.com/go-gost/core/ingress" + mdata "github.com/go-gost/core/metadata" + "github.com/go-gost/core/router" + "github.com/go-gost/core/sd" + mdutil "github.com/go-gost/x/metadata/util" + "github.com/go-gost/x/registry" +) + +const ( + defaultTTL = 15 * time.Second + defaultBufferSize = 1500 +) + +type metadata struct { + readTimeout time.Duration + bufferSize int + + entryPoint string + ingress ingress.Ingress + sd sd.SD + router router.Router + + observerPeriod time.Duration + observerResetTraffic bool + + limiterRefreshInterval time.Duration + limiterCleanupInterval time.Duration +} + +func (h *routerHandler) parseMetadata(md mdata.Metadata) (err error) { + h.md.readTimeout = mdutil.GetDuration(md, "readTimeout") + h.md.bufferSize = mdutil.GetInt(md, "router.bufferSize", "bufferSize") + if h.md.bufferSize <= 0 { + h.md.bufferSize = defaultBufferSize + } + + h.md.entryPoint = mdutil.GetString(md, "entrypoint") + h.md.ingress = registry.IngressRegistry().Get(mdutil.GetString(md, "ingress")) + h.md.sd = registry.SDRegistry().Get(mdutil.GetString(md, "sd")) + h.md.router = registry.RouterRegistry().Get(mdutil.GetString(md, "router")) + + h.md.observerPeriod = mdutil.GetDuration(md, "observePeriod", "observer.period", "observer.observePeriod") + if h.md.observerPeriod == 0 { + h.md.observerPeriod = 5 * time.Second + } + if h.md.observerPeriod < time.Second { + h.md.observerPeriod = time.Second + } + h.md.observerResetTraffic = mdutil.GetBool(md, "observer.resetTraffic") + + h.md.limiterRefreshInterval = mdutil.GetDuration(md, "limiter.refreshInterval") + h.md.limiterCleanupInterval = mdutil.GetDuration(md, "limiter.cleanupInterval") + + return +} diff --git a/handler/router/router.go b/handler/router/router.go new file mode 100644 index 00000000..24dd1bc5 --- /dev/null +++ b/handler/router/router.go @@ -0,0 +1,260 @@ +package router + +import ( + "context" + "io" + "sync" + "time" + + "github.com/go-gost/core/logger" + "github.com/go-gost/relay" + + "github.com/go-gost/x/selector" + "github.com/google/uuid" +) + +const ( + MaxWeight uint8 = 0xff +) + +type ConnectorOptions struct{} + +type Connector struct { + id relay.ConnectorID + rid relay.TunnelID + host string + w io.Writer + opts *ConnectorOptions + log logger.Logger +} + +func NewConnector(rid relay.TunnelID, cid relay.ConnectorID, host string, w io.Writer, opts *ConnectorOptions) *Connector { + if opts == nil { + opts = &ConnectorOptions{} + } + + c := &Connector{ + rid: rid, + id: cid, + host: host, + w: w, + opts: opts, + log: logger.Default().WithFields(map[string]any{ + "router": rid.String(), + "connector": cid.String(), + "host": host, + }), + } + + return c +} + +func (c *Connector) ID() relay.ConnectorID { + return c.id +} + +func (c *Connector) Writer() io.Writer { + if c == nil { + return nil + } + + return c.w +} + +func (c *Connector) Close() error { + if c == nil || c.w == nil { + return nil + } + + if closer, ok := c.w.(io.Closer); ok { + return closer.Close() + } + return nil +} + +type Router struct { + node string + id relay.TunnelID + connectors map[string][]*Connector + t time.Time + close chan struct{} + mu sync.RWMutex +} + +func NewRouter(node string, rid relay.TunnelID) *Router { + r := &Router{ + node: node, + id: rid, + connectors: make(map[string][]*Connector), + t: time.Now(), + close: make(chan struct{}), + } + return r +} + +func (r *Router) ID() relay.TunnelID { + return r.id +} + +func (r *Router) AddConnector(c *Connector) { + if c == nil { + return + } + + r.mu.Lock() + defer r.mu.Unlock() + + r.connectors[c.host] = append(r.connectors[c.host], c) +} + +func (r *Router) GetConnector(host string) *Connector { + r.mu.RLock() + defer r.mu.RUnlock() + + connectors := r.connectors[host] + + if len(connectors) == 1 { + return connectors[0] + } + + rw := selector.NewRandomWeighted[*Connector]() + + found := false + for _, c := range connectors { + weight := c.ID().Weight() + if weight == 0 { + weight = 1 + } + if weight == MaxWeight && !found { + rw.Reset() + found = true + } + + if weight == MaxWeight || !found { + rw.Add(c, int(weight)) + } + } + + return rw.Next() +} + +func (r *Router) DelConnector(host string, cid relay.ConnectorID) { + r.mu.RLock() + defer r.mu.RUnlock() + + connectors := r.connectors[host] + for i, c := range connectors { + if c.id.Equal(cid) { + r.connectors[host] = append(connectors[:i], connectors[i+1:]...) + } + } +} + +func (r *Router) Close() error { + r.mu.Lock() + defer r.mu.Unlock() + + select { + case <-r.close: + default: + for _, cs := range r.connectors { + for _, c := range cs { + c.Close() + } + } + close(r.close) + + clear(r.connectors) + } + + return nil +} + +type ConnectorPool struct { + node string + routers map[relay.TunnelID]*Router + mu sync.RWMutex + cancel context.CancelFunc +} + +func NewConnectorPool(node string) *ConnectorPool { + p := &ConnectorPool{ + node: node, + routers: make(map[relay.TunnelID]*Router), + } + + return p +} + +func (p *ConnectorPool) Add(rid relay.TunnelID, c *Connector) { + p.mu.Lock() + defer p.mu.Unlock() + + r := p.routers[rid] + if r == nil { + r = NewRouter(p.node, rid) + p.routers[rid] = r + } + r.AddConnector(c) +} + +func (p *ConnectorPool) Get(rid relay.TunnelID, host string) *Connector { + if p == nil { + return nil + } + + p.mu.RLock() + defer p.mu.RUnlock() + + r := p.routers[rid] + if r == nil { + return nil + } + + return r.GetConnector(host) +} + +func (p *ConnectorPool) Del(rid relay.TunnelID, host string, cid relay.ConnectorID) { + if p == nil { + return + } + + p.mu.RLock() + defer p.mu.RUnlock() + + r := p.routers[rid] + if r == nil { + return + } + + r.DelConnector(host, cid) +} + +func (p *ConnectorPool) Close() error { + if p == nil { + return nil + } + + if p.cancel != nil { + p.cancel() + } + + p.mu.Lock() + defer p.mu.Unlock() + + for _, v := range p.routers { + v.Close() + } + clear(p.routers) + + return nil +} + +func parseRouterID(s string) (rid relay.TunnelID) { + if s == "" { + return + } + uuid, _ := uuid.Parse(s) + + return relay.NewTunnelID(uuid[:]) +} diff --git a/handler/tun/client.go b/handler/tun/client.go index 926ab4cd..1c68770f 100644 --- a/handler/tun/client.go +++ b/handler/tun/client.go @@ -3,12 +3,15 @@ package tun import ( "bytes" "context" + "errors" + "fmt" "io" "net" "time" "github.com/go-gost/core/common/bufpool" "github.com/go-gost/core/logger" + xip "github.com/go-gost/x/internal/net/ip" tun_util "github.com/go-gost/x/internal/util/tun" "github.com/songgao/water/waterutil" "golang.org/x/net/ipv4" @@ -24,7 +27,7 @@ var ( magicHeader = []byte("GOST") ) -func (h *tunHandler) handleClient(ctx context.Context, conn net.Conn, raddr string, config *tun_util.Config, log logger.Logger) error { +func (h *tunHandler) handleClient(ctx context.Context, conn net.Conn, network string, raddr string, config *tun_util.Config, log logger.Logger) error { var ips []net.IP for _, net := range config.Net { ips = append(ips, net.IP) @@ -35,20 +38,22 @@ func (h *tunHandler) handleClient(ctx context.Context, conn net.Conn, raddr stri for { err := func() error { - cc, err := h.options.Router.Dial(ctx, "udp", raddr) + cc, err := h.options.Router.Dial(ctx, network, raddr) if err != nil { return err } defer cc.Close() - ctx, cancel := context.WithCancel(ctx) - defer cancel() + if network == "udp" { + ctx, cancel := context.WithCancel(ctx) + defer cancel() - go h.keepalive(ctx, cc, ips) + go h.keepalive(ctx, cc, ips) + } - return h.transportClient(conn, cc, log) + return h.transportClient(ctx, conn, cc, log) }() - if err == ErrTun { + if errors.Is(err, ErrTun) { return err } @@ -94,8 +99,8 @@ func (h *tunHandler) keepalive(ctx context.Context, conn net.Conn, ips []net.IP) } } -func (h *tunHandler) transportClient(tun io.ReadWriter, conn net.Conn, log logger.Logger) error { - errc := make(chan error, 1) +func (h *tunHandler) transportClient(ctx context.Context, tun io.ReadWriter, conn net.Conn, log logger.Logger) error { + errc := make(chan error, 2) go func() { for { @@ -105,7 +110,7 @@ func (h *tunHandler) transportClient(tun io.ReadWriter, conn net.Conn, log logge n, err := tun.Read(b) if err != nil { - return ErrTun + return fmt.Errorf("%w: %s", ErrTun, err.Error()) } if waterutil.IsIPv4(b[:n]) { @@ -115,9 +120,11 @@ func (h *tunHandler) transportClient(tun io.ReadWriter, conn net.Conn, log logge return nil } - log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", - header.Src, header.Dst, ipProtocol(waterutil.IPv4Protocol(b[:n])), - header.Len, header.TotalLen, header.ID, header.Flags) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", + header.Src, header.Dst, xip.Protocol(waterutil.IPv4Protocol(b[:n])), + header.Len, header.TotalLen, header.ID, header.Flags) + } } else if waterutil.IsIPv6(b[:n]) { header, err := ipv6.ParseHeader(b[:n]) if err != nil { @@ -125,10 +132,12 @@ func (h *tunHandler) transportClient(tun io.ReadWriter, conn net.Conn, log logge return nil } - log.Tracef("%s >> %s %s %d %d", - header.Src, header.Dst, - ipProtocol(waterutil.IPProtocol(header.NextHeader)), - header.PayloadLen, header.TrafficClass) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %s %d %d", + header.Src, header.Dst, + xip.Protocol(waterutil.IPProtocol(header.NextHeader)), + header.PayloadLen, header.TrafficClass) + } } else { log.Warnf("unknown packet, discarded(%d)", n) return nil @@ -166,16 +175,18 @@ func (h *tunHandler) transportClient(tun io.ReadWriter, conn net.Conn, log logge return nil } - if waterutil.IsIPv4(b[:n]) { + if waterutil.IsIPv4(b) { header, err := ipv4.ParseHeader(b[:n]) if err != nil { log.Warn(err) return nil } - log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", - header.Src, header.Dst, ipProtocol(waterutil.IPv4Protocol(b[:n])), - header.Len, header.TotalLen, header.ID, header.Flags) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", + header.Src, header.Dst, xip.Protocol(waterutil.IPv4Protocol(b[:n])), + header.Len, header.TotalLen, header.ID, header.Flags) + } } else if waterutil.IsIPv6(b[:n]) { header, err := ipv6.ParseHeader(b[:n]) if err != nil { @@ -183,17 +194,19 @@ func (h *tunHandler) transportClient(tun io.ReadWriter, conn net.Conn, log logge return nil } - log.Tracef("%s > %s %s %d %d", - header.Src, header.Dst, - ipProtocol(waterutil.IPProtocol(header.NextHeader)), - header.PayloadLen, header.TrafficClass) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s > %s %s %d %d", + header.Src, header.Dst, + xip.Protocol(waterutil.IPProtocol(header.NextHeader)), + header.PayloadLen, header.TrafficClass) + } } else { log.Warn("unknown packet, discarded") return nil } if _, err = tun.Write(b[:n]); err != nil { - return ErrTun + return fmt.Errorf("%w: %s", ErrTun, err.Error()) } return nil }() @@ -205,9 +218,14 @@ func (h *tunHandler) transportClient(tun io.ReadWriter, conn net.Conn, log logge } }() - err := <-errc - if err != nil && err == io.EOF { - err = nil + select { + case err := <-errc: + if err != nil && err == io.EOF { + err = nil + } + return err + + case <-ctx.Done(): + return ctx.Err() } - return err } diff --git a/handler/tun/handler.go b/handler/tun/handler.go index dc2edc60..dd77e934 100644 --- a/handler/tun/handler.go +++ b/handler/tun/handler.go @@ -12,10 +12,9 @@ import ( "github.com/go-gost/core/handler" "github.com/go-gost/core/hop" md "github.com/go-gost/core/metadata" + ctxvalue "github.com/go-gost/x/ctx" tun_util "github.com/go-gost/x/internal/util/tun" "github.com/go-gost/x/registry" - "github.com/songgao/water/waterutil" - ctxvalue "github.com/go-gost/x/ctx" ) var ( @@ -90,12 +89,17 @@ func (h *tunHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. target = h.hop.Select(ctx) } if target != nil { + network := "udp" + if _, _, err := net.SplitHostPort(target.Addr); err != nil { + network = "ip" + } + log = log.WithFields(map[string]any{ - "dst": fmt.Sprintf("%s/%s", target.Addr, "udp"), + "dst": fmt.Sprintf("%s/%s", target.Addr, network), }) log.Debugf("%s >> %s", conn.RemoteAddr(), target.Addr) - if err := h.handleClient(ctx, conn, target.Addr, config, log); err != nil { + if err := h.handleClient(ctx, conn, network, target.Addr, config, log); err != nil { log.Error(err) } return nil @@ -104,25 +108,6 @@ func (h *tunHandler) Handle(ctx context.Context, conn net.Conn, opts ...handler. return h.handleServer(ctx, conn, config, log) } -var mIPProts = map[waterutil.IPProtocol]string{ - waterutil.HOPOPT: "HOPOPT", - waterutil.ICMP: "ICMP", - waterutil.IGMP: "IGMP", - waterutil.GGP: "GGP", - waterutil.TCP: "TCP", - waterutil.UDP: "UDP", - waterutil.IPv6_Route: "IPv6-Route", - waterutil.IPv6_Frag: "IPv6-Frag", - waterutil.IPv6_ICMP: "IPv6-ICMP", -} - -func ipProtocol(p waterutil.IPProtocol) string { - if v, ok := mIPProts[p]; ok { - return v - } - return fmt.Sprintf("unknown(%d)", p) -} - type tunRouteKey [16]byte func ipToTunRouteKey(ip net.IP) (key tunRouteKey) { diff --git a/handler/tun/server.go b/handler/tun/server.go index ad22d20a..8e070940 100644 --- a/handler/tun/server.go +++ b/handler/tun/server.go @@ -11,6 +11,7 @@ import ( "github.com/go-gost/core/common/bufpool" "github.com/go-gost/core/logger" "github.com/go-gost/core/router" + xip "github.com/go-gost/x/internal/net/ip" tun_util "github.com/go-gost/x/internal/util/tun" "github.com/songgao/water/waterutil" "golang.org/x/net/ipv4" @@ -63,9 +64,11 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con } src, dst = header.Src, header.Dst - log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", - src, dst, ipProtocol(waterutil.IPv4Protocol(b[:n])), - header.Len, header.TotalLen, header.ID, header.Flags) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", + src, dst, xip.Protocol(waterutil.IPv4Protocol(b[:n])), + header.Len, header.TotalLen, header.ID, header.Flags) + } } else if waterutil.IsIPv6(b[:n]) { header, err := ipv6.ParseHeader(b[:n]) if err != nil { @@ -74,10 +77,12 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con } src, dst = header.Src, header.Dst - log.Tracef("%s >> %s %s %d %d", - src, dst, - ipProtocol(waterutil.IPProtocol(header.NextHeader)), - header.PayloadLen, header.TrafficClass) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %s %d %d", + src, dst, + xip.Protocol(waterutil.IPProtocol(header.NextHeader)), + header.PayloadLen, header.TrafficClass) + } } else { log.Warnf("unknown packet, discarded(%d)", n) return nil @@ -184,9 +189,11 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con } src, dst = header.Src, header.Dst - log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", - src, dst, ipProtocol(waterutil.IPv4Protocol(b[:n])), - header.Len, header.TotalLen, header.ID, header.Flags) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s >> %s %-4s %d/%-4d %-4x %d", + src, dst, xip.Protocol(waterutil.IPv4Protocol(b[:n])), + header.Len, header.TotalLen, header.ID, header.Flags) + } } else if waterutil.IsIPv6(b[:n]) { header, err := ipv6.ParseHeader(b[:n]) if err != nil { @@ -195,10 +202,12 @@ func (h *tunHandler) transportServer(ctx context.Context, tun io.ReadWriter, con } src, dst = header.Src, header.Dst - log.Tracef("%s > %s %s %d %d", - src, dst, - ipProtocol(waterutil.IPProtocol(header.NextHeader)), - header.PayloadLen, header.TrafficClass) + if log.IsLevelEnabled(logger.TraceLevel) { + log.Tracef("%s > %s %s %d %d", + src, dst, + xip.Protocol(waterutil.IPProtocol(header.NextHeader)), + header.PayloadLen, header.TrafficClass) + } } else { log.Warnf("unknown packet, discarded(%d): % x", n, b[:n]) return nil @@ -263,9 +272,11 @@ func (h *tunHandler) findRouteFor(ctx context.Context, dst net.IP, router router 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) + if route := router.GetRoute(ctx, dst); route != nil { + if gw := net.ParseIP(route.Gateway); gw != nil { + if v, ok := h.routes.Load(ipToTunRouteKey(gw)); ok { + return v.(net.Addr) + } } } return nil diff --git a/internal/net/ip/ip.go b/internal/net/ip/ip.go new file mode 100644 index 00000000..1dcdfb6f --- /dev/null +++ b/internal/net/ip/ip.go @@ -0,0 +1,26 @@ +package ip + +import ( + "fmt" + + "github.com/songgao/water/waterutil" +) + +var prots = map[waterutil.IPProtocol]string{ + waterutil.HOPOPT: "HOPOPT", + waterutil.ICMP: "ICMP", + waterutil.IGMP: "IGMP", + waterutil.GGP: "GGP", + waterutil.TCP: "TCP", + waterutil.UDP: "UDP", + waterutil.IPv6_Route: "IPv6-Route", + waterutil.IPv6_Frag: "IPv6-Frag", + waterutil.IPv6_ICMP: "IPv6-ICMP", +} + +func Protocol(p waterutil.IPProtocol) string { + if v, ok := prots[p]; ok { + return v + } + return fmt.Sprintf("unknown(%d)", p) +} diff --git a/listener/quic/listener.go b/listener/quic/listener.go index d286bc87..194edcae 100644 --- a/listener/quic/listener.go +++ b/listener/quic/listener.go @@ -93,6 +93,7 @@ func (l *quicListener) Init(md md.Metadata) (err error) { quic.Version2, }, MaxIncomingStreams: int64(l.md.maxStreams), + EnableDatagrams: l.md.enableDatagram, } tlsCfg := l.options.TLSConfig diff --git a/listener/quic/metadata.go b/listener/quic/metadata.go index cec46285..cc223425 100644 --- a/listener/quic/metadata.go +++ b/listener/quic/metadata.go @@ -17,6 +17,7 @@ type metadata struct { handshakeTimeout time.Duration maxIdleTimeout time.Duration maxStreams int + enableDatagram bool cipherKey []byte backlog int @@ -52,6 +53,7 @@ func (l *quicListener) parseMetadata(md mdata.Metadata) (err error) { l.md.handshakeTimeout = mdutil.GetDuration(md, handshakeTimeout) l.md.maxIdleTimeout = mdutil.GetDuration(md, maxIdleTimeout) l.md.maxStreams = mdutil.GetInt(md, maxStreams) + l.md.enableDatagram = mdutil.GetBool(md, "quic.enableDatagram", "enableDatagram") return } diff --git a/listener/tun/metadata.go b/listener/tun/metadata.go index b52245c5..0b0154d3 100644 --- a/listener/tun/metadata.go +++ b/listener/tun/metadata.go @@ -67,34 +67,36 @@ func (l *tunListener) parseMetadata(md mdata.Metadata) (err error) { } for _, s := range strings.Split(mdutil.GetString(md, route), ",") { - _, ipNet, _ := net.ParseCIDR(strings.TrimSpace(s)) - if ipNet == nil { - continue + gw := "" + if config.Gateway != nil { + gw = config.Gateway.String() + } + if route := xrouter.ParseRoute(strings.TrimSpace(s), gw); route != nil { + l.routes = append(l.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 router.Route _, ipNet, _ := net.ParseCIDR(strings.TrimSpace(ss[0])) if ipNet == nil { continue } - route.Net = ipNet gw := net.ParseIP(ss[1]) if gw == nil { gw = config.Gateway } + gateway := "" + if gw != nil { + gateway = gw.String() + } + l.routes = append(l.routes, &router.Route{ Net: ipNet, - Gateway: gw, + Dst: ipNet.String(), + Gateway: gateway, }) } } diff --git a/listener/tun/tun_linux.go b/listener/tun/tun_linux.go index 8f6b47bb..95c93850 100644 --- a/listener/tun/tun_linux.go +++ b/listener/tun/tun_linux.go @@ -49,9 +49,17 @@ func (l *tunListener) createTun() (dev io.ReadWriteCloser, name string, ip net.I func (l *tunListener) addRoutes(ifce *net.Interface) error { for _, route := range l.routes { + if route.Net == nil { + continue + } + gw := net.ParseIP(route.Gateway) + if gw == nil { + continue + } + r := netlink.Route{ Dst: route.Net, - Gw: route.Gateway, + Gw: gw, } if r.Gw == nil { r.LinkIndex = ifce.Index diff --git a/observer/stats/wrapper/conn.go b/observer/stats/wrapper/conn.go index b52c29f6..9f28115b 100644 --- a/observer/stats/wrapper/conn.go +++ b/observer/stats/wrapper/conn.go @@ -25,7 +25,7 @@ type conn struct { } func WrapConn(c net.Conn, pStats stats.Stats) net.Conn { - if pStats == nil { + if c == nil || pStats == nil { return c } diff --git a/observer/stats/wrapper/io.go b/observer/stats/wrapper/io.go index d232dbed..4eef8c2d 100644 --- a/observer/stats/wrapper/io.go +++ b/observer/stats/wrapper/io.go @@ -13,7 +13,7 @@ type readWriter struct { } func WrapReadWriter(rw io.ReadWriter, stats stats.Stats) io.ReadWriter { - if stats == nil { + if rw == nil || stats == nil { return rw } diff --git a/router/router.go b/router/router.go index 5c738c78..78cdf8bb 100644 --- a/router/router.go +++ b/router/router.go @@ -250,10 +250,11 @@ func (p *localRouter) Close() error { } func ParseRoute(dst string, gateway string) *router.Route { - _, ipNet, _ := net.ParseCIDR(dst) - if ipNet == nil { + if dst == "" { return nil } + _, ipNet, _ := net.ParseCIDR(dst) + gw := net.ParseIP(gateway) if gw == nil { return nil @@ -261,6 +262,7 @@ func ParseRoute(dst string, gateway string) *router.Route { return &router.Route{ Net: ipNet, - Gateway: gw, + Dst: dst, + Gateway: gateway, } } diff --git a/router/router_linux.go b/router/router_linux.go index 4a99bae9..af008a9f 100644 --- a/router/router_linux.go +++ b/router/router_linux.go @@ -2,6 +2,7 @@ package router import ( "fmt" + "net" "github.com/go-gost/core/router" "github.com/vishvananda/netlink" @@ -9,10 +10,18 @@ import ( func (p *localRouter) setSysRoutes(routes ...*router.Route) error { for _, route := range routes { + if route.Net == nil { + continue + } + gw := net.ParseIP(route.Gateway) + if gw == nil { + continue + } + p.options.logger.Debugf("ip route replace %s via %s", route.Net, route.Gateway) r := netlink.Route{ Dst: route.Net, - Gw: route.Gateway, + Gw: gw, } if err := netlink.RouteReplace(&r); err != nil { return fmt.Errorf("set route %v %v: %v", r.Dst, r.Gw, err)