Resolve merge conflict

This commit is contained in:
Kristoffer Dalby 2022-03-02 08:11:50 +00:00
commit 5b169010be
23 changed files with 681 additions and 510 deletions

View file

@ -29,6 +29,7 @@ linters:
- wrapcheck - wrapcheck
- dupl - dupl
- makezero - makezero
- maintidx
# We might want to enable this, but it might be a lot of work # We might want to enable this, but it might be a lot of work
- cyclop - cyclop

View file

@ -1,31 +1,37 @@
# CHANGELOG # CHANGELOG
**0.15.0 (2022-xx-xx):** ## 0.15.0 (2022-xx-xx)
**BREAKING**: **Note:** Take a backup of your database before upgrading.
### BREAKING
- Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357) - Boundaries between Namespaces has been removed and all nodes can communicate by default [#357](https://github.com/juanfont/headscale/pull/357)
- To limit access between nodes, use [ACLs](./docs/acls.md). - To limit access between nodes, use [ACLs](./docs/acls.md).
**Features**: ### Features
- Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359) - Add support for writing ACL files with YAML [#359](https://github.com/juanfont/headscale/pull/359)
- Users can now use emails in ACL's groups [#372](https://github.com/juanfont/headscale/issues/372)
**Changes**: ### Changes
- Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346) - Fix a bug were the same IP could be assigned to multiple hosts if joined in quick succession [#346](https://github.com/juanfont/headscale/pull/346)
- Simplify the code behind registration of machines [#366](https://github.com/juanfont/headscale/pull/366)
- Nodes are now only written to database if they are registrated successfully
- Fix a limitation in the ACLs that prevented users to write rules with `*` as source [#374](https://github.com/juanfont/headscale/issues/374)
**0.14.0 (2022-02-24):** ## 0.14.0 (2022-02-24)
**UPCOMING BREAKING**: **UPCOMING ### BREAKING
From the **next** version (`0.15.0`), all machines will be able to communicate regardless of From the **next\*\* version (`0.15.0`), all machines will be able to communicate regardless of
if they are in the same namespace. This means that the behaviour currently limited to ACLs if they are in the same namespace. This means that the behaviour currently limited to ACLs
will become default. From version `0.15.0`, all limitation of communications must be done will become default. From version `0.15.0`, all limitation of communications must be done
with ACLs. with ACLs.
This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour. This is a part of aligning `headscale`'s behaviour with Tailscale's upstream behaviour.
**BREAKING**: ### BREAKING
- ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs - ACLs have been rewritten to align with the bevaviour Tailscale Control Panel provides. **NOTE:** This is only active if you use ACLs
- Namespaces are now treated as Users - Namespaces are now treated as Users
@ -33,17 +39,17 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
- Tags should now work correctly and adding a host to Headscale should now reload the rules. - Tags should now work correctly and adding a host to Headscale should now reload the rules.
- The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features - The documentation have a [fictional example](docs/acls.md) that should cover some use cases of the ACLs features
**Features**: ### Features
- Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls) [#297](https://github.com/juanfont/headscale/pull/297) - Add support for configurable mTLS [docs](docs/tls.md#configuring-mutual-tls-authentication-mtls) [#297](https://github.com/juanfont/headscale/pull/297)
**Changes**: ### Changes
- Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346) - Remove dependency on CGO (switch from CGO SQLite to pure Go) [#346](https://github.com/juanfont/headscale/pull/346)
**0.13.0 (2022-02-18):** **0.13.0 (2022-02-18):**
**Features**: ### Features
- Add IPv6 support to the prefix assigned to namespaces - Add IPv6 support to the prefix assigned to namespaces
- Add API Key support - Add API Key support
@ -54,7 +60,7 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
- `oidc.domain_map` option has been removed - `oidc.domain_map` option has been removed
- `strip_email_domain` option has been added (see [config-example.yaml](./config_example.yaml)) - `strip_email_domain` option has been added (see [config-example.yaml](./config_example.yaml))
**Changes**: ### 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)
- Upgrade `tailscale` (1.20.4) and other dependencies to latest [#314](https://github.com/juanfont/headscale/pull/314) - Upgrade `tailscale` (1.20.4) and other dependencies to latest [#314](https://github.com/juanfont/headscale/pull/314)
@ -63,35 +69,35 @@ This is a part of aligning `headscale`'s behaviour with Tailscale's upstream beh
**0.12.4 (2022-01-29):** **0.12.4 (2022-01-29):**
**Changes**: ### Changes
- Make gRPC Unix Socket permissions configurable [#292](https://github.com/juanfont/headscale/pull/292) - 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) - 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) - Add new command to generate a private key for `headscale` [#290](https://github.com/juanfont/headscale/pull/290)
- 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) - 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)
**0.12.3 (2022-01-13):** ## 0.12.3 (2022-01-13)
**Changes**: ### Changes
- Added Alpine container [#270](https://github.com/juanfont/headscale/pull/270) - Added Alpine container [#270](https://github.com/juanfont/headscale/pull/270)
- Minor updates in dependencies [#271](https://github.com/juanfont/headscale/pull/271) - Minor updates in dependencies [#271](https://github.com/juanfont/headscale/pull/271)
**0.12.2 (2022-01-11):** ## 0.12.2 (2022-01-11)
Happy New Year! Happy New Year!
**Changes**: ### Changes
- Fix Docker release [#258](https://github.com/juanfont/headscale/pull/258) - Fix Docker release [#258](https://github.com/juanfont/headscale/pull/258)
- Rewrite main docs [#262](https://github.com/juanfont/headscale/pull/262) - Rewrite main docs [#262](https://github.com/juanfont/headscale/pull/262)
- Improve Docker docs [#263](https://github.com/juanfont/headscale/pull/263) - Improve Docker docs [#263](https://github.com/juanfont/headscale/pull/263)
**0.12.1 (2021-12-24):** ## 0.12.1 (2021-12-24)
(We are skipping 0.12.0 to correct a mishap done weeks ago with the version tagging) (We are skipping 0.12.0 to correct a mishap done weeks ago with the version tagging)
**BREAKING**: ### BREAKING
- Upgrade to Tailscale 1.18 [#229](https://github.com/juanfont/headscale/pull/229) - Upgrade to Tailscale 1.18 [#229](https://github.com/juanfont/headscale/pull/229)
- This change requires a new format for private key, private keys are now generated automatically: - This change requires a new format for private key, private keys are now generated automatically:
@ -99,19 +105,19 @@ Happy New Year!
2. Restart `headscale`, a new key will be generated. 2. Restart `headscale`, a new key will be generated.
3. Restart all Tailscale clients to fetch the new key 3. Restart all Tailscale clients to fetch the new key
**Changes**: ### Changes
- Unify configuration example [#197](https://github.com/juanfont/headscale/pull/197) - Unify configuration example [#197](https://github.com/juanfont/headscale/pull/197)
- Add stricter linting and formatting [#223](https://github.com/juanfont/headscale/pull/223) - Add stricter linting and formatting [#223](https://github.com/juanfont/headscale/pull/223)
**Features**: ### Features
- Add gRPC and HTTP API (HTTP API is currently disabled) [#204](https://github.com/juanfont/headscale/pull/204) - Add gRPC and HTTP API (HTTP API is currently disabled) [#204](https://github.com/juanfont/headscale/pull/204)
- Use gRPC between the CLI and the server [#206](https://github.com/juanfont/headscale/pull/206), [#212](https://github.com/juanfont/headscale/pull/212) - Use gRPC between the CLI and the server [#206](https://github.com/juanfont/headscale/pull/206), [#212](https://github.com/juanfont/headscale/pull/212)
- Beta OpenID Connect support [#126](https://github.com/juanfont/headscale/pull/126), [#227](https://github.com/juanfont/headscale/pull/227) - Beta OpenID Connect support [#126](https://github.com/juanfont/headscale/pull/126), [#227](https://github.com/juanfont/headscale/pull/227)
**0.11.0 (2021-10-25):** ## 0.11.0 (2021-10-25)
**BREAKING**: ### BREAKING
- Make headscale fetch DERP map from URL and file [#196](https://github.com/juanfont/headscale/pull/196) - Make headscale fetch DERP map from URL and file [#196](https://github.com/juanfont/headscale/pull/196)

46
acls.go
View file

@ -160,7 +160,7 @@ func (h *Headscale) generateACLPolicySrcIP(
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
u string, u string,
) ([]string, error) { ) ([]string, error) {
return expandAlias(machines, aclPolicy, u) return expandAlias(machines, aclPolicy, u, h.cfg.OIDC.StripEmaildomain)
} }
func (h *Headscale) generateACLPolicyDestPorts( func (h *Headscale) generateACLPolicyDestPorts(
@ -186,7 +186,12 @@ func (h *Headscale) generateACLPolicyDestPorts(
alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1]) alias = fmt.Sprintf("%s:%s", tokens[0], tokens[1])
} }
expanded, err := expandAlias(machines, aclPolicy, alias) expanded, err := expandAlias(
machines,
aclPolicy,
alias,
h.cfg.OIDC.StripEmaildomain,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -218,6 +223,7 @@ func expandAlias(
machines []Machine, machines []Machine,
aclPolicy ACLPolicy, aclPolicy ACLPolicy,
alias string, alias string,
stripEmailDomain bool,
) ([]string, error) { ) ([]string, error) {
ips := []string{} ips := []string{}
if alias == "*" { if alias == "*" {
@ -225,7 +231,7 @@ func expandAlias(
} }
if strings.HasPrefix(alias, "group:") { if strings.HasPrefix(alias, "group:") {
namespaces, err := expandGroup(aclPolicy, alias) namespaces, err := expandGroup(aclPolicy, alias, stripEmailDomain)
if err != nil { if err != nil {
return ips, err return ips, err
} }
@ -240,7 +246,7 @@ func expandAlias(
} }
if strings.HasPrefix(alias, "tag:") { if strings.HasPrefix(alias, "tag:") {
owners, err := expandTagOwners(aclPolicy, alias) owners, err := expandTagOwners(aclPolicy, alias, stripEmailDomain)
if err != nil { if err != nil {
return ips, err return ips, err
} }
@ -383,7 +389,11 @@ func filterMachinesByNamespace(machines []Machine, namespace string) []Machine {
// expandTagOwners will return a list of namespace. An owner can be either a namespace or a group // expandTagOwners will return a list of namespace. An owner can be either a namespace or a group
// a group cannot be composed of groups. // a group cannot be composed of groups.
func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) { func expandTagOwners(
aclPolicy ACLPolicy,
tag string,
stripEmailDomain bool,
) ([]string, error) {
var owners []string var owners []string
ows, ok := aclPolicy.TagOwners[tag] ows, ok := aclPolicy.TagOwners[tag]
if !ok { if !ok {
@ -395,7 +405,7 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
} }
for _, owner := range ows { for _, owner := range ows {
if strings.HasPrefix(owner, "group:") { if strings.HasPrefix(owner, "group:") {
gs, err := expandGroup(aclPolicy, owner) gs, err := expandGroup(aclPolicy, owner, stripEmailDomain)
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
@ -410,8 +420,13 @@ func expandTagOwners(aclPolicy ACLPolicy, tag string) ([]string, error) {
// expandGroup will return the list of namespace inside the group // expandGroup will return the list of namespace inside the group
// after some validation. // after some validation.
func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) { func expandGroup(
groups, ok := aclPolicy.Groups[group] aclPolicy ACLPolicy,
group string,
stripEmailDomain bool,
) ([]string, error) {
outGroups := []string{}
aclGroups, ok := aclPolicy.Groups[group]
if !ok { if !ok {
return []string{}, fmt.Errorf( return []string{}, fmt.Errorf(
"group %v isn't registered. %w", "group %v isn't registered. %w",
@ -419,14 +434,23 @@ func expandGroup(aclPolicy ACLPolicy, group string) ([]string, error) {
errInvalidGroup, errInvalidGroup,
) )
} }
for _, g := range groups { for _, group := range aclGroups {
if strings.HasPrefix(g, "group:") { if strings.HasPrefix(group, "group:") {
return []string{}, fmt.Errorf( return []string{}, fmt.Errorf(
"%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups", "%w. A group cannot be composed of groups. https://tailscale.com/kb/1018/acls/#groups",
errInvalidGroup, errInvalidGroup,
) )
} }
grp, err := NormalizeNamespaceName(group, stripEmailDomain)
if err != nil {
return []string{}, fmt.Errorf(
"failed to normalize group %q, err: %w",
group,
errInvalidGroup,
)
}
outGroups = append(outGroups, grp)
} }
return groups, nil return outGroups, nil
} }

View file

@ -121,7 +121,6 @@ func (s *Suite) TestValidExpandTagOwnersInUsers(c *check.C) {
Name: "testmachine", Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@ -168,7 +167,6 @@ func (s *Suite) TestValidExpandTagOwnersInPorts(c *check.C) {
Name: "testmachine", Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@ -215,7 +213,6 @@ func (s *Suite) TestInvalidTagValidNamespace(c *check.C) {
Name: "testmachine", Name: "testmachine",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@ -261,7 +258,6 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
Name: "webserver", Name: "webserver",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.1")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@ -281,7 +277,6 @@ func (s *Suite) TestValidTagInvalidNamespace(c *check.C) {
Name: "user", Name: "user",
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo2), HostInfo: HostInfo(hostInfo2),
@ -375,7 +370,6 @@ func (s *Suite) TestPortNamespace(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips, IPAddresses: ips,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@ -418,7 +412,6 @@ func (s *Suite) TestPortGroup(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: ips, IPAddresses: ips,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
@ -444,8 +437,9 @@ func (s *Suite) TestPortGroup(c *check.C) {
func Test_expandGroup(t *testing.T) { func Test_expandGroup(t *testing.T) {
type args struct { type args struct {
aclPolicy ACLPolicy aclPolicy ACLPolicy
group string group string
stripEmailDomain bool
} }
tests := []struct { tests := []struct {
name string name string
@ -462,7 +456,8 @@ func Test_expandGroup(t *testing.T) {
"group:foo": []string{"user2", "user3"}, "group:foo": []string{"user2", "user3"},
}, },
}, },
group: "group:test", group: "group:test",
stripEmailDomain: true,
}, },
want: []string{"user1", "user2", "user3"}, want: []string{"user1", "user2", "user3"},
wantErr: false, wantErr: false,
@ -476,15 +471,54 @@ func Test_expandGroup(t *testing.T) {
"group:foo": []string{"user2", "user3"}, "group:foo": []string{"user2", "user3"},
}, },
}, },
group: "group:undefined", group: "group:undefined",
stripEmailDomain: true,
}, },
want: []string{}, want: []string{},
wantErr: true, wantErr: true,
}, },
{
name: "Expand emails in group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:admin": []string{
"joe.bar@gmail.com",
"john.doe@yahoo.fr",
},
},
},
group: "group:admin",
stripEmailDomain: true,
},
want: []string{"joe.bar", "john.doe"},
wantErr: false,
},
{
name: "Expand emails in group",
args: args{
aclPolicy: ACLPolicy{
Groups: Groups{
"group:admin": []string{
"joe.bar@gmail.com",
"john.doe@yahoo.fr",
},
},
},
group: "group:admin",
stripEmailDomain: false,
},
want: []string{"joe.bar.gmail.com", "john.doe.yahoo.fr"},
wantErr: false,
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
got, err := expandGroup(test.args.aclPolicy, test.args.group) got, err := expandGroup(
test.args.aclPolicy,
test.args.group,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr { if (err != nil) != test.wantErr {
t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr) t.Errorf("expandGroup() error = %v, wantErr %v", err, test.wantErr)
@ -499,8 +533,9 @@ func Test_expandGroup(t *testing.T) {
func Test_expandTagOwners(t *testing.T) { func Test_expandTagOwners(t *testing.T) {
type args struct { type args struct {
aclPolicy ACLPolicy aclPolicy ACLPolicy
tag string tag string
stripEmailDomain bool
} }
tests := []struct { tests := []struct {
name string name string
@ -514,7 +549,8 @@ func Test_expandTagOwners(t *testing.T) {
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:test": []string{"user1"}}, TagOwners: TagOwners{"tag:test": []string{"user1"}},
}, },
tag: "tag:test", tag: "tag:test",
stripEmailDomain: true,
}, },
want: []string{"user1"}, want: []string{"user1"},
wantErr: false, wantErr: false,
@ -526,7 +562,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:foo": []string{"user1", "user2"}}, Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo"}},
}, },
tag: "tag:test", tag: "tag:test",
stripEmailDomain: true,
}, },
want: []string{"user1", "user2"}, want: []string{"user1", "user2"},
wantErr: false, wantErr: false,
@ -538,7 +575,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:foo": []string{"user1", "user2"}}, Groups: Groups{"group:foo": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo", "user3"}},
}, },
tag: "tag:test", tag: "tag:test",
stripEmailDomain: true,
}, },
want: []string{"user1", "user2", "user3"}, want: []string{"user1", "user2", "user3"},
wantErr: false, wantErr: false,
@ -549,7 +587,8 @@ func Test_expandTagOwners(t *testing.T) {
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}}, TagOwners: TagOwners{"tag:foo": []string{"group:foo", "user1"}},
}, },
tag: "tag:test", tag: "tag:test",
stripEmailDomain: true,
}, },
want: []string{}, want: []string{},
wantErr: true, wantErr: true,
@ -561,7 +600,8 @@ func Test_expandTagOwners(t *testing.T) {
Groups: Groups{"group:bar": []string{"user1", "user2"}}, Groups: Groups{"group:bar": []string{"user1", "user2"}},
TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}}, TagOwners: TagOwners{"tag:test": []string{"group:foo", "user2"}},
}, },
tag: "tag:test", tag: "tag:test",
stripEmailDomain: true,
}, },
want: []string{}, want: []string{},
wantErr: true, wantErr: true,
@ -569,7 +609,11 @@ func Test_expandTagOwners(t *testing.T) {
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
got, err := expandTagOwners(test.args.aclPolicy, test.args.tag) got, err := expandTagOwners(
test.args.aclPolicy,
test.args.tag,
test.args.stripEmailDomain,
)
if (err != nil) != test.wantErr { if (err != nil) != test.wantErr {
t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr) t.Errorf("expandTagOwners() error = %v, wantErr %v", err, test.wantErr)
@ -731,9 +775,10 @@ func Test_listMachinesInNamespace(t *testing.T) {
// nolint // nolint
func Test_expandAlias(t *testing.T) { func Test_expandAlias(t *testing.T) {
type args struct { type args struct {
machines []Machine machines []Machine
aclPolicy ACLPolicy aclPolicy ACLPolicy
alias string alias string
stripEmailDomain bool
} }
tests := []struct { tests := []struct {
name string name string
@ -753,7 +798,8 @@ func Test_expandAlias(t *testing.T) {
}, },
}, },
}, },
aclPolicy: ACLPolicy{}, aclPolicy: ACLPolicy{},
stripEmailDomain: true,
}, },
want: []string{"*"}, want: []string{"*"},
wantErr: false, wantErr: false,
@ -791,6 +837,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}}, Groups: Groups{"group:accountant": []string{"joe", "marc"}},
}, },
stripEmailDomain: true,
}, },
want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"}, want: []string{"100.64.0.1", "100.64.0.2", "100.64.0.3"},
wantErr: false, wantErr: false,
@ -828,6 +875,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
Groups: Groups{"group:accountant": []string{"joe", "marc"}}, Groups: Groups{"group:accountant": []string{"joe", "marc"}},
}, },
stripEmailDomain: true,
}, },
want: []string{}, want: []string{},
wantErr: true, wantErr: true,
@ -835,9 +883,10 @@ func Test_expandAlias(t *testing.T) {
{ {
name: "simple ipaddress", name: "simple ipaddress",
args: args{ args: args{
alias: "10.0.0.3", alias: "10.0.0.3",
machines: []Machine{}, machines: []Machine{},
aclPolicy: ACLPolicy{}, aclPolicy: ACLPolicy{},
stripEmailDomain: true,
}, },
want: []string{"10.0.0.3"}, want: []string{"10.0.0.3"},
wantErr: false, wantErr: false,
@ -852,6 +901,7 @@ func Test_expandAlias(t *testing.T) {
"homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"), "homeNetwork": netaddr.MustParseIPPrefix("192.168.1.0/24"),
}, },
}, },
stripEmailDomain: true,
}, },
want: []string{"192.168.1.0/24"}, want: []string{"192.168.1.0/24"},
wantErr: false, wantErr: false,
@ -859,9 +909,10 @@ func Test_expandAlias(t *testing.T) {
{ {
name: "simple host", name: "simple host",
args: args{ args: args{
alias: "10.0.0.1", alias: "10.0.0.1",
machines: []Machine{}, machines: []Machine{},
aclPolicy: ACLPolicy{}, aclPolicy: ACLPolicy{},
stripEmailDomain: true,
}, },
want: []string{"10.0.0.1"}, want: []string{"10.0.0.1"},
wantErr: false, wantErr: false,
@ -869,9 +920,10 @@ func Test_expandAlias(t *testing.T) {
{ {
name: "simple CIDR", name: "simple CIDR",
args: args{ args: args{
alias: "10.0.0.0/16", alias: "10.0.0.0/16",
machines: []Machine{}, machines: []Machine{},
aclPolicy: ACLPolicy{}, aclPolicy: ACLPolicy{},
stripEmailDomain: true,
}, },
want: []string{"10.0.0.0/16"}, want: []string{"10.0.0.0/16"},
wantErr: false, wantErr: false,
@ -919,6 +971,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}}, TagOwners: TagOwners{"tag:hr-webserver": []string{"joe"}},
}, },
stripEmailDomain: true,
}, },
want: []string{"100.64.0.1", "100.64.0.2"}, want: []string{"100.64.0.1", "100.64.0.2"},
wantErr: false, wantErr: false,
@ -959,6 +1012,7 @@ func Test_expandAlias(t *testing.T) {
"tag:accountant-webserver": []string{"group:accountant"}, "tag:accountant-webserver": []string{"group:accountant"},
}, },
}, },
stripEmailDomain: true,
}, },
want: []string{}, want: []string{},
wantErr: true, wantErr: true,
@ -1006,6 +1060,7 @@ func Test_expandAlias(t *testing.T) {
aclPolicy: ACLPolicy{ aclPolicy: ACLPolicy{
TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}}, TagOwners: TagOwners{"tag:accountant-webserver": []string{"joe"}},
}, },
stripEmailDomain: true,
}, },
want: []string{"100.64.0.4"}, want: []string{"100.64.0.4"},
wantErr: false, wantErr: false,
@ -1017,6 +1072,7 @@ func Test_expandAlias(t *testing.T) {
test.args.machines, test.args.machines,
test.args.aclPolicy, test.args.aclPolicy,
test.args.alias, test.args.alias,
test.args.stripEmailDomain,
) )
if (err != nil) != test.wantErr { if (err != nil) != test.wantErr {
t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr) t.Errorf("expandAlias() error = %v, wantErr %v", err, test.wantErr)

189
api.go
View file

@ -22,7 +22,7 @@ import (
const ( const (
reservedResponseHeaderSize = 4 reservedResponseHeaderSize = 4
RegisterMethodAuthKey = "authKey" RegisterMethodAuthKey = "authkey"
RegisterMethodOIDC = "oidc" RegisterMethodOIDC = "oidc"
RegisterMethodCLI = "cli" RegisterMethodCLI = "cli"
ErrRegisterMethodCLIDoesNotSupportExpire = Error( ErrRegisterMethodCLIDoesNotSupportExpire = Error(
@ -125,25 +125,50 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
machine, err := h.GetMachineByMachineKey(machineKey) machine, err := h.GetMachineByMachineKey(machineKey)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine") log.Info().Str("machine", req.Hostinfo.Hostname).Msg("New machine")
newMachine := Machine{
Expiry: &time.Time{}, machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
MachineKey: MachinePublicKeyStripPrefix(machineKey),
Name: req.Hostinfo.Hostname, // If the machine has AuthKey set, handle registration via PreAuthKeys
} if req.Auth.AuthKey != "" {
if err := h.db.Create(&newMachine).Error; err != nil { h.handleAuthKey(ctx, machineKey, req)
log.Error().
Caller().
Err(err).
Msg("Could not create row")
machineRegistrations.WithLabelValues("unknown", "web", "error", machine.Namespace.Name).
Inc()
return return
} }
machine = &newMachine
// The machine did not have a key to authenticate, which means
// that we rely on a method that calls back some how (OpenID or CLI)
// We create the machine and then keep it around until a callback
// happens
newMachine := Machine{
MachineKey: machineKeyStr,
Name: req.Hostinfo.Hostname,
NodeKey: NodePublicKeyStripPrefix(req.NodeKey),
LastSeen: &now,
Expiry: &time.Time{},
}
if !req.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", req.Hostinfo.Hostname).
Time("expiry", req.Expiry).
Msg("Non-zero expiry time requested")
newMachine.Expiry = &req.Expiry
}
h.registrationCache.Set(
machineKeyStr,
newMachine,
registerCacheExpiration,
)
h.handleMachineRegistrationNew(ctx, machineKey, req)
return
} }
if machine.Registered { // The machine is already registered, so we need to pass through reauth or key update.
if machine != nil {
// If the NodeKey stored in headscale is the same as the key presented in a registration // If the NodeKey stored in headscale is the same as the key presented in a registration
// request, then we have a node that is either: // request, then we have a node that is either:
// - Trying to log out (sending a expiry in the past) // - Trying to log out (sending a expiry in the past)
@ -180,15 +205,6 @@ func (h *Headscale) RegistrationHandler(ctx *gin.Context) {
return return
} }
// If the machine has AuthKey set, handle registration via PreAuthKeys
if req.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, req, *machine)
return
}
h.handleMachineRegistrationNew(ctx, machineKey, req, *machine)
} }
func (h *Headscale) getMapResponse( func (h *Headscale) getMapResponse(
@ -402,7 +418,7 @@ func (h *Headscale) handleMachineExpired(
Msg("Machine registration has expired. Sending a authurl to register") Msg("Machine registration has expired. Sending a authurl to register")
if registerRequest.Auth.AuthKey != "" { if registerRequest.Auth.AuthKey != "" {
h.handleAuthKey(ctx, machineKey, registerRequest, machine) h.handleAuthKey(ctx, machineKey, registerRequest)
return return
} }
@ -465,13 +481,12 @@ func (h *Headscale) handleMachineRegistrationNew(
ctx *gin.Context, ctx *gin.Context,
machineKey key.MachinePublic, machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest, registerRequest tailcfg.RegisterRequest,
machine Machine,
) { ) {
resp := tailcfg.RegisterResponse{} resp := tailcfg.RegisterResponse{}
// The machine registration is new, redirect the client to the registration URL // The machine registration is new, redirect the client to the registration URL
log.Debug(). log.Debug().
Str("machine", machine.Name). Str("machine", registerRequest.Hostinfo.Hostname).
Msg("The node is sending us a new NodeKey, sending auth url") Msg("The node is sending us a new NodeKey, sending auth url")
if h.cfg.OIDC.Issuer != "" { if h.cfg.OIDC.Issuer != "" {
resp.AuthURL = fmt.Sprintf( resp.AuthURL = fmt.Sprintf(
@ -484,24 +499,6 @@ func (h *Headscale) handleMachineRegistrationNew(
strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey)) strings.TrimSuffix(h.cfg.ServerURL, "/"), MachinePublicKeyStripPrefix(machineKey))
} }
if !registerRequest.Expiry.IsZero() {
log.Trace().
Caller().
Str("machine", machine.Name).
Time("expiry", registerRequest.Expiry).
Msg("Non-zero expiry time requested, adding to cache")
h.requestedExpiryCache.Set(
machineKey.String(),
registerRequest.Expiry,
requestedExpiryCacheExpiration,
)
}
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// save the NodeKey
h.db.Save(&machine)
respBody, err := encode(resp, &machineKey, h.privateKey) respBody, err := encode(resp, &machineKey, h.privateKey)
if err != nil { if err != nil {
log.Error(). log.Error().
@ -520,19 +517,21 @@ func (h *Headscale) handleAuthKey(
ctx *gin.Context, ctx *gin.Context,
machineKey key.MachinePublic, machineKey key.MachinePublic,
registerRequest tailcfg.RegisterRequest, registerRequest tailcfg.RegisterRequest,
machine Machine,
) { ) {
machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
log.Debug(). log.Debug().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", registerRequest.Hostinfo.Hostname). Str("machine", registerRequest.Hostinfo.Hostname).
Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname) Msgf("Processing auth key for %s", registerRequest.Hostinfo.Hostname)
resp := tailcfg.RegisterResponse{} resp := tailcfg.RegisterResponse{}
pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey) pak, err := h.checkKeyValidity(registerRequest.Auth.AuthKey)
if err != nil { if err != nil {
log.Error(). log.Error().
Caller(). Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", registerRequest.Hostinfo.Hostname).
Err(err). Err(err).
Msg("Failed authentication via AuthKey") Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false resp.MachineAuthorized = false
@ -541,76 +540,66 @@ func (h *Headscale) handleAuthKey(
log.Error(). log.Error().
Caller(). Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", registerRequest.Hostinfo.Hostname).
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
ctx.String(http.StatusInternalServerError, "") ctx.String(http.StatusInternalServerError, "")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc() Inc()
return return
} }
ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody) ctx.Data(http.StatusUnauthorized, "application/json; charset=utf-8", respBody)
log.Error(). log.Error().
Caller(). Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", registerRequest.Hostinfo.Hostname).
Msg("Failed authentication via AuthKey") Msg("Failed authentication via AuthKey")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc() Inc()
return return
} }
if machine.isRegistered() { log.Debug().
log.Trace(). Str("func", "handleAuthKey").
Caller(). Str("machine", registerRequest.Hostinfo.Hostname).
Str("machine", machine.Name). Msg("Authentication key was valid, proceeding to acquire IP addresses")
Msg("machine already registered, reauthenticating")
h.RefreshMachine(&machine, registerRequest.Expiry) nodeKey := NodePublicKeyStripPrefix(registerRequest.NodeKey)
} else { now := time.Now().UTC()
log.Debug().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Msg("Authentication key was valid, proceeding to acquire IP addresses")
h.ipAllocationMutex.Lock() machineToRegister := Machine{
Name: registerRequest.Hostinfo.Hostname,
ips, err := h.getAvailableIPs() NamespaceID: pak.Namespace.ID,
if err != nil { MachineKey: machineKeyStr,
log.Error(). RegisterMethod: RegisterMethodAuthKey,
Caller(). Expiry: &registerRequest.Expiry,
Str("func", "handleAuthKey"). NodeKey: nodeKey,
Str("machine", machine.Name). LastSeen: &now,
Msg("Failed to find an available IP address") AuthKeyID: uint(pak.ID),
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name).
Inc()
return
}
log.Info().
Str("func", "handleAuthKey").
Str("machine", machine.Name).
Str("ips", strings.Join(ips.ToStringSlice(), ",")).
Msgf("Assigning %s to %s", strings.Join(ips.ToStringSlice(), ","), machine.Name)
machine.Expiry = &registerRequest.Expiry
machine.AuthKeyID = uint(pak.ID)
machine.IPAddresses = ips
machine.NamespaceID = pak.NamespaceID
machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey)
// we update it just in case
machine.Registered = true
machine.RegisterMethod = RegisterMethodAuthKey
h.db.Save(&machine)
h.ipAllocationMutex.Unlock()
} }
pak.Used = true machine, err := h.RegisterMachine(
h.db.Save(&pak) machineToRegister,
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc()
ctx.String(
http.StatusInternalServerError,
"could not register machine",
)
return
}
h.UsePreAuthKey(pak)
resp.MachineAuthorized = true resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser() resp.User = *pak.Namespace.toUser()
@ -619,21 +608,21 @@ func (h *Headscale) handleAuthKey(
log.Error(). log.Error().
Caller(). Caller().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", registerRequest.Hostinfo.Hostname).
Err(err). Err(err).
Msg("Cannot encode message") Msg("Cannot encode message")
machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "error", pak.Namespace.Name).
Inc() Inc()
ctx.String(http.StatusInternalServerError, "Extremely sad!") ctx.String(http.StatusInternalServerError, "Extremely sad!")
return return
} }
machineRegistrations.WithLabelValues("new", "authkey", "success", machine.Namespace.Name). machineRegistrations.WithLabelValues("new", RegisterMethodAuthKey, "success", pak.Namespace.Name).
Inc() Inc()
ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody) ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody)
log.Info(). log.Info().
Str("func", "handleAuthKey"). Str("func", "handleAuthKey").
Str("machine", machine.Name). Str("machine", registerRequest.Hostinfo.Hostname).
Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")).
Msg("Successfully authenticated via AuthKey") Msg("Successfully authenticated via AuthKey")
} }

29
app.go
View file

@ -55,8 +55,8 @@ const (
HTTPReadTimeout = 30 * time.Second HTTPReadTimeout = 30 * time.Second
privateKeyFileMode = 0o600 privateKeyFileMode = 0o600
requestedExpiryCacheExpiration = time.Minute * 5 registerCacheExpiration = time.Minute * 15
requestedExpiryCacheCleanupInterval = time.Minute * 10 registerCacheCleanup = time.Minute * 20
errUnsupportedDatabase = Error("unsupported DB") errUnsupportedDatabase = Error("unsupported DB")
errUnsupportedLetsEncryptChallengeType = Error( errUnsupportedLetsEncryptChallengeType = Error(
@ -148,11 +148,10 @@ type Headscale struct {
lastStateChange sync.Map lastStateChange sync.Map
oidcProvider *oidc.Provider oidcProvider *oidc.Provider
oauth2Config *oauth2.Config oauth2Config *oauth2.Config
oidcStateCache *cache.Cache
requestedExpiryCache *cache.Cache registrationCache *cache.Cache
ipAllocationMutex sync.Mutex ipAllocationMutex sync.Mutex
} }
@ -202,18 +201,18 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
return nil, errUnsupportedDatabase return nil, errUnsupportedDatabase
} }
requestedExpiryCache := cache.New( registrationCache := cache.New(
requestedExpiryCacheExpiration, registerCacheExpiration,
requestedExpiryCacheCleanupInterval, registerCacheCleanup,
) )
app := Headscale{ app := Headscale{
cfg: cfg, cfg: cfg,
dbType: cfg.DBtype, dbType: cfg.DBtype,
dbString: dbString, dbString: dbString,
privateKey: privKey, privateKey: privKey,
aclRules: tailcfg.FilterAllowAll, // default allowall aclRules: tailcfg.FilterAllowAll, // default allowall
requestedExpiryCache: requestedExpiryCache, registrationCache: registrationCache,
} }
err = app.initDB() err = app.initDB()

View file

@ -5,7 +5,6 @@ import (
"os" "os"
"testing" "testing"
"github.com/patrickmn/go-cache"
"gopkg.in/check.v1" "gopkg.in/check.v1"
"inet.af/netaddr" "inet.af/netaddr"
) )
@ -50,10 +49,6 @@ func (s *Suite) ResetDB(c *check.C) {
cfg: cfg, cfg: cfg,
dbType: "sqlite3", dbType: "sqlite3",
dbString: tmpDir + "/headscale_test.db", dbString: tmpDir + "/headscale_test.db",
requestedExpiryCache: cache.New(
requestedExpiryCacheExpiration,
requestedExpiryCacheCleanupInterval,
),
} }
err = app.initDB() err = app.initDB()
if err != nil { if err != nil {

View file

@ -1,38 +0,0 @@
package headscale
import (
"time"
"gopkg.in/check.v1"
"inet.af/netaddr"
)
func (s *Suite) TestRegisterMachine(c *check.C) {
namespace, err := app.CreateNamespace("test")
c.Assert(err, check.IsNil)
now := time.Now().UTC()
machine := Machine{
ID: 0,
MachineKey: "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e",
NodeKey: "bar",
DiscoKey: "faa",
Name: "testmachine",
NamespaceID: namespace.ID,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")},
Expiry: &now,
}
err = app.db.Save(&machine).Error
c.Assert(err, check.IsNil)
_, err = app.GetMachine(namespace.Name, machine.Name)
c.Assert(err, check.IsNil)
machineAfterRegistering, err := app.RegisterMachine(
machine.MachineKey,
namespace.Name,
)
c.Assert(err, check.IsNil)
c.Assert(machineAfterRegistering.Registered, check.Equals, true)
}

33
db.go
View file

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/rs/zerolog/log"
"gorm.io/driver/postgres" "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
@ -39,6 +40,38 @@ func (h *Headscale) initDB() error {
_ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses") _ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses")
// If the Machine table has a column for registered,
// find all occourences of "false" and drop them. Then
// remove the column.
if db.Migrator().HasColumn(&Machine{}, "registered") {
log.Info().
Msg(`Database has legacy "registered" column in machine, removing...`)
machines := Machines{}
if err := h.db.Not("registered").Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db")
}
for _, machine := range machines {
log.Info().
Str("machine", machine.Name).
Str("machine_key", machine.MachineKey).
Msg("Deleting unregistered machine")
if err := h.db.Delete(&Machine{}, machine.ID).Error; err != nil {
log.Error().
Err(err).
Str("machine", machine.Name).
Str("machine_key", machine.MachineKey).
Msg("Error deleting unregistered machine")
}
}
err := db.Migrator().DropColumn(&Machine{}, "registered")
if err != nil {
log.Error().Err(err).Msg("Error dropping registered column")
}
}
err = db.AutoMigrate(&Machine{}) err = db.AutoMigrate(&Machine{})
if err != nil { if err != nil {
return err return err

View file

@ -164,7 +164,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_1", Name: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
@ -182,7 +181,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_2", Name: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
@ -200,7 +198,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_3", Name: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
@ -218,7 +215,6 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_4", Name: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(PreAuthKey2InShared1.ID), AuthKeyID: uint(PreAuthKey2InShared1.ID),
@ -311,7 +307,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_1", Name: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyInShared1.ID), AuthKeyID: uint(preAuthKeyInShared1.ID),
@ -329,7 +324,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_2", Name: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyInShared2.ID), AuthKeyID: uint(preAuthKeyInShared2.ID),
@ -347,7 +341,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_3", Name: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyInShared3.ID), AuthKeyID: uint(preAuthKeyInShared3.ID),
@ -365,7 +358,6 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
Name: "test_get_shared_nodes_4", Name: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2InShared1.ID), AuthKeyID: uint(preAuthKey2InShared1.ID),

View file

@ -85,13 +85,12 @@ type Machine struct {
IpAddresses []string `protobuf:"bytes,5,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,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"` Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"`
Namespace *Namespace `protobuf:"bytes,7,opt,name=namespace,proto3" json:"namespace,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"` LastSeen *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
RegisterMethod RegisterMethod `protobuf:"varint,9,opt,name=register_method,json=registerMethod,proto3,enum=headscale.v1.RegisterMethod" json:"register_method,omitempty"` LastSuccessfulUpdate *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=last_successful_update,json=lastSuccessfulUpdate,proto3" json:"last_successful_update,omitempty"`
LastSeen *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"` Expiry *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=expiry,proto3" json:"expiry,omitempty"`
LastSuccessfulUpdate *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=last_successful_update,json=lastSuccessfulUpdate,proto3" json:"last_successful_update,omitempty"` PreAuthKey *PreAuthKey `protobuf:"bytes,11,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"`
Expiry *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=expiry,proto3" json:"expiry,omitempty"` CreatedAt *timestamppb.Timestamp `protobuf:"bytes,12,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
PreAuthKey *PreAuthKey `protobuf:"bytes,13,opt,name=pre_auth_key,json=preAuthKey,proto3" json:"pre_auth_key,omitempty"` RegisterMethod RegisterMethod `protobuf:"varint,13,opt,name=register_method,json=registerMethod,proto3,enum=headscale.v1.RegisterMethod" json:"register_method,omitempty"`
CreatedAt *timestamppb.Timestamp `protobuf:"bytes,14,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
} }
func (x *Machine) Reset() { func (x *Machine) Reset() {
@ -175,20 +174,6 @@ func (x *Machine) GetNamespace() *Namespace {
return nil return nil
} }
func (x *Machine) GetRegistered() bool {
if x != nil {
return x.Registered
}
return false
}
func (x *Machine) GetRegisterMethod() RegisterMethod {
if x != nil {
return x.RegisterMethod
}
return RegisterMethod_REGISTER_METHOD_UNSPECIFIED
}
func (x *Machine) GetLastSeen() *timestamppb.Timestamp { func (x *Machine) GetLastSeen() *timestamppb.Timestamp {
if x != nil { if x != nil {
return x.LastSeen return x.LastSeen
@ -224,6 +209,13 @@ func (x *Machine) GetCreatedAt() *timestamppb.Timestamp {
return nil return nil
} }
func (x *Machine) GetRegisterMethod() RegisterMethod {
if x != nil {
return x.RegisterMethod
}
return RegisterMethod_REGISTER_METHOD_UNSPECIFIED
}
type RegisterMachineRequest struct { type RegisterMachineRequest struct {
state protoimpl.MessageState state protoimpl.MessageState
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@ -822,7 +814,7 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 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, 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, 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, 0xfd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xdd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63,
0x68, 0x69, 0x6e, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 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, 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, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69,
@ -836,33 +828,31 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 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, 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, 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, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x09, 0x6c,
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74,
0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63,
0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x09,
0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
0x6f, 0x64, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c,
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79,
0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e,
0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41,
0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d,
0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41,
0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x74, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65,
0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61,
0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74,
0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74,
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69,
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, 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, 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, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65,
@ -962,12 +952,12 @@ var file_headscale_v1_machine_proto_goTypes = []interface{}{
} }
var file_headscale_v1_machine_proto_depIdxs = []int32{ var file_headscale_v1_machine_proto_depIdxs = []int32{
14, // 0: headscale.v1.Machine.namespace:type_name -> headscale.v1.Namespace 14, // 0: headscale.v1.Machine.namespace:type_name -> headscale.v1.Namespace
0, // 1: headscale.v1.Machine.register_method:type_name -> headscale.v1.RegisterMethod 15, // 1: headscale.v1.Machine.last_seen:type_name -> google.protobuf.Timestamp
15, // 2: headscale.v1.Machine.last_seen:type_name -> google.protobuf.Timestamp 15, // 2: headscale.v1.Machine.last_successful_update:type_name -> google.protobuf.Timestamp
15, // 3: headscale.v1.Machine.last_successful_update:type_name -> google.protobuf.Timestamp 15, // 3: headscale.v1.Machine.expiry:type_name -> google.protobuf.Timestamp
15, // 4: headscale.v1.Machine.expiry:type_name -> google.protobuf.Timestamp 16, // 4: headscale.v1.Machine.pre_auth_key:type_name -> headscale.v1.PreAuthKey
16, // 5: headscale.v1.Machine.pre_auth_key:type_name -> headscale.v1.PreAuthKey 15, // 5: headscale.v1.Machine.created_at:type_name -> google.protobuf.Timestamp
15, // 6: headscale.v1.Machine.created_at:type_name -> google.protobuf.Timestamp 0, // 6: headscale.v1.Machine.register_method:type_name -> headscale.v1.RegisterMethod
1, // 7: headscale.v1.RegisterMachineResponse.machine:type_name -> headscale.v1.Machine 1, // 7: headscale.v1.RegisterMachineResponse.machine:type_name -> headscale.v1.Machine
1, // 8: headscale.v1.GetMachineResponse.machine:type_name -> headscale.v1.Machine 1, // 8: headscale.v1.GetMachineResponse.machine:type_name -> headscale.v1.Machine
1, // 9: headscale.v1.ExpireMachineResponse.machine:type_name -> headscale.v1.Machine 1, // 9: headscale.v1.ExpireMachineResponse.machine:type_name -> headscale.v1.Machine

View file

@ -885,12 +885,6 @@
"namespace": { "namespace": {
"$ref": "#/definitions/v1Namespace" "$ref": "#/definitions/v1Namespace"
}, },
"registered": {
"type": "boolean"
},
"registerMethod": {
"$ref": "#/definitions/v1RegisterMethod"
},
"lastSeen": { "lastSeen": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
@ -909,6 +903,9 @@
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"format": "date-time" "format": "date-time"
},
"registerMethod": {
"$ref": "#/definitions/v1RegisterMethod"
} }
} }
}, },

View file

@ -157,9 +157,11 @@ func (api headscaleV1APIServer) RegisterMachine(
Str("namespace", request.GetNamespace()). Str("namespace", request.GetNamespace()).
Str("machine_key", request.GetKey()). Str("machine_key", request.GetKey()).
Msg("Registering machine") Msg("Registering machine")
machine, err := api.h.RegisterMachine(
machine, err := api.h.RegisterMachineFromAuthCallback(
request.GetKey(), request.GetKey(),
request.GetNamespace(), request.GetNamespace(),
RegisterMethodCLI,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -379,11 +381,11 @@ func (api headscaleV1APIServer) DebugCreateMachine(
HostInfo: HostInfo(hostinfo), HostInfo: HostInfo(hostinfo),
} }
// log.Trace().Caller().Interface("machine", newMachine).Msg("") api.h.registrationCache.Set(
request.GetKey(),
if err := api.h.db.Create(&newMachine).Error; err != nil { newMachine,
return nil, err registerCacheExpiration,
} )
return &v1.DebugCreateMachineResponse{Machine: newMachine.toProto()}, nil return &v1.DebugCreateMachineResponse{Machine: newMachine.toProto()}, nil
} }

View file

@ -621,12 +621,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "machine-4", listAll[3].Name) assert.Equal(s.T(), "machine-4", listAll[3].Name)
assert.Equal(s.T(), "machine-5", listAll[4].Name) assert.Equal(s.T(), "machine-5", listAll[4].Name)
assert.True(s.T(), listAll[0].Registered)
assert.True(s.T(), listAll[1].Registered)
assert.True(s.T(), listAll[2].Registered)
assert.True(s.T(), listAll[3].Registered)
assert.True(s.T(), listAll[4].Registered)
otherNamespaceMachineKeys := []string{ otherNamespaceMachineKeys := []string{
"b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e", "b5b444774186d4217adcec407563a1223929465ee2c68a4da13af0d0185b4f8e",
"dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584", "dc721977ac7415aafa87f7d4574cbe07c6b171834a6d37375782bdc1fb6b3584",
@ -710,9 +704,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
assert.Equal(s.T(), "otherNamespace-machine-1", listAllWithotherNamespace[5].Name) assert.Equal(s.T(), "otherNamespace-machine-1", listAllWithotherNamespace[5].Name)
assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name) assert.Equal(s.T(), "otherNamespace-machine-2", listAllWithotherNamespace[6].Name)
assert.True(s.T(), listAllWithotherNamespace[5].Registered)
assert.True(s.T(), listAllWithotherNamespace[6].Registered)
// Test list all nodes after added otherNamespace // Test list all nodes after added otherNamespace
listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand( listOnlyotherNamespaceMachineNamespaceResult, err := ExecuteCommand(
&s.headscale, &s.headscale,
@ -752,9 +743,6 @@ func (s *IntegrationCLITestSuite) TestNodeCommand() {
listOnlyotherNamespaceMachineNamespace[1].Name, listOnlyotherNamespaceMachineNamespace[1].Name,
) )
assert.True(s.T(), listOnlyotherNamespaceMachineNamespace[0].Registered)
assert.True(s.T(), listOnlyotherNamespaceMachineNamespace[1].Registered)
// Delete a machines // Delete a machines
_, err = ExecuteCommand( _, err = ExecuteCommand(
&s.headscale, &s.headscale,
@ -979,7 +967,6 @@ func (s *IntegrationCLITestSuite) TestRouteCommand() {
assert.Equal(s.T(), uint64(1), machine.Id) assert.Equal(s.T(), uint64(1), machine.Id)
assert.Equal(s.T(), "route-machine", machine.Name) assert.Equal(s.T(), "route-machine", machine.Name)
assert.True(s.T(), machine.Registered)
listAllResult, err := ExecuteCommand( listAllResult, err := ExecuteCommand(
&s.headscale, &s.headscale,

View file

@ -18,11 +18,14 @@ import (
) )
const ( const (
errMachineNotFound = Error("machine not found") errMachineNotFound = Error("machine not found")
errMachineAlreadyRegistered = Error("machine already registered") errMachineRouteIsNotAvailable = Error("route is not available on machine")
errMachineRouteIsNotAvailable = Error("route is not available on machine") errMachineAddressesInvalid = Error("failed to parse machine addresses")
errMachineAddressesInvalid = Error("failed to parse machine addresses") errMachineNotFoundRegistrationCache = Error(
errHostnameTooLong = Error("Hostname too long") "machine not found in registration cache",
)
errCouldNotConvertMachineInterface = Error("failed to convert machine interface")
errHostnameTooLong = Error("Hostname too long")
) )
const ( const (
@ -40,10 +43,11 @@ type Machine struct {
NamespaceID uint NamespaceID uint
Namespace Namespace `gorm:"foreignKey:NamespaceID"` Namespace Namespace `gorm:"foreignKey:NamespaceID"`
Registered bool // temp
RegisterMethod string RegisterMethod string
AuthKeyID uint
AuthKey *PreAuthKey // TODO(kradalby): This seems like irrelevant information?
AuthKeyID uint
AuthKey *PreAuthKey
LastSeen *time.Time LastSeen *time.Time
LastSuccessfulUpdate *time.Time LastSuccessfulUpdate *time.Time
@ -63,11 +67,6 @@ type (
MachinesP []*Machine MachinesP []*Machine
) )
// For the time being this method is rather naive.
func (machine Machine) isRegistered() bool {
return machine.Registered
}
type MachineAddresses []netaddr.IP type MachineAddresses []netaddr.IP
func (ma MachineAddresses) ToStringSlice() []string { func (ma MachineAddresses) ToStringSlice() []string {
@ -114,7 +113,7 @@ func (machine Machine) isExpired() bool {
// If Expiry is not set, the client has not indicated that // If Expiry is not set, the client has not indicated that
// it wants an expiry time, it is therefor considered // it wants an expiry time, it is therefor considered
// to mean "not expired" // to mean "not expired"
if machine.Expiry.IsZero() { if machine.Expiry == nil || machine.Expiry.IsZero() {
return false return false
} }
@ -171,6 +170,12 @@ func getFilteredByACLPeers(
machine.IPAddresses.ToStringSlice(), machine.IPAddresses.ToStringSlice(),
peer.IPAddresses.ToStringSlice(), peer.IPAddresses.ToStringSlice(),
) || // match source and destination ) || // match source and destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
peer.IPAddresses.ToStringSlice(),
machine.IPAddresses.ToStringSlice(),
) || // match return path
matchSourceAndDestinationWithRule( matchSourceAndDestinationWithRule(
rule.SrcIPs, rule.SrcIPs,
dst, dst,
@ -180,9 +185,21 @@ func getFilteredByACLPeers(
matchSourceAndDestinationWithRule( matchSourceAndDestinationWithRule(
rule.SrcIPs, rule.SrcIPs,
dst, dst,
[]string{"*"},
[]string{"*"},
) || // match source and all destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
peer.IPAddresses.ToStringSlice(), peer.IPAddresses.ToStringSlice(),
) || // match source and all destination
matchSourceAndDestinationWithRule(
rule.SrcIPs,
dst,
[]string{"*"},
machine.IPAddresses.ToStringSlice(), machine.IPAddresses.ToStringSlice(),
) { // match return path ) { // match all sources and source
peers[peer.ID] = peer peers[peer.ID] = peer
} }
} }
@ -212,7 +229,7 @@ func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
Msg("Finding direct peers") Msg("Finding direct peers")
machines := Machines{} machines := Machines{}
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ? AND registered", if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ?",
machine.MachineKey).Find(&machines).Error; err != nil { machine.MachineKey).Find(&machines).Error; err != nil {
log.Error().Err(err).Msg("Error accessing db") log.Error().Err(err).Msg("Error accessing db")
@ -275,7 +292,7 @@ func (h *Headscale) getValidPeers(machine *Machine) (Machines, error) {
} }
for _, peer := range peers { for _, peer := range peers {
if peer.isRegistered() && !peer.isExpired() { if !peer.isExpired() {
validPeers = append(validPeers, peer) validPeers = append(validPeers, peer)
} }
} }
@ -364,8 +381,6 @@ func (h *Headscale) RefreshMachine(machine *Machine, expiry time.Time) {
// DeleteMachine softs deletes a Machine from the database. // DeleteMachine softs deletes a Machine from the database.
func (h *Headscale) DeleteMachine(machine *Machine) error { func (h *Headscale) DeleteMachine(machine *Machine) error {
machine.Registered = false
h.db.Save(&machine) // we mark it as unregistered, just in case
if err := h.db.Delete(&machine).Error; err != nil { if err := h.db.Delete(&machine).Error; err != nil {
return err return err
} }
@ -581,7 +596,7 @@ func (machine Machine) toNode(
LastSeen: machine.LastSeen, LastSeen: machine.LastSeen,
KeepAlive: true, KeepAlive: true,
MachineAuthorized: machine.Registered, MachineAuthorized: !machine.isExpired(),
Capabilities: []string{tailcfg.CapabilityFileSharing}, Capabilities: []string{tailcfg.CapabilityFileSharing},
} }
@ -599,8 +614,6 @@ func (machine *Machine) toProto() *v1.Machine {
Name: machine.Name, Name: machine.Name,
Namespace: machine.Namespace.toProto(), Namespace: machine.Namespace.toProto(),
Registered: machine.Registered,
// TODO(kradalby): Implement register method enum converter // TODO(kradalby): Implement register method enum converter
// RegisterMethod: , // RegisterMethod: ,
@ -628,74 +641,50 @@ func (machine *Machine) toProto() *v1.Machine {
return machineProto return machineProto
} }
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey. func (h *Headscale) RegisterMachineFromAuthCallback(
func (h *Headscale) RegisterMachine(
machineKeyStr string, machineKeyStr string,
namespaceName string, namespaceName string,
registrationMethod string,
) (*Machine, error) { ) (*Machine, error) {
namespace, err := h.GetNamespace(namespaceName) if machineInterface, ok := h.registrationCache.Get(machineKeyStr); ok {
if err != nil { if registrationMachine, ok := machineInterface.(Machine); ok {
return nil, err namespace, err := h.GetNamespace(namespaceName)
} if err != nil {
return nil, fmt.Errorf(
"failed to find namespace in register machine from auth callback, %w",
err,
)
}
var machineKey key.MachinePublic registrationMachine.NamespaceID = namespace.ID
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) registrationMachine.RegisterMethod = registrationMethod
if err != nil {
return nil, err
}
log.Trace(). machine, err := h.RegisterMachine(
Caller(). registrationMachine,
Str("machine_key_str", machineKeyStr). )
Str("machine_key", machineKey.String()).
Msg("Registering machine")
machine, err := h.GetMachineByMachineKey(machineKey) return machine, err
if err != nil { } else {
return nil, err return nil, errCouldNotConvertMachineInterface
}
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set
// This means that if a user is to slow with register a machine, it will possibly not
// have the correct expiry.
requestedTime := time.Time{}
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found {
log.Trace().
Caller().
Str("machine", machine.Name).
Msg("Expiry time found in cache, assigning to node")
if reqTime, ok := requestedTimeIf.(time.Time); ok {
requestedTime = reqTime
} }
} }
if machine.isRegistered() { return nil, errMachineNotFoundRegistrationCache
log.Trace(). }
Caller().
Str("machine", machine.Name).
Msg("machine already registered, reauthenticating")
h.RefreshMachine(machine, requestedTime) // RegisterMachine is executed from the CLI to register a new Machine using its MachineKey.
func (h *Headscale) RegisterMachine(machine Machine,
return machine, nil ) (*Machine, error) {
} log.Trace().
Caller().
Str("machine_key", machine.MachineKey).
Msg("Registering machine")
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", machine.Name). Str("machine", machine.Name).
Msg("Attempting to register machine") Msg("Attempting to register machine")
if machine.isRegistered() {
err := errMachineAlreadyRegistered
log.Error().
Caller().
Err(err).
Str("machine", machine.Name).
Msg("Attempting to register machine")
return nil, err
}
h.ipAllocationMutex.Lock() h.ipAllocationMutex.Lock()
defer h.ipAllocationMutex.Unlock() defer h.ipAllocationMutex.Unlock()
@ -710,17 +699,8 @@ func (h *Headscale) RegisterMachine(
return nil, err return nil, err
} }
log.Trace().
Caller().
Str("machine", machine.Name).
Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Found IP for host")
machine.IPAddresses = ips machine.IPAddresses = ips
machine.NamespaceID = namespace.ID
machine.Registered = true
machine.RegisterMethod = RegisterMethodCLI
machine.Expiry = &requestedTime
h.db.Save(&machine) h.db.Save(&machine)
log.Trace(). log.Trace().
@ -729,7 +709,7 @@ func (h *Headscale) RegisterMachine(
Str("ip", strings.Join(ips.ToStringSlice(), ",")). Str("ip", strings.Join(ips.ToStringSlice(), ",")).
Msg("Machine registered with the database") Msg("Machine registered with the database")
return machine, nil return &machine, nil
} }
func (machine *Machine) GetAdvertisedRoutes() []netaddr.IPPrefix { func (machine *Machine) GetAdvertisedRoutes() []netaddr.IPPrefix {

View file

@ -29,7 +29,6 @@ func (s *Suite) TestGetMachine(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
@ -56,7 +55,6 @@ func (s *Suite) TestGetMachineByID(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
@ -76,7 +74,6 @@ func (s *Suite) TestDeleteMachine(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(1), AuthKeyID: uint(1),
} }
@ -99,7 +96,6 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine3", Name: "testmachine3",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(1), AuthKeyID: uint(1),
} }
@ -130,7 +126,6 @@ func (s *Suite) TestListPeers(c *check.C) {
DiscoKey: "faa" + strconv.Itoa(index), DiscoKey: "faa" + strconv.Itoa(index),
Name: "testmachine" + strconv.Itoa(index), Name: "testmachine" + strconv.Itoa(index),
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
@ -179,7 +174,6 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
}, },
Name: "testmachine" + strconv.Itoa(index), Name: "testmachine" + strconv.Itoa(index),
NamespaceID: stor[index%2].namespace.ID, NamespaceID: stor[index%2].namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(stor[index%2].key.ID), AuthKeyID: uint(stor[index%2].key.ID),
} }
@ -246,7 +240,6 @@ func (s *Suite) TestExpireMachine(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
Expiry: &time.Time{}, Expiry: &time.Time{},
@ -284,6 +277,7 @@ func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) {
} }
} }
// nolint
func Test_getFilteredByACLPeers(t *testing.T) { func Test_getFilteredByACLPeers(t *testing.T) {
type args struct { type args struct {
machines []Machine machines []Machine
@ -431,7 +425,7 @@ func Test_getFilteredByACLPeers(t *testing.T) {
}, },
}, },
machine: &Machine{ // current machine machine: &Machine{ // current machine
ID: 1, ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")}, IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"}, Namespace: Namespace{Name: "marc"},
}, },
@ -444,6 +438,208 @@ func Test_getFilteredByACLPeers(t *testing.T) {
}, },
}, },
}, },
{
name: "rules allows all hosts to reach one destination",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
},
want: Machines{
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
},
},
{
name: "rules allows all hosts to reach one destination, destination can reach all hosts",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "100.64.0.2"},
},
},
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
},
{
name: "rule allows all hosts to reach all destinations",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
{
SrcIPs: []string{"*"},
DstPorts: []tailcfg.NetPortRange{
{IP: "*"},
},
},
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 3,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.3")},
Namespace: Namespace{Name: "mickael"},
},
},
},
{
name: "without rule all communications are forbidden",
args: args{
machines: []Machine{ // list of all machines in the database
{
ID: 1,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.1"),
},
Namespace: Namespace{Name: "joe"},
},
{
ID: 2,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.2"),
},
Namespace: Namespace{Name: "marc"},
},
{
ID: 3,
IPAddresses: MachineAddresses{
netaddr.MustParseIP("100.64.0.3"),
},
Namespace: Namespace{Name: "mickael"},
},
},
rules: []tailcfg.FilterRule{ // list of all ACLRules registered
},
machine: &Machine{ // current machine
ID: 2,
IPAddresses: MachineAddresses{netaddr.MustParseIP("100.64.0.2")},
Namespace: Namespace{Name: "marc"},
},
},
want: Machines{},
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {

View file

@ -54,7 +54,6 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
@ -146,7 +145,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_1", Name: "test_get_shared_nodes_1",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")},
AuthKeyID: uint(preAuthKeyShared1.ID), AuthKeyID: uint(preAuthKeyShared1.ID),
@ -164,7 +162,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_2", Name: "test_get_shared_nodes_2",
NamespaceID: namespaceShared2.ID, NamespaceID: namespaceShared2.ID,
Namespace: *namespaceShared2, Namespace: *namespaceShared2,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")},
AuthKeyID: uint(preAuthKeyShared2.ID), AuthKeyID: uint(preAuthKeyShared2.ID),
@ -182,7 +179,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_3", Name: "test_get_shared_nodes_3",
NamespaceID: namespaceShared3.ID, NamespaceID: namespaceShared3.ID,
Namespace: *namespaceShared3, Namespace: *namespaceShared3,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")},
AuthKeyID: uint(preAuthKeyShared3.ID), AuthKeyID: uint(preAuthKeyShared3.ID),
@ -200,7 +196,6 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
Name: "test_get_shared_nodes_4", Name: "test_get_shared_nodes_4",
NamespaceID: namespaceShared1.ID, NamespaceID: namespaceShared1.ID,
Namespace: *namespaceShared1, Namespace: *namespaceShared1,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")},
AuthKeyID: uint(preAuthKey2Shared1.ID), AuthKeyID: uint(preAuthKey2Shared1.ID),

137
oidc.go
View file

@ -10,21 +10,16 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/patrickmn/go-cache"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gorm.io/gorm"
"tailscale.com/types/key" "tailscale.com/types/key"
) )
const ( const (
oidcStateCacheExpiration = time.Minute * 5 randomByteSize = 16
oidcStateCacheCleanupInterval = time.Minute * 10
randomByteSize = 16
) )
type IDTokenClaims struct { type IDTokenClaims struct {
@ -61,14 +56,6 @@ func (h *Headscale) initOIDC() error {
} }
} }
// init the state cache if it hasn't been already
if h.oidcStateCache == nil {
h.oidcStateCache = cache.New(
oidcStateCacheExpiration,
oidcStateCacheCleanupInterval,
)
}
return nil return nil
} }
@ -101,7 +88,7 @@ func (h *Headscale) RegisterOIDC(ctx *gin.Context) {
stateStr := hex.EncodeToString(randomBlob)[:32] stateStr := hex.EncodeToString(randomBlob)[:32]
// place the machine key into the state cache, so it can be retrieved later // place the machine key into the state cache, so it can be retrieved later
h.oidcStateCache.Set(stateStr, machineKeyStr, oidcStateCacheExpiration) h.registrationCache.Set(stateStr, machineKeyStr, registerCacheExpiration)
authURL := h.oauth2Config.AuthCodeURL(stateStr) authURL := h.oauth2Config.AuthCodeURL(stateStr)
log.Debug().Msgf("Redirecting to %s for authentication", authURL) log.Debug().Msgf("Redirecting to %s for authentication", authURL)
@ -125,7 +112,6 @@ var oidcCallbackTemplate = template.Must(
</html>`), </html>`),
) )
// TODO: Why is the entire machine registration logic duplicated here?
// OIDCCallback handles the callback from the OIDC endpoint // OIDCCallback handles the callback from the OIDC endpoint
// Retrieves the mkey from the state cache and adds the machine to the users email namespace // 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 // TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities
@ -197,7 +183,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
} }
// retrieve machinekey from state cache // retrieve machinekey from state cache
machineKeyIf, machineKeyFound := h.oidcStateCache.Get(state) machineKeyIf, machineKeyFound := h.registrationCache.Get(state)
if !machineKeyFound { if !machineKeyFound {
log.Error(). log.Error().
@ -207,10 +193,12 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return return
} }
machineKeyStr, machineKeyOK := machineKeyIf.(string) machineKeyFromCache, machineKeyOK := machineKeyIf.(string)
var machineKey key.MachinePublic var machineKey key.MachinePublic
err = machineKey.UnmarshalText([]byte(MachinePublicKeyEnsurePrefix(machineKeyStr))) err = machineKey.UnmarshalText(
[]byte(MachinePublicKeyEnsurePrefix(machineKeyFromCache)),
)
if err != nil { if err != nil {
log.Error(). log.Error().
Msg("could not parse machine public key") Msg("could not parse machine public key")
@ -229,33 +217,19 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return return
} }
// TODO(kradalby): Currently, if it fails to find a requested expiry, non will be set // retrieve machine information if it exist
requestedTime := time.Time{} // The error is not important, because if it does not
if requestedTimeIf, found := h.requestedExpiryCache.Get(machineKey.String()); found { // exist, then this is a new machine and we will move
if reqTime, ok := requestedTimeIf.(time.Time); ok { // on to registration.
requestedTime = reqTime machine, _ := h.GetMachineByMachineKey(machineKey)
}
}
// retrieve machine information if machine != nil {
machine, err := h.GetMachineByMachineKey(machineKey)
if err != nil {
log.Error().Msg("machine key not found in database")
ctx.String(
http.StatusInternalServerError,
"could not get machine info from database",
)
return
}
if machine.isRegistered() {
log.Trace(). log.Trace().
Caller(). Caller().
Str("machine", machine.Name). Str("machine", machine.Name).
Msg("machine already registered, reauthenticating") Msg("machine already registered, reauthenticating")
h.RefreshMachine(machine, requestedTime) h.RefreshMachine(machine, *machine.Expiry)
var content bytes.Buffer var content bytes.Buffer
if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{ if err := oidcCallbackTemplate.Execute(&content, oidcCallbackTemplateConfig{
@ -279,8 +253,6 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return return
} }
now := time.Now().UTC()
namespaceName, err := NormalizeNamespaceName( namespaceName, err := NormalizeNamespaceName(
claims.Email, claims.Email,
h.cfg.OIDC.StripEmaildomain, h.cfg.OIDC.StripEmaildomain,
@ -294,61 +266,58 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) {
return return
} }
// register the machine if it's new // register the machine if it's new
if !machine.Registered { log.Debug().Msg("Registering new machine after successful callback")
log.Debug().Msg("Registering new machine after successful callback")
namespace, err := h.GetNamespace(namespaceName) namespace, err := h.GetNamespace(namespaceName)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, errNamespaceNotFound) {
namespace, err = h.CreateNamespace(namespaceName) namespace, err = h.CreateNamespace(namespaceName)
if err != nil {
log.Error().
Err(err).
Caller().
Msgf("could not create new namespace '%s'", namespaceName)
ctx.String(
http.StatusInternalServerError,
"could not create new namespace",
)
return
}
} else if err != nil {
log.Error().
Caller().
Err(err).
Str("namespace", namespaceName).
Msg("could not find or create namespace")
ctx.String(
http.StatusInternalServerError,
"could not find or create namespace",
)
return
}
ips, err := h.getAvailableIPs()
if err != nil { if err != nil {
log.Error(). log.Error().
Caller().
Err(err). Err(err).
Msg("could not get an IP from the pool") Caller().
Msgf("could not create new namespace '%s'", namespaceName)
ctx.String( ctx.String(
http.StatusInternalServerError, http.StatusInternalServerError,
"could not get an IP from the pool", "could not create new namespace",
) )
return return
} }
} else if err != nil {
log.Error().
Caller().
Err(err).
Str("namespace", namespaceName).
Msg("could not find or create namespace")
ctx.String(
http.StatusInternalServerError,
"could not find or create namespace",
)
machine.IPAddresses = ips return
machine.NamespaceID = namespace.ID }
machine.Registered = true
machine.RegisterMethod = RegisterMethodOIDC machineKeyStr := MachinePublicKeyStripPrefix(machineKey)
machine.LastSuccessfulUpdate = &now
machine.Expiry = &requestedTime _, err = h.RegisterMachineFromAuthCallback(
h.db.Save(&machine) machineKeyStr,
namespace.Name,
RegisterMethodOIDC,
)
if err != nil {
log.Error().
Caller().
Err(err).
Msg("could not register machine")
ctx.String(
http.StatusInternalServerError,
"could not register machine",
)
return
} }
var content bytes.Buffer var content bytes.Buffer

View file

@ -113,6 +113,12 @@ func (h *Headscale) ExpirePreAuthKey(k *PreAuthKey) error {
return nil return nil
} }
// UsePreAuthKey marks a PreAuthKey as used.
func (h *Headscale) UsePreAuthKey(k *PreAuthKey) {
k.Used = true
h.db.Save(k)
}
// checkKeyValidity does the heavy lifting for validation of the PreAuthKey coming from a node // checkKeyValidity does the heavy lifting for validation of the PreAuthKey coming from a node
// If returns no error and a PreAuthKey, it can be used. // If returns no error and a PreAuthKey, it can be used.
func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) { func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {

View file

@ -80,7 +80,6 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testest", Name: "testest",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
@ -105,7 +104,6 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testest", Name: "testest",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }
@ -143,7 +141,6 @@ func (*Suite) TestEphemeralKey(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testest", Name: "testest",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
LastSeen: &now, LastSeen: &now,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),

View file

@ -22,16 +22,16 @@ message Machine {
string name = 6; string name = 6;
Namespace namespace = 7; Namespace namespace = 7;
bool registered = 8;
RegisterMethod register_method = 9;
google.protobuf.Timestamp last_seen = 10; google.protobuf.Timestamp last_seen = 8;
google.protobuf.Timestamp last_successful_update = 11; google.protobuf.Timestamp last_successful_update = 9;
google.protobuf.Timestamp expiry = 12; google.protobuf.Timestamp expiry = 10;
PreAuthKey pre_auth_key = 13; PreAuthKey pre_auth_key = 11;
google.protobuf.Timestamp created_at = 14; google.protobuf.Timestamp created_at = 12;
RegisterMethod register_method = 13;
// google.protobuf.Timestamp updated_at = 14; // google.protobuf.Timestamp updated_at = 14;
// google.protobuf.Timestamp deleted_at = 15; // google.protobuf.Timestamp deleted_at = 15;

View file

@ -30,7 +30,6 @@ func (s *Suite) TestGetRoutes(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "test_get_route_machine", Name: "test_get_route_machine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),
@ -82,7 +81,6 @@ func (s *Suite) TestGetEnableRoutes(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "test_enable_route_machine", Name: "test_enable_route_machine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
HostInfo: HostInfo(hostInfo), HostInfo: HostInfo(hostInfo),

View file

@ -36,7 +36,6 @@ func (s *Suite) TestGetUsedIps(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
IPAddresses: ips, IPAddresses: ips,
@ -85,7 +84,6 @@ func (s *Suite) TestGetMultiIp(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
IPAddresses: ips, IPAddresses: ips,
@ -176,7 +174,6 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) {
DiscoKey: "faa", DiscoKey: "faa",
Name: "testmachine", Name: "testmachine",
NamespaceID: namespace.ID, NamespaceID: namespace.ID,
Registered: true,
RegisterMethod: RegisterMethodAuthKey, RegisterMethod: RegisterMethodAuthKey,
AuthKeyID: uint(pak.ID), AuthKeyID: uint(pak.ID),
} }