// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package icmp provides basic functions for the manipulation of
// messages used in the Internet Control Message Protocols,
// ICMPv4 and ICMPv6.
//
// ICMPv4 and ICMPv6 are defined in RFC 792 and RFC 4443.
// Multi-part message support for ICMP is defined in RFC 4884.
// ICMP extensions for MPLS are defined in RFC 4950.
// ICMP extensions for interface and next-hop identification are
// defined in RFC 5837.
// PROBE: A utility for probing interfaces is defined in RFC 8335.
package icmp // import "github.com/tailscale/xnet/icmp"

import (
	"encoding/binary"
	"errors"
	"net"
	"runtime"

	"github.com/tailscale/xnet/internal/iana"
	"github.com/tailscale/xnet/ipv4"
	"github.com/tailscale/xnet/ipv6"
)

// BUG(mikio): This package is not implemented on JS, NaCl and Plan 9.

var (
	errInvalidConn      = errors.New("invalid connection")
	errInvalidProtocol  = errors.New("invalid protocol")
	errMessageTooShort  = errors.New("message too short")
	errHeaderTooShort   = errors.New("header too short")
	errBufferTooShort   = errors.New("buffer too short")
	errInvalidBody      = errors.New("invalid body")
	errNoExtension      = errors.New("no extension")
	errInvalidExtension = errors.New("invalid extension")
	errNotImplemented   = errors.New("not implemented on " + runtime.GOOS + "/" + runtime.GOARCH)
)

func checksum(b []byte) uint16 {
	csumcv := len(b) - 1 // checksum coverage
	s := uint32(0)
	for i := 0; i < csumcv; i += 2 {
		s += uint32(b[i+1])<<8 | uint32(b[i])
	}
	if csumcv&1 == 0 {
		s += uint32(b[csumcv])
	}
	s = s>>16 + s&0xffff
	s = s + s>>16
	return ^uint16(s)
}

// A Type represents an ICMP message type.
type Type interface {
	Protocol() int
}

// A Message represents an ICMP message.
type Message struct {
	Type     Type        // type, either ipv4.ICMPType or ipv6.ICMPType
	Code     int         // code
	Checksum int         // checksum
	Body     MessageBody // body
}

// Marshal returns the binary encoding of the ICMP message m.
//
// For an ICMPv4 message, the returned message always contains the
// calculated checksum field.
//
// For an ICMPv6 message, the returned message contains the calculated
// checksum field when psh is not nil, otherwise the kernel will
// compute the checksum field during the message transmission.
// When psh is not nil, it must be the pseudo header for IPv6.
func (m *Message) Marshal(psh []byte) ([]byte, error) {
	var mtype byte
	switch typ := m.Type.(type) {
	case ipv4.ICMPType:
		mtype = byte(typ)
	case ipv6.ICMPType:
		mtype = byte(typ)
	default:
		return nil, errInvalidProtocol
	}
	b := []byte{mtype, byte(m.Code), 0, 0}
	proto := m.Type.Protocol()
	if proto == iana.ProtocolIPv6ICMP && psh != nil {
		b = append(psh, b...)
	}
	if m.Body != nil && m.Body.Len(proto) != 0 {
		mb, err := m.Body.Marshal(proto)
		if err != nil {
			return nil, err
		}
		b = append(b, mb...)
	}
	if proto == iana.ProtocolIPv6ICMP {
		if psh == nil { // cannot calculate checksum here
			return b, nil
		}
		off, l := 2*net.IPv6len, len(b)-len(psh)
		binary.BigEndian.PutUint32(b[off:off+4], uint32(l))
	}
	s := checksum(b)
	// Place checksum back in header; using ^= avoids the
	// assumption the checksum bytes are zero.
	b[len(psh)+2] ^= byte(s)
	b[len(psh)+3] ^= byte(s >> 8)
	return b[len(psh):], nil
}

var parseFns = map[Type]func(int, Type, []byte) (MessageBody, error){
	ipv4.ICMPTypeDestinationUnreachable: parseDstUnreach,
	ipv4.ICMPTypeTimeExceeded:           parseTimeExceeded,
	ipv4.ICMPTypeParameterProblem:       parseParamProb,

	ipv4.ICMPTypeEcho:                parseEcho,
	ipv4.ICMPTypeEchoReply:           parseEcho,
	ipv4.ICMPTypeExtendedEchoRequest: parseExtendedEchoRequest,
	ipv4.ICMPTypeExtendedEchoReply:   parseExtendedEchoReply,

	ipv6.ICMPTypeDestinationUnreachable: parseDstUnreach,
	ipv6.ICMPTypePacketTooBig:           parsePacketTooBig,
	ipv6.ICMPTypeTimeExceeded:           parseTimeExceeded,
	ipv6.ICMPTypeParameterProblem:       parseParamProb,

	ipv6.ICMPTypeEchoRequest:         parseEcho,
	ipv6.ICMPTypeEchoReply:           parseEcho,
	ipv6.ICMPTypeExtendedEchoRequest: parseExtendedEchoRequest,
	ipv6.ICMPTypeExtendedEchoReply:   parseExtendedEchoReply,
}

// ParseMessage parses b as an ICMP message.
// The provided proto must be either the ICMPv4 or ICMPv6 protocol
// number.
func ParseMessage(proto int, b []byte) (*Message, error) {
	if len(b) < 4 {
		return nil, errMessageTooShort
	}
	var err error
	m := &Message{Code: int(b[1]), Checksum: int(binary.BigEndian.Uint16(b[2:4]))}
	switch proto {
	case iana.ProtocolICMP:
		m.Type = ipv4.ICMPType(b[0])
	case iana.ProtocolIPv6ICMP:
		m.Type = ipv6.ICMPType(b[0])
	default:
		return nil, errInvalidProtocol
	}
	if fn, ok := parseFns[m.Type]; !ok {
		m.Body, err = parseRawBody(proto, b[4:])
	} else {
		m.Body, err = fn(proto, m.Type, b[4:])
	}
	if err != nil {
		return nil, err
	}
	return m, nil
}
