diff --git a/internal/dhcp/dhcp_test.go b/internal/dhcp/dhcp_test.go index 94c7d7b..a13d1a2 100644 --- a/internal/dhcp/dhcp_test.go +++ b/internal/dhcp/dhcp_test.go @@ -55,7 +55,7 @@ func newConf(t *testing.T, cidr string) Config { gw := net.ParseIP("192.168.1.1").To4() return Config{ Network: network, - VPCGateway: gw, + InterfaceIP: net.ParseIP("192.168.1.254").To4(), VPCRoute: vpcNet, DefaultGateway: gw, Name: "test", @@ -136,32 +136,29 @@ func TestGenerateConfig_VxlanEmitsNoRouterOption(t *testing.T) { } func TestGenerateConfig_ContainsVPCRoute(t *testing.T) { - conf := newConf(t, "192.168.1.0/29") - path, _, _ := GenerateConfig(conf) - content, _ := os.ReadFile(path) - - if !strings.Contains(string(content), "dhcp-option=121,10.0.0.0/16,192.168.1.1") { - t.Errorf("dhcp-option=121 absente ou incorrecte :\n%s", content) + routes := route121(t, confLines(t, newConf(t, "192.168.1.0/29"))) + if !strings.Contains(routes, "10.0.0.0/16,192.168.1.254") { + t.Errorf("route VPC absente, ou next-hop autre que l'interface_ip du subnet :\n%s", routes) } } func TestGenerateConfig_NoVPCRoute(t *testing.T) { conf := newConf(t, "192.168.1.0/29") conf.VPCRoute = nil - path, _, _ := GenerateConfig(conf) - content, _ := os.ReadFile(path) - if strings.Contains(string(content), "dhcp-option=121,") { - t.Errorf("dhcp-option=121 présente alors que VPCRoute=nil :\n%s", content) + routes := route121(t, confLines(t, conf)) + if strings.Contains(routes, "10.0.0.0/16") { + t.Errorf("route VPC présente alors que VPCRoute=nil :\n%s", routes) } } func TestGenerateConfig_ContainsDhcpRange(t *testing.T) { _, network, _ := net.ParseCIDR("10.10.0.0/24") conf := Config{ - Network: network, - Name: "vpc1", - ConfDir: t.TempDir(), + Network: network, + InterfaceIP: net.ParseIP("10.10.0.1").To4(), + Name: "vpc1", + ConfDir: t.TempDir(), } path, _, _ := GenerateConfig(conf) content, _ := os.ReadFile(path) @@ -204,9 +201,10 @@ func TestGenerateConfig_CreatesConfDir(t *testing.T) { dir := filepath.Join(t.TempDir(), "sous", "dossier") _, network, _ := net.ParseCIDR("10.0.0.0/30") conf := Config{ - Network: network, - Name: "net", - ConfDir: dir, + Network: network, + InterfaceIP: net.ParseIP("10.0.0.1").To4(), + Name: "net", + ConfDir: dir, } if _, _, err := GenerateConfig(conf); err != nil { t.Fatalf("GenerateConfig devrait créer les répertoires manquants : %v", err) @@ -215,3 +213,79 @@ func TestGenerateConfig_CreatesConfDir(t *testing.T) { t.Errorf("répertoire %q non créé", dir) } } + +// --- option 121 : routes classless --- + +func confLines(t *testing.T, c Config) string { + t.Helper() + path, _, err := GenerateConfig(c) + if err != nil { + t.Fatalf("GenerateConfig : %v", err) + } + content, err := os.ReadFile(path) + if err != nil { + t.Fatalf("lecture : %v", err) + } + return string(content) +} + +func route121(t *testing.T, content string) string { + t.Helper() + for _, line := range strings.Split(content, "\n") { + if strings.HasPrefix(line, "dhcp-option=121,") { + return strings.TrimPrefix(line, "dhcp-option=121,") + } + } + t.Fatalf("aucune dhcp-option=121 dans :\n%s", content) + return "" +} + +func TestGenerateConfig_AlwaysRoutesToMetadata(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + conf.VPCRoute = nil + conf.DefaultGateway = nil + + routes := route121(t, confLines(t, conf)) + if !strings.Contains(routes, "169.254.169.254/32,192.168.1.254") { + t.Errorf("sans route vers le serveur de métadonnées, cloud-init échoue et la VM n'est pas provisionnée :\n%s", routes) + } +} + +func TestGenerateConfig_MetadataRouteEvenInBridgeMode(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + conf.VPCRoute = nil + + routes := route121(t, confLines(t, conf)) + if !strings.Contains(routes, "169.254.169.254/32") { + t.Errorf("le mode bridge a besoin de la même route :\n%s", routes) + } +} + +func TestGenerateConfig_DefaultRouteAlsoInOption121(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + conf.VPCRoute = nil + + routes := route121(t, confLines(t, conf)) + if !strings.Contains(routes, "0.0.0.0/0,192.168.1.1") { + t.Errorf("RFC 3442 : un client qui lit l'option 121 ignore l'option 3, la route par défaut doit donc figurer dans la 121 :\n%s", routes) + } +} + +func TestGenerateConfig_NoDefaultRouteMeansNoCatchAllInOption121(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + conf.DefaultGateway = nil + + routes := route121(t, confLines(t, conf)) + if strings.Contains(routes, "0.0.0.0/0") { + t.Errorf("aucune route par défaut demandée, la 121 ne doit pas en contenir :\n%s", routes) + } +} + +func TestGenerateConfig_MissingInterfaceIPIsAnError(t *testing.T) { + conf := newConf(t, "192.168.1.0/29") + conf.InterfaceIP = nil + + if _, _, err := GenerateConfig(conf); err == nil { + t.Error("sans interface_ip aucune route metadata n'est possible : il faut échouer, pas écrire une conf muette") + } +} diff --git a/internal/metadata/struct.go b/internal/metadata/struct.go index 3c9da1e..39592ed 100644 --- a/internal/metadata/struct.go +++ b/internal/metadata/struct.go @@ -1,5 +1,8 @@ package metadata +// ServiceIP is the well-known address guests use to reach the metadata server. +// Traffic to it is DNATed to the subnet gateway by internal/iptables, and the +// route to it is advertised by internal/dhcp. const ServiceIP = "169.254.169.254" type NoCloudData struct {