001/*
002 * Copyright 2015-2024 the original author or authors
003 *
004 * This software is licensed under the Apache License, Version 2.0,
005 * the GNU Lesser General Public License version 2 or later ("LGPL")
006 * and the WTFPL.
007 * You may choose either license to govern your use of this software only
008 * upon the condition that you accept all of the terms of either
009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
010 */
011package org.minidns.iterative;
012
013import java.io.IOException;
014import java.util.ArrayList;
015import java.util.List;
016import java.util.logging.Level;
017
018import org.minidns.AbstractDnsClient;
019import org.minidns.DnsCache;
020import org.minidns.DnsClient;
021import org.minidns.dnsmessage.DnsMessage;
022import org.minidns.dnsmessage.Question;
023import org.minidns.dnsqueryresult.DnsQueryResult;
024import org.minidns.source.DnsDataSource;
025import org.minidns.util.MultipleIoException;
026
027/**
028 * A DNS client using a reliable strategy. First the configured resolver of the
029 * system are used, then, in case there is no answer, a fall back to iterative
030 * resolving is performed.
031 */
032public class ReliableDnsClient extends AbstractDnsClient {
033
034    public enum Mode {
035        /**
036         * Try the recursive servers first and fallback to iterative resolving if it fails. This is the default mode.
037         */
038        recursiveWithIterativeFallback,
039
040        /**
041         * Only try the recursive servers. This makes {@code ReliableDnsClient} behave like a {@link DnsClient}.
042         */
043        recursiveOnly,
044
045        /**
046         * Only use iterative resolving.  This makes {@code ReliableDnsClient} behave like a {@link IterativeDnsClient}.
047         */
048        iterativeOnly,
049    }
050
051    private final IterativeDnsClient recursiveDnsClient;
052    private final DnsClient dnsClient;
053
054    private Mode mode = Mode.recursiveWithIterativeFallback;
055
056    public ReliableDnsClient(DnsCache dnsCache) {
057        super(dnsCache);
058        recursiveDnsClient = new IterativeDnsClient(dnsCache) {
059            @Override
060            protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
061                questionMessage = super.newQuestion(questionMessage);
062                return ReliableDnsClient.this.newQuestion(questionMessage);
063            }
064            // TODO: Rename dnsMessage to result.
065            @Override
066            protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) {
067                boolean res = super.isResponseCacheable(q, dnsMessage);
068                return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
069            }
070        };
071        dnsClient = new DnsClient(dnsCache) {
072            @Override
073            protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
074                questionMessage = super.newQuestion(questionMessage);
075                return ReliableDnsClient.this.newQuestion(questionMessage);
076            }
077            // TODO: Rename dnsMessage to result.
078            @Override
079            protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) {
080                boolean res = super.isResponseCacheable(q, dnsMessage);
081                return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
082            }
083        };
084    }
085
086    public ReliableDnsClient() {
087        this(DEFAULT_CACHE);
088    }
089
090    @Override
091    protected DnsQueryResult query(DnsMessage.Builder q) throws IOException {
092        DnsQueryResult dnsMessage = null;
093        String unacceptableReason = null;
094        List<IOException> ioExceptions = new ArrayList<>();
095
096        if (mode != Mode.iterativeOnly) {
097            // Try a recursive query.
098            try {
099                dnsMessage = dnsClient.query(q);
100                if (dnsMessage != null) {
101                    unacceptableReason = isResponseAcceptable(dnsMessage.response);
102                    if (unacceptableReason == null) {
103                        return dnsMessage;
104                    }
105                }
106            } catch (IOException ioException) {
107                ioExceptions.add(ioException);
108            }
109        }
110
111        // Abort if we the are in "recursive only" mode.
112        if (mode == Mode.recursiveOnly) return dnsMessage;
113
114        // Eventually log that we fall back to iterative mode.
115        final Level FALLBACK_LOG_LEVEL = Level.FINE;
116        if (LOGGER.isLoggable(FALLBACK_LOG_LEVEL) && mode != Mode.iterativeOnly) {
117            String logString = "Resolution fall back to iterative mode because: ";
118            if (!ioExceptions.isEmpty()) {
119                logString += ioExceptions.get(0);
120            } else if (dnsMessage == null) {
121                logString += " DnsClient did not return a response";
122            } else if (unacceptableReason != null) {
123                logString += unacceptableReason + ". Response:\n" + dnsMessage;
124            } else {
125                throw new AssertionError("This should never been reached");
126            }
127            LOGGER.log(FALLBACK_LOG_LEVEL, logString);
128        }
129
130        try {
131            dnsMessage = recursiveDnsClient.query(q);
132            assert dnsMessage != null;
133        } catch (IOException ioException) {
134            ioExceptions.add(ioException);
135        }
136
137        if (dnsMessage == null) {
138            assert !ioExceptions.isEmpty();
139            MultipleIoException.throwIfRequired(ioExceptions);
140        }
141
142        return dnsMessage;
143    }
144
145    @Override
146    protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
147        return questionMessage;
148    }
149
150    @Override
151    protected boolean isResponseCacheable(Question q, DnsQueryResult result) {
152        return isResponseAcceptable(result.response) == null;
153    }
154
155    /**
156     * Check if the response from the system's nameserver is acceptable. Must return <code>null</code> if the response
157     * is acceptable, or a String describing why it is not acceptable. If the response is not acceptable then
158     * {@link ReliableDnsClient} will fall back to resolve the query iteratively.
159     *
160     * @param response the response we got from the system's nameserver.
161     * @return <code>null</code> if the response is acceptable, or a String if not.
162     */
163    protected String isResponseAcceptable(DnsMessage response) {
164        return null;
165    }
166
167    @Override
168    public void setDataSource(DnsDataSource dataSource) {
169        super.setDataSource(dataSource);
170        recursiveDnsClient.setDataSource(dataSource);
171        dnsClient.setDataSource(dataSource);
172    }
173
174    /**
175     * Set the mode used when resolving queries.
176     *
177     * @param mode the mode to use.
178     */
179    public void setMode(Mode mode) {
180        if (mode == null) {
181            throw new IllegalArgumentException("Mode must not be null.");
182        }
183        this.mode = mode;
184    }
185
186    public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) {
187        dnsClient.setUseHardcodedDnsServers(useHardcodedDnsServers);
188    }
189
190}