Compare commits

...

4 commits

Author SHA1 Message Date
Kristoffer Dalby
8b6812e1ec
Merge 98a65c76d3 into a7874af3d0 2024-11-16 18:11:09 -05:00
nblock
a7874af3d0
Use discord server invite link (#2235)
Some checks failed
Build / build (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Build documentation / deploy (push) Has been cancelled
Tests / test (push) Has been cancelled
Replace channel links with links to discord invite link and remove
channel list.

Fixes: #1521
2024-11-16 07:06:15 +01:00
Kristoffer Dalby
98a65c76d3
flesh out tests
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-13 14:23:41 +01:00
Kristoffer Dalby
1ec99c55e4
add test to reproduce #2129
Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
2024-09-13 07:22:24 +02:00
11 changed files with 299 additions and 46 deletions

View file

@ -60,6 +60,7 @@ jobs:
- TestEnableDisableAutoApprovedRoute - TestEnableDisableAutoApprovedRoute
- TestAutoApprovedSubRoute2068 - TestAutoApprovedSubRoute2068
- TestSubnetRouteACL - TestSubnetRouteACL
- TestHASubnetRouterFailoverWhenNodeDisconnects2129
- TestHeadscale - TestHeadscale
- TestCreateTailscale - TestCreateTailscale
- TestTailscaleNodesJoiningHeadcale - TestTailscaleNodesJoiningHeadcale

View file

@ -62,7 +62,7 @@ event.
Instances of abusive, harassing, or otherwise unacceptable behavior Instances of abusive, harassing, or otherwise unacceptable behavior
may be reported to the community leaders responsible for enforcement may be reported to the community leaders responsible for enforcement
at our Discord channel. All complaints on our [Discord server](https://discord.gg/c84AZQhmpx). All complaints
will be reviewed and investigated promptly and fairly. will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and All community leaders are obligated to respect the privacy and

View file

@ -4,7 +4,7 @@
An open source, self-hosted implementation of the Tailscale control server. An open source, self-hosted implementation of the Tailscale control server.
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat. Join our [Discord server](https://discord.gg/c84AZQhmpx) for a chat.
**Note:** Always select the same GitHub tag as the released version you use **Note:** Always select the same GitHub tag as the released version you use
to ensure you have the correct example configuration and documentation. to ensure you have the correct example configuration and documentation.

View file

@ -41,13 +41,15 @@ In addition to that, you may use packages provided by the community or from dist
[installation guide using community packages](../setup/install/community.md). [installation guide using community packages](../setup/install/community.md).
For convenience, we also [build Docker images with headscale](../setup/install/container.md). But **please be aware that For convenience, we also [build Docker images with headscale](../setup/install/container.md). But **please be aware that
we don't officially support deploying headscale using Docker**. We have a [Discord we don't officially support deploying headscale using Docker**. On our [Discord server](https://discord.gg/c84AZQhmpx)
channel](https://discord.com/channels/896711691637780480/1070619770942148618) where you can ask for Docker-specific help we have a "docker-issues" channel where you can ask for Docker-specific help to the community.
to the community.
## Why is my reverse proxy not working with headscale? ## Why is my reverse proxy not working with headscale?
We don't know. We don't use reverse proxies with headscale ourselves, so we don't have any experience with them. We have [community documentation](../ref/integration/reverse-proxy.md) on how to configure various reverse proxies, and a dedicated [Discord channel](https://discord.com/channels/896711691637780480/1070619818346164324) where you can ask for help to the community. We don't know. We don't use reverse proxies with headscale ourselves, so we don't have any experience with them. We have
[community documentation](../ref/integration/reverse-proxy.md) on how to configure various reverse proxies, and a
dedicated "reverse-proxy-issues" channel on our [Discord server](https://discord.gg/c84AZQhmpx) where you can ask for
help to the community.
## Can I use headscale and tailscale on the same machine? ## Can I use headscale and tailscale on the same machine?

View file

@ -1,11 +1,5 @@
# Getting help # Getting help
Join our Discord server for announcements and community support: Join our [Discord server](https://discord.gg/c84AZQhmpx) for announcements and community support.
- [announcements](https://discord.com/channels/896711691637780480/896711692120129538)
- [general](https://discord.com/channels/896711691637780480/896711692120129540)
- [docker-issues](https://discord.com/channels/896711691637780480/1070619770942148618)
- [reverse-proxy-issues](https://discord.com/channels/896711691637780480/1070619818346164324)
- [web-interfaces](https://discord.com/channels/896711691637780480/1105842846386356294)
Please report bugs via [GitHub issues](https://github.com/juanfont/headscale/issues) Please report bugs via [GitHub issues](https://github.com/juanfont/headscale/issues)

View file

@ -6,5 +6,4 @@ code archives. Container images are available on [Docker Hub](https://hub.docker
An Atom/RSS feed of headscale releases is available [here](https://github.com/juanfont/headscale/releases.atom). An Atom/RSS feed of headscale releases is available [here](https://github.com/juanfont/headscale/releases.atom).
Join the ["announcements" channel on Discord](https://discord.com/channels/896711691637780480/896711692120129538) for See the "announcements" channel on our [Discord server](https://discord.gg/c84AZQhmpx) for news about headscale.
news about headscale.

View file

@ -10,7 +10,7 @@ Headscale is an open source, self-hosted implementation of the Tailscale control
This page contains the documentation for the latest version of headscale. Please also check our [FAQ](./about/faq.md). This page contains the documentation for the latest version of headscale. Please also check our [FAQ](./about/faq.md).
Join our [Discord](https://discord.gg/c84AZQhmpx) server for a chat and community support. Join our [Discord server](https://discord.gg/c84AZQhmpx) for a chat and community support.
## Design goal ## Design goal

View file

@ -16,4 +16,4 @@ Headscale doesn't provide a built-in web interface but users may pick one from t
| headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for headscale | | headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for headscale |
| ouroboros | [Github](https://github.com/yellowsink/ouroboros) | Ouroboros is designed for users to manage their own devices, rather than for admins | | ouroboros | [Github](https://github.com/yellowsink/ouroboros) | Ouroboros is designed for users to manage their own devices, rather than for admins |
You can ask for support on our dedicated [Discord channel](https://discord.com/channels/896711691637780480/1105842846386356294). You can ask for support on our [Discord server](https://discord.gg/c84AZQhmpx) in the "web-interfaces" channel.

View file

@ -191,6 +191,7 @@ func (m *mapSession) serve() {
// //
//nolint:gocyclo //nolint:gocyclo
func (m *mapSession) serveLongPoll() { func (m *mapSession) serveLongPoll() {
start := time.Now()
m.beforeServeLongPoll() m.beforeServeLongPoll()
// Clean up the session when the client disconnects // Clean up the session when the client disconnects
@ -220,16 +221,6 @@ func (m *mapSession) serveLongPoll() {
m.pollFailoverRoutes("node connected", m.node) m.pollFailoverRoutes("node connected", m.node)
// Upgrade the writer to a ResponseController
rc := http.NewResponseController(m.w)
// Longpolling will break if there is a write timeout,
// so it needs to be disabled.
rc.SetWriteDeadline(time.Time{})
ctx, cancel := context.WithCancel(context.WithValue(m.ctx, nodeNameContextKey, m.node.Hostname))
defer cancel()
m.keepAliveTicker = time.NewTicker(m.keepAlive) m.keepAliveTicker = time.NewTicker(m.keepAlive)
m.h.nodeNotifier.AddNode(m.node.ID, m.ch) m.h.nodeNotifier.AddNode(m.node.ID, m.ch)
@ -243,12 +234,12 @@ func (m *mapSession) serveLongPoll() {
// consume channels with update, keep alives or "batch" blocking signals // consume channels with update, keep alives or "batch" blocking signals
select { select {
case <-m.cancelCh: case <-m.cancelCh:
m.tracef("poll cancelled received") m.tracef("poll cancelled received (%s)", time.Since(start).String())
mapResponseEnded.WithLabelValues("cancelled").Inc() mapResponseEnded.WithLabelValues("cancelled").Inc()
return return
case <-ctx.Done(): case <-m.ctx.Done():
m.tracef("poll context done") m.tracef("poll context done (%s): %s", time.Since(start).String(), m.ctx.Err().Error())
mapResponseEnded.WithLabelValues("done").Inc() mapResponseEnded.WithLabelValues("done").Inc()
return return
@ -339,14 +330,7 @@ func (m *mapSession) serveLongPoll() {
m.errf(err, "could not write the map response(%s), for mapSession: %p", update.Type.String(), m) m.errf(err, "could not write the map response(%s), for mapSession: %p", update.Type.String(), m)
return return
} }
m.w.(http.Flusher).Flush()
err = rc.Flush()
if err != nil {
mapResponseSent.WithLabelValues("error", updateType).Inc()
m.errf(err, "flushing the map response to client, for mapSession: %p", m)
return
}
log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", m.node.MachineKey.String()).Msg("finished writing mapresp to node") log.Trace().Str("node", m.node.Hostname).TimeDiff("timeSpent", time.Now(), startWrite).Str("mkey", m.node.MachineKey.String()).Msg("finished writing mapresp to node")
if debugHighCardinalityMetrics { if debugHighCardinalityMetrics {
@ -370,12 +354,7 @@ func (m *mapSession) serveLongPoll() {
mapResponseSent.WithLabelValues("error", "keepalive").Inc() mapResponseSent.WithLabelValues("error", "keepalive").Inc()
return return
} }
err = rc.Flush() m.w.(http.Flusher).Flush()
if err != nil {
m.errf(err, "flushing keep alive to client, for mapSession: %p", m)
mapResponseSent.WithLabelValues("error", "keepalive").Inc()
return
}
if debugHighCardinalityMetrics { if debugHighCardinalityMetrics {
mapResponseLastSentSeconds.WithLabelValues("keepalive", m.node.ID.String()).Set(float64(time.Now().Unix())) mapResponseLastSentSeconds.WithLabelValues("keepalive", m.node.ID.String()).Set(float64(time.Now().Unix()))

View file

@ -13,6 +13,7 @@ import (
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
v1 "github.com/juanfont/headscale/gen/go/headscale/v1" v1 "github.com/juanfont/headscale/gen/go/headscale/v1"
"github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/policy"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/hscontrol/util"
"github.com/juanfont/headscale/integration/hsic" "github.com/juanfont/headscale/integration/hsic"
"github.com/juanfont/headscale/integration/tsic" "github.com/juanfont/headscale/integration/tsic"
@ -1316,3 +1317,252 @@ func TestSubnetRouteACL(t *testing.T) {
t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff) t.Errorf("Subnet (%s) filter, unexpected result (-want +got):\n%s", subRouter1.Hostname(), diff)
} }
} }
func TestHASubnetRouterFailoverWhenNodeDisconnects2129(t *testing.T) {
IntegrationSkip(t)
t.Parallel()
user := "enable-routing"
scenario, err := NewScenario(dockertestMaxWait())
assertNoErrf(t, "failed to create scenario: %s", err)
// defer scenario.ShutdownAssertNoPanics(t)
spec := map[string]int{
user: 3,
}
err = scenario.CreateHeadscaleEnv(spec,
[]tsic.Option{},
hsic.WithTestName("clientdisc"),
hsic.WithEmbeddedDERPServerOnly(),
hsic.WithTLS(),
hsic.WithHostnameAsServerURL(),
hsic.WithIPAllocationStrategy(types.IPAllocationStrategyRandom),
)
assertNoErrHeadscaleEnv(t, err)
allClients, err := scenario.ListTailscaleClients()
assertNoErrListClients(t, err)
err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)
headscale, err := scenario.Headscale()
assertNoErrGetHeadscale(t, err)
expectedRoutes := map[string]string{
"1": "10.0.0.0/24",
"2": "10.0.0.0/24",
}
// Sort nodes by ID
sort.SliceStable(allClients, func(i, j int) bool {
statusI, err := allClients[i].Status()
if err != nil {
return false
}
statusJ, err := allClients[j].Status()
if err != nil {
return false
}
return statusI.Self.ID < statusJ.Self.ID
})
subRouter1 := allClients[0]
subRouter2 := allClients[1]
t.Logf("Advertise route from r1 (%s) and r2 (%s), making it HA, n1 is primary", subRouter1.Hostname(), subRouter2.Hostname())
// advertise HA route on node 1 and 2
// ID 1 will be primary
// ID 2 will be secondary
for _, client := range allClients[:2] {
status, err := client.Status()
assertNoErr(t, err)
if route, ok := expectedRoutes[string(status.Self.ID)]; ok {
command := []string{
"tailscale",
"set",
"--advertise-routes=" + route,
}
_, _, err = client.Execute(command)
assertNoErrf(t, "failed to advertise route: %s", err)
} else {
t.Fatalf("failed to find route for Node %s (id: %s)", status.Self.HostName, status.Self.ID)
}
}
err = scenario.WaitForTailscaleSync()
assertNoErrSync(t, err)
var routes []*v1.Route
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"routes",
"list",
"--output",
"json",
},
&routes,
)
assertNoErr(t, err)
assert.Len(t, routes, 2)
t.Logf("initial routes %#v", routes)
for _, route := range routes {
assert.Equal(t, true, route.GetAdvertised())
assert.Equal(t, false, route.GetEnabled())
assert.Equal(t, false, route.GetIsPrimary())
}
// Verify that no routes has been sent to the client,
// they are not yet enabled.
for _, client := range allClients {
status, err := client.Status()
assertNoErr(t, err)
for _, peerKey := range status.Peers() {
peerStatus := status.Peer[peerKey]
assert.Nil(t, peerStatus.PrimaryRoutes)
}
}
// Enable all routes
for _, route := range routes {
_, err = headscale.Execute(
[]string{
"headscale",
"routes",
"enable",
"--route",
strconv.Itoa(int(route.GetId())),
})
assertNoErr(t, err)
time.Sleep(time.Second)
}
var enablingRoutes []*v1.Route
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"routes",
"list",
"--output",
"json",
},
&enablingRoutes,
)
assertNoErr(t, err)
assert.Len(t, enablingRoutes, 2)
// Node 1 is primary
assert.Equal(t, true, enablingRoutes[0].GetAdvertised())
assert.Equal(t, true, enablingRoutes[0].GetEnabled())
assert.Equal(t, true, enablingRoutes[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be primary")
// Node 2 is not primary
assert.Equal(t, true, enablingRoutes[1].GetAdvertised())
assert.Equal(t, true, enablingRoutes[1].GetEnabled())
assert.Equal(t, false, enablingRoutes[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be non-primary")
var nodeList []v1.Node
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
&nodeList,
)
assert.Nil(t, err)
assert.Len(t, nodeList, 3)
assert.True(t, nodeList[0].Online)
assert.True(t, nodeList[1].Online)
assert.True(t, nodeList[2].Online)
// Kill off one of the docker containers to simulate a disconnect
err = scenario.DisconnectContainersFromScenario(subRouter1.Hostname())
assertNoErr(t, err)
time.Sleep(5 * time.Second)
var nodeListAfterDisconnect []v1.Node
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"nodes",
"list",
"--output",
"json",
},
&nodeListAfterDisconnect,
)
assert.Nil(t, err)
assert.Len(t, nodeListAfterDisconnect, 3)
assert.False(t, nodeListAfterDisconnect[0].Online)
assert.True(t, nodeListAfterDisconnect[1].Online)
assert.True(t, nodeListAfterDisconnect[2].Online)
var routesAfterDisconnect []*v1.Route
err = executeAndUnmarshal(
headscale,
[]string{
"headscale",
"routes",
"list",
"--output",
"json",
},
&routesAfterDisconnect,
)
assertNoErr(t, err)
assert.Len(t, routesAfterDisconnect, 2)
// Node 1 is primary
assert.Equal(t, true, routesAfterDisconnect[0].GetAdvertised())
assert.Equal(t, true, routesAfterDisconnect[0].GetEnabled())
assert.Equal(t, false, routesAfterDisconnect[0].GetIsPrimary(), "both subnet routers are up, expected r1 to be non-primary")
// Node 2 is not primary
assert.Equal(t, true, routesAfterDisconnect[1].GetAdvertised())
assert.Equal(t, true, routesAfterDisconnect[1].GetEnabled())
assert.Equal(t, true, routesAfterDisconnect[1].GetIsPrimary(), "both subnet routers are up, expected r2 to be primary")
// // Ensure the node can reconncet as expected
// err = scenario.ConnectContainersToScenario(subRouter1.Hostname())
// assertNoErr(t, err)
// time.Sleep(5 * time.Second)
// var nodeListAfterReconnect []v1.Node
// err = executeAndUnmarshal(
// headscale,
// []string{
// "headscale",
// "nodes",
// "list",
// "--output",
// "json",
// },
// &nodeListAfterReconnect,
// )
// assert.Nil(t, err)
// assert.Len(t, nodeListAfterReconnect, 3)
// assert.True(t, nodeListAfterReconnect[0].Online)
// assert.True(t, nodeListAfterReconnect[1].Online)
// assert.True(t, nodeListAfterReconnect[2].Online)
}

View file

@ -651,3 +651,31 @@ func (s *Scenario) WaitForTailscaleLogout() error {
return nil return nil
} }
// DisconnectContainersFromScenario disconnects a list of containers from the network.
func (s *Scenario) DisconnectContainersFromScenario(containers ...string) error {
for _, container := range containers {
if ctr, ok := s.pool.ContainerByName(container); ok {
err := ctr.DisconnectFromNetwork(s.network)
if err != nil {
return err
}
}
}
return nil
}
// ConnectContainersToScenario disconnects a list of containers from the network.
func (s *Scenario) ConnectContainersToScenario(containers ...string) error {
for _, container := range containers {
if ctr, ok := s.pool.ContainerByName(container); ok {
err := ctr.ConnectToNetwork(s.network)
if err != nil {
return err
}
}
}
return nil
}