Skip to content

Commit 35d0a90

Browse files
committed
fix: add machine with UFW firewall (allow ipv6 management traffic) fixes #65
1 parent a7273c8 commit 35d0a90

File tree

6 files changed

+125
-67
lines changed

6 files changed

+125
-67
lines changed

internal/machine/cluster.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/docker/docker/client"
1616
"github.com/psviderski/uncloud/internal/machine/api/pb"
1717
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
18+
"github.com/psviderski/uncloud/internal/machine/constants"
1819
"github.com/psviderski/uncloud/internal/machine/corroservice"
1920
"github.com/psviderski/uncloud/internal/machine/dns"
2021
"github.com/psviderski/uncloud/internal/machine/docker"
@@ -25,10 +26,6 @@ import (
2526
"google.golang.org/grpc"
2627
)
2728

28-
const (
29-
APIPort = 51000
30-
)
31-
3229
// clusterController is the main controller for the machine that is a cluster member. It manages components such as
3330
// the WireGuard network, API server listening the WireGuard network, Corrosion service, Docker network and containers,
3431
// and others.
@@ -120,7 +117,7 @@ func (cc *clusterController) Run(ctx context.Context) error {
120117
errGroup, ctx := errgroup.WithContext(ctx)
121118

122119
// Start the network API server. Assume the management IP can't be changed when the network is running.
123-
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(APIPort))
120+
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
124121
listener, err := net.Listen("tcp", apiAddr)
125122
if err != nil {
126123
return fmt.Errorf("listen API port: %w", err)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package constants
2+
3+
const (
4+
// MachineAPIPort is the port for the Machine API service on the management WireGuard network.
5+
MachineAPIPort = 51000
6+
)

internal/machine/firewall/iptables_linux.go

Lines changed: 112 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"strings"
88

99
"github.com/docker/docker/libnetwork/iptables"
10+
"github.com/psviderski/uncloud/internal/machine/constants"
11+
"github.com/psviderski/uncloud/internal/machine/corroservice"
1012
"github.com/psviderski/uncloud/internal/machine/network"
1113
)
1214

@@ -17,86 +19,138 @@ const (
1719

1820
// ConfigureIptablesChains sets up custom iptables chains and initial firewall rules for Uncloud networking.
1921
func ConfigureIptablesChains() error {
20-
// Ensure iptables UNCLOUD-INPUT chain exists. All existing rules are flushed.
21-
ipt := iptables.GetIptable(iptables.IPv4)
22-
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
23-
return fmt.Errorf("create iptables chain '%s': %w", UncloudInputChain, err)
22+
if err := createIptablesChains(); err != nil {
23+
return err
2424
}
25-
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
26-
return fmt.Errorf("flush iptables chain '%s': %w", UncloudInputChain, err)
25+
26+
ipt4 := iptables.GetIptable(iptables.IPv4)
27+
ipt6 := iptables.GetIptable(iptables.IPv6)
28+
29+
// Allow WireGuard traffic to the machine.
30+
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
31+
err := ipt4.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
32+
if err != nil {
33+
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
2734
}
2835

29-
// Ensure the main iptables INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
30-
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
31-
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
32-
// Look for the first DROP/REJECT rule in the INPUT chain.
33-
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
34-
if err != nil {
35-
return fmt.Errorf("get iptables rules for chain '%s': %w", UncloudInputChain, err)
36+
// Allow cluster machines to access Machine API via the management IPv6 WireGuard network.
37+
acceptMachineAPIRule := []string{
38+
"-i", network.WireGuardInterfaceName,
39+
"-s", "fdcc::/16",
40+
"-p", "tcp",
41+
"--dport", strconv.Itoa(constants.MachineAPIPort),
42+
"-j", "ACCEPT",
43+
}
44+
// Allow Corrosion gossip traffic from cluster machines via the management IPv6 WireGuard network.
45+
acceptCorrosionGossipRule := []string{
46+
"-i", network.WireGuardInterfaceName,
47+
"-s", "fdcc::/16",
48+
"-p", "udp",
49+
"--dport", strconv.Itoa(corroservice.DefaultGossipPort),
50+
"-j", "ACCEPT",
51+
}
52+
for _, rule := range [][]string{acceptMachineAPIRule, acceptCorrosionGossipRule} {
53+
if err = ipt6.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, rule); err != nil {
54+
return fmt.Errorf("insert ip6tables rule '%s': %w", strings.Join(rule, " "), err)
3655
}
56+
}
3757

38-
firstRejectRuleNum := 0
39-
for _, line := range strings.Split(string(out), "\n") {
40-
fields := strings.Fields(line)
41-
if len(fields) < 2 {
42-
continue
43-
}
44-
if fields[1] == "DROP" || fields[1] == "REJECT" {
45-
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
46-
firstRejectRuleNum = ruleNum
47-
break
48-
}
49-
}
58+
return nil
59+
}
60+
61+
// createIptablesChains ensures UNCLOUD-INPUT iptables and ip6tables chains exist and
62+
// there are jump rules from the main INPUT chains.
63+
func createIptablesChains() error {
64+
ipt4 := iptables.GetIptable(iptables.IPv4)
65+
ipt6 := iptables.GetIptable(iptables.IPv6)
66+
67+
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
68+
iptBin := "iptables"
69+
if i == 1 {
70+
iptBin = "ip6tables"
5071
}
5172

52-
var addJumpRule []string
53-
if firstRejectRuleNum > 0 {
54-
addJumpRule = append([]string{"-t", string(iptables.Filter), "-I", "INPUT", strconv.Itoa(firstRejectRuleNum)},
55-
jumpRule...)
56-
} else {
57-
addJumpRule = append([]string{"-t", string(iptables.Filter), "-A", "INPUT"}, jumpRule...)
73+
// Ensure UNCLOUD-INPUT chain exists. All existing rules are flushed.
74+
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
75+
return fmt.Errorf("create %s chain '%s': %w", iptBin, UncloudInputChain, err)
5876
}
59-
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
60-
return fmt.Errorf("add iptables rule '%s': %w", strings.Join(addJumpRule, " "), err)
77+
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
78+
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
6179
}
62-
}
6380

64-
// Allow WireGuard traffic to the machine.
65-
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
66-
err := ipt.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
67-
if err != nil {
68-
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
81+
// Ensure the main INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
82+
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
83+
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
84+
// Look for the first DROP/REJECT rule in the INPUT chain.
85+
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
86+
if err != nil {
87+
return fmt.Errorf("get %s rules for chain '%s': %w", iptBin, UncloudInputChain, err)
88+
}
89+
90+
firstRejectRuleNum := 0
91+
for _, line := range strings.Split(string(out), "\n") {
92+
fields := strings.Fields(line)
93+
if len(fields) < 2 {
94+
continue
95+
}
96+
if fields[1] == "DROP" || fields[1] == "REJECT" {
97+
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
98+
firstRejectRuleNum = ruleNum
99+
break
100+
}
101+
}
102+
}
103+
104+
var addJumpRule []string
105+
if firstRejectRuleNum > 0 {
106+
addJumpRule = append([]string{"-t", string(iptables.Filter), "-I", "INPUT",
107+
strconv.Itoa(firstRejectRuleNum)}, jumpRule...)
108+
} else {
109+
addJumpRule = append([]string{"-t", string(iptables.Filter), "-A", "INPUT"}, jumpRule...)
110+
}
111+
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
112+
return fmt.Errorf("add %s rule '%s': %w", iptBin, strings.Join(addJumpRule, " "), err)
113+
}
114+
}
69115
}
70116

71117
return nil
72118
}
73119

74120
// CleanupIptablesChains removes the custom iptables chains and rules created by ConfigureIptablesChains.
75121
func CleanupIptablesChains() error {
76-
ipt := iptables.GetIptable(iptables.IPv4)
122+
ipt4 := iptables.GetIptable(iptables.IPv4)
123+
ipt6 := iptables.GetIptable(iptables.IPv6)
77124

78-
// First, remove the jump rule from INPUT chain to UNCLOUD-INPUT.
79-
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
80-
if err := ipt.ProgramRule(iptables.Filter, "INPUT", iptables.Delete, jumpRule); err != nil {
81-
return fmt.Errorf("delete iptables jump rule from INPUT: %w", err)
82-
}
125+
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
126+
iptBin := "iptables"
127+
if i == 1 {
128+
iptBin = "ip6tables"
129+
}
83130

84-
// Flush all rules from UNCLOUD-INPUT chain as it must be empty before deletion.
85-
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
86-
// Chain might not exist which is fine.
87-
if !strings.Contains(err.Error(), "No chain") {
88-
return fmt.Errorf("flush iptables chain '%s': %w", UncloudInputChain, err)
131+
// First, remove the jump rule from INPUT chain to UNCLOUD-INPUT.
132+
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
133+
if err := ipt.ProgramRule(iptables.Filter, "INPUT", iptables.Delete, jumpRule); err != nil {
134+
return fmt.Errorf("delete %s jump rule from INPUT: %w", iptBin, err)
89135
}
90-
}
91136

92-
// Delete the UNCLOUD-INPUT chain.
93-
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-X", UncloudInputChain); err != nil {
94-
// Chain might not exist which is fine.
95-
if !strings.Contains(err.Error(), "No chain") {
96-
return fmt.Errorf("delete iptables chain '%s': %w", UncloudInputChain, err)
137+
// Flush all rules from UNCLOUD-INPUT chain as it must be empty before deletion.
138+
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
139+
// Chain might not exist which is fine.
140+
if !strings.Contains(err.Error(), "No chain") {
141+
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
142+
}
143+
}
144+
145+
// Delete the UNCLOUD-INPUT chain.
146+
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-X", UncloudInputChain); err != nil {
147+
// Chain might not exist which is fine.
148+
if !strings.Contains(err.Error(), "No chain") {
149+
return fmt.Errorf("delete %s chain '%s': %w", iptBin, UncloudInputChain, err)
150+
}
151+
} else {
152+
slog.Info("Deleted %s chain.", "chain", iptBin, UncloudInputChain)
97153
}
98-
} else {
99-
slog.Info("Deleted iptables chain.", "chain", UncloudInputChain)
100154
}
101155

102156
return nil

internal/machine/machine.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
2525
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
2626
"github.com/psviderski/uncloud/internal/machine/cluster"
27+
"github.com/psviderski/uncloud/internal/machine/constants"
2728
"github.com/psviderski/uncloud/internal/machine/corroservice"
2829
"github.com/psviderski/uncloud/internal/machine/dns"
2930
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
@@ -233,7 +234,7 @@ func NewMachine(config *Config) (*Machine, error) {
233234
}
234235

235236
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
236-
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
237+
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, constants.MachineAPIPort)
237238
localProxyServer := grpc.NewServer(
238239
grpc.ForceServerCodecV2(proxy.Codec()),
239240
grpc.UnknownServiceHandler(

internal/machine/network/ip.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ func MachineIP(subnet netip.Prefix) netip.Addr {
1414
}
1515

1616
// ManagementIP returns the IPv6 address of a peer derived from the first 14 bytes of its public key.
17-
// This address is intended for cluster management traffic.
17+
// This address always starts with fdcc: and is intended for cluster management traffic.
1818
func ManagementIP(publicKey secret.Secret) netip.Addr {
1919
bytes := [16]byte{0xfd, 0xcc}
2020
copy(bytes[2:], publicKey[:14])

pkg/client/connector/wireguard.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"strconv"
99

1010
"github.com/psviderski/uncloud/internal/cli/config"
11-
machine2 "github.com/psviderski/uncloud/internal/machine"
11+
"github.com/psviderski/uncloud/internal/machine/constants"
1212
"github.com/psviderski/uncloud/internal/machine/network"
1313
"github.com/psviderski/uncloud/internal/machine/network/tunnel"
1414
"github.com/psviderski/uncloud/pkg/client"
@@ -49,7 +49,7 @@ func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, err
4949
}
5050
endpoint := netip.AddrPortFrom(endpointAddr, tunnel.DefaultEndpointPort)
5151
machineManagementIP := network.ManagementIP(machine.PublicKey)
52-
machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(machine2.APIPort))
52+
machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
5353

5454
tunCfg := &tunnel.Config{
5555
LocalAddress: c.user.ManagementIP(),

0 commit comments

Comments
 (0)