// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// netstack doesn't build on 32-bit machines (https://github.com/google/gvisor/issues/5241)
// +build amd64 arm64 ppc64le riscv64 s390x

// Package netstack wires up gVisor's netstack into Tailscale.
package netstack

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net"
	"strings"

	"gvisor.dev/gvisor/pkg/tcpip"
	"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
	"gvisor.dev/gvisor/pkg/tcpip/buffer"
	"gvisor.dev/gvisor/pkg/tcpip/header"
	"gvisor.dev/gvisor/pkg/tcpip/link/channel"
	"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
	"gvisor.dev/gvisor/pkg/tcpip/stack"
	"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
	"gvisor.dev/gvisor/pkg/tcpip/transport/tcp"
	"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
	"gvisor.dev/gvisor/pkg/waiter"
	"inet.af/netaddr"
	"tailscale.com/control/controlclient"
	"tailscale.com/net/packet"
	"tailscale.com/types/logger"
	"tailscale.com/wgengine"
	"tailscale.com/wgengine/filter"
	"tailscale.com/wgengine/magicsock"
	"tailscale.com/wgengine/tstun"
)

func Impl(logf logger.Logf, tundev *tstun.TUN, e wgengine.Engine, mc *magicsock.Conn) error {
	if mc == nil {
		return errors.New("nil magicsock.Conn")
	}
	if tundev == nil {
		return errors.New("nil tundev")
	}
	if logf == nil {
		return errors.New("nil logger")
	}
	if e == nil {
		return errors.New("nil Engine")
	}
	ipstack := stack.New(stack.Options{
		NetworkProtocols:   []stack.NetworkProtocolFactory{ipv4.NewProtocol},
		TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4},
	})

	const mtu = 1500
	linkEP := channel.New(512, mtu, "")

	const nicID = 1
	if err := ipstack.CreateNIC(nicID, linkEP); err != nil {
		log.Fatal(err)
	}

	e.AddNetworkMapCallback(func(nm *controlclient.NetworkMap) {
		oldIPs := make(map[tcpip.Address]bool)
		for _, ip := range ipstack.AllAddresses()[nicID] {
			oldIPs[ip.AddressWithPrefix.Address] = true
		}
		newIPs := make(map[tcpip.Address]bool)
		for _, ip := range nm.Addresses {
			newIPs[tcpip.Address(ip.IPNet().IP)] = true
		}

		ipsToBeAdded := make(map[tcpip.Address]bool)
		for ip := range newIPs {
			if !oldIPs[ip] {
				ipsToBeAdded[ip] = true
			}
		}
		ipsToBeRemoved := make(map[tcpip.Address]bool)
		for ip := range oldIPs {
			if !newIPs[ip] {
				ipsToBeRemoved[ip] = true
			}
		}

		for ip := range ipsToBeRemoved {
			err := ipstack.RemoveAddress(nicID, ip)
			if err != nil {
				logf("netstack: could not deregister IP %s: %v", ip, err)
			} else {
				logf("netstack: deregistered IP %s", ip)
			}
		}
		for ip := range ipsToBeAdded {
			err := ipstack.AddAddress(nicID, ipv4.ProtocolNumber, ip)
			if err != nil {
				logf("netstack: could not register IP %s: %v", ip, err)
			} else {
				logf("netstack: registered IP %s", ip)
			}
		}
	})

	// Add 0.0.0.0/0 default route.
	subnet, _ := tcpip.NewSubnet(tcpip.Address(strings.Repeat("\x00", 4)), tcpip.AddressMask(strings.Repeat("\x00", 4)))
	ipstack.SetRouteTable([]tcpip.Route{
		{
			Destination: subnet,
			NIC:         nicID,
		},
	})

	// use Forwarder to accept any connection from stack
	fwd := tcp.NewForwarder(ipstack, 0, 16, func(r *tcp.ForwarderRequest) {
		logf("XXX ForwarderRequest: %v", r)
		var wq waiter.Queue
		ep, err := r.CreateEndpoint(&wq)
		if err != nil {
			r.Complete(true)
			return
		}
		r.Complete(false)
		c := gonet.NewTCPConn(&wq, ep)
		// TCP echo
		go echo(c, e, mc)

	})
	ipstack.SetTransportProtocolHandler(tcp.ProtocolNumber, fwd.HandlePacket)

	go func() {
		for {
			packetInfo, ok := linkEP.ReadContext(context.Background())
			if !ok {
				logf("XXX ReadContext-for-write = ok=false")
				continue
			}
			pkt := packetInfo.Pkt
			hdrNetwork := pkt.NetworkHeader()
			hdrTransport := pkt.TransportHeader()

			full := make([]byte, 0, pkt.Size())
			full = append(full, hdrNetwork.View()...)
			full = append(full, hdrTransport.View()...)
			full = append(full, pkt.Data.ToView()...)

			logf("XXX packet Write out: % x", full)
			if err := tundev.InjectOutbound(full); err != nil {
				log.Printf("netstack inject outbound: %v", err)
				return
			}

		}
	}()

	tundev.PostFilterIn = func(p *packet.Parsed, t *tstun.TUN) filter.Response {
		var pn tcpip.NetworkProtocolNumber
		switch p.IPVersion {
		case 4:
			pn = header.IPv4ProtocolNumber
		case 6:
			pn = header.IPv6ProtocolNumber
		}
		logf("XXX packet in (from %v): % x", p.Src, p.Buffer())
		vv := buffer.View(append([]byte(nil), p.Buffer()...)).ToVectorisedView()
		packetBuf := stack.NewPacketBuffer(stack.PacketBufferOptions{
			Data: vv,
		})
		linkEP.InjectInbound(pn, packetBuf)
		return filter.Accept
	}
	return nil
}

func echo(c *gonet.TCPConn, e wgengine.Engine, mc *magicsock.Conn) {
	defer c.Close()
	src, _ := netaddr.FromStdIP(c.RemoteAddr().(*net.TCPAddr).IP)
	who := ""
	if n, u, ok := mc.WhoIs(src); ok {
		who = fmt.Sprintf("%v from %v", u.DisplayName, n.Name)
	}
	fmt.Fprintf(c, "Hello, %s! Thanks for connecting to me on port %v (Try other ports too!)\nEchoing...\n",
		who,
		c.LocalAddr().(*net.TCPAddr).Port)
	buf := make([]byte, 1500)
	for {
		n, err := c.Read(buf)
		if err != nil {
			log.Printf("Err: %v", err)
			break
		}
		c.Write(buf[:n])
	}
	log.Print("Connection closed")
}