package openvpn

import (
	"context"
	"net"
	"net/netip"
	"slices"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/adapter/endpoint"
	"github.com/sagernet/sing-box/common/dialer"
	"github.com/sagernet/sing-box/common/iponly"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing-box/option"
	ovpntransport "github.com/sagernet/sing-box/transport/openvpn"
	ovpn "github.com/sagernet/sing-openvpn"
	"github.com/sagernet/sing-tun"
	"github.com/sagernet/sing-tun/gtcpip/header"
	"github.com/sagernet/sing/common"
	"github.com/sagernet/sing/common/buf"
	"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/service"

	"go4.org/netipx"
)

var (
	_ adapter.OutboundWithPreferredRoutes = (*ClientEndpoint)(nil)
	_ adapter.FlowOutbound                = (*ClientEndpoint)(nil)
	_ adapter.InterfaceUpdateListener     = (*ClientEndpoint)(nil)
	_ dialer.PacketDialerWithDestination  = (*ClientEndpoint)(nil)
	_ tun.Port                            = (*ClientEndpoint)(nil)
)

type ClientEndpoint struct {
	endpointBase
	ctx               context.Context
	loopContext       context.Context
	cancelLoop        context.CancelFunc
	dnsRouter         adapter.DNSRouter
	outboundDialer    N.Dialer
	queryOptions      adapter.DNSQueryOptions
	client            *ovpn.Client
	device            ovpntransport.Device
	stateAccess       sync.Mutex
	state             atomic.Pointer[clientState]
	dnsTransport      *DNSTransport
	deviceStarted     bool
	readLoopDone      chan struct{}
	statusAccess      sync.Mutex
	statusUpdated     chan struct{}
	terminalError     string
	challengeLoopDone chan struct{}
}

type clientState struct {
	started          bool
	tunnelConfigured bool
	localAddresses   []netip.Prefix
	routeSet         *netipx.IPSet
	blockIPv6        bool
	configuration    ovpntransport.Configuration
	preferredDomains []string
	tunnelInfo       adapter.OpenVPNTunnelInfo
}

func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.OpenVPNClientEndpointOptions) (adapter.Endpoint, error) {
	loopContext, cancelLoop := context.WithCancel(ctx)
	clientEndpoint := &ClientEndpoint{
		endpointBase: endpointBase{
			Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeOpenVPNClient, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, options.DialerOptions),
			router:  router,
			logger:  logger,
		},
		ctx:           ctx,
		loopContext:   loopContext,
		cancelLoop:    cancelLoop,
		dnsRouter:     service.FromContext[adapter.DNSRouter](ctx),
		statusUpdated: make(chan struct{}),
	}
	success := false
	defer func() {
		if success {
			return
		}
		if clientEndpoint.device != nil {
			_ = clientEndpoint.device.Close()
		}
		cancelLoop()
	}()
	clientOptions, err := clientEndpoint.buildClientOptions(options)
	if err != nil {
		return nil, err
	}
	clientEndpoint.state.Store(&clientState{localAddresses: clientOptions.Tunnel.LocalAddress})
	outboundDialer, err := dialer.NewWithOptions(dialer.Options{
		Context:          ctx,
		Options:          options.DialerOptions,
		RemoteIsDomain:   openVPNClientRemoteIsDomain(options),
		ResolverOnDetour: true,
		NewDialer:        true,
	})
	if err != nil {
		return nil, err
	}
	var queryOptions adapter.DNSQueryOptions
	resolveDialer, isResolveDialer := outboundDialer.(dialer.ResolveDialer)
	if isResolveDialer {
		queryOptions = resolveDialer.QueryOptions()
	}
	clientEndpoint.outboundDialer = outboundDialer
	clientEndpoint.queryOptions = queryOptions
	udpTimeout := C.UDPTimeout
	if options.UDPTimeout != 0 {
		udpTimeout = time.Duration(options.UDPTimeout)
	}
	device, err := ovpntransport.NewDevice(ovpntransport.DeviceOptions{
		Context:         ctx,
		Logger:          logger,
		System:          options.System,
		Handler:         clientEndpoint,
		UDPTimeout:      udpTimeout,
		ICMPTimeout:     C.ICMPTimeout,
		UDPMapping:      tun.NATMapping(options.UDPMapping),
		UDPFiltering:    tun.NATFiltering(options.UDPFiltering),
		UDPNATMax:       options.UDPNATMax,
		InterfaceFinder: service.FromContext[adapter.NetworkManager](ctx).InterfaceFinder(),
		Name:            options.Name,
		MTU:             options.MTU,
		Configuration: ovpntransport.Configuration{
			MTU:     options.MTU,
			Address: clientOptions.Tunnel.LocalAddress,
		},
	})
	if err != nil {
		return nil, err
	}
	clientEndpoint.device = device
	device.SetPacketWriter(clientEndpoint.writePacketBuffers)
	client, err := ovpn.NewClient(clientOptions)
	if err != nil {
		return nil, err
	}
	clientEndpoint.client = client
	success = true
	return clientEndpoint, nil
}

func (c *ClientEndpoint) buildClientOptions(options option.OpenVPNClientEndpointOptions) (ovpn.ClientOptions, error) {
	mode := options.Mode
	if mode == "" {
		mode = ovpn.ModeTLS
	}
	switch mode {
	case ovpn.ModeTLS, ovpn.ModeStaticKey:
	default:
		return ovpn.ClientOptions{}, E.New("unsupported mode: ", mode, " (expected \"tls\" or \"static_key\")")
	}
	if options.Server != "" && len(options.Servers) > 0 {
		return ovpn.ClientOptions{}, E.New("`server` is conflict with `servers`")
	}
	if options.Server == "" && len(options.Servers) == 0 {
		return ovpn.ClientOptions{}, E.New("missing `server` or `servers`")
	}
	protocol, remotes := buildClientRemoteOptions(options)
	tunnelOptions, err := buildClientTunnelOptions(options, mode == ovpn.ModeStaticKey)
	if err != nil {
		return ovpn.ClientOptions{}, err
	}
	if mode == ovpn.ModeStaticKey {
		return c.buildStaticKeyClientOptions(options, protocol, remotes, tunnelOptions)
	}
	if options.TLS == nil {
		return ovpn.ClientOptions{}, E.New("missing `tls` options")
	}
	if len(options.StaticKey) > 0 || options.StaticKeyPath != "" {
		return ovpn.ClientOptions{}, E.New("`static_key` and `static_key_path` are only supported in `static_key` mode")
	}
	if options.KeyDirection != "" {
		return ovpn.ClientOptions{}, E.New("`key_direction` is only supported in `static_key` mode; use `tls.control_wrap.direction` for `tls_auth`")
	}
	if options.Cipher != "" {
		return ovpn.ClientOptions{}, E.New("`cipher` is only supported in `static_key` mode; use `data_ciphers` or `data_ciphers_fallback` in TLS mode")
	}
	certificateAuthority, err := materialSource("tls.certificate", options.TLS.Certificate, options.TLS.CertificatePath)
	if err != nil {
		return ovpn.ClientOptions{}, err
	}
	clientCertificate, err := materialSource("tls.client_certificate", options.TLS.ClientCertificate, options.TLS.ClientCertificatePath)
	if err != nil {
		return ovpn.ClientOptions{}, err
	}
	clientKey, err := materialSource("tls.client_key", options.TLS.ClientKey, options.TLS.ClientKeyPath)
	if err != nil {
		return ovpn.ClientOptions{}, err
	}
	keyDirection := -1
	var controlAuth ovpn.Material
	var controlCrypt ovpn.Material
	var controlCryptV2 ovpn.Material
	controlWrap := options.TLS.ControlWrap
	if controlWrap != nil && (controlWrap.Type != "" || len(controlWrap.Key) > 0 || controlWrap.KeyPath != "" || controlWrap.Direction != "") {
		controlKey, controlErr := requiredMaterialSource("tls.control_wrap.key", controlWrap.Key, controlWrap.KeyPath)
		if controlErr != nil {
			return ovpn.ClientOptions{}, controlErr
		}
		switch controlWrap.Type {
		case "tls_auth":
			keyDirection, err = keyDirectionValue(controlWrap.Direction)
			if err != nil {
				return ovpn.ClientOptions{}, err
			}
			controlAuth = controlKey
		case "tls_crypt":
			if controlWrap.Direction != "" {
				return ovpn.ClientOptions{}, E.New("`tls.control_wrap.direction` is only supported by `tls_auth`")
			}
			controlCrypt = controlKey
		case "tls_crypt_v2":
			if controlWrap.Direction != "" {
				return ovpn.ClientOptions{}, E.New("`tls.control_wrap.direction` is only supported by `tls_auth`")
			}
			controlCryptV2 = controlKey
		case "":
			return ovpn.ClientOptions{}, E.New("missing control wrap type")
		default:
			return ovpn.ClientOptions{}, E.New("unknown control wrap type: ", controlWrap.Type)
		}
	}
	pullFilters := common.Map(options.PullFilters, func(filterOptions option.OpenVPNPullFilterOptions) ovpn.PullFilter {
		return ovpn.PullFilter{
			Action: filterOptions.Action,
			Text:   filterOptions.Text,
		}
	})
	remoteCertificateTLS := options.TLS.RemoteCertificateTLS
	switch remoteCertificateTLS {
	case "", "server", "client", "none":
	default:
		return ovpn.ClientOptions{}, E.New("invalid `tls.remote_certificate_tls`: ", remoteCertificateTLS)
	}
	if options.TLS.RemoteCertificateEKU != "" && remoteCertificateTLS != "" {
		return ovpn.ClientOptions{}, E.New("`tls.remote_certificate_eku` is conflict with `tls.remote_certificate_tls`")
	}
	if remoteCertificateTLS == "" && options.TLS.RemoteCertificateEKU == "" {
		remoteCertificateTLS = "server"
	} else if remoteCertificateTLS == "none" {
		remoteCertificateTLS = ""
	}
	clientTLSOptions := ovpn.ClientTLSOptions{
		CertificateAuthority: certificateAuthority,
		Certificate:          clientCertificate,
		Key:                  clientKey,
		Auth:                 controlAuth,
		Crypt:                controlCrypt,
		CryptV2:              controlCryptV2,
		VerifyX509Type:       options.TLS.ServerNameType,
		PeerFingerprint:      options.TLS.PeerFingerprint,
		CRLVerify:            options.TLS.CRLPath,
		RemoteCertificateKU:  options.TLS.RemoteCertificateKU,
		RemoteCertificateEKU: options.TLS.RemoteCertificateEKU,
		RemoteCertificateTLS: remoteCertificateTLS,
		NSCertificateType:    options.TLS.NSCertificateType,
		VersionMin:           options.TLS.VersionMin,
		VersionMax:           options.TLS.VersionMax,
		CertificateProfile:   options.TLS.CertificateProfile,
		Cipher:               options.TLS.Cipher,
		Groups:               options.TLS.Groups,
	}
	if options.TLS.ServerName != "" {
		clientTLSOptions.VerifyX509Name = options.TLS.ServerName
		if options.TLS.ServerNameType == "" {
			clientTLSOptions.VerifyX509Type = "name"
		}
	}
	return ovpn.ClientOptions{
		Context: c.loopContext,
		Mode:    mode,
		Transport: ovpn.ClientTransportOptions{
			Remotes:                     remotes,
			RemoteRandom:                options.RemoteRandom,
			Protocol:                    protocol,
			ExplicitExitNotify:          options.ExplicitExitNotify,
			DialContextWithAddressIndex: c.transportDialContextWithAddressIndex,
		},
		DataChannel: buildClientDataChannelOptions(options),
		TLS:         clientTLSOptions,
		Authentication: ovpn.ClientAuthenticationOptions{
			Username:            options.Username,
			Password:            options.Password,
			AuthRetry:           options.AuthRetry,
			StaticChallenge:     options.StaticChallenge,
			StaticChallengeEcho: options.StaticChallengeEcho,
		},
		Pull: ovpn.ClientPullOptions{
			Enabled:     true,
			Filters:     pullFilters,
			RouteNoPull: options.RouteNoPull,
		},
		Tunnel:                tunnelOptions,
		Timing:                buildClientTimingOptions(options),
		KeyDirection:          keyDirection,
		OnTunnelConfiguration: c.handleTunnelConfiguration,
		Logger:                c.logger,
	}, nil
}

func (c *ClientEndpoint) buildStaticKeyClientOptions(options option.OpenVPNClientEndpointOptions, protocol string, remotes []ovpn.Remote, tunnelOptions ovpn.ClientTunnelOptions) (ovpn.ClientOptions, error) {
	if options.TLS != nil {
		return ovpn.ClientOptions{}, E.New("`tls` options are not supported in `static_key` mode")
	}
	if options.Username != "" || options.Password != "" || (options.AuthRetry != "" && options.AuthRetry != "none") || options.StaticChallenge != "" || options.StaticChallengeEcho {
		return ovpn.ClientOptions{}, E.New("username/password authentication is not supported in `static_key` mode")
	}
	if options.RouteNoPull || len(options.PullFilters) > 0 {
		return ovpn.ClientOptions{}, E.New("pull options are not supported in `static_key` mode")
	}
	if options.RenegotiateInterval != 0 || options.RenegotiateDisabled || options.RenegotiateBytes != 0 || options.RenegotiatePackets != 0 || options.TLSTimeout != 0 || options.HandshakeWindow != 0 {
		return ovpn.ClientOptions{}, E.New("TLS timing and renegotiation options are not supported in `static_key` mode")
	}
	if len(options.DataCiphers) > 0 || options.DataCiphersFallback != "" {
		return ovpn.ClientOptions{}, E.New("`data_ciphers` and `data_ciphers_fallback` are not supported in `static_key` mode; use `cipher`")
	}
	staticKey, err := requiredMaterialSource("static_key", options.StaticKey, options.StaticKeyPath)
	if err != nil {
		return ovpn.ClientOptions{}, err
	}
	keyDirection, err := keyDirectionValue(options.KeyDirection)
	if err != nil {
		return ovpn.ClientOptions{}, err
	}
	return ovpn.ClientOptions{
		Context: c.loopContext,
		Mode:    ovpn.ModeStaticKey,
		Transport: ovpn.ClientTransportOptions{
			Remotes:                     remotes,
			RemoteRandom:                options.RemoteRandom,
			Protocol:                    protocol,
			ExplicitExitNotify:          options.ExplicitExitNotify,
			DialContextWithAddressIndex: c.transportDialContextWithAddressIndex,
		},
		DataChannel:           buildClientDataChannelOptions(options),
		Tunnel:                tunnelOptions,
		Timing:                buildClientTimingOptions(options),
		StaticKey:             staticKey,
		KeyDirection:          keyDirection,
		OnTunnelConfiguration: c.handleTunnelConfiguration,
		Logger:                c.logger,
	}, nil
}

func buildClientRemoteOptions(options option.OpenVPNClientEndpointOptions) (string, []ovpn.Remote) {
	protocol := options.Network
	if protocol == "" {
		protocol = N.NetworkUDP
	}
	if options.Server != "" {
		return protocol, []ovpn.Remote{{
			Host:     options.Server,
			Port:     options.ServerPort,
			Protocol: protocol,
		}}
	}
	remotes := make([]ovpn.Remote, 0, len(options.Servers))
	for _, remoteOptions := range options.Servers {
		remoteProtocol := remoteOptions.Network
		if remoteProtocol == "" {
			remoteProtocol = protocol
		}
		remotes = append(remotes, ovpn.Remote{
			Host:     remoteOptions.Server,
			Port:     remoteOptions.ServerPort,
			Protocol: remoteProtocol,
		})
	}
	return protocol, remotes
}

func buildClientDataChannelOptions(options option.OpenVPNClientEndpointOptions) ovpn.ClientDataChannelOptions {
	return ovpn.ClientDataChannelOptions{
		MTU:              options.MTU,
		MSSFix:           options.MSSFix,
		MSSFixDisabled:   options.MSSFixDisabled,
		MSSFixMode:       options.MSSFixMode,
		Fragment:         options.Fragment,
		Cipher:           options.Cipher,
		Ciphers:          options.DataCiphers,
		FallbackCipher:   options.DataCiphersFallback,
		Auth:             options.Auth,
		Compression:      options.Compression,
		CompressionLZO:   options.CompressionLZO,
		AllowCompression: options.AllowCompression,
		ReplayWindow:     options.ReplayWindow,
		ReplayWindowTime: time.Duration(options.ReplayWindowTime),
		PacketHeadroom:   ovpntransport.PacketHeadroom,
	}
}

func buildClientTunnelOptions(options option.OpenVPNClientEndpointOptions, requirePeerAddress bool) (ovpn.ClientTunnelOptions, error) {
	vpnGateway := netip.Addr(options.PeerAddress)
	if vpnGateway.IsValid() && !vpnGateway.Is4() {
		return ovpn.ClientTunnelOptions{}, E.New("`peer_address` must be an IPv4 address")
	}
	vpnGatewayIPv6 := netip.Addr(options.PeerAddressIPv6)
	if vpnGatewayIPv6.IsValid() && !vpnGatewayIPv6.Is6() {
		return ovpn.ClientTunnelOptions{}, E.New("`peer_address_ipv6` must be an IPv6 address")
	}
	var hasIPv4 bool
	var hasIPv6 bool
	for addressIndex, address := range options.Address {
		if !address.IsValid() {
			return ovpn.ClientTunnelOptions{}, E.New("`address[", addressIndex, "]` is invalid")
		}
		if address.Addr().Is4() {
			hasIPv4 = true
		} else {
			hasIPv6 = true
		}
	}
	if requirePeerAddress {
		if len(options.Address) == 0 {
			return ovpn.ClientTunnelOptions{}, E.New("missing `address` in `static_key` mode")
		}
		if hasIPv4 && !vpnGateway.IsValid() {
			return ovpn.ClientTunnelOptions{}, E.New("missing `peer_address` for the IPv4 tunnel address in `static_key` mode")
		}
		if hasIPv6 && !vpnGatewayIPv6.IsValid() {
			return ovpn.ClientTunnelOptions{}, E.New("missing `peer_address_ipv6` for the IPv6 tunnel address in `static_key` mode")
		}
		if vpnGateway.IsValid() && !hasIPv4 {
			return ovpn.ClientTunnelOptions{}, E.New("`peer_address` requires an IPv4 tunnel `address` in `static_key` mode")
		}
		if vpnGatewayIPv6.IsValid() && !hasIPv6 {
			return ovpn.ClientTunnelOptions{}, E.New("`peer_address_ipv6` requires an IPv6 tunnel `address` in `static_key` mode")
		}
	}
	tunnelRoutes := common.Map(options.Routes, func(route netip.Prefix) ovpn.TunnelRoute {
		return ovpn.TunnelRoute{Prefix: route}
	})
	return ovpn.ClientTunnelOptions{
		DevType:              "tun",
		Topology:             options.Topology,
		RedirectGateway:      options.RedirectGateway,
		RedirectGatewayFlags: options.RedirectGatewayFlags,
		RedirectPrivate:      options.RedirectPrivate,
		BlockIPv6:            options.BlockIPv6,
		RouteMetric:          options.RouteMetric,
		RouteGateway:         options.RouteGateway.Build(netip.Addr{}),
		Routes:               tunnelRoutes,
		LocalAddress:         options.Address,
		VPNGateway:           vpnGateway,
		VPNGatewayIPv6:       vpnGatewayIPv6,
	}, nil
}

func buildClientTimingOptions(options option.OpenVPNClientEndpointOptions) ovpn.ClientTimingOptions {
	return ovpn.ClientTimingOptions{
		RenegotiationInterval: time.Duration(options.RenegotiateInterval),
		RenegotiationDisabled: options.RenegotiateDisabled,
		RenegotiationBytes:    options.RenegotiateBytes,
		RenegotiationPackets:  options.RenegotiatePackets,
		PingInterval:          time.Duration(options.PingInterval),
		PingRestart:           time.Duration(options.PingRestart),
		PingRestartDisabled:   options.PingRestartDisabled,
		TLSTimeout:            time.Duration(options.TLSTimeout),
		HandWindow:            time.Duration(options.HandshakeWindow),
	}
}

func (c *ClientEndpoint) transportDialContextWithAddressIndex(ctx context.Context, network string, address string, addressIndex int) (net.Conn, error) {
	destination := M.ParseSocksaddr(address)
	if destination.IsDomain() {
		destinationAddresses, lookupErr := c.dnsRouter.Lookup(ctx, destination.Fqdn, c.queryOptions)
		if lookupErr != nil {
			return nil, lookupErr
		}
		if addressIndex < 0 || addressIndex >= len(destinationAddresses) {
			return nil, ovpn.ErrRemoteAddressExhausted
		}
		destination = M.SocksaddrFrom(destinationAddresses[addressIndex], destination.Port)
	} else if addressIndex != 0 {
		return nil, ovpn.ErrRemoteAddressExhausted
	}
	connection, err := c.outboundDialer.DialContext(ctx, network, destination)
	if err != nil {
		return nil, err
	}
	if N.NetworkName(network) == N.NetworkUDP {
		tuneOpenVPNUDPSocket(connection)
	}
	c.stateAccess.Lock()
	c.updateState(func(state *clientState) {
		state.tunnelInfo.Server = address
		state.tunnelInfo.Network = N.NetworkName(network)
	})
	c.stateAccess.Unlock()
	return connection, nil
}

func (c *ClientEndpoint) handleTunnelConfiguration(event ovpn.TunnelConfigurationEvent) error {
	defer c.notifyStatusUpdated()
	c.stateAccess.Lock()
	configuration := configurationFromClientEvent(event, c.logger)
	c.updateState(func(state *clientState) {
		state.tunnelConfigured = false
	})
	deviceConfiguration := ovpntransport.Configuration{
		MTU:       configuration.MTU,
		Address:   configuration.Address,
		BlockIPv6: configuration.BlockIPv6,
	}
	err := c.device.UpdateConfiguration(deviceConfiguration)
	if err != nil {
		c.stateAccess.Unlock()
		return E.Cause(err, "update device configuration")
	}
	if !c.deviceStarted {
		err = c.device.Start()
		if err != nil {
			c.stateAccess.Unlock()
			return E.Cause(err, "start device")
		}
		c.deviceStarted = true
	}
	routeSet, err := buildIPSet(configuration.Routes, configuration.ExcludedRoutes)
	if err != nil {
		c.stateAccess.Unlock()
		return E.Cause(err, "build route set")
	}
	preferredDomains := slices.Clone(configuration.DNSRoutes)
	preferredDomains = append(preferredDomains, configuration.SearchDomains...)
	if len(configuration.DNSServers) > 0 {
		servers := slices.Clone(configuration.DNSServers)
		slices.SortFunc(servers, func(left ovpntransport.DNSServer, right ovpntransport.DNSServer) int {
			return left.Priority - right.Priority
		})
		preferredDomains = append(preferredDomains, servers[0].ResolveDomains...)
	}
	c.updateState(func(state *clientState) {
		state.tunnelConfigured = true
		state.localAddresses = configuration.Address
		state.routeSet = routeSet
		state.blockIPv6 = configuration.BlockIPv6
		state.configuration = configuration
		state.preferredDomains = preferredDomains
		state.tunnelInfo.Cipher = event.Configuration.SelectedCipher
		state.tunnelInfo.IPv4 = event.Configuration.LocalIPv4
		state.tunnelInfo.IPv6 = event.Configuration.LocalIPv6
		state.tunnelInfo.DNS = event.Configuration.DNS
		state.tunnelInfo.MTU = configuration.MTU
		if event.Reason == ovpn.TunnelConfigurationEventInitial || state.tunnelInfo.ConnectedSince.IsZero() {
			state.tunnelInfo.ConnectedSince = time.Now()
		}
	})
	dnsTransport := c.dnsTransport
	c.stateAccess.Unlock()
	if dnsTransport != nil {
		dnsTransport.onReconfiguration(configuration)
	}
	return nil
}

func (c *ClientEndpoint) updateState(update func(state *clientState)) {
	newState := *c.state.Load()
	update(&newState)
	c.state.Store(&newState)
}

func (c *ClientEndpoint) installDNSTransport(dnsTransport *DNSTransport) error {
	c.stateAccess.Lock()
	defer c.stateAccess.Unlock()
	if c.dnsTransport != nil && c.dnsTransport != dnsTransport && c.dnsTransport.Tag() != dnsTransport.Tag() {
		return E.New("only one DNS server is allowed for an endpoint")
	}
	err := dnsTransport.updateResolvers(c.state.Load().configuration)
	if err != nil {
		return err
	}
	c.dnsTransport = dnsTransport
	return nil
}

func (c *ClientEndpoint) uninstallDNSTransport(dnsTransport *DNSTransport) {
	c.stateAccess.Lock()
	if c.dnsTransport == dnsTransport {
		c.dnsTransport = nil
	}
	c.stateAccess.Unlock()
}

func (c *ClientEndpoint) Start(stage adapter.StartStage) error {
	if stage != adapter.StartStatePostStart {
		return nil
	}
	err := c.client.Start()
	if err != nil {
		return err
	}
	c.stateAccess.Lock()
	c.updateState(func(state *clientState) {
		state.started = true
	})
	c.readLoopDone = make(chan struct{})
	c.challengeLoopDone = make(chan struct{})
	c.stateAccess.Unlock()
	go c.readLoop()
	go c.watchChallenges()
	return nil
}

func (c *ClientEndpoint) readLoop() {
	defer close(c.readLoopDone)
	for {
		packetBuffers, err := c.client.ReadDataPackets(c.loopContext)
		if err != nil {
			if E.IsClosedOrCanceled(err) || c.loopContext.Err() != nil {
				return
			}
			c.logger.Error(E.Cause(err, "client terminated"))
			c.setTerminalError(err)
			return
		}
		err = c.device.WriteInboundBuffers(packetBuffers)
		buf.ReleaseMulti(packetBuffers)
		if err != nil {
			err = E.Cause(err, "write packet to device")
			c.logger.Error(err)
			c.setTerminalError(err)
			return
		}
	}
}

func (c *ClientEndpoint) Close() error {
	c.stateAccess.Lock()
	c.updateState(func(state *clientState) {
		state.started = false
	})
	readLoopDone := c.readLoopDone
	challengeLoopDone := c.challengeLoopDone
	c.stateAccess.Unlock()
	c.cancelLoop()
	err := E.Errors(c.client.Close(), c.device.Close())
	if readLoopDone != nil {
		<-readLoopDone
	}
	if challengeLoopDone != nil {
		<-challengeLoopDone
	}
	c.notifyStatusUpdated()
	return err
}

func (c *ClientEndpoint) InterfaceUpdated(ctx context.Context) {
	c.client.RestartSession()
}

func (c *ClientEndpoint) PreMatchFlow(network string, destination netip.Addr) adapter.PreMatchAction {
	return adapter.PreMatchFlow
}

func (c *ClientEndpoint) PortAddresses() (netip.Addr, netip.Addr) {
	return c.device.PortAddresses()
}

func (c *ClientEndpoint) PortMTU() uint32 {
	return c.device.PortMTU()
}

func (c *ClientEndpoint) AttachReturn(returnPath tun.Return) error {
	return c.device.AttachReturn(returnPath)
}

func (c *ClientEndpoint) DetachReturn(returnPath tun.Return) error {
	return c.device.DetachReturn(returnPath)
}

func (c *ClientEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
	return judgeOpenVPNFlow(c.router, c.Tag(), c.Type(), c.state.Load().localAddresses, network, source, destination, firstPacket)
}

func (c *ClientEndpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
	c.newDNSPacket(log.ContextWithNewID(c.ctx), c, payload, source, destination, writer)
}

func (c *ClientEndpoint) ready() bool {
	state := c.state.Load()
	return state.started && state.tunnelConfigured
}

func (c *ClientEndpoint) WritePackets(packets [][]byte) error {
	state := c.state.Load()
	if !state.started || !state.tunnelConfigured {
		return E.New("endpoint is not ready yet")
	}
	if state.blockIPv6 {
		outboundPackets := packets[:0]
		for _, packet := range packets {
			if header.IPVersion(packet) != header.IPv6Version {
				outboundPackets = append(outboundPackets, packet)
			}
		}
		packets = outboundPackets
		if len(packets) == 0 {
			return nil
		}
	}
	packetBuffers := make([]*buf.Buffer, len(packets))
	for i, packet := range packets {
		packetBuffers[i] = buf.As(packet)
	}
	err := c.client.WriteDataPacketBuffers(packetBuffers)
	if E.IsMulti(err, ovpn.ErrDataChannelNotReady) {
		return E.New("endpoint is not ready yet")
	}
	return err
}

func (c *ClientEndpoint) writePacketBuffers(packetBuffers []*buf.Buffer) error {
	state := c.state.Load()
	if !state.started || !state.tunnelConfigured {
		buf.ReleaseMulti(packetBuffers)
		return nil
	}
	if state.blockIPv6 {
		outboundBuffers := packetBuffers[:0]
		for _, packetBuffer := range packetBuffers {
			if header.IPVersion(packetBuffer.Bytes()) == header.IPv6Version {
				packetBuffer.Release()
				continue
			}
			outboundBuffers = append(outboundBuffers, packetBuffer)
		}
		packetBuffers = outboundBuffers
		if len(packetBuffers) == 0 {
			return nil
		}
	}
	err := c.client.WriteDataPacketBuffers(packetBuffers)
	if E.IsMulti(err, ovpn.ErrDataChannelNotReady) {
		return nil
	}
	return err
}

func (c *ClientEndpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
	c.newConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose)
}

func (c *ClientEndpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
	c.newPacketConnection(ctx, c, c.state.Load().localAddresses, conn, source, destination, onClose)
}

func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
	switch network {
	case N.NetworkTCP:
		c.logger.InfoContext(ctx, "outbound connection to ", destination)
	case N.NetworkUDP:
		c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
	}
	if !c.ready() || !c.client.Ready() {
		return nil, E.New("endpoint is not ready yet")
	}
	if destination.IsDomain() {
		destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
		if err != nil {
			return nil, err
		}
		return N.DialSerial(ctx, c.device, network, destination, destinationAddresses)
	}
	if !destination.Addr.IsValid() {
		return nil, E.New("invalid destination: ", destination)
	}
	return c.device.DialContext(ctx, network, destination)
}

func (c *ClientEndpoint) ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error) {
	c.logger.InfoContext(ctx, "outbound packet connection to ", destination)
	if !c.ready() || !c.client.Ready() {
		return nil, netip.Addr{}, E.New("endpoint is not ready yet")
	}
	if destination.IsDomain() {
		destinationAddresses, err := c.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{})
		if err != nil {
			return nil, netip.Addr{}, err
		}
		packetConn, destinationAddress, err := N.ListenSerial(ctx, c.device, destination, destinationAddresses)
		if err != nil {
			return nil, netip.Addr{}, err
		}
		return iponly.NewPacketConn(c.logger, packetConn), destinationAddress, nil
	}
	packetConn, err := c.device.ListenPacket(ctx, destination)
	if err != nil {
		return nil, netip.Addr{}, err
	}
	if destination.IsIP() {
		return iponly.NewPacketConn(c.logger, packetConn), destination.Addr, nil
	}
	return iponly.NewPacketConn(c.logger, packetConn), netip.Addr{}, nil
}

func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
	packetConn, destinationAddress, err := c.ListenPacketWithDestination(ctx, destination)
	if err != nil {
		return nil, err
	}
	if destinationAddress.IsValid() && destination != M.SocksaddrFrom(destinationAddress, destination.Port) {
		return bufio.NewNATPacketConn(bufio.NewPacketConn(packetConn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
	}
	return packetConn, nil
}

func (c *ClientEndpoint) PreferredDomain(metadata *adapter.InboundContext, domain string) bool {
	state := c.state.Load()
	if !state.started || !state.tunnelConfigured || !c.client.Ready() {
		return false
	}
	for _, preferredDomain := range state.preferredDomains {
		if openVPNDomainMatches(preferredDomain, domain) {
			return true
		}
	}
	return false
}

func (c *ClientEndpoint) PreferredAddress(metadata *adapter.InboundContext, address netip.Addr) bool {
	state := c.state.Load()
	if !state.started || !state.tunnelConfigured || state.routeSet == nil || !c.client.Ready() {
		return false
	}
	return state.routeSet.Contains(address)
}

func openVPNDomainMatches(suffix string, domain string) bool {
	normalizedSuffix := strings.ToLower(strings.TrimSpace(suffix))
	if normalizedSuffix == "." {
		return true
	}
	normalizedSuffix = strings.TrimSuffix(normalizedSuffix, ".")
	normalizedDomain := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(domain)), ".")
	if normalizedSuffix == "" {
		return false
	}
	return normalizedDomain == normalizedSuffix || strings.HasSuffix(normalizedDomain, "."+normalizedSuffix)
}
