Skip to content

Commit

Permalink
Merge branch 'main' into preauthkey-tags
Browse files Browse the repository at this point in the history
  • Loading branch information
juanfont committed Sep 21, 2022
2 parents e056b86 + c9b39da commit 09863b5
Show file tree
Hide file tree
Showing 27 changed files with 959 additions and 130 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/test-integration.yml
Expand Up @@ -48,6 +48,15 @@ jobs:
retry_on: error
command: nix develop --command -- make test_integration_derp

- name: Run OIDC integration tests
if: steps.changed-files.outputs.any_changed == 'true'
uses: nick-fields/retry@v2
with:
timeout_minutes: 240
max_attempts: 5
retry_on: error
command: nix develop --command -- make test_integration_oidc

- name: Run general integration tests
if: steps.changed-files.outputs.any_changed == 'true'
uses: nick-fields/retry@v2
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,11 +2,19 @@

## 0.17.0 (2022-XX-XX)

### BREAKING

- Log level option `log_level` was moved to a distinct `log` config section and renamed to `level` [#768](https://github.com/juanfont/headscale/pull/768)

### Changes

- Added support for Tailscale TS2021 protocol [#738](https://github.com/juanfont/headscale/pull/738)
- Add ability to specify config location via env var `HEADSCALE_CONFIG` [#674](https://github.com/juanfont/headscale/issues/674)
- Target Go 1.19 for Headscale [#778](https://github.com/juanfont/headscale/pull/778)
- Target Tailscale v1.30.0 to build Headscale [#780](https://github.com/juanfont/headscale/pull/780)
- Give a warning when running Headscale with reverse proxy improperly configured for WebSockets [#788](https://github.com/juanfont/headscale/pull/788)
- Fix subnet routers with Primary Routes [#811](https://github.com/juanfont/headscale/pull/811)
- Added support for JSON logs [#653](https://github.com/juanfont/headscale/issues/653)

## 0.16.4 (2022-08-21)

Expand Down
5 changes: 4 additions & 1 deletion Makefile
Expand Up @@ -24,7 +24,7 @@ dev: lint test build
test:
@go test -coverprofile=coverage.out ./...

test_integration: test_integration_cli test_integration_derp test_integration_general
test_integration: test_integration_cli test_integration_derp test_integration_oidc test_integration_general

test_integration_cli:
go test -failfast -tags integration_cli,integration -timeout 30m -count=1 ./...
Expand All @@ -35,6 +35,9 @@ test_integration_derp:
test_integration_general:
go test -failfast -tags integration_general,integration -timeout 30m -count=1 ./...

test_integration_oidc:
go test -failfast -tags integration_oidc,integration -timeout 30m -count=1 ./...

coverprofile_func:
go tool cover -func=coverage.out

Expand Down
4 changes: 2 additions & 2 deletions api_common.go
Expand Up @@ -13,7 +13,7 @@ func (h *Headscale) generateMapResponse(
Str("func", "generateMapResponse").
Str("machine", mapRequest.Hostinfo.Hostname).
Msg("Creating Map response")
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
node, err := machine.toNode(h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil {
log.Error().
Caller().
Expand All @@ -37,7 +37,7 @@ func (h *Headscale) generateMapResponse(

profiles := getMapResponseUserProfiles(*machine, peers)

nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig, true)
nodePeers, err := peers.toNodes(h.cfg.BaseDomain, h.cfg.DNSConfig)
if err != nil {
log.Error().
Caller().
Expand Down
100 changes: 100 additions & 0 deletions cmd/headscale/cli/mockoidc.go
@@ -0,0 +1,100 @@
package cli

import (
"fmt"
"net"
"os"
"strconv"
"time"

"github.com/oauth2-proxy/mockoidc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)

const (
errMockOidcClientIDNotDefined = Error("MOCKOIDC_CLIENT_ID not defined")
errMockOidcClientSecretNotDefined = Error("MOCKOIDC_CLIENT_SECRET not defined")
errMockOidcPortNotDefined = Error("MOCKOIDC_PORT not defined")
accessTTL = 10 * time.Minute
refreshTTL = 60 * time.Minute
)

func init() {
rootCmd.AddCommand(mockOidcCmd)
}

var mockOidcCmd = &cobra.Command{
Use: "mockoidc",
Short: "Runs a mock OIDC server for testing",
Long: "This internal command runs a OpenID Connect for testing purposes",
Run: func(cmd *cobra.Command, args []string) {
err := mockOIDC()
if err != nil {
log.Error().Err(err).Msgf("Error running mock OIDC server")
os.Exit(1)
}
},
}

func mockOIDC() error {
clientID := os.Getenv("MOCKOIDC_CLIENT_ID")
if clientID == "" {
return errMockOidcClientIDNotDefined
}
clientSecret := os.Getenv("MOCKOIDC_CLIENT_SECRET")
if clientSecret == "" {
return errMockOidcClientSecretNotDefined
}
portStr := os.Getenv("MOCKOIDC_PORT")
if portStr == "" {
return errMockOidcPortNotDefined
}

port, err := strconv.Atoi(portStr)
if err != nil {
return err
}

mock, err := getMockOIDC(clientID, clientSecret)
if err != nil {
return err
}

listener, err := net.Listen("tcp", fmt.Sprintf("mockoidc:%d", port))
if err != nil {
return err
}

err = mock.Start(listener, nil)
if err != nil {
return err
}
log.Info().Msgf("Mock OIDC server listening on %s", listener.Addr().String())
log.Info().Msgf("Issuer: %s", mock.Issuer())
c := make(chan struct{})
<-c

return nil
}

func getMockOIDC(clientID string, clientSecret string) (*mockoidc.MockOIDC, error) {
keypair, err := mockoidc.NewKeypair(nil)
if err != nil {
return nil, err
}

mock := mockoidc.MockOIDC{
ClientID: clientID,
ClientSecret: clientSecret,
AccessTTL: accessTTL,
RefreshTTL: refreshTTL,
CodeChallengeMethodsSupported: []string{"plain", "S256"},
Keypair: keypair,
SessionStore: mockoidc.NewSessionStore(),
UserQueue: &mockoidc.UserQueue{},
ErrorQueue: &mockoidc.ErrorQueue{},
}

return &mock, nil
}
10 changes: 9 additions & 1 deletion cmd/headscale/cli/root.go
Expand Up @@ -15,6 +15,10 @@ import (
var cfgFile string = ""

func init() {
if len(os.Args) > 1 && os.Args[1] == "version" || os.Args[1] == "mockoidc" {
return
}

cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().
StringVarP(&cfgFile, "config", "c", "", "config file (default is /etc/headscale/config.yaml)")
Expand Down Expand Up @@ -47,14 +51,18 @@ func initConfig() {

machineOutput := HasMachineOutputFlag()

zerolog.SetGlobalLevel(cfg.LogLevel)
zerolog.SetGlobalLevel(cfg.Log.Level)

// If the user has requested a "machine" readable format,
// then disable login so the output remains valid.
if machineOutput {
zerolog.SetGlobalLevel(zerolog.Disabled)
}

if cfg.Log.Format == headscale.JSONLogFormat {
log.Logger = log.Output(os.Stdout)
}

if !cfg.DisableUpdateCheck && !machineOutput {
if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") &&
Version != "dev" {
Expand Down
5 changes: 4 additions & 1 deletion config-example.yaml
Expand Up @@ -172,7 +172,10 @@ tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""

log_level: info
log:
# Output formatting for logs: text or json
format: text
level: info

# Path to a file containg ACL policies.
# ACLs can be defined as YAML or HUJSON.
Expand Down
50 changes: 41 additions & 9 deletions config.go
Expand Up @@ -22,6 +22,9 @@ import (
const (
tlsALPN01ChallengeType = "TLS-ALPN-01"
http01ChallengeType = "HTTP-01"

JSONLogFormat = "json"
TextLogFormat = "text"
)

// Config contains the initial Headscale configuration.
Expand All @@ -37,7 +40,7 @@ type Config struct {
PrivateKeyPath string
NoisePrivateKeyPath string
BaseDomain string
LogLevel zerolog.Level
Log LogConfig
DisableUpdateCheck bool

DERP DERPConfig
Expand Down Expand Up @@ -124,6 +127,11 @@ type ACLConfig struct {
PolicyPath string
}

type LogConfig struct {
Format string
Level zerolog.Level
}

func LoadConfig(path string, isFile bool) error {
if isFile {
viper.SetConfigFile(path)
Expand All @@ -147,7 +155,8 @@ func LoadConfig(path string, isFile bool) error {
viper.SetDefault("tls_letsencrypt_challenge_type", http01ChallengeType)
viper.SetDefault("tls_client_auth_mode", "relaxed")

viper.SetDefault("log_level", "info")
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", TextLogFormat)

viper.SetDefault("dns_config", nil)

Expand Down Expand Up @@ -334,6 +343,34 @@ func GetACLConfig() ACLConfig {
}
}

func GetLogConfig() LogConfig {
logLevelStr := viper.GetString("log.level")
logLevel, err := zerolog.ParseLevel(logLevelStr)
if err != nil {
logLevel = zerolog.DebugLevel
}

logFormatOpt := viper.GetString("log.format")
var logFormat string
switch logFormatOpt {
case "json":
logFormat = JSONLogFormat
case "text":
logFormat = TextLogFormat
case "":
logFormat = TextLogFormat
default:
log.Error().
Str("func", "GetLogConfig").
Msgf("Could not parse log format: %s. Valid choices are 'json' or 'text'", logFormatOpt)
}

return LogConfig{
Format: logFormat,
Level: logLevel,
}
}

func GetDNSConfig() (*tailcfg.DNSConfig, string) {
if viper.IsSet("dns_config") {
dnsConfig := &tailcfg.DNSConfig{}
Expand Down Expand Up @@ -430,12 +467,6 @@ func GetHeadscaleConfig() (*Config, error) {
configuredPrefixes := viper.GetStringSlice("ip_prefixes")
parsedPrefixes := make([]netip.Prefix, 0, len(configuredPrefixes)+1)

logLevelStr := viper.GetString("log_level")
logLevel, err := zerolog.ParseLevel(logLevelStr)
if err != nil {
logLevel = zerolog.DebugLevel
}

legacyPrefixField := viper.GetString("ip_prefix")
if len(legacyPrefixField) > 0 {
log.
Expand Down Expand Up @@ -488,7 +519,6 @@ func GetHeadscaleConfig() (*Config, error) {
GRPCAddr: viper.GetString("grpc_listen_addr"),
GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"),
DisableUpdateCheck: viper.GetBool("disable_check_updates"),
LogLevel: logLevel,

IPPrefixes: prefixes,
PrivateKeyPath: AbsolutePathFromConfigPath(
Expand Down Expand Up @@ -550,5 +580,7 @@ func GetHeadscaleConfig() (*Config, error) {
},

ACL: GetACLConfig(),

Log: GetLogConfig(),
}, nil
}
1 change: 1 addition & 0 deletions docs/README.md
Expand Up @@ -28,6 +28,7 @@ written by community members. It is _not_ verified by `headscale` developers.

- [Running headscale in a container](running-headscale-container.md)
- [Running headscale on OpenBSD](running-headscale-openbsd.md)
- [Running headscale behind a reverse proxy](reverse-proxy.md)

## Misc

Expand Down
61 changes: 61 additions & 0 deletions docs/reverse-proxy.md
@@ -0,0 +1,61 @@
# Running headscale behind a reverse proxy

Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.

### WebSockets

The reverse proxy MUST be configured to support WebSockets, as it is needed for clients running Tailscale v1.30+.

WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml).

### TLS

Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.

```yaml
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
tls_cert_path: ""
tls_key_path: ""
```

## nginx

The following example configuration can be used in your nginx setup, substituting values as necessary. `<IP:PORT>` should be the IP address and port where headscale is running. In most cases, this will be `http://localhost:8080`.

```Nginx
map $http_upgrade $connection_upgrade {
default keep-alive;
'websocket' upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name <YOUR_SERVER_NAME>;
ssl_certificate <PATH_TO_CERT>;
ssl_certificate_key <PATH_CERT_KEY>;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://<IP:PORT>;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
}
```

0 comments on commit 09863b5

Please sign in to comment.