diff --git a/firewall.go b/firewall.go index 12d479a..c970003 100644 --- a/firewall.go +++ b/firewall.go @@ -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)) + } } } @@ -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)) } } diff --git a/firewall_blocklist_test.go b/firewall_blocklist_test.go index b40b2e6..cd16191 100644 --- a/firewall_blocklist_test.go +++ b/firewall_blocklist_test.go @@ -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 } @@ -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{ @@ -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") } } diff --git a/firewall_test.go b/firewall_test.go index fdcc2ce..20c07d6 100644 --- a/firewall_test.go +++ b/firewall_test.go @@ -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) + } +}