@@ -105,20 +105,31 @@ func (bl *BlockedLogger) Close() error {
105105 return bl .file .Close ()
106106}
107107
108- // ClientFilter validates client source IPs against an allowlist of IPs and CIDRs.
109- // A nil or empty ClientFilter allows all clients.
108+ // ClientFilter validates client source IPs against an allowlist of IPs, CIDRs,
109+ // and DNS hostnames. Hostnames are resolved to IPs at startup and periodically
110+ // re-resolved in the background (every 30s) to handle dynamic IP assignments
111+ // (e.g., Kubernetes pods restarting). A nil or empty ClientFilter allows all clients.
110112type ClientFilter struct {
111- ips []net.IP
112- nets []* net.IPNet
113+ ips []net.IP
114+ nets []* net.IPNet
115+ hostnames []string
116+ resolved map [string ][]net.IP // hostname -> resolved IPs (protected by mu)
117+ mu sync.RWMutex
118+ stopCh chan struct {}
119+ stopOnce sync.Once
113120}
114121
115- // NewClientFilter parses a comma-separated list of IPs and CIDRs.
116- // Returns nil if the input is empty (allow all).
122+ // NewClientFilter parses a comma-separated list of IPs, CIDRs, and DNS hostnames.
123+ // Returns nil if the input is empty (allow all). Hostnames are resolved immediately;
124+ // resolution failures are logged as warnings (the hostname may become resolvable later).
117125func NewClientFilter (s string ) (* ClientFilter , error ) {
118126 if s == "" {
119127 return nil , nil
120128 }
121- cf := & ClientFilter {}
129+ cf := & ClientFilter {
130+ stopCh : make (chan struct {}),
131+ resolved : make (map [string ][]net.IP ),
132+ }
122133 for _ , part := range strings .Split (s , "," ) {
123134 part = strings .TrimSpace (part )
124135 if part == "" {
@@ -130,18 +141,98 @@ func NewClientFilter(s string) (*ClientFilter, error) {
130141 return nil , fmt .Errorf ("invalid CIDR %q: %w" , part , err )
131142 }
132143 cf .nets = append (cf .nets , ipNet )
133- } else {
134- ip := net .ParseIP (part )
135- if ip == nil {
136- return nil , fmt .Errorf ("invalid IP %q" , part )
137- }
144+ } else if ip := net .ParseIP (part ); ip != nil {
138145 cf .ips = append (cf .ips , ip )
146+ } else {
147+ cf .hostnames = append (cf .hostnames , part )
139148 }
140149 }
150+
151+ if len (cf .hostnames ) > 0 {
152+ cf .resolveHostnames (true )
153+ }
154+
141155 return cf , nil
142156}
143157
144- // IsAllowed returns true if the given IP is in the allowlist.
158+ // resolveHostnames resolves all configured hostnames and updates the resolved IP map.
159+ // When initialResolve is true, all results are logged. Otherwise, only changes are logged.
160+ func (cf * ClientFilter ) resolveHostnames (initialResolve bool ) {
161+ newResolved := make (map [string ][]net.IP , len (cf .hostnames ))
162+ for _ , hostname := range cf .hostnames {
163+ addrs , err := net .LookupHost (hostname )
164+ if err != nil {
165+ log .Printf ("WARNING: failed to resolve allowed client hostname %q: %v" , hostname , err )
166+ continue
167+ }
168+ var ips []net.IP
169+ for _ , addr := range addrs {
170+ if ip := net .ParseIP (addr ); ip != nil {
171+ ips = append (ips , ip )
172+ }
173+ }
174+ newResolved [hostname ] = ips
175+ }
176+
177+ cf .mu .Lock ()
178+ old := cf .resolved
179+ cf .resolved = newResolved
180+ cf .mu .Unlock ()
181+
182+ for _ , hostname := range cf .hostnames {
183+ newIPs := newResolved [hostname ]
184+ oldIPs := old [hostname ]
185+ if initialResolve || ! ipsEqual (newIPs , oldIPs ) {
186+ ipStrs := make ([]string , len (newIPs ))
187+ for i , ip := range newIPs {
188+ ipStrs [i ] = ip .String ()
189+ }
190+ log .Printf ("Resolved allowed client hostname %q -> %s" , hostname , strings .Join (ipStrs , ", " ))
191+ }
192+ }
193+ }
194+
195+ // ipsEqual returns true if two IP slices contain the same IPs in the same order.
196+ func ipsEqual (a , b []net.IP ) bool {
197+ if len (a ) != len (b ) {
198+ return false
199+ }
200+ for i := range a {
201+ if ! a [i ].Equal (b [i ]) {
202+ return false
203+ }
204+ }
205+ return true
206+ }
207+
208+ // StartResolving starts a background goroutine that re-resolves all hostname
209+ // entries every 30 seconds to handle pods restarting with new IPs.
210+ func (cf * ClientFilter ) StartResolving () {
211+ if cf == nil || len (cf .hostnames ) == 0 {
212+ return
213+ }
214+ go func () {
215+ ticker := time .NewTicker (30 * time .Second )
216+ defer ticker .Stop ()
217+ for {
218+ select {
219+ case <- ticker .C :
220+ cf .resolveHostnames (false )
221+ case <- cf .stopCh :
222+ return
223+ }
224+ }
225+ }()
226+ }
227+
228+ // Stop stops the background hostname re-resolution goroutine. Safe to call multiple times.
229+ func (cf * ClientFilter ) Stop () {
230+ if cf == nil || len (cf .hostnames ) == 0 {
231+ return
232+ }
233+ cf .stopOnce .Do (func () { close (cf .stopCh ) })
234+ }
235+
145236func (cf * ClientFilter ) IsAllowed (ip net.IP ) bool {
146237 if cf == nil {
147238 return true
@@ -156,10 +247,19 @@ func (cf *ClientFilter) IsAllowed(ip net.IP) bool {
156247 return true
157248 }
158249 }
250+ cf .mu .RLock ()
251+ resolved := cf .resolved
252+ cf .mu .RUnlock ()
253+ for _ , ips := range resolved {
254+ for _ , allowed := range ips {
255+ if allowed .Equal (ip ) {
256+ return true
257+ }
258+ }
259+ }
159260 return false
160261}
161262
162- // String returns a human-readable representation of the filter.
163263func (cf * ClientFilter ) String () string {
164264 if cf == nil {
165265 return "disabled (all clients allowed)"
@@ -171,6 +271,22 @@ func (cf *ClientFilter) String() string {
171271 for _ , ipNet := range cf .nets {
172272 parts = append (parts , ipNet .String ())
173273 }
274+ if len (cf .hostnames ) > 0 {
275+ cf .mu .RLock ()
276+ resolved := cf .resolved
277+ cf .mu .RUnlock ()
278+ for _ , hostname := range cf .hostnames {
279+ if ips , ok := resolved [hostname ]; ok && len (ips ) > 0 {
280+ ipStrs := make ([]string , len (ips ))
281+ for i , ip := range ips {
282+ ipStrs [i ] = ip .String ()
283+ }
284+ parts = append (parts , fmt .Sprintf ("%s (resolved: %s)" , hostname , strings .Join (ipStrs , ", " )))
285+ } else {
286+ parts = append (parts , fmt .Sprintf ("%s (unresolved)" , hostname ))
287+ }
288+ }
289+ }
174290 return strings .Join (parts , ", " )
175291}
176292
0 commit comments