From 46cdce00af6f123576d572fe8facfa93db7b79a2 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sun, 31 Oct 2021 16:05:08 +0100 Subject: [PATCH 01/32] Do not assume IPv4 during address generation --- utils.go | 30 +++++++++++++----------------- utils_test.go | 4 ++-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/utils.go b/utils.go index 7011e63d..d2b6b902 100644 --- a/utils.go +++ b/utils.go @@ -141,36 +141,32 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { return nil, err } + ipPrefixNetworkAddress, ipPrefixBroadcastAddress := func() (netaddr.IP, netaddr.IP) { + ipRange := ipPrefix.Range() + return ipRange.From(), ipRange.To() + }() + // Get the first IP in our prefix - ip := ipPrefix.IP() + ip := ipPrefixNetworkAddress.Next() for { if !ipPrefix.Contains(ip) { return nil, errCouldNotAllocateIP } - // Some OS (including Linux) does not like when IPs ends with 0 or 255, which - // is typically called network or broadcast. Lets avoid them and continue - // to look when we get one of those traditionally reserved IPs. - ipRaw := ip.As4() - if ipRaw[3] == 0 || ipRaw[3] == 255 { + switch { + case ip.Compare(ipPrefixBroadcastAddress) == 0: + fallthrough + case containsIPs(usedIps, ip): + fallthrough + case ip.IsZero() || ip.IsLoopback(): ip = ip.Next() continue - } - if ip.IsZero() && - ip.IsLoopback() { - ip = ip.Next() - - continue - } - - if !containsIPs(usedIps, ip) { + default: return &ip, nil } - - ip = ip.Next() } } diff --git a/utils_test.go b/utils_test.go index 95722a83..9b0295b3 100644 --- a/utils_test.go +++ b/utils_test.go @@ -93,7 +93,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) - c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.47")) + c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.45")) // Check that we can read back the IPs machine1, err := app.GetMachineByID(1) @@ -112,7 +112,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) { netaddr.MustParseIP("10.27.0.50").String(), ) - expectedNextIP := netaddr.MustParseIP("10.27.1.97") + expectedNextIP := netaddr.MustParseIP("10.27.1.95") nextIP, err := app.getAvailableIP() c.Assert(err, check.IsNil) From 7ec834617959025bb3294d6dadb2ee3b070e62e0 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sun, 31 Oct 2021 16:05:48 +0100 Subject: [PATCH 02/32] Do not assume IPv4 during Tailscale node construction --- machine.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/machine.go b/machine.go index d58c9c5c..c13d79d7 100644 --- a/machine.go +++ b/machine.go @@ -470,16 +470,16 @@ func (machine Machine) toNode( } addrs := []netaddr.IPPrefix{} - ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", machine.IPAddress)) + nodeAddr, err := netaddr.ParseIP(m.IPAddresses) if err != nil { log.Trace(). Caller(). - Str("ip", machine.IPAddress). - Msgf("Failed to parse IP Prefix from IP: %s", machine.IPAddress) - + Str("ip", machine.IPAddresses). + Msgf("Failed to parse machine IP: %s", machine.IPAddresses) return nil, err } - addrs = append(addrs, ip) // missing the ipv6 ? + ip := netaddr.IPPrefixFrom(nodeAddr, nodeAddr.BitLen()) + addrs = append(addrs, ip) allowedIPs := []netaddr.IPPrefix{} allowedIPs = append( From 8b4034327776c2d1010d34387e4d237c7d84fedb Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sun, 16 Jan 2022 14:16:00 +0100 Subject: [PATCH 03/32] Add multiple IP prefixes support to ProtoBuf schema --- proto/headscale/v1/machine.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proto/headscale/v1/machine.proto b/proto/headscale/v1/machine.proto index ed99f002..9b032122 100644 --- a/proto/headscale/v1/machine.proto +++ b/proto/headscale/v1/machine.proto @@ -18,7 +18,7 @@ message Machine { string machine_key = 2; string node_key = 3; string disco_key = 4; - string ip_address = 5; + repeated string ip_addresses = 5; string name = 6; Namespace namespace = 7; From 3a3aecb7742d94d7747a44eb31ae634dbb83b189 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sun, 16 Jan 2022 14:15:35 +0100 Subject: [PATCH 04/32] Regenerate files based on ProtoBuf schema. --- gen/go/headscale/v1/device.pb.go | 2 +- gen/go/headscale/v1/headscale.pb.go | 2 +- gen/go/headscale/v1/machine.pb.go | 234 +++++++++--------- gen/go/headscale/v1/namespace.pb.go | 2 +- gen/go/headscale/v1/preauthkey.pb.go | 2 +- gen/go/headscale/v1/routes.pb.go | 2 +- .../headscale/v1/headscale.swagger.json | 7 +- 7 files changed, 127 insertions(+), 124 deletions(-) diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index 302c2653..a3b5911a 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/device.proto package v1 diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index dd27265d..ad8a50d7 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/headscale.proto package v1 diff --git a/gen/go/headscale/v1/machine.pb.go b/gen/go/headscale/v1/machine.pb.go index deafbe5c..07613697 100644 --- a/gen/go/headscale/v1/machine.pb.go +++ b/gen/go/headscale/v1/machine.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/machine.proto package v1 @@ -82,7 +82,7 @@ type Machine struct { MachineKey string `protobuf:"bytes,2,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"` NodeKey string `protobuf:"bytes,3,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"` DiscoKey string `protobuf:"bytes,4,opt,name=disco_key,json=discoKey,proto3" json:"disco_key,omitempty"` - IpAddress string `protobuf:"bytes,5,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + IpAddresses []string `protobuf:"bytes,5,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` Namespace *Namespace `protobuf:"bytes,7,opt,name=namespace,proto3" json:"namespace,omitempty"` Registered bool `protobuf:"varint,8,opt,name=registered,proto3" json:"registered,omitempty"` @@ -154,11 +154,11 @@ func (x *Machine) GetDiscoKey() string { return "" } -func (x *Machine) GetIpAddress() string { +func (x *Machine) GetIpAddresses() []string { if x != nil { - return x.IpAddress + return x.IpAddresses } - return "" + return nil } func (x *Machine) GetName() string { @@ -1026,129 +1026,129 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{ 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, - 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf9, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, + 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, - 0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0e, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x37, - 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, - 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, - 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x3a, 0x0a, - 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0d, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, - 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, + 0x0c, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, + 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, - 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4a, - 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, + 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x22, 0x4a, 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, + 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x32, + 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x49, 0x64, 0x22, 0x45, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, + 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, + 0x22, 0x17, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x45, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, + 0x22, 0x48, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, - 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x32, 0x0a, 0x11, 0x47, 0x65, - 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x45, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x17, 0x0a, 0x15, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x15, - 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, - 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, - 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a, 0x14, 0x4c, - 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x08, 0x6d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x52, 0x0a, 0x13, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x47, 0x0a, 0x14, 0x53, 0x68, - 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x22, 0x54, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x55, 0x6e, 0x73, - 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, - 0x68, 0x69, 0x6e, 0x65, 0x22, 0x77, 0x0a, 0x19, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x69, + 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x4d, 0x0a, - 0x1a, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2a, 0x82, 0x01, 0x0a, - 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, - 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, - 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, - 0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x17, - 0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, - 0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x45, 0x47, 0x49, 0x53, - 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x49, 0x44, 0x43, 0x10, - 0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, - 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, + 0x49, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x52, 0x0a, 0x13, 0x53, 0x68, + 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x47, + 0x0a, 0x14, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, + 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x54, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x68, 0x61, + 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, + 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a, + 0x16, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, + 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, + 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x77, 0x0a, 0x19, 0x44, 0x65, 0x62, 0x75, + 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x22, 0x4d, 0x0a, 0x1a, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x2a, 0x82, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, + 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, + 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59, + 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, + 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52, + 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, + 0x49, 0x44, 0x43, 0x10, 0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/gen/go/headscale/v1/namespace.pb.go b/gen/go/headscale/v1/namespace.pb.go index f8af539e..341f8ca0 100644 --- a/gen/go/headscale/v1/namespace.pb.go +++ b/gen/go/headscale/v1/namespace.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/namespace.proto package v1 diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index 82b16bf8..1169a89a 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/preauthkey.proto package v1 diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index e32e1d15..ba40856d 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/routes.proto package v1 diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json index 2274e22c..f1635ac2 100644 --- a/gen/openapiv2/headscale/v1/headscale.swagger.json +++ b/gen/openapiv2/headscale/v1/headscale.swagger.json @@ -775,8 +775,11 @@ "discoKey": { "type": "string" }, - "ipAddress": { - "type": "string" + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + } }, "name": { "type": "string" From 1a6e5d8770ddc2761c931ae9d6f704e80e9dc9bc Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sun, 16 Jan 2022 14:16:59 +0100 Subject: [PATCH 05/32] Add support for multiple IP prefixes --- acls.go | 6 +- acls_test.go | 50 +++--- api.go | 15 +- app.go | 6 +- app_test.go | 4 +- cli_test.go | 10 +- cmd/headscale/cli/nodes.go | 5 +- cmd/headscale/cli/utils.go | 14 +- dns.go | 27 +++- dns_test.go | 16 +- integration_test.go | 301 ++++++++++++++++++++++--------------- machine.go | 81 +++++++--- machine_test.go | 20 +++ namespaces_test.go | 9 +- oidc.go | 5 +- sharing_test.go | 7 +- utils.go | 43 ++++-- utils_test.go | 61 ++++---- 18 files changed, 423 insertions(+), 257 deletions(-) diff --git a/acls.go b/acls.go index 4017e28e..5e191e64 100644 --- a/acls.go +++ b/acls.go @@ -185,7 +185,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { return nil, errInvalidNamespace } for _, node := range nodes { - ips = append(ips, node.IPAddress) + ips = append(ips, node.IPAddresses.ToStringSlice()...) } } @@ -219,7 +219,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { // FIXME: Check TagOwners allows this for _, t := range hostinfo.RequestTags { if alias[4:] == t { - ips = append(ips, machine.IPAddress) + ips = append(ips, machine.IPAddresses.ToStringSlice()...) break } @@ -238,7 +238,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { } ips := []string{} for _, n := range nodes { - ips = append(ips, n.IPAddress) + ips = append(ips, n.IPAddresses.ToStringSlice()...) } return ips, nil diff --git a/acls_test.go b/acls_test.go index 629ce1da..c35f4f84 100644 --- a/acls_test.go +++ b/acls_test.go @@ -61,9 +61,9 @@ func (s *Suite) TestPortRange(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(5400)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500)) + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(5400)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500)) } func (s *Suite) TestPortWildcard(c *check.C) { @@ -75,11 +75,11 @@ func (s *Suite) TestPortWildcard(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert((rules)[0].SrcIPs, check.HasLen, 1) - c.Assert((rules)[0].SrcIPs[0], check.Equals, "*") + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert(rules[0].SrcIPs, check.HasLen, 1) + c.Assert(rules[0].SrcIPs[0], check.Equals, "*") } func (s *Suite) TestPortNamespace(c *check.C) { @@ -91,7 +91,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { _, err = app.GetMachine("testnamespace", "testmachine") c.Assert(err, check.NotNil) - ip, _ := app.getAvailableIP() + ips, _ := app.getAvailableIPs() machine := Machine{ ID: 0, MachineKey: "foo", @@ -101,7 +101,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { NamespaceID: namespace.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: ip.String(), + IPAddresses: ips, AuthKeyID: uint(pak.ID), } app.db.Save(&machine) @@ -116,12 +116,13 @@ func (s *Suite) TestPortNamespace(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert((rules)[0].SrcIPs, check.HasLen, 1) - c.Assert((rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") - c.Assert((rules)[0].SrcIPs[0], check.Equals, ip.String()) + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert(rules[0].SrcIPs, check.HasLen, 1) + c.Assert(rules[0].SrcIPs[0], check.Not(check.Equals), "not an ip") + c.Assert(len(ips), check.Equals, 1) + c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()) } func (s *Suite) TestPortGroup(c *check.C) { @@ -133,7 +134,7 @@ func (s *Suite) TestPortGroup(c *check.C) { _, err = app.GetMachine("testnamespace", "testmachine") c.Assert(err, check.NotNil) - ip, _ := app.getAvailableIP() + ips, _ := app.getAvailableIPs() machine := Machine{ ID: 0, MachineKey: "foo", @@ -143,7 +144,7 @@ func (s *Suite) TestPortGroup(c *check.C) { NamespaceID: namespace.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: ip.String(), + IPAddresses: ips, AuthKeyID: uint(pak.ID), } app.db.Save(&machine) @@ -156,10 +157,11 @@ func (s *Suite) TestPortGroup(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert((rules)[0].SrcIPs, check.HasLen, 1) - c.Assert((rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") - c.Assert((rules)[0].SrcIPs[0], check.Equals, ip.String()) + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert(rules[0].SrcIPs, check.HasLen, 1) + c.Assert(rules[0].SrcIPs[0], check.Not(check.Equals), "not an ip") + c.Assert(len(ips), check.Equals, 1) + c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()) } diff --git a/api.go b/api.go index d3bd5726..020ded01 100644 --- a/api.go +++ b/api.go @@ -497,6 +497,7 @@ func (h *Headscale) handleMachineRegistrationNew( ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody) } +// TODO: check if any locks are needed around IP allocation. func (h *Headscale) handleAuthKey( ctx *gin.Context, machineKey key.MachinePublic, @@ -554,14 +555,14 @@ func (h *Headscale) handleAuthKey( log.Debug(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Msg("Authentication key was valid, proceeding to acquire an IP address") - ip, err := h.getAvailableIP() + Msg("Authentication key was valid, proceeding to acquire IP addresses") + ips, err := h.getAvailableIPs() if err != nil { log.Error(). Caller(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Msg("Failed to find an available IP") + Msg("Failed to find an available IP address") machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name). Inc() @@ -570,12 +571,12 @@ func (h *Headscale) handleAuthKey( log.Info(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Str("ip", ip.String()). - Msgf("Assigning %s to %s", ip, machine.Name) + Str("ips", strings.Join(ips.ToStringSlice(), ",")). + Msgf("Assigning %s to %s", strings.Join(ips.ToStringSlice(), ","), machine.Name) machine.Expiry = ®isterRequest.Expiry machine.AuthKeyID = uint(pak.ID) - machine.IPAddress = ip.String() + machine.IPAddresses = ips machine.NamespaceID = pak.NamespaceID machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey) @@ -610,6 +611,6 @@ func (h *Headscale) handleAuthKey( log.Info(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Str("ip", machine.IPAddress). + Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). Msg("Successfully authenticated via AuthKey") } diff --git a/app.go b/app.go index b9d570c5..b7dc8c67 100644 --- a/app.go +++ b/app.go @@ -68,7 +68,7 @@ type Config struct { ServerURL string Addr string EphemeralNodeInactivityTimeout time.Duration - IPPrefix netaddr.IPPrefix + IPPrefixes []netaddr.IPPrefix PrivateKeyPath string BaseDomain string @@ -197,9 +197,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) { } if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS - magicDNSDomains := generateMagicDNSRootDomains( - app.cfg.IPPrefix, - ) + magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes) // we might have routes already from Split DNS if app.cfg.DNSConfig.Routes == nil { app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) diff --git a/app_test.go b/app_test.go index bff13933..02fdce8b 100644 --- a/app_test.go +++ b/app_test.go @@ -41,7 +41,9 @@ func (s *Suite) ResetDB(c *check.C) { c.Fatal(err) } cfg := Config{ - IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"), + IPPrefixes: []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("10.27.0.0/23"), + }, } app = Headscale{ diff --git a/cli_test.go b/cli_test.go index ef7e2993..71f2bea5 100644 --- a/cli_test.go +++ b/cli_test.go @@ -4,6 +4,7 @@ import ( "time" "gopkg.in/check.v1" + "inet.af/netaddr" ) func (s *Suite) TestRegisterMachine(c *check.C) { @@ -19,16 +20,17 @@ func (s *Suite) TestRegisterMachine(c *check.C) { DiscoKey: "faa", Name: "testmachine", NamespaceID: namespace.ID, - IPAddress: "10.0.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")}, Expiry: &now, } - app.db.Save(&machine) + err = app.db.Save(&machine).Error + c.Assert(err, check.IsNil) - _, err = app.GetMachine("test", "testmachine") + _, err = app.GetMachine(namespace.Name, machine.Name) c.Assert(err, check.IsNil) machineAfterRegistering, err := app.RegisterMachine( - "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e", + machine.MachineKey, namespace.Name, ) c.Assert(err, check.IsNil) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 26ead6dc..d6b86ee1 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "strconv" + "strings" "time" survey "github.com/AlecAivazis/survey/v2" @@ -459,7 +460,7 @@ func nodesToPtables( "Name", "NodeKey", "Namespace", - "IP address", + "IP addresses", "Ephemeral", "Last seen", "Online", @@ -523,7 +524,7 @@ func nodesToPtables( machine.Name, nodeKey.ShortString(), namespace, - machine.IpAddress, + strings.Join(machine.IpAddresses, ", "), strconv.FormatBool(ephemeral), lastSeenTime, online, diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index a4856642..2c8f2a6b 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -41,7 +41,7 @@ func LoadConfig(path string) error { viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01") - viper.SetDefault("ip_prefix", "100.64.0.0/10") + viper.SetDefault("ip_prefixes", []string{"100.64.0.0/10"}) viper.SetDefault("log_level", "info") @@ -221,10 +221,20 @@ func getHeadscaleConfig() headscale.Config { dnsConfig, baseDomain := GetDNSConfig() derpConfig := GetDERPConfig() + configuredPrefixes := viper.GetStringSlice("ip_prefixes") + prefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)) + for i, prefixInConfig := range configuredPrefixes { + prefix, err := netaddr.ParseIPPrefix(prefixInConfig) + if err != nil { + panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err)) + } + prefixes = append(prefixes, prefix) + } + return headscale.Config{ ServerURL: viper.GetString("server_url"), Addr: viper.GetString("listen_addr"), - IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), + IPPrefixes: prefixes, PrivateKeyPath: absPath(viper.GetString("private_key_path")), BaseDomain: baseDomain, diff --git a/dns.go b/dns.go index af6f989d..5a4f682a 100644 --- a/dns.go +++ b/dns.go @@ -34,14 +34,25 @@ const ( // From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). // This allows us to then calculate the subnets included in the subsequent class block and generate the entries. -func generateMagicDNSRootDomains( - ipPrefix netaddr.IPPrefix, -) []dnsname.FQDN { - // TODO(juanfont): we are not handing out IPv6 addresses yet - // and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network) - ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") - fqdns := []dnsname.FQDN{ipv6base} +func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN { + fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes)) + for _, ipPrefix := range ipPrefixes { + var generateDnsRoot func(netaddr.IPPrefix) []dnsname.FQDN + switch ipPrefix.IP().BitLen() { + case 32: + generateDnsRoot = generateIPv4DNSRootDomain + default: + panic(fmt.Sprintf("unsupported IP version with address length %d", ipPrefix.IP().BitLen())) + } + + fqdns = append(fqdns, generateDnsRoot(ipPrefix)...) + } + + return fqdns +} + +func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) (fqdns []dnsname.FQDN) { // Conversion to the std lib net.IPnet, a bit easier to operate netRange := ipPrefix.IPNet() maskBits, _ := netRange.Mask.Size() @@ -73,7 +84,7 @@ func generateMagicDNSRootDomains( fqdns = append(fqdns, fqdn) } - return fqdns + return } func getMapResponseDNSConfig( diff --git a/dns_test.go b/dns_test.go index 92f7476f..8c0da638 100644 --- a/dns_test.go +++ b/dns_test.go @@ -124,7 +124,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(preAuthKeyInShared1.ID), } app.db.Save(machineInShared1) @@ -142,7 +142,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared2, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.2", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, AuthKeyID: uint(preAuthKeyInShared2.ID), } app.db.Save(machineInShared2) @@ -160,7 +160,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared3, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.3", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, AuthKeyID: uint(preAuthKeyInShared3.ID), } app.db.Save(machineInShared3) @@ -178,7 +178,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(PreAuthKey2InShared1.ID), } app.db.Save(machine2InShared1) @@ -273,7 +273,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(preAuthKeyInShared1.ID), } app.db.Save(machineInShared1) @@ -291,7 +291,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared2, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.2", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, AuthKeyID: uint(preAuthKeyInShared2.ID), } app.db.Save(machineInShared2) @@ -309,7 +309,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared3, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.3", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, AuthKeyID: uint(preAuthKeyInShared3.ID), } app.db.Save(machineInShared3) @@ -327,7 +327,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(preAuthKey2InShared1.ID), } app.db.Save(machine2InShared1) diff --git a/integration_test.go b/integration_test.go index fc71ed15..ade85bfe 100644 --- a/integration_test.go +++ b/integration_test.go @@ -372,70 +372,74 @@ func (s *IntegrationTestSuite) TestListNodes() { func (s *IntegrationTestSuite) TestGetIpAddresses() { for _, scales := range s.namespaces { - ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10") ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) - for hostname := range scales.tailscales { - s.T().Run(hostname, func(t *testing.T) { - ip, ok := ips[hostname] + for hostname, _ := range scales.tailscales { + ips := ips[hostname] + for _, ip := range ips { + s.T().Run(hostname, func(t *testing.T) { + assert.NotNil(t, ip) - assert.True(t, ok) - assert.NotNil(t, ip) + fmt.Printf("IP for %s: %s\n", hostname, ip) - fmt.Printf("IP for %s: %s\n", hostname, ip) - - // c.Assert(ip.Valid(), check.IsTrue) - assert.True(t, ip.Is4()) - assert.True(t, ipPrefix.Contains(ip)) - }) + // c.Assert(ip.Valid(), check.IsTrue) + assert.True(t, ip.Is4() || ip.Is6()) + switch { + case ip.Is4(): + assert.True(t, IpPrefix4.Contains(ip)) + case ip.Is6(): + assert.True(t, IpPrefix6.Contains(ip)) + } + }) + } } } } // TODO(kradalby): fix this test -// We need some way to impot ipnstate.Status from multiple go packages. +// We need some way to import ipnstate.Status from multiple go packages. // Currently it will only work with 1.18.x since that is the last // version we have in go.mod // func (s *IntegrationTestSuite) TestStatus() { -// for _, scales := range s.namespaces { -// ips, err := getIPs(scales.tailscales) -// assert.Nil(s.T(), err) +// for _, scales := range s.namespaces { +// ips, err := getIPs(scales.tailscales) +// assert.Nil(s.T(), err) // -// for hostname, tailscale := range scales.tailscales { -// s.T().Run(hostname, func(t *testing.T) { -// command := []string{"tailscale", "status", "--json"} +// for hostname, tailscale := range scales.tailscales { +// s.T().Run(hostname, func(t *testing.T) { +// command := []string{"tailscale", "status", "--json"} // -// fmt.Printf("Getting status for %s\n", hostname) -// result, err := ExecuteCommand( -// &tailscale, -// command, -// []string{}, -// ) -// assert.Nil(t, err) +// fmt.Printf("Getting status for %s\n", hostname) +// result, err := ExecuteCommand( +// &tailscale, +// command, +// []string{}, +// ) +// assert.Nil(t, err) // -// var status ipnstate.Status -// err = json.Unmarshal([]byte(result), &status) -// assert.Nil(s.T(), err) +// var status ipnstate.Status +// err = json.Unmarshal([]byte(result), &status) +// assert.Nil(s.T(), err) // -// // TODO(kradalby): Replace this check with peer length of SAME namespace -// // Check if we have as many nodes in status -// // as we have IPs/tailscales -// // lines := strings.Split(result, "\n") -// // assert.Equal(t, len(ips), len(lines)-1) -// // assert.Equal(t, len(scales.tailscales), len(lines)-1) +// // TODO(kradalby): Replace this check with peer length of SAME namespace +// // Check if we have as many nodes in status +// // as we have IPs/tailscales +// // lines := strings.Split(result, "\n") +// // assert.Equal(t, len(ips), len(lines)-1) +// // assert.Equal(t, len(scales.tailscales), len(lines)-1) // -// peerIps := getIPsfromIPNstate(status) +// peerIps := getIPsfromIPNstate(status) // -// // Check that all hosts is present in all hosts status -// for ipHostname, ip := range ips { -// if hostname != ipHostname { -// assert.Contains(t, peerIps, ip) -// } -// } -// }) -// } -// } +// // Check that all hosts is present in all hosts status +// for ipHostname, ip := range ips { +// if hostname != ipHostname { +// assert.Contains(t, peerIps, ip) +// } +// } +// }) +// } +// } // } func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP { @@ -448,16 +452,19 @@ func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP { return ips } -func (s *IntegrationTestSuite) TestPingAllPeers() { +func (s *IntegrationTestSuite) TestPingAllPeersByAddress() { for _, scales := range s.namespaces { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) for hostname, tailscale := range scales.tailscales { - for peername, ip := range ips { - s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + for peername, peerIPs := range ips { + for i, ip := range peerIPs { // We currently cant ping ourselves, so skip that. - if peername != hostname { + if peername == hostname { + continue + } + s.T().Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) { // We are only interested in "direct ping" which means what we // might need a couple of more attempts before reaching the node. command := []string{ @@ -469,9 +476,8 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { } fmt.Printf( - "Pinging from %s (%s) to %s (%s)\n", + "Pinging from %s to %s (%s)\n", hostname, - ips[hostname], peername, ip, ) @@ -483,8 +489,8 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { assert.Nil(t, err) fmt.Printf("Result for %s: %s\n", hostname, result) assert.Contains(t, result, "pong") - } - }) + }) + } } } } @@ -553,17 +559,17 @@ func (s *IntegrationTestSuite) TestSharedNodes() { // TODO(juanfont): We have to find out why do we need to wait time.Sleep(100 * time.Second) // Wait for the nodes to receive updates - mainIps, err := getIPs(main.tailscales) - assert.Nil(s.T(), err) - sharedIps, err := getIPs(shared.tailscales) assert.Nil(s.T(), err) for hostname, tailscale := range main.tailscales { - for peername, ip := range sharedIps { - s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + for peername, peerIPs := range sharedIps { + for i, ip := range peerIPs { // We currently cant ping ourselves, so skip that. - if peername != hostname { + if peername == hostname { + continue + } + s.T().Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) { // We are only interested in "direct ping" which means what we // might need a couple of more attempts before reaching the node. command := []string{ @@ -575,9 +581,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() { } fmt.Printf( - "Pinging from %s (%s) to %s (%s)\n", + "Pinging from %s to %s (%s)\n", hostname, - mainIps[hostname], peername, ip, ) @@ -589,8 +594,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() { assert.Nil(t, err) fmt.Printf("Result for %s: %s\n", hostname, result) assert.Contains(t, result, "pong") - } - }) + }) + } } } } @@ -607,7 +612,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { _, err := ExecuteCommand( &tailscale, command, - []string{}, + []string{"GOMAXPROCS=32"}, ) assert.Nil(s.T(), err) for peername, ip := range ips { @@ -653,7 +658,7 @@ func (s *IntegrationTestSuite) TestTailDrop() { _, err = ExecuteCommand( &tailscale, command, - []string{"ALL_PROXY=socks5://localhost:1055"}, + []string{"ALL_PROXY=socks5://localhost:1055", "GOMAXPROCS=32"}, ) if err == nil { break @@ -684,78 +689,125 @@ func (s *IntegrationTestSuite) TestTailDrop() { ) assert.Nil(s.T(), err) for peername, ip := range ips { + if peername == hostname { + continue + } s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - if peername != hostname { - command := []string{ - "ls", - fmt.Sprintf("/tmp/file_from_%s", peername), - } - fmt.Printf( - "Checking file in %s (%s) from %s (%s)\n", - hostname, - ips[hostname], - peername, - ip, - ) - result, err := ExecuteCommand( - &tailscale, - command, - []string{}, - ) - assert.Nil(t, err) - fmt.Printf("Result for %s: %s\n", peername, result) - assert.Equal( - t, - result, - fmt.Sprintf("/tmp/file_from_%s\n", peername), - ) + command := []string{ + "ls", + fmt.Sprintf("/tmp/file_from_%s", peername), } + fmt.Printf( + "Checking file in %s (%s) from %s (%s)\n", + hostname, + ips[hostname], + peername, + ip, + ) + result, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", peername, result) + assert.Equal( + t, + fmt.Sprintf("/tmp/file_from_%s\n", peername), + result, + ) }) } } } } -func (s *IntegrationTestSuite) TestMagicDNS() { +func (s *IntegrationTestSuite) TestPingAllPeersByHostname() { for namespace, scales := range s.namespaces { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) for hostname, tailscale := range scales.tailscales { - for peername, ip := range ips { + for peername, _ := range ips { + if peername == hostname { + continue + } s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - if peername != hostname { - command := []string{ - "tailscale", "ping", - "--timeout=10s", - "--c=20", - "--until-direct=true", - fmt.Sprintf("%s.%s.headscale.net", peername, namespace), - } - - fmt.Printf( - "Pinging using Hostname (magicdns) from %s (%s) to %s (%s)\n", - hostname, - ips[hostname], - peername, - ip, - ) - result, err := ExecuteCommand( - &tailscale, - command, - []string{}, - ) - assert.Nil(t, err) - fmt.Printf("Result for %s: %s\n", hostname, result) - assert.Contains(t, result, "pong") + command := []string{ + "tailscale", "ping", + "--timeout=10s", + "--c=20", + "--until-direct=true", + fmt.Sprintf("%s.%s.headscale.net", peername, namespace), } + + fmt.Printf( + "Pinging using Hostname from %s to %s\n", + hostname, + peername, + ) + result, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", hostname, result) + assert.Contains(t, result, "pong") }) } } } } -func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) { - ips := make(map[string]netaddr.IP) +// TODO: +// * With manual testing, MagicDNS does not respond to AAAA queries. Why? +// * Tailscaled only adds a route to the IPv4 (100.100.100.100) address of the MagicDNS service, +// event though there is an IPv6 one (fd7a:115c:a1e0::53) as well. +func (s *IntegrationTestSuite) TestMagicDNSv4() { + for namespace, scales := range s.namespaces { + ips, err := getIPs(scales.tailscales) + assert.Nil(s.T(), err) + for hostname, tailscale := range scales.tailscales { + for peername, ips := range ips { + if peername == hostname { + continue + } + s.T().Run(fmt.Sprintf("%s-%s-ipv4", hostname, peername), func(t *testing.T) { + command := []string{ + "host", "-4", "-t", "A", + fmt.Sprintf("%s.%s.headscale.net", peername, namespace), + "100.100.100.100", + } + + fmt.Printf( + "Resolving name %s (IPv4) from %s over IPv4\n", + peername, + hostname, + ) + result, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", hostname, result) + + resolved := false + for _, ip := range ips { + if strings.Contains(result, fmt.Sprintf("has address %s", ip.String())) { + resolved = true + break + } + } + assert.Equal(t, true, resolved) + }) + } + } + } +} + +func getIPs(tailscales map[string]dockertest.Resource) (map[string][]netaddr.IP, error) { + ips := make(map[string][]netaddr.IP) for hostname, tailscale := range tailscales { command := []string{"tailscale", "ip"} @@ -768,12 +820,17 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e return nil, err } - ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n")) - if err != nil { - return nil, err + for _, address := range strings.Split(result, "\n") { + address = strings.TrimSuffix(address, "\n") + if len(address) < 1 { + continue + } + ip, err := netaddr.ParseIP(address) + if err != nil { + return nil, err + } + ips[hostname] = append(ips[hostname], ip) } - - ips[hostname] = ip } return ips, nil diff --git a/machine.go b/machine.go index c13d79d7..d94fb004 100644 --- a/machine.go +++ b/machine.go @@ -1,6 +1,7 @@ package headscale import ( + "database/sql/driver" "encoding/json" "errors" "fmt" @@ -23,6 +24,7 @@ const ( errMachineNotFound = Error("machine not found") errMachineAlreadyRegistered = Error("machine already registered") errMachineRouteIsNotAvailable = Error("route is not available on machine") + errMachineAddressesInvalid = Error("failed to parse machine addresses") ) // Machine is a Headscale client. @@ -31,7 +33,7 @@ type Machine struct { MachineKey string `gorm:"type:varchar(64);unique_index"` NodeKey string DiscoKey string - IPAddress string + IPAddresses MachineAddresses Name string NamespaceID uint Namespace Namespace `gorm:"foreignKey:NamespaceID"` @@ -64,6 +66,47 @@ func (machine Machine) isRegistered() bool { return machine.Registered } +type MachineAddresses []netaddr.IP + +func (ma MachineAddresses) ToStringSlice() []string { + strSlice := make([]string, 0, len(ma)) + for _, addr := range ma { + strSlice = append(strSlice, addr.String()) + } + + return strSlice +} + +func (ma *MachineAddresses) Scan(destination interface{}) error { + switch value := destination.(type) { + case string: + addresses := strings.Split(value, ",") + *ma = (*ma)[:0] + for _, addr := range addresses { + if len(addr) < 1 { + continue + } + parsed, err := netaddr.ParseIP(addr) + if err != nil { + return err + } + *ma = append(*ma, parsed) + } + + return nil + + default: + return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination) + } +} + +// Value return json value, implement driver.Valuer interface. +func (ma MachineAddresses) Value() (driver.Value, error) { + addresses := strings.Join(ma.ToStringSlice(), ",") + + return addresses, nil +} + // isExpired returns whether the machine registration has expired. func (machine Machine) isExpired() bool { // If Expiry is not set, the client has not indicated that @@ -470,22 +513,12 @@ func (machine Machine) toNode( } addrs := []netaddr.IPPrefix{} - nodeAddr, err := netaddr.ParseIP(m.IPAddresses) - if err != nil { - log.Trace(). - Caller(). - Str("ip", machine.IPAddresses). - Msgf("Failed to parse machine IP: %s", machine.IPAddresses) - return nil, err + for _, machineAddress := range machine.IPAddresses { + ip := netaddr.IPPrefixFrom(machineAddress, machineAddress.BitLen()) + addrs = append(addrs, ip) } - ip := netaddr.IPPrefixFrom(nodeAddr, nodeAddr.BitLen()) - addrs = append(addrs, ip) - allowedIPs := []netaddr.IPPrefix{} - allowedIPs = append( - allowedIPs, - ip, - ) // we append the node own IP, as it is required by the clients + allowedIPs := append([]netaddr.IPPrefix{}, addrs...) // we append the node own IP, as it is required by the clients if includeRoutes { routesStr := []string{} @@ -592,11 +625,11 @@ func (machine *Machine) toProto() *v1.Machine { Id: machine.ID, MachineKey: machine.MachineKey, - NodeKey: machine.NodeKey, - DiscoKey: machine.DiscoKey, - IpAddress: machine.IPAddress, - Name: machine.Name, - Namespace: machine.Namespace.toProto(), + NodeKey: machine.NodeKey, + DiscoKey: machine.DiscoKey, + IpAddresses: machine.IPAddresses.ToStringSlice(), + Name: machine.Name, + Namespace: machine.Namespace.toProto(), Registered: machine.Registered, @@ -695,7 +728,7 @@ func (h *Headscale) RegisterMachine( return nil, err } - ip, err := h.getAvailableIP() + ips, err := h.getAvailableIPs() if err != nil { log.Error(). Caller(). @@ -709,10 +742,10 @@ func (h *Headscale) RegisterMachine( log.Trace(). Caller(). Str("machine", machine.Name). - Str("ip", ip.String()). + Str("ip", strings.Join(ips.ToStringSlice(), ",")). Msg("Found IP for host") - machine.IPAddress = ip.String() + machine.IPAddresses = ips machine.NamespaceID = namespace.ID machine.Registered = true machine.RegisterMethod = RegisterMethodCLI @@ -722,7 +755,7 @@ func (h *Headscale) RegisterMachine( log.Trace(). Caller(). Str("machine", machine.Name). - Str("ip", ip.String()). + Str("ip", strings.Join(ips.ToStringSlice(), ",")). Msg("Machine registered with the database") return machine, nil diff --git a/machine_test.go b/machine_test.go index eb090072..ecb50de3 100644 --- a/machine_test.go +++ b/machine_test.go @@ -6,6 +6,7 @@ import ( "time" "gopkg.in/check.v1" + "inet.af/netaddr" ) func (s *Suite) TestGetMachine(c *check.C) { @@ -199,3 +200,22 @@ func (s *Suite) TestExpireMachine(c *check.C) { c.Assert(machineFromDB.isExpired(), check.Equals, true) } + +func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) { + input := MachineAddresses([]netaddr.IP{ + netaddr.MustParseIP("192.0.2.1"), + netaddr.MustParseIP("2001:db8::1"), + }) + serialized, err := input.Value() + c.Assert(err, check.IsNil) + c.Assert(serialized.(string), check.Equals, "192.0.2.1,2001:db8::1") + + var deserialized MachineAddresses + err = deserialized.Scan(serialized) + c.Assert(err, check.IsNil) + + c.Assert(len(deserialized), check.Equals, len(input)) + for i := range deserialized { + c.Assert(deserialized[i], check.Equals, input[i]) + } +} diff --git a/namespaces_test.go b/namespaces_test.go index 9793e608..d07deb96 100644 --- a/namespaces_test.go +++ b/namespaces_test.go @@ -4,6 +4,7 @@ import ( "github.com/rs/zerolog/log" "gopkg.in/check.v1" "gorm.io/gorm" + "inet.af/netaddr" ) func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) { @@ -146,7 +147,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(preAuthKeyShared1.ID), } app.db.Save(machineInShared1) @@ -164,7 +165,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared2, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.2", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, AuthKeyID: uint(preAuthKeyShared2.ID), } app.db.Save(machineInShared2) @@ -182,7 +183,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared3, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.3", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, AuthKeyID: uint(preAuthKeyShared3.ID), } app.db.Save(machineInShared3) @@ -200,7 +201,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(preAuthKey2Shared1.ID), } app.db.Save(machine2InShared1) diff --git a/oidc.go b/oidc.go index 120a4cff..a47863ff 100644 --- a/oidc.go +++ b/oidc.go @@ -126,6 +126,7 @@ var oidcCallbackTemplate = template.Must( `), ) +// TODO: Why is the entire machine registration logic duplicated here? // OIDCCallback handles the callback from the OIDC endpoint // Retrieves the mkey from the state cache and adds the machine to the users email namespace // TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities @@ -316,7 +317,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) { return } - ip, err := h.getAvailableIP() + ips, err := h.getAvailableIPs() if err != nil { log.Error(). Caller(). @@ -330,7 +331,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) { return } - machine.IPAddress = ip.String() + machine.IPAddresses = ips machine.NamespaceID = namespace.ID machine.Registered = true machine.RegisterMethod = RegisterMethodOIDC diff --git a/sharing_test.go b/sharing_test.go index fd7634da..b7fef4e2 100644 --- a/sharing_test.go +++ b/sharing_test.go @@ -2,6 +2,7 @@ package headscale import ( "gopkg.in/check.v1" + "inet.af/netaddr" ) func CreateNodeNamespace( @@ -26,7 +27,7 @@ func CreateNodeNamespace( NamespaceID: namespace.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: ip, + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(pak1.ID), } app.db.Save(machine) @@ -214,7 +215,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { NamespaceID: namespace1.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(pak4.ID), } app.db.Save(machine4) @@ -294,7 +295,7 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) { NamespaceID: namespace1.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(pak4n1.ID), } app.db.Save(machine4) diff --git a/utils.go b/utils.go index d2b6b902..3b18a406 100644 --- a/utils.go +++ b/utils.go @@ -133,9 +133,24 @@ func encode( return privKey.SealTo(*pubKey, b), nil } -func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { - ipPrefix := h.cfg.IPPrefix +func (h *Headscale) getAvailableIPs() (ips MachineAddresses, err error) { + ipPrefixes := h.cfg.IPPrefixes + for _, ipPrefix := range ipPrefixes { + var ip *netaddr.IP + ip, err = h.getAvailableIP(ipPrefix) + if err != nil { + return + } + ips = append(ips, *ip) + } + return +} + +// TODO: Is this concurrency safe? +// What would happen if multiple hosts were to register at the same time? +// Would we attempt to assign the same addresses to multiple nodes? +func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, error) { usedIps, err := h.getUsedIPs() if err != nil { return nil, err @@ -143,6 +158,7 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { ipPrefixNetworkAddress, ipPrefixBroadcastAddress := func() (netaddr.IP, netaddr.IP) { ipRange := ipPrefix.Range() + return ipRange.From(), ipRange.To() }() @@ -171,19 +187,20 @@ func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { } func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { - var addresses []string - h.db.Model(&Machine{}).Pluck("ip_address", &addresses) + // FIXME: This really deserves a better data model, + // but this was quick to get running and it should be enough + // to begin experimenting with a dual stack tailnet. + var addressesSlices []string + h.db.Model(&Machine{}).Pluck("ip_addresses", &addressesSlices) - ips := make([]netaddr.IP, len(addresses)) - for index, addr := range addresses { - if addr != "" { - ip, err := netaddr.ParseIP(addr) - if err != nil { - return nil, fmt.Errorf("failed to parse ip from database: %w", err) - } - - ips[index] = ip + ips := make([]netaddr.IP, 0, len(h.cfg.IPPrefixes)*len(addressesSlices)) + for _, slice := range addressesSlices { + var a MachineAddresses + err := a.Scan(slice) + if err != nil { + return nil, fmt.Errorf("failed to read ip from database: %w", err) } + ips = append(ips, a...) } return ips, nil diff --git a/utils_test.go b/utils_test.go index 9b0295b3..feb44d5a 100644 --- a/utils_test.go +++ b/utils_test.go @@ -6,17 +6,18 @@ import ( ) func (s *Suite) TestGetAvailableIp(c *check.C) { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) expected := netaddr.MustParseIP("10.27.0.1") - c.Assert(ip.String(), check.Equals, expected.String()) + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0].String(), check.Equals, expected.String()) } func (s *Suite) TestGetUsedIps(c *check.C) { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) namespace, err := app.CreateNamespace("test_ip") @@ -38,22 +39,24 @@ func (s *Suite) TestGetUsedIps(c *check.C) { Registered: true, RegisterMethod: RegisterMethodAuthKey, AuthKeyID: uint(pak.ID), - IPAddress: ip.String(), + IPAddresses: ips, } app.db.Save(&machine) - ips, err := app.getUsedIPs() + usedIps, err := app.getUsedIPs() c.Assert(err, check.IsNil) expected := netaddr.MustParseIP("10.27.0.1") - c.Assert(ips[0], check.Equals, expected) + c.Assert(len(usedIps), check.Equals, 1) + c.Assert(usedIps[0], check.Equals, expected) machine1, err := app.GetMachineByID(0) c.Assert(err, check.IsNil) - c.Assert(machine1.IPAddress, check.Equals, expected.String()) + c.Assert(len(machine1.IPAddresses), check.Equals, 1) + c.Assert(machine1.IPAddresses[0], check.Equals, expected) } func (s *Suite) TestGetMultiIp(c *check.C) { @@ -61,7 +64,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(err, check.IsNil) for index := 1; index <= 350; index++ { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) @@ -80,59 +83,64 @@ func (s *Suite) TestGetMultiIp(c *check.C) { Registered: true, RegisterMethod: RegisterMethodAuthKey, AuthKeyID: uint(pak.ID), - IPAddress: ip.String(), + IPAddresses: ips, } app.db.Save(&machine) } - ips, err := app.getUsedIPs() + usedIps, err := app.getUsedIPs() c.Assert(err, check.IsNil) - c.Assert(len(ips), check.Equals, 350) + c.Assert(len(usedIps), check.Equals, 350) - c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) - c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) - c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.45")) + c.Assert(usedIps[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) + c.Assert(usedIps[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) + c.Assert(usedIps[300], check.Equals, netaddr.MustParseIP("10.27.1.45")) // Check that we can read back the IPs machine1, err := app.GetMachineByID(1) c.Assert(err, check.IsNil) + c.Assert(len(machine1.IPAddresses), check.Equals, 1) c.Assert( - machine1.IPAddress, + machine1.IPAddresses[0], check.Equals, - netaddr.MustParseIP("10.27.0.1").String(), + netaddr.MustParseIP("10.27.0.1"), ) machine50, err := app.GetMachineByID(50) c.Assert(err, check.IsNil) + c.Assert(len(machine50.IPAddresses), check.Equals, 1) c.Assert( - machine50.IPAddress, + machine50.IPAddresses[0], check.Equals, - netaddr.MustParseIP("10.27.0.50").String(), + netaddr.MustParseIP("10.27.0.50"), ) expectedNextIP := netaddr.MustParseIP("10.27.1.95") - nextIP, err := app.getAvailableIP() + nextIP, err := app.getAvailableIPs() c.Assert(err, check.IsNil) - c.Assert(nextIP.String(), check.Equals, expectedNextIP.String()) + c.Assert(len(nextIP), check.Equals, 1) + c.Assert(nextIP[0].String(), check.Equals, expectedNextIP.String()) // If we call get Available again, we should receive // the same IP, as it has not been reserved. - nextIP2, err := app.getAvailableIP() + nextIP2, err := app.getAvailableIPs() c.Assert(err, check.IsNil) - c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String()) + c.Assert(len(nextIP2), check.Equals, 1) + c.Assert(nextIP2[0].String(), check.Equals, expectedNextIP.String()) } func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) expected := netaddr.MustParseIP("10.27.0.1") - c.Assert(ip.String(), check.Equals, expected.String()) + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0].String(), check.Equals, expected.String()) namespace, err := app.CreateNamespace("test_ip") c.Assert(err, check.IsNil) @@ -156,8 +164,9 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { } app.db.Save(&machine) - ip2, err := app.getAvailableIP() + ips2, err := app.getAvailableIPs() c.Assert(err, check.IsNil) - c.Assert(ip2.String(), check.Equals, expected.String()) + c.Assert(len(ips2), check.Equals, 1) + c.Assert(ips2[0].String(), check.Equals, expected.String()) } From 115d0cbe855ef526148ba0191ca2df14106779c1 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 15 Jan 2022 16:18:49 +0100 Subject: [PATCH 06/32] dns: IPv6 roots generation --- dns.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++----- dns_test.go | 46 +++++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 10 deletions(-) diff --git a/dns.go b/dns.go index 5a4f682a..df896090 100644 --- a/dns.go +++ b/dns.go @@ -14,6 +14,11 @@ const ( ByteSize = 8 ) +const ( + ipv4AddressLength = 32 + ipv6AddressLength = 128 +) + // generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`. // This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS // server (listening in 100.100.100.100 udp/53) should be used for. @@ -37,22 +42,25 @@ const ( func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN { fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes)) for _, ipPrefix := range ipPrefixes { - var generateDnsRoot func(netaddr.IPPrefix) []dnsname.FQDN + var generateDNSRoot func(netaddr.IPPrefix) []dnsname.FQDN switch ipPrefix.IP().BitLen() { - case 32: - generateDnsRoot = generateIPv4DNSRootDomain + case ipv4AddressLength: + generateDNSRoot = generateIPv4DNSRootDomain + + case ipv6AddressLength: + generateDNSRoot = generateIPv6DNSRootDomain default: panic(fmt.Sprintf("unsupported IP version with address length %d", ipPrefix.IP().BitLen())) } - fqdns = append(fqdns, generateDnsRoot(ipPrefix)...) + fqdns = append(fqdns, generateDNSRoot(ipPrefix)...) } return fqdns } -func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) (fqdns []dnsname.FQDN) { +func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN { // Conversion to the std lib net.IPnet, a bit easier to operate netRange := ipPrefix.IPNet() maskBits, _ := netRange.Mask.Size() @@ -76,6 +84,7 @@ func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) (fqdns []dnsname.FQDN) rdnsSlice = append(rdnsSlice, "in-addr.arpa.") rdnsBase := strings.Join(rdnsSlice, ".") + fqdns := make([]dnsname.FQDN, 0, max-min+1) for i := min; i <= max; i++ { fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase)) if err != nil { @@ -84,7 +93,55 @@ func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) (fqdns []dnsname.FQDN) fqdns = append(fqdns, fqdn) } - return + return fqdns +} + +func generateIPv6DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN { + const nibbleLen = 4 + + maskBits, _ := ipPrefix.IPNet().Mask.Size() + expanded := ipPrefix.IP().StringExpanded() + nibbleStr := strings.Map(func(r rune) rune { + if r == ':' { + return -1 + } + + return r + }, expanded) + + // TODO?: that does not look the most efficient implementation, + // but the inputs are not so long as to cause problems, + // and from what I can see, the generateMagicDNSRootDomains + // function is called only once over the lifetime of a server process. + prefixConstantParts := []string{} + for i := 0; i < maskBits/nibbleLen; i++ { + prefixConstantParts = append([]string{string(nibbleStr[i])}, prefixConstantParts...) + } + + makeDomain := func(variablePrefix ...string) (dnsname.FQDN, error) { + prefix := strings.Join(append(variablePrefix, prefixConstantParts...), ".") + + return dnsname.ToFQDN(fmt.Sprintf("%s.ip6.arpa", prefix)) + } + + var fqdns []dnsname.FQDN + if maskBits%4 == 0 { + dom, _ := makeDomain() + fqdns = append(fqdns, dom) + } else { + domCount := 1 << (maskBits % nibbleLen) + fqdns = make([]dnsname.FQDN, 0, domCount) + for i := 0; i < domCount; i++ { + varNibble := fmt.Sprintf("%x", i) + dom, err := makeDomain(varNibble) + if err != nil { + continue + } + fqdns = append(fqdns, dom) + } + } + + return fqdns } func getMapResponseDNSConfig( diff --git a/dns_test.go b/dns_test.go index 8c0da638..f8f7fb96 100644 --- a/dns_test.go +++ b/dns_test.go @@ -10,8 +10,10 @@ import ( ) func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { - prefix := netaddr.MustParseIPPrefix("100.64.0.0/10") - domains := generateMagicDNSRootDomains(prefix) + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("100.64.0.0/10"), + } + domains := generateMagicDNSRootDomains(prefixes) found := false for _, domain := range domains { @@ -45,8 +47,10 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { } func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { - prefix := netaddr.MustParseIPPrefix("172.16.0.0/16") - domains := generateMagicDNSRootDomains(prefix) + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("172.16.0.0/16"), + } + domains := generateMagicDNSRootDomains(prefixes) found := false for _, domain := range domains { @@ -69,6 +73,40 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { c.Assert(found, check.Equals, true) } +// Happens when netmask is a multiple of 4 bits (sounds likely). +func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) { + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48"), + } + domains := generateMagicDNSRootDomains(prefixes) + + c.Assert(len(domains), check.Equals, 1) + c.Assert(domains[0].WithTrailingDot(), check.Equals, "0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") +} + +func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) { + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/50"), + } + domains := generateMagicDNSRootDomains(prefixes) + + yieldsRoot := func(dom string) bool { + for _, candidate := range domains { + if candidate.WithTrailingDot() == dom { + return true + } + } + + return false + } + + c.Assert(len(domains), check.Equals, 4) + c.Assert(yieldsRoot("0.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("1.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("2.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("3.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) +} + func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { namespaceShared1, err := app.CreateNamespace("shared1") c.Assert(err, check.IsNil) From d35fb8bba0a34c96055f77072b087d14e50f58ae Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Fri, 31 Dec 2021 17:35:17 +0100 Subject: [PATCH 07/32] integration-test: add IPv6 prefix to configuration --- integration_test/etc/config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml index 6f68f304..63af7ebb 100644 --- a/integration_test/etc/config.yaml +++ b/integration_test/etc/config.yaml @@ -2,6 +2,9 @@ log_level: trace acl_policy_path: "" db_type: sqlite3 ephemeral_node_inactivity_timeout: 30m +ip_prefixes: + - fd7a:115c:a1e0::/48 + - 100.64.0.0/10 dns_config: base_domain: headscale.net magic_dns: true From a32175f791a3f1e5ffbe9a6db05f6f74b0a2143a Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Fri, 31 Dec 2021 20:51:20 +0100 Subject: [PATCH 08/32] PollNetMapHandler: refactor with chan lifetimes in mind * Resolves an issue where sometimes attempted sends on a closed channel happened by ensuring the channels remain open for the entire goroutine. * May be of help with regards to issue #203 --- Makefile | 2 +- poll.go | 93 +++++++++++++++++++++++++++----------------------------- 2 files changed, 45 insertions(+), 50 deletions(-) diff --git a/Makefile b/Makefile index 6b4c02ff..69935bc0 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ test: @go test -coverprofile=coverage.out ./... test_integration: - go test -tags integration -timeout 30m ./... + go test -tags integration -timeout 30m -count=1 ./... test_integration_cli: go test -tags integration -v integration_cli_test.go integration_common_test.go diff --git a/poll.go b/poll.go index 1d2db944..94c60dc1 100644 --- a/poll.go +++ b/poll.go @@ -1,8 +1,10 @@ package headscale import ( + "context" "encoding/json" "errors" + "fmt" "io" "net/http" "time" @@ -152,14 +154,33 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { Str("id", ctx.Param("id")). Str("machine", machine.Name). Msg("Loading or creating update channel") - updateChan := make(chan struct{}) - pollDataChan := make(chan []byte) + // TODO: could probably remove all that duplication once generics land. + closeChanWithLog := func(channel interface{}, name string) { + log.Trace(). + Str("handler", "PollNetMap"). + Str("machine", machine.Name). + Str("channel", "Done"). + Msg(fmt.Sprintf("Closing %s channel", name)) + + switch c := channel.(type) { + case (chan struct{}): + close(c) + + case (chan []byte): + close(c) + } + } + + const chanSize = 8 + updateChan := make(chan struct{}, chanSize) + defer closeChanWithLog(updateChan, "updateChan") + + pollDataChan := make(chan []byte, chanSize) + defer closeChanWithLog(pollDataChan, "pollDataChan") keepAliveChan := make(chan []byte) - - cancelKeepAlive := make(chan struct{}) - defer close(cancelKeepAlive) + defer closeChanWithLog(keepAliveChan, "keepAliveChan") if req.OmitPeers && !req.Stream { log.Info(). @@ -172,7 +193,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { // even tho the comments in the tailscale code dont explicitly say so. updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "endpoint-update"). Inc() - go func() { updateChan <- struct{}{} }() + updateChan <- struct{}{} return } else if req.OmitPeers && req.Stream { @@ -193,7 +214,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { Str("handler", "PollNetMap"). Str("machine", machine.Name). Msg("Sending initial map") - go func() { pollDataChan <- data }() + pollDataChan <- data log.Info(). Str("handler", "PollNetMap"). @@ -201,7 +222,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { Msg("Notifying peers") updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "full-update"). Inc() - go func() { updateChan <- struct{}{} }() + updateChan <- struct{}{} h.PollNetMapStream( ctx, @@ -211,7 +232,6 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { pollDataChan, keepAliveChan, updateChan, - cancelKeepAlive, ) log.Trace(). Str("handler", "PollNetMap"). @@ -231,16 +251,20 @@ func (h *Headscale) PollNetMapStream( pollDataChan chan []byte, keepAliveChan chan []byte, updateChan chan struct{}, - cancelKeepAlive chan struct{}, ) { - go h.scheduledPollWorker( - cancelKeepAlive, - updateChan, - keepAliveChan, - machineKey, - mapRequest, - machine, - ) + { + ctx, cancel := context.WithCancel(ctx.Request.Context()) + defer cancel() + + go h.scheduledPollWorker( + ctx, + updateChan, + keepAliveChan, + machineKey, + mapRequest, + machine, + ) + } ctx.Stream(func(writer io.Writer) bool { log.Trace(). @@ -455,42 +479,13 @@ func (h *Headscale) PollNetMapStream( machine.LastSeen = &now h.db.Save(&machine) - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Cancelling keepAlive channel") - cancelKeepAlive <- struct{}{} - - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Closing update channel") - // h.closeUpdateChannel(m) - close(updateChan) - - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Closing pollData channel") - close(pollDataChan) - - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Closing keepAliveChan channel") - close(keepAliveChan) - return false } }) } func (h *Headscale) scheduledPollWorker( - cancelChan <-chan struct{}, + ctx context.Context, updateChan chan<- struct{}, keepAliveChan chan<- []byte, machineKey key.MachinePublic, @@ -502,7 +497,7 @@ func (h *Headscale) scheduledPollWorker( for { select { - case <-cancelChan: + case <-ctx.Done(): return case <-keepAliveTicker.C: From 8f632e9062cd8b8c29db973fcb14e9e4925c1428 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 15 Jan 2022 16:20:14 +0100 Subject: [PATCH 09/32] machine: isOutdated: handle machines without LastSuccefulUpdate set --- machine.go | 10 +++++++--- poll.go | 12 ++++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/machine.go b/machine.go index d94fb004..d0be2df7 100644 --- a/machine.go +++ b/machine.go @@ -420,14 +420,18 @@ func (h *Headscale) isOutdated(machine *Machine) bool { } lastChange := h.getLastStateChange(namespaces...) + lastUpdate := machine.CreatedAt + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } log.Trace(). Caller(). Str("machine", machine.Name). - Time("last_successful_update", *machine.LastSuccessfulUpdate). - Time("last_state_change", lastChange). + Time("last_successful_update", lastChange). + Time("last_state_change", lastUpdate). Msgf("Checking if %s is missing updates", machine.Name) - return machine.LastSuccessfulUpdate.Before(lastChange) + return lastUpdate.Before(lastChange) } func (machine Machine) String() string { diff --git a/poll.go b/poll.go index 94c60dc1..883aa621 100644 --- a/poll.go +++ b/poll.go @@ -388,10 +388,14 @@ func (h *Headscale) PollNetMapStream( updateRequestsReceivedOnChannel.WithLabelValues(machine.Name, machine.Namespace.Name). Inc() if h.isOutdated(machine) { + var lastUpdate time.Time + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } log.Debug(). Str("handler", "PollNetMapStream"). Str("machine", machine.Name). - Time("last_successful_update", *machine.LastSuccessfulUpdate). + Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Msgf("There has been updates since the last successful update to %s", machine.Name) data, err := h.getMapResponse(machineKey, mapRequest, machine) @@ -448,10 +452,14 @@ func (h *Headscale) PollNetMapStream( h.db.Save(&machine) } else { + var lastUpdate time.Time + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", machine.Name). - Time("last_successful_update", *machine.LastSuccessfulUpdate). + Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Msgf("%s is up to date", machine.Name) } From ed39b91f717e06e9b76f6dba8c68ba0d0b0b179a Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 15 Jan 2022 16:04:00 +0100 Subject: [PATCH 10/32] Dockerfiles: specify origin registry explicitly --- Dockerfile | 2 +- Dockerfile.alpine | 4 ++-- Dockerfile.debug | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0d344268..75fd0c5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Builder image -FROM golang:1.17.1-bullseye AS build +FROM docker.io/golang:1.17.1-bullseye AS build ENV GOPATH /go WORKDIR /go/src/headscale diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 7d90e1ec..8e0ef6d2 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,5 +1,5 @@ # Builder image -FROM golang:1.17.1-alpine AS build +FROM docker.io/golang:1.17.1-alpine AS build ENV GOPATH /go WORKDIR /go/src/headscale @@ -13,7 +13,7 @@ RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_ex RUN test -e /go/bin/headscale # Production image -FROM alpine:latest +FROM docker.io/alpine:latest COPY --from=build /go/bin/headscale /bin/headscale ENV TZ UTC diff --git a/Dockerfile.debug b/Dockerfile.debug index 320b5c93..b81ee67e 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -1,5 +1,5 @@ # Builder image -FROM golang:1.17.1-bullseye AS build +FROM docker.io/golang:1.17.1-bullseye AS build ENV GOPATH /go WORKDIR /go/src/headscale From 78039f4ceae1efa7c2d5aced42206af146210224 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 15 Jan 2022 16:25:38 +0100 Subject: [PATCH 11/32] integration-test: use TUN devices, enable IPv6 addresses on local interfaces in containers --- integration_common_test.go | 55 +++++++++++++++++++++++++++++++++----- integration_test.go | 6 ++--- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/integration_common_test.go b/integration_common_test.go index 31bae514..94291fc6 100644 --- a/integration_common_test.go +++ b/integration_common_test.go @@ -8,22 +8,48 @@ import ( "fmt" "time" + "inet.af/netaddr" + "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" ) const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second +var IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10") +var IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48") + +type ExecuteCommandConfig struct { + timeout time.Duration +} + +type ExecuteCommandOption func(*ExecuteCommandConfig) error + +func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption { + return ExecuteCommandOption(func(conf *ExecuteCommandConfig) error { + conf.timeout = timeout + return nil + }) +} + func ExecuteCommand( resource *dockertest.Resource, cmd []string, env []string, + options ...ExecuteCommandOption, ) (string, error) { var stdout bytes.Buffer var stderr bytes.Buffer - // TODO(kradalby): Make configurable - timeout := DOCKER_EXECUTE_TIMEOUT + execConfig := ExecuteCommandConfig{ + timeout: DOCKER_EXECUTE_TIMEOUT, + } + + for _, opt := range options { + if err := opt(&execConfig); err != nil { + return "", fmt.Errorf("execute-command/options: %w", err) + } + } type result struct { exitCode int @@ -62,16 +88,33 @@ func ExecuteCommand( } return stdout.String(), nil - case <-time.After(timeout): + case <-time.After(execConfig.timeout): - return "", fmt.Errorf("command timed out after %s", timeout) + return "", fmt.Errorf("command timed out after %s", execConfig.timeout) } } func DockerRestartPolicy(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true + // set AutoRemove to true so that stopped container goes away by itself on error *immediately*. + // when set to false, containers remain until the end of the integration test. + config.AutoRemove = false config.RestartPolicy = docker.RestartPolicy{ Name: "no", } } + +func DockerAllowLocalIPv6(config *docker.HostConfig) { + if config.Sysctls == nil { + config.Sysctls = make(map[string]string, 1) + } + config.Sysctls["net.ipv6.conf.all.disable_ipv6"] = "0" +} + +func DockerAllowNetworkAdministration(config *docker.HostConfig) { + config.CapAdd = append(config.CapAdd, "NET_ADMIN") + config.Mounts = append(config.Mounts, docker.HostMount{ + Type: "bind", + Source: "/dev/net/tun", + Target: "/dev/net/tun", + }) +} diff --git a/integration_test.go b/integration_test.go index ade85bfe..c81749f5 100644 --- a/integration_test.go +++ b/integration_test.go @@ -164,9 +164,7 @@ func (s *IntegrationTestSuite) tailscaleContainer( Name: hostname, Networks: []*dockertest.Network{&s.network}, Cmd: []string{ - "tailscaled", - "--tun=userspace-networking", - "--socks5-server=localhost:1055", + "tailscaled", "--tun=tsdev", }, } @@ -174,6 +172,8 @@ func (s *IntegrationTestSuite) tailscaleContainer( tailscaleBuildOptions, tailscaleOptions, DockerRestartPolicy, + DockerAllowLocalIPv6, + DockerAllowNetworkAdministration, ) if err != nil { log.Fatalf("Could not start resource: %s", err) From beb3e9abc231d6da826b657f3d29cf363b022f16 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 15 Jan 2022 16:28:26 +0100 Subject: [PATCH 12/32] integration-test: taildrop test refactor --- Dockerfile.tailscale | 2 +- integration_test.go | 92 +++++++++++++++++--------------------------- 2 files changed, 36 insertions(+), 58 deletions(-) diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale index df8cdcb1..f96c6b9c 100644 --- a/Dockerfile.tailscale +++ b/Dockerfile.tailscale @@ -7,5 +7,5 @@ RUN apt-get update \ && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \ && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ && apt-get update \ - && apt-get install -y tailscale=${TAILSCALE_VERSION} \ + && apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \ && rm -rf /var/lib/apt/lists/* diff --git a/integration_test.go b/integration_test.go index c81749f5..fd280f88 100644 --- a/integration_test.go +++ b/integration_test.go @@ -604,74 +604,52 @@ func (s *IntegrationTestSuite) TestTailDrop() { for _, scales := range s.namespaces { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) - apiURLs, err := getAPIURLs(scales.tailscales) assert.Nil(s.T(), err) + retry := func(times int, sleepInverval time.Duration, doWork func() error) (err error) { + for attempts := 0; attempts < times; attempts++ { + err = doWork() + if err == nil { + return + } + time.Sleep(sleepInverval) + } + return + } + for hostname, tailscale := range scales.tailscales { command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)} _, err := ExecuteCommand( &tailscale, command, - []string{"GOMAXPROCS=32"}, + []string{}, ) assert.Nil(s.T(), err) - for peername, ip := range ips { + for peername, _ := range ips { + if peername == hostname { + continue + } s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - if peername != hostname { - // Under normal circumstances, we should be able to send a file - // using `tailscale file cp` - but not in userspace networking mode - // So curl! - peerAPI, ok := apiURLs[ip] - assert.True(t, ok) - - // TODO(juanfont): We still have some issues with the test infrastructure, so - // lets run curl multiple times until it works. - attempts := 0 - var err error - for { - command := []string{ - "curl", - "--retry-connrefused", - "--retry-delay", - "30", - "--retry", - "10", - "--connect-timeout", - "60", - "-X", - "PUT", - "--upload-file", - fmt.Sprintf("/tmp/file_from_%s", hostname), - fmt.Sprintf( - "%s/v0/put/file_from_%s", - peerAPI, - hostname, - ), - } - fmt.Printf( - "Sending file from %s (%s) to %s (%s)\n", - hostname, - ips[hostname], - peername, - ip, - ) - _, err = ExecuteCommand( - &tailscale, - command, - []string{"ALL_PROXY=socks5://localhost:1055", "GOMAXPROCS=32"}, - ) - if err == nil { - break - } else { - time.Sleep(10 * time.Second) - attempts++ - if attempts > 10 { - break - } - } - } - assert.Nil(t, err) + command := []string{ + "tailscale", "file", "cp", + fmt.Sprintf("/tmp/file_from_%s", hostname), + fmt.Sprintf("%s:", peername), } + retry(10, 1*time.Second, func() error { + fmt.Printf( + "Sending file from %s to %s\n", + hostname, + peername, + ) + _, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ExecuteCommandTimeout(60*time.Second), + ) + return err + }) + assert.Nil(t, err) }) } } From e2f8c69e2ea6c202343ee696dd52fabf56722adb Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sun, 16 Jan 2022 11:37:12 +0100 Subject: [PATCH 13/32] integration-test: use tailscale ip to test dual-stack MagicDNS --- integration_test.go | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/integration_test.go b/integration_test.go index fd280f88..a7981cd4 100644 --- a/integration_test.go +++ b/integration_test.go @@ -719,7 +719,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByHostname() { } fmt.Printf( - "Pinging using Hostname from %s to %s\n", + "Pinging using hostname from %s to %s\n", hostname, peername, ) @@ -737,11 +737,7 @@ func (s *IntegrationTestSuite) TestPingAllPeersByHostname() { } } -// TODO: -// * With manual testing, MagicDNS does not respond to AAAA queries. Why? -// * Tailscaled only adds a route to the IPv4 (100.100.100.100) address of the MagicDNS service, -// event though there is an IPv6 one (fd7a:115c:a1e0::53) as well. -func (s *IntegrationTestSuite) TestMagicDNSv4() { +func (s *IntegrationTestSuite) TestMagicDNS() { for namespace, scales := range s.namespaces { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) @@ -750,15 +746,14 @@ func (s *IntegrationTestSuite) TestMagicDNSv4() { if peername == hostname { continue } - s.T().Run(fmt.Sprintf("%s-%s-ipv4", hostname, peername), func(t *testing.T) { + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { command := []string{ - "host", "-4", "-t", "A", + "tailscale", "ip", fmt.Sprintf("%s.%s.headscale.net", peername, namespace), - "100.100.100.100", } fmt.Printf( - "Resolving name %s (IPv4) from %s over IPv4\n", + "Resolving name %s from %s\n", peername, hostname, ) @@ -770,14 +765,9 @@ func (s *IntegrationTestSuite) TestMagicDNSv4() { assert.Nil(t, err) fmt.Printf("Result for %s: %s\n", hostname, result) - resolved := false for _, ip := range ips { - if strings.Contains(result, fmt.Sprintf("has address %s", ip.String())) { - resolved = true - break - } + assert.Contains(t, result, ip.String()) } - assert.Equal(t, true, resolved) }) } } From bf7ee7832434a5b9bf0eb52a3f9e79233fa39859 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Fri, 28 Jan 2022 22:13:45 +0100 Subject: [PATCH 14/32] config-example: add configuration for a dual-stack tailnet --- config-example.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config-example.yaml b/config-example.yaml index 3301669d..fcbaa538 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -22,6 +22,13 @@ listen_addr: 0.0.0.0:8080 # autogenerated if it's missing private_key_path: /var/lib/headscale/private.key +# List of IP prefixes to allocate tailaddresses from. +# Each prefix consists of either an IPv4 or IPv6 address, +# and the associated prefix length, delimited by a slash. +ip_prefixes: + - fd7a:115c:a1e0::/48 + - 100.64.0.0/10 + # DERP is a relay system that Tailscale uses when a direct # connection cannot be established. # https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp From 62208360505f92a54248f88db2571910a758e789 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 29 Jan 2022 15:26:28 +0100 Subject: [PATCH 15/32] utils: extract GetIPPrefixEndpoints from anonymous function --- utils.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/utils.go b/utils.go index 3b18a406..babc69f8 100644 --- a/utils.go +++ b/utils.go @@ -147,6 +147,13 @@ func (h *Headscale) getAvailableIPs() (ips MachineAddresses, err error) { return } +func GetIPPrefixEndpoints(na netaddr.IPPrefix) (network, broadcast netaddr.IP) { + ipRange := na.Range() + network = ipRange.From() + broadcast = ipRange.To() + return +} + // TODO: Is this concurrency safe? // What would happen if multiple hosts were to register at the same time? // Would we attempt to assign the same addresses to multiple nodes? @@ -156,11 +163,7 @@ func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, erro return nil, err } - ipPrefixNetworkAddress, ipPrefixBroadcastAddress := func() (netaddr.IP, netaddr.IP) { - ipRange := ipPrefix.Range() - - return ipRange.From(), ipRange.To() - }() + ipPrefixNetworkAddress, ipPrefixBroadcastAddress := GetIPPrefixEndpoints(ipPrefix) // Get the first IP in our prefix ip := ipPrefixNetworkAddress.Next() From 7a86321252c78ca846982f78e3ab8c122300fe07 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 29 Jan 2022 15:33:54 +0100 Subject: [PATCH 16/32] CHANGELOG: document breaking configuration change regarding multiple prefixes --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f823f0c7..9c381acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ **TBD (TBD):** - Fixed issue where hosts deleted from control server may be written back to the database, as long as they are connected to the control server [#278](https://github.com/juanfont/headscale/pull/278) - ) +- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208) + **0.12.3 (2022-01-13):** From e66f8b0eeb65470a1adf8220b966f7480bd2d742 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 29 Jan 2022 16:04:15 +0100 Subject: [PATCH 17/32] cmd/headscale/cli/utils: merge ip_prefix with ip_prefixes in config --- cmd/headscale/cli/utils.go | 42 ++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 2c8f2a6b..af766085 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -41,8 +41,6 @@ func LoadConfig(path string) error { viper.SetDefault("tls_letsencrypt_cache_dir", "/var/www/.cache") viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01") - viper.SetDefault("ip_prefixes", []string{"100.64.0.0/10"}) - viper.SetDefault("log_level", "info") viper.SetDefault("dns_config", nil) @@ -222,13 +220,49 @@ func getHeadscaleConfig() headscale.Config { derpConfig := GetDERPConfig() configuredPrefixes := viper.GetStringSlice("ip_prefixes") - prefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)) + parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1) + + legacyPrefixField := viper.GetString("ip_prefix") + if len(legacyPrefixField) > 0 { + log. + Warn(). + Msgf( + "%s, %s", + "use of 'ip_prefix' for configuration is deprecated", + "please see 'ip_prefixes' in the shipped example.", + ) + legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField) + if err != nil { + panic(fmt.Errorf("failed to parse ip_prefix: %w", err)) + } + parsedPrefixes = append(parsedPrefixes, legacyPrefix) + } + for i, prefixInConfig := range configuredPrefixes { prefix, err := netaddr.ParseIPPrefix(prefixInConfig) if err != nil { panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err)) } - prefixes = append(prefixes, prefix) + parsedPrefixes = append(parsedPrefixes, prefix) + } + + prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes)) + { + // dedup + normalizedPrefixes := make(map[string]int, len(parsedPrefixes)) + for i, p := range parsedPrefixes { + normalizedPrefixes[p.String()] = i + } + + // convert back to list + for _, i := range normalizedPrefixes { + prefixes = append(prefixes, parsedPrefixes[i]) + } + } + + if len(prefixes) < 1 { + prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10")) + log.Warn().Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) } return headscale.Config{ From 74f26d36859280f89f3731e53579ae015be88474 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 29 Jan 2022 16:08:02 +0100 Subject: [PATCH 18/32] fixup! CHANGELOG: document breaking configuration change regarding multiple prefixes --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c381acf..3a64766f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,6 @@ - Fixed issue where hosts deleted from control server may be written back to the database, as long as they are connected to the control server [#278](https://github.com/juanfont/headscale/pull/278) - `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208) - **0.12.3 (2022-01-13):** **Changes**: From 0a1db89d33060e78dc3e6019106b5fe9d6fc670d Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 29 Jan 2022 16:27:36 +0100 Subject: [PATCH 19/32] fixup! cmd/headscale/cli/utils: merge ip_prefix with ip_prefixes in config --- cmd/headscale/cli/utils.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index af766085..6047211c 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -251,7 +251,8 @@ func getHeadscaleConfig() headscale.Config { // dedup normalizedPrefixes := make(map[string]int, len(parsedPrefixes)) for i, p := range parsedPrefixes { - normalizedPrefixes[p.String()] = i + normalized, _ := p.Range().Prefix() + normalizedPrefixes[normalized.String()] = i } // convert back to list @@ -265,6 +266,8 @@ func getHeadscaleConfig() headscale.Config { log.Warn().Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) } + log.Warn().Msgf("==> %v", prefixes) + return headscale.Config{ ServerURL: viper.GetString("server_url"), Addr: viper.GetString("listen_addr"), From 45bcf3989407e6a418302c891a248778f2dabef6 Mon Sep 17 00:00:00 2001 From: Csaba Sarkadi Date: Sat, 29 Jan 2022 16:52:27 +0100 Subject: [PATCH 20/32] fixup! fixup! cmd/headscale/cli/utils: merge ip_prefix with ip_prefixes in config --- cmd/headscale/cli/utils.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 6047211c..53b0a89d 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -266,8 +266,6 @@ func getHeadscaleConfig() headscale.Config { log.Warn().Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) } - log.Warn().Msgf("==> %v", prefixes) - return headscale.Config{ ServerURL: viper.GetString("server_url"), Addr: viper.GetString("listen_addr"), From 2f576b2fb154b6b2314d4885b77629c16244c54b Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 29 Jan 2022 20:04:56 +0000 Subject: [PATCH 21/32] Tag 0.12.4 in CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba96eab..e9ac7191 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ **Changes**: +**0.12.4 (2022-01-29):** + +**Changes**: + - Make gRPC Unix Socket permissions configurable [#292](https://github.com/juanfont/headscale/pull/292) - Trim whitespace before reading Private Key from file [#289](https://github.com/juanfont/headscale/pull/289) - Add new command to generate a private key for `headscale` [#290](https://github.com/juanfont/headscale/pull/290) From ccd41b9a135e65f3d98ae719265ffd36db716400 Mon Sep 17 00:00:00 2001 From: Bryan Stenson Date: Sat, 29 Jan 2022 22:34:51 -0800 Subject: [PATCH 22/32] Typo --- docs/running-headscale-linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 7a62fcc6..973a6eb8 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -44,7 +44,7 @@ touch /var/lib/headscale/db.sqlite touch /etc/headscale/config.yaml ``` -It is **strongly recommended** to copy and modifiy the [example configuration](../config-example.yaml) +It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml) from the [headscale repository](../) 6. Start the headscale server: From ad4e3a89e0a2ae1bb5aa480b106163ec58f1e014 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 08:25:49 +0000 Subject: [PATCH 23/32] Format changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c762ffea..6c74144e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,12 @@ **0.13.0 (2022-xx-xx):** **Features**: + - Add IPv6 support to the prefix assigned to namespaces **Changes**: -- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208) +- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208) **0.12.4 (2022-01-29):** From 445c04baf7817bebbd61e7d1d448e04b8eb88854 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 08:35:10 +0000 Subject: [PATCH 24/32] Fix lint --- machine_test.go | 4 +++- utils.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/machine_test.go b/machine_test.go index ecb50de3..1cc4d40c 100644 --- a/machine_test.go +++ b/machine_test.go @@ -208,7 +208,9 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) { }) serialized, err := input.Value() c.Assert(err, check.IsNil) - c.Assert(serialized.(string), check.Equals, "192.0.2.1,2001:db8::1") + if serial, ok := serialized.(string); ok { + c.Assert(serial, check.Equals, "192.0.2.1,2001:db8::1") + } var deserialized MachineAddresses err = deserialized.Scan(serialized) diff --git a/utils.go b/utils.go index babc69f8..9e85599f 100644 --- a/utils.go +++ b/utils.go @@ -151,6 +151,7 @@ func GetIPPrefixEndpoints(na netaddr.IPPrefix) (network, broadcast netaddr.IP) { ipRange := na.Range() network = ipRange.From() broadcast = ipRange.To() + return } From 4e63bba4fe9484fe508694bfad92adbaa9c0cb7e Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 08:55:41 +0000 Subject: [PATCH 25/32] Only compat go 1.17 in go mod tidy --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 5073b57c..a492860c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,7 +2,7 @@ # Make sure to check the documentation at http://goreleaser.com before: hooks: - - go mod tidy + - go mod tidy -compat=1.17 release: prerelease: auto From e0c22a414b0dac1f3fa698c1f3170df554af235b Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 08:56:28 +0000 Subject: [PATCH 26/32] Remove wrong comment --- .goreleaser.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index a492860c..b240ab0c 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,4 @@ -# This is an example .goreleaser.yml file with some sane defaults. -# Make sure to check the documentation at http://goreleaser.com +--- before: hooks: - go mod tidy -compat=1.17 From 71ab4c9b2cce7c2aaf4fbc14181c7551b98c4e3c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 08:59:25 +0000 Subject: [PATCH 27/32] Fix type according to config schema --- .goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index b240ab0c..d1dec26f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -32,7 +32,7 @@ builds: goarch: - arm goarm: - - 7 + - "7" env: - CC=arm-linux-gnueabihf-gcc - CXX=arm-linux-gnueabihf-g++ From 1815040d9821cd2ced960e1c21ca9b867d7ab10c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 09:43:48 +0000 Subject: [PATCH 28/32] Set up build build avoidance This commit configures the CI to run specific parts of the CI when relevant changes has been made. This should help us not have to deal with the integration tests when we do doc/admin changes. --- .github/workflows/lint.yml | 26 ++++++++++++++++++++++++++ .github/workflows/test-integration.yml | 17 ++++++++++++----- .github/workflows/test.yml | 22 +++++++++++++++------- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6fb985f7..65b37f5f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -9,7 +9,18 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + go.* + **/*.go + integration_test/ + config-example.yaml + - name: golangci-lint + if: steps.changed-files.outputs.any_changed == 'true' uses: golangci/golangci-lint-action@v2 with: version: latest @@ -25,6 +36,21 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + **/*.md + **/*.yml + **/*.yaml + **/*.ts + **/*.js + **/*.sass + **/*.css + **/*.scss + **/*.html + - name: Prettify code uses: creyD/prettier_action@v4.0 with: diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index d4179ea2..c0613705 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -3,21 +3,28 @@ name: CI on: [pull_request] jobs: - # The "build" workflow integration-test: - # The type of runner that the job will run on runs-on: ubuntu-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - # Setup Go + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + go.* + **/*.go + integration_test/ + config-example.yaml + - name: Setup Go + if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@v2 with: go-version: "1.17" - name: Run Integration tests + if: steps.changed-files.outputs.any_changed == 'true' run: go test -tags integration -timeout 30m diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 651063fc..6f8ac08e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,31 +3,39 @@ name: CI on: [push, pull_request] jobs: - # The "build" workflow test: - # The type of runner that the job will run on runs-on: ubuntu-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - # Setup Go + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + go.* + **/*.go + integration_test/ + config-example.yaml + - name: Setup Go + if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@v2 with: - go-version: "1.17" # The Go version to download (if necessary) and use. + go-version: "1.17" - # Install all the dependencies - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' run: | go version sudo apt update sudo apt install -y make - name: Run tests + if: steps.changed-files.outputs.any_changed == 'true' run: make test - name: Run build + if: steps.changed-files.outputs.any_changed == 'true' run: make From 991175f2aa36fb6a7f99d029cde6b1cf039cab36 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 09:49:15 +0000 Subject: [PATCH 29/32] Add depth and avoidance for build --- .github/workflows/build.yml | 15 +++++++++++++++ .github/workflows/lint.yml | 3 +++ .github/workflows/test-integration.yml | 1 + .github/workflows/test.yml | 1 + 4 files changed, 20 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 589d8c58..b8ea7d6c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,21 +15,36 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + fetch-depth: 2 + files: | + go.* + **/*.go + integration_test/ + config-example.yaml + - name: Setup Go + if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@v2 with: go-version: "1.17" - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' run: | go version sudo apt update sudo apt install -y make - name: Run build + if: steps.changed-files.outputs.any_changed == 'true' run: make build - uses: actions/upload-artifact@v2 + if: steps.changed-files.outputs.any_changed == 'true' with: name: headscale-linux path: headscale diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 65b37f5f..1c0e86d4 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -13,6 +13,7 @@ jobs: id: changed-files uses: tj-actions/changed-files@v14.1 with: + fetch-depth: 2 files: | go.* **/*.go @@ -40,6 +41,7 @@ jobs: id: changed-files uses: tj-actions/changed-files@v14.1 with: + fetch-depth: 2 files: | **/*.md **/*.yml @@ -52,6 +54,7 @@ jobs: **/*.html - name: Prettify code + if: steps.changed-files.outputs.any_changed == 'true' uses: creyD/prettier_action@v4.0 with: prettier_options: >- diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index c0613705..f6eb7b48 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -13,6 +13,7 @@ jobs: id: changed-files uses: tj-actions/changed-files@v14.1 with: + fetch-depth: 2 files: | go.* **/*.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f8ac08e..c2859467 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,6 +13,7 @@ jobs: id: changed-files uses: tj-actions/changed-files@v14.1 with: + fetch-depth: 2 files: | go.* **/*.go From 0862f60ff05546a995bf68322b159900c4635c73 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 09:54:26 +0000 Subject: [PATCH 30/32] Put depth in the correct place --- .github/workflows/build.yml | 3 ++- .github/workflows/lint.yml | 6 ++++-- .github/workflows/test-integration.yml | 3 ++- .github/workflows/test.yml | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b8ea7d6c..d92e9e6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,12 +14,13 @@ jobs: steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v14.1 with: - fetch-depth: 2 files: | go.* **/*.go diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1c0e86d4..26a24ae7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,12 +8,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v14.1 with: - fetch-depth: 2 files: | go.* **/*.go @@ -36,12 +37,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v14.1 with: - fetch-depth: 2 files: | **/*.md **/*.yml diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index f6eb7b48..9f526f97 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -8,12 +8,13 @@ jobs: steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v14.1 with: - fetch-depth: 2 files: | go.* **/*.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c2859467..9ce8a779 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,12 +8,13 @@ jobs: steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 - name: Get changed files id: changed-files uses: tj-actions/changed-files@v14.1 with: - fetch-depth: 2 files: | go.* **/*.go From e9adfcd6782d0237a14b4271913c1dff27b6e2f7 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 13:06:49 +0000 Subject: [PATCH 31/32] Migrate ip_address field to ip_addresses The automatic migration did not pick up this change and it breaks current setups as it only adds a new field which is empty. This means that clients who dont request a new IP will not have any IP at all. --- db.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/db.go b/db.go index 51363256..256f1bf3 100644 --- a/db.go +++ b/db.go @@ -30,18 +30,24 @@ func (h *Headscale) initDB() error { if h.dbType == Postgres { db.Exec("create extension if not exists \"uuid-ossp\";") } + + _ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses") + err = db.AutoMigrate(&Machine{}) if err != nil { return err } + err = db.AutoMigrate(&KV{}) if err != nil { return err } + err = db.AutoMigrate(&Namespace{}) if err != nil { return err } + err = db.AutoMigrate(&PreAuthKey{}) if err != nil { return err From 9e3318ca27b61e8a203821e5794abf9663c601a5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 30 Jan 2022 14:53:40 +0000 Subject: [PATCH 32/32] Simplify postgres uuid-ossp stirng --- db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db.go b/db.go index 256f1bf3..7b777863 100644 --- a/db.go +++ b/db.go @@ -28,7 +28,7 @@ func (h *Headscale) initDB() error { h.db = db if h.dbType == Postgres { - db.Exec("create extension if not exists \"uuid-ossp\";") + db.Exec(`create extension if not exists "uuid-ossp";`) } _ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")