| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- package nebula
- import (
- "bytes"
- "encoding/json"
- "net/netip"
- "testing"
- "time"
- "github.com/slackhq/nebula/cert"
- "github.com/slackhq/nebula/cert_test"
- "github.com/slackhq/nebula/commands"
- "github.com/slackhq/nebula/test"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- // identityFixture is an Interface with our own certificate loaded and one peer behind an
- // established tunnel, which is all identity reads.
- func identityFixture(t *testing.T) (*Interface, cert.Certificate) {
- t.Helper()
- l := test.NewLogger()
- me := netip.MustParseAddr("10.0.0.1")
- peer := netip.MustParsePrefix("10.0.0.2/24")
- ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
- peerCert, _, _, _ := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca, caKey, "peer", time.Time{}, time.Time{},
- []netip.Prefix{peer}, []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")}, []string{"web", "db"})
- hm := newHostMap(l)
- hm.Hosts[peer.Addr()] = &HostInfo{
- vpnAddrs: []netip.Addr{peer.Addr()},
- ConnectionState: &ConnectionState{peerCert: &cert.CachedCertificate{Certificate: peerCert}},
- }
- // A tunnel still handshaking has no certificate yet, and must not be reported.
- hm.Hosts[netip.MustParseAddr("10.0.0.3")] = &HostInfo{
- vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.3")},
- ConnectionState: &ConnectionState{},
- }
- return &Interface{pki: newTestPKI(t, "me", []netip.Addr{me}), hostMap: hm}, peerCert
- }
- // identity runs the command with -address given as address.
- func identity(t *testing.T, f *Interface, address string, operands ...string) commands.Result {
- t.Helper()
- fs := &identityFlags{}
- require.NoError(t, fs.setAddress(address))
- r, err := runIdentity(f, fs, operands)
- require.NoError(t, err)
- return r
- }
- // whoami runs the whoami command.
- func whoami(t *testing.T, f *Interface) commands.Result {
- t.Helper()
- r, err := runWhoami(f)
- require.NoError(t, err)
- return r
- }
- func TestWhoamiReportsThisNode(t *testing.T) {
- f, _ := identityFixture(t)
- r := whoami(t, f)
- require.True(t, r.OK)
- id, ok := r.Value.(hostIdentity)
- require.True(t, ok, "got %T", r.Value)
- assert.Equal(t, "me", id.Name)
- assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.1")}, id.VpnAddrs)
- assert.Equal(t, 2, id.CertVersion)
- assert.NotEmpty(t, id.Fingerprint)
- assert.NotEmpty(t, id.Issuer)
- // Our own address answers the same way: the hostmap never holds ourselves.
- self := identity(t, f, "10.0.0.1")
- assert.Equal(t, id, self.Value)
- }
- func TestIdentityReportsAPeerFromItsCertificate(t *testing.T) {
- f, peerCert := identityFixture(t)
- for _, addr := range []string{
- "10.0.0.2",
- "10.0.0.2:54321", // a connection's RemoteAddr, as a server has it
- "::ffff:10.0.0.2", // the same peer seen through a dual stack listener
- "[::ffff:10.0.0.2]:8", // and with a port
- } {
- r := identity(t, f, addr)
- require.True(t, r.OK, "%s: %s", addr, r.Reason)
- id := r.Value.(hostIdentity)
- assert.Equal(t, "peer", id.Name, addr)
- assert.Equal(t, []netip.Addr{netip.MustParseAddr("10.0.0.2")}, id.VpnAddrs, addr)
- assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("10.0.0.2/24")}, id.Networks, addr)
- assert.Equal(t, []netip.Prefix{netip.MustParsePrefix("192.168.1.0/24")}, id.UnsafeNetworks, addr)
- assert.Equal(t, []string{"web", "db"}, id.Groups, addr)
- assert.Equal(t, peerCert.Issuer(), id.Issuer, addr)
- assert.Equal(t, peerCert.NotAfter(), id.NotAfter, addr)
- }
- }
- // TestIdentityRefusals: the ways a lookup fails are told apart, because a program
- // deciding whether to let a connection in treats them differently.
- func TestIdentityRefusals(t *testing.T) {
- f, _ := identityFixture(t)
- unknown := identity(t, f, "10.0.0.9")
- assert.False(t, unknown.OK)
- assert.True(t, unknown.NotFound, "no tunnel is a not found")
- assert.Equal(t, "No active tunnel for vpn addr: 10.0.0.9", unknown.Reason)
- pending := identity(t, f, "10.0.0.3:1")
- assert.True(t, pending.NotFound, "a tunnel without a certificate yet is not active")
- assert.Equal(t, "No active tunnel for vpn addr: 10.0.0.3", pending.Reason, "the reason names the address, not the port")
- garbage := identity(t, f, "not-an-address")
- assert.False(t, garbage.OK)
- assert.False(t, garbage.NotFound, "an address that does not parse is the caller's mistake")
- assert.Equal(t, "The provided vpn addr could not be parsed: not-an-address", garbage.Reason)
- // An address that was given and is empty is a caller that failed to fill it in, and
- // must not be answered with our own identity.
- empty := identity(t, f, "")
- assert.False(t, empty.OK)
- assert.False(t, empty.NotFound)
- assert.Equal(t, "The provided vpn addr could not be parsed: ", empty.Reason)
- operand := identity(t, f, "", "10.0.0.2")
- assert.False(t, operand.OK)
- assert.Contains(t, operand.Reason, "identity -address 10.0.0.2")
- // No address at all is a caller that forgot the parameter, and is pointed at whoami
- // rather than handed this node's identity.
- omitted, err := runIdentity(f, &identityFlags{}, nil)
- require.NoError(t, err)
- assert.False(t, omitted.OK)
- assert.False(t, omitted.NotFound)
- assert.Contains(t, omitted.Reason, "-address")
- assert.Contains(t, omitted.Reason, "whoami")
- }
- // TestOwnCertificatePrefersV2: with both versions loaded, the v2 certificate is the one
- // myVpnAddrsTable is built from and may carry addresses the v1 does not, so it is the one
- // whoami and an identity lookup of our own address report, whichever version handshakes
- // are initiated with.
- func TestOwnCertificatePrefersV2(t *testing.T) {
- v4 := netip.MustParsePrefix("10.0.0.1/24")
- ca1, _, caKey1, _ := cert_test.NewTestCaCert(cert.Version1, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
- v1, _, _, _ := cert_test.NewTestCert(cert.Version1, cert.Curve_CURVE25519, ca1, caKey1, "me", time.Time{}, time.Time{}, []netip.Prefix{v4}, nil, nil)
- ca2, _, caKey2, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Time{}, time.Time{}, nil, nil, nil)
- v2, _, _, _ := cert_test.NewTestCert(cert.Version2, cert.Curve_CURVE25519, ca2, caKey2, "me", time.Time{}, time.Time{},
- []netip.Prefix{v4, netip.MustParsePrefix("fd00::1/64")}, nil, nil)
- assert.Nil(t, ownCertificate(nil))
- assert.Equal(t, v1, ownCertificate(&CertState{v1Cert: v1, initiatingVersion: cert.Version1}))
- assert.Equal(t, v2, ownCertificate(&CertState{v2Cert: v2, initiatingVersion: cert.Version2}))
- assert.Equal(t, v2, ownCertificate(&CertState{v1Cert: v1, v2Cert: v2, initiatingVersion: cert.Version1}),
- "the initiating version is v1, the reported certificate is still the v2")
- }
- // TestIdentityShapes pins the JSON a program reads and the text a person reads. The
- // field names are an API served to programs not built against nebula.
- func TestIdentityShapes(t *testing.T) {
- f, _ := identityFixture(t)
- r := identity(t, f, "10.0.0.2")
- b, err := json.Marshal(r.Payload())
- require.NoError(t, err)
- var got map[string]any
- require.NoError(t, json.Unmarshal(b, &got))
- var keys []string
- for k := range got {
- keys = append(keys, k)
- }
- assert.ElementsMatch(t, []string{
- "name", "vpnAddrs", "networks", "unsafeNetworks", "groups",
- "fingerprint", "issuer", "notBefore", "notAfter", "certVersion",
- }, keys)
- // Empty lists marshal as [], never null.
- self := whoami(t, f)
- b, err = json.Marshal(self.Payload())
- require.NoError(t, err)
- assert.Contains(t, string(b), `"groups":[]`)
- assert.Contains(t, string(b), `"unsafeNetworks":[]`)
- out := &bytes.Buffer{}
- require.NoError(t, commands.Render(&commands.Command{Name: commands.Identity, Text: commands.Typed(textIdentity)}, r, commands.FormatText, out))
- assert.Contains(t, out.String(), "name: peer\n")
- assert.Contains(t, out.String(), "vpn addrs: 10.0.0.2\n")
- assert.Contains(t, out.String(), "groups: web, db\n")
- assert.Contains(t, out.String(), "cert version: 2\n")
- }
- func TestParseVpnAddrWithOptionalPort(t *testing.T) {
- for in, want := range map[string]string{
- "10.0.0.2": "10.0.0.2",
- "10.0.0.2:443": "10.0.0.2",
- "fd00::1": "fd00::1",
- "[fd00::1]:443": "fd00::1",
- "::ffff:10.0.0.2": "10.0.0.2",
- "[::ffff:10.0.0.2]:1": "10.0.0.2",
- "fe80::1%eth0": "fe80::1",
- } {
- got, err := parseVpnAddrWithOptionalPort(in)
- require.NoError(t, err, in)
- assert.Equal(t, netip.MustParseAddr(want), got, in)
- }
- for _, in := range []string{"", "10.0.0.2:", ":443", "10.0.0", "example.com:80"} {
- _, err := parseVpnAddrWithOptionalPort(in)
- assert.Error(t, err, in)
- }
- }
|