WG/AWG Android splitTunnel
This commit is contained in:
parent
2a0166bb26
commit
546d4c1d3d
6 changed files with 874 additions and 48 deletions
509
client/android/src/com/wireguard/config/IPRange.java
Normal file
509
client/android/src/com/wireguard/config/IPRange.java
Normal file
|
@ -0,0 +1,509 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012-2017 Tobias Brunner
|
||||||
|
* HSR Hochschule fuer Technik Rapperswil
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation; either version 2 of the License, or (at your
|
||||||
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.config;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that represents a range of IP addresses. This range could be a proper subnet, but that's
|
||||||
|
* not necessarily the case (see {@code getPrefix} and {@code toSubnets}).
|
||||||
|
*/
|
||||||
|
public class IPRange implements Comparable<IPRange>
|
||||||
|
{
|
||||||
|
private final byte[] mBitmask = { (byte)0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 };
|
||||||
|
private byte[] mFrom;
|
||||||
|
private byte[] mTo;
|
||||||
|
private Integer mPrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the range is a proper subnet and, if so, what the network prefix is.
|
||||||
|
*/
|
||||||
|
private void determinePrefix()
|
||||||
|
{
|
||||||
|
boolean matching = true;
|
||||||
|
|
||||||
|
mPrefix = mFrom.length * 8;
|
||||||
|
for (int i = 0; i < mFrom.length; i++)
|
||||||
|
{
|
||||||
|
for (int bit = 0; bit < 8; bit++)
|
||||||
|
{
|
||||||
|
if (matching)
|
||||||
|
{
|
||||||
|
if ((mFrom[i] & mBitmask[bit]) != (mTo[i] & mBitmask[bit]))
|
||||||
|
{
|
||||||
|
mPrefix = (i * 8) + bit;
|
||||||
|
matching = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if ((mFrom[i] & mBitmask[bit]) != 0 || (mTo[i] & mBitmask[bit]) == 0)
|
||||||
|
{
|
||||||
|
mPrefix = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IPRange(byte[] from, byte[] to)
|
||||||
|
{
|
||||||
|
mFrom = from;
|
||||||
|
mTo = to;
|
||||||
|
determinePrefix();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPRange(String from, String to) throws UnknownHostException
|
||||||
|
{
|
||||||
|
this(Utils.parseInetAddress(from), Utils.parseInetAddress(to));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPRange(InetAddress from, InetAddress to)
|
||||||
|
{
|
||||||
|
initializeFromRange(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeFromRange(InetAddress from, InetAddress to)
|
||||||
|
{
|
||||||
|
byte[] fa = from.getAddress(), ta = to.getAddress();
|
||||||
|
if (fa.length != ta.length)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid range");
|
||||||
|
}
|
||||||
|
if (compareAddr(fa, ta) < 0)
|
||||||
|
{
|
||||||
|
mFrom = fa;
|
||||||
|
mTo = ta;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mTo = fa;
|
||||||
|
mFrom = ta;
|
||||||
|
}
|
||||||
|
determinePrefix();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPRange(String base, int prefix) throws UnknownHostException
|
||||||
|
{
|
||||||
|
this(Utils.parseInetAddress(base), prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPRange(InetAddress base, int prefix)
|
||||||
|
{
|
||||||
|
this(base.getAddress(), prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IPRange(byte[] from, int prefix)
|
||||||
|
{
|
||||||
|
initializeFromCIDR(from, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeFromCIDR(byte[] from, int prefix)
|
||||||
|
{
|
||||||
|
if (from.length != 4 && from.length != 16)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid address");
|
||||||
|
}
|
||||||
|
if (prefix < 0 || prefix > from.length * 8)
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid prefix");
|
||||||
|
}
|
||||||
|
byte[] to = from.clone();
|
||||||
|
byte mask = (byte)(0xff << (8 - prefix % 8));
|
||||||
|
int i = prefix / 8;
|
||||||
|
|
||||||
|
if (i < from.length)
|
||||||
|
{
|
||||||
|
from[i] = (byte)(from[i] & mask);
|
||||||
|
to[i] = (byte)(to[i] | ~mask);
|
||||||
|
Arrays.fill(from, i+1, from.length, (byte)0);
|
||||||
|
Arrays.fill(to, i+1, to.length, (byte)0xff);
|
||||||
|
}
|
||||||
|
mFrom = from;
|
||||||
|
mTo = to;
|
||||||
|
mPrefix = prefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IPRange(String cidr) throws UnknownHostException
|
||||||
|
{
|
||||||
|
/* only verify the basic structure */
|
||||||
|
if (!cidr.matches("(?i)^(([0-9.]+)|([0-9a-f:]+))(-(([0-9.]+)|([0-9a-f:]+))|(/\\d+))?$"))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("Invalid CIDR or range notation");
|
||||||
|
}
|
||||||
|
if (cidr.contains("-"))
|
||||||
|
{
|
||||||
|
String[] parts = cidr.split("-");
|
||||||
|
InetAddress from = InetAddress.getByName(parts[0]);
|
||||||
|
InetAddress to = InetAddress.getByName(parts[1]);
|
||||||
|
initializeFromRange(from, to);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
String[] parts = cidr.split("/");
|
||||||
|
InetAddress addr = InetAddress.getByName(parts[0]);
|
||||||
|
byte[] base = addr.getAddress();
|
||||||
|
int prefix = base.length * 8;
|
||||||
|
if (parts.length > 1)
|
||||||
|
{
|
||||||
|
prefix = Integer.parseInt(parts[1]);
|
||||||
|
}
|
||||||
|
initializeFromCIDR(base, prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first address of the range. The network ID in case this is a proper subnet.
|
||||||
|
*/
|
||||||
|
public InetAddress getFrom()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return InetAddress.getByAddress(mFrom);
|
||||||
|
}
|
||||||
|
catch (UnknownHostException ignored)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last address of the range.
|
||||||
|
*/
|
||||||
|
public InetAddress getTo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return InetAddress.getByAddress(mTo);
|
||||||
|
}
|
||||||
|
catch (UnknownHostException ignored)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this range is a proper subnet returns its prefix, otherwise returns null.
|
||||||
|
*/
|
||||||
|
public Integer getPrefix()
|
||||||
|
{
|
||||||
|
return mPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NonNull IPRange other)
|
||||||
|
{
|
||||||
|
int cmp = compareAddr(mFrom, other.mFrom);
|
||||||
|
if (cmp == 0)
|
||||||
|
{ /* smaller ranges first */
|
||||||
|
cmp = compareAddr(mTo, other.mTo);
|
||||||
|
}
|
||||||
|
return cmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o)
|
||||||
|
{
|
||||||
|
if (o == null || !(o instanceof IPRange))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this == o || compareTo((IPRange)o) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (mPrefix != null)
|
||||||
|
{
|
||||||
|
return InetAddress.getByAddress(mFrom).getHostAddress() + "/" + mPrefix;
|
||||||
|
}
|
||||||
|
return InetAddress.getByAddress(mFrom).getHostAddress() + "-" +
|
||||||
|
InetAddress.getByAddress(mTo).getHostAddress();
|
||||||
|
}
|
||||||
|
catch (UnknownHostException ignored)
|
||||||
|
{
|
||||||
|
return super.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compareAddr(byte a[], byte b[])
|
||||||
|
{
|
||||||
|
if (a.length != b.length)
|
||||||
|
{
|
||||||
|
return (a.length < b.length) ? -1 : 1;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < a.length; i++)
|
||||||
|
{
|
||||||
|
if (a[i] != b[i])
|
||||||
|
{
|
||||||
|
if (((int)a[i] & 0xff) < ((int)b[i] & 0xff))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this range fully contains the given range.
|
||||||
|
*/
|
||||||
|
public boolean contains(IPRange range)
|
||||||
|
{
|
||||||
|
return compareAddr(mFrom, range.mFrom) <= 0 && compareAddr(range.mTo, mTo) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this and the given range overlap.
|
||||||
|
*/
|
||||||
|
public boolean overlaps(IPRange range)
|
||||||
|
{
|
||||||
|
return !(compareAddr(mTo, range.mFrom) < 0 || compareAddr(range.mTo, mFrom) < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] dec(byte[] addr)
|
||||||
|
{
|
||||||
|
for (int i = addr.length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (--addr[i] != (byte)0xff)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] inc(byte[] addr)
|
||||||
|
{
|
||||||
|
for (int i = addr.length - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (++addr[i] != 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given range from the current range. Returns a list of resulting ranges (these are
|
||||||
|
* not proper subnets). At most two ranges are returned, in case the given range is contained in
|
||||||
|
* this but does not equal it, which would result in an empty list (which is also the case if
|
||||||
|
* this range is fully contained in the given range).
|
||||||
|
*/
|
||||||
|
public List<IPRange> remove(IPRange range)
|
||||||
|
{
|
||||||
|
ArrayList<IPRange> list = new ArrayList<>();
|
||||||
|
if (!overlaps(range))
|
||||||
|
{ /* | this | or | this |
|
||||||
|
* | range | | range | */
|
||||||
|
list.add(this);
|
||||||
|
}
|
||||||
|
else if (!range.contains(this))
|
||||||
|
{ /* we are not completely removed, so none of these cases applies:
|
||||||
|
* | this | or | this | or | this |
|
||||||
|
* | range | | range | | range | */
|
||||||
|
if (compareAddr(mFrom, range.mFrom) < 0 && compareAddr(range.mTo, mTo) < 0)
|
||||||
|
{ /* the removed range is completely within our boundaries:
|
||||||
|
* | this |
|
||||||
|
* | range | */
|
||||||
|
list.add(new IPRange(mFrom, dec(range.mFrom.clone())));
|
||||||
|
list.add(new IPRange(inc(range.mTo.clone()), mTo));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ /* one end is within our boundaries the other at or outside it:
|
||||||
|
* | this | or | this | or | this | or | this |
|
||||||
|
* | range | | range | | range | | range | */
|
||||||
|
byte[] from = compareAddr(mFrom, range.mFrom) < 0 ? mFrom : inc(range.mTo.clone());
|
||||||
|
byte[] to = compareAddr(mTo, range.mTo) > 0 ? mTo : dec(range.mFrom.clone());
|
||||||
|
list.add(new IPRange(from, to));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean adjacent(IPRange range)
|
||||||
|
{
|
||||||
|
if (compareAddr(mTo, range.mFrom) < 0)
|
||||||
|
{
|
||||||
|
byte[] to = inc(mTo.clone());
|
||||||
|
return compareAddr(to, range.mFrom) == 0;
|
||||||
|
}
|
||||||
|
byte[] from = dec(mFrom.clone());
|
||||||
|
return compareAddr(from, range.mTo) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge two adjacent or overlapping ranges, returns null if it's not possible to merge them.
|
||||||
|
*/
|
||||||
|
public IPRange merge(IPRange range)
|
||||||
|
{
|
||||||
|
if (overlaps(range))
|
||||||
|
{
|
||||||
|
if (contains(range))
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
else if (range.contains(this))
|
||||||
|
{
|
||||||
|
return range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!adjacent(range))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] from = compareAddr(mFrom, range.mFrom) < 0 ? mFrom : range.mFrom;
|
||||||
|
byte[] to = compareAddr(mTo, range.mTo) > 0 ? mTo : range.mTo;
|
||||||
|
return new IPRange(from, to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split the given range into a sorted list of proper subnets.
|
||||||
|
*/
|
||||||
|
public List<IPRange> toSubnets()
|
||||||
|
{
|
||||||
|
ArrayList<IPRange> list = new ArrayList<>();
|
||||||
|
if (mPrefix != null)
|
||||||
|
{
|
||||||
|
list.add(this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int i = 0, bit = 0, prefix, netmask, common_byte, common_bit;
|
||||||
|
int from_cur, from_prev = 0, to_cur, to_prev = 1;
|
||||||
|
boolean from_full = true, to_full = true;
|
||||||
|
|
||||||
|
byte[] from = mFrom.clone();
|
||||||
|
byte[] to = mTo.clone();
|
||||||
|
|
||||||
|
/* find a common prefix */
|
||||||
|
while (i < from.length && (from[i] & mBitmask[bit]) == (to[i] & mBitmask[bit]))
|
||||||
|
{
|
||||||
|
if (++bit == 8)
|
||||||
|
{
|
||||||
|
bit = 0;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefix = i * 8 + bit;
|
||||||
|
|
||||||
|
/* at this point we know that the addresses are either equal, or that the
|
||||||
|
* current bits in the 'from' and 'to' addresses are 0 and 1, respectively.
|
||||||
|
* we now look at the rest of the bits as two binary trees (0=left, 1=right)
|
||||||
|
* where 'from' and 'to' are both leaf nodes. all leaf nodes between these
|
||||||
|
* nodes are addresses contained in the range. to collect them as subnets
|
||||||
|
* we follow the trees from both leaf nodes to their root node and record
|
||||||
|
* all complete subtrees (right for from, left for to) we come across as
|
||||||
|
* subnets. in that process host bits are zeroed out. if both addresses
|
||||||
|
* are equal we won't enter the loop below.
|
||||||
|
* 0_____|_____1 for the 'from' address we assume we start on a
|
||||||
|
* 0__|__ 1 0__|__1 left subtree (0) and follow the left edges until
|
||||||
|
* _|_ _|_ _|_ _|_ we reach the root of this subtree, which is
|
||||||
|
* | | | | | | | | either the root of this whole 'from'-subtree
|
||||||
|
* 0 1 0 1 0 1 0 1 (causing us to leave the loop) or the root node
|
||||||
|
* of the right subtree (1) of another node (which actually could be the
|
||||||
|
* leaf node we start from). that whole subtree gets recorded as subnet.
|
||||||
|
* next we follow the right edges to the root of that subtree which again is
|
||||||
|
* either the 'from'-root or the root node in the left subtree (0) of
|
||||||
|
* another node. the complete right subtree of that node is the next subnet
|
||||||
|
* we record. from there we assume that we are in that right subtree and
|
||||||
|
* recursively follow right edges to its root. for the 'to' address the
|
||||||
|
* procedure is exactly the same but with left and right reversed.
|
||||||
|
*/
|
||||||
|
if (++bit == 8)
|
||||||
|
{
|
||||||
|
bit = 0;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
common_byte = i;
|
||||||
|
common_bit = bit;
|
||||||
|
netmask = from.length * 8;
|
||||||
|
for (i = from.length - 1; i >= common_byte; i--)
|
||||||
|
{
|
||||||
|
int bit_min = (i == common_byte) ? common_bit : 0;
|
||||||
|
for (bit = 7; bit >= bit_min; bit--)
|
||||||
|
{
|
||||||
|
byte mask = mBitmask[bit];
|
||||||
|
|
||||||
|
from_cur = from[i] & mask;
|
||||||
|
if (from_prev == 0 && from_cur != 0)
|
||||||
|
{ /* 0 -> 1: subnet is the whole current (right) subtree */
|
||||||
|
list.add(new IPRange(from.clone(), netmask));
|
||||||
|
from_full = false;
|
||||||
|
}
|
||||||
|
else if (from_prev != 0 && from_cur == 0)
|
||||||
|
{ /* 1 -> 0: invert bit to switch to right subtree and add it */
|
||||||
|
from[i] ^= mask;
|
||||||
|
list.add(new IPRange(from.clone(), netmask));
|
||||||
|
from_cur = 1;
|
||||||
|
}
|
||||||
|
/* clear the current bit */
|
||||||
|
from[i] &= ~mask;
|
||||||
|
from_prev = from_cur;
|
||||||
|
|
||||||
|
to_cur = to[i] & mask;
|
||||||
|
if (to_prev != 0 && to_cur == 0)
|
||||||
|
{ /* 1 -> 0: subnet is the whole current (left) subtree */
|
||||||
|
list.add(new IPRange(to.clone(), netmask));
|
||||||
|
to_full = false;
|
||||||
|
}
|
||||||
|
else if (to_prev == 0 && to_cur != 0)
|
||||||
|
{ /* 0 -> 1: invert bit to switch to left subtree and add it */
|
||||||
|
to[i] ^= mask;
|
||||||
|
list.add(new IPRange(to.clone(), netmask));
|
||||||
|
to_cur = 0;
|
||||||
|
}
|
||||||
|
/* clear the current bit */
|
||||||
|
to[i] &= ~mask;
|
||||||
|
to_prev = to_cur;
|
||||||
|
netmask--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from_full && to_full)
|
||||||
|
{ /* full subnet (from=to or from=0.. and to=1.. after common prefix) - not reachable
|
||||||
|
* due to the shortcut at the top */
|
||||||
|
list.add(new IPRange(from.clone(), prefix));
|
||||||
|
}
|
||||||
|
else if (from_full)
|
||||||
|
{ /* full from subnet (from=0.. after prefix) */
|
||||||
|
list.add(new IPRange(from.clone(), prefix + 1));
|
||||||
|
}
|
||||||
|
else if (to_full)
|
||||||
|
{ /* full to subnet (to=1.. after prefix) */
|
||||||
|
list.add(new IPRange(to.clone(), prefix + 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Collections.sort(list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
223
client/android/src/com/wireguard/config/IPRangeSet.java
Normal file
223
client/android/src/com/wireguard/config/IPRangeSet.java
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012-2017 Tobias Brunner
|
||||||
|
* HSR Hochschule fuer Technik Rapperswil
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation; either version 2 of the License, or (at your
|
||||||
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that represents a set of IP address ranges (not necessarily proper subnets) and allows
|
||||||
|
* modifying the set and enumerating the resulting subnets.
|
||||||
|
*/
|
||||||
|
public class IPRangeSet implements Iterable<IPRange>
|
||||||
|
{
|
||||||
|
private TreeSet<IPRange> mRanges = new TreeSet<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the given string (space separated ranges in CIDR or range notation) and return the
|
||||||
|
* resulting set or {@code null} if the string was invalid. An empty set is returned if the given string
|
||||||
|
* is {@code null}.
|
||||||
|
*/
|
||||||
|
public static IPRangeSet fromString(String ranges)
|
||||||
|
{
|
||||||
|
IPRangeSet set = new IPRangeSet();
|
||||||
|
if (ranges != null)
|
||||||
|
{
|
||||||
|
for (String range : ranges.split("\\s+"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
set.add(new IPRange(range));
|
||||||
|
}
|
||||||
|
catch (Exception unused)
|
||||||
|
{ /* besides due to invalid strings exceptions might get thrown if the string
|
||||||
|
* contains a hostname (NetworkOnMainThreadException) */
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a range to this set. Automatically gets merged with existing ranges.
|
||||||
|
*/
|
||||||
|
public void add(IPRange range)
|
||||||
|
{
|
||||||
|
if (mRanges.contains(range))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reinsert:
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Iterator<IPRange> iterator = mRanges.iterator();
|
||||||
|
while (iterator.hasNext())
|
||||||
|
{
|
||||||
|
IPRange existing = iterator.next();
|
||||||
|
IPRange replacement = existing.merge(range);
|
||||||
|
if (replacement != null)
|
||||||
|
{
|
||||||
|
iterator.remove();
|
||||||
|
range = replacement;
|
||||||
|
continue reinsert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mRanges.add(range);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all ranges from the given set.
|
||||||
|
*/
|
||||||
|
public void add(IPRangeSet ranges)
|
||||||
|
{
|
||||||
|
if (ranges == this)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IPRange range : ranges.mRanges)
|
||||||
|
{
|
||||||
|
add(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add all ranges from the given collection to this set.
|
||||||
|
*/
|
||||||
|
public void addAll(Collection<? extends IPRange> coll)
|
||||||
|
{
|
||||||
|
for (IPRange range : coll)
|
||||||
|
{
|
||||||
|
add(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given range from this set. Existing ranges are automatically adjusted.
|
||||||
|
*/
|
||||||
|
public void remove(IPRange range)
|
||||||
|
{
|
||||||
|
ArrayList <IPRange> additions = new ArrayList<>();
|
||||||
|
Iterator<IPRange> iterator = mRanges.iterator();
|
||||||
|
while (iterator.hasNext())
|
||||||
|
{
|
||||||
|
IPRange existing = iterator.next();
|
||||||
|
List<IPRange> result = existing.remove(range);
|
||||||
|
if (result.size() == 0)
|
||||||
|
{
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
else if (!result.get(0).equals(existing))
|
||||||
|
{
|
||||||
|
iterator.remove();
|
||||||
|
additions.addAll(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mRanges.addAll(additions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the given ranges from ranges in this set.
|
||||||
|
*/
|
||||||
|
public void remove(IPRangeSet ranges)
|
||||||
|
{
|
||||||
|
if (ranges == this)
|
||||||
|
{
|
||||||
|
mRanges.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (IPRange range : ranges.mRanges)
|
||||||
|
{
|
||||||
|
remove(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all the subnets derived from all the ranges in this set.
|
||||||
|
*/
|
||||||
|
public Iterable<IPRange> subnets()
|
||||||
|
{
|
||||||
|
return new Iterable<IPRange>()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public Iterator<IPRange> iterator()
|
||||||
|
{
|
||||||
|
return new Iterator<IPRange>()
|
||||||
|
{
|
||||||
|
private Iterator<IPRange> mIterator = mRanges.iterator();
|
||||||
|
private List<IPRange> mSubnets;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasNext()
|
||||||
|
{
|
||||||
|
return (mSubnets != null && mSubnets.size() > 0) || mIterator.hasNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IPRange next()
|
||||||
|
{
|
||||||
|
if (mSubnets == null || mSubnets.size() == 0)
|
||||||
|
{
|
||||||
|
IPRange range = mIterator.next();
|
||||||
|
mSubnets = range.toSubnets();
|
||||||
|
}
|
||||||
|
return mSubnets.remove(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void remove()
|
||||||
|
{
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterator<IPRange> iterator()
|
||||||
|
{
|
||||||
|
return mRanges.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of ranges, not subnets.
|
||||||
|
*/
|
||||||
|
public int size()
|
||||||
|
{
|
||||||
|
return mRanges.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{ /* we could use TextUtils, but that causes the unit tests to fail */
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (IPRange range : mRanges)
|
||||||
|
{
|
||||||
|
if (sb.length() > 0)
|
||||||
|
{
|
||||||
|
sb.append(" ");
|
||||||
|
}
|
||||||
|
sb.append(range.toString());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
77
client/android/src/com/wireguard/config/Utils.java
Normal file
77
client/android/src/com/wireguard/config/Utils.java
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014-2019 Tobias Brunner
|
||||||
|
* HSR Hochschule fuer Technik Rapperswil
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it
|
||||||
|
* under the terms of the GNU General Public License as published by the
|
||||||
|
* Free Software Foundation; either version 2 of the License, or (at your
|
||||||
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.wireguard.config;
|
||||||
|
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
public class Utils
|
||||||
|
{
|
||||||
|
static final char[] HEXDIGITS = "0123456789abcdef".toCharArray();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the given byte array to a hexadecimal string encoding.
|
||||||
|
*
|
||||||
|
* @param bytes byte array to convert
|
||||||
|
* @return hex string
|
||||||
|
*/
|
||||||
|
public static String bytesToHex(byte[] bytes)
|
||||||
|
{
|
||||||
|
char[] hex = new char[bytes.length * 2];
|
||||||
|
for (int i = 0; i < bytes.length; i++)
|
||||||
|
{
|
||||||
|
int value = bytes[i];
|
||||||
|
hex[i*2] = HEXDIGITS[(value & 0xf0) >> 4];
|
||||||
|
hex[i*2+1] = HEXDIGITS[ value & 0x0f];
|
||||||
|
}
|
||||||
|
return new String(hex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given proposal string
|
||||||
|
*
|
||||||
|
* @param ike true for IKE, false for ESP
|
||||||
|
* @param proposal proposal string
|
||||||
|
* @return true if valid
|
||||||
|
*/
|
||||||
|
public native static boolean isProposalValid(boolean ike, String proposal);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an IP address without doing a name lookup
|
||||||
|
*
|
||||||
|
* @param address IP address string
|
||||||
|
* @return address bytes if valid
|
||||||
|
*/
|
||||||
|
private native static byte[] parseInetAddressBytes(String address);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse an IP address without doing a name lookup (as compared to InetAddress.fromName())
|
||||||
|
*
|
||||||
|
* @param address IP address string
|
||||||
|
* @return address if valid
|
||||||
|
* @throws UnknownHostException if address is invalid
|
||||||
|
*/
|
||||||
|
public static InetAddress parseInetAddress(String address) throws UnknownHostException
|
||||||
|
{
|
||||||
|
byte[] bytes = parseInetAddressBytes(address);
|
||||||
|
if (bytes == null)
|
||||||
|
{
|
||||||
|
throw new UnknownHostException();
|
||||||
|
}
|
||||||
|
return InetAddress.getByAddress(bytes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -564,6 +564,7 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
return parseData
|
return parseData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Wireguard [Config] from a [json] string -
|
* Create a Wireguard [Config] from a [json] string -
|
||||||
* The [json] will be created in AndroidVpnProtocol.cpp
|
* The [json] will be created in AndroidVpnProtocol.cpp
|
||||||
|
@ -578,47 +579,67 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
val peerBuilder = Peer.Builder()
|
val peerBuilder = Peer.Builder()
|
||||||
val peerConfig = config["Peer"]!!
|
val peerConfig = config["Peer"]!!
|
||||||
peerBuilder.setPublicKey(Key.fromBase64(peerConfig["PublicKey"]))
|
peerBuilder.setPublicKey(Key.fromBase64(peerConfig["PublicKey"]))
|
||||||
peerConfig["PresharedKey"]?.let {
|
peerConfig["PresharedKey"]?.let { peerBuilder.setPreSharedKey(Key.fromBase64(it)) }
|
||||||
peerBuilder.setPreSharedKey(Key.fromBase64(it))
|
|
||||||
}
|
|
||||||
val allowedIPList = peerConfig["AllowedIPs"]?.split(",") ?: emptyList()
|
|
||||||
|
|
||||||
Log.e(tag, "splitTunnelSites $splitTunnelSites")
|
|
||||||
for (i in 0 until splitTunnelSites.length()) {
|
|
||||||
val site = splitTunnelSites.getString(i)
|
|
||||||
if (site.contains("\\/")) {
|
|
||||||
val internet = InetNetwork.parse(site + "\\32")
|
|
||||||
peerBuilder.addAllowedIp(internet)
|
|
||||||
} else {
|
|
||||||
val internet = InetNetwork.parse(site)
|
|
||||||
peerBuilder.addAllowedIp(internet)
|
|
||||||
}
|
|
||||||
Log.e(tag, "splitTunnelSites $site")
|
|
||||||
}
|
|
||||||
|
|
||||||
// if (allowedIPList.isEmpty() /*&& splitTunnelType.equals("0", true) */) {
|
|
||||||
// Log.e(tag, "splitTunnelSites $splitTunnelSites")
|
|
||||||
// for (i in 0 until splitTunnelSites.length()) {
|
|
||||||
// val site = splitTunnelSites.getString(i)
|
|
||||||
// Log.e(tag, "splitTunnelSites $site")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// val internet = InetNetwork.parse("0.0.0.0/0") // aka The whole internet.
|
|
||||||
// peerBuilder.addAllowedIp(internet)
|
|
||||||
// } else {
|
|
||||||
|
|
||||||
|
val allIpString = peerConfig["AllowedIPs"]
|
||||||
// allowedIPList.forEach {
|
|
||||||
// val network = InetNetwork.parse(it.trim())
|
var allowedIPList = peerConfig["AllowedIPs"]?.split(",") ?: emptyList()
|
||||||
// peerBuilder.addAllowedIp(network)
|
|
||||||
// }
|
/* default value in template */
|
||||||
// }
|
if (allIpString == "0.0.0.0/0, ::/0") {
|
||||||
|
allowedIPList = emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowedIPList.isEmpty() && (splitTunnelType == 0)) {
|
||||||
|
/* AllowedIP is empty and splitTunnel is turnoff */
|
||||||
|
/* use VPN for whole Internet */
|
||||||
|
val internetV4 = InetNetwork.parse("0.0.0.0/0") // aka The whole internet.
|
||||||
|
peerBuilder.addAllowedIp(internetV4)
|
||||||
|
val internetV6 = InetNetwork.parse("::/0") // aka The whole internet.
|
||||||
|
peerBuilder.addAllowedIp(internetV6)
|
||||||
|
} else {
|
||||||
|
if (!allowedIPList.isEmpty()) {
|
||||||
|
/* We have predefined AllowedIP in WG config */
|
||||||
|
/* It's have higher priority than system SplitTunnel */
|
||||||
|
allowedIPList.forEach {
|
||||||
|
val network = InetNetwork.parse(it.trim())
|
||||||
|
peerBuilder.addAllowedIp(network)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (splitTunnelType == 1) {
|
||||||
|
/* Use system SplitTunnel */
|
||||||
|
/* VPN connection used only for defined IPs */
|
||||||
|
for (i in 0 until splitTunnelSites.length()) {
|
||||||
|
val site = splitTunnelSites.getString(i)
|
||||||
|
Log.e(tag, "splitTunnelSites $site")
|
||||||
|
if (site.contains("\\/")) {
|
||||||
|
val internet = InetNetwork.parse(site + "\\32")
|
||||||
|
peerBuilder.addAllowedIp(internet)
|
||||||
|
} else {
|
||||||
|
val internet = InetNetwork.parse(site)
|
||||||
|
peerBuilder.addAllowedIp(internet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (splitTunnelType == 2) {
|
||||||
|
/* Use system SplitTunnel */
|
||||||
|
/* VPN connection used for all Internet exclude defined IPs */
|
||||||
|
val ipRangeSet = IPRangeSet.fromString("0.0.0.0/0")
|
||||||
|
ipRangeSet.remove(IPRange("127.0.0.0/8"))
|
||||||
|
for (i in 0 until splitTunnelSites.length()) {
|
||||||
|
val site = splitTunnelSites.getString(i)
|
||||||
|
ipRangeSet.remove(IPRange(site))
|
||||||
|
}
|
||||||
|
val allowedIps = ipRangeSet.subnets().joinToString(", ") + ", 2000::/3"
|
||||||
|
Log.e(tag, "allowedIps $allowedIps")
|
||||||
|
peerBuilder.parseAllowedIPs(allowedIps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
val endpointConfig = peerConfig["Endpoint"]
|
val endpointConfig = peerConfig["Endpoint"]
|
||||||
val endpoint = InetEndpoint.parse(endpointConfig)
|
val endpoint = InetEndpoint.parse(endpointConfig)
|
||||||
peerBuilder.setEndpoint(endpoint)
|
peerBuilder.setEndpoint(endpoint)
|
||||||
peerConfig["PersistentKeepalive"]?.let {
|
peerConfig["PersistentKeepalive"]?.let { peerBuilder.setPersistentKeepalive(it.toInt()) }
|
||||||
peerBuilder.setPersistentKeepalive(it.toInt())
|
|
||||||
}
|
|
||||||
confBuilder.addPeer(peerBuilder.build())
|
confBuilder.addPeer(peerBuilder.build())
|
||||||
|
|
||||||
val ifaceBuilder = Interface.Builder()
|
val ifaceBuilder = Interface.Builder()
|
||||||
|
@ -628,7 +649,7 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
ifaceConfig["DNS"]!!.split(",").forEach {
|
ifaceConfig["DNS"]!!.split(",").forEach {
|
||||||
ifaceBuilder.addDnsServer(InetNetwork.parse(it.trim()).address)
|
ifaceBuilder.addDnsServer(InetNetwork.parse(it.trim()).address)
|
||||||
}
|
}
|
||||||
|
|
||||||
ifaceBuilder.parsePrivateKey(ifaceConfig["PrivateKey"])
|
ifaceBuilder.parsePrivateKey(ifaceConfig["PrivateKey"])
|
||||||
if (type == "awg_config_data") {
|
if (type == "awg_config_data") {
|
||||||
ifaceBuilder.parseJc(ifaceConfig["Jc"])
|
ifaceBuilder.parseJc(ifaceConfig["Jc"])
|
||||||
|
@ -649,14 +670,13 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
ifaceBuilder.parseH1("0")
|
ifaceBuilder.parseH1("0")
|
||||||
ifaceBuilder.parseH2("0")
|
ifaceBuilder.parseH2("0")
|
||||||
ifaceBuilder.parseH3("0")
|
ifaceBuilder.parseH3("0")
|
||||||
ifaceBuilder.parseH4("0")
|
ifaceBuilder.parseH4("0")
|
||||||
|
|
||||||
}
|
}
|
||||||
/*val jExcludedApplication = obj.getJSONArray("excludedApps")
|
/*val jExcludedApplication = obj.getJSONArray("excludedApps")
|
||||||
(0 until jExcludedApplication.length()).toList().forEach {
|
(0 until jExcludedApplication.length()).toList().forEach {
|
||||||
val appName = jExcludedApplication.get(it).toString()
|
val appName = jExcludedApplication.get(it).toString()
|
||||||
ifaceBuilder.excludeApplication(appName)
|
ifaceBuilder.excludeApplication(appName)
|
||||||
}*/
|
}*/
|
||||||
confBuilder.setInterface(ifaceBuilder.build())
|
confBuilder.setInterface(ifaceBuilder.build())
|
||||||
|
|
||||||
return confBuilder.build()
|
return confBuilder.build()
|
||||||
|
@ -771,7 +791,6 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
|
|
||||||
private fun startWireGuard(type: String) {
|
private fun startWireGuard(type: String) {
|
||||||
val wireguard_conf = buildWireguardConfig(mConfig!!, type + "_config_data")
|
val wireguard_conf = buildWireguardConfig(mConfig!!, type + "_config_data")
|
||||||
Log.i(tag, "startWireGuard: wireguard_conf : $wireguard_conf")
|
|
||||||
if (currentTunnelHandle != -1) {
|
if (currentTunnelHandle != -1) {
|
||||||
Log.e(tag, "Tunnel already up")
|
Log.e(tag, "Tunnel already up")
|
||||||
// Turn the tunnel down because this might be a switch
|
// Turn the tunnel down because this might be a switch
|
||||||
|
@ -779,8 +798,6 @@ class VPNService : BaseVpnService(), LocalDnsService.Interface {
|
||||||
}
|
}
|
||||||
val wgConfig: String = wireguard_conf.toWgUserspaceString()
|
val wgConfig: String = wireguard_conf.toWgUserspaceString()
|
||||||
|
|
||||||
Log.e(tag, "wgConfig : $wgConfig")
|
|
||||||
|
|
||||||
val builder = Builder()
|
val builder = Builder()
|
||||||
setupBuilder(wireguard_conf, builder)
|
setupBuilder(wireguard_conf, builder)
|
||||||
builder.setSession("Amnezia")
|
builder.setSession("Amnezia")
|
||||||
|
|
|
@ -127,7 +127,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(QString jsonConfig)
|
||||||
if (m_settings->routeMode() == Settings::VpnAllSites) {
|
if (m_settings->routeMode() == Settings::VpnAllSites) {
|
||||||
qDebug() << "Settings::VpnAllSites";
|
qDebug() << "Settings::VpnAllSites";
|
||||||
|
|
||||||
//config.append("\nredirect-gateway def1 ipv6 bypass-dhcp\n");
|
config.append("\nredirect-gateway def1 ipv6 bypass-dhcp\n");
|
||||||
// Prevent ipv6 leak
|
// Prevent ipv6 leak
|
||||||
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
|
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
|
||||||
config.append("block-ipv6\n");
|
config.append("block-ipv6\n");
|
||||||
|
@ -140,7 +140,7 @@ QString OpenVpnConfigurator::processConfigWithLocalSettings(QString jsonConfig)
|
||||||
if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
|
if (m_settings->routeMode() == Settings::VpnAllExceptSites) {
|
||||||
qDebug() << "Settings::VpnAllExceptSites";
|
qDebug() << "Settings::VpnAllExceptSites";
|
||||||
|
|
||||||
//config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n");
|
config.append("\nredirect-gateway ipv6 !ipv4 bypass-dhcp\n");
|
||||||
// Prevent ipv6 leak
|
// Prevent ipv6 leak
|
||||||
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
|
config.append("ifconfig-ipv6 fd15:53b6:dead::2/64 fd15:53b6:dead::1\n");
|
||||||
config.append("block-ipv6\n");
|
config.append("block-ipv6\n");
|
||||||
|
|
|
@ -234,7 +234,7 @@ QString Settings::routeModeString(RouteMode mode) const
|
||||||
Settings::RouteMode Settings::routeMode() const
|
Settings::RouteMode Settings::routeMode() const
|
||||||
{
|
{
|
||||||
// TODO implement for mobiles
|
// TODO implement for mobiles
|
||||||
#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
|
#if defined(Q_OS_IOS)
|
||||||
return RouteMode::VpnAllSites;
|
return RouteMode::VpnAllSites;
|
||||||
#endif
|
#endif
|
||||||
return static_cast<RouteMode>(m_settings.value("Conf/routeMode", 0).toInt());
|
return static_cast<RouteMode>(m_settings.value("Conf/routeMode", 0).toInt());
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue