mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-06 12:46:20 +02:00
cmd/k8s-operator/manifests,cmd/nameserver: add a basic nameserver
That can read hosts from a configmap and be deployed on kube Signed-off-by: irbekrm <irbekrm@gmail.com>
This commit is contained in:
parent
fe709c81e5
commit
47b105a4ff
8
Makefile
8
Makefile
@ -77,6 +77,14 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
|
||||
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
|
||||
TAGS=latest REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
|
||||
|
||||
publishdevnameserver: ## Build and publish nameserver to location specified by ${REPO}
|
||||
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
|
||||
@test "${REPO}" != "tailscale/nameserver" || (echo "REPO=... must not be tailscale/nameserver" && exit 1)
|
||||
@test "${REPO}" != "ghcr.io/tailscale/nameserver" || (echo "REPO=... must not be ghcr.io/tailscale/nameserver" && exit 1)
|
||||
TAGS=latest REPOS=${REPO} PUSH=true TARGET=nameserver ./build_docker.sh
|
||||
|
||||
help: ## Show this help
|
||||
@echo "\nSpecify a command. The choices are:\n"
|
||||
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
|
||||
|
||||
@ -67,6 +67,21 @@ case "$TARGET" in
|
||||
--push="${PUSH}" \
|
||||
/usr/local/bin/operator
|
||||
;;
|
||||
nameserver)
|
||||
DEFAULT_REPOS="tailscale/nameserver"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
go run github.com/tailscale/mkctr \
|
||||
--gopaths="tailscale.com/cmd/nameserver:/usr/local/bin/nameserver" \
|
||||
--ldflags=" \
|
||||
-X tailscale.com/version.longStamp=${VERSION_LONG} \
|
||||
-X tailscale.com/version.shortStamp=${VERSION_SHORT} \
|
||||
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}" \
|
||||
/usr/local/bin/nameserver
|
||||
;;
|
||||
*)
|
||||
echo "unknown target: $TARGET"
|
||||
exit 1
|
||||
|
||||
72
cmd/k8s-operator/manifests/nameserver.yaml
Normal file
72
cmd/k8s-operator/manifests/nameserver.yaml
Normal file
@ -0,0 +1,72 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: nameserver
|
||||
namespace: tailscale
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nameserver
|
||||
namespace: tailscale
|
||||
spec:
|
||||
replicas: 1
|
||||
revisionHistoryLimit: 10
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nameserver
|
||||
strategy:
|
||||
type: Recreate # TODO: is this the right strategy?
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nameserver
|
||||
spec:
|
||||
containers:
|
||||
- image: tailscale/nameserver:unstable # TODO: actually publish this image
|
||||
imagePullPolicy: IfNotPresent
|
||||
name: nameserver
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
ports:
|
||||
- name: udp
|
||||
protocol: UDP
|
||||
containerPort: 53
|
||||
volumeMounts:
|
||||
- name: dnsconfig
|
||||
mountPath: /config
|
||||
restartPolicy: Always
|
||||
schedulerName: default-scheduler
|
||||
serviceAccount: nameserver
|
||||
serviceAccountName: nameserver
|
||||
terminationGracePeriodSeconds: 30
|
||||
volumes:
|
||||
- name: dnsconfig
|
||||
configMap:
|
||||
name: dnsconfig
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nameserver
|
||||
namespace: tailscale
|
||||
spec:
|
||||
selector:
|
||||
app: nameserver
|
||||
ports:
|
||||
- name: udp
|
||||
targetPort: 53
|
||||
port: 53
|
||||
protocol: UDP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: dnsconfig
|
||||
namespace: tailscale
|
||||
labels:
|
||||
app.kubernetes.io/name: tailscale
|
||||
app.kubernetes.io/component: nameserver
|
||||
data:
|
||||
dns.json: |
|
||||
{}
|
||||
171
cmd/nameserver/main.go
Normal file
171
cmd/nameserver/main.go
Normal file
@ -0,0 +1,171 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDNSConfigDir = "/config"
|
||||
defaultDNSFile = "dns.json"
|
||||
)
|
||||
|
||||
type nameserver struct {
|
||||
// config file holds FQDN to IP address mappings
|
||||
configFilePath string
|
||||
res resolver.Resolver
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger := log.Printf
|
||||
|
||||
res := resolver.New(logger, nil, nil, &tsdial.Dialer{Logf: logger})
|
||||
|
||||
ns := &nameserver{
|
||||
configFilePath: fmt.Sprintf("%s/%s", defaultDNSConfigDir, defaultDNSFile),
|
||||
logf: logger,
|
||||
res: *res, // TODO (irbekrm): linter error here
|
||||
}
|
||||
|
||||
// ensure resolver config is updated before starting to serve
|
||||
err := ns.updateResolverConfig()
|
||||
if err != nil {
|
||||
logger("error updating resolver conf: %v", err)
|
||||
panic(err)
|
||||
}
|
||||
logger("Hosts configured")
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logger("error starting file watcher: %v", err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
go func() {
|
||||
logger("starting DNS file watch")
|
||||
for {
|
||||
logger("in DNS file watch...")
|
||||
select {
|
||||
// TODO (irbekrm): it appears like we get a whole bunch
|
||||
// of different events (except for the WRITE one that
|
||||
// fsnotify recommends watching) on an update. They also
|
||||
// come with a delay. Figure out if we can add a
|
||||
// reliable filter to only trigger update once and
|
||||
// whether we can speed this up a bit.
|
||||
case event, ok := <-watcher.Events:
|
||||
if !ok {
|
||||
logger("watcher finished")
|
||||
return
|
||||
}
|
||||
// it seems like an update to the file has a
|
||||
// potential to produce different types of
|
||||
// events, but only one per configmap update.
|
||||
// fsnotify docs suggest to only react to Write
|
||||
// events, however we should be safe to react to
|
||||
// other events too as we run in a container and
|
||||
// there shouldn't be random changes to file
|
||||
// metadata
|
||||
logger("a %v event detected, updating DNS config...", event)
|
||||
// resolver locks config on updates so this is safe
|
||||
err := ns.updateResolverConfig()
|
||||
if err != nil {
|
||||
logger("error updating resolver conf: %v", err)
|
||||
continue
|
||||
}
|
||||
logger("Hosts updated")
|
||||
case err, ok := <-watcher.Errors:
|
||||
if !ok {
|
||||
logger("watcher finished")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logger("error watching DNS config: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
err = watcher.Add(defaultDNSConfigDir)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error setting up DNS config watch: %v", err))
|
||||
}
|
||||
|
||||
addr, err := net.ResolveUDPAddr("udp", ":53")
|
||||
if err != nil {
|
||||
panic("error resolving UDP address")
|
||||
}
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error opening udp connection: %v", err))
|
||||
}
|
||||
defer conn.Close()
|
||||
logger("nameserver listening on: %v", addr)
|
||||
|
||||
for {
|
||||
payloadBuff := make([]byte, 10000)
|
||||
metadataBuff := make([]byte, 512)
|
||||
_, _, _, addr, err := conn.ReadMsgUDP(payloadBuff, metadataBuff)
|
||||
if err != nil {
|
||||
logger("error reading from UDP socket: %v", err)
|
||||
continue
|
||||
}
|
||||
dnsAnswer, err := ns.res.Query(context.Background(), payloadBuff, addr.AddrPort())
|
||||
if err != nil {
|
||||
logger("error doing DNS query: %v", err)
|
||||
// reply with the dnsAnswer anyway- in some cases
|
||||
// resolver might have written some useful data there
|
||||
}
|
||||
conn.WriteToUDP(dnsAnswer, addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *nameserver) updateResolverConfig() error {
|
||||
// file is mounted to pod from a configmap so it cannot not exist
|
||||
dnsCfgBytes, err := os.ReadFile(n.configFilePath)
|
||||
if err != nil {
|
||||
n.logf("error reading configFile: %v", err)
|
||||
return err
|
||||
}
|
||||
dnsCfgM := make(map[string]string)
|
||||
err = json.Unmarshal(dnsCfgBytes, &dnsCfgM)
|
||||
if err != nil {
|
||||
n.logf("error unmarshaling json: %v", err)
|
||||
return err
|
||||
}
|
||||
c := resolver.Config{}
|
||||
c.Hosts = make(map[dnsname.FQDN][]netip.Addr)
|
||||
// TODO (irbekrm): ensure that it handles the case of empty configmap
|
||||
for key, val := range dnsCfgM {
|
||||
fqdn, err := dnsname.ToFQDN(key)
|
||||
if err != nil {
|
||||
n.logf("invalid DNS config: cannot convert %s to FQDN: %v", key, err)
|
||||
return err
|
||||
}
|
||||
ip, err := netip.ParseAddr(val)
|
||||
if err != nil {
|
||||
n.logf("invalid DNS config: cannot convert %s to netip.Addr: %v", val, err)
|
||||
return err
|
||||
}
|
||||
c.Hosts[fqdn] = []netip.Addr{ip}
|
||||
}
|
||||
// resolver will lock config so this is safe
|
||||
n.res.SetConfig(c)
|
||||
|
||||
// TODO (irbekrm): get a diff and log when/if resolver config is actually being changed
|
||||
|
||||
return nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user