//go:build with_quic package outbound import ( "context" "net" "os" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/tuic" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/uot" "github.com/gofrs/uuid/v5" ) var ( _ adapter.Outbound = (*TUIC)(nil) _ adapter.InterfaceUpdateListener = (*TUIC)(nil) ) type TUIC struct { myOutboundAdapter client *tuic.Client udpStream bool } func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (*TUIC, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired } tlsConfig, err := tls.NewClient(ctx, options.Server, common.PtrValueOrDefault(options.TLS)) if err != nil { return nil, err } userUUID, err := uuid.FromString(options.UUID) if err != nil { return nil, E.Cause(err, "invalid uuid") } var tuicUDPStream bool if options.UDPOverStream && options.UDPRelayMode != "" { return nil, E.New("udp_over_stream is conflict with udp_relay_mode") } switch options.UDPRelayMode { case "native": case "quic": tuicUDPStream = true } outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { return nil, err } client, err := tuic.NewClient(tuic.ClientOptions{ Context: ctx, Dialer: outboundDialer, ServerAddress: options.ServerOptions.Build(), TLSConfig: tlsConfig, UUID: userUUID, Password: options.Password, CongestionControl: options.CongestionControl, UDPStream: tuicUDPStream, ZeroRTTHandshake: options.ZeroRTTHandshake, Heartbeat: time.Duration(options.Heartbeat), }) if err != nil { return nil, err } return &TUIC{ myOutboundAdapter: myOutboundAdapter{ protocol: C.TypeTUIC, network: options.Network.Build(), router: router, logger: logger, tag: tag, dependencies: withDialerDependency(options.DialerOptions), }, client: client, udpStream: options.UDPOverStream, }, nil } func (h *TUIC) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialConn(ctx, destination) case N.NetworkUDP: if h.udpStream { h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) if err != nil { return nil, err } return uot.NewLazyConn(streamConn, uot.Request{ IsConnect: true, Destination: destination, }), nil } else { conn, err := h.ListenPacket(ctx, destination) if err != nil { return nil, err } return bufio.NewBindPacketConn(conn, destination), nil } default: return nil, E.New("unsupported network: ", network) } } func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.udpStream { h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) if err != nil { return nil, err } return uot.NewLazyConn(streamConn, uot.Request{ IsConnect: false, Destination: destination, }), nil } else { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx) } } func (h *TUIC) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { return NewConnection(ctx, h, conn, metadata) } func (h *TUIC) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return NewPacketConnection(ctx, h, conn, metadata) } func (h *TUIC) InterfaceUpdated() { _ = h.client.CloseWithError(E.New("network changed")) } func (h *TUIC) Close() error { return h.client.CloseWithError(os.ErrClosed) }