Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
unreality authored Oct 16, 2021
2 parents 0603e29 + a9a1a8f commit afbfc1d
Show file tree
Hide file tree
Showing 23 changed files with 625 additions and 103 deletions.
15 changes: 12 additions & 3 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ builds:
- -mod=readonly
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}

- id: linux-armhf
main: ./cmd/headscale/headscale.go
mod_timestamp: '{{ .CommitTimestamp }}'
Expand Down Expand Up @@ -49,9 +50,16 @@ builds:
- linux
goarch:
- amd64
goarm:
- 6
- 7
main: ./cmd/headscale/headscale.go
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags:
- -s -w -X github.com/juanfont/headscale/cmd/headscale/cli.Version=v{{.Version}}

- id: linux-arm64
goos:
- linux
goarch:
- arm64
main: ./cmd/headscale/headscale.go
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags:
Expand All @@ -63,6 +71,7 @@ archives:
- darwin-amd64
- linux-armhf
- linux-amd64
- linux-arm64
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
format: binary

Expand Down
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Headscale
# headscale

[![Join the chat at https://gitter.im/headscale-dev/community](https://badges.gitter.im/headscale-dev/community.svg)](https://gitter.im/headscale-dev/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)
![ci](https://github.com/juanfont/headscale/actions/workflows/test.yml/badge.svg)

An open source, self-hosted implementation of the Tailscale coordination server.

Join our [Discord](https://discord.gg/XcQxk2VHjx) server for a chat.

## Overview

Tailscale is [a modern VPN](https://tailscale.com/) built on top of [Wireguard](https://www.wireguard.com/). It [works like an overlay network](https://tailscale.com/blog/how-tailscale-works/) between the computers of your networks - using all kinds of [NAT traversal sorcery](https://tailscale.com/blog/how-nat-traversal-works/).
Expand All @@ -29,18 +31,18 @@ Headscale implements this coordination server.
- [x] DNS (passing DNS servers to nodes)
- [x] Share nodes between ~~users~~ namespaces
- [x] SSO (via OIDC)
- [ ] MagicDNS / Smart DNS
- [x] MagicDNS (see `docs/`)

## Client OS support

| OS | Supports headscale |
| --- | --- |
| Linux | Yes |
| OpenBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes |
| OS | Supports headscale |
| ------- | ----------------------------------------------------------------------------------------------------------------- |
| Linux | Yes |
| OpenBSD | Yes |
| macOS | Yes (see `/apple` on your headscale for more information) |
| Windows | Yes |
| Android | [You need to compile the client yourself](https://github.com/juanfont/headscale/issues/58#issuecomment-885255270) |
| iOS | Not yet |
| iOS | Not yet |

## Roadmap 🤷

Expand Down
39 changes: 26 additions & 13 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
Str("func", "getMapResponse").
Str("machine", req.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := m.toNode(true)
node, err := m.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Str("func", "getMapResponse").
Expand All @@ -277,7 +277,7 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
DisplayName: m.Namespace.Name,
}

nodePeers, err := peers.toNodes(true)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
if err != nil {
log.Error().
Str("func", "getMapResponse").
Expand All @@ -286,20 +286,25 @@ func (h *Headscale) getMapResponse(mKey wgkey.Key, req tailcfg.MapRequest, m *Ma
return nil, err
}

var dnsConfig *tailcfg.DNSConfig
if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS is enabled
// Only inject the Search Domain of the current namespace - shared nodes should use their full FQDN
dnsConfig = h.cfg.DNSConfig.Clone()
dnsConfig.Domains = append(dnsConfig.Domains, fmt.Sprintf("%s.%s", m.Namespace.Name, h.cfg.BaseDomain))
} else {
dnsConfig = h.cfg.DNSConfig
}

resp := tailcfg.MapResponse{
KeepAlive: false,
Node: node,
Peers: nodePeers,
// TODO(kradalby): As per tailscale docs, if DNSConfig is nil,
// it means its not updated, maybe we can have some logic
// to check and only pass updates when its updates.
// This is probably more relevant if we try to implement
// "MagicDNS"
DNSConfig: h.cfg.DNSConfig,
SearchPaths: []string{},
Domain: "headscale.net",
KeepAlive: false,
Node: node,
Peers: nodePeers,
DNSConfig: dnsConfig,
Domain: h.cfg.BaseDomain,
PacketFilter: *h.aclRules,
DERPMap: h.cfg.DerpMap,

// TODO(juanfont): We should send the profiles of all the peers (this own namespace + those from the shared peers)
UserProfiles: []tailcfg.UserProfile{profile},
}
log.Trace().
Expand Down Expand Up @@ -365,6 +370,11 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key,
resp := tailcfg.RegisterResponse{}
pak, err := h.checkKeyValidity(req.Auth.AuthKey)
if err != nil {
log.Error().
Str("func", "handleAuthKey").
Str("machine", m.Name).
Err(err).
Msg("Failed authentication via AuthKey")
resp.MachineAuthorized = false
respBody, err := encode(resp, &idKey, h.privateKey)
if err != nil {
Expand Down Expand Up @@ -414,6 +424,9 @@ func (h *Headscale) handleAuthKey(c *gin.Context, db *gorm.DB, idKey wgkey.Key,
db.Save(&m)

h.updateMachineExpiry(&m) // TODO: do we want to do different expiry times for AuthKeys?

pak.Used = true
db.Save(&pak)

resp.MachineAuthorized = true
resp.User = *pak.Namespace.toUser()
Expand Down
15 changes: 14 additions & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ import (
"github.com/rs/zerolog/log"

"github.com/gin-gonic/gin"
"github.com/zsais/go-gin-prometheus"
ginprometheus "github.com/zsais/go-gin-prometheus"
"golang.org/x/crypto/acme"
"golang.org/x/crypto/acme/autocert"
"gorm.io/gorm"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/types/wgkey"
)

Expand All @@ -33,6 +34,7 @@ type Config struct {
DerpMap *tailcfg.DERPMap
EphemeralNodeInactivityTimeout time.Duration
IPPrefix netaddr.IPPrefix
BaseDomain string

DBtype string
DBpath string
Expand Down Expand Up @@ -125,6 +127,17 @@ func NewHeadscale(cfg Config) (*Headscale, error) {
if err != nil {
return nil, err
}
}

if h.cfg.DNSConfig != nil && h.cfg.DNSConfig.Proxied { // if MagicDNS
magicDNSDomains, err := generateMagicDNSRootDomains(h.cfg.IPPrefix, h.cfg.BaseDomain)
if err != nil {
return nil, err
}
h.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver)
for _, d := range magicDNSDomains {
h.cfg.DNSConfig.Routes[d.WithoutTrailingDot()] = nil
}
}

return &h, nil
Expand Down
26 changes: 19 additions & 7 deletions cmd/headscale/cli/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ var deleteNodeCmd = &cobra.Command{
return nil
},
Run: func(cmd *cobra.Command, args []string) {
output, _ := cmd.Flags().GetString("output")
h, err := getHeadscaleApp()
if err != nil {
log.Fatalf("Error initializing: %s", err)
Expand All @@ -143,21 +144,32 @@ var deleteNodeCmd = &cobra.Command{
}

confirm := false
prompt := &survey.Confirm{
Message: fmt.Sprintf("Do you want to remove the node %s?", m.Name),
}
err = survey.AskOne(prompt, &confirm)
if err != nil {
return
force, _ := cmd.Flags().GetBool("force")
if !force {
prompt := &survey.Confirm{
Message: fmt.Sprintf("Do you want to remove the node %s?", m.Name),
}
err = survey.AskOne(prompt, &confirm)
if err != nil {
return
}
}

if confirm {
if confirm || force {
err = h.DeleteMachine(m)
if strings.HasPrefix(output, "json") {
JsonOutput(map[string]string{"Result": "Node deleted"}, err, output)
return
}
if err != nil {
log.Fatalf("Error deleting node: %s", err)
}
fmt.Printf("Node deleted\n")
} else {
if strings.HasPrefix(output, "json") {
JsonOutput(map[string]string{"Result": "Node not deleted"}, err, output)
return
}
fmt.Printf("Node not deleted\n")
}
},
Expand Down
9 changes: 7 additions & 2 deletions cmd/headscale/cli/preauthkeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ var listPreAuthKeys = &cobra.Command{
return
}

d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Expiration", "Created"}}
d := pterm.TableData{{"ID", "Key", "Reusable", "Ephemeral", "Used", "Expiration", "Created"}}
for _, k := range *keys {
expiration := "-"
if k.Expiration != nil {
Expand All @@ -76,6 +76,7 @@ var listPreAuthKeys = &cobra.Command{
k.Key,
reusable,
strconv.FormatBool(k.Ephemeral),
fmt.Sprintf("%v", k.Used),
expiration,
k.CreatedAt.Format("2006-01-02 15:04:05"),
})
Expand Down Expand Up @@ -130,7 +131,7 @@ var createPreAuthKeyCmd = &cobra.Command{
}

var expirePreAuthKeyCmd = &cobra.Command{
Use: "expire",
Use: "expire KEY",
Short: "Expire a preauthkey",
Args: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
Expand All @@ -152,6 +153,10 @@ var expirePreAuthKeyCmd = &cobra.Command{

k, err := h.GetPreAuthKey(n, args[0])
if err != nil {
if strings.HasPrefix(o, "json") {
JsonOutput(k, err, o)
return
}
log.Fatalf("Error getting the key: %s", err)
}

Expand Down
1 change: 1 addition & 0 deletions cmd/headscale/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

func init() {
rootCmd.PersistentFlags().StringP("output", "o", "", "Output format. Empty for human-readable, 'json' or 'json-line'")
rootCmd.PersistentFlags().Bool("force", false, "Disable prompts and forces the execution")
}

var rootCmd = &cobra.Command{
Expand Down
41 changes: 37 additions & 4 deletions cmd/headscale/cli/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func LoadConfig(path string) error {

}

func GetDNSConfig() *tailcfg.DNSConfig {
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config") {
dnsConfig := &tailcfg.DNSConfig{}

Expand Down Expand Up @@ -108,10 +108,27 @@ func GetDNSConfig() *tailcfg.DNSConfig {
dnsConfig.Domains = viper.GetStringSlice("dns_config.domains")
}

return dnsConfig
if viper.IsSet("dns_config.magic_dns") {
magicDNS := viper.GetBool("dns_config.magic_dns")
if len(dnsConfig.Nameservers) > 0 {
dnsConfig.Proxied = magicDNS
} else if magicDNS {
log.Warn().
Msg("Warning: dns_config.magic_dns is set, but no nameservers are configured. Ignoring magic_dns.")
}
}

var baseDomain string
if viper.IsSet("dns_config.base_domain") {
baseDomain = viper.GetString("dns_config.base_domain")
} else {
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
}

return dnsConfig, baseDomain
}

return nil
return nil, ""
}

func absPath(path string) string {
Expand Down Expand Up @@ -144,7 +161,8 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
return nil, err
}

// maxMachineRegistrationDuration is the maximum time a client can request for a client registration

// maxMachineRegistrationDuration is the maximum time a client can request for a client registration
maxMachineRegistrationDuration, _ := time.ParseDuration("10h")
if viper.GetDuration("max_machine_registration_duration") >= time.Second {
maxMachineRegistrationDuration = viper.GetDuration("max_machine_registration_duration")
Expand All @@ -156,12 +174,15 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
defaultMachineRegistrationDuration = viper.GetDuration("default_machine_registration_duration")
}

dnsConfig, baseDomain := GetDNSConfig()

cfg := headscale.Config{
ServerURL: viper.GetString("server_url"),
Addr: viper.GetString("listen_addr"),
PrivateKeyPath: absPath(viper.GetString("private_key_path")),
DerpMap: derpMap,
IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")),
BaseDomain: baseDomain,

EphemeralNodeInactivityTimeout: viper.GetDuration("ephemeral_node_inactivity_timeout"),

Expand All @@ -181,6 +202,8 @@ func getHeadscaleApp() (*headscale.Headscale, error) {
TLSCertPath: absPath(viper.GetString("tls_cert_path")),
TLSKeyPath: absPath(viper.GetString("tls_key_path")),

DNSConfig: dnsConfig,

ACMEEmail: viper.GetString("acme_email"),
ACMEURL: viper.GetString("acme_url"),

Expand All @@ -192,6 +215,7 @@ func getHeadscaleApp() (*headscale.Headscale, error) {

MaxMachineRegistrationDuration: maxMachineRegistrationDuration, // the maximum duration a client may request for expiry time
DefaultMachineRegistrationDuration: defaultMachineRegistrationDuration, // if a client does not request a specific expiry time, use this duration

}

h, err := headscale.NewHeadscale(cfg)
Expand Down Expand Up @@ -261,3 +285,12 @@ func JsonOutput(result interface{}, errResult error, outputFormat string) {
}
fmt.Println(string(j))
}

func HasJsonOutputFlag() bool {
for _, arg := range os.Args {
if arg == "json" || arg == "json-line" {
return true
}
}
return false
}
3 changes: 2 additions & 1 deletion cmd/headscale/headscale.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ func main() {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}

if !viper.GetBool("disable_check_updates") {
jsonOutput := cli.HasJsonOutputFlag()
if !viper.GetBool("disable_check_updates") && !jsonOutput {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && cli.Version != "dev" {
githubTag := &latest.GithubTag{
Owner: "juanfont",
Expand Down
Loading

0 comments on commit afbfc1d

Please sign in to comment.