Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 23 additions & 24 deletions firewall.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,15 @@ func addBlockRules(firewall *Firewall, endpoints []ipAddressEndpoint, chain, net
}

for _, endpoint := range endpoints {
err = ipt.Append(filterTable, chain, direction, netInterface, protocol, tcp,
destination, endpoint.ipAddress,
destinationPort, endpoint.port, target, accept)
// Allow both TCP and UDP so protocols like QUIC (HTTP/3) work for allowed endpoints.
for _, ipProtocol := range []string{tcp, udp} {
err = ipt.Append(filterTable, chain, direction, netInterface, protocol, ipProtocol,
destination, endpoint.ipAddress,
destinationPort, endpoint.port, target, accept)

if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to append endpoint rule ip:%s, port:%s", endpoint.ipAddress, endpoint.port))
if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to append endpoint rule ip:%s, port:%s, protocol:%s", endpoint.ipAddress, endpoint.port, ipProtocol))
}
}
}

Expand Down Expand Up @@ -199,39 +202,35 @@ func InsertAllowRule(firewall *Firewall, blocklist *GlobalBlocklist, ipAddress,
ipt = firewall.IPTables
}

exists, err := ipt.Exists(filterTable, outputChain, outbound, defaultInterface, protocol, tcp,
destination, ipAddress,
destinationPort, port, target, accept)

if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to check if endpoint exists ip:%s, port:%s, interface:%s", ipAddress, port, defaultInterface))
}

if !exists {
err = ipt.Insert(filterTable, outputChain, 1, outbound, defaultInterface, protocol, tcp,
destination, ipAddress,
destinationPort, port, target, accept)

if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to insert endpoint rule ip:%s, port:%s, interface:%s", ipAddress, port, defaultInterface))
// Allow both TCP and UDP so protocols like QUIC (HTTP/3) work for allowed endpoints.
for _, ipProtocol := range []string{tcp, udp} {
if err := insertAllowRuleForProtocol(ipt, outputChain, outbound, defaultInterface, ipProtocol, ipAddress, port); err != nil {
return err
}
if err := insertAllowRuleForProtocol(ipt, dockerUserChain, inbound, dockerInterface, ipProtocol, ipAddress, port); err != nil {
return err
}
}

exists, err = ipt.Exists(filterTable, dockerUserChain, inbound, dockerInterface, protocol, tcp,
return nil
}

func insertAllowRuleForProtocol(ipt IPTables, chain, direction, netInterface, ipProtocol, ipAddress, port string) error {
exists, err := ipt.Exists(filterTable, chain, direction, netInterface, protocol, ipProtocol,
destination, ipAddress,
destinationPort, port, target, accept)

if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to check if endpoint exists ip:%s, port:%s, interface:%s", ipAddress, port, dockerInterface))
return errors.Wrap(err, fmt.Sprintf("failed to check if endpoint exists ip:%s, port:%s, protocol:%s, interface:%s", ipAddress, port, ipProtocol, netInterface))
}

if !exists {
err = ipt.Insert(filterTable, dockerUserChain, 1, inbound, dockerInterface, protocol, tcp,
err = ipt.Insert(filterTable, chain, 1, direction, netInterface, protocol, ipProtocol,
destination, ipAddress,
destinationPort, port, target, accept)

if err != nil {
return errors.Wrap(err, fmt.Sprintf("failed to insert endpoint rule ip:%s, port:%s, interface:%s", ipAddress, port, defaultInterface))
return errors.Wrap(err, fmt.Sprintf("failed to insert endpoint rule ip:%s, port:%s, protocol:%s, interface:%s", ipAddress, port, ipProtocol, netInterface))
}
}

Expand Down
72 changes: 70 additions & 2 deletions firewall_blocklist_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package main
import "testing"

type recorderIPTables struct {
appended [][]string
inserted [][]string
}

func (m *recorderIPTables) Append(table, chain string, rulespec ...string) error {
record := append([]string{table, chain}, rulespec...)
m.appended = append(m.appended, record)
return nil
}

Expand Down Expand Up @@ -34,6 +37,16 @@ func insertedRuleTarget(record []string) string {
return ""
}

func ruleProtocol(record []string) string {
for i := 0; i < len(record)-1; i++ {
if record[i] == protocol {
return record[i+1]
}
}

return ""
}

func TestAddGlobalBlockRules(t *testing.T) {
ipt := &recorderIPTables{}
blocklist := NewGlobalBlocklist(&GlobalBlocklistResponse{
Expand Down Expand Up @@ -95,13 +108,68 @@ func TestInsertAllowRule_AllowsWhenBlocklistIsNil(t *testing.T) {
t.Fatalf("InsertAllowRule() error = %v", err)
}

if len(ipt.inserted) != 2 {
t.Fatalf("expected two inserted allow rules, got %d", len(ipt.inserted))
// TCP + UDP for OUTPUT and DOCKER-USER
if len(ipt.inserted) != 4 {
t.Fatalf("expected four inserted allow rules (tcp+udp x 2 chains), got %d", len(ipt.inserted))
}

expectedProtocols := []string{tcp, tcp, udp, udp}
expectedChains := []string{outputChain, dockerUserChain, outputChain, dockerUserChain}
for i, record := range ipt.inserted {
if insertedRuleTarget(record) != accept {
t.Fatalf("expected inserted rule %d target %s, got %#v", i, accept, record)
}
if ruleProtocol(record) != expectedProtocols[i] {
t.Fatalf("expected inserted rule %d protocol %s, got %#v", i, expectedProtocols[i], record)
}
if record[1] != expectedChains[i] {
t.Fatalf("expected inserted rule %d chain %s, got %#v", i, expectedChains[i], record)
}
}
}

func TestAddBlockRules_AllowsTCPAndUDPForEndpoints(t *testing.T) {
ipt := &recorderIPTables{}
endpoints := []ipAddressEndpoint{{ipAddress: "1.1.1.1", port: "443"}}

err := addBlockRules(&Firewall{ipt}, endpoints, outputChain, defaultInterface, outbound)
if err != nil {
t.Fatalf("addBlockRules() error = %v", err)
}

var tcpAllow, udpAllow bool
for _, record := range ipt.appended {
if insertedRuleTarget(record) != accept {
continue
}
if record[1] != outputChain {
continue
}
dest := ""
dport := ""
for i := 0; i < len(record)-1; i++ {
if record[i] == destination {
dest = record[i+1]
}
if record[i] == destinationPort {
dport = record[i+1]
}
}
if dest != "1.1.1.1" || dport != "443" {
continue
}
switch ruleProtocol(record) {
case tcp:
tcpAllow = true
case udp:
udpAllow = true
}
}

if !tcpAllow {
t.Fatal("expected TCP ACCEPT rule for allowed endpoint")
}
if !udpAllow {
t.Fatal("expected UDP ACCEPT rule for allowed endpoint")
}
}
43 changes: 43 additions & 0 deletions firewall_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,49 @@ func Test_addAuditRules(t *testing.T) {
t.Errorf("Error not expected creating iptables %v", err)
}

assertAcceptRule(t, ipt, outputChain, outbound, defaultInterface, tcp, "1.1.1.1", "443")
assertAcceptRule(t, ipt, outputChain, outbound, defaultInterface, udp, "1.1.1.1", "443")
assertAcceptRule(t, ipt, dockerUserChain, inbound, dockerInterface, tcp, "1.1.1.1", "443")
assertAcceptRule(t, ipt, dockerUserChain, inbound, dockerInterface, udp, "1.1.1.1", "443")

ipt.ClearChain("filter", "OUTPUT")
ipt.ClearChain("filter", "DOCKER-USER")
}

func Test_InsertAllowRule_AddsTCPAndUDP(t *testing.T) {
ipt, err := iptables.New()
if err != nil {
t.Fatalf("iptables.New: %v", err)
}

_ = ipt.NewChain(filterTable, dockerUserChain)
_ = ipt.ClearChain(filterTable, outputChain)
_ = ipt.ClearChain(filterTable, dockerUserChain)

t.Cleanup(func() {
_ = ipt.ClearChain(filterTable, outputChain)
_ = ipt.ClearChain(filterTable, dockerUserChain)
})

err = InsertAllowRule(&Firewall{IPTables: ipt}, nil, "1.1.1.1", "443")
if err != nil {
t.Fatalf("InsertAllowRule: %v", err)
}

assertAcceptRule(t, ipt, outputChain, outbound, defaultInterface, tcp, "1.1.1.1", "443")
assertAcceptRule(t, ipt, outputChain, outbound, defaultInterface, udp, "1.1.1.1", "443")
assertAcceptRule(t, ipt, dockerUserChain, inbound, dockerInterface, tcp, "1.1.1.1", "443")
assertAcceptRule(t, ipt, dockerUserChain, inbound, dockerInterface, udp, "1.1.1.1", "443")
}

func assertAcceptRule(t *testing.T, ipt *iptables.IPTables, chain, direction, iface, proto, ip, port string) {
t.Helper()
exists, err := ipt.Exists(filterTable, chain, direction, iface, protocol, proto,
destination, ip, destinationPort, port, target, accept)
if err != nil {
t.Fatalf("Exists(%s %s %s:%s): %v", chain, proto, ip, port, err)
}
if !exists {
t.Fatalf("missing ACCEPT rule: chain=%s proto=%s dest=%s:%s iface=%s", chain, proto, ip, port, iface)
}
}