package vpn import ( "context" "net" "net/netip" "time" "github.com/gofrs/uuid/v5" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/outbound" sbUot "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/service" ) func RegisterClientEndpoint(registry *endpoint.Registry) { endpoint.Register[option.VPNClientEndpointOptions](registry, C.TypeVPNClient, NewClientEndpoint) } type ClientEndpoint struct { outbound.Adapter ctx context.Context outbound adapter.Outbound router adapter.ConnectionRouterEx logger logger.ContextLogger address IPv4 key uuid.UUID uotClient *uot.Client } func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VPNClientEndpointOptions) (adapter.Endpoint, error) { address := options.Address if !address.Is4() { return nil, E.New("invalid address: ", address) } key, err := uuid.FromString(options.Key) if err != nil { return nil, err } client := &ClientEndpoint{ Adapter: outbound.NewAdapter(C.TypeVPNClient, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}), ctx: ctx, router: sbUot.NewRouter(router, logger), logger: logger, address: address.As4(), key: key, } outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) outbound, err := outboundRegistry.CreateOutbound(ctx, router, logger, options.Outbound.Tag, options.Outbound.Type, options.Outbound.Options) if err != nil { return nil, err } client.outbound = outbound client.uotClient = &uot.Client{ Dialer: outbound, Version: uot.Version, } return client, nil } func (c *ClientEndpoint) Start(stage adapter.StartStage) error { if stage != adapter.StartStatePostStart { return nil } for range 5 { go func() { for { select { case <-c.ctx.Done(): return default: err := c.startInboundConn() if err != nil { c.logger.ErrorContext(c.ctx, err) time.Sleep(time.Second * 5) } } } }() } return nil } func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if N.NetworkName(network) == N.NetworkUDP { return c.uotClient.DialContext(ctx, network, destination) } if destination.Addr.Is4() && destination.Addr.As4() == c.address { return nil, E.New("routing loop on ", destination.Addr) } conn, err := c.outbound.DialContext(ctx, N.NetworkTCP, Destination) if err != nil { return nil, err } gateway := Loopback.As4() if metadata := adapter.ContextFrom(ctx); metadata != nil { if metadata.Gateway != nil { gateway = metadata.Gateway.As4() if gateway == c.address { return nil, E.New("routing loop on ", destination.Addr) } } } err = WriteClientRequest( conn, &ClientRequest{ Key: c.key, Command: CommandTCP, Gateway: gateway, Destination: destination, }, ) if err != nil { return nil, err } return conn, nil } func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return c.uotClient.ListenPacket(ctx, destination) } func (c *ClientEndpoint) Close() error { return common.Close(c.outbound) } func (c *ClientEndpoint) startInboundConn() error { conn, err := c.outbound.DialContext(c.ctx, N.NetworkTCP, Destination) if err != nil { return err } err = WriteClientRequest(conn, &ClientRequest{Key: c.key, Command: CommandInbound, Destination: Destination}) if err != nil { return err } request, err := ReadServerRequest(conn) if err != nil { return err } if request.Source == c.address { return E.New("routing loop") } metadata := adapter.InboundContext{ Inbound: c.Tag(), Source: M.Socksaddr{Addr: netip.AddrFrom4(request.Source)}, Destination: request.Destination, } go c.router.RouteConnectionEx(c.ctx, conn, metadata, func(it error) {}) return nil }