package persist import ( "fmt" "strconv" "strings" "github.com/drs/gre-panel/internal/link" ) // networkd file suffixes. const ( NetdevSuffix = ".netdev" NetworkSuffix = ".network" ) // NetdevName is the .netdev file name for a tunnel. func NetdevName(interfaceName string) string { return interfaceName + NetdevSuffix } // NetworkName is the .network file name for a tunnel. func NetworkName(interfaceName string) string { return interfaceName + NetworkSuffix } // networkdKind maps a tunnel kind onto the NetDev Kind= value. systemd-networkd // spells the IPv6 variants the same way iproute2 does. func networkdKind(kind string) string { return kind } // Netdev renders the .netdev file, which declares the tunnel device // itself (§9.4). func (r *Renderer) Netdev(spec link.TunnelSpec) string { var b strings.Builder b.WriteString(OwnershipMarker + " interface=" + spec.Name + "\n") b.WriteString("# This file is generated by gre-panel. Edit the tunnel in the panel; changes made\n") b.WriteString("# here are overwritten the next time it is applied.\n") b.WriteString("\n[NetDev]\n") fmt.Fprintf(&b, "Name=%s\n", spec.Name) fmt.Fprintf(&b, "Kind=%s\n", networkdKind(spec.Kind)) fmt.Fprintf(&b, "Description=GRE Tunnel %s (managed by gre-panel)\n", spec.Name) if spec.Mtu > 0 { fmt.Fprintf(&b, "MTUBytes=%d\n", spec.Mtu) } b.WriteString("\n[Tunnel]\n") if spec.Local != "" { fmt.Fprintf(&b, "Local=%s\n", spec.Local) } if spec.Remote != "" { fmt.Fprintf(&b, "Remote=%s\n", spec.Remote) } // Without a bind device the tunnel does not hang off any particular link, and // networkd needs to be told so explicitly or it never creates the device. if spec.BindDevice == "" { b.WriteString("Independent=yes\n") } if link.IsIPv6Kind(spec.Kind) { hop := spec.Ttl if spec.HopLimit != nil { hop = *spec.HopLimit } if hop > 0 { fmt.Fprintf(&b, "TTL=%d\n", hop) } if spec.EncapLimit != nil { fmt.Fprintf(&b, "EncapsulationLimit=%d\n", *spec.EncapLimit) } if spec.FlowLabel != "" && spec.FlowLabel != "inherit" { fmt.Fprintf(&b, "IPv6FlowLabel=%s\n", spec.FlowLabel) } } else { if spec.Ttl > 0 { fmt.Fprintf(&b, "TTL=%d\n", spec.Ttl) } else { // networkd spells a TTL of zero "inherit", matching the kernel. b.WriteString("TTL=inherit\n") } } // Stated explicitly for the same reason the ip command states it: "inherit" // is a value, not an absence, and leaving it out would mean a fixed TOS of // zero on a tunnel configured to copy the inner one. if spec.Tos != "" && spec.Tos != "inherit" { fmt.Fprintf(&b, "TOS=%s\n", spec.Tos) } else { b.WriteString("TOS=inherit\n") } // The keys are written as separate input and output values whenever they // differ, because they are independent attributes and collapsing them would // quietly change an asymmetric tunnel into a symmetric one. switch { case spec.IKey != nil && spec.OKey != nil && *spec.IKey == *spec.OKey: fmt.Fprintf(&b, "Key=%s\n", strconv.FormatUint(uint64(*spec.IKey), 10)) default: if spec.IKey != nil { fmt.Fprintf(&b, "InputKey=%s\n", strconv.FormatUint(uint64(*spec.IKey), 10)) } if spec.OKey != nil { fmt.Fprintf(&b, "OutputKey=%s\n", strconv.FormatUint(uint64(*spec.OKey), 10)) } } fmt.Fprintf(&b, "DiscoverPathMTU=%s\n", yesNo(spec.IsPathMtuDiscovery)) if spec.HasInputSequence || spec.HasOutputSequence { b.WriteString("SerializeTunneledPackets=yes\n") } if spec.IsIgnoreDf { b.WriteString("IgnoreDontFragment=yes\n") } return b.String() } // Network renders the .network file, which configures the addresses on // the tunnel device (§9.4). func (r *Renderer) Network(spec link.TunnelSpec, addresses []link.Address) string { var b strings.Builder b.WriteString(OwnershipMarker + " interface=" + spec.Name + "\n") b.WriteString("# This file is generated by gre-panel. Edit the tunnel in the panel; changes made\n") b.WriteString("# here are overwritten the next time it is applied.\n") b.WriteString("\n[Match]\n") fmt.Fprintf(&b, "Name=%s\n", spec.Name) b.WriteString("\n[Network]\n") if len(addresses) == 0 { b.WriteString("# This tunnel carries no addresses.\n") } for _, addr := range addresses { fmt.Fprintf(&b, "Address=%s\n", addr.String()) } // The panel manages the layer-3 link only. Routing, NAT and firewalling are // deliberately out of scope, so nothing else is configured here. b.WriteString("ConfigureWithoutCarrier=yes\n") for _, addr := range addresses { if !addr.NeedsExplicitPeer() { continue } b.WriteString("\n[Address]\n") fmt.Fprintf(&b, "Address=%s\n", addr.String()) fmt.Fprintf(&b, "Peer=%s\n", addr.Peer) } return b.String() } // UnderlayNetworkNote explains why a tunnel with a bind device needs a line on // that device's own .network file, which the panel does not write because // touching the underlay's configuration is out of scope. func UnderlayNetworkNote(interfaceName, bindDevice string) string { return fmt.Sprintf("This tunnel is bound to %s. For systemd-networkd to create it, the .network "+ "file for %s needs Tunnel=%s in its [Network] section. The panel does not write that file, "+ "because changing the configuration of a physical interface is outside what it manages.", bindDevice, bindDevice, interfaceName) } func yesNo(v bool) string { if v { return "yes" } return "no" }