diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 68db7c5d..7702d0a3 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -70,7 +70,6 @@ func NewMapper( } func (m *Mapper) tempWrap( - mapRequest tailcfg.MapRequest, machine *types.Machine, pol *policy.ACLPolicy, ) (*tailcfg.MapResponse, error) { @@ -85,7 +84,6 @@ func (m *Mapper) tempWrap( } return fullMapResponse( - mapRequest, pol, machine, peers, @@ -99,7 +97,6 @@ func (m *Mapper) tempWrap( } func fullMapResponse( - mapRequest tailcfg.MapRequest, pol *policy.ACLPolicy, machine *types.Machine, peers types.Machines, @@ -185,12 +182,6 @@ func fullMapResponse( }, } - log.Trace(). - Caller(). - Str("machine", mapRequest.Hostinfo.Hostname). - // Interface("payload", resp). - Msgf("Generated map response: %s", util.TailMapResponseToString(resp)) - return &resp, nil } @@ -292,7 +283,7 @@ func (m Mapper) CreateMapResponse( machine *types.Machine, pol *policy.ACLPolicy, ) ([]byte, error) { - mapResponse, err := m.tempWrap(mapRequest, machine, pol) + mapResponse, err := m.tempWrap(machine, pol) if err != nil { return nil, err } @@ -345,8 +336,12 @@ func (m Mapper) CreateKeepAliveResponse( return m.marshalMapResponse(keepAliveResponse, machineKey, mapRequest.Compress) } +// MarshalResponse takes an Tailscale Response, marhsal it to JSON. +// If isNoise is set, then the JSON body will be returned +// If !isNoise and privateKey2019 is set, the JSON body will be sealed in a Nacl box. func MarshalResponse( resp interface{}, + isNoise bool, privateKey2019 *key.MachinePrivate, machineKey key.MachinePublic, ) ([]byte, error) { @@ -360,7 +355,7 @@ func MarshalResponse( return nil, err } - if privateKey2019 != nil { + if !isNoise && privateKey2019 != nil { return privateKey2019.SealTo(machineKey, jsonBody), nil } diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index e593eaab..64aead77 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -2,14 +2,18 @@ package mapper import ( "fmt" + "net/netip" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/types" "gopkg.in/check.v1" "tailscale.com/tailcfg" "tailscale.com/types/dnstype" + "tailscale.com/types/key" ) func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { @@ -129,3 +133,349 @@ func TestDNSConfigMapResponse(t *testing.T) { }) } } + +func Test_fullMapResponse(t *testing.T) { + mustNK := func(str string) key.NodePublic { + var k key.NodePublic + _ = k.UnmarshalText([]byte(str)) + + return k + } + + mustDK := func(str string) key.DiscoPublic { + var k key.DiscoPublic + _ = k.UnmarshalText([]byte(str)) + + return k + } + + mustMK := func(str string) key.MachinePublic { + var k key.MachinePublic + _ = k.UnmarshalText([]byte(str)) + + return k + } + + hiview := func(hoin tailcfg.Hostinfo) tailcfg.HostinfoView { + return hoin.View() + } + + created := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC) + lastSeen := time.Date(2009, time.November, 10, 23, 9, 0, 0, time.UTC) + expire := time.Date(2500, time.November, 11, 23, 0, 0, 0, time.UTC) + + tests := []struct { + name string + pol *policy.ACLPolicy + machine *types.Machine + peers types.Machines + + stripEmailDomain bool + baseDomain string + dnsConfig *tailcfg.DNSConfig + derpMap *tailcfg.DERPMap + logtail bool + randomClientPort bool + want *tailcfg.MapResponse + wantErr bool + }{ + // { + // name: "empty-machine", + // machine: types.Machine{}, + // pol: &policy.ACLPolicy{}, + // dnsConfig: &tailcfg.DNSConfig{}, + // baseDomain: "", + // stripEmailDomain: false, + // want: nil, + // wantErr: true, + // }, + { + name: "no-pol-no-peers-map-response", + pol: &policy.ACLPolicy{}, + machine: &types.Machine{ + ID: 0, + MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, + Hostname: "mini", + GivenName: "mini", + UserID: 0, + User: types.User{Name: "mini"}, + ForcedTags: []string{}, + AuthKeyID: 0, + AuthKey: &types.PreAuthKey{}, + LastSeen: &lastSeen, + Expiry: &expire, + HostInfo: types.HostInfo{}, + Endpoints: []string{}, + Routes: []types.Route{ + { + Prefix: types.IPPrefix(netip.MustParsePrefix("0.0.0.0/0")), + Advertised: true, + Enabled: true, + IsPrimary: false, + }, + { + Prefix: types.IPPrefix(netip.MustParsePrefix("192.168.0.0/24")), + Advertised: true, + Enabled: true, + IsPrimary: true, + }, + { + Prefix: types.IPPrefix(netip.MustParsePrefix("172.0.0.0/10")), + Advertised: true, + Enabled: false, + IsPrimary: true, + }, + }, + CreatedAt: created, + }, + peers: []types.Machine{}, + stripEmailDomain: false, + baseDomain: "", + dnsConfig: &tailcfg.DNSConfig{}, + derpMap: &tailcfg.DERPMap{}, + logtail: false, + randomClientPort: false, + want: &tailcfg.MapResponse{ + KeepAlive: false, + Node: &tailcfg.Node{ + ID: 0, + StableID: "0", + Name: "mini", + User: 0, + Key: mustNK( + "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + ), + KeyExpiry: expire, + Machine: mustMK( + "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + ), + DiscoKey: mustDK( + "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + ), + Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}, + AllowedIPs: []netip.Prefix{ + netip.MustParsePrefix("100.64.0.1/32"), + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("192.168.0.0/24"), + }, + Endpoints: []string{}, + DERP: "127.3.3.40:0", + Hostinfo: hiview(tailcfg.Hostinfo{}), + Created: created, + Tags: []string{}, + PrimaryRoutes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")}, + LastSeen: &lastSeen, + Online: new(bool), + KeepAlive: true, + MachineAuthorized: true, + Capabilities: []string{ + tailcfg.CapabilityFileSharing, + tailcfg.CapabilityAdmin, + tailcfg.CapabilitySSH, + }, + }, + DERPMap: &tailcfg.DERPMap{}, + Peers: []*tailcfg.Node{}, + DNSConfig: &tailcfg.DNSConfig{}, + Domain: "", + CollectServices: "false", + PacketFilter: []tailcfg.FilterRule{}, + UserProfiles: []tailcfg.UserProfile{{LoginName: "mini", DisplayName: "mini"}}, + SSHPolicy: nil, + ControlTime: &time.Time{}, + Debug: &tailcfg.Debug{ + DisableLogTail: true, + }, + }, + wantErr: false, + }, + { + name: "no-pol-map-response", + pol: &policy.ACLPolicy{}, + machine: &types.Machine{ + ID: 0, + MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.1")}, + Hostname: "mini", + GivenName: "mini", + UserID: 0, + User: types.User{Name: "mini"}, + ForcedTags: []string{}, + LastSeen: &lastSeen, + Expiry: &expire, + HostInfo: types.HostInfo{}, + Endpoints: []string{}, + Routes: []types.Route{ + { + Prefix: types.IPPrefix(netip.MustParsePrefix("0.0.0.0/0")), + Advertised: true, + Enabled: true, + IsPrimary: false, + }, + { + Prefix: types.IPPrefix(netip.MustParsePrefix("192.168.0.0/24")), + Advertised: true, + Enabled: true, + IsPrimary: true, + }, + { + Prefix: types.IPPrefix(netip.MustParsePrefix("172.0.0.0/10")), + Advertised: true, + Enabled: false, + IsPrimary: true, + }, + }, + CreatedAt: created, + }, + peers: []types.Machine{ + { + ID: 1, + MachineKey: "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + NodeKey: "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + DiscoKey: "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + IPAddresses: []netip.Addr{netip.MustParseAddr("100.64.0.2")}, + Hostname: "peer1", + GivenName: "peer1", + UserID: 0, + User: types.User{Name: "mini"}, + ForcedTags: []string{}, + LastSeen: &lastSeen, + Expiry: &expire, + HostInfo: types.HostInfo{}, + Endpoints: []string{}, + Routes: []types.Route{}, + CreatedAt: created, + }, + }, + stripEmailDomain: false, + baseDomain: "", + dnsConfig: &tailcfg.DNSConfig{}, + derpMap: &tailcfg.DERPMap{}, + logtail: false, + randomClientPort: false, + want: &tailcfg.MapResponse{ + KeepAlive: false, + Node: &tailcfg.Node{ + ID: 0, + StableID: "0", + Name: "mini", + User: 0, + Key: mustNK( + "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + ), + KeyExpiry: expire, + Machine: mustMK( + "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + ), + DiscoKey: mustDK( + "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + ), + Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.1/32")}, + AllowedIPs: []netip.Prefix{ + netip.MustParsePrefix("100.64.0.1/32"), + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("192.168.0.0/24"), + }, + Endpoints: []string{}, + DERP: "127.3.3.40:0", + Hostinfo: hiview(tailcfg.Hostinfo{}), + Created: created, + Tags: []string{}, + PrimaryRoutes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")}, + LastSeen: &lastSeen, + Online: new(bool), + KeepAlive: true, + MachineAuthorized: true, + Capabilities: []string{ + tailcfg.CapabilityFileSharing, + tailcfg.CapabilityAdmin, + tailcfg.CapabilitySSH, + }, + }, + DERPMap: &tailcfg.DERPMap{}, + Peers: []*tailcfg.Node{ + { + ID: 1, + StableID: "1", + Name: "peer1", + Key: mustNK( + "nodekey:9b2ffa7e08cc421a3d2cca9012280f6a236fd0de0b4ce005b30a98ad930306fe", + ), + KeyExpiry: expire, + Machine: mustMK( + "mkey:f08305b4ee4250b95a70f3b7504d048d75d899993c624a26d422c67af0422507", + ), + DiscoKey: mustDK( + "discokey:cf7b0fd05da556fdc3bab365787b506fd82d64a70745db70e00e86c1b1c03084", + ), + Addresses: []netip.Prefix{netip.MustParsePrefix("100.64.0.2/32")}, + AllowedIPs: []netip.Prefix{netip.MustParsePrefix("100.64.0.2/32")}, + Endpoints: []string{}, + DERP: "127.3.3.40:0", + Hostinfo: hiview(tailcfg.Hostinfo{}), + Created: created, + Tags: []string{}, + PrimaryRoutes: []netip.Prefix{}, + LastSeen: &lastSeen, + Online: new(bool), + KeepAlive: true, + MachineAuthorized: true, + Capabilities: []string{ + tailcfg.CapabilityFileSharing, + tailcfg.CapabilityAdmin, + tailcfg.CapabilitySSH, + }, + }, + }, + DNSConfig: &tailcfg.DNSConfig{}, + Domain: "", + CollectServices: "false", + PacketFilter: []tailcfg.FilterRule{}, + UserProfiles: []tailcfg.UserProfile{{LoginName: "mini", DisplayName: "mini"}}, + SSHPolicy: nil, + ControlTime: &time.Time{}, + Debug: &tailcfg.Debug{ + DisableLogTail: true, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := fullMapResponse( + tt.pol, + tt.machine, + tt.peers, + tt.stripEmailDomain, + tt.baseDomain, + tt.dnsConfig, + tt.derpMap, + tt.logtail, + tt.randomClientPort, + ) + + if (err != nil) != tt.wantErr { + t.Errorf("fullMapResponse() error = %v, wantErr %v", err, tt.wantErr) + + return + } + + if diff := cmp.Diff( + tt.want, + got, + cmpopts.EquateEmpty(), + // Ignore ControlTime, it is set to now and we dont really need to mock it. + cmpopts.IgnoreFields(tailcfg.MapResponse{}, "ControlTime"), + ); diff != "" { + t.Errorf("fullMapResponse() unexpected result (-want +got):\n%s", diff) + } + }) + } +} diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 6b42ebe7..531274e8 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -126,7 +126,7 @@ func GenerateFilterRules( stripEmailDomain bool, ) ([]tailcfg.FilterRule, *tailcfg.SSHPolicy, error) { if policy == nil { - return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, ErrEmptyPolicy + return []tailcfg.FilterRule{}, &tailcfg.SSHPolicy{}, nil } rules, err := policy.generateFilterRules(machines, stripEmailDomain) diff --git a/hscontrol/protocol_common.go b/hscontrol/protocol_common.go index 85d18941..bb1d00c0 100644 --- a/hscontrol/protocol_common.go +++ b/hscontrol/protocol_common.go @@ -324,7 +324,7 @@ func (h *Headscale) handleAuthKeyCommon( Msg("Failed authentication via AuthKey") resp.MachineAuthorized = false - respBody, err := mapper.MarshalResponse(resp, h.privateKey2019, machineKey) + respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey) if err != nil { log.Error(). Caller(). @@ -484,7 +484,7 @@ func (h *Headscale) handleAuthKeyCommon( // Otherwise it will need to exec `tailscale up` twice to fetch the *LoginName* resp.Login = *pak.User.TailscaleLogin() - respBody, err := mapper.MarshalResponse(resp, h.privateKey2019, machineKey) + respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey) if err != nil { log.Error(). Caller(). @@ -549,7 +549,7 @@ func (h *Headscale) handleNewMachineCommon( registerRequest.NodeKey) } - respBody, err := mapper.MarshalResponse(resp, h.privateKey2019, machineKey) + respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey) if err != nil { log.Error(). Caller(). @@ -610,7 +610,7 @@ func (h *Headscale) handleMachineLogOutCommon( resp.MachineAuthorized = false resp.NodeKeyExpired = true resp.User = *machine.User.TailscaleUser() - respBody, err := mapper.MarshalResponse(resp, h.privateKey2019, machineKey) + respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey) if err != nil { log.Error(). Caller(). @@ -674,7 +674,7 @@ func (h *Headscale) handleMachineValidRegistrationCommon( resp.User = *machine.User.TailscaleUser() resp.Login = *machine.User.TailscaleLogin() - respBody, err := mapper.MarshalResponse(resp, h.privateKey2019, machineKey) + respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey) if err != nil { log.Error(). Caller(). @@ -736,7 +736,7 @@ func (h *Headscale) handleMachineRefreshKeyCommon( resp.AuthURL = "" resp.User = *machine.User.TailscaleUser() - respBody, err := mapper.MarshalResponse(resp, h.privateKey2019, machineKey) + respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey) if err != nil { log.Error(). Caller(). @@ -803,7 +803,7 @@ func (h *Headscale) handleMachineExpiredOrLoggedOutCommon( registerRequest.NodeKey) } - respBody, err := mapper.MarshalResponse(resp, h.privateKey2019, machineKey) + respBody, err := mapper.MarshalResponse(resp, isNoise, h.privateKey2019, machineKey) if err != nil { log.Error(). Caller().