Skip to content

Commit

Permalink
Add support for Tailscale SSH (tailscale#22)
Browse files Browse the repository at this point in the history
This commit adds the `ssh` section to the ACL type, allowing users of
the package to specify their SSH configuration for the Tailscale SSH
feature:

https://tailscale.com/blog/tailscale-ssh/

Signed-off-by: David Bond <davidsbond93@gmail.com>
  • Loading branch information
davidsbond committed Jun 22, 2022
1 parent 7d2065c commit c5c40c9
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 60 deletions.
160 changes: 101 additions & 59 deletions tailscale/client.go
Expand Up @@ -223,64 +223,75 @@ func (c *Client) DNSNameservers(ctx context.Context) ([]string, error) {
return resp["dns"], nil
}

type ACL struct {
ACLs []ACLEntry `json:"acls" hujson:"ACLs,omitempty"`
AutoApprovers *ACLAutoApprovers `json:"autoapprovers,omitempty" hujson:"AutoApprovers,omitempty"`
Groups map[string][]string `json:"groups,omitempty" hujson:"Groups,omitempty"`
Hosts map[string]string `json:"hosts,omitempty" hujson:"Hosts,omitempty"`
TagOwners map[string][]string `json:"tagowners,omitempty" hujson:"TagOwners,omitempty"`
DERPMap *ACLDERPMap `json:"derpMap,omitempty" hujson:"DerpMap,omitempty"`
Tests []ACLTest `json:"tests,omitempty" hujson:"Tests,omitempty"`
}

type ACLAutoApprovers struct {
Routes map[string][]string `json:"routes" hujson:"Routes"`
ExitNode []string `json:"exitNode" hujson:"ExitNode"`
}

type ACLEntry struct {
Action string `json:"action" hujson:"Action"`
Ports []string `json:"ports" hujson:"Ports"`
Users []string `json:"users" hujson:"Users"`
Source []string `json:"src" hujson:"Src"`
Destination []string `json:"dst" hujson:"Dst"`
Protocol string `json:"proto" hujson:"Proto"`
}

type ACLTest struct {
User string `json:"user" hujson:"User"`
Allow []string `json:"allow" hujson:"Allow"`
Deny []string `json:"deny" hujson:"Deny"`
Source string `json:"src" hujson:"Src"`
Accept []string `json:"accept" hujson:"Accept"`
}

type ACLDERPMap struct {
Regions map[int]*ACLDERPRegion `json:"regions" hujson:"Regions"`
OmitDefaultRegions bool `json:"omitDefaultRegions,omitempty" hujson:"OmitDefaultRegions,omitempty"`
}

type ACLDERPRegion struct {
RegionID int `json:"regionID" hujson:"RegionID"`
RegionCode string `json:"regionCode" hujson:"RegionCode"`
RegionName string `json:"regionName" hujson:"RegionName"`
Avoid bool `json:"avoid,omitempty" hujson:"Avoid,omitempty"`
Nodes []*ACLDERPNode `json:"nodes" hujson:"Nodes"`
}

type ACLDERPNode struct {
Name string `json:"name" hujson:"Name"`
RegionID int `json:"regionID" hujson:"RegionID"`
HostName string `json:"hostName" hujson:"HostName"`
CertName string `json:"certName,omitempty" hujson:"CertName,omitempty"`
IPv4 string `json:"ipv4,omitempty" hujson:"IPv4,omitempty"`
IPv6 string `json:"ipv6,omitempty" hujson:"IPv6,omitempty"`
STUNPort int `json:"stunPort,omitempty" hujson:"STUNPort,omitempty"`
STUNOnly bool `json:"stunOnly,omitempty" hujson:"STUNOnly,omitempty"`
DERPPort int `json:"derpPort,omitempty" hujson:"DERPPort,omitempty"`
InsecureForTests bool `json:"insecureForRests,omitempty" hujson:"InsecureForTests,omitempty"`
STUNTestIP string `json:"stunTestIP,omitempty" hujson:"STUNTestIP,omitempty"`
}
type (
ACL struct {
ACLs []ACLEntry `json:"acls" hujson:"ACLs,omitempty"`
AutoApprovers *ACLAutoApprovers `json:"autoapprovers,omitempty" hujson:"AutoApprovers,omitempty"`
Groups map[string][]string `json:"groups,omitempty" hujson:"Groups,omitempty"`
Hosts map[string]string `json:"hosts,omitempty" hujson:"Hosts,omitempty"`
TagOwners map[string][]string `json:"tagowners,omitempty" hujson:"TagOwners,omitempty"`
DERPMap *ACLDERPMap `json:"derpMap,omitempty" hujson:"DerpMap,omitempty"`
Tests []ACLTest `json:"tests,omitempty" hujson:"Tests,omitempty"`
SSH []ACLSSH `json:"ssh,omitempty" hujson:"SSH,omitempty"`
}

ACLAutoApprovers struct {
Routes map[string][]string `json:"routes" hujson:"Routes"`
ExitNode []string `json:"exitNode" hujson:"ExitNode"`
}

ACLEntry struct {
Action string `json:"action" hujson:"Action"`
Ports []string `json:"ports" hujson:"Ports"`
Users []string `json:"users" hujson:"Users"`
Source []string `json:"src" hujson:"Src"`
Destination []string `json:"dst" hujson:"Dst"`
Protocol string `json:"proto" hujson:"Proto"`
}

ACLTest struct {
User string `json:"user" hujson:"User"`
Allow []string `json:"allow" hujson:"Allow"`
Deny []string `json:"deny" hujson:"Deny"`
Source string `json:"src" hujson:"Src"`
Accept []string `json:"accept" hujson:"Accept"`
}

ACLDERPMap struct {
Regions map[int]*ACLDERPRegion `json:"regions" hujson:"Regions"`
OmitDefaultRegions bool `json:"omitDefaultRegions,omitempty" hujson:"OmitDefaultRegions,omitempty"`
}

ACLDERPRegion struct {
RegionID int `json:"regionID" hujson:"RegionID"`
RegionCode string `json:"regionCode" hujson:"RegionCode"`
RegionName string `json:"regionName" hujson:"RegionName"`
Avoid bool `json:"avoid,omitempty" hujson:"Avoid,omitempty"`
Nodes []*ACLDERPNode `json:"nodes" hujson:"Nodes"`
}

ACLDERPNode struct {
Name string `json:"name" hujson:"Name"`
RegionID int `json:"regionID" hujson:"RegionID"`
HostName string `json:"hostName" hujson:"HostName"`
CertName string `json:"certName,omitempty" hujson:"CertName,omitempty"`
IPv4 string `json:"ipv4,omitempty" hujson:"IPv4,omitempty"`
IPv6 string `json:"ipv6,omitempty" hujson:"IPv6,omitempty"`
STUNPort int `json:"stunPort,omitempty" hujson:"STUNPort,omitempty"`
STUNOnly bool `json:"stunOnly,omitempty" hujson:"STUNOnly,omitempty"`
DERPPort int `json:"derpPort,omitempty" hujson:"DERPPort,omitempty"`
InsecureForTests bool `json:"insecureForRests,omitempty" hujson:"InsecureForTests,omitempty"`
STUNTestIP string `json:"stunTestIP,omitempty" hujson:"STUNTestIP,omitempty"`
}

ACLSSH struct {
Action string `json:"action" hujson:"Action"`
Users []string `json:"users" hujson:"Users"`
Source []string `json:"src" hujson:"Src"`
Destination []string `json:"dst" hujson:"Dst"`
CheckPeriod Duration `json:"checkPeriod" hujson:"CheckPeriod"`
}
)

// ACL retrieves the ACL that is currently set for the given tailnet.
func (c *Client) ACL(ctx context.Context) (*ACL, error) {
Expand Down Expand Up @@ -398,7 +409,7 @@ func (t Time) MarshalJSON() ([]byte, error) {
return json.Marshal(t.Time)
}

// MarshalJSON unmarshals the content of data as a time.Duration, a blank string will keep the time at its zero value.
// UnmarshalJSON unmarshals the content of data as a time.Time, a blank string will keep the time at its zero value.
func (t *Time) UnmarshalJSON(data []byte) error {
if string(data) == `""` {
return nil
Expand Down Expand Up @@ -593,3 +604,34 @@ func ErrorData(err error) []APIErrorData {

return nil
}

// The Duration type wraps a time.Duration, allowing it to be JSON marshalled as a string like "20h" rather than
// a numeric value.
type Duration struct {
time.Duration
}

// MarshalJSON is an implementation of json.Marshal.
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Duration.String())
}

// UnmarshalJSON unmarshals the content of data as a time.Duration, a blank string will keep the duration at its zero value.
func (d *Duration) UnmarshalJSON(data []byte) error {
if string(data) == `""` {
return nil
}

var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}

dur, err := time.ParseDuration(str)
if err != nil {
return err
}

d.Duration = dur
return nil
}
42 changes: 42 additions & 0 deletions tailscale/client_test.go
Expand Up @@ -98,6 +98,27 @@ func TestACL_Unmarshal(t *testing.T) {
Source: "alice@example.com",
Accept: []string{"tag:dev:80"}},
},
SSH: []tailscale.ACLSSH{
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"autogroup:self"},
Users: []string{"root", "autogroup:nonroot"},
},
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"tag:prod"},
Users: []string{"root", "autogroup:nonroot"},
},
{
Action: "accept",
Source: []string{"tag:logging"},
Destination: []string{"tag:prod"},
Users: []string{"root", "autogroup:nonroot"},
CheckPeriod: tailscale.Duration{Duration: time.Hour * 20},
},
},
},
},
{
Expand Down Expand Up @@ -156,6 +177,27 @@ func TestACL_Unmarshal(t *testing.T) {
"tag:prod": {"group:devops"},
},
DERPMap: (*tailscale.ACLDERPMap)(nil),
SSH: []tailscale.ACLSSH{
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"autogroup:self"},
Users: []string{"root", "autogroup:nonroot"},
},
{
Action: "accept",
Source: []string{"autogroup:members"},
Destination: []string{"tag:prod"},
Users: []string{"root", "autogroup:nonroot"},
},
{
Action: "accept",
Source: []string{"tag:logging"},
Destination: []string{"tag:prod"},
Users: []string{"root", "autogroup:nonroot"},
CheckPeriod: tailscale.Duration{Duration: time.Hour * 20},
},
},
Tests: []tailscale.ACLTest{
{
User: "",
Expand Down
21 changes: 21 additions & 0 deletions tailscale/testdata/acl.hujson
Expand Up @@ -38,4 +38,25 @@
"deny": ["tag:prod:80"],
},
],
"ssh": [
{
"action": "accept",
"src": ["autogroup:members"],
"dst": ["autogroup:self"],
"users": ["root", "autogroup:nonroot"]
},
{
"action": "accept",
"src": ["autogroup:members"],
"dst": ["tag:prod"],
"users": ["root", "autogroup:nonroot"]
},
{
"action": "accept",
"src": ["tag:logging"],
"dst": ["tag:prod"],
"users": ["root", "autogroup:nonroot"],
"checkPeriod": "20h"
},
]
}
21 changes: 21 additions & 0 deletions tailscale/testdata/acl.json
Expand Up @@ -24,5 +24,26 @@
"accept": ["tag:dev:80"],
"deny": ["tag:prod:80"]
}
],
"ssh": [
{
"action": "accept",
"src": ["autogroup:members"],
"dst": ["autogroup:self"],
"users": ["root", "autogroup:nonroot"]
},
{
"action": "accept",
"src": ["autogroup:members"],
"dst": ["tag:prod"],
"users": ["root", "autogroup:nonroot"]
},
{
"action": "accept",
"src": ["tag:logging"],
"dst": ["tag:prod"],
"users": ["root", "autogroup:nonroot"],
"checkPeriod": "20h"
}
]
}
9 changes: 8 additions & 1 deletion tailscale/time_test.go
Expand Up @@ -4,8 +4,9 @@ import (
"testing"
"time"

"github.com/davidsbond/tailscale-client-go/tailscale"
"github.com/stretchr/testify/assert"

"github.com/davidsbond/tailscale-client-go/tailscale"
)

func TestWrapsStdTime(t *testing.T) {
Expand Down Expand Up @@ -55,3 +56,9 @@ func TestMarshalingTimestamps(t *testing.T) {
})
}
}

func TestWrapsStdDuration(t *testing.T) {
expectedDuration := tailscale.Duration{}
newDuration := time.Duration(0)
assert.Equal(t, expectedDuration.Duration, newDuration)
}

0 comments on commit c5c40c9

Please sign in to comment.