Compare commits

..

44 Commits

Author SHA1 Message Date
Juan Font
536e8b71bf Removed wrong syntax in actions 2021-09-13 22:59:33 +02:00
Juan Font
acc43c39af Increased linter timeout in makefile 2021-09-13 22:58:35 +02:00
Juan Font
eae1b6a3de More timeout in linting 2021-09-13 22:51:58 +02:00
Juan Font
31cc61478f More timeout in linting 2021-09-13 22:47:38 +02:00
Juan Font
3095c1e150 Trying to correct Actions issues 2021-09-13 22:45:31 +02:00
Juan Font
e1d5da5bd9 Merge pull request #107 from qbit/no_color_trace
Remove trace lines about NO_COLOR.
2021-09-13 22:38:37 +02:00
Juan Font
5f818b7349 Merge pull request #89 from ohdearaugustin/topic/docker-release
Topic/docker release
2021-09-13 22:37:33 +02:00
ohdearaugustin
1e93347a26 Merge branch 'main' into topic/docker-release 2021-09-12 18:18:34 +02:00
ohdearaugustin
18867a4c84 update docu 2021-09-12 18:08:43 +02:00
ohdearaugustin
3b97c7bdec gitignore: add jetbrains 2021-09-12 18:08:43 +02:00
Aaron Bieber
203e6bc6b2 Remove trace lines about NO_COLOR. 2021-09-12 07:30:35 -06:00
Juan Font
e27753e46e Merge pull request #103 from juanfont/shared-nodes
Add support for sharing nodes across namespaces
2021-09-11 23:31:37 +02:00
Juan Font
11fbef4bf0 Added extra timeout 2021-09-11 23:21:45 +02:00
Juan Font
c4e6ad1ec7 Fixed some typos 2021-09-10 00:52:08 +02:00
Juan Font
263a3f1983 Merge branch 'main' into shared-nodes 2021-09-10 00:49:50 +02:00
Juan Font
8acaea0fbe Increased timeout 2021-09-10 00:44:27 +02:00
Juan Font
bd6adfaec6 Changes a few more variables 2021-09-10 00:37:01 +02:00
Juan Font
4b4a5a4b93 Update sharing.go
Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2021-09-10 00:32:42 +02:00
Juan Font
b098d84557 Apply suggestions from code review
Changed more variable names

Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2021-09-10 00:32:06 +02:00
Juan Font
b937f9b762 Update machine.go
Added comment on toNode
2021-09-10 00:30:02 +02:00
Juan Font
55f3e07bd4 Apply suggestions from code review
Removed one letter variables

Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2021-09-10 00:26:46 +02:00
Juan Font
2780623076 Renamed SharedNode to SharedMachine 2021-09-06 14:43:43 +02:00
Juan Font
75a342f96e Renamed files 2021-09-06 14:40:37 +02:00
Juan Font
729cd54401 Renamed sharing function 2021-09-06 14:39:52 +02:00
Juan Font
a023f51971 Merge pull request #101 from SilverBut/main
fix: check last seen time without possible null pointer
2021-09-03 10:35:49 +02:00
Juan Font
5076eb9215 Merge pull request #102 from SilverBut/patch-1
docs: add notes on how to build own DERP server
2021-09-03 10:24:32 +02:00
Juan Font
7edd0cd14c Added add node cli 2021-09-03 10:23:45 +02:00
Juan Font
7ce4738d8a Preload namespace so the name can be shown 2021-09-03 10:23:26 +02:00
Juan Font
7287e0259c Minor linting issues 2021-09-02 17:08:39 +02:00
Juan Font
d86de68b40 Show namespace in node list table 2021-09-02 17:06:47 +02:00
Juan Font
4ba107a765 README updated 2021-09-02 17:00:46 +02:00
Juan Font
187b016d09 Added helper function to get list of shared nodes 2021-09-02 16:59:50 +02:00
Juan Font
7010f5afad Added unit tests on sharing nodes 2021-09-02 16:59:12 +02:00
Juan Font
48b73fa12f Implement node sharing functionality 2021-09-02 16:59:03 +02:00
Juan Font
1ecd0d7ca4 Added DB SharedNode model to support sharing nodes 2021-09-02 16:57:26 +02:00
Silver Bullet
6faaae0c5f docs: add notes on how to build own DERP server
The official doc is hidden under a bunch of issues. Add a doc link here and hope it could be helpful.
2021-09-02 06:08:12 +08:00
Silver Bullet
e4ef65be76 fix: check last seen time without possible null pointer 2021-09-02 05:44:42 +08:00
Juan Font
39c661d408 Merge pull request #99 from juanfont/explicit-ubuntu-version
Use explicit version in Dockerfile
2021-08-26 21:18:16 +02:00
Juan Font
91a48d6a43 Update Dockerfile
Use explicit version in Dockerfile (addresses #95)
2021-08-26 10:23:45 +02:00
ohdearaugustin
a613501ff2 Update .github/workflows/release.yml
Fix typo

Co-authored-by: Kristoffer Dalby <kradalby@kradalby.no>
2021-08-21 11:17:21 +02:00
ohdearaugustin
75afdc6306 github/workflows: remove version tag 2021-08-20 20:10:34 +02:00
ohdearaugustin
f02beaf075 github/workflows: add checkout 2021-08-20 19:45:01 +02:00
ohdearaugustin
8bcc7e88f0 github/workflows: add dispatch 2021-08-20 19:37:15 +02:00
ohdearaugustin
0adbd720bf github/workflows: add docker release 2021-08-20 19:15:20 +02:00
18 changed files with 658 additions and 54 deletions

View File

@@ -19,7 +19,7 @@ jobs:
# golangci-lint manually in the `Run lint` step. # golangci-lint manually in the `Run lint` step.
- uses: golangci/golangci-lint-action@v2 - uses: golangci/golangci-lint-action@v2
with: with:
args: --timeout 2m args: --timeout 5m
# Setup Go # Setup Go
- name: Setup Go - name: Setup Go

View File

@@ -1,9 +1,11 @@
name: goreleaser ---
name: release
on: on:
push: push:
tags: tags:
- "*" # triggers only if push new tag version - "*" # triggers only if push new tag version
workflow_dispatch:
jobs: jobs:
goreleaser: goreleaser:
@@ -28,3 +30,47 @@ jobs:
args: release --rm-dist args: release --rm-dist
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker-release:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
# list of Docker images to use as base name for tags
images: |
${{ secrets.DOCKERHUB_USERNAME }}/headscale
ghcr.io/${{ github.repository_owner }}/headscale
tags: |
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Login to GHCR
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
context: .
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

3
.gitignore vendored
View File

@@ -20,4 +20,7 @@ config.json
/db.sqlite /db.sqlite
*.sqlite3 *.sqlite3
# Exclude Jetbrains Editors
.idea
test_output/ test_output/

View File

@@ -10,7 +10,7 @@ COPY . /go/src/headscale
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
RUN test -e /go/bin/headscale RUN test -e /go/bin/headscale
FROM ubuntu:latest FROM ubuntu:20.04
COPY --from=build /go/bin/headscale /usr/local/bin/headscale COPY --from=build /go/bin/headscale /usr/local/bin/headscale
ENV TZ UTC ENV TZ UTC

View File

@@ -20,7 +20,7 @@ coverprofile_html:
lint: lint:
golint golint
golangci-lint run golangci-lint run --timeout 5m
compress: build compress: build
upx --brute headscale upx --brute headscale

View File

@@ -26,22 +26,28 @@ Headscale implements this coordination server.
- [X] ACLs - [X] ACLs
- [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10) - [X] Support for alternative IP ranges in the tailnets (default Tailscale's 100.64.0.0/10)
- [X] DNS (passing DNS servers to nodes) - [X] DNS (passing DNS servers to nodes)
- [ ] Share nodes between ~~users~~ namespaces - [X] Share nodes between ~~users~~ namespaces
- [ ] MagicDNS / Smart DNS - [ ] MagicDNS / Smart DNS
## Roadmap 🤷 ## Roadmap 🤷
We are now focusing on adding integration tests with the official clients.
Suggestions/PRs welcomed! Suggestions/PRs welcomed!
## Running it ## Running it
1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH 1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH or use the docker container
```shell
docker pull headscale/headscale:x.x.x
```
or
```shell
docker pull ghrc.io/juanfont/headscale:x.x.x
```
2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running 2. (Optional, you can also use SQLite) Get yourself a PostgreSQL DB running
@@ -66,11 +72,19 @@ Suggestions/PRs welcomed!
```shell ```shell
headscale namespaces create myfirstnamespace headscale namespaces create myfirstnamespace
``` ```
or docker:
```shell
docker run -v ./private.key:/private.key -v ./config.json:/config.json headscale/headscale:x.x.x headscale namespace create myfirstnamespace
```
5. Run the server 5. Run the server
```shell ```shell
headscale serve headscale serve
``` ```
or docker:
```shell
docker run -v $(pwd)/private.key:/private.key -v $(pwd)/config.json:/config.json -v $(pwd)/derb.yaml:/derb.yaml -p 127.0.0.1:8080:8080 headscale/headscale:x.x.x headscale serve
```
6. If you used tailscale.com before in your nodes, make sure you clear the tailscaled data folder 6. If you used tailscale.com before in your nodes, make sure you clear the tailscaled data folder
```shell ```shell
@@ -90,6 +104,10 @@ Suggestions/PRs welcomed!
```shell ```shell
headscale -n myfirstnamespace node register YOURMACHINEKEY headscale -n myfirstnamespace node register YOURMACHINEKEY
``` ```
or docker:
```shell
docker run -v ./private.key:/private.key -v ./config.json:/config.json headscale/headscale:x.x.x headscale -n myfirstnamespace node register YOURMACHINEKEY
```
Alternatively, you can use Auth Keys to register your machines: Alternatively, you can use Auth Keys to register your machines:
@@ -97,6 +115,10 @@ Alternatively, you can use Auth Keys to register your machines:
```shell ```shell
headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
``` ```
or docker:
```shell
docker run -v ./private.key:/private.key -v ./config.json:/config.json headscale/headscale:x.x.x headscale -n myfirstnamespace preauthkeys create --reusable --expiration 24h
```
2. Use the authkey from your machine to register it 2. Use the authkey from your machine to register it
```shell ```shell

5
api.go
View File

@@ -33,8 +33,6 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
return return
} }
// spew.Dump(c.Params)
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(` c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(fmt.Sprintf(`
<html> <html>
<body> <body>
@@ -220,7 +218,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
Str("func", "getMapResponse"). Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname). Str("machine", req.Hostinfo.Hostname).
Msg("Creating Map response") Msg("Creating Map response")
node, err := m.toNode() node, err := m.toNode(true)
if err != nil { if err != nil {
log.Error(). log.Error().
Str("func", "getMapResponse"). Str("func", "getMapResponse").
@@ -280,7 +278,6 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Mac
return nil, err return nil, err
} }
} }
// spew.Dump(resp)
// declare the incoming size on the first 4 bytes // declare the incoming size on the first 4 bytes
data := make([]byte, 4) data := make([]byte, 4)
binary.LittleEndian.PutUint32(data, uint32(len(respBody))) binary.LittleEndian.PutUint32(data, uint32(len(respBody)))

View File

@@ -25,6 +25,7 @@ func init() {
nodeCmd.AddCommand(listNodesCmd) nodeCmd.AddCommand(listNodesCmd)
nodeCmd.AddCommand(registerNodeCmd) nodeCmd.AddCommand(registerNodeCmd)
nodeCmd.AddCommand(deleteNodeCmd) nodeCmd.AddCommand(deleteNodeCmd)
nodeCmd.AddCommand(shareMachineCmd)
} }
var nodeCmd = &cobra.Command{ var nodeCmd = &cobra.Command{
@@ -79,9 +80,26 @@ var listNodesCmd = &cobra.Command{
if err != nil { if err != nil {
log.Fatalf("Error initializing: %s", err) log.Fatalf("Error initializing: %s", err)
} }
namespace, err := h.GetNamespace(n)
if err != nil {
log.Fatalf("Error fetching namespace: %s", err)
}
machines, err := h.ListMachinesInNamespace(n) machines, err := h.ListMachinesInNamespace(n)
if err != nil {
log.Fatalf("Error fetching machines: %s", err)
}
sharedMachines, err := h.ListSharedMachinesInNamespace(n)
if err != nil {
log.Fatalf("Error fetching shared machines: %s", err)
}
allMachines := append(*machines, *sharedMachines...)
if strings.HasPrefix(o, "json") { if strings.HasPrefix(o, "json") {
JsonOutput(machines, err, o) JsonOutput(allMachines, err, o)
return return
} }
@@ -89,7 +107,7 @@ var listNodesCmd = &cobra.Command{
log.Fatalf("Error getting nodes: %s", err) log.Fatalf("Error getting nodes: %s", err)
} }
d, err := nodesToPtables(*machines) d, err := nodesToPtables(*namespace, allMachines)
if err != nil { if err != nil {
log.Fatalf("Error converting to table: %s", err) log.Fatalf("Error converting to table: %s", err)
} }
@@ -145,31 +163,94 @@ var deleteNodeCmd = &cobra.Command{
}, },
} }
func nodesToPtables(m []headscale.Machine) (pterm.TableData, error) { var shareMachineCmd = &cobra.Command{
d := pterm.TableData{{"ID", "Name", "NodeKey", "IP address", "Ephemeral", "Last seen", "Online"}} Use: "share ID namespace",
Short: "Shares a node from the current namespace to the specified one",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return fmt.Errorf("missing parameters")
}
return nil
},
Run: func(cmd *cobra.Command, args []string) {
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
log.Fatalf("Error getting namespace: %s", err)
}
output, _ := cmd.Flags().GetString("output")
for _, m := range m { h, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
}
_, err = h.GetNamespace(namespace)
if err != nil {
log.Fatalf("Error fetching origin namespace: %s", err)
}
destinationNamespace, err := h.GetNamespace(args[1])
if err != nil {
log.Fatalf("Error fetching destination namespace: %s", err)
}
id, err := strconv.Atoi(args[0])
if err != nil {
log.Fatalf("Error converting ID to integer: %s", err)
}
machine, err := h.GetMachineByID(uint64(id))
if err != nil {
log.Fatalf("Error getting node: %s", err)
}
err = h.AddSharedMachineToNamespace(machine, destinationNamespace)
if strings.HasPrefix(output, "json") {
JsonOutput(map[string]string{"Result": "Node shared"}, err, output)
return
}
if err != nil {
fmt.Printf("Error sharing node: %s\n", err)
return
}
fmt.Println("Node shared!")
},
}
func nodesToPtables(currentNamespace headscale.Namespace, machines []headscale.Machine) (pterm.TableData, error) {
d := pterm.TableData{{"ID", "Name", "NodeKey", "Namespace", "IP address", "Ephemeral", "Last seen", "Online"}}
for _, machine := range machines {
var ephemeral bool var ephemeral bool
if m.AuthKey != nil && m.AuthKey.Ephemeral { if machine.AuthKey != nil && machine.AuthKey.Ephemeral {
ephemeral = true ephemeral = true
} }
var lastSeen time.Time var lastSeen time.Time
if m.LastSeen != nil { var lastSeenTime string
lastSeen = *m.LastSeen if machine.LastSeen != nil {
lastSeen = *machine.LastSeen
lastSeenTime = lastSeen.Format("2006-01-02 15:04:05")
} }
nKey, err := wgkey.ParseHex(m.NodeKey) nKey, err := wgkey.ParseHex(machine.NodeKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nodeKey := tailcfg.NodeKey(nKey) nodeKey := tailcfg.NodeKey(nKey)
var online string var online string
if m.LastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online if lastSeen.After(time.Now().Add(-5 * time.Minute)) { // TODO: Find a better way to reliably show if online
online = pterm.LightGreen("true") online = pterm.LightGreen("true")
} else { } else {
online = pterm.LightRed("false") online = pterm.LightRed("false")
} }
d = append(d, []string{strconv.FormatUint(m.ID, 10), m.Name, nodeKey.ShortString(), m.IPAddress, strconv.FormatBool(ephemeral), lastSeen.Format("2006-01-02 15:04:05"), online})
var namespace string
if currentNamespace.ID == machine.NamespaceID {
namespace = pterm.LightMagenta(machine.Namespace.Name)
} else {
namespace = pterm.LightYellow(machine.Namespace.Name)
}
d = append(d, []string{strconv.FormatUint(machine.ID, 10), machine.Name, nodeKey.ShortString(), namespace, machine.IPAddress, strconv.FormatBool(ephemeral), lastSeenTime, online})
} }
return d, nil return d, nil
} }

View File

@@ -22,14 +22,12 @@ func main() {
colors = true colors = true
default: default:
// no color, return text as is. // no color, return text as is.
log.Trace().Msg("Colors are not supported, disabling")
colors = false colors = false
} }
// Adhere to no-color.org manifesto of allowing users to // Adhere to no-color.org manifesto of allowing users to
// turn off color in cli/services // turn off color in cli/services
if _, noColorIsSet := os.LookupEnv("NO_COLOR"); noColorIsSet { if _, noColorIsSet := os.LookupEnv("NO_COLOR"); noColorIsSet {
log.Trace().Msg("NO_COLOR is set, disabling colors")
colors = false colors = false
} }

5
db.go
View File

@@ -44,6 +44,11 @@ func (h *Headscale) initDB() error {
return err return err
} }
err = db.AutoMigrate(&SharedMachine{})
if err != nil {
return err
}
err = h.setValue("db_version", dbVersion) err = h.setValue("db_version", dbVersion)
return err return err
} }

View File

@@ -1,7 +1,7 @@
# This file contains some of the official Tailscale DERP servers, # This file contains some of the official Tailscale DERP servers,
# shamelessly taken from https://github.com/tailscale/tailscale/blob/main/net/dnsfallback/dns-fallback-servers.json # shamelessly taken from https://github.com/tailscale/tailscale/blob/main/net/dnsfallback/dns-fallback-servers.json
# #
# If you plan to somehow use headscale, please deploy your own DERP infra # If you plan to somehow use headscale, please deploy your own DERP infra: https://tailscale.com/kb/1118/custom-derp-servers/
regions: regions:
1: 1:
regionid: 1 regionid: 1

View File

@@ -89,7 +89,6 @@ Use "headscale [command] --help" for more information about a command.
# TODO / Ideas # TODO / Ideas
- Github action to publish the docker image
- Interpolate `email:` option to the ClusterIssuer from site configuration. - Interpolate `email:` option to the ClusterIssuer from site configuration.
This probably needs to be done with a transformer, kustomize vars don't seem to work. This probably needs to be done with a transformer, kustomize vars don't seem to work.
- Add kustomize examples for cloud-native ingress, load balancer - Add kustomize examples for cloud-native ingress, load balancer

View File

@@ -50,7 +50,9 @@ func (m Machine) isAlreadyRegistered() bool {
return m.Registered return m.Registered
} }
func (m Machine) toNode() (*tailcfg.Node, error) { // toNode converts a Machine into a Tailscale Node. includeRoutes is false for shared nodes
// as per the expected behaviour in the official SaaS
func (m Machine) toNode(includeRoutes bool) (*tailcfg.Node, error) {
nKey, err := wgkey.ParseHex(m.NodeKey) nKey, err := wgkey.ParseHex(m.NodeKey)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -85,6 +87,7 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
allowedIPs := []netaddr.IPPrefix{} allowedIPs := []netaddr.IPPrefix{}
allowedIPs = append(allowedIPs, ip) // we append the node own IP, as it is required by the clients allowedIPs = append(allowedIPs, ip) // we append the node own IP, as it is required by the clients
if includeRoutes {
routesStr := []string{} routesStr := []string{}
if len(m.EnabledRoutes) != 0 { if len(m.EnabledRoutes) != 0 {
allwIps, err := m.EnabledRoutes.MarshalJSON() allwIps, err := m.EnabledRoutes.MarshalJSON()
@@ -97,13 +100,14 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
} }
} }
for _, aip := range routesStr { for _, routeStr := range routesStr {
ip, err := netaddr.ParseIPPrefix(aip) ip, err := netaddr.ParseIPPrefix(routeStr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
allowedIPs = append(allowedIPs, ip) allowedIPs = append(allowedIPs, ip)
} }
}
endpoints := []string{} endpoints := []string{}
if len(m.Endpoints) != 0 { if len(m.Endpoints) != 0 {
@@ -136,13 +140,20 @@ func (m Machine) toNode() (*tailcfg.Node, error) {
derp = "127.3.3.40:0" // Zero means disconnected or unknown. derp = "127.3.3.40:0" // Zero means disconnected or unknown.
} }
var keyExpiry time.Time
if m.Expiry != nil {
keyExpiry = *m.Expiry
} else {
keyExpiry = time.Time{}
}
n := tailcfg.Node{ n := tailcfg.Node{
ID: tailcfg.NodeID(m.ID), // this is the actual ID ID: tailcfg.NodeID(m.ID), // this is the actual ID
StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent StableID: tailcfg.StableNodeID(strconv.FormatUint(m.ID, 10)), // in headscale, unlike tailcontrol server, IDs are permanent
Name: hostinfo.Hostname, Name: hostinfo.Hostname,
User: tailcfg.UserID(m.NamespaceID), User: tailcfg.UserID(m.NamespaceID),
Key: tailcfg.NodeKey(nKey), Key: tailcfg.NodeKey(nKey),
KeyExpiry: *m.Expiry, KeyExpiry: keyExpiry,
Machine: tailcfg.MachineKey(mKey), Machine: tailcfg.MachineKey(mKey),
DiscoKey: discoKey, DiscoKey: discoKey,
Addresses: addrs, Addresses: addrs,
@@ -165,6 +176,7 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
Str("func", "getPeers"). Str("func", "getPeers").
Str("machine", m.Name). Str("machine", m.Name).
Msg("Finding peers") Msg("Finding peers")
machines := []Machine{} machines := []Machine{}
if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered", if err := h.db.Where("namespace_id = ? AND machine_key <> ? AND registered",
m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil { m.NamespaceID, m.MachineKey).Find(&machines).Error; err != nil {
@@ -172,9 +184,23 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
return nil, err return nil, err
} }
// We fetch here machines that are shared to the `Namespace` of the machine we are getting peers for
sharedMachines := []SharedMachine{}
if err := h.db.Preload("Namespace").Preload("Machine").Where("namespace_id = ?",
m.NamespaceID).Find(&sharedMachines).Error; err != nil {
return nil, err
}
peers := []*tailcfg.Node{} peers := []*tailcfg.Node{}
for _, mn := range machines { for _, mn := range machines {
peer, err := mn.toNode() peer, err := mn.toNode(true)
if err != nil {
return nil, err
}
peers = append(peers, peer)
}
for _, sharedMachine := range sharedMachines {
peer, err := sharedMachine.Machine.toNode(false) // shared nodes do not expose their routes
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -201,13 +227,13 @@ func (h *Headscale) GetMachine(namespace string, name string) (*Machine, error)
return &m, nil return &m, nil
} }
} }
return nil, fmt.Errorf("not found") return nil, fmt.Errorf("machine not found")
} }
// GetMachineByID finds a Machine by ID and returns the Machine struct // GetMachineByID finds a Machine by ID and returns the Machine struct
func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) { func (h *Headscale) GetMachineByID(id uint64) (*Machine, error) {
m := Machine{} m := Machine{}
if result := h.db.Find(&Machine{ID: id}).First(&m); result.Error != nil { if result := h.db.Preload("Namespace").Find(&Machine{ID: id}).First(&m); result.Error != nil {
return nil, result.Error return nil, result.Error
} }
return &m, nil return &m, nil
@@ -260,7 +286,14 @@ func (m *Machine) GetHostInfo() (*tailcfg.Hostinfo, error) {
} }
func (h *Headscale) notifyChangesToPeers(m *Machine) { func (h *Headscale) notifyChangesToPeers(m *Machine) {
peers, _ := h.getPeers(*m) peers, err := h.getPeers(*m)
if err != nil {
log.Error().
Str("func", "notifyChangesToPeers").
Str("machine", m.Name).
Msgf("Error getting peers: %s", err)
return
}
for _, p := range *peers { for _, p := range *peers {
log.Info(). log.Info().
Str("func", "notifyChangesToPeers"). Str("func", "notifyChangesToPeers").

View File

@@ -91,12 +91,34 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) {
} }
machines := []Machine{} machines := []Machine{}
if err := h.db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil { if err := h.db.Preload("AuthKey").Preload("Namespace").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
return nil, err return nil, err
} }
return &machines, nil return &machines, nil
} }
// ListSharedMachinesInNamespace returns all the machines that are shared to the specified namespace
func (h *Headscale) ListSharedMachinesInNamespace(name string) (*[]Machine, error) {
namespace, err := h.GetNamespace(name)
if err != nil {
return nil, err
}
sharedMachines := []SharedMachine{}
if err := h.db.Preload("Namespace").Where(&SharedMachine{NamespaceID: namespace.ID}).Find(&sharedMachines).Error; err != nil {
return nil, err
}
machines := []Machine{}
for _, sharedMachine := range sharedMachines {
machine, err := h.GetMachineByID(sharedMachine.MachineID) // otherwise not everything comes filled
if err != nil {
return nil, err
}
machines = append(machines, *machine)
}
return &machines, nil
}
// SetMachineNamespace assigns a Machine to a namespace // SetMachineNamespace assigns a Machine to a namespace
func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error { func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error {
n, err := h.GetNamespace(namespaceName) n, err := h.GetNamespace(namespaceName)

View File

@@ -440,7 +440,7 @@ func (h *Headscale) scheduledPollWorker(
case <-updateCheckerTicker.C: case <-updateCheckerTicker.C:
// Send an update request regardless of outdated or not, if data is sent // Send an update request regardless of outdated or not, if data is sent
// to the node is determined in the updateChan consumer block // to the node is determined in the updateChan consumer block
n, _ := m.toNode() n, _ := m.toNode(true)
err := h.sendRequestOnUpdateChannel(n) err := h.sendRequestOnUpdateChannel(n)
if err != nil { if err != nil {
log.Error(). log.Error().

View File

@@ -56,6 +56,7 @@ func (h *Headscale) GetEnabledNodeRoutes(namespace string, nodeName string) ([]n
return routes, nil return routes, nil
} }
// IsNodeRouteEnabled checks if a certain route has been enabled
func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool { func (h *Headscale) IsNodeRouteEnabled(namespace string, nodeName string, routeStr string) bool {
route, err := netaddr.ParseIPPrefix(routeStr) route, err := netaddr.ParseIPPrefix(routeStr)
if err != nil { if err != nil {
@@ -129,6 +130,7 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr
return nil return nil
} }
// RoutesToPtables converts the list of routes to a nice table
func (h *Headscale) RoutesToPtables(namespace string, nodeName string, availableRoutes []netaddr.IPPrefix) pterm.TableData { func (h *Headscale) RoutesToPtables(namespace string, nodeName string, availableRoutes []netaddr.IPPrefix) pterm.TableData {
d := pterm.TableData{{"Route", "Enabled"}} d := pterm.TableData{{"Route", "Enabled"}}

37
sharing.go Normal file
View File

@@ -0,0 +1,37 @@
package headscale
import "gorm.io/gorm"
const errorSameNamespace = Error("Destination namespace same as origin")
const errorMachineAlreadyShared = Error("Node already shared to this namespace")
// SharedMachine is a join table to support sharing nodes between namespaces
type SharedMachine struct {
gorm.Model
MachineID uint64
Machine Machine
NamespaceID uint
Namespace Namespace
}
// AddSharedMachineToNamespace adds a machine as a shared node to a namespace
func (h *Headscale) AddSharedMachineToNamespace(m *Machine, ns *Namespace) error {
if m.NamespaceID == ns.ID {
return errorSameNamespace
}
sharedMachine := SharedMachine{}
if err := h.db.Where("machine_id = ? AND namespace_id", m.ID, ns.ID).First(&sharedMachine).Error; err == nil {
return errorMachineAlreadyShared
}
sharedMachine = SharedMachine{
MachineID: m.ID,
Machine: *m,
NamespaceID: ns.ID,
Namespace: *ns,
}
h.db.Save(&sharedMachine)
return nil
}

359
sharing_test.go Normal file
View File

@@ -0,0 +1,359 @@
package headscale
import (
"gopkg.in/check.v1"
"tailscale.com/tailcfg"
)
func (s *Suite) TestBasicSharedNodesInNamespace(c *check.C) {
n1, err := h.CreateNamespace("shared1")
c.Assert(err, check.IsNil)
n2, err := h.CreateNamespace("shared2")
c.Assert(err, check.IsNil)
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil)
m1 := Machine{
ID: 0,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
NamespaceID: n1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.1",
AuthKeyID: uint(pak1.ID),
}
h.db.Save(&m1)
_, err = h.GetMachine(n1.Name, m1.Name)
c.Assert(err, check.IsNil)
m2 := Machine{
ID: 1,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
NamespaceID: n2.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.2",
AuthKeyID: uint(pak2.ID),
}
h.db.Save(&m2)
_, err = h.GetMachine(n2.Name, m2.Name)
c.Assert(err, check.IsNil)
p1s, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1s), check.Equals, 0)
err = h.AddSharedMachineToNamespace(&m2, n1)
c.Assert(err, check.IsNil)
p1sAfter, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1sAfter), check.Equals, 1)
c.Assert((*p1sAfter)[0].ID, check.Equals, tailcfg.NodeID(m2.ID))
}
func (s *Suite) TestSameNamespace(c *check.C) {
n1, err := h.CreateNamespace("shared1")
c.Assert(err, check.IsNil)
n2, err := h.CreateNamespace("shared2")
c.Assert(err, check.IsNil)
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil)
m1 := Machine{
ID: 0,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
NamespaceID: n1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.1",
AuthKeyID: uint(pak1.ID),
}
h.db.Save(&m1)
_, err = h.GetMachine(n1.Name, m1.Name)
c.Assert(err, check.IsNil)
m2 := Machine{
ID: 1,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
NamespaceID: n2.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.2",
AuthKeyID: uint(pak2.ID),
}
h.db.Save(&m2)
_, err = h.GetMachine(n2.Name, m2.Name)
c.Assert(err, check.IsNil)
p1s, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1s), check.Equals, 0)
err = h.AddSharedMachineToNamespace(&m1, n1)
c.Assert(err, check.Equals, errorSameNamespace)
}
func (s *Suite) TestAlreadyShared(c *check.C) {
n1, err := h.CreateNamespace("shared1")
c.Assert(err, check.IsNil)
n2, err := h.CreateNamespace("shared2")
c.Assert(err, check.IsNil)
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil)
m1 := Machine{
ID: 0,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
NamespaceID: n1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.1",
AuthKeyID: uint(pak1.ID),
}
h.db.Save(&m1)
_, err = h.GetMachine(n1.Name, m1.Name)
c.Assert(err, check.IsNil)
m2 := Machine{
ID: 1,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
NamespaceID: n2.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.2",
AuthKeyID: uint(pak2.ID),
}
h.db.Save(&m2)
_, err = h.GetMachine(n2.Name, m2.Name)
c.Assert(err, check.IsNil)
p1s, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1s), check.Equals, 0)
err = h.AddSharedMachineToNamespace(&m2, n1)
c.Assert(err, check.IsNil)
err = h.AddSharedMachineToNamespace(&m2, n1)
c.Assert(err, check.Equals, errorMachineAlreadyShared)
}
func (s *Suite) TestDoNotIncludeRoutesOnShared(c *check.C) {
n1, err := h.CreateNamespace("shared1")
c.Assert(err, check.IsNil)
n2, err := h.CreateNamespace("shared2")
c.Assert(err, check.IsNil)
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil)
m1 := Machine{
ID: 0,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
NamespaceID: n1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.1",
AuthKeyID: uint(pak1.ID),
}
h.db.Save(&m1)
_, err = h.GetMachine(n1.Name, m1.Name)
c.Assert(err, check.IsNil)
m2 := Machine{
ID: 1,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
NamespaceID: n2.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.2",
AuthKeyID: uint(pak2.ID),
}
h.db.Save(&m2)
_, err = h.GetMachine(n2.Name, m2.Name)
c.Assert(err, check.IsNil)
p1s, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1s), check.Equals, 0)
err = h.AddSharedMachineToNamespace(&m2, n1)
c.Assert(err, check.IsNil)
p1sAfter, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1sAfter), check.Equals, 1)
c.Assert(len((*p1sAfter)[0].AllowedIPs), check.Equals, 1)
}
func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) {
n1, err := h.CreateNamespace("shared1")
c.Assert(err, check.IsNil)
n2, err := h.CreateNamespace("shared2")
c.Assert(err, check.IsNil)
n3, err := h.CreateNamespace("shared3")
c.Assert(err, check.IsNil)
pak1, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak2, err := h.CreatePreAuthKey(n2.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak3, err := h.CreatePreAuthKey(n3.Name, false, false, nil)
c.Assert(err, check.IsNil)
pak4, err := h.CreatePreAuthKey(n1.Name, false, false, nil)
c.Assert(err, check.IsNil)
_, err = h.GetMachine(n1.Name, "test_get_shared_nodes_1")
c.Assert(err, check.NotNil)
m1 := Machine{
ID: 0,
MachineKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
NodeKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
DiscoKey: "686824e749f3b7f2a5927ee6c1e422aee5292592d9179a271ed7b3e659b44a66",
Name: "test_get_shared_nodes_1",
NamespaceID: n1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.1",
AuthKeyID: uint(pak1.ID),
}
h.db.Save(&m1)
_, err = h.GetMachine(n1.Name, m1.Name)
c.Assert(err, check.IsNil)
m2 := Machine{
ID: 1,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_2",
NamespaceID: n2.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.2",
AuthKeyID: uint(pak2.ID),
}
h.db.Save(&m2)
_, err = h.GetMachine(n2.Name, m2.Name)
c.Assert(err, check.IsNil)
m3 := Machine{
ID: 2,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_3",
NamespaceID: n3.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.3",
AuthKeyID: uint(pak3.ID),
}
h.db.Save(&m3)
_, err = h.GetMachine(n3.Name, m3.Name)
c.Assert(err, check.IsNil)
m4 := Machine{
ID: 3,
MachineKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
NodeKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
DiscoKey: "dec46ef9dc45c7d2f03bfcd5a640d9e24e3cc68ce3d9da223867c9bc6d5e9863",
Name: "test_get_shared_nodes_4",
NamespaceID: n1.ID,
Registered: true,
RegisterMethod: "authKey",
IPAddress: "100.64.0.4",
AuthKeyID: uint(pak4.ID),
}
h.db.Save(&m4)
_, err = h.GetMachine(n1.Name, m4.Name)
c.Assert(err, check.IsNil)
p1s, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1s), check.Equals, 1) // nodes 1 and 4
err = h.AddSharedMachineToNamespace(&m2, n1)
c.Assert(err, check.IsNil)
p1sAfter, err := h.getPeers(m1)
c.Assert(err, check.IsNil)
c.Assert(len(*p1sAfter), check.Equals, 2) // nodes 1, 2, 4
pAlone, err := h.getPeers(m3)
c.Assert(err, check.IsNil)
c.Assert(len(*pAlone), check.Equals, 0) // node 3 is alone
}