Golang makes it really simple to code some otherwise arduous tasks. In this case, we needed to sniff raw network data, filter out DNS data, and send the DNS queries and responses to an Elasticsearch cluster.
The resulting code is surprisingly simple thanks to libraries such as GoPacket, as you can see below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"bytes" | |
"encoding/json" | |
"fmt" | |
"log" | |
"net/http" | |
"strconv" | |
"sync" | |
"time" | |
"github.com/google/gopacket" | |
"github.com/google/gopacket/layers" | |
"github.com/google/gopacket/pcap" | |
) | |
var ( | |
devName string | |
es_index string | |
es_docType string | |
es_server string | |
err error | |
handle *pcap.Handle | |
InetAddr string | |
SrcIP string | |
DstIP string | |
) | |
type DnsMsg struct { | |
Timestamp string | |
SourceIP string | |
DestinationIP string | |
DnsQuery string | |
DnsAnswer []string | |
DnsAnswerTTL []string | |
NumberOfAnswers string | |
DnsResponseCode string | |
DnsOpCode string | |
} | |
func sendToElastic(dnsMsg DnsMsg, wg *sync.WaitGroup) { | |
defer wg.Done() | |
var jsonMsg, jsonErr = json.Marshal(dnsMsg) | |
if jsonErr != nil { | |
panic(jsonErr) | |
} | |
// getting ready for elasticsearch | |
request, reqErr := http.NewRequest("POST", "http://"+es_server+":9200/"+es_index+"/"+es_docType, | |
bytes.NewBuffer(jsonMsg)) | |
if reqErr != nil { | |
panic(reqErr) | |
} | |
client := &http.Client{} | |
resp, elErr := client.Do(request) | |
if elErr != nil { | |
panic(elErr) | |
} | |
defer resp.Body.Close() | |
} | |
func main() { | |
////// CONFIG SECTION: REVIEW THESE BEFORE USING | |
// select a device to listen on | |
//windows example | |
devName = "\\Device\\NPF_{9CA25EBF-B3D8-4FD0-90A6-070A16A7F2B4}" | |
//linux example | |
//devName = "eth0" | |
// define an elasticsearch server to send to | |
es_server = "192.168.10.15" | |
// define an elasticsearch index to send to | |
es_index = "dns_index" | |
es_docType = "syslog" | |
// END CONFIG SECTION | |
var eth layers.Ethernet | |
var ip4 layers.IPv4 | |
var ip6 layers.IPv6 | |
var tcp layers.TCP | |
var udp layers.UDP | |
var dns layers.DNS | |
var payload gopacket.Payload | |
wg := new(sync.WaitGroup) | |
// Find all devices | |
devices, devErr := pcap.FindAllDevs() | |
if devErr != nil { | |
log.Fatal(devErr) | |
} | |
// Print device information | |
fmt.Println("Devices found:") | |
for _, device := range devices { | |
fmt.Println("\nName: ", device.Name) | |
fmt.Println("Description: ", device.Description) | |
fmt.Println("Devices addresses: ", device.Description) | |
for _, address := range device.Addresses { | |
if device.Name == devName { | |
InetAddr = address.IP.String() | |
break | |
} | |
fmt.Println("- IP address: ", address.IP) | |
fmt.Println("- Subnet mask: ", address.Netmask) | |
} | |
} | |
// // Create DNSQuery index | |
// _, elErr = client.CreateIndex("dns_query").Do() | |
// if elErr != nil { | |
// // Handle error | |
// panic(elErr) | |
// } | |
// Open device | |
handle, err = pcap.OpenLive(devName, 1600, false, pcap.BlockForever) | |
if err != nil { | |
log.Fatal(err) | |
} | |
defer handle.Close() | |
// Set filter | |
var filter string = "udp and port 53 and src host " + InetAddr | |
fmt.Println(" Filter: ", filter) | |
err := handle.SetBPFFilter(filter) | |
if err != nil { | |
log.Fatal(err) | |
} | |
parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet, ð, &ip4, &ip6, &tcp, &udp, &dns, &payload) | |
decodedLayers := make([]gopacket.LayerType, 0, 10) | |
for { | |
data, _, err := handle.ReadPacketData() | |
if err != nil { | |
fmt.Println("Error reading packet data: ", err) | |
continue | |
} | |
err = parser.DecodeLayers(data, &decodedLayers) | |
for _, typ := range decodedLayers { | |
switch typ { | |
case layers.LayerTypeIPv4: | |
SrcIP = ip4.SrcIP.String() | |
DstIP = ip4.DstIP.String() | |
case layers.LayerTypeIPv6: | |
SrcIP = ip6.SrcIP.String() | |
DstIP = ip6.DstIP.String() | |
case layers.LayerTypeDNS: | |
dnsOpCode := int(dns.OpCode) | |
dnsResponseCode := int(dns.ResponseCode) | |
dnsANCount := int(dns.ANCount) | |
if (dnsANCount == 0 && dnsResponseCode > 0) || (dnsANCount > 0) { | |
fmt.Println("————————") | |
fmt.Println(" DNS Record Detected") | |
for _, dnsQuestion := range dns.Questions { | |
t := time.Now() | |
timestamp := t.Format(time.RFC3339) | |
// Add a document to the index | |
d := DnsMsg{Timestamp: timestamp, SourceIP: SrcIP, | |
DestinationIP: DstIP, | |
DnsQuery: string(dnsQuestion.Name), | |
DnsOpCode: strconv.Itoa(dnsOpCode), | |
DnsResponseCode: strconv.Itoa(dnsResponseCode), | |
NumberOfAnswers: strconv.Itoa(dnsANCount)} | |
fmt.Println(" DNS OpCode: ", strconv.Itoa(int(dns.OpCode))) | |
fmt.Println(" DNS ResponseCode: ", dns.ResponseCode.String()) | |
fmt.Println(" DNS # Answers: ", strconv.Itoa(dnsANCount)) | |
fmt.Println(" DNS Question: ", string(dnsQuestion.Name)) | |
fmt.Println(" DNS Endpoints: ", SrcIP, DstIP) | |
if dnsANCount > 0 { | |
for _, dnsAnswer := range dns.Answers { | |
d.DnsAnswerTTL = append(d.DnsAnswerTTL, fmt.Sprint(dnsAnswer.TTL)) | |
if dnsAnswer.IP.String() != "<nil>" { | |
fmt.Println(" DNS Answer: ", dnsAnswer.IP.String()) | |
d.DnsAnswer = append(d.DnsAnswer, dnsAnswer.IP.String()) | |
} | |
} | |
} | |
wg.Add(1) | |
sendToElastic(d, wg) | |
} | |
} | |
} | |
} | |
if err != nil { | |
fmt.Println(" Error encountered:", err) | |
} | |
} | |
} |
The program compiles on Windows as well as Linux (make sure you have CGO enabled). The project was ultimately discarded because Packetbeat is awesome, but it still served as a very good coding exersize.
Some screenshots of the program in action on Windows Server:
And the resulting entries in ElasticSearch:
There’s obviously loads missing, like proper error recovery… but it works.
PS the program can be converted into a service in the background using the excellent Non-sucking service manager
References
http://www.devdungeon.com/content/packet-capture-injection-and-analysis-gopacket
You must be logged in to post a comment.