Skip to content
Merged
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
6 changes: 3 additions & 3 deletions agent/cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func main() {
}

if err = container.EnsureNetwork(config.SubnetID); err != nil {
log.Printf("Warning: Failed to ensure container network: %v", err)
log.Printf("Warning: Failed to ensure container network/forwarding: %v", err)
}

if !disableDNS {
Expand Down Expand Up @@ -258,7 +258,7 @@ func main() {

log.Println("Ensuring container network exists...")
if err = container.EnsureNetwork(config.SubnetID); err != nil {
log.Printf("Warning: Failed to create container network: %v", err)
log.Printf("Warning: Failed to ensure container network/forwarding: %v", err)
} else {
log.Println("Container network ready")
}
Expand Down Expand Up @@ -337,7 +337,7 @@ func main() {

publicIP := network.PublicIP()
privateIP := network.PrivateIP()
log.Printf("Agent v%s started. Public IP: %s, Private IP: %s. Tick interval: %v", agent.Version, publicIP, privateIP, agent.TickInterval)
log.Printf("Agent %s started. Public IP: %s, Private IP: %s. Tick interval: %v", agent.Version, publicIP, privateIP, agent.TickInterval)

agentInstance := agent.NewAgent(client, reconciler, config, publicIP, privateIP, dataDir, logCollector, traefikLogCollector, metricsSender, routeOwners, builder, config.IsProxy, disableDNS)
agentInstance.Run(ctx)
Expand Down
79 changes: 60 additions & 19 deletions agent/internal/container/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"log"
"os"
Expand All @@ -13,6 +14,7 @@ import (
"time"

"techulus/cloud-agent/internal/retry"
"techulus/cloud-agent/internal/wireguard"
)

func ContainerExists(containerID string) (bool, error) {
Expand Down Expand Up @@ -498,31 +500,70 @@ func EnsureNetwork(subnetId int) error {
gateway := fmt.Sprintf("10.200.%d.1", subnetId)

checkCmd := exec.Command("podman", "network", "inspect", NetworkName)
if err := checkCmd.Run(); err == nil {
return nil
if err := checkCmd.Run(); err != nil {
args := []string{
"network", "create",
"--driver", "bridge",
"--subnet", subnet,
"--gateway", gateway,
"--disable-dns",
NetworkName,
}

createCmd := exec.Command("podman", args...)
output, err := createCmd.CombinedOutput()
if err != nil && !strings.Contains(string(output), "already exists") {
return fmt.Errorf("failed to create network: %s: %w", string(output), err)
}

if err == nil {
// Podman only creates the bridge interface when a container uses the network.
// Run a throwaway container to force bridge creation so DNS can bind to the gateway IP.
exec.Command("podman", "run", "--rm", "--network", NetworkName, "busybox", "true").Run()
}
}

args := []string{
"network", "create",
"--driver", "bridge",
"--subnet", subnet,
"--gateway", gateway,
"--disable-dns",
NetworkName,
return ensureForwarding(subnetId)
}

func forwardingRuleArgs(subnetId int) []string {
return []string{
"-i", wireguard.DefaultInterface,
"-d", fmt.Sprintf("10.200.%d.0/24", subnetId),
"-m", "conntrack",
"--ctstate", "NEW,RELATED,ESTABLISHED",
"-j", "ACCEPT",
}
}

createCmd := exec.Command("podman", args...)
output, err := createCmd.CombinedOutput()
if err != nil {
if strings.Contains(string(output), "already exists") {
return nil
}
return fmt.Errorf("failed to create network: %s: %w", string(output), err)
func isIPTablesRuleMissing(err error) bool {
var exitErr *exec.ExitError
return errors.As(err, &exitErr) && exitErr.ExitCode() == 1
}

func ensureForwarding(subnetId int) error {
if exec.Command("systemctl", "is-active", "--quiet", "firewalld").Run() == nil {
return fmt.Errorf("active firewalld is not supported for WireGuard container forwarding")
}
if _, err := exec.LookPath("iptables"); err != nil {
return fmt.Errorf("iptables not found: %w", err)
}

// Podman only creates the bridge interface when a container uses the network.
// Run a throwaway container to force bridge creation so DNS can bind to the gateway IP.
exec.Command("podman", "run", "--rm", "--network", NetworkName, "busybox", "true").Run()
rule := forwardingRuleArgs(subnetId)
checkArgs := append([]string{"-w", "5", "-C", "FORWARD"}, rule...)
output, err := exec.Command("iptables", checkArgs...).CombinedOutput()
if err == nil {
return nil
}
if !isIPTablesRuleMissing(err) {
return fmt.Errorf("failed to check WireGuard container forwarding: %s: %w", strings.TrimSpace(string(output)), err)
}

insertArgs := append([]string{"-w", "5", "-I", "FORWARD", "1"}, rule...)
output, err = exec.Command("iptables", insertArgs...).CombinedOutput()
if err != nil {
return fmt.Errorf("failed to allow WireGuard container forwarding: %s: %w", strings.TrimSpace(string(output)), err)
}

return nil
}
7 changes: 7 additions & 0 deletions cli/internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ func NewClient(host, apiKey string) *Client {
}

func (c *Client) RequestJSON(ctx context.Context, method, path string, query url.Values, body any, out any) error {
return c.RequestJSONWithHeaders(ctx, method, path, query, nil, body, out)
}

func (c *Client) RequestJSONWithHeaders(ctx context.Context, method, path string, query url.Values, customHeaders map[string]string, body any, out any) error {
endpoint := c.Host + path
if len(query) > 0 {
endpoint += "?" + query.Encode()
Expand All @@ -72,6 +76,9 @@ func (c *Client) RequestJSON(ctx context.Context, method, path string, query url
if c.APIKey != "" {
headers["x-api-key"] = c.APIKey
}
for key, value := range customHeaders {
headers[key] = value
}
return JSON(ctx, c.HTTPClient, method, endpoint, headers, body, out)
}

Expand Down
Loading
Loading