// Copyright 2018 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tcp

import (
	"context"
	"fmt"

	"github.com/sagernet/gvisor/pkg/atomicbitops"
	"github.com/sagernet/gvisor/pkg/log"
	"github.com/sagernet/gvisor/pkg/sync"
	"github.com/sagernet/gvisor/pkg/tcpip"
	"github.com/sagernet/gvisor/pkg/tcpip/header"
	"github.com/sagernet/gvisor/pkg/tcpip/seqnum"
	"github.com/sagernet/gvisor/pkg/tcpip/stack"
)

// logDisconnectOnce ensures we don't spam logs when many connections are terminated.
var logDisconnectOnce sync.Once

func logDisconnect() {
	logDisconnectOnce.Do(func() {
		log.Infof("One or more TCP connections terminated during save restore")
	})
}

// beforeSave is invoked by stateify.
func (e *Endpoint) beforeSave() {
	// Stop incoming packets.
	e.segmentQueue.freeze()

	e.mu.Lock()
	defer e.mu.Unlock()

	epState := e.EndpointState()
	switch {
	case epState == StateInitial || epState == StateBound:
	case epState.connected() || epState.handshake():
		// Terminate valid connections only for restore.
		if !e.stack.GetAllowConnectedOnSave() && !e.route.HasSaveRestoreCapability() {
			if e.stack.GetRemoveConf() {
				// Terminate the endpoint when resume=false.
				e.terminateAtRestore = false
				if !e.stack.AllowLiveTCPMigration() {
					logDisconnect()
					e.resetConnectionLocked(&tcpip.ErrConnectionAborted{})
					e.mu.Unlock()
					e.Close()
					e.mu.Lock()
				}
			} else {
				// This is set only when resume=true, the termination
				// of this endpoint will happen during restore of the
				// saved snapshot.
				e.terminateAtRestore = true
			}
		}
		fallthrough
	case epState == StateListen:
		// Nothing to do.
	case epState.closed():
		// Nothing to do.
	default:
		panic(fmt.Sprintf("endpoint in unknown state %v", e.EndpointState()))
	}

	e.stack.RegisterResumableEndpoint(e)
}

// saveEndpoints is invoked by stateify.
func (a *acceptQueue) saveEndpoints() []*Endpoint {
	acceptedEndpoints := make([]*Endpoint, a.endpoints.Len())
	for i, e := 0, a.endpoints.Front(); e != nil; i, e = i+1, e.Next() {
		acceptedEndpoints[i] = e.Value.(*Endpoint)
	}
	return acceptedEndpoints
}

// loadEndpoints is invoked by stateify.
func (a *acceptQueue) loadEndpoints(_ context.Context, acceptedEndpoints []*Endpoint) {
	for _, ep := range acceptedEndpoints {
		a.endpoints.PushBack(ep)
	}
}

// saveState is invoked by stateify.
func (e *Endpoint) saveState() EndpointState {
	return e.EndpointState()
}

// Endpoint loading must be done in the following ordering by their state, to
// avoid dangling connecting w/o listening peer, and to avoid conflicts in port
// reservation.
var (
	connectedLoading  sync.WaitGroup
	listenLoading     sync.WaitGroup
	connectingLoading sync.WaitGroup
)

// Bound endpoint loading happens last.

// loadState is invoked by stateify.
func (e *Endpoint) loadState(_ context.Context, epState EndpointState) {
	// This is to ensure that the loading wait groups include all applicable
	// endpoints before any asynchronous calls to the Wait() methods.
	// For restore purposes we treat all endpoints with state after
	// StateEstablished and before StateClosed like connected endpoint.
	if epState.connected() {
		connectedLoading.Add(1)
	}
	switch {
	case epState == StateListen:
		listenLoading.Add(1)
	case epState.connecting():
		connectingLoading.Add(1)
	}
	// Directly update the state here rather than using e.setEndpointState
	// as the endpoint is still being loaded and the stack reference is not
	// yet initialized.
	e.state.Store(uint32(epState))
}

// afterLoad is invoked by stateify.
func (e *Endpoint) afterLoad(ctx context.Context) {
	// RacyLoad() can be used because we are initializing e.
	e.origEndpointState = e.state.RacyLoad()
	// Restore the endpoint to InitialState as it will be moved to
	// its origEndpointState during Restore.
	e.state = atomicbitops.FromUint32(uint32(StateInitial))
	e.stack.RegisterRestoredEndpoint(e)
}

// Close the endpoint during restore if terminateAtRestore was set for the endpoint.
func (e *Endpoint) closeEndpointAtRestore() {
	e.mu.Lock()
	defer e.mu.Unlock()

	epState := EndpointState(e.origEndpointState)
	if !epState.connected() && !epState.handshake() {
		log.Debugf("endpoint was marked to terminate at restore in a wrong state, ID: %+v state: %v", e.ID, epState)
		return
	}

	if epState.handshake() {
		connectedLoading.Wait()
		listenLoading.Wait()
	}

	// Put the endpoint in the error state and do cleanup. Do not
	// attempt to send RST as route will be nil.
	e.purgeReadQueue()
	if epState.connected() {
		e.purgeWriteQueue()
		e.purgePendingRcvQueue()
		e.cleanupLocked()
	}
	e.state.Store(uint32(StateError))
	e.closeNoShutdownLocked()
	tcpip.DeleteDanglingEndpoint(e)

	if epState.connected() {
		connectedLoading.Done()
	} else {
		connectingLoading.Done()
	}
}

// Restore implements tcpip.RestoredEndpoint.Restore.
func (e *Endpoint) Restore(s *stack.Stack) {
	if !e.EndpointState().closed() {
		e.keepalive.timer.init(s.Clock(), timerHandler(e, e.keepaliveTimerExpired))
	}
	if snd := e.snd; snd != nil {
		snd.resendTimer.init(s.Clock(), timerHandler(e, e.snd.retransmitTimerExpired))
		snd.reorderTimer.init(s.Clock(), timerHandler(e, e.snd.rc.reorderTimerExpired))
		snd.probeTimer.init(s.Clock(), timerHandler(e, e.snd.probeTimerExpired))
		snd.corkTimer.init(s.Clock(), timerHandler(e, e.snd.corkTimerExpired))
	}
	e.ops.InitHandler(e, e.stack, GetTCPSendBufferLimits, GetTCPReceiveBufferLimits)
	e.segmentQueue.thaw()

	e.mu.Lock()
	id := e.ID
	terminateAtRestore := e.terminateAtRestore
	e.mu.Unlock()

	bind := func() {
		e.mu.Lock()
		defer e.mu.Unlock()
		e.isPortReserved = true

		// Mark endpoint as bound.
		e.setEndpointState(StateBound)
	}

	if terminateAtRestore && !e.stack.AllowLiveTCPMigration() {
		e.closeEndpointAtRestore()
		return
	}

	epState := EndpointState(e.origEndpointState)
	switch {
	case epState.connected():
		if e.stack.AllowLiveTCPMigration() {
			// Handle dual stack addresses.
			netProto := e.NetProto
			switch e.TransportEndpointInfo.ID.LocalAddress.BitLen() {
			case header.IPv4AddressSizeBits:
				netProto = header.IPv4ProtocolNumber
			case header.IPv6AddressSizeBits:
				netProto = header.IPv6ProtocolNumber
			}
			// Get the new local NIC for source IP and do a FindRoute here to
			// identify if the network config is same. Then only attempt restore,
			// else close the connection on our end.
			r, err := e.stack.FindRoute(0, e.TransportEndpointInfo.ID.LocalAddress, e.TransportEndpointInfo.ID.RemoteAddress, netProto, false /* multicastLoop */)
			if err != nil {
				e.closeEndpointAtRestore()
				log.Infof("Cannot find the route %+v", e.TransportEndpointInfo.ID)
				return
			}
			e.boundNICID = r.NICID()
			r.Release()
		}
		bind()
		if e.connectingAddress.BitLen() == 0 {
			e.connectingAddress = e.TransportEndpointInfo.ID.RemoteAddress
			// This endpoint is accepted by netstack but not yet by
			// the app. If the endpoint is IPv6 but the remote
			// address is IPv4, we need to connect as IPv6 so that
			// dual-stack mode can be properly activated.
			if e.NetProto == header.IPv6ProtocolNumber && e.TransportEndpointInfo.ID.RemoteAddress.BitLen() != header.IPv6AddressSizeBits {
				e.connectingAddress = tcpip.AddrFrom16Slice(append(
					[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff},
					e.TransportEndpointInfo.ID.RemoteAddress.AsSlice()...,
				))
			}
		}
		// Reset the scoreboard to reinitialize the sack information as
		// we do not restore SACK information.
		e.scoreboard.Reset()
		// Unregister the endpoint before registering again during Connect.
		e.stack.UnregisterTransportEndpoint(e.effectiveNetProtos, header.TCPProtocolNumber, e.TransportEndpointInfo.ID, e, e.boundPortFlags, e.boundBindToDevice)
		e.mu.Lock()
		err := e.connect(tcpip.FullAddress{NIC: e.boundNICID, Addr: e.connectingAddress, Port: e.TransportEndpointInfo.ID.RemotePort}, false /* handshake */)
		if _, ok := err.(*tcpip.ErrConnectStarted); !ok {
			log.Warningf("TCP endpoint connect failed for connected endpoint with ID: %+v err: %v", id, err)
			e.mu.Unlock()
			e.Close()
			connectedLoading.Done()
			return
		}
		e.state.Store(e.origEndpointState)
		log.Infof("connect success: %+v", e.TransportEndpointInfo.ID)
		// For FIN-WAIT-2 and TIME-WAIT we need to start the appropriate timers so
		// that the socket is closed correctly.
		switch epState {
		case StateFinWait2:
			e.finWait2Timer = e.stack.Clock().AfterFunc(e.tcpLingerTimeout, e.finWait2TimerExpired)
		case StateTimeWait:
			e.timeWaitTimer = e.stack.Clock().AfterFunc(e.getTimeWaitDuration(), e.timeWaitTimerExpired)
		}

		if e.ops.GetCorkOption() {
			// Rearm the timer if TCP_CORK is enabled which will
			// drain all the segments in the queue after restore.
			e.snd.corkTimer.enable(MinRTO)
		}
		e.mu.Unlock()
		e.requeueOnRestore()
		connectedLoading.Done()
	case epState == StateListen:
		tcpip.AsyncLoading.Add(1)
		go func() {
			connectedLoading.Wait()
			e.LockUser()
			// All endpoints will be moved to initial state after
			// restore. Set endpoint to its originial listen state.
			e.setEndpointState(StateListen)
			// Initialize the listening context.
			rcvWnd := seqnum.Size(e.receiveBufferAvailable())
			e.listenCtx = newListenContext(e.stack, e.protocol, e, rcvWnd, e.ops.GetV6Only(), e.NetProto)
			e.UnlockUser()
			e.requeueOnRestore()
			listenLoading.Done()
			tcpip.AsyncLoading.Done()
		}()
	case epState == StateConnecting:
		// Initial SYN hasn't been sent yet so initiate a connect.
		tcpip.AsyncLoading.Add(1)
		go func() {
			connectedLoading.Wait()
			listenLoading.Wait()
			bind()
			err := e.Connect(tcpip.FullAddress{NIC: e.boundNICID, Addr: e.connectingAddress, Port: e.TransportEndpointInfo.ID.RemotePort})
			if _, ok := err.(*tcpip.ErrConnectStarted); !ok {
				log.Warningf("TCP endpoint connect failed for connecting endpoint with ID: %+v err: %v", id, err)
				e.Close()
			}
			connectingLoading.Done()
			tcpip.AsyncLoading.Done()
		}()
	case epState == StateSynSent || epState == StateSynRecv:
		tcpip.AsyncLoading.Add(1)
		go func() {
			connectedLoading.Wait()
			listenLoading.Wait()
			// Initial SYN has been sent/received so we should bind the
			// ports start the retransmit timer for the SYNs and let it
			// naturally complete the connection.
			bind()
			e.mu.Lock()
			e.setEndpointState(epState)
			r, err := e.stack.FindRoute(e.boundNICID, e.TransportEndpointInfo.ID.LocalAddress, e.TransportEndpointInfo.ID.RemoteAddress, e.effectiveNetProtos[0], false /* multicastLoop */)
			if err != nil {
				e.mu.Unlock()
				log.Warningf("FindRoute failed when restoring endpoint w/ ID: %+v err: %v", id, err)
				e.Close()
				connectingLoading.Done()
				tcpip.AsyncLoading.Done()
				return
			}
			e.route = r
			timer, err := newBackoffTimer(e.stack.Clock(), InitialRTO, MaxRTO, timerHandler(e, e.h.retransmitHandlerLocked))
			if err != nil {
				panic(fmt.Sprintf("newBackOffTimer(_, %s, %s, _) failed: %s", InitialRTO, MaxRTO, err))
			}
			e.h.retransmitTimer = timer
			connectingLoading.Done()
			tcpip.AsyncLoading.Done()
			e.mu.Unlock()
			e.requeueOnRestore()
		}()
	case epState == StateBound:
		tcpip.AsyncLoading.Add(1)
		go func() {
			connectedLoading.Wait()
			listenLoading.Wait()
			connectingLoading.Wait()
			bind()
			tcpip.AsyncLoading.Done()
		}()
	case epState == StateClose:
		e.isPortReserved = false
		e.state.Store(uint32(StateClose))
		e.stack.CompleteTransportEndpointCleanup(e)
		tcpip.DeleteDanglingEndpoint(e)
	case epState == StateError:
		e.state.Store(uint32(StateError))
		e.stack.CompleteTransportEndpointCleanup(e)
		tcpip.DeleteDanglingEndpoint(e)
	}
}

// Resume implements tcpip.ResumableEndpoint.Resume.
func (e *Endpoint) Resume() {
	e.segmentQueue.thaw()
}

// requeueOnRestore re-adds the endpoint to its processor's run-queue if it has
// queued segments. The run-queue is not saved across checkpoint/restore.
func (e *Endpoint) requeueOnRestore() {
	if e.segmentQueue.empty() || e.isOwnedByUser() {
		return
	}
	e.protocol.dispatcher.selectProcessor(e.TransportEndpointInfo.ID).queueEndpoint(e)
}
