mirror of
https://github.com/juanfont/headscale.git
synced 2026-08-07 07:48:44 +09:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa27709e60 | |||
| 736182f651 | |||
| c4aa9d8aed | |||
| d8e0b16512 | |||
| d67be9ef58 | |||
| 69ba750b38 | |||
| df0d214faf | |||
| 73186eeb2f | |||
| fdcd3bb574 | |||
| c64d756ea7 | |||
| a63fb6b007 | |||
| 27e97cbd09 | |||
| 39550e262c | |||
| cfef55447f | |||
| 9c276f33bd |
+48
-11
@@ -4,29 +4,66 @@ before:
|
|||||||
hooks:
|
hooks:
|
||||||
- go mod tidy
|
- go mod tidy
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- id: darwin-amd64
|
||||||
- CGO_ENABLED=0
|
main: ./cmd/headscale/headscale.go
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
goos:
|
goos:
|
||||||
- linux
|
|
||||||
- windows
|
|
||||||
- darwin
|
- darwin
|
||||||
goarch:
|
goarch:
|
||||||
- amd64
|
- amd64
|
||||||
|
env:
|
||||||
|
- PKG_CONFIG_SYSROOT_DIR=/sysroot/macos/amd64
|
||||||
|
- PKG_CONFIG_PATH=/sysroot/macos/amd64/usr/local/lib/pkgconfig
|
||||||
|
- CC=o64-clang
|
||||||
|
- CXX=o64-clang++
|
||||||
|
flags:
|
||||||
|
- -mod=readonly
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X main.version={{.Version}}
|
||||||
|
- id: linux-armhf
|
||||||
|
main: ./cmd/headscale/headscale.go
|
||||||
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
goarm:
|
||||||
|
- 7
|
||||||
|
env:
|
||||||
|
- CC=arm-linux-gnueabihf-gcc
|
||||||
|
- CXX=arm-linux-gnueabihf-g++
|
||||||
|
- CGO_FLAGS=--sysroot=/sysroot/linux/armhf
|
||||||
|
- CGO_LDFLAGS=--sysroot=/sysroot/linux/armhf
|
||||||
|
- PKG_CONFIG_SYSROOT_DIR=/sysroot/linux/armhf
|
||||||
|
- PKG_CONFIG_PATH=/sysroot/linux/armhf/opt/vc/lib/pkgconfig:/sysroot/linux/armhf/usr/lib/arm-linux-gnueabihf/pkgconfig:/sysroot/linux/armhf/usr/lib/pkgconfig:/sysroot/linux/armhf/usr/local/lib/pkgconfig
|
||||||
|
flags:
|
||||||
|
- -mod=readonly
|
||||||
|
ldflags:
|
||||||
|
- -s -w -X main.version={{.Version}}
|
||||||
|
|
||||||
|
|
||||||
|
- id: linux-amd64
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
goarm:
|
goarm:
|
||||||
- 6
|
- 6
|
||||||
- 7
|
- 7
|
||||||
|
|
||||||
main: ./cmd/headscale/headscale.go
|
main: ./cmd/headscale/headscale.go
|
||||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- replacements:
|
- id: golang-cross
|
||||||
darwin: Darwin
|
builds:
|
||||||
linux: Linux
|
- darwin-amd64
|
||||||
windows: Windows
|
- linux-armhf
|
||||||
amd64: x86_64
|
- linux-amd64
|
||||||
|
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
|
format: zip
|
||||||
|
# wrap_in_directory: true
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:latest AS build
|
||||||
|
ENV GOPATH /go
|
||||||
|
COPY . /go/src/headscale
|
||||||
|
WORKDIR /go/src/headscale
|
||||||
|
RUN go install -a -ldflags="-extldflags=-static" -tags netgo,sqlite_omit_load_extension ./cmd/headscale
|
||||||
|
RUN test -e /go/bin/headscale
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=build /go/bin/headscale /go/bin/headscale
|
||||||
|
ENV TZ UTC
|
||||||
|
EXPOSE 8080/tcp
|
||||||
|
ENTRYPOINT ["/go/bin/headscale"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Headscale
|
# Headscale
|
||||||
|
|
||||||
[](https://gitter.im/headscale-dev/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 
|
[](https://gitter.im/headscale-dev/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 
|
||||||
|
|
||||||
An open source implementation of the Tailscale coordination server.
|
An open source implementation of the Tailscale coordination server.
|
||||||
|
|
||||||
@@ -39,10 +39,8 @@ Suggestions/PRs welcomed!
|
|||||||
|
|
||||||
## Running it
|
## Running it
|
||||||
|
|
||||||
1. Compile the headscale binary
|
1. Download the Headscale binary https://github.com/juanfont/headscale/releases, and place it somewhere in your PATH
|
||||||
```shell
|
|
||||||
make
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -63,33 +61,40 @@ Suggestions/PRs welcomed!
|
|||||||
cp config.json.sqlite.example config.json
|
cp config.json.sqlite.example config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Create a namespace (equivalent to a user in tailscale.com)
|
4. Create a namespace (a namespace is a 'tailnet', a group of Tailscale nodes that can talk to each other)
|
||||||
```shell
|
```shell
|
||||||
./headscale namespace create myfirstnamespace
|
headscale namespace create myfirstnamespace
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Run the server
|
5. Run the server
|
||||||
```shell
|
```shell
|
||||||
./headscale serve
|
headscale serve
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Add your first machine
|
6. If you used tailscale.com before in your nodes, make sure you clear the tailscaled data folder
|
||||||
|
```shell
|
||||||
|
systemctl stop tailscaled
|
||||||
|
rm -fr /var/lib/tailscale
|
||||||
|
systemctl start tailscaled
|
||||||
|
```
|
||||||
|
|
||||||
|
7. Add your first machine
|
||||||
```shell
|
```shell
|
||||||
tailscale up -login-server YOUR_HEADSCALE_URL
|
tailscale up -login-server YOUR_HEADSCALE_URL
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Navigate to the URL you will get with `tailscale up`, where you'll find your machine key.
|
8. Navigate to the URL you will get with `tailscale up`, where you'll find your machine key.
|
||||||
|
|
||||||
8. In the server, register your machine to a namespace with the CLI
|
9. In the server, register your machine to a namespace with the CLI
|
||||||
```shell
|
```shell
|
||||||
./headscale -n myfirstnamespace node register YOURMACHINEKEY
|
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:
|
||||||
|
|
||||||
1. Create an authkey
|
1. Create an authkey
|
||||||
```shell
|
```shell
|
||||||
./headscale -n myfirstnamespace preauthkey create --reusable --expiration 24h
|
headscale -n myfirstnamespace preauthkey create --reusable --expiration 24h
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Use the authkey from your machine to register it
|
2. Use the authkey from your machine to register it
|
||||||
@@ -162,7 +167,6 @@ To get a certificate automatically via [Let's Encrypt](https://letsencrypt.org/)
|
|||||||
|
|
||||||
1. We have nothing to do with Tailscale, or Tailscale Inc.
|
1. We have nothing to do with Tailscale, or Tailscale Inc.
|
||||||
2. The purpose of writing this was to learn how Tailscale works.
|
2. The purpose of writing this was to learn how Tailscale works.
|
||||||
3. ~~I don't use Headscale myself.~~
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package headscale
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -10,12 +11,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
|
"gorm.io/gorm"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/wgengine/wgcfg"
|
"tailscale.com/types/wgkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyHandler provides the Headscale pub key
|
// KeyHandler provides the Headscale pub key
|
||||||
@@ -60,7 +61,7 @@ func (h *Headscale) RegisterWebAPI(c *gin.Context) {
|
|||||||
func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
||||||
body, _ := io.ReadAll(c.Request.Body)
|
body, _ := io.ReadAll(c.Request.Body)
|
||||||
mKeyStr := c.Param("id")
|
mKeyStr := c.Param("id")
|
||||||
mKey, err := wgcfg.ParseHexKey(mKeyStr)
|
mKey, err := wgkey.ParseHex(mKeyStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot parse machine key: %s", err)
|
log.Printf("Cannot parse machine key: %s", err)
|
||||||
c.String(http.StatusInternalServerError, "Sad!")
|
c.String(http.StatusInternalServerError, "Sad!")
|
||||||
@@ -80,16 +81,15 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
|||||||
c.String(http.StatusInternalServerError, ":(")
|
c.String(http.StatusInternalServerError, ":(")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
var m Machine
|
var m Machine
|
||||||
if db.First(&m, "machine_key = ?", mKey.HexString()).RecordNotFound() {
|
if result := db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
log.Println("New Machine!")
|
log.Println("New Machine!")
|
||||||
m = Machine{
|
m = Machine{
|
||||||
Expiry: &req.Expiry,
|
Expiry: &req.Expiry,
|
||||||
MachineKey: mKey.HexString(),
|
MachineKey: mKey.HexString(),
|
||||||
Name: req.Hostinfo.Hostname,
|
Name: req.Hostinfo.Hostname,
|
||||||
NodeKey: wgcfg.Key(req.NodeKey).HexString(),
|
NodeKey: wgkey.Key(req.NodeKey).HexString(),
|
||||||
}
|
}
|
||||||
if err := db.Create(&m).Error; err != nil {
|
if err := db.Create(&m).Error; err != nil {
|
||||||
log.Printf("Could not create row: %s", err)
|
log.Printf("Could not create row: %s", err)
|
||||||
@@ -105,7 +105,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
|||||||
resp := tailcfg.RegisterResponse{}
|
resp := tailcfg.RegisterResponse{}
|
||||||
|
|
||||||
// We have the updated key!
|
// We have the updated key!
|
||||||
if m.NodeKey == wgcfg.Key(req.NodeKey).HexString() {
|
if m.NodeKey == wgkey.Key(req.NodeKey).HexString() {
|
||||||
if m.Registered {
|
if m.Registered {
|
||||||
log.Printf("[%s] Client is registered and we have the current NodeKey. All clear to /map", m.Name)
|
log.Printf("[%s] Client is registered and we have the current NodeKey. All clear to /map", m.Name)
|
||||||
resp.AuthURL = ""
|
resp.AuthURL = ""
|
||||||
@@ -135,9 +135,9 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The NodeKey we have matches OldNodeKey, which means this is a refresh after an key expiration
|
// The NodeKey we have matches OldNodeKey, which means this is a refresh after an key expiration
|
||||||
if m.NodeKey == wgcfg.Key(req.OldNodeKey).HexString() {
|
if m.NodeKey == wgkey.Key(req.OldNodeKey).HexString() {
|
||||||
log.Printf("[%s] We have the OldNodeKey in the database. This is a key refresh", m.Name)
|
log.Printf("[%s] We have the OldNodeKey in the database. This is a key refresh", m.Name)
|
||||||
m.NodeKey = wgcfg.Key(req.NodeKey).HexString()
|
m.NodeKey = wgkey.Key(req.NodeKey).HexString()
|
||||||
db.Save(&m)
|
db.Save(&m)
|
||||||
|
|
||||||
resp.AuthURL = ""
|
resp.AuthURL = ""
|
||||||
@@ -192,7 +192,7 @@ func (h *Headscale) RegistrationHandler(c *gin.Context) {
|
|||||||
func (h *Headscale) PollNetMapHandler(c *gin.Context) {
|
func (h *Headscale) PollNetMapHandler(c *gin.Context) {
|
||||||
body, _ := io.ReadAll(c.Request.Body)
|
body, _ := io.ReadAll(c.Request.Body)
|
||||||
mKeyStr := c.Param("id")
|
mKeyStr := c.Param("id")
|
||||||
mKey, err := wgcfg.ParseHexKey(mKeyStr)
|
mKey, err := wgkey.ParseHex(mKeyStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot parse client key: %s", err)
|
log.Printf("Cannot parse client key: %s", err)
|
||||||
return
|
return
|
||||||
@@ -209,9 +209,8 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
var m Machine
|
var m Machine
|
||||||
if db.First(&m, "machine_key = ?", mKey.HexString()).RecordNotFound() {
|
if result := db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
log.Printf("Ignoring request, cannot find machine with key %s", mKey.HexString())
|
log.Printf("Ignoring request, cannot find machine with key %s", mKey.HexString())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -219,7 +218,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
|
|||||||
hostinfo, _ := json.Marshal(req.Hostinfo)
|
hostinfo, _ := json.Marshal(req.Hostinfo)
|
||||||
m.Name = req.Hostinfo.Hostname
|
m.Name = req.Hostinfo.Hostname
|
||||||
m.HostInfo = datatypes.JSON(hostinfo)
|
m.HostInfo = datatypes.JSON(hostinfo)
|
||||||
m.DiscoKey = wgcfg.Key(req.DiscoKey).HexString()
|
m.DiscoKey = wgkey.Key(req.DiscoKey).HexString()
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
|
||||||
// From Tailscale client:
|
// From Tailscale client:
|
||||||
@@ -335,7 +334,7 @@ func (h *Headscale) PollNetMapHandler(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgcfg.Key, req tailcfg.MapRequest, m Machine) {
|
func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgkey.Key, req tailcfg.MapRequest, m Machine) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-cancel:
|
case <-cancel:
|
||||||
@@ -356,7 +355,7 @@ func (h *Headscale) keepAlive(cancel chan []byte, pollData chan []byte, mKey wgc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getMapResponse(mKey wgcfg.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) {
|
func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) {
|
||||||
node, err := m.toNode()
|
node, err := m.toNode()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Cannot convert to node: %s", err)
|
log.Printf("Cannot convert to node: %s", err)
|
||||||
@@ -377,7 +376,6 @@ func (h *Headscale) getMapResponse(mKey wgcfg.Key, req tailcfg.MapRequest, m Mac
|
|||||||
PacketFilter: tailcfg.FilterAllowAll,
|
PacketFilter: tailcfg.FilterAllowAll,
|
||||||
DERPMap: h.cfg.DerpMap,
|
DERPMap: h.cfg.DerpMap,
|
||||||
UserProfiles: []tailcfg.UserProfile{},
|
UserProfiles: []tailcfg.UserProfile{},
|
||||||
Roles: []tailcfg.Role{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var respBody []byte
|
var respBody []byte
|
||||||
@@ -403,7 +401,7 @@ func (h *Headscale) getMapResponse(mKey wgcfg.Key, req tailcfg.MapRequest, m Mac
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) getMapKeepAliveResponse(mKey wgcfg.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) {
|
func (h *Headscale) getMapKeepAliveResponse(mKey wgkey.Key, req tailcfg.MapRequest, m Machine) (*[]byte, error) {
|
||||||
resp := tailcfg.MapResponse{
|
resp := tailcfg.MapResponse{
|
||||||
KeepAlive: true,
|
KeepAlive: true,
|
||||||
}
|
}
|
||||||
@@ -429,7 +427,7 @@ func (h *Headscale) getMapKeepAliveResponse(mKey wgcfg.Key, req tailcfg.MapReque
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgcfg.Key, req tailcfg.RegisterRequest, m Machine) {
|
func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key, req tailcfg.RegisterRequest, m Machine) {
|
||||||
resp := tailcfg.RegisterResponse{}
|
resp := tailcfg.RegisterResponse{}
|
||||||
pak, err := h.checkKeyValidity(req.Auth.AuthKey)
|
pak, err := h.checkKeyValidity(req.Auth.AuthKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -453,7 +451,7 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgcfg.Key,
|
|||||||
m.AuthKeyID = uint(pak.ID)
|
m.AuthKeyID = uint(pak.ID)
|
||||||
m.IPAddress = ip.String()
|
m.IPAddress = ip.String()
|
||||||
m.NamespaceID = pak.NamespaceID
|
m.NamespaceID = pak.NamespaceID
|
||||||
m.NodeKey = wgcfg.Key(req.NodeKey).HexString() // we update it just in case
|
m.NodeKey = wgkey.Key(req.NodeKey).HexString() // we update it just in case
|
||||||
m.Registered = true
|
m.Registered = true
|
||||||
m.RegisterMethod = "authKey"
|
m.RegisterMethod = "authKey"
|
||||||
db.Save(&m)
|
db.Save(&m)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"golang.org/x/crypto/acme/autocert"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/wgengine/wgcfg"
|
"tailscale.com/types/wgkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config contains the initial Headscale configuration
|
// Config contains the initial Headscale configuration
|
||||||
@@ -46,8 +46,8 @@ type Headscale struct {
|
|||||||
dbString string
|
dbString string
|
||||||
dbType string
|
dbType string
|
||||||
dbDebug bool
|
dbDebug bool
|
||||||
publicKey *wgcfg.Key
|
publicKey *wgkey.Key
|
||||||
privateKey *wgcfg.PrivateKey
|
privateKey *wgkey.Private
|
||||||
|
|
||||||
pollMu sync.Mutex
|
pollMu sync.Mutex
|
||||||
clientsPolling map[uint64]chan []byte // this is by all means a hackity hack
|
clientsPolling map[uint64]chan []byte // this is by all means a hackity hack
|
||||||
@@ -59,7 +59,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
privKey, err := wgcfg.ParsePrivateKey(string(content))
|
privKey, err := wgkey.ParsePrivate(string(content))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,6 @@ func (h *Headscale) expireEphemeralNodesWorker() {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
namespaces, err := h.ListNamespaces()
|
namespaces, err := h.ListNamespaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite" // sql driver
|
|
||||||
|
|
||||||
"gopkg.in/check.v1"
|
"gopkg.in/check.v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"tailscale.com/wgengine/wgcfg"
|
"gorm.io/gorm"
|
||||||
|
"tailscale.com/types/wgkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey
|
// RegisterMachine is executed from the CLI to register a new Machine using its MachineKey
|
||||||
@@ -13,7 +14,7 @@ func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mKey, err := wgcfg.ParseHexKey(key)
|
mKey, err := wgkey.ParseHex(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -22,9 +23,8 @@ func (h *Headscale) RegisterMachine(key string, namespace string) (*Machine, err
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
m := Machine{}
|
m := Machine{}
|
||||||
if db.First(&m, "machine_key = ?", mKey.HexString()).RecordNotFound() {
|
if result := db.First(&m, "machine_key = ?", mKey.HexString()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, errors.New("Machine not found")
|
return nil, errors.New("Machine not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ func (s *Suite) TestRegisterMachine(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
m := Machine{
|
m := Machine{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -71,7 +72,11 @@ var ListNodesCmd = &cobra.Command{
|
|||||||
if m.AuthKey != nil && m.AuthKey.Ephemeral {
|
if m.AuthKey != nil && m.AuthKey.Ephemeral {
|
||||||
ephemeral = true
|
ephemeral = true
|
||||||
}
|
}
|
||||||
fmt.Printf("%s\t%s\t%t\n", m.Name, m.LastSeen.Format("2006-01-02 15:04:05"), ephemeral)
|
var lastSeen time.Time
|
||||||
|
if m.LastSeen != nil {
|
||||||
|
lastSeen = *m.LastSeen
|
||||||
|
}
|
||||||
|
fmt.Printf("%s\t%s\t%t\n", m.Name, lastSeen.Format("2006-01-02 15:04:05"), ephemeral)
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package headscale
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"gorm.io/driver/postgres"
|
||||||
_ "github.com/jinzhu/gorm/dialects/postgres" // sql driver
|
"gorm.io/driver/sqlite"
|
||||||
_ "github.com/jinzhu/gorm/dialects/sqlite" // sql driver
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const dbVersion = "1"
|
const dbVersion = "1"
|
||||||
@@ -17,30 +17,53 @@ type KV struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) initDB() error {
|
func (h *Headscale) initDB() error {
|
||||||
db, err := gorm.Open(h.dbType, h.dbString)
|
db, err := h.db()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if h.dbType == "postgres" {
|
if h.dbType == "postgres" {
|
||||||
db.Exec("create extension if not exists \"uuid-ossp\";")
|
db.Exec("create extension if not exists \"uuid-ossp\";")
|
||||||
}
|
}
|
||||||
db.AutoMigrate(&Machine{})
|
err = db.AutoMigrate(&Machine{})
|
||||||
db.AutoMigrate(&KV{})
|
if err != nil {
|
||||||
db.AutoMigrate(&Namespace{})
|
return err
|
||||||
db.AutoMigrate(&PreAuthKey{})
|
}
|
||||||
db.Close()
|
err = db.AutoMigrate(&KV{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.AutoMigrate(&Namespace{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.AutoMigrate(&PreAuthKey{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
err = h.setValue("db_version", dbVersion)
|
err = h.setValue("db_version", dbVersion)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Headscale) db() (*gorm.DB, error) {
|
func (h *Headscale) db() (*gorm.DB, error) {
|
||||||
db, err := gorm.Open(h.dbType, h.dbString)
|
var db *gorm.DB
|
||||||
|
var err error
|
||||||
|
switch h.dbType {
|
||||||
|
case "sqlite3":
|
||||||
|
db, err = gorm.Open(sqlite.Open(h.dbString), &gorm.Config{
|
||||||
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
})
|
||||||
|
case "postgres":
|
||||||
|
db, err = gorm.Open(postgres.Open(h.dbString), &gorm.Config{
|
||||||
|
DisableForeignKeyConstraintWhenMigrating: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if h.dbDebug {
|
if h.dbDebug {
|
||||||
db.LogMode(true)
|
db.Debug()
|
||||||
}
|
}
|
||||||
return db, nil
|
return db, nil
|
||||||
}
|
}
|
||||||
@@ -50,9 +73,8 @@ func (h *Headscale) getValue(key string) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
var row KV
|
var row KV
|
||||||
if db.First(&row, "key = ?", key).RecordNotFound() {
|
if result := db.First(&row, "key = ?", key); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return "", errors.New("not found")
|
return "", errors.New("not found")
|
||||||
}
|
}
|
||||||
return row.Value, nil
|
return row.Value, nil
|
||||||
@@ -67,7 +89,6 @@ func (h *Headscale) setValue(key string, value string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
_, err = h.getValue(key)
|
_, err = h.getValue(key)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
db.Model(&kv).Where("key = ?", key).Update("value", value)
|
db.Model(&kv).Where("key = ?", key).Update("value", value)
|
||||||
|
|||||||
@@ -3,26 +3,20 @@ module github.com/juanfont/headscale
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
github.com/gin-gonic/gin v1.7.2
|
||||||
github.com/gin-gonic/gin v1.7.1
|
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||||
github.com/hako/durafmt v0.0.0-20210316092057-3a2c319c1acd
|
github.com/klauspost/compress v1.13.1
|
||||||
github.com/jinzhu/gorm v1.9.16
|
github.com/lib/pq v1.10.2 // indirect
|
||||||
github.com/json-iterator/go v1.1.11 // indirect
|
|
||||||
github.com/klauspost/compress v1.12.2
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/lib/pq v1.10.1 // indirect
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
github.com/mattn/go-sqlite3 v1.14.7 // indirect
|
||||||
github.com/spf13/cobra v1.1.3
|
github.com/spf13/cobra v1.1.3
|
||||||
github.com/spf13/viper v1.7.1
|
github.com/spf13/viper v1.8.1
|
||||||
github.com/stretchr/testify v1.7.0 // indirect
|
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
|
||||||
golang.org/x/text v0.3.6 // indirect
|
|
||||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gorm.io/datatypes v1.0.1
|
gorm.io/datatypes v1.0.1
|
||||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c
|
gorm.io/driver/postgres v1.1.0
|
||||||
tailscale.com v1.6.0
|
gorm.io/driver/sqlite v1.1.4
|
||||||
|
gorm.io/gorm v1.21.11
|
||||||
|
inet.af/netaddr v0.0.0-20210603230628-bf05d8b52dda
|
||||||
|
tailscale.com v1.10.0
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/**/site
|
||||||
|
/**/secrets
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Deploying Headscale on Kubernetes
|
||||||
|
|
||||||
|
This directory contains [Kustomize](https://kustomize.io) templates that deploy
|
||||||
|
Headscale in various configurations.
|
||||||
|
|
||||||
|
These templates currently support Rancher k3s. Other clusters may require
|
||||||
|
adaptation, especially around volume claims and ingress.
|
||||||
|
|
||||||
|
Commands below assume this directory is your current working directory.
|
||||||
|
|
||||||
|
# Generate secrets and site configuration
|
||||||
|
|
||||||
|
Run `./init.bash` to generate keys, passwords, and site configuration files.
|
||||||
|
|
||||||
|
Edit `base/site/public.env`, changing `public-hostname` to the public DNS name
|
||||||
|
that will be used for your headscale deployment.
|
||||||
|
|
||||||
|
Set `public-proto` to "https" if you're planning to use TLS & Let's Encrypt.
|
||||||
|
|
||||||
|
Configure DERP servers by editing `base/site/derp.yaml` if needed.
|
||||||
|
|
||||||
|
# Add the image to the registry
|
||||||
|
|
||||||
|
You'll somehow need to get `headscale:latest` into your cluster image registry.
|
||||||
|
|
||||||
|
An easy way to do this with k3s:
|
||||||
|
- Reconfigure k3s to use docker instead of containerd (`k3s server --docker`)
|
||||||
|
- `docker build -t headscale:latest ..` from here
|
||||||
|
|
||||||
|
# Create the namespace
|
||||||
|
|
||||||
|
If it doesn't already exist, `kubectl create ns headscale`.
|
||||||
|
|
||||||
|
# Deploy headscale
|
||||||
|
|
||||||
|
## sqlite
|
||||||
|
|
||||||
|
`kubectl -n headscale apply -k ./sqlite`
|
||||||
|
|
||||||
|
## postgres
|
||||||
|
|
||||||
|
`kubectl -n headscale apply -k ./postgres`
|
||||||
|
|
||||||
|
# TLS & Let's Encrypt
|
||||||
|
|
||||||
|
Test a staging certificate with your configured DNS name and Let's Encrypt.
|
||||||
|
|
||||||
|
`kubectl -n headscale apply -k ./staging-tls`
|
||||||
|
|
||||||
|
Replace with a production certificate.
|
||||||
|
|
||||||
|
`kubectl -n headscale apply -k ./production-tls`
|
||||||
|
|
||||||
|
## Static / custom TLS certificates
|
||||||
|
|
||||||
|
Only Let's Encrypt is supported. If you need other TLS settings, modify or patch the ingress.
|
||||||
|
|
||||||
|
# Administration
|
||||||
|
|
||||||
|
Use the wrapper script to remotely operate headscale to perform administrative
|
||||||
|
tasks like creating namespaces, authkeys, etc.
|
||||||
|
|
||||||
|
```
|
||||||
|
[c@nix-slate:~/Projects/headscale/k8s]$ ./headscale.bash
|
||||||
|
|
||||||
|
headscale is an open source implementation of the Tailscale control server
|
||||||
|
|
||||||
|
Juan Font Alonso <juanfontalonso@gmail.com> - 2021
|
||||||
|
https://gitlab.com/juanfont/headscale
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
headscale [command]
|
||||||
|
|
||||||
|
Available Commands:
|
||||||
|
help Help about any command
|
||||||
|
namespace Manage the namespaces of Headscale
|
||||||
|
node Manage the nodes of Headscale
|
||||||
|
preauthkey Handle the preauthkeys in Headscale
|
||||||
|
routes Manage the routes of Headscale
|
||||||
|
serve Launches the headscale server
|
||||||
|
version Print the version.
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for headscale
|
||||||
|
-o, --output string Output format. Empty for human-readable, 'json' or 'json-line'
|
||||||
|
|
||||||
|
Use "headscale [command] --help" for more information about a command.
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
# TODO / Ideas
|
||||||
|
|
||||||
|
- Github action to publish the docker image
|
||||||
|
- 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.
|
||||||
|
- Add kustomize examples for cloud-native ingress, load balancer
|
||||||
|
- CockroachDB for the backend
|
||||||
|
- DERP server deployment
|
||||||
|
- Tor hidden service
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: headscale-config
|
||||||
|
data:
|
||||||
|
server_url: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
|
||||||
|
listen_addr: "0.0.0.0:8080"
|
||||||
|
ephemeral_node_inactivity_timeout: "30m"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: headscale
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: $(PUBLIC_HOSTNAME)
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- backend:
|
||||||
|
service:
|
||||||
|
name: headscale
|
||||||
|
port:
|
||||||
|
number: 8080
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace: headscale
|
||||||
|
resources:
|
||||||
|
- configmap.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- service.yaml
|
||||||
|
generatorOptions:
|
||||||
|
disableNameSuffixHash: true
|
||||||
|
configMapGenerator:
|
||||||
|
- name: headscale-site
|
||||||
|
files:
|
||||||
|
- derp.yaml=site/derp.yaml
|
||||||
|
envs:
|
||||||
|
- site/public.env
|
||||||
|
- name: headscale-etc
|
||||||
|
literals:
|
||||||
|
- config.json={}
|
||||||
|
secretGenerator:
|
||||||
|
- name: headscale
|
||||||
|
files:
|
||||||
|
- secrets/private-key
|
||||||
|
vars:
|
||||||
|
- name: PUBLIC_PROTO
|
||||||
|
objRef:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: headscale-site
|
||||||
|
apiVersion: v1
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: data.public-proto
|
||||||
|
- name: PUBLIC_HOSTNAME
|
||||||
|
objRef:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: headscale-site
|
||||||
|
apiVersion: v1
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: data.public-hostname
|
||||||
|
- name: CONTACT_EMAIL
|
||||||
|
objRef:
|
||||||
|
kind: ConfigMap
|
||||||
|
name: headscale-site
|
||||||
|
apiVersion: v1
|
||||||
|
fieldRef:
|
||||||
|
fieldPath: data.contact-email
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: headscale
|
||||||
|
labels:
|
||||||
|
app: headscale
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: headscale
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
targetPort: http
|
||||||
|
port: 8080
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eu
|
||||||
|
exec kubectl -n headscale exec -ti pod/headscale-0 -- /go/bin/headscale "$@"
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eux
|
||||||
|
cd $(dirname $0)
|
||||||
|
|
||||||
|
umask 022
|
||||||
|
mkdir -p base/site/
|
||||||
|
[ ! -e base/site/public.env ] && (
|
||||||
|
cat >base/site/public.env <<EOF
|
||||||
|
public-hostname=localhost
|
||||||
|
public-proto=http
|
||||||
|
contact-email=headscale@example.com
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
[ ! -e base/site/derp.yaml ] && cp ../derp.yaml base/site/derp.yaml
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
mkdir -p base/secrets/
|
||||||
|
[ ! -e base/secrets/private-key ] && (
|
||||||
|
wg genkey > base/secrets/private-key
|
||||||
|
)
|
||||||
|
mkdir -p postgres/secrets/
|
||||||
|
[ ! -e postgres/secrets/password ] && (head -c 32 /dev/urandom | base64 -w0 > postgres/secrets/password)
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eux
|
||||||
|
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.4.0/cert-manager.yaml
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: headscale
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: headscale
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: headscale
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: headscale
|
||||||
|
image: "headscale:latest"
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command: ["/go/bin/headscale", "serve"]
|
||||||
|
env:
|
||||||
|
- name: SERVER_URL
|
||||||
|
value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
|
||||||
|
- name: LISTEN_ADDR
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: headscale-config
|
||||||
|
key: listen_addr
|
||||||
|
- name: PRIVATE_KEY_PATH
|
||||||
|
value: /vol/secret/private-key
|
||||||
|
- name: DERP_MAP_PATH
|
||||||
|
value: /vol/config/derp.yaml
|
||||||
|
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: headscale-config
|
||||||
|
key: ephemeral_node_inactivity_timeout
|
||||||
|
- name: DB_TYPE
|
||||||
|
value: postgres
|
||||||
|
- name: DB_HOST
|
||||||
|
value: postgres.headscale.svc.cluster.local
|
||||||
|
- name: DB_PORT
|
||||||
|
value: "5432"
|
||||||
|
- name: DB_USER
|
||||||
|
value: headscale
|
||||||
|
- name: DB_PASS
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: postgresql
|
||||||
|
key: password
|
||||||
|
- name: DB_NAME
|
||||||
|
value: headscale
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
protocol: TCP
|
||||||
|
containerPort: 8080
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 15
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /vol/config
|
||||||
|
- name: secret
|
||||||
|
mountPath: /vol/secret
|
||||||
|
- name: etc
|
||||||
|
mountPath: /etc/headscale
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: headscale-site
|
||||||
|
- name: etc
|
||||||
|
configMap:
|
||||||
|
name: headscale-etc
|
||||||
|
- name: secret
|
||||||
|
secret:
|
||||||
|
secretName: headscale
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace: headscale
|
||||||
|
bases:
|
||||||
|
- ../base
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- postgres-service.yaml
|
||||||
|
- postgres-statefulset.yaml
|
||||||
|
generatorOptions:
|
||||||
|
disableNameSuffixHash: true
|
||||||
|
secretGenerator:
|
||||||
|
- name: postgresql
|
||||||
|
files:
|
||||||
|
- secrets/password
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: postgres
|
||||||
|
labels:
|
||||||
|
app: postgres
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: postgres
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
targetPort: postgres
|
||||||
|
port: 5432
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: postgres
|
||||||
|
spec:
|
||||||
|
serviceName: postgres
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: postgres
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: postgres
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: "postgres:13"
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: postgresql
|
||||||
|
key: password
|
||||||
|
- name: POSTGRES_USER
|
||||||
|
value: headscale
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
protocol: TCP
|
||||||
|
containerPort: 5432
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 5432
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 15
|
||||||
|
volumeMounts:
|
||||||
|
- name: pgdata
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: pgdata
|
||||||
|
spec:
|
||||||
|
storageClassName: local-path
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: headscale
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-production
|
||||||
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
|
spec:
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- $(PUBLIC_HOSTNAME)
|
||||||
|
secretName: production-cert
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace: headscale
|
||||||
|
bases:
|
||||||
|
- ../base
|
||||||
|
resources:
|
||||||
|
- production-issuer.yaml
|
||||||
|
patches:
|
||||||
|
- path: ingress-patch.yaml
|
||||||
|
target:
|
||||||
|
kind: Ingress
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-production
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
# TODO: figure out how to get kustomize to interpolate this, or use a transformer
|
||||||
|
#email: $(CONTACT_EMAIL)
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
privateKeySecretRef:
|
||||||
|
# Secret resource used to store the account's private key.
|
||||||
|
name: letsencrypt-production-acc-key
|
||||||
|
solvers:
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace: headscale
|
||||||
|
bases:
|
||||||
|
- ../base
|
||||||
|
resources:
|
||||||
|
- statefulset.yaml
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: headscale
|
||||||
|
spec:
|
||||||
|
serviceName: headscale
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: headscale
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: headscale
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: headscale
|
||||||
|
image: "headscale:latest"
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command: ["/go/bin/headscale", "serve"]
|
||||||
|
env:
|
||||||
|
- name: SERVER_URL
|
||||||
|
value: $(PUBLIC_PROTO)://$(PUBLIC_HOSTNAME)
|
||||||
|
- name: LISTEN_ADDR
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: headscale-config
|
||||||
|
key: listen_addr
|
||||||
|
- name: PRIVATE_KEY_PATH
|
||||||
|
value: /vol/secret/private-key
|
||||||
|
- name: DERP_MAP_PATH
|
||||||
|
value: /vol/config/derp.yaml
|
||||||
|
- name: EPHEMERAL_NODE_INACTIVITY_TIMEOUT
|
||||||
|
valueFrom:
|
||||||
|
configMapKeyRef:
|
||||||
|
name: headscale-config
|
||||||
|
key: ephemeral_node_inactivity_timeout
|
||||||
|
- name: DB_TYPE
|
||||||
|
value: sqlite3
|
||||||
|
- name: DB_PATH
|
||||||
|
value: /vol/data/db.sqlite
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
protocol: TCP
|
||||||
|
containerPort: 8080
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: http
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
timeoutSeconds: 5
|
||||||
|
periodSeconds: 15
|
||||||
|
volumeMounts:
|
||||||
|
- name: config
|
||||||
|
mountPath: /vol/config
|
||||||
|
- name: data
|
||||||
|
mountPath: /vol/data
|
||||||
|
- name: secret
|
||||||
|
mountPath: /vol/secret
|
||||||
|
- name: etc
|
||||||
|
mountPath: /etc/headscale
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap:
|
||||||
|
name: headscale-site
|
||||||
|
- name: etc
|
||||||
|
configMap:
|
||||||
|
name: headscale-etc
|
||||||
|
- name: secret
|
||||||
|
secret:
|
||||||
|
secretName: headscale
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
storageClassName: local-path
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 1Gi
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: headscale
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: letsencrypt-staging
|
||||||
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
|
spec:
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- $(PUBLIC_HOSTNAME)
|
||||||
|
secretName: staging-cert
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace: headscale
|
||||||
|
bases:
|
||||||
|
- ../base
|
||||||
|
resources:
|
||||||
|
- staging-issuer.yaml
|
||||||
|
patches:
|
||||||
|
- path: ingress-patch.yaml
|
||||||
|
target:
|
||||||
|
kind: Ingress
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-staging
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
# TODO: figure out how to get kustomize to interpolate this, or use a transformer
|
||||||
|
#email: $(CONTACT_EMAIL)
|
||||||
|
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
privateKeySecretRef:
|
||||||
|
# Secret resource used to store the account's private key.
|
||||||
|
name: letsencrypt-staging-acc-key
|
||||||
|
solvers:
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
+5
-6
@@ -11,7 +11,7 @@ import (
|
|||||||
"gorm.io/datatypes"
|
"gorm.io/datatypes"
|
||||||
"inet.af/netaddr"
|
"inet.af/netaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/wgengine/wgcfg"
|
"tailscale.com/types/wgkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Machine is a Headscale client
|
// Machine is a Headscale client
|
||||||
@@ -23,7 +23,7 @@ type Machine struct {
|
|||||||
IPAddress string
|
IPAddress string
|
||||||
Name string
|
Name string
|
||||||
NamespaceID uint
|
NamespaceID uint
|
||||||
Namespace Namespace
|
Namespace Namespace `gorm:"foreignKey:NamespaceID"`
|
||||||
|
|
||||||
Registered bool // temp
|
Registered bool // temp
|
||||||
RegisterMethod string
|
RegisterMethod string
|
||||||
@@ -48,18 +48,18 @@ func (m Machine) isAlreadyRegistered() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Machine) toNode() (*tailcfg.Node, error) {
|
func (m Machine) toNode() (*tailcfg.Node, error) {
|
||||||
nKey, err := wgcfg.ParseHexKey(m.NodeKey)
|
nKey, err := wgkey.ParseHex(m.NodeKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
mKey, err := wgcfg.ParseHexKey(m.MachineKey)
|
mKey, err := wgkey.ParseHex(m.MachineKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var discoKey tailcfg.DiscoKey
|
var discoKey tailcfg.DiscoKey
|
||||||
if m.DiscoKey != "" {
|
if m.DiscoKey != "" {
|
||||||
dKey, err := wgcfg.ParseHexKey(m.DiscoKey)
|
dKey, err := wgkey.ParseHex(m.DiscoKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,6 @@ func (h *Headscale) getPeers(m Machine) (*[]*tailcfg.Node, error) {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
machines := []Machine{}
|
machines := []Machine{}
|
||||||
if err = db.Where("namespace_id = ? AND machine_key <> ? AND registered",
|
if err = db.Where("namespace_id = ? AND machine_key <> ? AND registered",
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ func (s *Suite) TestGetMachine(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
_, err = h.GetMachine("test", "testmachine")
|
_, err = h.GetMachine("test", "testmachine")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|||||||
+4
-11
@@ -1,10 +1,11 @@
|
|||||||
package headscale
|
package headscale
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"gorm.io/gorm"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +30,6 @@ func (h *Headscale) CreateNamespace(name string) (*Namespace, error) {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
n := Namespace{}
|
n := Namespace{}
|
||||||
if err := db.Where("name = ?", name).First(&n).Error; err == nil {
|
if err := db.Where("name = ?", name).First(&n).Error; err == nil {
|
||||||
@@ -51,7 +51,6 @@ func (h *Headscale) DestroyNamespace(name string) error {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
n, err := h.GetNamespace(name)
|
n, err := h.GetNamespace(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,8 +65,7 @@ func (h *Headscale) DestroyNamespace(name string) error {
|
|||||||
return errorNamespaceNotEmpty
|
return errorNamespaceNotEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
err = db.Unscoped().Delete(&n).Error
|
if result := db.Unscoped().Delete(&n); result.Error != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,10 +79,9 @@ func (h *Headscale) GetNamespace(name string) (*Namespace, error) {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
n := Namespace{}
|
n := Namespace{}
|
||||||
if db.First(&n, "name = ?", name).RecordNotFound() {
|
if result := db.First(&n, "name = ?", name); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, errorNamespaceNotFound
|
return nil, errorNamespaceNotFound
|
||||||
}
|
}
|
||||||
return &n, nil
|
return &n, nil
|
||||||
@@ -97,7 +94,6 @@ func (h *Headscale) ListNamespaces() (*[]Namespace, error) {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
namespaces := []Namespace{}
|
namespaces := []Namespace{}
|
||||||
if err := db.Find(&namespaces).Error; err != nil {
|
if err := db.Find(&namespaces).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -116,7 +112,6 @@ func (h *Headscale) ListMachinesInNamespace(name string) (*[]Machine, error) {
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
machines := []Machine{}
|
machines := []Machine{}
|
||||||
if err := db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
if err := db.Preload("AuthKey").Where(&Machine{NamespaceID: n.ID}).Find(&machines).Error; err != nil {
|
||||||
@@ -136,7 +131,6 @@ func (h *Headscale) SetMachineNamespace(m *Machine, namespaceName string) error
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
m.NamespaceID = n.ID
|
m.NamespaceID = n.ID
|
||||||
db.Save(&m)
|
db.Save(&m)
|
||||||
return nil
|
return nil
|
||||||
@@ -150,7 +144,6 @@ func (n *Namespace) toUser() *tailcfg.User {
|
|||||||
ProfilePicURL: "",
|
ProfilePicURL: "",
|
||||||
Domain: "",
|
Domain: "",
|
||||||
Logins: []tailcfg.LoginID{},
|
Logins: []tailcfg.LoginID{},
|
||||||
Roles: []tailcfg.RoleID{},
|
|
||||||
Created: time.Time{},
|
Created: time.Time{},
|
||||||
}
|
}
|
||||||
return &u
|
return &u
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ func (s *Suite) TestDestroyNamespaceErrors(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
m := Machine{
|
m := Machine{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
MachineKey: "foo",
|
MachineKey: "foo",
|
||||||
|
|||||||
+4
-4
@@ -3,8 +3,11 @@ package headscale
|
|||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const errorAuthKeyNotFound = Error("AuthKey not found")
|
const errorAuthKeyNotFound = Error("AuthKey not found")
|
||||||
@@ -36,7 +39,6 @@ func (h *Headscale) CreatePreAuthKey(namespaceName string, reusable bool, epheme
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
kstr, err := h.generateKey()
|
kstr, err := h.generateKey()
|
||||||
@@ -69,7 +71,6 @@ func (h *Headscale) GetPreAuthKeys(namespaceName string) (*[]PreAuthKey, error)
|
|||||||
log.Printf("Cannot open DB: %s", err)
|
log.Printf("Cannot open DB: %s", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
keys := []PreAuthKey{}
|
keys := []PreAuthKey{}
|
||||||
if err := db.Preload("Namespace").Where(&PreAuthKey{NamespaceID: n.ID}).Find(&keys).Error; err != nil {
|
if err := db.Preload("Namespace").Where(&PreAuthKey{NamespaceID: n.ID}).Find(&keys).Error; err != nil {
|
||||||
@@ -85,10 +86,9 @@ func (h *Headscale) checkKeyValidity(k string) (*PreAuthKey, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
pak := PreAuthKey{}
|
pak := PreAuthKey{}
|
||||||
if db.Preload("Namespace").First(&pak, "key = ?", k).RecordNotFound() {
|
if result := db.Preload("Namespace").First(&pak, "key = ?", k); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return nil, errorAuthKeyNotFound
|
return nil, errorAuthKeyNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ func (*Suite) TestAlreadyUsedKey(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
m := Machine{
|
m := Machine{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
MachineKey: "foo",
|
MachineKey: "foo",
|
||||||
@@ -107,7 +106,6 @@ func (*Suite) TestReusableBeingUsedKey(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
m := Machine{
|
m := Machine{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
MachineKey: "foo",
|
MachineKey: "foo",
|
||||||
@@ -149,7 +147,6 @@ func (*Suite) TestEphemeralKey(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
m := Machine{
|
m := Machine{
|
||||||
ID: 0,
|
ID: 0,
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ func (h *Headscale) EnableNodeRoute(namespace string, nodeName string, routeStr
|
|||||||
routes, _ := json.Marshal([]string{routeStr}) // TODO: only one for the time being, so overwriting the rest
|
routes, _ := json.Marshal([]string{routeStr}) // TODO: only one for the time being, so overwriting the rest
|
||||||
m.EnabledRoutes = datatypes.JSON(routes)
|
m.EnabledRoutes = datatypes.JSON(routes)
|
||||||
db.Save(&m)
|
db.Save(&m)
|
||||||
db.Close()
|
|
||||||
|
|
||||||
// THIS IS COMPLETELY USELESS.
|
// THIS IS COMPLETELY USELESS.
|
||||||
// The peers map is stored in memory in the server process.
|
// The peers map is stored in memory in the server process.
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ func (s *Suite) TestGetRoutes(c *check.C) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
c.Fatal(err)
|
c.Fatal(err)
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
|
|
||||||
_, err = h.GetMachine("test", "testmachine")
|
_, err = h.GetMachine("test", "testmachine")
|
||||||
c.Assert(err, check.NotNil)
|
c.Assert(err, check.NotNil)
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import (
|
|||||||
mathrand "math/rand"
|
mathrand "math/rand"
|
||||||
|
|
||||||
"golang.org/x/crypto/nacl/box"
|
"golang.org/x/crypto/nacl/box"
|
||||||
"tailscale.com/wgengine/wgcfg"
|
"gorm.io/gorm"
|
||||||
|
"tailscale.com/types/wgkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
|
// Error is used to compare errors as per https://dave.cheney.net/2016/04/07/constant-errors
|
||||||
@@ -26,11 +27,11 @@ type Error string
|
|||||||
|
|
||||||
func (e Error) Error() string { return string(e) }
|
func (e Error) Error() string { return string(e) }
|
||||||
|
|
||||||
func decode(msg []byte, v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) error {
|
func decode(msg []byte, v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) error {
|
||||||
return decodeMsg(msg, v, pubKey, privKey)
|
return decodeMsg(msg, v, pubKey, privKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decodeMsg(msg []byte, v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) error {
|
func decodeMsg(msg []byte, v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) error {
|
||||||
decrypted, err := decryptMsg(msg, pubKey, privKey)
|
decrypted, err := decryptMsg(msg, pubKey, privKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -42,7 +43,7 @@ func decodeMsg(msg []byte, v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.Priv
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func decryptMsg(msg []byte, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) ([]byte, error) {
|
func decryptMsg(msg []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, error) {
|
||||||
var nonce [24]byte
|
var nonce [24]byte
|
||||||
if len(msg) < len(nonce)+1 {
|
if len(msg) < len(nonce)+1 {
|
||||||
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
|
return nil, fmt.Errorf("response missing nonce, len=%d", len(msg))
|
||||||
@@ -58,7 +59,7 @@ func decryptMsg(msg []byte, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) ([]byt
|
|||||||
return decrypted, nil
|
return decrypted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func encode(v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) ([]byte, error) {
|
func encode(v interface{}, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, error) {
|
||||||
b, err := json.Marshal(v)
|
b, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -66,7 +67,7 @@ func encode(v interface{}, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) ([]byte
|
|||||||
return encodeMsg(b, pubKey, privKey)
|
return encodeMsg(b, pubKey, privKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeMsg(b []byte, pubKey *wgcfg.Key, privKey *wgcfg.PrivateKey) ([]byte, error) {
|
func encodeMsg(b []byte, pubKey *wgkey.Key, privKey *wgkey.Private) ([]byte, error) {
|
||||||
var nonce [24]byte
|
var nonce [24]byte
|
||||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -81,7 +82,6 @@ func (h *Headscale) getAvailableIP() (*net.IP, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
|
||||||
i := 0
|
i := 0
|
||||||
for {
|
for {
|
||||||
ip, err := getRandomIP()
|
ip, err := getRandomIP()
|
||||||
@@ -89,7 +89,7 @@ func (h *Headscale) getAvailableIP() (*net.IP, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
m := Machine{}
|
m := Machine{}
|
||||||
if db.First(&m, "ip_address = ?", ip.String()).RecordNotFound() {
|
if result := db.First(&m, "ip_address = ?", ip.String()); errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
return ip, nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
i++
|
i++
|
||||||
|
|||||||
Reference in New Issue
Block a user