Use vanilla bridges in parallel with obfs4 bridges.

This commit is contained in:
akwizgran
2022-03-30 14:49:03 +01:00
parent 05737d858d
commit fa0610fff1
6 changed files with 74 additions and 56 deletions

View File

@@ -11,6 +11,7 @@ public interface CircumventionProvider {
enum BridgeType { enum BridgeType {
DEFAULT_OBFS4, DEFAULT_OBFS4,
NON_DEFAULT_OBFS4, NON_DEFAULT_OBFS4,
VANILLA,
MEEK MEEK
} }
@@ -23,27 +24,27 @@ public interface CircumventionProvider {
String[] BLOCKED = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"}; String[] BLOCKED = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"};
/** /**
* Countries where obfs4 or meek bridge connections are likely to work. * Countries where bridge connections are likely to work.
* Should be a subset of {@link #BLOCKED} and the union of * Should be a subset of {@link #BLOCKED} and the union of
* {@link #DEFAULT_OBFS4_BRIDGES}, {@link #NON_DEFAULT_OBFS4_BRIDGES} and * {@link #DEFAULT_BRIDGES}, {@link #NON_DEFAULT_BRIDGES} and
* {@link #MEEK_BRIDGES}. * {@link #MEEK_BRIDGES}.
*/ */
String[] BRIDGES = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"}; String[] BRIDGES = {"BY", "CN", "EG", "IR", "RU", "TM", "VE"};
/** /**
* Countries where default obfs4 bridges are likely to work. * Countries where default obfs4 or vanilla bridges are likely to work.
* Should be a subset of {@link #BRIDGES}. * Should be a subset of {@link #BRIDGES}.
*/ */
String[] DEFAULT_OBFS4_BRIDGES = {"EG", "VE"}; String[] DEFAULT_BRIDGES = {"EG", "VE"};
/** /**
* Countries where non-default obfs4 bridges are likely to work. * Countries where non-default obfs4 or vanilla bridges are likely to work.
* Should be a subset of {@link #BRIDGES}. * Should be a subset of {@link #BRIDGES}.
*/ */
String[] NON_DEFAULT_OBFS4_BRIDGES = {"BY", "RU", "TM"}; String[] NON_DEFAULT_BRIDGES = {"BY", "RU", "TM"};
/** /**
* Countries where obfs4 bridges won't work and meek is needed. * Countries where obfs4 and vanilla bridges won't work and meek is needed.
* Should be a subset of {@link #BRIDGES}. * Should be a subset of {@link #BRIDGES}.
*/ */
String[] MEEK_BRIDGES = {"CN", "IR"}; String[] MEEK_BRIDGES = {"CN", "IR"};
@@ -60,10 +61,11 @@ public interface CircumventionProvider {
boolean doBridgesWork(String countryCode); boolean doBridgesWork(String countryCode);
/** /**
* Returns the best type of bridge connection for the given country, or * Returns the types of bridge connection that are suitable for the given
* {@link #DEFAULT_OBFS4_BRIDGES} if no bridge type is known to work. * country, or {@link #DEFAULT_BRIDGES} if no bridge type is known
* to work.
*/ */
BridgeType getBestBridgeType(String countryCode); List<BridgeType> getSuitableBridgeTypes(String countryCode);
@IoExecutor @IoExecutor
List<String> getBridges(BridgeType type); List<String> getBridges(BridgeType type);

View File

@@ -14,10 +14,12 @@ import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.VANILLA;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
@@ -30,9 +32,9 @@ class CircumventionProviderImpl implements CircumventionProvider {
private static final Set<String> BRIDGE_COUNTRIES = private static final Set<String> BRIDGE_COUNTRIES =
new HashSet<>(asList(BRIDGES)); new HashSet<>(asList(BRIDGES));
private static final Set<String> DEFAULT_OBFS4_BRIDGE_COUNTRIES = private static final Set<String> DEFAULT_OBFS4_BRIDGE_COUNTRIES =
new HashSet<>(asList(DEFAULT_OBFS4_BRIDGES)); new HashSet<>(asList(DEFAULT_BRIDGES));
private static final Set<String> NON_DEFAULT_OBFS4_BRIDGE_COUNTRIES = private static final Set<String> NON_DEFAULT_BRIDGE_COUNTRIES =
new HashSet<>(asList(NON_DEFAULT_OBFS4_BRIDGES)); new HashSet<>(asList(NON_DEFAULT_BRIDGES));
private static final Set<String> MEEK_COUNTRIES = private static final Set<String> MEEK_COUNTRIES =
new HashSet<>(asList(MEEK_BRIDGES)); new HashSet<>(asList(MEEK_BRIDGES));
@@ -51,15 +53,15 @@ class CircumventionProviderImpl implements CircumventionProvider {
} }
@Override @Override
public BridgeType getBestBridgeType(String countryCode) { public List<BridgeType> getSuitableBridgeTypes(String countryCode) {
if (DEFAULT_OBFS4_BRIDGE_COUNTRIES.contains(countryCode)) { if (DEFAULT_OBFS4_BRIDGE_COUNTRIES.contains(countryCode)) {
return DEFAULT_OBFS4; return asList(DEFAULT_OBFS4, VANILLA);
} else if (NON_DEFAULT_OBFS4_BRIDGE_COUNTRIES.contains(countryCode)) { } else if (NON_DEFAULT_BRIDGE_COUNTRIES.contains(countryCode)) {
return NON_DEFAULT_OBFS4; return asList(NON_DEFAULT_OBFS4, VANILLA);
} else if (MEEK_COUNTRIES.contains(countryCode)) { } else if (MEEK_COUNTRIES.contains(countryCode)) {
return MEEK; return singletonList(MEEK);
} else { } else {
return DEFAULT_OBFS4; return asList(DEFAULT_OBFS4, VANILLA);
} }
} }
@@ -73,12 +75,10 @@ class CircumventionProviderImpl implements CircumventionProvider {
List<String> bridges = new ArrayList<>(); List<String> bridges = new ArrayList<>();
while (scanner.hasNextLine()) { while (scanner.hasNextLine()) {
String line = scanner.nextLine(); String line = scanner.nextLine();
boolean isDefaultObfs4 = line.startsWith("d "); if ((type == DEFAULT_OBFS4 && line.startsWith("d ")) ||
boolean isNonDefaultObfs4 = line.startsWith("n "); (type == NON_DEFAULT_OBFS4 && line.startsWith("n ")) ||
boolean isMeek = line.startsWith("m "); (type == VANILLA && line.startsWith("v ")) ||
if ((type == DEFAULT_OBFS4 && isDefaultObfs4) || (type == MEEK && line.startsWith("m "))) {
(type == NON_DEFAULT_OBFS4 && isNonDefaultObfs4) ||
(type == MEEK && isMeek)) {
bridges.add(line.substring(2)); bridges.add(line.substring(2));
} }
} }

View File

@@ -92,7 +92,9 @@ import static org.briarproject.bramble.api.plugin.TorConstants.PROP_ONION_V3;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY; import static org.briarproject.bramble.api.plugin.TorConstants.REASON_BATTERY;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED; import static org.briarproject.bramble.api.plugin.TorConstants.REASON_COUNTRY_BLOCKED;
import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA; import static org.briarproject.bramble.api.plugin.TorConstants.REASON_MOBILE_DATA;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES; import static org.briarproject.bramble.plugin.tor.TorRendezvousCrypto.SEED_BYTES;
import static org.briarproject.bramble.util.IoUtils.copyAndClose; import static org.briarproject.bramble.util.IoUtils.copyAndClose;
import static org.briarproject.bramble.util.IoUtils.tryToClose; import static org.briarproject.bramble.util.IoUtils.tryToClose;
@@ -528,20 +530,24 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
controlConnection.setConf("DisableNetwork", enable ? "0" : "1"); controlConnection.setConf("DisableNetwork", enable ? "0" : "1");
} }
private void enableBridges(boolean enable, BridgeType bridgeType) private void enableBridges(boolean enable, List<BridgeType> bridgeTypes)
throws IOException { throws IOException {
if (enable) { if (enable) {
Collection<String> conf = new ArrayList<>(); Collection<String> conf = new ArrayList<>();
conf.add("UseBridges 1"); conf.add("UseBridges 1");
File obfs4File = getObfs4ExecutableFile(); File obfs4File = getObfs4ExecutableFile();
if (bridgeType == MEEK) { if (bridgeTypes.contains(MEEK)) {
conf.add("ClientTransportPlugin meek_lite exec " + conf.add("ClientTransportPlugin meek_lite exec " +
obfs4File.getAbsolutePath()); obfs4File.getAbsolutePath());
} else { }
if (bridgeTypes.contains(DEFAULT_OBFS4) ||
bridgeTypes.contains(NON_DEFAULT_OBFS4)) {
conf.add("ClientTransportPlugin obfs4 exec " + conf.add("ClientTransportPlugin obfs4 exec " +
obfs4File.getAbsolutePath()); obfs4File.getAbsolutePath());
} }
conf.addAll(circumventionProvider.getBridges(bridgeType)); for (BridgeType bridgeType : bridgeTypes) {
conf.addAll(circumventionProvider.getBridges(bridgeType));
}
controlConnection.setConf(conf); controlConnection.setConf(conf);
} else { } else {
controlConnection.setConf("UseBridges", "0"); controlConnection.setConf("UseBridges", "0");
@@ -831,8 +837,8 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
int reasonsDisabled = 0; int reasonsDisabled = 0;
boolean enableNetwork = false, enableBridges = false; boolean enableNetwork = false, enableBridges = false;
boolean enableConnectionPadding = false; boolean enableConnectionPadding = false;
BridgeType bridgeType = List<BridgeType> bridgeTypes =
circumventionProvider.getBestBridgeType(country); circumventionProvider.getSuitableBridgeTypes(country);
if (!online) { if (!online) {
LOG.info("Disabling network, device is offline"); LOG.info("Disabling network, device is offline");
@@ -861,10 +867,10 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
enableNetwork = true; enableNetwork = true;
if (network == PREF_TOR_NETWORK_WITH_BRIDGES || if (network == PREF_TOR_NETWORK_WITH_BRIDGES ||
(automatic && bridgesWork)) { (automatic && bridgesWork)) {
if (ipv6Only) bridgeType = MEEK; if (ipv6Only) bridgeTypes = singletonList(MEEK);
enableBridges = true; enableBridges = true;
if (LOG.isLoggable(INFO)) { if (LOG.isLoggable(INFO)) {
LOG.info("Using bridge type " + bridgeType); LOG.info("Using bridge types " + bridgeTypes);
} }
} else { } else {
LOG.info("Not using bridges"); LOG.info("Not using bridges");
@@ -882,7 +888,7 @@ abstract class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
try { try {
if (enableNetwork) { if (enableNetwork) {
enableBridges(enableBridges, bridgeType); enableBridges(enableBridges, bridgeTypes);
enableConnectionPadding(enableConnectionPadding); enableConnectionPadding(enableConnectionPadding);
useIpv6(ipv6Only); useIpv6(ipv6Only);
} }

View File

@@ -19,4 +19,8 @@ n Bridge obfs4 70.34.242.31:443 7F026956402CDFF4BCBA8E11EE9C50E3FE0A2B72 cert=hP
n Bridge obfs4 192.3.163.88:57145 DEB62DE9643E5956CA4FA78035B48C9BBABE7F29 cert=RMz2z9uVVrioUhx0A8xUmiftRe2RpcXiqIuDfisdIomcHDf82nzfn83X/ixGUiA4rLCAdw iat-mode=0 n Bridge obfs4 192.3.163.88:57145 DEB62DE9643E5956CA4FA78035B48C9BBABE7F29 cert=RMz2z9uVVrioUhx0A8xUmiftRe2RpcXiqIuDfisdIomcHDf82nzfn83X/ixGUiA4rLCAdw iat-mode=0
n Bridge obfs4 93.95.226.151:41185 460B0CFFC0CF1D965F3DE064E08BA1915E7C916A cert=inluPzp5Jp5OzZar1eQb4dcQ/YlAj/v0kHAUCoCr3rmLt03+pVuVTjoH4mRy4+acXpn+Gw iat-mode=0 n Bridge obfs4 93.95.226.151:41185 460B0CFFC0CF1D965F3DE064E08BA1915E7C916A cert=inluPzp5Jp5OzZar1eQb4dcQ/YlAj/v0kHAUCoCr3rmLt03+pVuVTjoH4mRy4+acXpn+Gw iat-mode=0
n Bridge obfs4 120.29.217.52:5223 40FE3DB9800272F9CDC76422F8ED7883280EE96D cert=/71PS4l8c/XJ4DIItlH9xMqNvPFg2RUTrHvPlQWh48u5et8h/yyyjCcYphUadDsfBWpaGQ iat-mode=0 n Bridge obfs4 120.29.217.52:5223 40FE3DB9800272F9CDC76422F8ED7883280EE96D cert=/71PS4l8c/XJ4DIItlH9xMqNvPFg2RUTrHvPlQWh48u5et8h/yyyjCcYphUadDsfBWpaGQ iat-mode=0
v Bridge 5.45.96.40:9001 8723B591712AAA03FB92000370BD356AB4997FA7
v Bridge 135.181.113.164:54444 74AF4CCA614C454B7D3E81FF8BACD78CEBC7D7DE
v Bridge 152.44.197.85:10507 FF07DF6B4720DA4C50F1A025662D50916D6223F6
v Bridge 209.216.78.21:443 C870D381E7264CDB83BAEEBF074804808CCCDB8D
m Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com m Bridge meek_lite 192.0.2.2:2 97700DFE9F483596DDA6264C4D7DF7641E1E39CE url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com

View File

@@ -7,14 +7,16 @@ import java.util.HashSet;
import java.util.Set; import java.util.Set;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BLOCKED; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BLOCKED;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BRIDGES; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BRIDGES;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.DEFAULT_OBFS4_BRIDGES; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.VANILLA;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.DEFAULT_BRIDGES;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.MEEK_BRIDGES; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.MEEK_BRIDGES;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.NON_DEFAULT_OBFS4_BRIDGES; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.NON_DEFAULT_BRIDGES;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@@ -27,37 +29,39 @@ public class CircumventionProviderTest extends BrambleTestCase {
public void testInvariants() { public void testInvariants() {
Set<String> blocked = new HashSet<>(asList(BLOCKED)); Set<String> blocked = new HashSet<>(asList(BLOCKED));
Set<String> bridges = new HashSet<>(asList(BRIDGES)); Set<String> bridges = new HashSet<>(asList(BRIDGES));
Set<String> defaultObfs4Bridges = Set<String> defaultBridges = new HashSet<>(asList(DEFAULT_BRIDGES));
new HashSet<>(asList(DEFAULT_OBFS4_BRIDGES)); Set<String> nonDefaultBridges =
Set<String> nonDefaultObfs4Bridges = new HashSet<>(asList(NON_DEFAULT_BRIDGES));
new HashSet<>(asList(NON_DEFAULT_OBFS4_BRIDGES));
Set<String> meekBridges = new HashSet<>(asList(MEEK_BRIDGES)); Set<String> meekBridges = new HashSet<>(asList(MEEK_BRIDGES));
// BRIDGES should be a subset of BLOCKED // BRIDGES should be a subset of BLOCKED
assertTrue(blocked.containsAll(bridges)); assertTrue(blocked.containsAll(bridges));
// BRIDGES should be the union of the bridge type sets // BRIDGES should be the union of the bridge type sets
Set<String> union = new HashSet<>(defaultObfs4Bridges); Set<String> union = new HashSet<>(defaultBridges);
union.addAll(nonDefaultObfs4Bridges); union.addAll(nonDefaultBridges);
union.addAll(meekBridges); union.addAll(meekBridges);
assertEquals(bridges, union); assertEquals(bridges, union);
// The bridge type sets should not overlap // The bridge type sets should not overlap
assertEmptyIntersection(defaultObfs4Bridges, nonDefaultObfs4Bridges); assertEmptyIntersection(defaultBridges, nonDefaultBridges);
assertEmptyIntersection(defaultObfs4Bridges, meekBridges); assertEmptyIntersection(defaultBridges, meekBridges);
assertEmptyIntersection(nonDefaultObfs4Bridges, meekBridges); assertEmptyIntersection(nonDefaultBridges, meekBridges);
} }
@Test @Test
public void testGetBestBridgeType() { public void testGetBestBridgeType() {
for (String country : DEFAULT_OBFS4_BRIDGES) { for (String country : DEFAULT_BRIDGES) {
assertEquals(DEFAULT_OBFS4, provider.getBestBridgeType(country)); assertEquals(asList(DEFAULT_OBFS4, VANILLA),
provider.getSuitableBridgeTypes(country));
} }
for (String country : NON_DEFAULT_OBFS4_BRIDGES) { for (String country : NON_DEFAULT_BRIDGES) {
assertEquals(NON_DEFAULT_OBFS4, assertEquals(asList(NON_DEFAULT_OBFS4, VANILLA),
provider.getBestBridgeType(country)); provider.getSuitableBridgeTypes(country));
} }
for (String country : MEEK_BRIDGES) { for (String country : MEEK_BRIDGES) {
assertEquals(MEEK, provider.getBestBridgeType(country)); assertEquals(singletonList(MEEK),
provider.getSuitableBridgeTypes(country));
} }
assertEquals(DEFAULT_OBFS4, provider.getBestBridgeType("ZZ")); assertEquals(asList(DEFAULT_OBFS4, VANILLA),
provider.getSuitableBridgeTypes("ZZ"));
} }
private <T> void assertEmptyIntersection(Set<T> a, Set<T> b) { private <T> void assertEmptyIntersection(Set<T> a, Set<T> b) {

View File

@@ -47,6 +47,7 @@ import static org.briarproject.bramble.api.plugin.TorConstants.DEFAULT_SOCKS_POR
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.MEEK;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4; import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.NON_DEFAULT_OBFS4;
import static org.briarproject.bramble.plugin.tor.CircumventionProvider.BridgeType.VANILLA;
import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
import static org.briarproject.bramble.test.TestUtils.getTestDirectory; import static org.briarproject.bramble.test.TestUtils.getTestDirectory;
import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled; import static org.briarproject.bramble.test.TestUtils.isOptionalTestEnabled;
@@ -74,6 +75,9 @@ public class BridgeTest extends BrambleTestCase {
for (String bridge : provider.getBridges(NON_DEFAULT_OBFS4)) { for (String bridge : provider.getBridges(NON_DEFAULT_OBFS4)) {
states.add(new Params(bridge, NON_DEFAULT_OBFS4, stats, false)); states.add(new Params(bridge, NON_DEFAULT_OBFS4, stats, false));
} }
for (String bridge : provider.getBridges(VANILLA)) {
states.add(new Params(bridge, VANILLA, stats, false));
}
for (String bridge : provider.getBridges(MEEK)) { for (String bridge : provider.getBridges(MEEK)) {
states.add(new Params(bridge, MEEK, stats, true)); states.add(new Params(bridge, MEEK, stats, true));
} }
@@ -99,8 +103,6 @@ public class BridgeTest extends BrambleTestCase {
@Inject @Inject
ResourceProvider resourceProvider; ResourceProvider resourceProvider;
@Inject @Inject
CircumventionProvider circumventionProvider;
@Inject
BatteryManager batteryManager; BatteryManager batteryManager;
@Inject @Inject
EventBus eventBus; EventBus eventBus;
@@ -150,8 +152,8 @@ public class BridgeTest extends BrambleTestCase {
} }
@Override @Override
public BridgeType getBestBridgeType(String countryCode) { public List<BridgeType> getSuitableBridgeTypes(String countryCode) {
return params.bridgeType; return singletonList(params.bridgeType);
} }
@Override @Override