mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
fix: add machine with UFW firewall (allow ipv6 management traffic) fixes #65
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
"github.com/psviderski/uncloud/internal/machine/api/pb"
|
||||||
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
|
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/constants"
|
||||||
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
"github.com/psviderski/uncloud/internal/machine/dns"
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
"github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
@@ -25,10 +26,6 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
APIPort = 51000
|
|
||||||
)
|
|
||||||
|
|
||||||
// clusterController is the main controller for the machine that is a cluster member. It manages components such as
|
// clusterController is the main controller for the machine that is a cluster member. It manages components such as
|
||||||
// the WireGuard network, API server listening the WireGuard network, Corrosion service, Docker network and containers,
|
// the WireGuard network, API server listening the WireGuard network, Corrosion service, Docker network and containers,
|
||||||
// and others.
|
// and others.
|
||||||
@@ -120,7 +117,7 @@ func (cc *clusterController) Run(ctx context.Context) error {
|
|||||||
errGroup, ctx := errgroup.WithContext(ctx)
|
errGroup, ctx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
// Start the network API server. Assume the management IP can't be changed when the network is running.
|
||||||
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(APIPort))
|
apiAddr := net.JoinHostPort(cc.state.Network.ManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
||||||
listener, err := net.Listen("tcp", apiAddr)
|
listener, err := net.Listen("tcp", apiAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("listen API port: %w", err)
|
return fmt.Errorf("listen API port: %w", err)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package constants
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MachineAPIPort is the port for the Machine API service on the management WireGuard network.
|
||||||
|
MachineAPIPort = 51000
|
||||||
|
)
|
||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/docker/libnetwork/iptables"
|
"github.com/docker/docker/libnetwork/iptables"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/constants"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,86 +19,138 @@ const (
|
|||||||
|
|
||||||
// ConfigureIptablesChains sets up custom iptables chains and initial firewall rules for Uncloud networking.
|
// ConfigureIptablesChains sets up custom iptables chains and initial firewall rules for Uncloud networking.
|
||||||
func ConfigureIptablesChains() error {
|
func ConfigureIptablesChains() error {
|
||||||
// Ensure iptables UNCLOUD-INPUT chain exists. All existing rules are flushed.
|
if err := createIptablesChains(); err != nil {
|
||||||
ipt := iptables.GetIptable(iptables.IPv4)
|
return err
|
||||||
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
|
|
||||||
return fmt.Errorf("create iptables chain '%s': %w", UncloudInputChain, err)
|
|
||||||
}
|
|
||||||
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
|
|
||||||
return fmt.Errorf("flush iptables chain '%s': %w", UncloudInputChain, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the main iptables INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
|
ipt4 := iptables.GetIptable(iptables.IPv4)
|
||||||
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
|
ipt6 := iptables.GetIptable(iptables.IPv6)
|
||||||
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
|
|
||||||
// Look for the first DROP/REJECT rule in the INPUT chain.
|
|
||||||
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get iptables rules for chain '%s': %w", UncloudInputChain, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
firstRejectRuleNum := 0
|
|
||||||
for _, line := range strings.Split(string(out), "\n") {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fields[1] == "DROP" || fields[1] == "REJECT" {
|
|
||||||
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
|
|
||||||
firstRejectRuleNum = ruleNum
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var addJumpRule []string
|
|
||||||
if firstRejectRuleNum > 0 {
|
|
||||||
addJumpRule = append([]string{"-t", string(iptables.Filter), "-I", "INPUT", strconv.Itoa(firstRejectRuleNum)},
|
|
||||||
jumpRule...)
|
|
||||||
} else {
|
|
||||||
addJumpRule = append([]string{"-t", string(iptables.Filter), "-A", "INPUT"}, jumpRule...)
|
|
||||||
}
|
|
||||||
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
|
|
||||||
return fmt.Errorf("add iptables rule '%s': %w", strings.Join(addJumpRule, " "), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow WireGuard traffic to the machine.
|
// Allow WireGuard traffic to the machine.
|
||||||
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
|
acceptWireGuardRule := []string{"-p", "udp", "--dport", strconv.Itoa(network.WireGuardPort), "-j", "ACCEPT"}
|
||||||
err := ipt.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
|
err := ipt4.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, acceptWireGuardRule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
|
return fmt.Errorf("insert iptables rule '%s': %w", strings.Join(acceptWireGuardRule, " "), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow cluster machines to access Machine API via the management IPv6 WireGuard network.
|
||||||
|
acceptMachineAPIRule := []string{
|
||||||
|
"-i", network.WireGuardInterfaceName,
|
||||||
|
"-s", "fdcc::/16",
|
||||||
|
"-p", "tcp",
|
||||||
|
"--dport", strconv.Itoa(constants.MachineAPIPort),
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
}
|
||||||
|
// Allow Corrosion gossip traffic from cluster machines via the management IPv6 WireGuard network.
|
||||||
|
acceptCorrosionGossipRule := []string{
|
||||||
|
"-i", network.WireGuardInterfaceName,
|
||||||
|
"-s", "fdcc::/16",
|
||||||
|
"-p", "udp",
|
||||||
|
"--dport", strconv.Itoa(corroservice.DefaultGossipPort),
|
||||||
|
"-j", "ACCEPT",
|
||||||
|
}
|
||||||
|
for _, rule := range [][]string{acceptMachineAPIRule, acceptCorrosionGossipRule} {
|
||||||
|
if err = ipt6.ProgramRule(iptables.Filter, UncloudInputChain, iptables.Insert, rule); err != nil {
|
||||||
|
return fmt.Errorf("insert ip6tables rule '%s': %w", strings.Join(rule, " "), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createIptablesChains ensures UNCLOUD-INPUT iptables and ip6tables chains exist and
|
||||||
|
// there are jump rules from the main INPUT chains.
|
||||||
|
func createIptablesChains() error {
|
||||||
|
ipt4 := iptables.GetIptable(iptables.IPv4)
|
||||||
|
ipt6 := iptables.GetIptable(iptables.IPv6)
|
||||||
|
|
||||||
|
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
|
||||||
|
iptBin := "iptables"
|
||||||
|
if i == 1 {
|
||||||
|
iptBin = "ip6tables"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure UNCLOUD-INPUT chain exists. All existing rules are flushed.
|
||||||
|
if _, err := ipt.NewChain(UncloudInputChain, iptables.Filter); err != nil {
|
||||||
|
return fmt.Errorf("create %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
|
||||||
|
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the main INPUT chain has a jump rule to the UNCLOUD-INPUT chain before any DROP/REJECT rules.
|
||||||
|
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
|
||||||
|
if !ipt.Exists(iptables.Filter, "INPUT", jumpRule...) {
|
||||||
|
// Look for the first DROP/REJECT rule in the INPUT chain.
|
||||||
|
out, err := ipt.Raw("-t", string(iptables.Filter), "-L", "INPUT", "--line-numbers")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get %s rules for chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
firstRejectRuleNum := 0
|
||||||
|
for _, line := range strings.Split(string(out), "\n") {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fields[1] == "DROP" || fields[1] == "REJECT" {
|
||||||
|
if ruleNum, err := strconv.Atoi(fields[0]); err == nil {
|
||||||
|
firstRejectRuleNum = ruleNum
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addJumpRule []string
|
||||||
|
if firstRejectRuleNum > 0 {
|
||||||
|
addJumpRule = append([]string{"-t", string(iptables.Filter), "-I", "INPUT",
|
||||||
|
strconv.Itoa(firstRejectRuleNum)}, jumpRule...)
|
||||||
|
} else {
|
||||||
|
addJumpRule = append([]string{"-t", string(iptables.Filter), "-A", "INPUT"}, jumpRule...)
|
||||||
|
}
|
||||||
|
if err = ipt.RawCombinedOutput(addJumpRule...); err != nil {
|
||||||
|
return fmt.Errorf("add %s rule '%s': %w", iptBin, strings.Join(addJumpRule, " "), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CleanupIptablesChains removes the custom iptables chains and rules created by ConfigureIptablesChains.
|
// CleanupIptablesChains removes the custom iptables chains and rules created by ConfigureIptablesChains.
|
||||||
func CleanupIptablesChains() error {
|
func CleanupIptablesChains() error {
|
||||||
ipt := iptables.GetIptable(iptables.IPv4)
|
ipt4 := iptables.GetIptable(iptables.IPv4)
|
||||||
|
ipt6 := iptables.GetIptable(iptables.IPv6)
|
||||||
|
|
||||||
// First, remove the jump rule from INPUT chain to UNCLOUD-INPUT.
|
for i, ipt := range []*iptables.IPTable{ipt4, ipt6} {
|
||||||
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
|
iptBin := "iptables"
|
||||||
if err := ipt.ProgramRule(iptables.Filter, "INPUT", iptables.Delete, jumpRule); err != nil {
|
if i == 1 {
|
||||||
return fmt.Errorf("delete iptables jump rule from INPUT: %w", err)
|
iptBin = "ip6tables"
|
||||||
}
|
|
||||||
|
|
||||||
// Flush all rules from UNCLOUD-INPUT chain as it must be empty before deletion.
|
|
||||||
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
|
|
||||||
// Chain might not exist which is fine.
|
|
||||||
if !strings.Contains(err.Error(), "No chain") {
|
|
||||||
return fmt.Errorf("flush iptables chain '%s': %w", UncloudInputChain, err)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the UNCLOUD-INPUT chain.
|
// First, remove the jump rule from INPUT chain to UNCLOUD-INPUT.
|
||||||
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-X", UncloudInputChain); err != nil {
|
jumpRule := []string{"-m", "comment", "--comment", "Uncloud-managed", "-j", UncloudInputChain}
|
||||||
// Chain might not exist which is fine.
|
if err := ipt.ProgramRule(iptables.Filter, "INPUT", iptables.Delete, jumpRule); err != nil {
|
||||||
if !strings.Contains(err.Error(), "No chain") {
|
return fmt.Errorf("delete %s jump rule from INPUT: %w", iptBin, err)
|
||||||
return fmt.Errorf("delete iptables chain '%s': %w", UncloudInputChain, err)
|
}
|
||||||
|
|
||||||
|
// Flush all rules from UNCLOUD-INPUT chain as it must be empty before deletion.
|
||||||
|
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-F", UncloudInputChain); err != nil {
|
||||||
|
// Chain might not exist which is fine.
|
||||||
|
if !strings.Contains(err.Error(), "No chain") {
|
||||||
|
return fmt.Errorf("flush %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the UNCLOUD-INPUT chain.
|
||||||
|
if err := ipt.RawCombinedOutput("-t", string(iptables.Filter), "-X", UncloudInputChain); err != nil {
|
||||||
|
// Chain might not exist which is fine.
|
||||||
|
if !strings.Contains(err.Error(), "No chain") {
|
||||||
|
return fmt.Errorf("delete %s chain '%s': %w", iptBin, UncloudInputChain, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog.Info("Deleted %s chain.", "chain", iptBin, UncloudInputChain)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
slog.Info("Deleted iptables chain.", "chain", UncloudInputChain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
|
apiproxy "github.com/psviderski/uncloud/internal/machine/api/proxy"
|
||||||
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
|
"github.com/psviderski/uncloud/internal/machine/caddyconfig"
|
||||||
"github.com/psviderski/uncloud/internal/machine/cluster"
|
"github.com/psviderski/uncloud/internal/machine/cluster"
|
||||||
|
"github.com/psviderski/uncloud/internal/machine/constants"
|
||||||
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
"github.com/psviderski/uncloud/internal/machine/corroservice"
|
||||||
"github.com/psviderski/uncloud/internal/machine/dns"
|
"github.com/psviderski/uncloud/internal/machine/dns"
|
||||||
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
|
machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
|
||||||
@@ -233,7 +234,7 @@ func NewMachine(config *Config) (*Machine, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
// Init a local gRPC proxy server that proxies requests to the local or remote machine API servers.
|
||||||
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, APIPort)
|
proxyDirector := apiproxy.NewDirector(config.MachineSockPath, constants.MachineAPIPort)
|
||||||
localProxyServer := grpc.NewServer(
|
localProxyServer := grpc.NewServer(
|
||||||
grpc.ForceServerCodecV2(proxy.Codec()),
|
grpc.ForceServerCodecV2(proxy.Codec()),
|
||||||
grpc.UnknownServiceHandler(
|
grpc.UnknownServiceHandler(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func MachineIP(subnet netip.Prefix) netip.Addr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ManagementIP returns the IPv6 address of a peer derived from the first 14 bytes of its public key.
|
// ManagementIP returns the IPv6 address of a peer derived from the first 14 bytes of its public key.
|
||||||
// This address is intended for cluster management traffic.
|
// This address always starts with fdcc: and is intended for cluster management traffic.
|
||||||
func ManagementIP(publicKey secret.Secret) netip.Addr {
|
func ManagementIP(publicKey secret.Secret) netip.Addr {
|
||||||
bytes := [16]byte{0xfd, 0xcc}
|
bytes := [16]byte{0xfd, 0xcc}
|
||||||
copy(bytes[2:], publicKey[:14])
|
copy(bytes[2:], publicKey[:14])
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/psviderski/uncloud/internal/cli/config"
|
"github.com/psviderski/uncloud/internal/cli/config"
|
||||||
machine2 "github.com/psviderski/uncloud/internal/machine"
|
"github.com/psviderski/uncloud/internal/machine/constants"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network"
|
"github.com/psviderski/uncloud/internal/machine/network"
|
||||||
"github.com/psviderski/uncloud/internal/machine/network/tunnel"
|
"github.com/psviderski/uncloud/internal/machine/network/tunnel"
|
||||||
"github.com/psviderski/uncloud/pkg/client"
|
"github.com/psviderski/uncloud/pkg/client"
|
||||||
@@ -49,7 +49,7 @@ func (c *WireGuardConnector) Connect(ctx context.Context) (*grpc.ClientConn, err
|
|||||||
}
|
}
|
||||||
endpoint := netip.AddrPortFrom(endpointAddr, tunnel.DefaultEndpointPort)
|
endpoint := netip.AddrPortFrom(endpointAddr, tunnel.DefaultEndpointPort)
|
||||||
machineManagementIP := network.ManagementIP(machine.PublicKey)
|
machineManagementIP := network.ManagementIP(machine.PublicKey)
|
||||||
machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(machine2.APIPort))
|
machineAPIAddr := net.JoinHostPort(machineManagementIP.String(), strconv.Itoa(constants.MachineAPIPort))
|
||||||
|
|
||||||
tunCfg := &tunnel.Config{
|
tunCfg := &tunnel.Config{
|
||||||
LocalAddress: c.user.ManagementIP(),
|
LocalAddress: c.user.ManagementIP(),
|
||||||
|
|||||||
Reference in New Issue
Block a user