Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/test-integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
- TestResolveMagicDNS
- TestValidateResolvConf
- TestDERPServerScenario
- TestDERPServerWebsocketScenario
- TestPingAllByIP
- TestPingAllByIPPublicDERP
- TestAuthKeyLogoutAndRelogin
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.23.0

require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/coder/websocket v1.8.12
github.com/coreos/go-oidc/v3 v3.11.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/deckarep/golang-set/v2 v2.6.0
Expand Down Expand Up @@ -79,7 +80,6 @@ require (
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
Expand Down
2 changes: 1 addition & 1 deletion hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router {
router := mux.NewRouter()
router.Use(prometheusMiddleware)

router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost)
router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost, http.MethodGet)

router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet)
router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet)
Expand Down
52 changes: 52 additions & 0 deletions hscontrol/derp/server/derp_server.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package server

import (
"bufio"
"context"
"encoding/json"
"fmt"
Expand All @@ -12,11 +13,13 @@ import (
"strings"
"time"

"github.com/coder/websocket"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"tailscale.com/derp"
"tailscale.com/net/stun"
"tailscale.com/net/wsconn"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
Expand Down Expand Up @@ -132,6 +135,55 @@ func (d *DERPServer) DERPHandler(
return
}

if strings.Contains(req.Header.Get("Sec-Websocket-Protocol"), "derp") {
d.serveWebsocket(writer, req)
} else {
d.servePlain(writer, req)
}
}

func (d *DERPServer) serveWebsocket(writer http.ResponseWriter, req *http.Request) {
websocketConn, err := websocket.Accept(writer, req, &websocket.AcceptOptions{
Subprotocols: []string{"derp"},
OriginPatterns: []string{"*"},
// Disable compression because DERP transmits WireGuard messages that
// are not compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to upgrade websocket request")

writer.Header().Set("Content-Type", "text/plain")
writer.WriteHeader(http.StatusInternalServerError)

_, err = writer.Write([]byte("Failed to upgrade websocket request"))
if err != nil {
log.Error().
Caller().
Err(err).
Msg("Failed to write response")
}

return
}
defer websocketConn.Close(websocket.StatusInternalError, "closing")
if websocketConn.Subprotocol() != "derp" {
websocketConn.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
return
}

wc := wsconn.NetConn(req.Context(), websocketConn, websocket.MessageBinary, req.RemoteAddr)
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
d.tailscaleDERP.Accept(req.Context(), wc, brw, req.RemoteAddr)
}

func (d *DERPServer) servePlain(writer http.ResponseWriter, req *http.Request) {
fastStart := req.Header.Get(fastStartHeader) == "1"

hijacker, ok := writer.(http.Hijacker)
Expand Down
2 changes: 1 addition & 1 deletion hscontrol/types/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type User struct {
Name string `gorm:"unique"`
}

// TODO(kradalby): See if we can fill in Gravatar here
// TODO(kradalby): See if we can fill in Gravatar here.
func (u *User) profilePicURL() string {
return ""
}
Expand Down
1 change: 0 additions & 1 deletion hscontrol/util/net.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) {
return d.DialContext(ctx, "unix", addr)
}


// TODO(kradalby): Remove after go 1.24, will be in stdlib.
// Compare returns an integer comparing two prefixes.
// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2.
Expand Down
1 change: 0 additions & 1 deletion integration/dns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,5 +242,4 @@ func TestValidateResolvConf(t *testing.T) {
}
})
}

}
41 changes: 27 additions & 14 deletions integration/dockertestutil/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dockertestutil
import (
"bytes"
"context"
"io"
"log"
"os"
"path"
Expand All @@ -13,25 +14,18 @@ import (

const filePerm = 0o644

func SaveLog(
func WriteLog(
pool *dockertest.Pool,
resource *dockertest.Resource,
basePath string,
) (string, string, error) {
err := os.MkdirAll(basePath, os.ModePerm)
if err != nil {
return "", "", err
}

var stdout bytes.Buffer
var stderr bytes.Buffer

err = pool.Client.Logs(
stdout io.Writer,
stderr io.Writer,
) error {
return pool.Client.Logs(
docker.LogsOptions{
Context: context.TODO(),
Container: resource.Container.ID,
OutputStream: &stdout,
ErrorStream: &stderr,
OutputStream: stdout,
ErrorStream: stderr,
Tail: "all",
RawTerminal: false,
Stdout: true,
Expand All @@ -40,6 +34,25 @@ func SaveLog(
Timestamps: false,
},
)
}

func SaveLog(
pool *dockertest.Pool,
resource *dockertest.Resource,
basePath string,
) (string, string, error) {
err := os.MkdirAll(basePath, os.ModePerm)
if err != nil {
return "", "", err
}

// Wouldn't it be simpler to
// open and wrap the destination files in a
// bufio.Writer, and pass those in docker.LogsOptions?
var stdout bytes.Buffer
var stderr bytes.Buffer
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially, not tested or was aware of it, happy for that to be changed, maybe you can test in a followup pr?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't tested it either, I just thought maybe doing the equivalent of > output.file is simpler than doing an extra lap around the same thing. I don't expect any significant improvements or breakages from it either, other than the code becoming shorter. I don't mind doing an extra PR, but should it include only this spot, or all other file operations that may involve redundant io.Writers?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have time, all would of course be useful. Since this is in the integration tests, its not really that critical, but always nice to have it neater.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to look for other manually buffered disk writes with git grep, but as far as I can see, this was the only spot where it happened.

I can submit an MR with the changes, but... the code did not become much shorter. The tally comes to -5 lines, three of which were the comment I added. I could still see some value in it, because unlike bytes.Buffer, which (at least if I remember correctly) by default extends however many times it needs to fit things into memory, bufio.Writer has a defined buffer size and automatically flushes when it is about to cross it. So, in theory it could make it so that longer logs can be dumped without issue. I'm still waiting for the integration tests to run on github, but I do not expect problems with it.

(My machine does not seem to like the idea of running the entire test suite at once too much - best I could do was 34 to go before it timed out, and I think I might have raised the timeout before reaching that result).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaaand it actually failed ina few tests - must have forgot about a flag.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair, this isnt really critical, if it isnt much simpler or neater, it can be dropped. I'm happy either way as it currently does work, good suggestion tho. You can submit it if it seem to work, and we can take if from there but you dont have to spend that much time on it.

Copy link
Contributor Author

@enoperm enoperm Sep 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the comment for now. I may take another look at it, but at this point I would not really mind leaving it as it is either.


err = WriteLog(pool, resource, &stdout, &stderr)
if err != nil {
return "", "", err
}
Expand Down
112 changes: 99 additions & 13 deletions integration/embedded_derp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,72 @@ import (
"github.com/ory/dockertest/v3"
)

type ClientsSpec struct {
Plain int
WebsocketDERP int
}

type EmbeddedDERPServerScenario struct {
*Scenario

tsicNetworks map[string]*dockertest.Network
}

func TestDERPServerScenario(t *testing.T) {
spec := map[string]ClientsSpec{
"user1": {
Plain: len(MustTestVersions),
WebsocketDERP: 0,
},
}

DERPServerScenario(t, spec, func(scenario *EmbeddedDERPServerScenario) {
allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)
t.Logf("checking %d clients for websocket connections", len(allClients))

for _, client := range allClients {
if didClientUseWebsocketForDERP(t, client) {
t.Logf(
"client %q used websocket a connection, but was not expected to",
client.Hostname(),
)
t.Fail()
}
}
})
}

func TestDERPServerWebsocketScenario(t *testing.T) {
spec := map[string]ClientsSpec{
"user1": {
Plain: 0,
WebsocketDERP: len(MustTestVersions),
},
}

DERPServerScenario(t, spec, func(scenario *EmbeddedDERPServerScenario) {
allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)
t.Logf("checking %d clients for websocket connections", len(allClients))

for _, client := range allClients {
if !didClientUseWebsocketForDERP(t, client) {
t.Logf(
"client %q does not seem to have used a websocket connection, even though it was expected to do so",
client.Hostname(),
)
t.Fail()
}
}
})
}

func DERPServerScenario(
t *testing.T,
spec map[string]ClientsSpec,
furtherAssertions ...func(*EmbeddedDERPServerScenario),
) {
IntegrationSkip(t)
// t.Parallel()

Expand All @@ -34,20 +93,18 @@ func TestDERPServerScenario(t *testing.T) {
}
defer scenario.Shutdown()

spec := map[string]int{
"user1": len(MustTestVersions),
}

err = scenario.CreateHeadscaleEnv(
spec,
hsic.WithTestName("derpserver"),
hsic.WithExtraPorts([]string{"3478/udp"}),
hsic.WithEmbeddedDERPServerOnly(),
hsic.WithPort(443),
hsic.WithTLS(),
hsic.WithHostnameAsServerURL(),
hsic.WithConfigEnv(map[string]string{
"HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "true",
"HEADSCALE_DERP_UPDATE_FREQUENCY": "10s",
"HEADSCALE_LISTEN_ADDR": "0.0.0.0:443",
}),
)
assertNoErrHeadscaleEnv(t, err)
Expand Down Expand Up @@ -76,6 +133,10 @@ func TestDERPServerScenario(t *testing.T) {
}

success := pingDerpAllHelper(t, allClients, allHostnames)
if len(allHostnames)*len(allClients) > success {
t.FailNow()
return
}

for _, client := range allClients {
status, err := client.Status()
Expand All @@ -98,6 +159,9 @@ func TestDERPServerScenario(t *testing.T) {
time.Sleep(30 * time.Second)

success = pingDerpAllHelper(t, allClients, allHostnames)
if len(allHostnames)*len(allClients) > success {
t.Fail()
}

for _, client := range allClients {
status, err := client.Status()
Expand All @@ -114,10 +178,14 @@ func TestDERPServerScenario(t *testing.T) {
}

t.Logf("Run2: %d successful pings out of %d", success, len(allClients)*len(allHostnames))

for _, check := range furtherAssertions {
check(&scenario)
}
}

func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv(
users map[string]int,
users map[string]ClientsSpec,
opts ...hsic.Option,
) error {
hsServer, err := s.Headscale(opts...)
Expand All @@ -137,6 +205,7 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv(
if err != nil {
return err
}
log.Printf("headscale server ip address: %s", hsServer.GetIP())

hash, err := util.GenerateRandomStringDNSSafe(scenarioHashLength)
if err != nil {
Expand All @@ -149,14 +218,31 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv(
return err
}

err = s.CreateTailscaleIsolatedNodesInUser(
hash,
userName,
"all",
clientCount,
)
if err != nil {
return err
if clientCount.Plain > 0 {
// Containers that use default DERP config
err = s.CreateTailscaleIsolatedNodesInUser(
hash,
userName,
"all",
clientCount.Plain,
)
if err != nil {
return err
}
}

if clientCount.WebsocketDERP > 0 {
// Containers that use DERP-over-WebSocket
err = s.CreateTailscaleIsolatedNodesInUser(
hash,
userName,
"all",
clientCount.WebsocketDERP,
tsic.WithWebsocketDERP(true),
)
if err != nil {
return err
}
}

key, err := s.CreatePreAuthKey(userName, true, false)
Expand Down
Loading