| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223 |
- package nebula
- import (
- "fmt"
- "io"
- "net/netip"
- "strings"
- "time"
- "github.com/slackhq/nebula/cert"
- "github.com/slackhq/nebula/commands"
- )
- type identityFlags struct {
- Address string
- // addressSet records that -address was given at all, so a line that left it out is
- // told to pass it and one that passed it empty is told the value did not parse. Neither
- // is answered with this node's identity, which is whoami's to report: a program
- // passing a connection's remote address through must get an error for a value it
- // failed to fill in, not the daemon's own identity to authorize against.
- addressSet bool
- }
- // setAddress is -address's setter. flag.Func rather than StringVar so that presence is
- // known; its String is empty, which is what a client building a line from the schema
- // sees as the default.
- func (f *identityFlags) setAddress(v string) error {
- f.Address = v
- f.addressSet = true
- return nil
- }
- // hostIdentity is what identity reports: the identity a certificate asserts for a host,
- // and nothing else. Every field is read from a certificate nebula verified when the
- // tunnel came up, or from our own, which is what lets a program on this host make an
- // authorization decision from it, within one limit. Nebula drops an inbound packet
- // whose source address is not in the sender's certificate, and one with an overlay
- // source address that arrived through a peer's unsafe network rather than from the node
- // holding it, so for a connection that arrived on the nebula interface the source
- // address is the sender's and this is the sender's identity. A connection that arrived
- // on any other interface with an overlay source address was never checked by nebula at
- // all, so a program authorizing on this answer should confirm the connection came in
- // on the nebula interface. The field names are an API: the info API serves this shape
- // to programs that were not built against nebula.
- type hostIdentity struct {
- Name string `json:"name"`
- VpnAddrs []netip.Addr `json:"vpnAddrs"`
- Networks []netip.Prefix `json:"networks"`
- UnsafeNetworks []netip.Prefix `json:"unsafeNetworks"`
- Groups []string `json:"groups"`
- Fingerprint string `json:"fingerprint"`
- Issuer string `json:"issuer"`
- NotBefore time.Time `json:"notBefore"`
- NotAfter time.Time `json:"notAfter"`
- CertVersion int `json:"certVersion"`
- }
- // runIdentity answers for whoever owns the address: this node, when it is one of ours, or
- // the peer behind an established tunnel. The address is required. This node's own
- // identity is whoami's to report, so a caller that failed to pass an address is told so
- // rather than handed an identity it could mistake for its peer's.
- func runIdentity(ifce *Interface, args *identityFlags, a []string) (commands.Result, error) {
- // print-cert takes its address as an operand; this takes a flag, because the query
- // string of the info API names its parameters and an operand has no name there. One
- // spelling on every surface, so the mistake is named rather than ignored.
- if len(a) > 0 {
- return commands.Failed("identity takes the vpn addr as -address, not as an operand: identity -address %s", a[0]), nil
- }
- if !args.addressSet {
- return commands.Failed("No vpn addr was provided, use -address <vpn addr>[:port]; whoami prints this node's own identity"), nil
- }
- vpnAddr, err := parseVpnAddrWithOptionalPort(args.Address)
- if err != nil {
- return commands.Failed("The provided vpn addr could not be parsed: %s", args.Address), nil
- }
- crt, fingerprint := findCertificateForVpnAddr(ifce.pki.getCertState(), ifce.hostMap, vpnAddr)
- if crt == nil {
- return commands.NotFound("No active tunnel for vpn addr: %s", vpnAddr), nil
- }
- id, err := newHostIdentity(crt, fingerprint)
- if err != nil {
- return commands.Result{}, err
- }
- return commands.Value(id), nil
- }
- // runWhoami answers with this node's own identity, from its own certificate.
- func runWhoami(ifce *Interface) (commands.Result, error) {
- crt := ownCertificate(ifce.pki.getCertState())
- if crt == nil {
- return commands.Result{}, fmt.Errorf("no certificate is loaded")
- }
- id, err := newHostIdentity(crt, "")
- if err != nil {
- return commands.Result{}, err
- }
- return commands.Value(id), nil
- }
- // textIdentity is the human rendering, shared by every surface.
- func textIdentity(id hostIdentity, w io.Writer) error {
- _, err := fmt.Fprintf(w,
- "name: %s\nvpn addrs: %s\nnetworks: %s\nunsafe networks: %s\ngroups: %s\nfingerprint: %s\nissuer: %s\nnot before: %s\nnot after: %s\ncert version: %d\n",
- id.Name,
- joinStringers(id.VpnAddrs),
- joinStringers(id.Networks),
- joinStringers(id.UnsafeNetworks),
- strings.Join(id.Groups, ", "),
- id.Fingerprint,
- id.Issuer,
- id.NotBefore.Format(time.RFC3339),
- id.NotAfter.Format(time.RFC3339),
- id.CertVersion,
- )
- return err
- }
- func joinStringers[T fmt.Stringer](in []T) string {
- parts := make([]string, 0, len(in))
- for _, v := range in {
- parts = append(parts, v.String())
- }
- return strings.Join(parts, ", ")
- }
- // newHostIdentity reads the identity out of a certificate. fingerprint is the
- // certificate's when the caller already holds it, as a verified peer certificate does,
- // and "" to have it hashed here. Slices are always allocated so a host with no groups
- // reports [] rather than null.
- func newHostIdentity(crt cert.Certificate, fingerprint string) (hostIdentity, error) {
- if fingerprint == "" {
- fp, err := crt.Fingerprint()
- if err != nil {
- return hostIdentity{}, err
- }
- fingerprint = fp
- }
- networks, unsafeNetworks, groups := crt.Networks(), crt.UnsafeNetworks(), crt.Groups()
- id := hostIdentity{
- Name: crt.Name(),
- VpnAddrs: make([]netip.Addr, 0, len(networks)),
- Networks: append(make([]netip.Prefix, 0, len(networks)), networks...),
- UnsafeNetworks: append(make([]netip.Prefix, 0, len(unsafeNetworks)), unsafeNetworks...),
- Groups: append(make([]string, 0, len(groups)), groups...),
- Fingerprint: fingerprint,
- Issuer: crt.Issuer(),
- NotBefore: crt.NotBefore(),
- NotAfter: crt.NotAfter(),
- CertVersion: int(crt.Version()),
- }
- for _, n := range networks {
- id.VpnAddrs = append(id.VpnAddrs, n.Addr())
- }
- return id, nil
- }
- // ownCertificate is this node's certificate: the v2 one when it is loaded, else the v1.
- // A v2 certificate is a superset of the v1 and may carry more networks, and it is the
- // one newCertState builds myVpnAddrsTable from, so an address that matched that table is
- // always listed in the certificate reported here. It is nil only when no certificate is
- // loaded, which a running nebula never has.
- func ownCertificate(cs *CertState) cert.Certificate {
- if cs == nil {
- return nil
- }
- if crt := cs.getCertificate(cert.Version2); crt != nil {
- return crt
- }
- return cs.getCertificate(cert.Version1)
- }
- // findCertificateForVpnAddr answers who owns a vpn address: this node, from its own
- // certificate state, since the hostmap never carries an entry for ourselves, or the peer
- // behind an established tunnel. The certificate is nil when the address is unknown,
- // when the tunnel is still pending and when it is mid teardown. A peer's comes with the
- // fingerprint nebula computed when it verified the certificate; our own comes with "",
- // since none is kept for it. The lighthouse's DNS TXT lookup and identity both go
- // through it, so the two surfaces cannot disagree about whose address it is.
- func findCertificateForVpnAddr(cs *CertState, hostMap *HostMap, ip netip.Addr) (crt cert.Certificate, fingerprint string) {
- if cs != nil && cs.myVpnAddrsTable != nil && cs.myVpnAddrsTable.Contains(ip) {
- return ownCertificate(cs), ""
- }
- hostinfo := hostMap.QueryVpnAddr(ip)
- if hostinfo == nil {
- return nil, ""
- }
- cc := hostinfo.GetCert()
- if cc == nil {
- return nil, ""
- }
- return cc.Certificate, cc.Fingerprint
- }
- // parseVpnAddrWithOptionalPort reads a vpn address alone or with a port,
- // 192.168.100.7:54321 or [fd00::1]:443, so a program can pass a connection's remote
- // address through as its runtime reported it. A 4in6 address (::ffff:a.b.c.d) is
- // unmapped, which is how an IPv4 peer arrives on a dual stack listener, and a zone is
- // dropped, since no vpn address carries one.
- func parseVpnAddrWithOptionalPort(s string) (netip.Addr, error) {
- ip, err := netip.ParseAddr(s)
- if err != nil {
- ap, err := netip.ParseAddrPort(s)
- if err != nil {
- return netip.Addr{}, err
- }
- ip = ap.Addr()
- }
- return ip.Unmap().WithZone(""), nil
- }
|