Skip to content

Commit

Permalink
fix: filtering dns server in agent instead of nftables (#45)
Browse files Browse the repository at this point in the history
Filtering in agent gives us better visibility for now, since we don't
currently log blocked traffic from nftables
  • Loading branch information
fallard84 authored Jul 1, 2024
1 parent 46f90f1 commit e1a20a3
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/bullfrog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ jobs:
exit 1;
fi;
if timeout 5 dig @8.8.8.8 www.google.com; then
echo 'Expected 'dig @8.8.8.8 www.google.com' to fail, but it succeeded';
exit 1;
fi;
test-block-but-allow-any-dns-requests:
needs: build
runs-on: ubuntu-22.04
Expand Down
7 changes: 6 additions & 1 deletion .vagrant/provision.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ export DEBIAN_FRONTEND=noninteractive
apt-get update

# install curl and other dependencies
apt-get install -y curl software-properties-common apt-utils jq golang
apt-get install -y curl software-properties-common apt-utils jq make net-tools libnetfilter-queue-dev

# install golang 1.22.4
curl -OL https://go.dev/dl/go1.22.4.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> /home/vagrant/.bashrc

# install Node.js 20.x
curl -sL https://deb.nodesource.com/setup_20.x | bash -
Expand Down
90 changes: 85 additions & 5 deletions agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,61 @@ func setAgentIsReady() {
fmt.Fprintf(f, "%d\n", time.Now().Unix())
}

func getDNSServer() (string, error) {
networkInterface, err := exec.Command("sh", "-c", "ip route | grep default | awk '{print $5}'").Output()
if err != nil {
fmt.Printf("Error getting default network interface: %s\n", err)
return "", err
}
// remove new line of networkInterface
networkInterface = networkInterface[:len(networkInterface)-1]
fmt.Printf("Network interface: %s\n", networkInterface)

cmd := fmt.Sprintf("resolvectl status %s | grep 'DNS Servers' | awk '{print $3}'", networkInterface)
fmt.Printf("cmd: %s\n", cmd)

dnsServer, err := exec.Command("sh", "-c", cmd).CombinedOutput()
if err != nil {
fmt.Println("Error getting DNS server: ", err)
return "", err
}
// remove new line of dnsServer
dnsServer = dnsServer[:len(dnsServer)-1]
return string(dnsServer), nil
}

func loadAllowedDNSServers(allowedDNSServers map[string]bool) error {
dnsServer, err := getDNSServer()
if err != nil {
return err
}
fmt.Printf("DNS Server: %s\n", dnsServer)
allowedDNSServers[dnsServer] = true

// trust systemd-resolved by default
allowedDNSServers["127.0.0.53"] = true
return nil
}

func getDestinationIP(packet *netfilter.NFPacket) (string, error) {
ipLayer := packet.Packet.Layer(layers.LayerTypeIPv4)
if ipLayer == nil {
ipLayer = packet.Packet.Layer(layers.LayerTypeIPv6)
}
if ipLayer == nil {
return "", fmt.Errorf("Failed to get IP layer")
}
ip, _ := ipLayer.(*layers.IPv4)
if ip == nil {
ip6, _ := ipLayer.(*layers.IPv6)
if ip6 == nil {
return "", fmt.Errorf("Failed to get IP layer")
}
return ip6.DstIP.String(), nil
}
return ip.DstIP.String(), nil
}

func main() {
// set the mode (audit or block) based on the program argument
blockDNS := false
Expand All @@ -235,6 +290,7 @@ func main() {

allowedDomains := make(map[string]bool)
allowedIps := make(map[string]bool)
allowedDNSServers := make(map[string]bool)
allowedCIDR := []*net.IPNet{}

err := loadAllowedIp("allowed_ips.txt", allowedIps, &allowedCIDR)
Expand All @@ -247,6 +303,11 @@ func main() {
log.Fatalf("Loading domain allowlist: %v", err)
}

err = loadAllowedDNSServers(allowedDNSServers)
if err != nil {
log.Fatalf("Loading DNS servers allowlist: %v", err)
}

err = addToNftables(allowedIps, allowedCIDR)
if err != nil {
log.Fatalf("Error adding to nftables: %v", err)
Expand Down Expand Up @@ -275,23 +336,42 @@ func main() {
for _, q := range dns.Questions {
fmt.Printf("DNS Question: %s %s\n", q.Name, q.Type)
}
// if we are blocking DNS queries, intercept the DNS queries and decide whether to block or allow them
if blockDNS && !dns.QR {
for _, q := range dns.Questions {
if q.Type == layers.DNSTypeA || q.Type == layers.DNSTypeCNAME {
domain := string(q.Name)
fmt.Printf("DNS Question: %s %s\n", q.Name, q.Type)
fmt.Print(domain)
if isDomainAllowed(domain, allowedDomains) {
fmt.Println("-> Allowed DNS Query")
p.SetVerdict(netfilter.Verdict(netfilter.NF_ACCEPT))
} else {
fmt.Println("-> Blocked DNS Query")

// making sure the DNS query is using a trusted DNS server
destinationIP, err := getDestinationIP(&p)
if err != nil {
fmt.Println("Failed to get destination IP")
addIpToLogs("blocked", domain, "unknown")
p.SetVerdict(netfilter.Verdict(netfilter.NF_DROP))
continue
}
if !allowedDNSServers[destinationIP] {
fmt.Printf("-> Blocked DNS Query. Untrusted DNS server %s\n", destinationIP)
addIpToLogs("blocked", domain, "unknown")
p.SetVerdict(netfilter.Verdict(netfilter.NF_DROP))
continue
}

if isDomainAllowed(domain, allowedDomains) {
fmt.Println("-> Allowed DNS Query")
p.SetVerdict(netfilter.Verdict(netfilter.NF_ACCEPT))
continue
}
fmt.Println("-> Blocked DNS Query")
addIpToLogs("blocked", domain, "unknown")
p.SetVerdict(netfilter.Verdict(netfilter.NF_DROP))
continue
}
}
}
// interface DNS responses so we can allow IPs from allowed domains
for _, a := range dns.Answers {
if a.Type == layers.DNSTypeA {
fmt.Printf("DNS Answer: %s %s %s\n", a.Name, a.Type, a.IP)
Expand Down
8 changes: 4 additions & 4 deletions agent/queue_block_with_dns.nft
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ insert rule ip filter DOCKER-USER iifname "docker0" ip daddr @allowed_ips counte

# Match DNS request (dest port 53) and enqueue to userspace
# Block DNS queries for unallowed domains, preventing DNS exfiltration
insert rule ip filter DOCKER-USER iifname "docker0" ip daddr 127.0.0.53 udp dport 53 counter queue num 0
insert rule ip filter DOCKER-USER iifname "docker0" ip daddr 127.0.0.53 tcp dport 53 counter queue num 0
insert rule ip filter DOCKER-USER iifname "docker0" udp dport 53 counter queue num 0
insert rule ip filter DOCKER-USER iifname "docker0" tcp dport 53 counter queue num 0

# Match DNS responses (source port 53) and enqueue to userspace
insert rule ip filter DOCKER-USER oif "docker0" udp sport 53 counter queue num 0
Expand Down Expand Up @@ -41,8 +41,8 @@ table inet filter {

# Match DNS request (dest port 53) and enqueue to userspace
# Block DNS queries for unallowed domains, preventing DNS exfiltration
ip daddr 127.0.0.53 udp dport 53 queue num 0
ip daddr 127.0.0.53 tcp dport 53 queue num 0
udp dport 53 queue num 0
tcp dport 53 queue num 0

# TODO: get rid of this
# Allow established and related traffic
Expand Down
5 changes: 4 additions & 1 deletion test/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ process.env["INPUT_LOG-DIRECTORY"] = "/tmp/gha-agent/logs";
process.env["INPUT_ALLOWED-DOMAINS"] = `
*.google.com
bing.com
*.github.com
`;

// uncomment to test with local agent
// uncomment to test with local agent or download release
// process.env["INPUT_LOCAL-AGENT-PATH"] = "agent/agent";
process.env["INPUT_AGENT-DOWNLOAD-BASE-URL"] =
"https://github.com/bullfrogsec/bullfrog/releases/download/";

0 comments on commit e1a20a3

Please sign in to comment.