mirror of https://github.com/hashicorp/consul
Merge pull request #4215 from hashicorp/feature/config-node-meta-dns-txt
Add configuration entry to control including TXT records for node meta in DNS responsespull/4261/head
commit
0d4e8676d1
|
@ -592,6 +592,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
|
||||||
DNSRecursors: dnsRecursors,
|
DNSRecursors: dnsRecursors,
|
||||||
DNSServiceTTL: dnsServiceTTL,
|
DNSServiceTTL: dnsServiceTTL,
|
||||||
DNSUDPAnswerLimit: b.intVal(c.DNS.UDPAnswerLimit),
|
DNSUDPAnswerLimit: b.intVal(c.DNS.UDPAnswerLimit),
|
||||||
|
DNSNodeMetaTXT: b.boolValWithDefault(c.DNS.NodeMetaTXT, true),
|
||||||
|
|
||||||
// HTTP
|
// HTTP
|
||||||
HTTPPort: httpPort,
|
HTTPPort: httpPort,
|
||||||
|
@ -1010,13 +1011,18 @@ func (b *Builder) serviceVal(v *ServiceDefinition) *structs.ServiceDefinition {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) boolVal(v *bool) bool {
|
func (b *Builder) boolValWithDefault(v *bool, default_val bool) bool {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return false
|
return default_val
|
||||||
}
|
}
|
||||||
|
|
||||||
return *v
|
return *v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *Builder) boolVal(v *bool) bool {
|
||||||
|
return b.boolValWithDefault(v, false)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *Builder) durationVal(name string, v *string) (d time.Duration) {
|
func (b *Builder) durationVal(name string, v *string) (d time.Duration) {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -360,6 +360,7 @@ type DNS struct {
|
||||||
RecursorTimeout *string `json:"recursor_timeout,omitempty" hcl:"recursor_timeout" mapstructure:"recursor_timeout"`
|
RecursorTimeout *string `json:"recursor_timeout,omitempty" hcl:"recursor_timeout" mapstructure:"recursor_timeout"`
|
||||||
ServiceTTL map[string]string `json:"service_ttl,omitempty" hcl:"service_ttl" mapstructure:"service_ttl"`
|
ServiceTTL map[string]string `json:"service_ttl,omitempty" hcl:"service_ttl" mapstructure:"service_ttl"`
|
||||||
UDPAnswerLimit *int `json:"udp_answer_limit,omitempty" hcl:"udp_answer_limit" mapstructure:"udp_answer_limit"`
|
UDPAnswerLimit *int `json:"udp_answer_limit,omitempty" hcl:"udp_answer_limit" mapstructure:"udp_answer_limit"`
|
||||||
|
NodeMetaTXT *bool `json:"enable_additional_node_meta_txt,omitempty" hcl:"enable_additional_node_meta_txt" mapstructure:"enable_additional_node_meta_txt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPConfig struct {
|
type HTTPConfig struct {
|
||||||
|
|
|
@ -281,6 +281,11 @@ type RuntimeConfig struct {
|
||||||
// hcl: dns_config { udp_answer_limit = int }
|
// hcl: dns_config { udp_answer_limit = int }
|
||||||
DNSUDPAnswerLimit int
|
DNSUDPAnswerLimit int
|
||||||
|
|
||||||
|
// DNSNodeMetaTXT controls whether DNS queries will synthesize
|
||||||
|
// TXT records for the node metadata and add them when not specifically
|
||||||
|
// request (query type = TXT). If unset this will default to true
|
||||||
|
DNSNodeMetaTXT bool
|
||||||
|
|
||||||
// DNSRecursors can be set to allow the DNS servers to recursively
|
// DNSRecursors can be set to allow the DNS servers to recursively
|
||||||
// resolve non-consul domains.
|
// resolve non-consul domains.
|
||||||
//
|
//
|
||||||
|
|
|
@ -3371,6 +3371,7 @@ func TestFullConfig(t *testing.T) {
|
||||||
DNSRecursors: []string{"63.38.39.58", "92.49.18.18"},
|
DNSRecursors: []string{"63.38.39.58", "92.49.18.18"},
|
||||||
DNSServiceTTL: map[string]time.Duration{"*": 32030 * time.Second},
|
DNSServiceTTL: map[string]time.Duration{"*": 32030 * time.Second},
|
||||||
DNSUDPAnswerLimit: 29909,
|
DNSUDPAnswerLimit: 29909,
|
||||||
|
DNSNodeMetaTXT: true,
|
||||||
DataDir: dataDir,
|
DataDir: dataDir,
|
||||||
Datacenter: "rzo029wg",
|
Datacenter: "rzo029wg",
|
||||||
DevMode: true,
|
DevMode: true,
|
||||||
|
@ -4043,6 +4044,7 @@ func TestSanitize(t *testing.T) {
|
||||||
"DNSDomain": "",
|
"DNSDomain": "",
|
||||||
"DNSEnableTruncate": false,
|
"DNSEnableTruncate": false,
|
||||||
"DNSMaxStale": "0s",
|
"DNSMaxStale": "0s",
|
||||||
|
"DNSNodeMetaTXT": false,
|
||||||
"DNSNodeTTL": "0s",
|
"DNSNodeTTL": "0s",
|
||||||
"DNSOnlyPassing": false,
|
"DNSOnlyPassing": false,
|
||||||
"DNSPort": 0,
|
"DNSPort": 0,
|
||||||
|
|
31
agent/dns.go
31
agent/dns.go
|
@ -51,6 +51,7 @@ type dnsConfig struct {
|
||||||
ServiceTTL map[string]time.Duration
|
ServiceTTL map[string]time.Duration
|
||||||
UDPAnswerLimit int
|
UDPAnswerLimit int
|
||||||
ARecordLimit int
|
ARecordLimit int
|
||||||
|
NodeMetaTXT bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSServer is used to wrap an Agent and expose various
|
// DNSServer is used to wrap an Agent and expose various
|
||||||
|
@ -109,6 +110,7 @@ func GetDNSConfig(conf *config.RuntimeConfig) *dnsConfig {
|
||||||
SegmentName: conf.SegmentName,
|
SegmentName: conf.SegmentName,
|
||||||
ServiceTTL: conf.DNSServiceTTL,
|
ServiceTTL: conf.DNSServiceTTL,
|
||||||
UDPAnswerLimit: conf.DNSUDPAnswerLimit,
|
UDPAnswerLimit: conf.DNSUDPAnswerLimit,
|
||||||
|
NodeMetaTXT: conf.DNSNodeMetaTXT,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,7 +376,7 @@ func (d *DNSServer) nameservers(edns bool) (ns []dns.RR, extra []dns.RR) {
|
||||||
}
|
}
|
||||||
ns = append(ns, nsrr)
|
ns = append(ns, nsrr)
|
||||||
|
|
||||||
glue := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns)
|
glue := d.formatNodeRecord(nil, addr, fqdn, dns.TypeANY, d.config.NodeTTL, edns, false)
|
||||||
extra = append(extra, glue...)
|
extra = append(extra, glue...)
|
||||||
|
|
||||||
// don't provide more than 3 servers
|
// don't provide more than 3 servers
|
||||||
|
@ -582,7 +584,7 @@ RPC:
|
||||||
n := out.NodeServices.Node
|
n := out.NodeServices.Node
|
||||||
edns := req.IsEdns0() != nil
|
edns := req.IsEdns0() != nil
|
||||||
addr := d.agent.TranslateAddress(datacenter, n.Address, n.TaggedAddresses)
|
addr := d.agent.TranslateAddress(datacenter, n.Address, n.TaggedAddresses)
|
||||||
records := d.formatNodeRecord(out.NodeServices.Node, addr, req.Question[0].Name, qType, d.config.NodeTTL, edns)
|
records := d.formatNodeRecord(out.NodeServices.Node, addr, req.Question[0].Name, qType, d.config.NodeTTL, edns, true)
|
||||||
if records != nil {
|
if records != nil {
|
||||||
resp.Answer = append(resp.Answer, records...)
|
resp.Answer = append(resp.Answer, records...)
|
||||||
}
|
}
|
||||||
|
@ -610,7 +612,7 @@ func encodeKVasRFC1464(key, value string) (txt string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatNodeRecord takes a Node and returns an A, AAAA, TXT or CNAME record
|
// formatNodeRecord takes a Node and returns an A, AAAA, TXT or CNAME record
|
||||||
func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qType uint16, ttl time.Duration, edns bool) (records []dns.RR) {
|
func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qType uint16, ttl time.Duration, edns, answer bool) (records []dns.RR) {
|
||||||
// Parse the IP
|
// Parse the IP
|
||||||
ip := net.ParseIP(addr)
|
ip := net.ParseIP(addr)
|
||||||
var ipv4 net.IP
|
var ipv4 net.IP
|
||||||
|
@ -671,7 +673,20 @@ func (d *DNSServer) formatNodeRecord(node *structs.Node, addr, qName string, qTy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if node != nil && (qType == dns.TypeANY || qType == dns.TypeTXT) {
|
node_meta_txt := false
|
||||||
|
|
||||||
|
if node == nil {
|
||||||
|
node_meta_txt = false
|
||||||
|
} else if answer {
|
||||||
|
node_meta_txt = true
|
||||||
|
} else {
|
||||||
|
// Use configuration when the TXT RR would
|
||||||
|
// end up in the Additional section of the
|
||||||
|
// DNS response
|
||||||
|
node_meta_txt = d.config.NodeMetaTXT
|
||||||
|
}
|
||||||
|
|
||||||
|
if node_meta_txt {
|
||||||
for key, value := range node.Meta {
|
for key, value := range node.Meta {
|
||||||
txt := value
|
txt := value
|
||||||
if !strings.HasPrefix(strings.ToLower(key), "rfc1035-") {
|
if !strings.HasPrefix(strings.ToLower(key), "rfc1035-") {
|
||||||
|
@ -782,8 +797,8 @@ func (d *DNSServer) trimTCPResponse(req, resp *dns.Msg) (trimmed bool) {
|
||||||
originalNumRecords := len(resp.Answer)
|
originalNumRecords := len(resp.Answer)
|
||||||
|
|
||||||
// It is not possible to return more than 4k records even with compression
|
// It is not possible to return more than 4k records even with compression
|
||||||
// Since we are performing binary search it is not a big deal, but it
|
// Since we are performing binary search it is not a big deal, but it
|
||||||
// improves a bit performance, even with binary search
|
// improves a bit performance, even with binary search
|
||||||
truncateAt := 4096
|
truncateAt := 4096
|
||||||
if req.Question[0].Qtype == dns.TypeSRV {
|
if req.Question[0].Qtype == dns.TypeSRV {
|
||||||
// More than 1024 SRV records do not fit in 64k
|
// More than 1024 SRV records do not fit in 64k
|
||||||
|
@ -1143,7 +1158,7 @@ func (d *DNSServer) serviceNodeRecords(dc string, nodes structs.CheckServiceNode
|
||||||
handled[addr] = struct{}{}
|
handled[addr] = struct{}{}
|
||||||
|
|
||||||
// Add the node record
|
// Add the node record
|
||||||
records := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns)
|
records := d.formatNodeRecord(node.Node, addr, qName, qType, ttl, edns, true)
|
||||||
if records != nil {
|
if records != nil {
|
||||||
resp.Answer = append(resp.Answer, records...)
|
resp.Answer = append(resp.Answer, records...)
|
||||||
count++
|
count++
|
||||||
|
@ -1192,7 +1207,7 @@ func (d *DNSServer) serviceSRVRecords(dc string, nodes structs.CheckServiceNodes
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the extra record
|
// Add the extra record
|
||||||
records := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl, edns)
|
records := d.formatNodeRecord(node.Node, addr, srvRec.Target, dns.TypeANY, ttl, edns, false)
|
||||||
if len(records) > 0 {
|
if len(records) > 0 {
|
||||||
// Use the node address if it doesn't differ from the service address
|
// Use the node address if it doesn't differ from the service address
|
||||||
if addr == node.Node.Address {
|
if addr == node.Node.Address {
|
||||||
|
|
|
@ -472,6 +472,51 @@ func TestDNS_NodeLookup_TXT(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDNS_NodeLookup_TXT_DontSuppress(t *testing.T) {
|
||||||
|
a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = false }`)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
args := &structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "google",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
NodeMeta: map[string]string{
|
||||||
|
"rfc1035-00": "value0",
|
||||||
|
"key0": "value1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var out struct{}
|
||||||
|
if err := a.RPC("Catalog.Register", args, &out); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("google.node.consul.", dns.TypeTXT)
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
in, _, err := c.Exchange(m, a.DNSAddr())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should have the 1 TXT record reply
|
||||||
|
if len(in.Answer) != 2 {
|
||||||
|
t.Fatalf("Bad: %#v", in)
|
||||||
|
}
|
||||||
|
|
||||||
|
txtRec, ok := in.Answer[0].(*dns.TXT)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||||
|
}
|
||||||
|
if len(txtRec.Txt) != 1 {
|
||||||
|
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||||
|
}
|
||||||
|
if txtRec.Txt[0] != "value0" && txtRec.Txt[0] != "key0=value1" {
|
||||||
|
t.Fatalf("Bad: %#v", in.Answer[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDNS_NodeLookup_ANY(t *testing.T) {
|
func TestDNS_NodeLookup_ANY(t *testing.T) {
|
||||||
a := NewTestAgent(t.Name(), ``)
|
a := NewTestAgent(t.Name(), ``)
|
||||||
defer a.Shutdown()
|
defer a.Shutdown()
|
||||||
|
@ -510,7 +555,46 @@ func TestDNS_NodeLookup_ANY(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
verify.Values(t, "answer", in.Answer, wantAnswer)
|
verify.Values(t, "answer", in.Answer, wantAnswer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNS_NodeLookup_ANY_DontSuppressTXT(t *testing.T) {
|
||||||
|
a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = false }`)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
args := &structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "bar",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
NodeMeta: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var out struct{}
|
||||||
|
if err := a.RPC("Catalog.Register", args, &out); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("bar.node.consul.", dns.TypeANY)
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
in, _, err := c.Exchange(m, a.DNSAddr())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantAnswer := []dns.RR{
|
||||||
|
&dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "bar.node.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4},
|
||||||
|
A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1
|
||||||
|
},
|
||||||
|
&dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{Name: "bar.node.consul.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Rdlength: 0xa},
|
||||||
|
Txt: []string{"key=value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
verify.Values(t, "answer", in.Answer, wantAnswer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDNS_EDNS0(t *testing.T) {
|
func TestDNS_EDNS0(t *testing.T) {
|
||||||
|
@ -4613,6 +4697,93 @@ func TestDNS_ServiceLookup_FilterACL(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDNS_ServiceLookup_MetaTXT(t *testing.T) {
|
||||||
|
a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = true }`)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
args := &structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "bar",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
NodeMeta: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Service: "db",
|
||||||
|
Tags: []string{"master"},
|
||||||
|
Port: 12345,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var out struct{}
|
||||||
|
if err := a.RPC("Catalog.Register", args, &out); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("db.service.consul.", dns.TypeSRV)
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
in, _, err := c.Exchange(m, a.DNSAddr())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantAdditional := []dns.RR{
|
||||||
|
&dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "bar.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4},
|
||||||
|
A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1
|
||||||
|
},
|
||||||
|
&dns.TXT{
|
||||||
|
Hdr: dns.RR_Header{Name: "bar.node.dc1.consul.", Rrtype: dns.TypeTXT, Class: dns.ClassINET, Rdlength: 0xa},
|
||||||
|
Txt: []string{"key=value"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
verify.Values(t, "additional", in.Extra, wantAdditional)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDNS_ServiceLookup_SuppressTXT(t *testing.T) {
|
||||||
|
a := NewTestAgent(t.Name(), `dns_config = { enable_additional_node_meta_txt = false }`)
|
||||||
|
defer a.Shutdown()
|
||||||
|
|
||||||
|
// Register a node with a service.
|
||||||
|
args := &structs.RegisterRequest{
|
||||||
|
Datacenter: "dc1",
|
||||||
|
Node: "bar",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
NodeMeta: map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
Service: &structs.NodeService{
|
||||||
|
Service: "db",
|
||||||
|
Tags: []string{"master"},
|
||||||
|
Port: 12345,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var out struct{}
|
||||||
|
if err := a.RPC("Catalog.Register", args, &out); err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion("db.service.consul.", dns.TypeSRV)
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
in, _, err := c.Exchange(m, a.DNSAddr())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantAdditional := []dns.RR{
|
||||||
|
&dns.A{
|
||||||
|
Hdr: dns.RR_Header{Name: "bar.node.dc1.consul.", Rrtype: dns.TypeA, Class: dns.ClassINET, Rdlength: 0x4},
|
||||||
|
A: []byte{0x7f, 0x0, 0x0, 0x1}, // 127.0.0.1
|
||||||
|
},
|
||||||
|
}
|
||||||
|
verify.Values(t, "additional", in.Extra, wantAdditional)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDNS_AddressLookup(t *testing.T) {
|
func TestDNS_AddressLookup(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
a := NewTestAgent(t.Name(), "")
|
a := NewTestAgent(t.Name(), "")
|
||||||
|
|
|
@ -778,6 +778,12 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
|
||||||
be increasingly uncommon to need to change this value with modern
|
be increasingly uncommon to need to change this value with modern
|
||||||
resolvers).
|
resolvers).
|
||||||
|
|
||||||
|
* <a name="enable_additional_node_meta_txt"></a><a href="#enable_additional_node_meta_txt">`enable_additional_node_meta_txt`</a> -
|
||||||
|
When set to true, Consul will add TXT records for Node metadata into the Additional section of the DNS responses for several
|
||||||
|
query types such as SRV queries. When set to false those records are emitted. This does not impact the behavior of those
|
||||||
|
same TXT records when they would be added to the Answer section of the response like when querying with type TXT or ANY. This
|
||||||
|
defaults to true.
|
||||||
|
|
||||||
* <a name="domain"></a><a href="#domain">`domain`</a> Equivalent to the
|
* <a name="domain"></a><a href="#domain">`domain`</a> Equivalent to the
|
||||||
[`-domain` command-line flag](#_domain).
|
[`-domain` command-line flag](#_domain).
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue