mirror of
https://github.com/juanfont/headscale.git
synced 2024-09-20 07:16:35 +08:00
Compare commits
6 commits
4d3a5b06da
...
030c9dbd6e
Author | SHA1 | Date | |
---|---|---|---|
030c9dbd6e | |||
fe68f50328 | |||
c3ef90a7f7 | |||
064c46f2a5 | |||
4d4964d1d1 | |||
b6492ad54b |
18
README.md
18
README.md
|
@ -62,15 +62,15 @@ buttons available in the repo.
|
|||
|
||||
## Client OS support
|
||||
|
||||
| OS | Supports headscale |
|
||||
| ------- | --------------------------------------------------------- |
|
||||
| Linux | Yes |
|
||||
| OpenBSD | Yes |
|
||||
| FreeBSD | Yes |
|
||||
| macOS | Yes (see `/apple` on your headscale for more information) |
|
||||
| Windows | Yes [docs](./docs/windows-client.md) |
|
||||
| Android | Yes [docs](./docs/android-client.md) |
|
||||
| iOS | Yes [docs](./docs/iOS-client.md) |
|
||||
| OS | Supports headscale |
|
||||
| ------- | -------------------------------------------------------------------------------------------------- |
|
||||
| Linux | Yes |
|
||||
| OpenBSD | Yes |
|
||||
| FreeBSD | Yes |
|
||||
| Windows | Yes (see [docs](./docs/windows-client.md) and `/windows` on your headscale for more information) |
|
||||
| Android | Yes (see [docs](./docs/android-client.md)) |
|
||||
| macOS | Yes (see [docs](./docs/apple-client.md#macos) and `/apple` on your headscale for more information) |
|
||||
| iOS | Yes (see [docs](./docs/apple-client.md#ios) and `/apple` on your headscale for more information) |
|
||||
|
||||
## Running headscale
|
||||
|
||||
|
|
51
docs/apple-client.md
Normal file
51
docs/apple-client.md
Normal file
|
@ -0,0 +1,51 @@
|
|||
# Connecting an Apple client
|
||||
|
||||
## Goal
|
||||
|
||||
This documentation has the goal of showing how a user can use the official iOS and macOS [Tailscale](https://tailscale.com) clients with `headscale`.
|
||||
|
||||
!!! info "Instructions on your headscale instance"
|
||||
|
||||
An endpoint with information on how to connect your Apple device
|
||||
is also available at `/apple` on your running instance.
|
||||
|
||||
## iOS
|
||||
|
||||
### Installation
|
||||
|
||||
Install the official Tailscale iOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037).
|
||||
|
||||
### Configuring the headscale URL
|
||||
|
||||
- Open Tailscale and make sure you are _not_ logged in to any account
|
||||
- Open Settings on the iOS device
|
||||
- Scroll down to the `third party apps` section, under `Game Center` or `TV Provider`
|
||||
- Find Tailscale and select it
|
||||
- If the iOS device was previously logged into Tailscale, switch the `Reset Keychain` toggle to `on`
|
||||
- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) under `Alternate Coordination Server URL`
|
||||
- Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option
|
||||
_(non-SSO)_. It should open up to the headscale authentication page.
|
||||
- Enter your credentials and log in. Headscale should now be working on your iOS device.
|
||||
|
||||
## macOS
|
||||
|
||||
### Installation
|
||||
|
||||
Choose one of the available [Tailscale clients for macOS](https://tailscale.com/kb/1065/macos-variants) and install it.
|
||||
|
||||
### Configuring the headscale URL
|
||||
|
||||
#### Command line
|
||||
|
||||
Use Tailscale's login command to connect with your headscale instance (e.g `https://headscale.example.com`):
|
||||
|
||||
```
|
||||
tailscale login --login-server <YOUR_HEADSCALE_URL>
|
||||
```
|
||||
|
||||
#### GUI
|
||||
|
||||
- ALT + Click the Tailscale icon in the menu and hover over the Debug menu
|
||||
- Under `Custom Login Server`, select `Add Account...`
|
||||
- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) and press `Add Account`
|
||||
- Follow the login procedure in the browser
|
|
@ -5,7 +5,7 @@
|
|||
Register the node and make it advertise itself as an exit node:
|
||||
|
||||
```console
|
||||
$ sudo tailscale up --login-server https://my-server.com --advertise-exit-node
|
||||
$ sudo tailscale up --login-server https://headscale.example.com --advertise-exit-node
|
||||
```
|
||||
|
||||
If the node is already registered, it can advertise exit capabilities like this:
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
# Connecting an iOS client
|
||||
|
||||
## Goal
|
||||
|
||||
This documentation has the goal of showing how a user can use the official iOS [Tailscale](https://tailscale.com) client with `headscale`.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the official Tailscale iOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037).
|
||||
|
||||
Ensure that the installed version is at least 1.38.1, as that is the first release to support alternate control servers.
|
||||
|
||||
## Configuring the headscale URL
|
||||
|
||||
!!! info "Apple devices"
|
||||
|
||||
An endpoint with information on how to connect your Apple devices
|
||||
(currently macOS only) is available at `/apple` on your running instance.
|
||||
|
||||
Ensure that the tailscale app is logged out before proceeding.
|
||||
|
||||
Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box.
|
||||
|
||||
> **Note**
|
||||
>
|
||||
> If the app was previously logged into tailscale, toggle on the _Reset Keychain_ switch.
|
||||
|
||||
Restart the app by closing it from the iOS app switcher, open the app and select the regular _Sign in_ option (non-SSO), and it should open up to the headscale authentication page.
|
||||
|
||||
Enter your credentials and log in. Headscale should now be working on your iOS device.
|
|
@ -4,17 +4,17 @@
|
|||
|
||||
This documentation has the goal of showing how a user can use the official Windows [Tailscale](https://tailscale.com) client with `headscale`.
|
||||
|
||||
!!! info "Instructions on your headscale instance"
|
||||
|
||||
An endpoint with information on how to connect your Windows device
|
||||
is also available at `/windows` on your running instance.
|
||||
|
||||
## Installation
|
||||
|
||||
Download the [Official Windows Client](https://tailscale.com/download/windows) and install it.
|
||||
|
||||
## Configuring the headscale URL
|
||||
|
||||
!!! info "Instructions on your headscale instance"
|
||||
|
||||
An endpoint with information on how to connect your Windows device
|
||||
is also available at `/windows` on your running instance.
|
||||
|
||||
Open a Command Prompt or Powershell and use Tailscale's login command to connect with your headscale instance (e.g
|
||||
`https://headscale.example.com`):
|
||||
|
||||
|
|
1
go.mod
1
go.mod
|
@ -79,6 +79,7 @@ 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/chasefleming/elem-go v0.28.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
|
||||
|
|
2
go.sum
2
go.sum
|
@ -90,6 +90,8 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
|
|||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chasefleming/elem-go v0.28.0 h1:7GsXU7CP3ylK7y8N/0jLpqS1JBf2r1yy30+wda3LXyk=
|
||||
github.com/chasefleming/elem-go v0.28.0/go.mod h1:hz73qILBIKnTgOujnSMtEj20/epI+f6vg71RUilJAA4=
|
||||
github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||
github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=
|
||||
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||
|
|
|
@ -66,7 +66,7 @@ func (h *Headscale) handleRegister(
|
|||
regReq tailcfg.RegisterRequest,
|
||||
machineKey key.MachinePublic,
|
||||
) {
|
||||
logInfo, logTrace, logErr := logAuthFunc(regReq, machineKey)
|
||||
logInfo, logTrace, _ := logAuthFunc(regReq, machineKey)
|
||||
now := time.Now().UTC()
|
||||
logTrace("handleRegister called, looking up machine in DB")
|
||||
node, err := h.db.GetNodeByAnyKey(machineKey, regReq.NodeKey, regReq.OldNodeKey)
|
||||
|
@ -105,16 +105,6 @@ func (h *Headscale) handleRegister(
|
|||
|
||||
logInfo("Node not found in database, creating new")
|
||||
|
||||
givenName, err := h.db.GenerateGivenName(
|
||||
machineKey,
|
||||
regReq.Hostinfo.Hostname,
|
||||
)
|
||||
if err != nil {
|
||||
logErr(err, "Failed to generate given name for node")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// The node did not have a key to authenticate, which means
|
||||
// that we rely on a method that calls back some how (OpenID or CLI)
|
||||
// We create the node and then keep it around until a callback
|
||||
|
@ -122,7 +112,6 @@ func (h *Headscale) handleRegister(
|
|||
newNode := types.Node{
|
||||
MachineKey: machineKey,
|
||||
Hostname: regReq.Hostinfo.Hostname,
|
||||
GivenName: givenName,
|
||||
NodeKey: regReq.NodeKey,
|
||||
LastSeen: &now,
|
||||
Expiry: &time.Time{},
|
||||
|
@ -354,21 +343,8 @@ func (h *Headscale) handleAuthKey(
|
|||
} else {
|
||||
now := time.Now().UTC()
|
||||
|
||||
givenName, err := h.db.GenerateGivenName(machineKey, registerRequest.Hostinfo.Hostname)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Str("func", "RegistrationHandler").
|
||||
Str("hostinfo.name", registerRequest.Hostinfo.Hostname).
|
||||
Err(err).
|
||||
Msg("Failed to generate given name for node")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
nodeToRegister := types.Node{
|
||||
Hostname: registerRequest.Hostinfo.Hostname,
|
||||
GivenName: givenName,
|
||||
UserID: pak.User.ID,
|
||||
User: pak.User,
|
||||
MachineKey: machineKey,
|
||||
|
|
|
@ -90,20 +90,6 @@ func (hsdb *HSDatabase) ListEphemeralNodes() (types.Nodes, error) {
|
|||
})
|
||||
}
|
||||
|
||||
func listNodesByGivenName(tx *gorm.DB, givenName string) (types.Nodes, error) {
|
||||
nodes := types.Nodes{}
|
||||
if err := tx.
|
||||
Preload("AuthKey").
|
||||
Preload("AuthKey.User").
|
||||
Preload("User").
|
||||
Preload("Routes").
|
||||
Where("given_name = ?", givenName).Find(&nodes).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (hsdb *HSDatabase) getNode(user string, name string) (*types.Node, error) {
|
||||
return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) {
|
||||
return getNode(rx, user, name)
|
||||
|
@ -242,9 +228,9 @@ func SetTags(
|
|||
}
|
||||
|
||||
// RenameNode takes a Node struct and a new GivenName for the nodes
|
||||
// and renames it.
|
||||
// and renames it. If the name is not unique, it will return an error.
|
||||
func RenameNode(tx *gorm.DB,
|
||||
nodeID uint64, newName string,
|
||||
nodeID types.NodeID, newName string,
|
||||
) error {
|
||||
err := util.CheckForFQDNRules(
|
||||
newName,
|
||||
|
@ -253,6 +239,15 @@ func RenameNode(tx *gorm.DB,
|
|||
return fmt.Errorf("renaming node: %w", err)
|
||||
}
|
||||
|
||||
uniq, err := isUnqiueName(tx, newName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("checking if name is unique: %w", err)
|
||||
}
|
||||
|
||||
if !uniq {
|
||||
return fmt.Errorf("name is not unique: %s", newName)
|
||||
}
|
||||
|
||||
if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("given_name", newName).Error; err != nil {
|
||||
return fmt.Errorf("failed to rename node in the database: %w", err)
|
||||
}
|
||||
|
@ -415,6 +410,15 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad
|
|||
node.IPv4 = ipv4
|
||||
node.IPv6 = ipv6
|
||||
|
||||
if node.GivenName == "" {
|
||||
givenName, err := ensureUniqueGivenName(tx, node.Hostname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure unique given name: %w", err)
|
||||
}
|
||||
|
||||
node.GivenName = givenName
|
||||
}
|
||||
|
||||
if err := tx.Save(&node).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed register(save) node in the database: %w", err)
|
||||
}
|
||||
|
@ -642,40 +646,32 @@ func generateGivenName(suppliedName string, randomSuffix bool) (string, error) {
|
|||
return normalizedHostname, nil
|
||||
}
|
||||
|
||||
func (hsdb *HSDatabase) GenerateGivenName(
|
||||
mkey key.MachinePublic,
|
||||
suppliedName string,
|
||||
) (string, error) {
|
||||
return Read(hsdb.DB, func(rx *gorm.DB) (string, error) {
|
||||
return GenerateGivenName(rx, mkey, suppliedName)
|
||||
})
|
||||
func isUnqiueName(tx *gorm.DB, name string) (bool, error) {
|
||||
nodes := types.Nodes{}
|
||||
if err := tx.
|
||||
Where("given_name = ?", name).Find(&nodes).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return len(nodes) == 0, nil
|
||||
}
|
||||
|
||||
func GenerateGivenName(
|
||||
func ensureUniqueGivenName(
|
||||
tx *gorm.DB,
|
||||
mkey key.MachinePublic,
|
||||
suppliedName string,
|
||||
name string,
|
||||
) (string, error) {
|
||||
givenName, err := generateGivenName(suppliedName, false)
|
||||
givenName, err := generateGivenName(name, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Tailscale rules (may differ) https://tailscale.com/kb/1098/machine-names/
|
||||
nodes, err := listNodesByGivenName(tx, givenName)
|
||||
unique, err := isUnqiueName(tx, givenName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var nodeFound *types.Node
|
||||
for idx, node := range nodes {
|
||||
if node.GivenName == givenName {
|
||||
nodeFound = nodes[idx]
|
||||
}
|
||||
}
|
||||
|
||||
if nodeFound != nil && nodeFound.MachineKey.String() != mkey.String() {
|
||||
postfixedName, err := generateGivenName(suppliedName, true)
|
||||
if !unique {
|
||||
postfixedName, err := generateGivenName(name, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/puzpuzpuz/xsync/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/check.v1"
|
||||
"gorm.io/gorm"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/ptr"
|
||||
|
@ -313,51 +314,6 @@ func (s *Suite) TestExpireNode(c *check.C) {
|
|||
c.Assert(nodeFromDB.IsExpired(), check.Equals, true)
|
||||
}
|
||||
|
||||
func (s *Suite) TestGenerateGivenName(c *check.C) {
|
||||
user1, err := db.CreateUser("user-1")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
pak, err := db.CreatePreAuthKey(user1.Name, false, false, nil, nil)
|
||||
c.Assert(err, check.IsNil)
|
||||
|
||||
_, err = db.getNode("user-1", "testnode")
|
||||
c.Assert(err, check.NotNil)
|
||||
|
||||
nodeKey := key.NewNode()
|
||||
machineKey := key.NewMachine()
|
||||
|
||||
machineKey2 := key.NewMachine()
|
||||
|
||||
node := &types.Node{
|
||||
ID: 0,
|
||||
MachineKey: machineKey.Public(),
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostname: "hostname-1",
|
||||
GivenName: "hostname-1",
|
||||
UserID: user1.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
AuthKeyID: ptr.To(pak.ID),
|
||||
}
|
||||
|
||||
trx := db.DB.Save(node)
|
||||
c.Assert(trx.Error, check.IsNil)
|
||||
|
||||
givenName, err := db.GenerateGivenName(machineKey2.Public(), "hostname-2")
|
||||
comment := check.Commentf("Same user, unique nodes, unique hostnames, no conflict")
|
||||
c.Assert(err, check.IsNil, comment)
|
||||
c.Assert(givenName, check.Equals, "hostname-2", comment)
|
||||
|
||||
givenName, err = db.GenerateGivenName(machineKey.Public(), "hostname-1")
|
||||
comment = check.Commentf("Same user, same node, same hostname, no conflict")
|
||||
c.Assert(err, check.IsNil, comment)
|
||||
c.Assert(givenName, check.Equals, "hostname-1", comment)
|
||||
|
||||
givenName, err = db.GenerateGivenName(machineKey2.Public(), "hostname-1")
|
||||
comment = check.Commentf("Same user, unique nodes, same hostname, conflict")
|
||||
c.Assert(err, check.IsNil, comment)
|
||||
c.Assert(givenName, check.Matches, fmt.Sprintf("^hostname-1-[a-z0-9]{%d}$", NodeGivenNameHashLength), comment)
|
||||
}
|
||||
|
||||
func (s *Suite) TestSetTags(c *check.C) {
|
||||
user, err := db.CreateUser("test")
|
||||
c.Assert(err, check.IsNil)
|
||||
|
@ -778,3 +734,100 @@ func TestListEphemeralNodes(t *testing.T) {
|
|||
assert.Equal(t, nodeEph.UserID, ephemeralNodes[0].UserID)
|
||||
assert.Equal(t, nodeEph.Hostname, ephemeralNodes[0].Hostname)
|
||||
}
|
||||
|
||||
func TestRenameNode(t *testing.T) {
|
||||
db, err := newTestDB()
|
||||
if err != nil {
|
||||
t.Fatalf("creating db: %s", err)
|
||||
}
|
||||
|
||||
user, err := db.CreateUser("test")
|
||||
assert.NoError(t, err)
|
||||
|
||||
user2, err := db.CreateUser("test2")
|
||||
assert.NoError(t, err)
|
||||
|
||||
node := types.Node{
|
||||
ID: 0,
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "test",
|
||||
UserID: user.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
}
|
||||
|
||||
node2 := types.Node{
|
||||
ID: 0,
|
||||
MachineKey: key.NewMachine().Public(),
|
||||
NodeKey: key.NewNode().Public(),
|
||||
Hostname: "test",
|
||||
UserID: user2.ID,
|
||||
RegisterMethod: util.RegisterMethodAuthKey,
|
||||
}
|
||||
|
||||
err = db.DB.Save(&node).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = db.DB.Save(&node2).Error
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = db.DB.Transaction(func(tx *gorm.DB) error {
|
||||
_, err := RegisterNode(tx, node, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = RegisterNode(tx, node2, nil, nil)
|
||||
return err
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
nodes, err := db.ListNodes()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Len(t, nodes, 2)
|
||||
|
||||
t.Logf("node1 %s %s", nodes[0].Hostname, nodes[0].GivenName)
|
||||
t.Logf("node2 %s %s", nodes[1].Hostname, nodes[1].GivenName)
|
||||
|
||||
assert.Equal(t, nodes[0].Hostname, nodes[0].GivenName)
|
||||
assert.NotEqual(t, nodes[1].Hostname, nodes[1].GivenName)
|
||||
assert.Equal(t, nodes[0].Hostname, nodes[1].Hostname)
|
||||
assert.NotEqual(t, nodes[0].Hostname, nodes[1].GivenName)
|
||||
assert.Contains(t, nodes[1].GivenName, nodes[0].Hostname)
|
||||
assert.Equal(t, nodes[0].GivenName, nodes[1].Hostname)
|
||||
assert.Len(t, nodes[0].Hostname, 4)
|
||||
assert.Len(t, nodes[1].Hostname, 4)
|
||||
assert.Len(t, nodes[0].GivenName, 4)
|
||||
assert.Len(t, nodes[1].GivenName, 13)
|
||||
|
||||
// Nodes can be renamed to a unique name
|
||||
err = db.Write(func(tx *gorm.DB) error {
|
||||
return RenameNode(tx, nodes[0].ID, "newname")
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
nodes, err = db.ListNodes()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, nodes, 2)
|
||||
assert.Equal(t, nodes[0].Hostname, "test")
|
||||
assert.Equal(t, nodes[0].GivenName, "newname")
|
||||
|
||||
// Nodes can reuse name that is no longer used
|
||||
err = db.Write(func(tx *gorm.DB) error {
|
||||
return RenameNode(tx, nodes[1].ID, "test")
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
nodes, err = db.ListNodes()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, nodes, 2)
|
||||
assert.Equal(t, nodes[0].Hostname, "test")
|
||||
assert.Equal(t, nodes[0].GivenName, "newname")
|
||||
assert.Equal(t, nodes[1].GivenName, "test")
|
||||
|
||||
// Nodes cannot be renamed to used names
|
||||
err = db.Write(func(tx *gorm.DB) error {
|
||||
return RenameNode(tx, nodes[0].ID, "test")
|
||||
})
|
||||
assert.ErrorContains(t, err, "name is not unique")
|
||||
}
|
||||
|
|
|
@ -373,7 +373,7 @@ func (api headscaleV1APIServer) RenameNode(
|
|||
node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) {
|
||||
err := db.RenameNode(
|
||||
tx,
|
||||
request.GetNodeId(),
|
||||
types.NodeID(request.GetNodeId()),
|
||||
request.GetNewName(),
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -802,18 +802,12 @@ func (api headscaleV1APIServer) DebugCreateNode(
|
|||
return nil, err
|
||||
}
|
||||
|
||||
givenName, err := api.h.db.GenerateGivenName(mkey, request.GetName())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nodeKey := key.NewNode()
|
||||
|
||||
newNode := types.Node{
|
||||
MachineKey: mkey,
|
||||
NodeKey: nodeKey.Public(),
|
||||
Hostname: request.GetName(),
|
||||
GivenName: givenName,
|
||||
User: *user,
|
||||
|
||||
Expiry: &time.Time{},
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
package hscontrol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/chasefleming/elem-go"
|
||||
"github.com/chasefleming/elem-go/attrs"
|
||||
"github.com/chasefleming/elem-go/styles"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog/log"
|
||||
"tailscale.com/tailcfg"
|
||||
|
@ -138,34 +139,37 @@ type registerWebAPITemplateConfig struct {
|
|||
Key string
|
||||
}
|
||||
|
||||
var registerWebAPITemplate = template.Must(
|
||||
template.New("registerweb").Parse(`
|
||||
<html>
|
||||
<head>
|
||||
<title>Registration - Headscale</title>
|
||||
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body {
|
||||
font-family: sans;
|
||||
}
|
||||
code {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
border: 1px solid #bbb;
|
||||
background-color: #eee;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>headscale</h1>
|
||||
<h2>Machine registration</h2>
|
||||
<p>
|
||||
Run the command below in the headscale server to add this machine to your network:
|
||||
</p>
|
||||
<code>headscale nodes register --user USERNAME --key {{.Key}}</code>
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
var codeStyleRegisterWebAPI = styles.Props{
|
||||
styles.Display: "block",
|
||||
styles.Padding: "20px",
|
||||
styles.Border: "1px solid #bbb",
|
||||
styles.BackgroundColor: "#eee",
|
||||
}
|
||||
|
||||
func registerWebAPITemplate(config registerWebAPITemplateConfig) *elem.Element {
|
||||
return elem.Html(nil,
|
||||
elem.Head(
|
||||
nil,
|
||||
elem.Title(nil, elem.Text("Registration - Headscale")),
|
||||
elem.Meta(attrs.Props{
|
||||
attrs.Name: "viewport",
|
||||
attrs.Content: "width=device-width, initial-scale=1",
|
||||
}),
|
||||
),
|
||||
elem.Body(attrs.Props{
|
||||
attrs.Style: styles.Props{
|
||||
styles.FontFamily: "sans",
|
||||
}.ToInline(),
|
||||
},
|
||||
elem.H1(nil, elem.Text("headscale")),
|
||||
elem.H2(nil, elem.Text("Machine registration")),
|
||||
elem.P(nil, elem.Text("Run the command below in the headscale server to add this machine to your network:")),
|
||||
elem.Code(attrs.Props{attrs.Style: codeStyleRegisterWebAPI.ToInline()},
|
||||
elem.Text(fmt.Sprintf("headscale nodes register --user USERNAME --key %s", config.Key)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// RegisterWebAPI shows a simple message in the browser to point to the CLI
|
||||
// Listens in /register/:nkey.
|
||||
|
@ -202,34 +206,17 @@ func (h *Headscale) RegisterWebAPI(
|
|||
return
|
||||
}
|
||||
|
||||
var content bytes.Buffer
|
||||
if err := registerWebAPITemplate.Execute(&content, registerWebAPITemplateConfig{
|
||||
var htmlContent = registerWebAPITemplate(registerWebAPITemplateConfig{
|
||||
Key: machineKey.String(),
|
||||
}); err != nil {
|
||||
log.Error().
|
||||
Str("func", "RegisterWebAPI").
|
||||
Err(err).
|
||||
Msg("Could not render register web API template")
|
||||
writer.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = writer.Write([]byte("Could not render register web API template"))
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
_, err = writer.Write(content.Bytes())
|
||||
if err != nil {
|
||||
if _, err := writer.Write([]byte(htmlContent.Render())); err != nil {
|
||||
log.Error().
|
||||
Caller().
|
||||
Err(err).
|
||||
Msg("Failed to write response")
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,17 +25,48 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<h1>headscale: iOS configuration</h1>
|
||||
<h2>GUI</h2>
|
||||
<ol>
|
||||
<li>
|
||||
Install the official Tailscale iOS client from the
|
||||
<a href="https://apps.apple.com/app/tailscale/id1470499037"
|
||||
>App store</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
Open Tailscale and make sure you are <i>not</i> logged in to any account
|
||||
</li>
|
||||
<li>Open Settings on the iOS device</li>
|
||||
<li>
|
||||
Scroll down to the "third party apps" section, under "Game Center" or
|
||||
"TV Provider"
|
||||
</li>
|
||||
<li>
|
||||
Find Tailscale and select it
|
||||
<ul>
|
||||
<li>
|
||||
If the iOS device was previously logged into Tailscale, switch the
|
||||
"Reset Keychain" toggle to "on"
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Enter "{{.URL}}" under "Alternate Coordination Server URL"</li>
|
||||
<li>
|
||||
Restart the app by closing it from the iOS app switcher, open the app
|
||||
and select the regular sign in option <i>(non-SSO)</i>. It should open
|
||||
up to the headscale authentication page.
|
||||
</li>
|
||||
<li>
|
||||
Enter your credentials and log in. Headscale should now be working on
|
||||
your iOS device
|
||||
</li>
|
||||
</ol>
|
||||
<h1>headscale: macOS configuration</h1>
|
||||
<h2>Recent Tailscale versions (1.34.0 and higher)</h2>
|
||||
<p>
|
||||
Tailscale added Fast User Switching in version 1.34 and you can now use
|
||||
the new login command to connect to one or more headscale (and Tailscale)
|
||||
servers. The previously used profiles does not have an effect anymore.
|
||||
</p>
|
||||
<h3>Command line</h3>
|
||||
<h2>Command line</h2>
|
||||
<p>Use Tailscale's login command to add your profile:</p>
|
||||
<pre><code>tailscale login --login-server {{.URL}}</code></pre>
|
||||
<h3>GUI</h3>
|
||||
<h2>GUI</h2>
|
||||
<ol>
|
||||
<li>
|
||||
ALT + Click the Tailscale icon in the menu and hover over the Debug menu
|
||||
|
@ -46,44 +77,7 @@
|
|||
</li>
|
||||
<li>Follow the login procedure in the browser</li>
|
||||
</ol>
|
||||
<h2>Apple configuration profiles (1.32.0 and lower)</h2>
|
||||
<p>
|
||||
This page provides
|
||||
<a href="https://support.apple.com/guide/mdm/mdm-overview-mdmbf9e668/web"
|
||||
>configuration profiles</a
|
||||
>
|
||||
for the official Tailscale clients for
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://apps.apple.com/app/tailscale/id1475387142"
|
||||
>macOS - AppStore Client</a
|
||||
>.
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://pkgs.tailscale.com/stable/#macos"
|
||||
>macOS - Standalone Client</a
|
||||
>.
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
The profiles will configure Tailscale.app to use <code>{{.URL}}</code> as
|
||||
its control server.
|
||||
</p>
|
||||
<h3>Caution</h3>
|
||||
<p>
|
||||
You should always download and inspect the profile before installing it:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
for app store client: <code>curl {{.URL}}/apple/macos-app-store</code>
|
||||
</li>
|
||||
<li>
|
||||
for standalone client: <code>curl {{.URL}}/apple/macos-standalone</code>
|
||||
</li>
|
||||
</ul>
|
||||
<h2>Profiles</h2>
|
||||
<h3>macOS</h3>
|
||||
<p>
|
||||
Headscale can be set to the default server by installing a Headscale
|
||||
configuration profile:
|
||||
|
@ -121,50 +115,17 @@
|
|||
</li>
|
||||
</ul>
|
||||
<p>Restart Tailscale.app and log in.</p>
|
||||
<h1>headscale: iOS configuration</h1>
|
||||
<h2>Recent Tailscale versions (1.38.1 and higher)</h2>
|
||||
<h3>Caution</h3>
|
||||
<p>
|
||||
Tailscale 1.38.1 on
|
||||
<a href="https://apps.apple.com/app/tailscale/id1470499037">iOS</a>
|
||||
added a configuration option to allow user to set an "Alternate
|
||||
Coordination server". This can be used to connect to your headscale
|
||||
server.
|
||||
You should always download and inspect the profile before installing it:
|
||||
</p>
|
||||
<h3>GUI</h3>
|
||||
<ol>
|
||||
<ul>
|
||||
<li>
|
||||
Install the official Tailscale iOS client from the
|
||||
<a href="https://apps.apple.com/app/tailscale/id1470499037"
|
||||
>App store</a
|
||||
>
|
||||
for app store client: <code>curl {{.URL}}/apple/macos-app-store</code>
|
||||
</li>
|
||||
<li>
|
||||
Open Tailscale and make sure you are <i>not</i> logged in to any account
|
||||
for standalone client: <code>curl {{.URL}}/apple/macos-standalone</code>
|
||||
</li>
|
||||
<li>Open Settings on the iOS device</li>
|
||||
<li>
|
||||
Scroll down to the "third party apps" section, under "Game Center" or
|
||||
"TV Provider"
|
||||
</li>
|
||||
<li>
|
||||
Find Tailscale and select it
|
||||
<ul>
|
||||
<li>
|
||||
If the iOS device was previously logged into Tailscale, switch the
|
||||
"Reset Keychain" toggle to "on"
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Enter "{{.URL}}" under "Alternate Coordination Server URL"</li>
|
||||
<li>
|
||||
Restart the app by closing it from the iOS app switcher, open the app
|
||||
and select the regular sign in option <i>(non-SSO)</i>. It should open
|
||||
up to the headscale authentication page.
|
||||
</li>
|
||||
<li>
|
||||
Enter your credentials and log in. Headscale should now be working on
|
||||
your iOS device
|
||||
</li>
|
||||
</ol>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
)
|
||||
|
||||
// This file is intended to "test the test framework", by proxy it will also test
|
||||
// some Headcsale/Tailscale stuff, but mostly in very simple ways.
|
||||
// some Headscale/Tailscale stuff, but mostly in very simple ways.
|
||||
|
||||
func IntegrationSkip(t *testing.T) {
|
||||
t.Helper()
|
||||
|
|
|
@ -139,5 +139,5 @@ nav:
|
|||
- Remote CLI: remote-cli.md
|
||||
- Usage:
|
||||
- Android: android-client.md
|
||||
- Apple: apple-client.md
|
||||
- Windows: windows-client.md
|
||||
- iOS: iOS-client.md
|
||||
|
|
Loading…
Reference in a new issue