mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-30 02:43:05 +00:00
Merge pull request #357 from kradalby/make-namespace-to-users
Remove boundaries between Namespaces
This commit is contained in:
commit
8689a39c96
8 changed files with 117 additions and 101 deletions
|
@ -1,14 +1,17 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
**TBD (TBD):**
|
|
||||||
|
|
||||||
**0.15.0 (2022-xx-xx):**
|
**0.15.0 (2022-xx-xx):**
|
||||||
|
|
||||||
|
**BREAKING**:
|
||||||
|
|
||||||
|
- 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).
|
||||||
|
|
||||||
**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)
|
||||||
|
|
||||||
**0.14.0 (2022-02-25):**
|
**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
|
||||||
|
|
86
README.md
86
README.md
|
@ -2,44 +2,67 @@
|
||||||
|
|
||||||
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
|
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
|
||||||
|
|
||||||
An open source, self-hosted implementation of the Tailscale coordination server.
|
An open source, self-hosted implementation of the Tailscale control server.
|
||||||
|
|
||||||
Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.
|
Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.
|
||||||
|
|
||||||
**Note:** Always select the same GitHub tag as the released version you use to ensure you have the correct example configuration and documentation. The `main` branch might contain unreleased changes.
|
**Note:** Always select the same GitHub tag as the released version you use
|
||||||
|
to ensure you have the correct example configuration and documentation.
|
||||||
|
The `main` branch might contain unreleased changes.
|
||||||
|
|
||||||
## Overview
|
## What is Tailscale
|
||||||
|
|
||||||
Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using [NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).
|
Tailscale is [a modern VPN](https://tailscale.com/) built on top of
|
||||||
|
[Wireguard](https://www.wireguard.com/).
|
||||||
|
It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/)
|
||||||
|
between the computers of your networks - using
|
||||||
|
[NAT traversal](https://tailscale.com/blog/how-nat-traversal-works/).
|
||||||
|
|
||||||
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS (Windows and macOS/iOS), and the 'coordination/control server'.
|
Everything in Tailscale is Open Source, except the GUI clients for proprietary OS
|
||||||
|
(Windows and macOS/iOS), and the control server.
|
||||||
|
|
||||||
The control server works as an exchange point of Wireguard public keys for the nodes in the Tailscale network. It also assigns the IP addresses of the clients, creates the boundaries between each user, enables sharing machines between users, and exposes the advertised routes of your nodes.
|
The control server works as an exchange point of Wireguard public keys for the
|
||||||
|
nodes in the Tailscale network. It assigns the IP addresses of the clients,
|
||||||
|
creates the boundaries between each user, enables sharing machines between users,
|
||||||
|
and exposes the advertised routes of your nodes.
|
||||||
|
|
||||||
headscale implements this coordination server.
|
A [Tailscale network (tailnet)](https://tailscale.com/kb/1136/tailnet/) is private
|
||||||
|
network which Tailscale assigns to a user in terms of private users or an
|
||||||
|
organisations.
|
||||||
|
|
||||||
|
## Design goal
|
||||||
|
|
||||||
|
`headscale` aims to implement a self-hosted, open source alternative to the Tailscale
|
||||||
|
control server. `headscale` has a narrower scope and an instance of `headscale`
|
||||||
|
implements a _single_ Tailnet, which is typically what a single organisation, or
|
||||||
|
home/personal setup would use.
|
||||||
|
|
||||||
|
`headscale` uses terms that maps to Tailscale's control server, consult the
|
||||||
|
[glossary](./docs/glossary.md) for explainations.
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
|
||||||
If you like `headscale` and find it useful, there is sponsorship and donation buttons available in the repo.
|
If you like `headscale` and find it useful, there is a sponsorship and donation
|
||||||
|
buttons available in the repo.
|
||||||
|
|
||||||
If you would like to sponsor features, bugs or prioritisation, reach out to one of the maintainers.
|
If you would like to sponsor features, bugs or prioritisation, reach out to
|
||||||
|
one of the maintainers.
|
||||||
|
|
||||||
## Status
|
## Features
|
||||||
|
|
||||||
- [x] Base functionality (nodes can communicate with each other)
|
- Full "base" support of Tailscale's features
|
||||||
- [x] Node registration through the web flow
|
- Configurable DNS
|
||||||
- [x] Network changes are relayed to the nodes
|
- [Split DNS](https://tailscale.com/kb/1054/dns/#using-dns-settings-in-the-admin-console)
|
||||||
- [x] Namespaces support (~tailnets in Tailscale.com naming)
|
- Node registration
|
||||||
- [x] Routing (advertise & accept, including exit nodes)
|
- Single-Sign-On (via Open ID Connect)
|
||||||
- [x] Node registration via pre-auth keys (including reusable keys, and ephemeral node support)
|
- Pre authenticated key
|
||||||
- [x] JSON-formatted output
|
- Taildrop (File Sharing)
|
||||||
- [x] ACLs
|
- [Access control lists](https://tailscale.com/kb/1018/acls/)
|
||||||
- [x] Taildrop (File Sharing)
|
- [MagicDNS](https://tailscale.com/kb/1081/magicdns)
|
||||||
- [x] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
|
- Support for multiple IP ranges in the tailnet
|
||||||
- [x] DNS (passing DNS servers to nodes)
|
- Dual stack (IPv4 and IPv6)
|
||||||
- [x] Single-Sign-On (via Open ID Connect)
|
- Routing advertising (including exit nodes)
|
||||||
- [x] Share nodes between namespaces
|
- Ephemeral nodes
|
||||||
- [x] MagicDNS (see `docs/`)
|
|
||||||
|
|
||||||
## Client OS support
|
## Client OS support
|
||||||
|
|
||||||
|
@ -53,10 +76,6 @@ If you would like to sponsor features, bugs or prioritisation, reach out to one
|
||||||
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
|
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
|
||||||
| iOS | Not yet |
|
| iOS | Not yet |
|
||||||
|
|
||||||
## Roadmap
|
|
||||||
|
|
||||||
Suggestions/PRs welcomed!
|
|
||||||
|
|
||||||
## Running headscale
|
## Running headscale
|
||||||
|
|
||||||
Please have a look at the documentation under [`docs/`](docs/).
|
Please have a look at the documentation under [`docs/`](docs/).
|
||||||
|
@ -68,11 +87,15 @@ Please have a look at the documentation under [`docs/`](docs/).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
To contribute to Headscale you would need the lastest version of [Go](https://golang.org) and [Buf](https://buf.build)(Protobuf generator).
|
To contribute to headscale you would need the lastest version of [Go](https://golang.org)
|
||||||
|
and [Buf](https://buf.build)(Protobuf generator).
|
||||||
|
|
||||||
|
PRs and suggestions are welcome.
|
||||||
|
|
||||||
### Code style
|
### Code style
|
||||||
|
|
||||||
To ensure we have some consistency with a growing number of contributions, this project has adopted linting and style/formatting rules:
|
To ensure we have some consistency with a growing number of contributions,
|
||||||
|
this project has adopted linting and style/formatting rules:
|
||||||
|
|
||||||
The **Go** code is linted with [`golangci-lint`](https://golangci-lint.run) and
|
The **Go** code is linted with [`golangci-lint`](https://golangci-lint.run) and
|
||||||
formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
|
formatted with [`golines`](https://github.com/segmentio/golines) (width 88) and
|
||||||
|
@ -99,7 +122,8 @@ make install-protobuf-plugins
|
||||||
|
|
||||||
### Testing and building
|
### Testing and building
|
||||||
|
|
||||||
Some parts of the project require the generation of Go code from Protobuf (if changes are made in `proto/`) and it must be (re-)generated with:
|
Some parts of the project require the generation of Go code from Protobuf
|
||||||
|
(if changes are made in `proto/`) and it must be (re-)generated with:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
make generate
|
make generate
|
||||||
|
|
2
acls.go
2
acls.go
|
@ -90,7 +90,7 @@ func (h *Headscale) generateACLRules() ([]tailcfg.FilterRule, error) {
|
||||||
return nil, errEmptyPolicy
|
return nil, errEmptyPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
machines, err := h.ListAllMachines()
|
machines, err := h.ListMachines()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
27
dns_test.go
27
dns_test.go
|
@ -1,6 +1,8 @@
|
||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
@ -241,20 +243,19 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) {
|
||||||
)
|
)
|
||||||
c.Assert(dnsConfig, check.NotNil)
|
c.Assert(dnsConfig, check.NotNil)
|
||||||
|
|
||||||
// TODO: Remove comment out when we have all nodes available to every node
|
c.Assert(len(dnsConfig.Routes), check.Equals, 3)
|
||||||
// c.Assert(len(dnsConfig.Routes), check.Equals, 2)
|
|
||||||
|
|
||||||
// domainRouteShared1 := fmt.Sprintf("%s.%s", namespaceShared1.Name, baseDomain)
|
domainRouteShared1 := fmt.Sprintf("%s.%s", namespaceShared1.Name, baseDomain)
|
||||||
// _, ok := dnsConfig.Routes[domainRouteShared1]
|
_, ok := dnsConfig.Routes[domainRouteShared1]
|
||||||
// c.Assert(ok, check.Equals, true)
|
c.Assert(ok, check.Equals, true)
|
||||||
//
|
|
||||||
// domainRouteShared2 := fmt.Sprintf("%s.%s", namespaceShared2.Name, baseDomain)
|
domainRouteShared2 := fmt.Sprintf("%s.%s", namespaceShared2.Name, baseDomain)
|
||||||
// _, ok = dnsConfig.Routes[domainRouteShared2]
|
_, ok = dnsConfig.Routes[domainRouteShared2]
|
||||||
// c.Assert(ok, check.Equals, true)
|
c.Assert(ok, check.Equals, true)
|
||||||
//
|
|
||||||
// domainRouteShared3 := fmt.Sprintf("%s.%s", namespaceShared3.Name, baseDomain)
|
domainRouteShared3 := fmt.Sprintf("%s.%s", namespaceShared3.Name, baseDomain)
|
||||||
// _, ok = dnsConfig.Routes[domainRouteShared3]
|
_, ok = dnsConfig.Routes[domainRouteShared3]
|
||||||
// c.Assert(ok, check.Equals, false)
|
c.Assert(ok, check.Equals, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) {
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
# Glossary
|
# Glossary
|
||||||
|
|
||||||
- Namespace: Collection of Tailscale nodes that can see each other. In Tailscale.com this is called Tailnet.
|
| Term | Description |
|
||||||
|
| --------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Machine | A machine is a single entity connected to `headscale`, typically an installation of Tailscale. Also known as **Node** |
|
||||||
|
| Namespace | A namespace is a logical grouping of machines "owned" by the same entity, in Tailscale, this is typically a User |
|
||||||
|
|
25
machine.go
25
machine.go
|
@ -118,19 +118,6 @@ func (machine Machine) isExpired() bool {
|
||||||
return time.Now().UTC().After(*machine.Expiry)
|
return time.Now().UTC().After(*machine.Expiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) ListAllMachines() ([]Machine, error) {
|
|
||||||
machines := []Machine{}
|
|
||||||
if err := h.db.Preload("AuthKey").
|
|
||||||
Preload("AuthKey.Namespace").
|
|
||||||
Preload("Namespace").
|
|
||||||
Where("registered").
|
|
||||||
Find(&machines).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return machines, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func containsAddresses(inputs []string, addrs []string) bool {
|
func containsAddresses(inputs []string, addrs []string) bool {
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
if containsString(inputs, addr) {
|
if containsString(inputs, addr) {
|
||||||
|
@ -215,15 +202,15 @@ func getFilteredByACLPeers(
|
||||||
return authorizedPeers
|
return authorizedPeers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
|
func (h *Headscale) ListPeers(machine *Machine) (Machines, error) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Str("machine", machine.Name).
|
Str("machine", machine.Name).
|
||||||
Msg("Finding direct peers")
|
Msg("Finding direct peers")
|
||||||
|
|
||||||
machines := Machines{}
|
machines := Machines{}
|
||||||
if err := h.db.Preload("Namespace").Where("namespace_id = ? AND machine_key <> ? AND registered",
|
if err := h.db.Preload("AuthKey").Preload("AuthKey.Namespace").Preload("Namespace").Where("machine_key <> ? AND registered",
|
||||||
machine.NamespaceID, 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")
|
||||||
|
|
||||||
return Machines{}, err
|
return Machines{}, err
|
||||||
|
@ -234,7 +221,7 @@ func (h *Headscale) getDirectPeers(machine *Machine) (Machines, error) {
|
||||||
log.Trace().
|
log.Trace().
|
||||||
Caller().
|
Caller().
|
||||||
Str("machine", machine.Name).
|
Str("machine", machine.Name).
|
||||||
Msgf("Found direct machines: %s", machines.String())
|
Msgf("Found peers: %s", machines.String())
|
||||||
|
|
||||||
return machines, nil
|
return machines, nil
|
||||||
}
|
}
|
||||||
|
@ -247,7 +234,7 @@ func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
||||||
// else use the classic namespace scope
|
// else use the classic namespace scope
|
||||||
if h.aclPolicy != nil {
|
if h.aclPolicy != nil {
|
||||||
var machines []Machine
|
var machines []Machine
|
||||||
machines, err = h.ListAllMachines()
|
machines, err = h.ListMachines()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().Err(err).Msg("Error retrieving list of machines")
|
log.Error().Err(err).Msg("Error retrieving list of machines")
|
||||||
|
|
||||||
|
@ -255,7 +242,7 @@ func (h *Headscale) getPeers(machine *Machine) (Machines, error) {
|
||||||
}
|
}
|
||||||
peers = getFilteredByACLPeers(machines, h.aclRules, machine)
|
peers = getFilteredByACLPeers(machines, h.aclRules, machine)
|
||||||
} else {
|
} else {
|
||||||
peers, err = h.getDirectPeers(machine)
|
peers, err = h.ListPeers(machine)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
log.Error().
|
||||||
Caller().
|
Caller().
|
||||||
|
|
|
@ -118,7 +118,7 @@ func (s *Suite) TestHardDeleteMachine(c *check.C) {
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Suite) TestGetDirectPeers(c *check.C) {
|
func (s *Suite) TestListPeers(c *check.C) {
|
||||||
namespace, err := app.CreateNamespace("test")
|
namespace, err := app.CreateNamespace("test")
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ func (s *Suite) TestGetDirectPeers(c *check.C) {
|
||||||
_, err = machine0ByID.GetHostInfo()
|
_, err = machine0ByID.GetHostInfo()
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
peersOfMachine0, err := app.getDirectPeers(machine0ByID)
|
peersOfMachine0, err := app.ListPeers(machine0ByID)
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
c.Assert(len(peersOfMachine0), check.Equals, 9)
|
c.Assert(len(peersOfMachine0), check.Equals, 9)
|
||||||
|
@ -222,7 +222,7 @@ func (s *Suite) TestGetACLFilteredPeers(c *check.C) {
|
||||||
_, err = testMachine.GetHostInfo()
|
_, err = testMachine.GetHostInfo()
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
machines, err := app.ListAllMachines()
|
machines, err := app.ListMachines()
|
||||||
c.Assert(err, check.IsNil)
|
c.Assert(err, check.IsNil)
|
||||||
|
|
||||||
peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine)
|
peersOfTestMachine := getFilteredByACLPeers(machines, app.aclRules, testMachine)
|
||||||
|
|
|
@ -205,35 +205,33 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) {
|
||||||
}
|
}
|
||||||
app.db.Save(machine2InShared1)
|
app.db.Save(machine2InShared1)
|
||||||
|
|
||||||
// TODO: Remove comment out when we have all nodes available to every node
|
peersOfMachine1InShared1, err := app.getPeers(machineInShared1)
|
||||||
// peersOfMachine1InShared1, err := app.getPeers(machineInShared1)
|
c.Assert(err, check.IsNil)
|
||||||
// c.Assert(err, check.IsNil)
|
|
||||||
|
|
||||||
// userProfiles := getMapResponseUserProfiles(
|
userProfiles := getMapResponseUserProfiles(
|
||||||
// *machineInShared1,
|
*machineInShared1,
|
||||||
// peersOfMachine1InShared1,
|
peersOfMachine1InShared1,
|
||||||
// )
|
)
|
||||||
//
|
|
||||||
// log.Trace().Msgf("userProfiles %#v", userProfiles)
|
c.Assert(len(userProfiles), check.Equals, 3)
|
||||||
// c.Assert(len(userProfiles), check.Equals, 2)
|
|
||||||
//
|
found := false
|
||||||
// found := false
|
for _, userProfiles := range userProfiles {
|
||||||
// for _, userProfiles := range userProfiles {
|
if userProfiles.DisplayName == namespaceShared1.Name {
|
||||||
// if userProfiles.DisplayName == namespaceShared1.Name {
|
found = true
|
||||||
// found = true
|
|
||||||
//
|
break
|
||||||
// break
|
}
|
||||||
// }
|
}
|
||||||
// }
|
c.Assert(found, check.Equals, true)
|
||||||
// c.Assert(found, check.Equals, true)
|
|
||||||
//
|
found = false
|
||||||
// found = false
|
for _, userProfile := range userProfiles {
|
||||||
// for _, userProfile := range userProfiles {
|
if userProfile.DisplayName == namespaceShared2.Name {
|
||||||
// if userProfile.DisplayName == namespaceShared2.Name {
|
found = true
|
||||||
// found = true
|
|
||||||
//
|
break
|
||||||
// break
|
}
|
||||||
// }
|
}
|
||||||
// }
|
c.Assert(found, check.Equals, true)
|
||||||
// c.Assert(found, check.Equals, true)
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue