package netlink

import (
	"fmt"
	"net"
	"strconv"
	"syscall"

	"github.com/evilsocket/opensnitch/daemon/log"
	"github.com/vishvananda/netlink"
	"golang.org/x/sys/unix"
)

// GetSocketInfo asks the kernel via netlink for a given connection.
// If the connection is found, we return the uid and the possible
// associated inodes.
// If the outgoing connection is not found but there're entries with the source
// port and same protocol, add all the inodes to the list.
//
// Some examples:
// outgoing connection as seen by netfilter || connection details dumped from kernel
//
// 47344:192.168.1.106 -> 151.101.65.140:443 || in kernel: 47344:192.168.1.106 -> 151.101.65.140:443
// 8612:192.168.1.5 -> 192.168.1.255:8612  || in kernel: 8612:192.168.1.105 -> 0.0.0.0:0
// 123:192.168.1.5  -> 217.144.138.234:123 || in kernel: 123:0.0.0.0 -> 0.0.0.0:0
// 45015:127.0.0.1 -> 239.255.255.250:1900 || in kernel: 45015:127.0.0.1 -> 0.0.0.0:0
// 50416:fe80::9fc2:ddcf:df22:aa50 -> fe80::1:53 || in kernel: 50416:254.128.0.0 -> 254.128.0.0:53
// 51413:192.168.1.106 -> 103.224.182.250:1337 || in kernel: 51413:0.0.0.0 -> 0.0.0.0:0
func GetSocketInfo(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) (uid int, inodes []int) {
	uid = -1
	family := uint8(syscall.AF_INET)
	ipproto := uint8(syscall.IPPROTO_TCP)
	protoLen := len(proto)
	if proto[protoLen-1:protoLen] == "6" {
		family = syscall.AF_INET6
	}

	if proto[:3] == "udp" {
		ipproto = syscall.IPPROTO_UDP
		if protoLen >= 7 && proto[:7] == "udplite" {
			ipproto = syscall.IPPROTO_UDPLITE
		}
	}
	if protoLen >= 4 && proto[:4] == "sctp" {
		ipproto = syscall.IPPROTO_SCTP
	}
	if protoLen >= 4 && proto[:4] == "icmp" {
		ipproto = syscall.IPPROTO_RAW
	}
	if sockList, err := SocketGet(family, ipproto, uint16(srcPort), uint16(dstPort), srcIP, dstIP); err == nil {
		for n, sock := range sockList {
			if sock.UID != 0xffffffff {
				uid = int(sock.UID)
			}
			log.Debug("[%d/%d] outgoing connection uid: %d, %d:%v -> %v:%d || netlink response: %d:%v -> %v:%d inode: %d - loopback: %v multicast: %v unspecified: %v linklocalunicast: %v ifaceLocalMulticast: %v GlobalUni: %v ",
				n, len(sockList),
				int(sock.UID),
				srcPort, srcIP, dstIP, dstPort,
				sock.ID.SourcePort, sock.ID.Source,
				sock.ID.Destination, sock.ID.DestinationPort, sock.INode,
				sock.ID.Destination.IsLoopback(),
				sock.ID.Destination.IsMulticast(),
				sock.ID.Destination.IsUnspecified(),
				sock.ID.Destination.IsLinkLocalUnicast(),
				sock.ID.Destination.IsLinkLocalMulticast(),
				sock.ID.Destination.IsGlobalUnicast(),
			)

			if sock.ID.SourcePort == uint16(srcPort) && sock.ID.Source.Equal(srcIP) &&
				(sock.ID.DestinationPort == uint16(dstPort)) &&
				((sock.ID.Destination.IsGlobalUnicast() || sock.ID.Destination.IsLoopback()) && sock.ID.Destination.Equal(dstIP)) {
				inodes = append([]int{int(sock.INode)}, inodes...)
				continue
			}
			log.Debug("GetSocketInfo() invalid: %d:%v -> %v:%d", sock.ID.SourcePort, sock.ID.Source, sock.ID.Destination, sock.ID.DestinationPort)
		}

		// handle special cases (see function description): ntp queries (123), broadcasts, incomming connections.
		if len(inodes) == 0 && len(sockList) > 0 {
			for n, sock := range sockList {
				if sockList[n].ID.Destination.Equal(net.IPv4zero) || sockList[n].ID.Destination.Equal(net.IPv6zero) {
					inodes = append([]int{int(sock.INode)}, inodes...)
					log.Debug("netlink socket not found, adding entry:  %d:%v -> %v:%d || %d:%v -> %v:%d inode: %d state: %s",
						srcPort, srcIP, dstIP, dstPort,
						sockList[n].ID.SourcePort, sockList[n].ID.Source,
						sockList[n].ID.Destination, sockList[n].ID.DestinationPort,
						sockList[n].INode, TCPStatesMap[sock.State])
				} else if sock.ID.SourcePort == uint16(srcPort) && sock.ID.Source.Equal(srcIP) &&
					(sock.ID.DestinationPort == uint16(dstPort)) {
					inodes = append([]int{int(sock.INode)}, inodes...)
					continue
				} else {
					log.Debug("netlink socket not found, EXCLUDING entry:  %d:%v -> %v:%d || %d:%v -> %v:%d inode: %d state: %s",
						srcPort, srcIP, dstIP, dstPort,
						sockList[n].ID.SourcePort, sockList[n].ID.Source,
						sockList[n].ID.Destination, sockList[n].ID.DestinationPort,
						sockList[n].INode, TCPStatesMap[sock.State])
				}
			}
		}
	} else {
		log.Debug("netlink socket error: %v - %d:%v -> %v:%d", err, srcPort, srcIP, dstIP, dstPort)
	}

	return uid, inodes
}

// GetSocketInfoByInode dumps the kernel sockets table and searches the given
// inode on it.
func GetSocketInfoByInode(inodeStr string) (*Socket, error) {
	inode, err := strconv.ParseUint(inodeStr, 10, 32)
	if err != nil {
		return nil, err
	}

	type inetStruct struct{ family, proto uint8 }
	socketTypes := []inetStruct{
		{syscall.AF_INET, syscall.IPPROTO_TCP},
		{syscall.AF_INET, syscall.IPPROTO_UDP},
		{syscall.AF_INET6, syscall.IPPROTO_TCP},
		{syscall.AF_INET6, syscall.IPPROTO_UDP},
	}

	for _, socket := range socketTypes {
		socketList, err := SocketsDump(socket.family, socket.proto)
		if err != nil {
			return nil, err
		}
		for idx := range socketList {
			if uint32(inode) == socketList[idx].INode {
				return socketList[idx], nil
			}
		}
	}
	return nil, fmt.Errorf("Inode not found")
}

// KillSocket kills a socket given the properties of a connection.
func KillSocket(proto string, srcIP net.IP, srcPort uint, dstIP net.IP, dstPort uint) {
	family := uint8(syscall.AF_INET)
	ipproto := uint8(syscall.IPPROTO_TCP)
	protoLen := len(proto)
	if proto[protoLen-1:protoLen] == "6" {
		family = syscall.AF_INET6
	}

	if proto[:3] == "udp" {
		ipproto = syscall.IPPROTO_UDP
		if protoLen >= 7 && proto[:7] == "udplite" {
			ipproto = syscall.IPPROTO_UDPLITE
		}
	}

	if sockList, err := SocketGet(family, ipproto, uint16(srcPort), uint16(dstPort), srcIP, dstIP); err == nil {
		for _, s := range sockList {
			if err := SocketKill(family, ipproto, s.ID); err != nil {
				log.Debug("Unable to kill socket: %d, %d, %v", srcPort, dstPort, err)
			}
		}
	}
}

// KillSockets kills all sockets given a family and a protocol.
// Be careful if you don't exclude local sockets, many local servers may misbehave,
// entering in an infinite loop.
func KillSockets(fam, proto uint8, excludeLocal bool) error {
	sockListTCP, err := SocketsDump(fam, proto)
	if err != nil {
		return fmt.Errorf("eBPF could not dump TCP (%d/%d) sockets via netlink: %v", fam, proto, err)
	}

	for _, sock := range sockListTCP {
		if sock == nil {
			continue
		}
		if excludeLocal && (isPrivate(sock.ID.Destination) ||
			sock.ID.Source.IsUnspecified() ||
			sock.ID.Destination.IsUnspecified()) {
			log.Trace("not killing socket: %+v", sock.ID)
			continue
		}
		log.Trace("killing socket: %+v", sock.ID)
		if err := SocketKill(fam, proto, sock.ID); err != nil {
			log.Trace("Unable to kill socket (%+v): %s", sock.ID, err)
		}
	}

	return nil
}

// KillAllSockets kills the sockets for the given families and protocols.
func KillAllSockets() {
	type opts struct {
		fam   uint8
		proto uint8
	}
	optList := []opts{
		// add families and protos as wish
		{unix.AF_INET, uint8(syscall.IPPROTO_TCP)},
		{unix.AF_INET6, uint8(syscall.IPPROTO_TCP)},
		{unix.AF_INET, uint8(syscall.IPPROTO_UDP)},
		{unix.AF_INET6, uint8(syscall.IPPROTO_UDP)},
		{unix.AF_INET, uint8(syscall.IPPROTO_SCTP)},
		{unix.AF_INET6, uint8(syscall.IPPROTO_SCTP)},
	}
	for _, opt := range optList {
		KillSockets(opt.fam, opt.proto, true)
	}

}

// FlushConnections flushes conntrack as soon as netfilter rule is set.
// This ensures that already-established connections will go to netfilter queue.
func FlushConnections() {
	if err := netlink.ConntrackTableFlush(netlink.ConntrackTable); err != nil {
		log.Error("error flushing ConntrackTable %s", err)
	}
	if err := netlink.ConntrackTableFlush(netlink.ConntrackExpectTable); err != nil {
		log.Error("error flusing ConntrackExpectTable %s", err)
	}

	// Force established connections to reestablish again.
	KillAllSockets()
}

// SocketsAreEqual compares 2 different sockets to see if they match.
func SocketsAreEqual(aSocket, bSocket *Socket) bool {
	return ((*aSocket).INode == (*bSocket).INode &&
		//inodes are unique enough, so the matches below will never have to be checked
		(*aSocket).ID.SourcePort == (*bSocket).ID.SourcePort &&
		(*aSocket).ID.Source.Equal((*bSocket).ID.Source) &&
		(*aSocket).ID.Destination.Equal((*bSocket).ID.Destination) &&
		(*aSocket).ID.DestinationPort == (*bSocket).ID.DestinationPort &&
		(*aSocket).UID == (*bSocket).UID)
}
