Copyright (c) 2024 Huawei Technologies Co., Ltd.
openFuyao is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.
*/
package checker
import (
"bytes"
"encoding/gob"
"fmt"
"log"
"net"
"os"
"strings"
"sync"
"time"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
"openfuyao.com/oscheck/internal/conf"
"openfuyao.com/oscheck/internal/utils"
)
const (
icmpPayloadMagic = "OS_CHECK_ICMP_ECHO"
protocolICMP = 1
protocolICMPv6 = 58
pingTimeout = 5 * time.Second
pingTimes = 5
pingReadInterval = 100 * time.Microsecond
pidMask = 0xffff
successRate = 0.5
)
const (
icmpPingStatusToStart = iota
icmpPingStatusRunning
icmpPingStatusToStop
icmpPingStoping
icmpPingStatusStopped
)
var (
idGenerator = NewIdGenerator()
)
type PingChecker struct {
ctx Context
config conf.CheckItem
logger *log.Logger
targets map[string]PingTarget
ipv4Ping IcmpPing
ipv6Ping IcmpPing
}
type PingTarget struct {
IP net.IP
Name string
}
type PingTargets struct {
Targets []string `yaml:"targets" json:"targets"`
}
func (c *PingChecker) Init(ctx Context, item conf.CheckItem) *ParamCheckError {
c.logger = utils.GetLogger("PingChecker")
c.ctx = ctx
c.config = item
spec := &PingTargets{}
err := convertSpec2Special(item, &spec)
if err != nil {
c.logger.Printf("Failed to init PingChecker from %s, because: %s", item.FilePath, err.Error())
return &ParamCheckError{
FormatErr: true,
Msg: "Format Error",
}
}
c.targets = make(map[string]PingTarget, len(spec.Targets))
hasIpv4Target, hasIpv6Target, failedIP := c.getTarget(spec)
if len(failedIP) > 0 {
c.logger.Printf("Failed to init PingChecker from %s, because some ip format is error, ips: %s",
item.FilePath, strings.Join(failedIP, ","))
return &ParamCheckError{
ErrField: []string{fmt.Sprintf("spec.targets:%s", strings.Join(failedIP, ","))},
FormatErr: true,
Msg: "Format Error",
}
}
if hasIpv4Target {
c.ipv4Ping, err = NewIcmpPing(c.logger, true)
if err != nil {
c.logger.Printf("Failed to init PingChecker from %s, because: %s", item.FilePath, err.Error())
return &ParamCheckError{
FormatErr: true,
Msg: "Format Error",
}
}
}
if hasIpv6Target {
c.ipv6Ping, err = NewIcmpPing(c.logger, false)
if err != nil {
c.logger.Printf("Failed to init PingChecker from %s, because: %s", item.FilePath, err.Error())
return &ParamCheckError{
FormatErr: true,
Msg: "Format Error",
}
}
}
return nil
}
func (c *PingChecker) getTarget(spec *PingTargets) (bool, bool, []string) {
failedIps := make([]string, 0)
hasIpv4Target := false
hasIpv6Target := false
for _, target := range spec.Targets {
target = strings.TrimSpace(target)
if len(target) == 0 {
continue
}
splitedTarget := strings.Split(target, "@")
target := PingTarget{}
targetIp := splitedTarget[0]
target.Name = targetIp
if len(splitedTarget) > 1 {
target.Name = splitedTarget[1]
}
target.IP = net.ParseIP(targetIp)
if target.IP == nil {
failedIps = append(failedIps, targetIp)
continue
}
c.targets[targetIp] = target
if target.IP.To4() != nil {
hasIpv4Target = true
} else {
hasIpv6Target = true
}
}
return hasIpv4Target, hasIpv6Target, failedIps
}
func (c *PingChecker) Check() ItemRslt {
rslt := ItemRslt{
Key: c.config.Name,
Result: ResultValid,
Doc: c.config.Doc,
SubItems: make([]SubItemRslt, 0, len(c.targets)),
}
c.logger.Printf("Start to check ping targets: %v", c.targets)
var wg sync.WaitGroup
resultChan := make(chan SubItemRslt, len(c.targets))
for _, target := range c.targets {
wg.Add(1)
go func(target PingTarget) {
defer wg.Done()
c.execPing(target, resultChan)
}(target)
}
wg.Wait()
close(resultChan)
for subRslt := range resultChan {
rslt.SubItems = append(rslt.SubItems, subRslt)
if subRslt.Result == ResultError {
rslt.Result = ResultError
} else if subRslt.Result == ResultInvalid && rslt.Result != ResultError {
rslt.Result = ResultInvalid
}
}
if c.ipv4Ping != nil {
c.ipv4Ping.Stop()
}
if c.ipv6Ping != nil {
c.ipv6Ping.Stop()
}
c.logger.Printf("Ping check result: %v", rslt)
return rslt
}
func (c *PingChecker) execPing(target PingTarget, resultChan chan SubItemRslt) {
subRslt := SubItemRslt{
Key: target.Name,
Expect: fmt.Sprintf("%s Reachable", target.IP),
Result: ResultValid,
}
pingInstance := c.ipv4Ping
if target.IP.To4() == nil {
pingInstance = c.ipv6Ping
}
c.logger.Printf("Start to ping target %s", target.IP)
successCount, err := pingAndGetSuccessCount(
pingInstance, target.IP, pingTimes, pingTimeout, c.logger)
if err != nil {
c.logger.Printf("ICMP Ping task %d ping failed, error: %s", target.IP, err.Error())
subRslt.Result = ResultError
}
if float64(successCount) < float64(pingTimes)*successRate {
subRslt.Result = ResultInvalid
}
subRslt.Real = fmt.Sprintf("%d successful, %d attempts", successCount, pingTimes)
c.logger.Printf("Ping target %s result: %v, %d/%d", target.IP, subRslt, successCount, pingTimes)
resultChan <- subRslt
}
type IdGenerator struct {
mu sync.Mutex
counter int64
}
func NewIdGenerator() *IdGenerator {
return &IdGenerator{}
}
func (g *IdGenerator) NextId() int64 {
g.mu.Lock()
defer g.mu.Unlock()
g.counter++
return g.counter
}
type ICMPResp struct {
Addr net.Addr
Id int64
Timestamp int64
seq int
}
func pingAndGetSuccessCount(
icmpPing IcmpPing, target net.IP, times int,
timeout time.Duration, logger *log.Logger) (int, error) {
msgChan := make(chan ICMPResp, times)
id := idGenerator.NextId()
icmpPing.AddRespProcessor(id, func(resp ICMPResp) {
msgChan <- resp
})
timer := time.NewTimer(timeout)
defer timer.Stop()
successCount := 0
for i := 1; i <= times; i++ {
err := icmpPing.SendICMPEchoRequest(target, id, i)
if err != nil {
logger.Printf("ICMP Ping task %d send request failed, error: %s", id, err.Error())
return successCount, err
}
select {
case resp := <-msgChan:
logger.Printf("ICMP Ping task %d received response: %v, expect seq: %d", id, resp, i)
if resp.seq == i {
successCount++
}
if !timer.Stop() {
<-timer.C
}
timer.Reset(timeout)
case <-timer.C:
logger.Printf("ICMP Ping task %d timed out", id)
timer.Reset(timeout)
}
}
return successCount, nil
}
type IcmpPing interface {
Start() error
AddRespProcessor(id int64, respProcessor func(ICMPResp))
DelRespProcessor(id int64)
SendICMPEchoRequest(target net.IP, id int64, seq int) error
Stop()
}
type IcmpPingImpl struct {
mutex *sync.RWMutex
conn *icmp.PacketConn
taskMsgRecevers map[int64]func(ICMPResp)
status int
isIpv4 bool
logger *log.Logger
}
func (p *IcmpPingImpl) DelRespProcessor(id int64) {
p.mutex.Lock()
delete(p.taskMsgRecevers, id)
p.mutex.Unlock()
}
func (p *IcmpPingImpl) AddRespProcessor(id int64, processor func(ICMPResp)) {
p.mutex.Lock()
p.taskMsgRecevers[id] = processor
p.mutex.Unlock()
}
func (p *IcmpPingImpl) newConn(isIPv4 bool) (*icmp.PacketConn, error) {
var network string
var localAddr string
if isIPv4 {
network = "ip4:icmp"
localAddr = "0.0.0.0"
} else {
network = "ip6:ipv6-icmp"
localAddr = "::"
}
c, err := icmp.ListenPacket(network, localAddr)
if err != nil {
return nil, fmt.Errorf("failed to create ICMP connection: %w", err)
}
return c, nil
}
func (p *IcmpPingImpl) Start() error {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.status == icmpPingStatusRunning {
return nil
}
if p.status != icmpPingStatusToStart {
return fmt.Errorf("failed to start icmp ping, status is not to start nor running, but %d", p.status)
}
conn, err := p.newConn(p.isIpv4)
if err != nil {
p.logger.Printf("Failed to create ICMP connection: %s", err.Error())
return fmt.Errorf("failed to create ICMP connection: %w", err)
}
p.conn = conn
if p.status == icmpPingStatusToStart || p.status == icmpPingStatusStopped {
go p.readIcmpMsgLoop()
}
p.status = icmpPingStatusRunning
return nil
}
func (p *IcmpPingImpl) readIcmpMsgLoop() {
conn := p.conn
pid := os.Getpid() & pidMask
var proto = protocolICMP
if !p.isIpv4 {
proto = protocolICMPv6
}
for {
p.mutex.RLock()
status := p.status
p.mutex.RUnlock()
if status == icmpPingStatusToStop {
p.logger.Printf("ICMP ping read loop stoped")
break
}
buffer := make([]byte, 1024)
byteSize, addr, err := conn.ReadFrom(buffer)
if p.status == icmpPingStatusToStop {
p.logger.Printf("ICMP ping read loop stoped")
return
}
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
p.logger.Printf("ICMP conn error while read, error: %s", err.Error())
break
}
icmpMsg, err := icmp.ParseMessage(proto, buffer[:byteSize])
if err != nil {
p.logger.Printf("Failed to parse ICMP message: %s", err.Error())
continue
}
p.parseEchoReply(icmpMsg, addr, pid)
}
p.mutex.Lock()
defer p.mutex.Unlock()
err := conn.Close()
if err != nil {
p.logger.Printf("ICMP conn error while close, error: %s", err.Error())
}
p.status = icmpPingStatusStopped
p.conn = nil
}
func (p *IcmpPingImpl) parseEchoReply(icmpMsg *icmp.Message, addr net.Addr, pid int) bool {
switch icmpMsg.Type {
case ipv4.ICMPTypeEchoReply, ipv6.ICMPTypeEchoReply:
echo, ok := icmpMsg.Body.(*icmp.Echo)
if !ok {
p.logger.Printf("Failed to parse ICMP message, not Echo msg")
return false
}
if echo.ID != pid {
p.logger.Printf("Failed to parse ICMP message, other pid: %d", echo.ID)
return false
}
payload := &ICMPPayload{Header: icmpPayloadMagic}
payload.Unmarshal(echo.Data)
if payload.Header != icmpPayloadMagic {
p.logger.Printf("Failed to parse ICMP message, payload magic does not match: %v, expect: %v",
payload.Header, icmpPayloadMagic)
return false
}
resp := ICMPResp{
Addr: addr,
Id: payload.Id,
Timestamp: payload.Timestamp,
seq: echo.Seq,
}
receiver, ok := p.taskMsgRecevers[resp.Id]
if ok {
receiver(resp)
return true
}
return false
default:
return false
}
}
func (p *IcmpPingImpl) Stop() {
p.mutex.Lock()
defer p.mutex.Unlock()
if p.status == icmpPingStatusToStop {
return
}
p.status = icmpPingStatusToStop
}
func (p *IcmpPingImpl) SendICMPEchoRequest(target net.IP, id int64, seq int) error {
payload := &ICMPPayload{
Id: id,
Timestamp: time.Now().UnixNano(),
}
payloadBytes, err := payload.Marshal()
if err != nil {
return fmt.Errorf("failed to marshal ICMP payload: %w", err)
}
var msgType icmp.Type
if target.To4() != nil {
msgType = ipv4.ICMPTypeEcho
} else {
msgType = ipv6.ICMPTypeEchoRequest
}
msg := icmp.Message{
Type: msgType,
Code: 0,
Body: &icmp.Echo{
ID: os.Getpid() & pidMask,
Seq: seq,
Data: payloadBytes,
},
}
msgBytes, err := msg.Marshal(nil)
if err != nil {
return fmt.Errorf("failed to marshal ICMP message: %w", err)
}
_, err = p.conn.WriteTo(msgBytes, &net.IPAddr{IP: target})
if err != nil {
return fmt.Errorf("failed to send ICMP echo request: %w", err)
}
return nil
}
func NewIcmpPing(logger *log.Logger, isIpv4 bool) (IcmpPing, error) {
ping := IcmpPingImpl{
logger: logger,
isIpv4: isIpv4,
taskMsgRecevers: make(map[int64]func(ICMPResp)),
status: icmpPingStatusToStart,
mutex: &sync.RWMutex{},
conn: nil,
}
err := ping.Start()
if err != nil {
return nil, err
}
return &ping, nil
}
type ICMPPayload struct {
Header string
Id int64
Timestamp int64
}
func (p *ICMPPayload) Marshal() ([]byte, error) {
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
if err := encoder.Encode(p); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (p *ICMPPayload) Unmarshal(data []byte) error {
decoder := gob.NewDecoder(bytes.NewReader(data))
if err := decoder.Decode(p); err != nil {
return err
}
return nil
}
func init() {
factory := func() Checker {
return &PingChecker{}
}
RegisterCheckerFactory("ping", factory)
RegisterCheckerFactory("ping-checker", factory)
}