Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ README.md
.git/
.gitignore
.vscode/
manifests/
skaffold.yaml
19 changes: 10 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
FROM --platform=$BUILDPLATFORM golang:1.16 as build
LABEL maintainer="Blake Covarrubias <blake@covarrubi.as>" \
org.opencontainers.image.authors="Blake Covarrubias <blake@covarrubi.as>" \
org.opencontainers.image.description="Advertises records for Kubernetes resources over multicast DNS." \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="git@github.com:blake/external-mdns" \
org.opencontainers.image.title="external-mdns" \
org.opencontainers.image.url="https://github.com/blake/external-mdns"
FROM --platform=$BUILDPLATFORM golang:1.23 as build
LABEL \
maintainer="Blake Covarrubias <blake@covarrubi.as>" \
org.opencontainers.image.authors="Blake Covarrubias <blake@covarrubi.as>" \
org.opencontainers.image.description="Advertises records for Kubernetes resources over multicast DNS." \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="git@github.com:blake/external-mdns" \
org.opencontainers.image.title="external-mdns" \
org.opencontainers.image.url="https://github.com/blake/external-mdns"

ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT

ADD . /go/src/github.com/blake/external-mdns
ADD . /go/src/github.com/blake/external-mdns/
WORKDIR /go/src/github.com/blake/external-mdns

RUN mkdir -p /release/etc &&\
Expand Down
147 changes: 35 additions & 112 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,121 +31,42 @@ with environment variables. For instance, `--record-ttl` could be replaced with
`EXTERNAL_MDNS_RECORD_TTL=60`, or `--namespace kube-system` could be replaced
with `EXTERNAL_MDNS_NAMESPACE=kube-system`.

### Manifest (without RBAC)

```yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-mdns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-mdns
template:
metadata:
labels:
app: external-mdns
spec:
securityContext:
runAsUser: 65534
runAsGroup: 65534
runAsNonRoot: true
hostNetwork: true
serviceAccountName: external-mdns
containers:
- name: external-mdns
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
image: blakec/external-mdns:latest
args:
- -source=ingress
- -source=service
Deployment manifests are located in the [manifests/](manifests/) directory.

To deploy External-mDNS into a cluster without RBAC, use the following command.

```shell
kubectl apply --kustomize manifests/
```

### Manifest (with RBAC)

```yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-mdns
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-mdns
rules:
- apiGroups: [""]
resources: ["services"]
verbs: ["list", "watch"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-mdns-viewer
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-mdns
subjects:
- kind: ServiceAccount
name: external-mdns
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-mdns
spec:
strategy:
type: Recreate
selector:
matchLabels:
app: external-mdns
template:
metadata:
labels:
app: external-mdns
spec:
securityContext:
runAsUser: 65534
runAsGroup: 65534
runAsNonRoot: true
hostNetwork: true
serviceAccountName: external-mdns
containers:
- name: external-mdns
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
image: blakec/external-mdns:latest
args:
- -source=ingress
- -source=service
To deploy External-mDNS into a cluster with RBAC, use manifests overlay.

```shell
kubectl apply --kustomize manifests/rbac
```

Deploy External-mDNS using `kubectl apply --filename external-mdns.yaml`.
Verify the External-mDNS resources have correctly been deployed using
`kubectl get`.

Check that External-mDNS has created the desired DNS records for your advertised
services, and that it points to its load balancer's IP.
### Without RBAC

```shell
kubectl get --kustomize manifests
```

Test that the record is resolvable from the local LAN using the appropriate
command for your operating system.
### With RBAC

#### BSD/macOS
```shell
kubectl get --kustomize manifests/rbac
```

## Verifying operation

Check that External-mDNS has created the desired DNS records for your advertised
services and that they resolve to the correct load balancer or ingress IP by
using the appropriate command for your operating system.

### BSD/macOS

```console
$ dns-sd -Q example.local a in
Expand All @@ -155,14 +76,16 @@ Timestamp A/R Flags if Name Type Class Rdata
22:50:37.959 Add 2 4 example.local. Addr IN 192.0.2.10
```

#### Linux
### Linux

Resolve the hostname using the `getent` command.

```console
$ getent hosts example.local
192.0.2.10 example.local
```

Or, resolve the hostname using Avahi.
Alternatively, you may also attempt to resolve the hostname using Avahi.

```console
$ avahi-resolve-address -4 --name example.local
Expand All @@ -173,11 +96,11 @@ Note about Linux DNS lookups:

If `/etc/nsswitch.conf` is configured to use the `mdns4_minimal` module,
`libnss-mdns` will reject the request if the request has more than two labels.
Example: example.default.local is rejected.
Example: `example.default.local` is rejected.

In order to resolve hostnames that are published from non-default Kubernetes
namespaces, modify `/etc/nsswitch.conf` and replace `mdns4_minimal` with `mdns4`.
Also, create the file `/etc/mdns.allow` and insert the following contents.
Also, create or modify `/etc/mdns.allow` and add the following contents.

```text
# /etc/mdns.allow
Expand Down
68 changes: 38 additions & 30 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,21 @@ func reverseAddress(addr string) (arpa string, err error) {
return string(buf), nil
}

var (
master = ""
namespace = ""
defaultNamespace = "default"
withoutNamespace = false
test = flag.Bool("test", false, "testing mode, no connection to k8s")
sourceFlag k8sSource
kubeconfig string
exposeIPv4 = true
exposeIPv6 = false
publishInternal = flag.Bool("publish-internal-services", false, "Publish DNS records for ClusterIP services (optional)")
recordTTL = 120
truncateLongRecords bool
)

func constructRecords(r resource.Resource) []string {
var records []string

Expand All @@ -135,29 +150,31 @@ func constructRecords(r resource.Resource) []string {

var recordType string
if ip.To4() != nil {
if exposeIPv4 == false {
if !exposeIPv4 {
continue
}
recordType = "A"
} else {
if exposeIPv6 == false {
if !exposeIPv6 {
continue
}
recordType = "AAAA"
}

// Publish records resources as <name>.<namespace>.local
// Ensure corresponding PTR records map to this hostname
records = append(records, fmt.Sprintf("%s.%s.local. %d IN %s %s", r.Name, r.Namespace, recordTTL, recordType, ip))
records = append(records, validatedRecord(r.Name, r.Namespace, recordTTL, recordType, ip, truncateLongRecords))

if reverseIP != "" {
records = append(records, fmt.Sprintf("%s %d IN PTR %s.%s.local.", reverseIP, recordTTL, r.Name, r.Namespace))
records = append(records, validatedPTRRecord(reverseIP, recordTTL, r.Name, r.Namespace, truncateLongRecords))
}

// Publish records resources as <name>-<namespace>.local
// Because Windows does not support subdomains resolution via mDNS and uses regular DNS query instead.
records = append(records, fmt.Sprintf("%s-%s.local. %d IN %s %s", r.Name, r.Namespace, recordTTL, recordType, ip))
records = append(records, validatedRecord(fmt.Sprintf("%s-%s", r.Name, r.Namespace), "", recordTTL, recordType, ip, truncateLongRecords))

if reverseIP != "" {
records = append(records, fmt.Sprintf("%s %d IN PTR %s-%s.local.", reverseIP, recordTTL, r.Name, r.Namespace))
records = append(records, validatedPTRRecord(reverseIP, recordTTL, fmt.Sprintf("%s-%s", r.Name, r.Namespace), "", truncateLongRecords))
}

// Publish services without the name in the namespace if any of the following
Expand All @@ -166,9 +183,10 @@ func constructRecords(r resource.Resource) []string {
// 2. The -without-namespace flag is equal to true
// 3. The record to be published is from an Ingress with a defined hostname
if r.Namespace == defaultNamespace || withoutNamespace || r.SourceType == "ingress" {
records = append(records, fmt.Sprintf("%s.local. %d IN %s %s", r.Name, recordTTL, recordType, ip))
records = append(records, validatedRecord(r.Name, "", recordTTL, recordType, ip, truncateLongRecords))

if reverseIP != "" {
records = append(records, fmt.Sprintf("%s %d IN PTR %s.local.", reverseIP, recordTTL, r.Name))
records = append(records, validatedPTRRecord(reverseIP, recordTTL, r.Name, "", truncateLongRecords))
}
}
}
Expand All @@ -178,30 +196,16 @@ func constructRecords(r resource.Resource) []string {

func publishRecord(rr string) {
if err := mdns.Publish(rr); err != nil {
log.Fatalf(`Unable to publish record "%s": %v`, rr, err)
log.Fatalf(`🔥 Failed to publish record "%s": %v`, rr, err)
}
}

func unpublishRecord(rr string) {
if err := mdns.UnPublish(rr); err != nil {
log.Fatalf(`Unable to publish record "%s": %v`, rr, err)
log.Fatalf(`🔥 Failed to unpublish record "%s": %v`, rr, err)
}
}

var (
master = ""
namespace = ""
defaultNamespace = "default"
withoutNamespace = false
test = flag.Bool("test", false, "testing mode, no connection to k8s")
sourceFlag k8sSource
kubeconfig string
exposeIPv4 = true
exposeIPv6 = false
publishInternal = flag.Bool("publish-internal-services", false, "Publish DNS records for ClusterIP services (optional)")
recordTTL = 120
)

func main() {

// Kubernetes options
Expand All @@ -216,6 +220,7 @@ func main() {
flag.BoolVar(&exposeIPv4, "expose-ipv4", lookupEnvOrBool("EXTERNAL_MDNS_EXPOSE_IPV4", exposeIPv4), "Publish A DNS entry (default: true)")
flag.BoolVar(&exposeIPv6, "expose-ipv6", lookupEnvOrBool("EXTERNAL_MDNS_EXPOSE_IPV6", exposeIPv6), "Publish AAAA DNS entry (default: false)")
flag.IntVar(&recordTTL, "record-ttl", lookupEnvOrInt("EXTERNAL_MDNS_RECORD_TTL", recordTTL), "DNS record time-to-live")
flag.BoolVar(&truncateLongRecords, "truncate-long-records", lookupEnvOrBool("EXTERNAL_MDNS_TRUNCATE_LONG_RECORDS", false), "Truncate long record names using SHA-256 hash (default: false)")
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thekoma What do you think about enabling this by default given that the current behavior violates section 2.3.1 of RFC 1035, DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION.


flag.Parse()

Expand All @@ -228,16 +233,16 @@ func main() {

// No sources provided.
if len(sourceFlag) == 0 {
fmt.Println("Specify at least once source to sync records from.")
log.Println("❌ Error: No sources specified. Please specify at least one source to sync records from")
os.Exit(1)
}

// Print parsed configuration
log.Printf("app.config %v\n", getConfig(flag.CommandLine))
log.Printf("🚀 Starting external-mdns with configuration:\n%v\n", getConfig(flag.CommandLine))

k8sClient, err := newK8sClient()
if err != nil {
log.Fatalln("Failed to create Kubernetes client:", err)
log.Fatalf("🔥 Failed to create Kubernetes client: %v", err)
}

notifyMdns := make(chan resource.Resource)
Expand All @@ -261,17 +266,20 @@ func main() {
select {
case advertiseResource := <-notifyMdns:
for _, record := range constructRecords(advertiseResource) {
if record == "" {
continue
}
switch advertiseResource.Action {
case resource.Added:
log.Printf("Added %s\n", record)
log.Printf("➕ Publishing new DNS record: %s\n", record)
publishRecord(record)
case resource.Deleted:
log.Printf("Remove %s\n", record)
log.Printf("➖ Removing DNS record: %s\n", record)
unpublishRecord(record)
}
}
case <-stopper:
fmt.Println("Stopping program")
log.Println("🛑 Stopping external-mdns")
}
}
}
Loading