package org.shadowsocks; import java.io.IOException; import java.io.InputStream; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.net.URLEncoder; import java.net.UnknownHostException; import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Random; import org.shadowsocks.db.DNSResponse; import org.shadowsocks.db.DatabaseHelper; import android.content.Context; import android.util.Log; import com.j256.ormlite.android.apptools.OpenHelperManager; import com.j256.ormlite.dao.Dao; import com.loopj.android.http.AsyncHttpClient; import com.loopj.android.http.AsyncHttpResponseHandler; /** * 此类实现了DNS代理 * * @author biaji */ public class DNSServer implements Runnable { public static byte[] int2byte(int res) { byte[] targets = new byte[4]; targets[0] = (byte) (res & 0xff);// 最低位 targets[1] = (byte) ((res >> 8) & 0xff);// 次低位 targets[2] = (byte) ((res >> 16) & 0xff);// 次高位 targets[3] = (byte) (res >>> 24);// 最高位,无符号右移。 return targets; } private final String TAG = "ShadowsocksDNSProxy"; private DatagramSocket srvSocket; public HashSet<String> domains; private int srvPort = 8153; final protected int DNS_PKG_HEADER_LEN = 12; final private int[] DNS_HEADERS = { 0, 0, 0x81, 0x80, 0, 0, 0, 0, 0, 0, 0, 0 }; final private int[] DNS_PAYLOAD = { 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x3c, 0x00, 0x04 }; final private int IP_SECTION_LEN = 4; private boolean inService = false; /** * 内建自定义缓存 */ private Hashtable<String, String> orgCache = new Hashtable<String, String>(); private String appHost = "127.0.0.1"; private static final String CANT_RESOLVE = "Error"; private DatabaseHelper helper; private String port; private final static AsyncHttpClient client = new AsyncHttpClient(); public DNSServer(Context ctx, String appHost, String port) { this.appHost = appHost; this.port = port; client.setTimeout(6 * 1000); domains = new HashSet<String>(); OpenHelperManager.setOpenHelperClass(DatabaseHelper.class); if (helper == null) { helper = OpenHelperManager.getHelper(ctx, DatabaseHelper.class); } try { srvSocket = new DatagramSocket(0, InetAddress.getByName("127.0.0.1")); srvPort = srvSocket.getLocalPort(); Log.d(TAG, "start at port " + srvPort); inService = true; } catch (SocketException e) { Log.e(TAG, "error to initilized at port " + srvPort, e); } catch (UnknownHostException e) { Log.e(TAG, "error to initilized at port " + srvPort, e); } } /** * 在缓存中添加一个域名解析 * * @param questDomainName 域名 * @param answer 解析结果 */ private synchronized void addToCache(String questDomainName, byte[] answer) { DNSResponse response = new DNSResponse(questDomainName); response.setAddress(DNSResponse.getIPString(answer)); try { Dao<DNSResponse, String> dnsCacheDao = helper.getDNSCacheDao(); dnsCacheDao.createOrUpdate(response); } catch (Exception e) { Log.e(TAG, "Cannot open DAO", e); } } public void close() throws IOException { inService = false; srvSocket.close(); if (helper != null) { OpenHelperManager.releaseHelper(); helper = null; } Log.i(TAG, "DNS Proxy closed"); } /* * Create a DNS response packet, which will send back to application. * @author yanghong Reference to: Mini Fake DNS server (Python) * http://code.activestate.com/recipes/491264-mini-fake-dns-server/ DOMAIN * NAMES - IMPLEMENTATION AND SPECIFICATION * http://www.ietf.org/rfc/rfc1035.txt */ protected byte[] createDNSResponse(byte[] quest, byte[] ips) { byte[] response = null; int start = 0; response = new byte[128]; for (int val : DNS_HEADERS) { response[start] = (byte) val; start++; } System.arraycopy(quest, 0, response, 0, 2); /* 0:2 | NAME */ System.arraycopy(quest, 4, response, 4, 2); /* 4:6 -> 4:6 | TYPE */ System.arraycopy(quest, 4, response, 6, 2); /* 4:6 -> 7:9 | CLASS */ /* 10:14 | TTL */ System.arraycopy(quest, DNS_PKG_HEADER_LEN, response, start, quest.length - DNS_PKG_HEADER_LEN); /* 12:~ -> 15:~ */ start += quest.length - DNS_PKG_HEADER_LEN; for (int val : DNS_PAYLOAD) { response[start] = (byte) val; start++; } /* IP address in response */ for (byte ip : ips) { response[start] = ip; start++; } byte[] result = new byte[start]; System.arraycopy(response, 0, result, 0, start); return result; } public void fetchAnswerHTTP(final DatagramPacket dnsq, final byte[] quest) { final String domain = getRequestDomain(quest); DomainValidator dv = DomainValidator.getInstance(); /* Not support reverse domain name query */ if (domain.endsWith("ip6.arpa") || domain.endsWith("in-addr.arpa") || !dv.isValid(domain)) { final byte[] answer = createDNSResponse(quest, parseIPString("127.0.0.1")); addToCache(domain, answer); sendDns(answer, dnsq, srvSocket); synchronized (domains) { domains.remove(domain); } return; } final long startTime = System.currentTimeMillis(); AsyncHttpResponseHandler handler = new AsyncHttpResponseHandler() { @Override public void onFinish() { synchronized (domains) { domains.remove(domain); } } @Override public void onSuccess(String response) { try { byte[] answer = null; if (response == null) { Log.e(TAG, "Failed to resolve domain name: " + domain); return; } response = response.trim(); if (response.equals(CANT_RESOLVE)) { Log.e(TAG, "Cannot resolve domain name: " + domain); return; } byte[] ips = parseIPString(response); if (ips != null) { answer = createDNSResponse(quest, ips); } if (answer != null && answer.length != 0) { addToCache(domain, answer); sendDns(answer, dnsq, srvSocket); Log.d(TAG, "Success to resolve: " + domain + " length: " + answer.length + " cost: " + (System.currentTimeMillis() - startTime) / 1000 + "s"); } else { Log.e(TAG, "The size of DNS packet returned is 0"); } } catch (Exception e) { // Nothing } } }; resolveDomainName(domain, handler); } /** * 获取UDP DNS请求的域名 * * @param request dns udp包 * @return 请求的域名 */ protected String getRequestDomain(byte[] request) { String requestDomain = ""; int reqLength = request.length; if (reqLength > 13) { // 包含包体 byte[] question = new byte[reqLength - 12]; System.arraycopy(request, 12, question, 0, reqLength - 12); requestDomain = parseDomain(question); if (requestDomain.length() > 1) requestDomain = requestDomain.substring(0, requestDomain.length() - 1); } return requestDomain; } public int getServPort() { return this.srvPort; } public boolean isClosed() { return srvSocket.isClosed(); } public boolean isInService() { return inService; } /** * 由缓存载入域名解析缓存 */ private void loadCache() { try { Dao<DNSResponse, String> dnsCacheDao = helper.getDNSCacheDao(); List<DNSResponse> list = dnsCacheDao.queryForAll(); for (DNSResponse resp : list) { // expire after 10 days if ((System.currentTimeMillis() - resp.getTimestamp()) > 864000000L) { Log.d(TAG, "deleted: " + resp.getRequest()); dnsCacheDao.delete(resp); } } } catch (Exception e) { Log.e(TAG, "Cannot open DAO", e); } } /** * 解析域名 * * @param request * @return */ private String parseDomain(byte[] request) { String result = ""; int length = request.length; int partLength = request[0]; if (partLength == 0) return result; try { byte[] left = new byte[length - partLength - 1]; System.arraycopy(request, partLength + 1, left, 0, length - partLength - 1); result = new String(request, 1, partLength) + "."; result += parseDomain(left); } catch (Exception e) { Log.e(TAG, e.getLocalizedMessage()); } return result; } /* * Parse IP string into byte, do validation. * @param ip IP string * @return IP in byte array */ protected byte[] parseIPString(String ip) { byte[] result = null; int value; int i = 0; String[] ips = null; ips = ip.split("\\."); // Log.d(TAG, "Start parse ip string: " + ip + ", Sectons: " + // ips.length); if (ips.length != IP_SECTION_LEN) { Log.e(TAG, "Malformed IP string : " + ip); return null; } result = new byte[IP_SECTION_LEN]; for (String section : ips) { try { value = Integer.parseInt(section); /* 0.*.*.* and *.*.*.0 is invalid */ if ((i == 0 || i == 3) && value == 0) { return null; } result[i] = (byte) value; i++; } catch (NumberFormatException e) { Log.e(TAG, "Malformed IP string: " + ip); return null; } } return result; } private synchronized DNSResponse queryFromCache(String questDomainName) { try { Dao<DNSResponse, String> dnsCacheDao = helper.getDNSCacheDao(); return dnsCacheDao.queryForId(questDomainName); } catch (Exception e) { Log.e(TAG, "Cannot open DAO", e); } return null; } /* * Resolve host name by access a DNSRelay running on Shadowsocks: Example: * http://www.hosts.dotcloud.com/lookup.php?(domain name encoded) * http://shadowsocksdnsproxy.appspot.com/?d=(domain name encoded) */ private void resolveDomainName(String domain, AsyncHttpResponseHandler handler) { String ip = null; InputStream is; String encode_host = URLEncoder.encode(Base64.encodeBytes(Base64.encodeBytesToBytes(domain .getBytes()))); String url = "http://shadowsocksdnsproxy1.appspot.com:" + port + "/?d=" + encode_host; String host = "shadowsocksdnsproxy1.appspot.com"; url = url.replace(host, appHost); Random random = new Random(System.currentTimeMillis()); int n = random.nextInt(2); if (n == 0) { url = "http://shadowsocksdnsproxy2.appspot.com:" + port + "/?d=" + encode_host; host = "shadowsocksdnsproxy2.appspot.com"; url = url.replace(host, appHost); } else if (n == 1) { url = "http://shadowsocksdnsproxy3.appspot.com:" + port + "/?d=" + encode_host; host = "shadowsocksdnsproxy3.appspot.com"; url = url.replace(host, appHost); } // Log.d(TAG, "DNS Relay URL: " + url); // RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html client.get(url, appHost, handler); } @Override public void run() { loadCache(); byte[] qbuffer = new byte[576]; while (true) { try { final DatagramPacket dnsq = new DatagramPacket(qbuffer, qbuffer.length); srvSocket.receive(dnsq); // try to build dnsreq here byte[] data = dnsq.getData(); int dnsqLength = dnsq.getLength(); final byte[] udpreq = new byte[dnsqLength]; System.arraycopy(data, 0, udpreq, 0, dnsqLength); // begin to query from dns cache final String questDomain = getRequestDomain(udpreq); DNSResponse resp = queryFromCache(questDomain); if (resp != null) { sendDns(createDNSResponse(udpreq, parseIPString(resp.getAddress())), dnsq, srvSocket); Log.d(TAG, "DNS cache hit: " + questDomain); } else if (orgCache.containsKey(questDomain)) { // 如果为自定义域名解析 byte[] ips = parseIPString(orgCache.get(questDomain)); byte[] answer = createDNSResponse(udpreq, ips); addToCache(questDomain, answer); sendDns(answer, dnsq, srvSocket); Log.d(TAG, "Custom DNS resolver: " + questDomain); } else if (questDomain.toLowerCase().contains("appspot.com")) { // 如果为apphost域名解析 byte[] ips = parseIPString(appHost); byte[] answer = createDNSResponse(udpreq, ips); addToCache(questDomain, answer); sendDns(answer, dnsq, srvSocket); Log.d(TAG, "Custom DNS resolver: " + questDomain); } else { synchronized (domains) { if (domains.contains(questDomain)) continue; else domains.add(questDomain); } fetchAnswerHTTP(dnsq, udpreq); } } catch (SocketException e) { Log.e(TAG, e.getLocalizedMessage()); break; } catch (NullPointerException e) { Log.e(TAG, "Srvsocket wrong", e); break; } catch (IOException e) { Log.e(TAG, e.getLocalizedMessage()); } } } /** * send response to the source * * @param response response * @param dnsq request * @param srvSocket local socket */ private void sendDns(byte[] response, DatagramPacket dnsq, DatagramSocket srvSocket) { // 同步identifier System.arraycopy(dnsq.getData(), 0, response, 0, 2); DatagramPacket resp = new DatagramPacket(response, 0, response.length); resp.setPort(dnsq.getPort()); resp.setAddress(dnsq.getAddress()); try { srvSocket.send(resp); } catch (IOException e) { Log.e(TAG, "", e); } } }