diff --git a/mazepa-server/cli/parser.go b/mazepa-server/cli/parser.go
@@ -20,6 +20,6 @@ func GetParser(opts *MazepaOptions) *cobra.Command {
}
defaults := GetDefaultOptions()
- rootCmd.Flags().StringVarP(&opts.IP, "ip", "i", defaults.IP, "Address to listen on (only IPv4 supported for now)")
+ rootCmd.Flags().StringVarP(&opts.IP, "ip", "i", defaults.IP, "Address to listen on (IPv4 or IPv6)")
return rootCmd
}
diff --git a/mazepa-server/cli/types.go b/mazepa-server/cli/types.go
@@ -1,16 +1,11 @@
package cli
type MazepaOptions struct {
-
- /// Address to listen on (only IPv4 supported for now)
IP string
}
func GetDefaultOptions() MazepaOptions {
-
- opts := MazepaOptions{
+ return MazepaOptions{
IP: "127.0.0.1",
}
-
- return opts
}
diff --git a/mazepa-server/icmp/pkg.go b/mazepa-server/icmp/pkg.go
@@ -7,77 +7,162 @@ import (
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
)
-// The network must be "ip4:icmp" for IPv4 ICMP.
-const network = "ip4:icmp"
+const (
+ NetworkIPv4 = "ip4:icmp"
+ NetworkIPv6 = "ip6:ipv6-icmp"
+ ListenAddrV4 = "0.0.0.0"
+ ListenAddrV6 = "::"
+ ReadTimeout = 5 * time.Second
+ MaxPacketSize = 1500
+)
+
+type Protocol int
+
+const (
+ ProtocolIPv4 Protocol = iota
+ ProtocolIPv6
+)
+
+func DetectProtocol(addr string) Protocol {
+ ip := net.ParseIP(addr)
+ if ip == nil {
+ return ProtocolIPv4
+ }
+ if ip.To4() != nil {
+ return ProtocolIPv4
+ }
+ return ProtocolIPv6
+}
+
+func getNetworkConfig(proto Protocol) (network, listenAddr string, icmpProto int) {
+ if proto == ProtocolIPv6 {
+ return NetworkIPv6, ListenAddrV6, ipv6.ICMPTypeEchoRequest.Protocol()
+ }
+ return NetworkIPv4, ListenAddrV4, ipv4.ICMPTypeEcho.Protocol()
+}
+
+func getEchoTypes(proto Protocol) (echoRequest, echoReply icmp.Type) {
+ if proto == ProtocolIPv6 {
+ return ipv6.ICMPTypeEchoRequest, ipv6.ICMPTypeEchoReply
+ }
+ return ipv4.ICMPTypeEcho, ipv4.ICMPTypeEchoReply
+}
+
+func createConnection(network, listenAddr string) (*icmp.PacketConn, error) {
+ conn, err := icmp.ListenPacket(network, listenAddr)
+ if err != nil {
+ return nil, err
+ }
+ return conn, nil
+}
+
+func setReadDeadline(conn *icmp.PacketConn) error {
+ return conn.SetReadDeadline(time.Now().Add(ReadTimeout))
+}
+
+func readPacket(conn *icmp.PacketConn, buffer []byte) (int, net.Addr, error) {
+ return conn.ReadFrom(buffer)
+}
+
+func isTimeout(err error) bool {
+ opErr, ok := err.(*net.OpError)
+ return ok && opErr.Timeout()
+}
+
+func parseICMPMessage(data []byte, proto int) (*icmp.Message, error) {
+ return icmp.ParseMessage(proto, data)
+}
+
+func createEchoReply(echoBody *icmp.Echo, replyType icmp.Type) *icmp.Message {
+ return &icmp.Message{
+ Type: replyType,
+ Code: 0,
+ Body: &icmp.Echo{
+ ID: echoBody.ID,
+ Seq: echoBody.Seq,
+ Data: echoBody.Data,
+ },
+ }
+}
+
+func sendReply(conn *icmp.PacketConn, msg *icmp.Message, peer net.Addr) error {
+ data, err := msg.Marshal(nil)
+ if err != nil {
+ return err
+ }
+ _, err = conn.WriteTo(data, peer)
+ return err
+}
+
+func handleEchoRequest(conn *icmp.PacketConn, msg *icmp.Message, peer net.Addr, replyType icmp.Type) {
+ echoBody := msg.Body.(*icmp.Echo)
+ log.Printf("Received Echo Request from %v: ID=%d, Seq=%d", peer, echoBody.ID, echoBody.Seq)
+
+ reply := createEchoReply(echoBody, replyType)
+ if err := sendReply(conn, reply, peer); err != nil {
+ log.Printf("Failed to send reply: %v", err)
+ return
+ }
+ log.Printf("Sent Echo Reply to %v", peer)
+}
+
+func handleEchoReply(peer net.Addr) {
+ log.Printf("Received Echo Reply from %v", peer)
+}
+
+func handleUnknownType(msgType icmp.Type, peer net.Addr) {
+ log.Printf("Received unhandled ICMP message type %v from %v", msgType, peer)
+}
+
+func processMessage(conn *icmp.PacketConn, msg *icmp.Message, peer net.Addr, echoRequest, echoReply icmp.Type) {
+ switch msg.Type {
+ case echoRequest:
+ handleEchoRequest(conn, msg, peer, echoReply)
+ case echoReply:
+ handleEchoReply(peer)
+ default:
+ handleUnknownType(msg.Type, peer)
+ }
+}
func ListenAndPrint(addr string) {
+ proto := DetectProtocol(addr)
+ network, listenAddr, icmpProto := getNetworkConfig(proto)
+ echoRequest, echoReply := getEchoTypes(proto)
- conn, err := icmp.ListenPacket(network, "0.0.0.0")
+ conn, err := createConnection(network, listenAddr)
if err != nil {
log.Fatalf("ListenPacket failed: %v\nRun with sudo or elevated permissions.", err)
}
defer conn.Close()
log.Printf("Listening for ICMP packets on %s...", conn.LocalAddr().String())
- rb := make([]byte, 1500)
+ buffer := make([]byte, MaxPacketSize)
for {
- /* Set a read deadline to prevent the program from blocking indefinitely */
- err := conn.SetReadDeadline(time.Now().Add(time.Second * 5))
- if err != nil {
+ if err := setReadDeadline(conn); err != nil {
log.Printf("Error setting read deadline: %v", err)
continue
}
- n, peer, err := conn.ReadFrom(rb)
+ n, peer, err := readPacket(conn, buffer)
if err != nil {
- if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
+ if isTimeout(err) {
continue
}
log.Printf("ReadFrom failed: %v", err)
continue
}
- rm, err := icmp.ParseMessage(ipv4.ICMPTypeEcho.Protocol(), rb[:n])
+ msg, err := parseICMPMessage(buffer[:n], icmpProto)
if err != nil {
log.Printf("ParseMessage failed: %v", err)
continue
}
- switch rm.Type {
- case ipv4.ICMPTypeEcho:
- echoBody := rm.Body.(*icmp.Echo)
- log.Printf("Received Echo Request from %v: ID=%d, Seq=%d",
- peer, echoBody.ID, echoBody.Seq)
-
- wm := icmp.Message{
- Type: ipv4.ICMPTypeEchoReply, // Change type to Reply
- Code: 0,
- Body: &icmp.Echo{
- ID: echoBody.ID,
- Seq: echoBody.Seq,
- Data: echoBody.Data,
- },
- }
-
- wb, err := wm.Marshal(nil)
- if err != nil {
- log.Printf("Marshal failed: %v", err)
- continue
- }
-
- if _, err := conn.WriteTo(wb, peer); err != nil {
- log.Printf("WriteTo failed: %v", err)
- }
- log.Printf("Sent Echo Reply to %v", peer)
-
- case ipv4.ICMPTypeEchoReply:
- log.Printf("Received Echo Reply from %v", peer)
-
- default:
- log.Printf("Received unhandled ICMP message type %v from %v", rm.Type, peer)
- }
+ processMessage(conn, msg, peer, echoRequest, echoReply)
}
}
diff --git a/mazepa-server/icmp/pkg_test.go b/mazepa-server/icmp/pkg_test.go
@@ -0,0 +1,253 @@
+package icmp
+
+import (
+ "net"
+ "testing"
+
+ "golang.org/x/net/icmp"
+ "golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
+)
+
+func TestDetectProtocol(t *testing.T) {
+ tests := []struct {
+ name string
+ addr string
+ expected Protocol
+ }{
+ {"IPv4 localhost", "127.0.0.1", ProtocolIPv4},
+ {"IPv4 any", "0.0.0.0", ProtocolIPv4},
+ {"IPv4 public", "8.8.8.8", ProtocolIPv4},
+ {"IPv6 localhost", "::1", ProtocolIPv6},
+ {"IPv6 any", "::", ProtocolIPv6},
+ {"IPv6 full", "2001:4860:4860::8888", ProtocolIPv6},
+ {"IPv6 compressed", "fe80::1", ProtocolIPv6},
+ {"invalid address", "invalid", ProtocolIPv4},
+ {"empty string", "", ProtocolIPv4},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := DetectProtocol(tt.addr)
+ if result != tt.expected {
+ t.Errorf("DetectProtocol(%q) = %v, want %v", tt.addr, result, tt.expected)
+ }
+ })
+ }
+}
+
+func TestGetNetworkConfig(t *testing.T) {
+ tests := []struct {
+ name string
+ proto Protocol
+ wantNetwork string
+ wantListenAddr string
+ wantICMPProto int
+ }{
+ {
+ "IPv4",
+ ProtocolIPv4,
+ NetworkIPv4,
+ ListenAddrV4,
+ ipv4.ICMPTypeEcho.Protocol(),
+ },
+ {
+ "IPv6",
+ ProtocolIPv6,
+ NetworkIPv6,
+ ListenAddrV6,
+ ipv6.ICMPTypeEchoRequest.Protocol(),
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ network, listenAddr, icmpProto := getNetworkConfig(tt.proto)
+ if network != tt.wantNetwork {
+ t.Errorf("network = %q, want %q", network, tt.wantNetwork)
+ }
+ if listenAddr != tt.wantListenAddr {
+ t.Errorf("listenAddr = %q, want %q", listenAddr, tt.wantListenAddr)
+ }
+ if icmpProto != tt.wantICMPProto {
+ t.Errorf("icmpProto = %d, want %d", icmpProto, tt.wantICMPProto)
+ }
+ })
+ }
+}
+
+func TestGetEchoTypes(t *testing.T) {
+ tests := []struct {
+ name string
+ proto Protocol
+ wantEchoRequest icmp.Type
+ wantEchoReply icmp.Type
+ }{
+ {
+ "IPv4",
+ ProtocolIPv4,
+ ipv4.ICMPTypeEcho,
+ ipv4.ICMPTypeEchoReply,
+ },
+ {
+ "IPv6",
+ ProtocolIPv6,
+ ipv6.ICMPTypeEchoRequest,
+ ipv6.ICMPTypeEchoReply,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ echoRequest, echoReply := getEchoTypes(tt.proto)
+ if echoRequest != tt.wantEchoRequest {
+ t.Errorf("echoRequest = %v, want %v", echoRequest, tt.wantEchoRequest)
+ }
+ if echoReply != tt.wantEchoReply {
+ t.Errorf("echoReply = %v, want %v", echoReply, tt.wantEchoReply)
+ }
+ })
+ }
+}
+
+func TestIsTimeout(t *testing.T) {
+ tests := []struct {
+ name string
+ err error
+ expected bool
+ }{
+ {
+ "timeout error",
+ &net.OpError{Op: "read", Err: &timeoutError{}},
+ true,
+ },
+ {
+ "non-timeout OpError",
+ &net.OpError{Op: "read", Err: &nonTimeoutError{}},
+ false,
+ },
+ {
+ "regular error",
+ net.ErrClosed,
+ false,
+ },
+ {
+ "nil error",
+ nil,
+ false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ result := isTimeout(tt.err)
+ if result != tt.expected {
+ t.Errorf("isTimeout() = %v, want %v", result, tt.expected)
+ }
+ })
+ }
+}
+
+type timeoutError struct{}
+
+func (e *timeoutError) Error() string { return "timeout" }
+func (e *timeoutError) Timeout() bool { return true }
+func (e *timeoutError) Temporary() bool { return true }
+
+type nonTimeoutError struct{}
+
+func (e *nonTimeoutError) Error() string { return "not timeout" }
+func (e *nonTimeoutError) Timeout() bool { return false }
+func (e *nonTimeoutError) Temporary() bool { return false }
+
+func TestCreateEchoReply(t *testing.T) {
+ echoBody := &icmp.Echo{
+ ID: 1234,
+ Seq: 5678,
+ Data: []byte("test data"),
+ }
+
+ tests := []struct {
+ name string
+ replyType icmp.Type
+ }{
+ {"IPv4 reply", ipv4.ICMPTypeEchoReply},
+ {"IPv6 reply", ipv6.ICMPTypeEchoReply},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ reply := createEchoReply(echoBody, tt.replyType)
+
+ if reply.Type != tt.replyType {
+ t.Errorf("Type = %v, want %v", reply.Type, tt.replyType)
+ }
+ if reply.Code != 0 {
+ t.Errorf("Code = %d, want 0", reply.Code)
+ }
+
+ replyBody := reply.Body.(*icmp.Echo)
+ if replyBody.ID != echoBody.ID {
+ t.Errorf("ID = %d, want %d", replyBody.ID, echoBody.ID)
+ }
+ if replyBody.Seq != echoBody.Seq {
+ t.Errorf("Seq = %d, want %d", replyBody.Seq, echoBody.Seq)
+ }
+ if string(replyBody.Data) != string(echoBody.Data) {
+ t.Errorf("Data = %q, want %q", replyBody.Data, echoBody.Data)
+ }
+ })
+ }
+}
+
+func TestParseICMPMessage(t *testing.T) {
+ echoMsg := icmp.Message{
+ Type: ipv4.ICMPTypeEcho,
+ Code: 0,
+ Body: &icmp.Echo{
+ ID: 100,
+ Seq: 1,
+ Data: []byte("hello"),
+ },
+ }
+
+ data, err := echoMsg.Marshal(nil)
+ if err != nil {
+ t.Fatalf("Failed to marshal test message: %v", err)
+ }
+
+ parsed, err := parseICMPMessage(data, ipv4.ICMPTypeEcho.Protocol())
+ if err != nil {
+ t.Fatalf("parseICMPMessage failed: %v", err)
+ }
+
+ if parsed.Type != ipv4.ICMPTypeEcho {
+ t.Errorf("Type = %v, want %v", parsed.Type, ipv4.ICMPTypeEcho)
+ }
+
+ parsedBody := parsed.Body.(*icmp.Echo)
+ if parsedBody.ID != 100 {
+ t.Errorf("ID = %d, want 100", parsedBody.ID)
+ }
+ if parsedBody.Seq != 1 {
+ t.Errorf("Seq = %d, want 1", parsedBody.Seq)
+ }
+}
+
+func TestConstants(t *testing.T) {
+ if NetworkIPv4 != "ip4:icmp" {
+ t.Errorf("NetworkIPv4 = %q, want %q", NetworkIPv4, "ip4:icmp")
+ }
+ if NetworkIPv6 != "ip6:ipv6-icmp" {
+ t.Errorf("NetworkIPv6 = %q, want %q", NetworkIPv6, "ip6:ipv6-icmp")
+ }
+ if ListenAddrV4 != "0.0.0.0" {
+ t.Errorf("ListenAddrV4 = %q, want %q", ListenAddrV4, "0.0.0.0")
+ }
+ if ListenAddrV6 != "::" {
+ t.Errorf("ListenAddrV6 = %q, want %q", ListenAddrV6, "::")
+ }
+ if MaxPacketSize != 1500 {
+ t.Errorf("MaxPacketSize = %d, want 1500", MaxPacketSize)
+ }
+}
| | | |