showconf.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. // Copyright 2021 the Kilo authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package main
  15. import (
  16. "errors"
  17. "fmt"
  18. "net"
  19. "os"
  20. "strings"
  21. "time"
  22. "github.com/spf13/cobra"
  23. "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
  24. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  25. "k8s.io/apimachinery/pkg/runtime"
  26. "k8s.io/apimachinery/pkg/runtime/schema"
  27. "k8s.io/apimachinery/pkg/runtime/serializer/json"
  28. "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
  29. "github.com/squat/kilo/pkg/mesh"
  30. "github.com/squat/kilo/pkg/wireguard"
  31. )
  32. const (
  33. outputFormatJSON = "json"
  34. outputFormatWireGuard = "wireguard"
  35. outputFormatYAML = "yaml"
  36. )
  37. var (
  38. availableOutputFormats = strings.Join([]string{
  39. outputFormatJSON,
  40. outputFormatWireGuard,
  41. outputFormatYAML,
  42. }, ", ")
  43. allowedIPs []string
  44. showConfOpts struct {
  45. allowedIPs []net.IPNet
  46. serializer *json.Serializer
  47. output string
  48. asPeer bool
  49. }
  50. )
  51. func showConf() *cobra.Command {
  52. cmd := &cobra.Command{
  53. Use: "showconf",
  54. Short: "Show the WireGuard configuration for a node or peer in the Kilo network",
  55. PersistentPreRunE: runShowConf,
  56. }
  57. for _, subCmd := range []*cobra.Command{
  58. showConfNode(),
  59. showConfPeer(),
  60. } {
  61. cmd.AddCommand(subCmd)
  62. }
  63. cmd.PersistentFlags().BoolVar(&showConfOpts.asPeer, "as-peer", false, "Should the resource be shown as a peer? Useful to configure this resource as a peer of another WireGuard interface.")
  64. cmd.PersistentFlags().StringVarP(&showConfOpts.output, "output", "o", "wireguard", fmt.Sprintf("The output format of the resource. Only valid when combined with 'as-peer'. Possible values: %s", availableOutputFormats))
  65. cmd.PersistentFlags().StringSliceVar(&allowedIPs, "allowed-ips", []string{}, "Add the given IPs to the allowed IPs of the configuration. Only valid when combined with 'as-peer'.")
  66. return cmd
  67. }
  68. func runShowConf(c *cobra.Command, args []string) error {
  69. switch showConfOpts.output {
  70. case outputFormatJSON:
  71. showConfOpts.serializer = json.NewSerializer(json.DefaultMetaFactory, peerCreatorTyper{}, peerCreatorTyper{}, true)
  72. case outputFormatWireGuard:
  73. case outputFormatYAML:
  74. showConfOpts.serializer = json.NewYAMLSerializer(json.DefaultMetaFactory, peerCreatorTyper{}, peerCreatorTyper{})
  75. default:
  76. return fmt.Errorf("output format %s unknown; posible values are: %s", showConfOpts.output, availableOutputFormats)
  77. }
  78. for i := range allowedIPs {
  79. _, aip, err := net.ParseCIDR(allowedIPs[i])
  80. if err != nil {
  81. return fmt.Errorf("allowed-ips must contain only valid CIDRs; got %q", allowedIPs[i])
  82. }
  83. showConfOpts.allowedIPs = append(showConfOpts.allowedIPs, *aip)
  84. }
  85. return runRoot(c, args)
  86. }
  87. func showConfNode() *cobra.Command {
  88. return &cobra.Command{
  89. Use: "node [name]",
  90. Short: "Show the WireGuard configuration for a node in the Kilo network",
  91. RunE: runShowConfNode,
  92. Args: cobra.ExactArgs(1),
  93. ValidArgsFunction: func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
  94. ns, err := opts.backend.Nodes().List()
  95. if err != nil {
  96. cobra.CompError(err.Error())
  97. return nil, cobra.ShellCompDirectiveNoFileComp
  98. }
  99. completions := make([]string, 0, len(ns))
  100. for _, n := range ns {
  101. if n.Ready() {
  102. completions = append(completions, n.Name)
  103. }
  104. }
  105. return completions, cobra.ShellCompDirectiveNoFileComp
  106. },
  107. }
  108. }
  109. func showConfPeer() *cobra.Command {
  110. return &cobra.Command{
  111. Use: "peer [name]",
  112. Short: "Show the WireGuard configuration for a peer in the Kilo network",
  113. RunE: runShowConfPeer,
  114. Args: cobra.ExactArgs(1),
  115. ValidArgsFunction: func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
  116. ps, err := opts.backend.Peers().List()
  117. if err != nil {
  118. cobra.CompError(err.Error())
  119. return nil, cobra.ShellCompDirectiveNoFileComp
  120. }
  121. completions := make([]string, 0, len(ps))
  122. for _, p := range ps {
  123. if p.Ready() {
  124. completions = append(completions, p.Name)
  125. }
  126. }
  127. return completions, cobra.ShellCompDirectiveNoFileComp
  128. },
  129. }
  130. }
  131. func runShowConfNode(_ *cobra.Command, args []string) error {
  132. ns, err := opts.backend.Nodes().List()
  133. if err != nil {
  134. return fmt.Errorf("failed to list nodes: %w", err)
  135. }
  136. ps, err := opts.backend.Peers().List()
  137. if err != nil {
  138. return fmt.Errorf("failed to list peers: %w", err)
  139. }
  140. // Obtain the Granularity by looking at the annotation of the first node.
  141. if opts.granularity, err = determineGranularity(opts.granularity, ns); err != nil {
  142. return fmt.Errorf("failed to determine granularity: %w", err)
  143. }
  144. hostname := args[0]
  145. subnet := mesh.DefaultKiloSubnet
  146. nodes := make(map[string]*mesh.Node)
  147. for _, n := range ns {
  148. if n.Ready() {
  149. nodes[n.Name] = n
  150. }
  151. if n.WireGuardIP != nil {
  152. subnet = n.WireGuardIP
  153. }
  154. }
  155. subnet.IP = subnet.IP.Mask(subnet.Mask)
  156. if len(nodes) == 0 {
  157. return errors.New("did not find any valid Kilo nodes in the cluster")
  158. }
  159. if _, ok := nodes[hostname]; !ok {
  160. return fmt.Errorf("did not find any node named %q in the cluster", hostname)
  161. }
  162. peers := make(map[string]*mesh.Peer)
  163. for _, p := range ps {
  164. if p.Ready() {
  165. peers[p.Name] = p
  166. }
  167. }
  168. t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, int(opts.port), wgtypes.Key{}, subnet, nil, nodes[hostname].PersistentKeepalive, nil)
  169. if err != nil {
  170. return fmt.Errorf("failed to create topology: %w", err)
  171. }
  172. var found bool
  173. for _, p := range t.PeerConf("").Peers {
  174. if p.PublicKey == nodes[hostname].Key {
  175. found = true
  176. break
  177. }
  178. }
  179. if !found {
  180. _, err := os.Stderr.WriteString(fmt.Sprintf("Node %q is not a leader node\n", hostname))
  181. return err
  182. }
  183. if !showConfOpts.asPeer {
  184. c, err := t.Conf().Bytes()
  185. if err != nil {
  186. return fmt.Errorf("failed to generate configuration: %w", err)
  187. }
  188. _, err = os.Stdout.Write(c)
  189. return err
  190. }
  191. switch showConfOpts.output {
  192. case outputFormatJSON:
  193. fallthrough
  194. case outputFormatYAML:
  195. p := t.AsPeer()
  196. if p == nil {
  197. return errors.New("cannot generate config from nil peer")
  198. }
  199. p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
  200. p.DeduplicateIPs()
  201. k8sp := translatePeer(p)
  202. k8sp.Name = hostname
  203. return showConfOpts.serializer.Encode(k8sp, os.Stdout)
  204. case outputFormatWireGuard:
  205. p := t.AsPeer()
  206. if p == nil {
  207. return errors.New("cannot generate config from nil peer")
  208. }
  209. p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
  210. p.DeduplicateIPs()
  211. c, err := (&wireguard.Conf{
  212. Peers: []wireguard.Peer{*p},
  213. }).Bytes()
  214. if err != nil {
  215. return fmt.Errorf("failed to generate configuration: %w", err)
  216. }
  217. _, err = os.Stdout.Write(c)
  218. return err
  219. }
  220. return nil
  221. }
  222. func runShowConfPeer(_ *cobra.Command, args []string) error {
  223. ns, err := opts.backend.Nodes().List()
  224. if err != nil {
  225. return fmt.Errorf("failed to list nodes: %w", err)
  226. }
  227. ps, err := opts.backend.Peers().List()
  228. if err != nil {
  229. return fmt.Errorf("failed to list peers: %w", err)
  230. }
  231. // Obtain the Granularity by looking at the annotation of the first node.
  232. if opts.granularity, err = determineGranularity(opts.granularity, ns); err != nil {
  233. return fmt.Errorf("failed to determine granularity: %w", err)
  234. }
  235. var hostname string
  236. subnet := mesh.DefaultKiloSubnet
  237. nodes := make(map[string]*mesh.Node)
  238. for _, n := range ns {
  239. if n.Ready() {
  240. nodes[n.Name] = n
  241. hostname = n.Name
  242. }
  243. if n.WireGuardIP != nil {
  244. subnet = n.WireGuardIP
  245. }
  246. }
  247. subnet.IP = subnet.IP.Mask(subnet.Mask)
  248. if len(nodes) == 0 {
  249. return errors.New("did not find any valid Kilo nodes in the cluster")
  250. }
  251. peer := args[0]
  252. peers := make(map[string]*mesh.Peer)
  253. for _, p := range ps {
  254. if p.Ready() {
  255. peers[p.Name] = p
  256. }
  257. }
  258. if _, ok := peers[peer]; !ok {
  259. return fmt.Errorf("did not find any peer named %q in the cluster", peer)
  260. }
  261. pka := time.Duration(0)
  262. if p := peers[peer].PersistentKeepaliveInterval; p != nil {
  263. pka = *p
  264. }
  265. t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, mesh.DefaultKiloPort, wgtypes.Key{}, subnet, nil, pka, nil)
  266. if err != nil {
  267. return fmt.Errorf("failed to create topology: %w", err)
  268. }
  269. if !showConfOpts.asPeer {
  270. c, err := t.PeerConf(peer).Bytes()
  271. if err != nil {
  272. return fmt.Errorf("failed to generate configuration: %w", err)
  273. }
  274. _, err = os.Stdout.Write(c)
  275. return err
  276. }
  277. switch showConfOpts.output {
  278. case outputFormatJSON:
  279. fallthrough
  280. case outputFormatYAML:
  281. p := peers[peer]
  282. p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
  283. p.DeduplicateIPs()
  284. k8sp := translatePeer(&p.Peer)
  285. k8sp.Name = peer
  286. return showConfOpts.serializer.Encode(k8sp, os.Stdout)
  287. case outputFormatWireGuard:
  288. p := &peers[peer].Peer
  289. p.AllowedIPs = append(p.AllowedIPs, showConfOpts.allowedIPs...)
  290. p.DeduplicateIPs()
  291. c, err := (&wireguard.Conf{
  292. Peers: []wireguard.Peer{*p},
  293. }).Bytes()
  294. if err != nil {
  295. return fmt.Errorf("failed to generate configuration: %w", err)
  296. }
  297. _, err = os.Stdout.Write(c)
  298. return err
  299. }
  300. return nil
  301. }
  302. // translatePeer translates a wireguard.Peer to a Peer CRD.
  303. // TODO this function has many similarities to peerBackend.Set(name, peer)
  304. func translatePeer(peer *wireguard.Peer) *v1alpha1.Peer {
  305. if peer == nil {
  306. return &v1alpha1.Peer{}
  307. }
  308. var aips []string
  309. for _, aip := range peer.AllowedIPs {
  310. // Skip any invalid IPs.
  311. // TODO all IPs should be valid, so no need to skip here?
  312. if aip.String() == (&net.IPNet{}).String() {
  313. continue
  314. }
  315. aips = append(aips, aip.String())
  316. }
  317. var endpoint *v1alpha1.PeerEndpoint
  318. if peer.Endpoint.Port() > 0 || !peer.Endpoint.HasDNS() {
  319. endpoint = &v1alpha1.PeerEndpoint{
  320. DNSOrIP: v1alpha1.DNSOrIP{
  321. IP: peer.Endpoint.IP().String(),
  322. DNS: peer.Endpoint.DNS(),
  323. },
  324. Port: uint32(peer.Endpoint.Port()),
  325. }
  326. }
  327. var key string
  328. if peer.PublicKey != (wgtypes.Key{}) {
  329. key = peer.PublicKey.String()
  330. }
  331. var psk string
  332. if peer.PresharedKey != nil {
  333. psk = peer.PresharedKey.String()
  334. }
  335. var pka int
  336. if peer.PersistentKeepaliveInterval != nil && *peer.PersistentKeepaliveInterval > time.Duration(0) {
  337. pka = int(*peer.PersistentKeepaliveInterval)
  338. }
  339. return &v1alpha1.Peer{
  340. TypeMeta: metav1.TypeMeta{
  341. Kind: v1alpha1.PeerKind,
  342. APIVersion: v1alpha1.SchemeGroupVersion.String(),
  343. },
  344. Spec: v1alpha1.PeerSpec{
  345. AllowedIPs: aips,
  346. Endpoint: endpoint,
  347. PersistentKeepalive: pka,
  348. PresharedKey: psk,
  349. PublicKey: key,
  350. },
  351. }
  352. }
  353. type peerCreatorTyper struct{}
  354. func (p peerCreatorTyper) New(_ schema.GroupVersionKind) (runtime.Object, error) {
  355. return &v1alpha1.Peer{}, nil
  356. }
  357. func (p peerCreatorTyper) ObjectKinds(_ runtime.Object) ([]schema.GroupVersionKind, bool, error) {
  358. return []schema.GroupVersionKind{v1alpha1.PeerGVK}, false, nil
  359. }
  360. func (p peerCreatorTyper) Recognizes(_ schema.GroupVersionKind) bool {
  361. return true
  362. }