Skip to content

Commit 1e9ca82

Browse files
committed
v0.2.0: hairpin-proxy-etchosts-controller DaemonSet for /etc/hosts rewrites
1 parent 588a561 commit 1e9ca82

File tree

4 files changed

+117
-6
lines changed

4 files changed

+117
-6
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ If you've had problems with ingress-nginx, cert-manager, LetsEncrypt ACME HTTP01
77
## One-line install
88

99
```shell
10-
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.1.2/deploy.yml
10+
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.2.0/deploy.yml
1111
```
1212

1313
If you're using [ingress-nginx](https://kubernetes.github.io/ingress-nginx/) and [cert-manager](https://github.com/jetstack/cert-manager), it will work out of the box. See detailed installation and testing instructions below.
@@ -72,7 +72,7 @@ The `dig` should show the external load balancer IP address. The first `curl` sh
7272
### Step 1: Install hairpin-proxy in your Kubernetes cluster
7373

7474
```shell
75-
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.1.2/deploy.yml
75+
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.2.0/deploy.yml
7676
```
7777

7878
If you're using `ingress-nginx`, this will work as-is.
@@ -119,3 +119,15 @@ This time, the first `dig` should show an internal service IP address (generally
119119
NOTE: CoreDNS is a cache, so even if you see the `rewrite` rules in Step 2, it will take another minute or two before the queries resolve correctly. Be patient. You may wish to `watch -n 1 dig subdomain.example.com` to see when this changeover happens.
120120

121121
At this point, cert-manager's self-check will pass, and you'll get valid LetsEncrypt certificates within a few minutes.
122+
123+
### Step 4: (Optional) Install hairpin-proxy-etchosts-controller DaemonSet
124+
125+
Note that the CoreDNS rewrites above only cover access within containers, while the iptables rewrite applies to the Node itself. This mismatch causes a problem if your node itself needs to access something behind your ingress. An example is if you're hosting your own container registry with [trow](https://github.com/ContainerSolutions/trow) and it's behind the ingress. If you follow only steps 1-3 above, you'll experience image pull failures because the Docker daemon (running on the Node directly, not in a container) can't access your registry.
126+
127+
To resolve this, we need to rewrite the DNS on the Node itself. The Node does not use CoreDNS, so we can instead rewrite `/etc/hosts` to point to the IP address of the `hairpin-proxy-haproxy` service. This runs as a DaemonSet, so that it can modify each Node's copy of `/etc/hosts`.
128+
129+
To install this DaemonSet:
130+
131+
```shell
132+
kubectl apply -f https://raw.githubusercontent.com/compumike/hairpin-proxy/v0.2.0/deploy-etchosts-daemonset.yml
133+
```

deploy-etchosts-daemonset.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
apiVersion: apps/v1
2+
kind: DaemonSet
3+
metadata:
4+
labels:
5+
app: hairpin-proxy-etchosts-controller
6+
name: hairpin-proxy-etchosts-controller
7+
namespace: hairpin-proxy
8+
spec:
9+
selector:
10+
matchLabels:
11+
app: hairpin-proxy-etchosts-controller
12+
template:
13+
metadata:
14+
labels:
15+
app: hairpin-proxy-etchosts-controller
16+
spec:
17+
serviceAccountName: hairpin-proxy-controller-sa
18+
containers:
19+
- image: compumike/hairpin-proxy-controller:0.2.0
20+
name: main
21+
command: ["/app/src/main.rb", "--etc-hosts", "/app/etchosts"]
22+
volumeMounts:
23+
- name: etchosts
24+
mountPath: /app/etchosts
25+
resources:
26+
requests:
27+
memory: "50Mi"
28+
cpu: "10m"
29+
limits:
30+
memory: "100Mi"
31+
cpu: "50m"
32+
volumes:
33+
- name: etchosts
34+
hostPath:
35+
path: /etc/hosts
36+
type: File

deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ spec:
2323
app: hairpin-proxy-haproxy
2424
spec:
2525
containers:
26-
- image: compumike/hairpin-proxy-haproxy:0.1.2
26+
- image: compumike/hairpin-proxy-haproxy:0.2.0
2727
name: main
2828
resources:
2929
requests:
@@ -151,7 +151,7 @@ spec:
151151
runAsUser: 405
152152
runAsGroup: 65533
153153
containers:
154-
- image: compumike/hairpin-proxy-controller:0.1.2
154+
- image: compumike/hairpin-proxy-controller:0.2.0
155155
name: main
156156
resources:
157157
requests:

hairpin-proxy-controller/src/main.rb

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
require "k8s-ruby"
55
require "logger"
6+
require "optparse"
7+
require "socket"
68

79
class HairpinProxyController
810
COMMENT_LINE_SUFFIX = "# Added by hairpin-proxy"
@@ -32,7 +34,9 @@ def fetch_ingress_hosts
3234
end
3335
}.flatten
3436
all_tls_blocks = all_ingresses.map { |r| r.spec.tls }.flatten.compact
35-
all_tls_blocks.map(&:hosts).flatten.compact.sort.uniq
37+
hosts = all_tls_blocks.map(&:hosts).flatten.compact
38+
hosts.filter! { |host| /\A[A-Za-z0-9.\-_]+\z/.match?(host) }
39+
hosts.sort.uniq
3640
end
3741

3842
def coredns_corefile_with_rewrite_rules(original_corefile, hosts)
@@ -68,10 +72,69 @@ def check_and_rewrite_coredns
6872
end
6973
end
7074

75+
def dns_rewrite_destination_ip_address
76+
Addrinfo.ip(DNS_REWRITE_DESTINATION).ip_address
77+
end
78+
79+
def etchosts_with_rewrite_rules(original_etchosts, hosts)
80+
# Returns a String represeting the original /etc/hosts file, modified to include a rule for
81+
# mapping *hosts to dns_rewrite_destination_ip_address. This handles kubelet and the node's Docker engine,
82+
# which does not go through CoreDNS.
83+
# This is an idempotent transformation because our rewrites are labeled with COMMENT_LINE_SUFFIX.
84+
85+
# Extract base configuration, without our hairpin-proxy rewrites
86+
our_lines, original_lines = original_etchosts.strip.split("\n").partition { |line| line.strip.end_with?(COMMENT_LINE_SUFFIX) }
87+
88+
ip = dns_rewrite_destination_ip_address
89+
hostlist = hosts.join(" ")
90+
new_rewrite_line = "#{ip}\t#{hostlist} #{COMMENT_LINE_SUFFIX}"
91+
92+
if our_lines == [new_rewrite_line]
93+
# Return early so that we're indifferent to the ordering of /etc/hosts lines.
94+
return original_etchosts
95+
end
96+
97+
(original_lines + [new_rewrite_line]).join("\n") + "\n"
98+
end
99+
100+
def check_and_rewrite_etchosts(etchosts_path)
101+
@log.info("Polling all Ingress resources and etchosts file at #{etchosts_path}...")
102+
hosts = fetch_ingress_hosts
103+
104+
old_etchostsfile = File.read(etchosts_path)
105+
new_etchostsfile = etchosts_with_rewrite_rules(old_etchostsfile, hosts)
106+
107+
if old_etchostsfile.strip != new_etchostsfile.strip
108+
@log.info("/etc/hosts has changed! New contents:\n#{new_etchostsfile}\nWriting to #{etchosts_path}...")
109+
File.write(etchosts_path, new_etchostsfile)
110+
end
111+
end
112+
71113
def main_loop
114+
etchosts_path = nil
115+
116+
OptionParser.new { |opts|
117+
opts.on("--etc-hosts ETCHOSTSPATH", "Path to writable /etc/hosts file") do |h|
118+
etchosts_path = h
119+
raise "File #{etchosts_path} doesn't exist!" unless File.exist?(etchosts_path)
120+
raise "File #{etchosts_path} isn't writable!" unless File.writable?(etchosts_path)
121+
end
122+
}.parse!
123+
124+
if etchosts_path && etchosts_path != ""
125+
@log.info("Starting in /etc/hosts mutation mode on #{etchosts_path}. (Intended to be run as a DaemonSet: one instance per Node.)")
126+
else
127+
etchosts_path = nil
128+
@log.info("Starting in CoreDNS mode. (Indended to be run as a Deployment: one instance per cluster.)")
129+
end
130+
72131
@log.info("Starting main_loop with #{POLL_INTERVAL}s polling interval.")
73132
loop do
74-
check_and_rewrite_coredns
133+
if etchosts_path.nil?
134+
check_and_rewrite_coredns
135+
else
136+
check_and_rewrite_etchosts(etchosts_path)
137+
end
75138

76139
sleep(POLL_INTERVAL)
77140
end

0 commit comments

Comments
 (0)