/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.networkstack.tethering; import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP; import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST; import static android.net.util.TetheringUtils.uint16; import android.annotation.IntDef; import android.annotation.NonNull; import android.hardware.tetheroffload.config.V1_0.IOffloadConfig; import android.hardware.tetheroffload.control.V1_0.IOffloadControl; import android.hardware.tetheroffload.control.V1_0.NatTimeoutUpdate; import android.hardware.tetheroffload.control.V1_0.NetworkProtocol; import android.hardware.tetheroffload.control.V1_0.OffloadCallbackEvent; import android.hardware.tetheroffload.control.V1_1.ITetheringOffloadCallback; import android.net.netlink.NetlinkSocket; import android.net.netlink.StructNfGenMsg; import android.net.netlink.StructNlMsgHdr; import android.net.util.SharedLog; import android.net.util.SocketUtils; import android.os.Handler; import android.os.NativeHandle; import android.os.RemoteException; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.util.Log; import android.util.Pair; import com.android.internal.annotations.VisibleForTesting; import java.io.FileDescriptor; import java.io.IOException; import java.io.InterruptedIOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.SocketAddress; import java.net.SocketException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.NoSuchElementException; /** * Capture tethering dependencies, for injection. * * @hide */ public class OffloadHardwareInterface { private static final String TAG = OffloadHardwareInterface.class.getSimpleName(); private static final String YIELDS = " -> "; // Change this value to control whether tether offload is enabled or // disabled by default in the absence of an explicit Settings value. // See accompanying unittest to distinguish 0 from non-0 values. private static final int DEFAULT_TETHER_OFFLOAD_DISABLED = 0; private static final String NO_INTERFACE_NAME = ""; private static final String NO_IPV4_ADDRESS = ""; private static final String NO_IPV4_GATEWAY = ""; // Reference kernel/uapi/linux/netfilter/nfnetlink_compat.h public static final int NF_NETLINK_CONNTRACK_NEW = 1; public static final int NF_NETLINK_CONNTRACK_UPDATE = 2; public static final int NF_NETLINK_CONNTRACK_DESTROY = 4; // Reference libnetfilter_conntrack/linux_nfnetlink_conntrack.h public static final short NFNL_SUBSYS_CTNETLINK = 1; public static final short IPCTNL_MSG_CT_NEW = 0; public static final short IPCTNL_MSG_CT_GET = 1; private final long NETLINK_MESSAGE_TIMEOUT_MS = 500; private final Handler mHandler; private final SharedLog mLog; private final Dependencies mDeps; private IOffloadControl mOffloadControl; // TODO: Use major-minor version control to prevent from defining new constants. static final int OFFLOAD_HAL_VERSION_NONE = 0; static final int OFFLOAD_HAL_VERSION_1_0 = 1; static final int OFFLOAD_HAL_VERSION_1_1 = 2; /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "OFFLOAD_HAL_VERSION_", value = { OFFLOAD_HAL_VERSION_NONE, OFFLOAD_HAL_VERSION_1_0, OFFLOAD_HAL_VERSION_1_1 }) public @interface OffloadHalVersion {} @OffloadHalVersion private int mOffloadControlVersion = OFFLOAD_HAL_VERSION_NONE; @NonNull static String halVerToString(int version) { switch(version) { case OFFLOAD_HAL_VERSION_1_0: return "1.0"; case OFFLOAD_HAL_VERSION_1_1: return "1.1"; case OFFLOAD_HAL_VERSION_NONE: return "None"; default: throw new IllegalArgumentException("Unsupported version int " + version); } } private TetheringOffloadCallback mTetheringOffloadCallback; private ControlCallback mControlCallback; /** The callback to notify status of offload management process. */ public static class ControlCallback { /** Offload started. */ public void onStarted() {} /** * Offload stopped because an error has occurred in lower layer. */ public void onStoppedError() {} /** * Offload stopped because the device has moved to a bearer on which hardware offload is * not supported. Subsequent calls to setUpstreamParameters and add/removeDownstream will * likely fail and cannot be presumed to be saved inside of the hardware management process. * Upon receiving #onSupportAvailable(), the caller should reprogram the hardware to begin * offload again. */ public void onStoppedUnsupported() {} /** Indicate that offload is able to proivde support for this time. */ public void onSupportAvailable() {} /** Offload stopped because of usage limit reached. */ public void onStoppedLimitReached() {} /** Indicate that data warning quota is reached. */ public void onWarningReached() {} /** Indicate to update NAT timeout. */ public void onNatTimeoutUpdate(int proto, String srcAddr, int srcPort, String dstAddr, int dstPort) {} } /** The object which records Tx/Rx forwarded bytes. */ public static class ForwardedStats { public long rxBytes; public long txBytes; public ForwardedStats() { rxBytes = 0; txBytes = 0; } @VisibleForTesting public ForwardedStats(long rxBytes, long txBytes) { this.rxBytes = rxBytes; this.txBytes = txBytes; } /** Add Tx/Rx bytes. */ public void add(ForwardedStats other) { rxBytes += other.rxBytes; txBytes += other.txBytes; } /** Returns the string representation of this object. */ public String toString() { return String.format("rx:%s tx:%s", rxBytes, txBytes); } } public OffloadHardwareInterface(Handler h, SharedLog log) { this(h, log, new Dependencies(log)); } OffloadHardwareInterface(Handler h, SharedLog log, Dependencies deps) { mHandler = h; mLog = log.forSubComponent(TAG); mDeps = deps; } /** Capture OffloadHardwareInterface dependencies, for injection. */ static class Dependencies { private final SharedLog mLog; Dependencies(SharedLog log) { mLog = log; } public IOffloadConfig getOffloadConfig() { try { return IOffloadConfig.getService(true /*retry*/); } catch (RemoteException | NoSuchElementException e) { mLog.e("getIOffloadConfig error " + e); return null; } } @NonNull public Pair getOffloadControl() { IOffloadControl hal = null; int version = OFFLOAD_HAL_VERSION_NONE; try { hal = android.hardware.tetheroffload.control .V1_1.IOffloadControl.getService(true /*retry*/); version = OFFLOAD_HAL_VERSION_1_1; } catch (NoSuchElementException e) { // Unsupported by device. } catch (RemoteException e) { mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_1); } if (hal == null) { try { hal = IOffloadControl.getService(true /*retry*/); version = OFFLOAD_HAL_VERSION_1_0; } catch (NoSuchElementException e) { // Unsupported by device. } catch (RemoteException e) { mLog.e("Unable to get offload control " + OFFLOAD_HAL_VERSION_1_0); } } return new Pair(hal, version); } public NativeHandle createConntrackSocket(final int groups) { final FileDescriptor fd; try { fd = NetlinkSocket.forProto(OsConstants.NETLINK_NETFILTER); } catch (ErrnoException e) { mLog.e("Unable to create conntrack socket " + e); return null; } final SocketAddress sockAddr = SocketUtils.makeNetlinkSocketAddress(0, groups); try { Os.bind(fd, sockAddr); } catch (ErrnoException | SocketException e) { mLog.e("Unable to bind conntrack socket for groups " + groups + " error: " + e); try { SocketUtils.closeSocket(fd); } catch (IOException ie) { // Nothing we can do here } return null; } try { Os.connect(fd, sockAddr); } catch (ErrnoException | SocketException e) { mLog.e("connect to kernel fail for groups " + groups + " error: " + e); try { SocketUtils.closeSocket(fd); } catch (IOException ie) { // Nothing we can do here } return null; } return new NativeHandle(fd, true); } } /** Get default value indicating whether offload is supported. */ public int getDefaultTetherOffloadDisabled() { return DEFAULT_TETHER_OFFLOAD_DISABLED; } /** * Offload management process need to know conntrack rules to support NAT, but it may not have * permission to create netlink netfilter sockets. Create two netlink netfilter sockets and * share them with offload management process. */ public boolean initOffloadConfig() { final IOffloadConfig offloadConfig = mDeps.getOffloadConfig(); if (offloadConfig == null) { mLog.e("Could not find IOffloadConfig service"); return false; } // Per the IConfigOffload definition: // // h1 provides a file descriptor bound to the following netlink groups // (NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY). // // h2 provides a file descriptor bound to the following netlink groups // (NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY). final NativeHandle h1 = mDeps.createConntrackSocket( NF_NETLINK_CONNTRACK_NEW | NF_NETLINK_CONNTRACK_DESTROY); if (h1 == null) return false; sendIpv4NfGenMsg(h1, (short) ((NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_GET), (short) (NLM_F_REQUEST | NLM_F_DUMP)); final NativeHandle h2 = mDeps.createConntrackSocket( NF_NETLINK_CONNTRACK_UPDATE | NF_NETLINK_CONNTRACK_DESTROY); if (h2 == null) { closeFdInNativeHandle(h1); return false; } final CbResults results = new CbResults(); try { offloadConfig.setHandles(h1, h2, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record("initOffloadConfig, setHandles fail", e); return false; } // Explicitly close FDs. closeFdInNativeHandle(h1); closeFdInNativeHandle(h2); record("initOffloadConfig, setHandles results:", results); return results.mSuccess; } @VisibleForTesting public void sendIpv4NfGenMsg(@NonNull NativeHandle handle, short type, short flags) { final int length = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE; final byte[] msg = new byte[length]; final ByteBuffer byteBuffer = ByteBuffer.wrap(msg); byteBuffer.order(ByteOrder.nativeOrder()); final StructNlMsgHdr nlh = new StructNlMsgHdr(); nlh.nlmsg_len = length; nlh.nlmsg_type = type; nlh.nlmsg_flags = flags; nlh.nlmsg_seq = 0; nlh.pack(byteBuffer); // Header needs to be added to buffer since a generic netlink request is being sent. final StructNfGenMsg nfh = new StructNfGenMsg((byte) OsConstants.AF_INET); nfh.pack(byteBuffer); try { NetlinkSocket.sendMessage(handle.getFileDescriptor(), msg, 0 /* offset */, length, NETLINK_MESSAGE_TIMEOUT_MS); } catch (ErrnoException | InterruptedIOException e) { mLog.e("Unable to send netfilter message, error: " + e); } } private void closeFdInNativeHandle(final NativeHandle h) { try { h.close(); } catch (IOException | IllegalStateException e) { // IllegalStateException means fd is already closed, do nothing here. // Also nothing we can do if IOException. } } /** * Initialize the tethering offload HAL. * * @return one of {@code OFFLOAD_HAL_VERSION_*} represents the HAL version, or * {@link #OFFLOAD_HAL_VERSION_NONE} if failed. */ public int initOffloadControl(ControlCallback controlCb) { mControlCallback = controlCb; if (mOffloadControl == null) { final Pair halAndVersion = mDeps.getOffloadControl(); mOffloadControl = halAndVersion.first; mOffloadControlVersion = halAndVersion.second; if (mOffloadControl == null) { mLog.e("tethering IOffloadControl.getService() returned null"); return OFFLOAD_HAL_VERSION_NONE; } mLog.i("tethering offload control version " + halVerToString(mOffloadControlVersion) + " is supported."); } final String logmsg = String.format("initOffloadControl(%s)", (controlCb == null) ? "null" : "0x" + Integer.toHexString(System.identityHashCode(controlCb))); mTetheringOffloadCallback = new TetheringOffloadCallback( mHandler, mControlCallback, mLog, mOffloadControlVersion); final CbResults results = new CbResults(); try { mOffloadControl.initOffload( mTetheringOffloadCallback, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record(logmsg, e); return OFFLOAD_HAL_VERSION_NONE; } record(logmsg, results); return results.mSuccess ? mOffloadControlVersion : OFFLOAD_HAL_VERSION_NONE; } /** Stop IOffloadControl. */ public void stopOffloadControl() { if (mOffloadControl != null) { try { mOffloadControl.stopOffload( (boolean success, String errMsg) -> { if (!success) mLog.e("stopOffload failed: " + errMsg); }); } catch (RemoteException e) { mLog.e("failed to stopOffload: " + e); } } mOffloadControl = null; mTetheringOffloadCallback = null; mControlCallback = null; mLog.log("stopOffloadControl()"); } /** Get Tx/Rx usage from last query. */ public ForwardedStats getForwardedStats(String upstream) { final String logmsg = String.format("getForwardedStats(%s)", upstream); final ForwardedStats stats = new ForwardedStats(); try { mOffloadControl.getForwardedStats( upstream, (long rxBytes, long txBytes) -> { stats.rxBytes = (rxBytes > 0) ? rxBytes : 0; stats.txBytes = (txBytes > 0) ? txBytes : 0; }); } catch (RemoteException e) { record(logmsg, e); return stats; } return stats; } /** Set local prefixes to offload management process. */ public boolean setLocalPrefixes(ArrayList localPrefixes) { final String logmsg = String.format("setLocalPrefixes([%s])", String.join(",", localPrefixes)); final CbResults results = new CbResults(); try { mOffloadControl.setLocalPrefixes(localPrefixes, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record(logmsg, e); return false; } record(logmsg, results); return results.mSuccess; } /** Set data limit value to offload management process. */ public boolean setDataLimit(String iface, long limit) { final String logmsg = String.format("setDataLimit(%s, %d)", iface, limit); final CbResults results = new CbResults(); try { mOffloadControl.setDataLimit( iface, limit, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record(logmsg, e); return false; } record(logmsg, results); return results.mSuccess; } /** Set data warning and limit value to offload management process. */ public boolean setDataWarningAndLimit(String iface, long warning, long limit) { if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) { throw new IllegalArgumentException( "setDataWarningAndLimit is not supported below HAL V1.1"); } final String logmsg = String.format("setDataWarningAndLimit(%s, %d, %d)", iface, warning, limit); final CbResults results = new CbResults(); try { ((android.hardware.tetheroffload.control.V1_1.IOffloadControl) mOffloadControl) .setDataWarningAndLimit( iface, warning, limit, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record(logmsg, e); return false; } record(logmsg, results); return results.mSuccess; } /** Set upstream parameters to offload management process. */ public boolean setUpstreamParameters( String iface, String v4addr, String v4gateway, ArrayList v6gws) { iface = (iface != null) ? iface : NO_INTERFACE_NAME; v4addr = (v4addr != null) ? v4addr : NO_IPV4_ADDRESS; v4gateway = (v4gateway != null) ? v4gateway : NO_IPV4_GATEWAY; v6gws = (v6gws != null) ? v6gws : new ArrayList<>(); final String logmsg = String.format("setUpstreamParameters(%s, %s, %s, [%s])", iface, v4addr, v4gateway, String.join(",", v6gws)); final CbResults results = new CbResults(); try { mOffloadControl.setUpstreamParameters( iface, v4addr, v4gateway, v6gws, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record(logmsg, e); return false; } record(logmsg, results); return results.mSuccess; } /** Add downstream prefix to offload management process. */ public boolean addDownstreamPrefix(String ifname, String prefix) { final String logmsg = String.format("addDownstreamPrefix(%s, %s)", ifname, prefix); final CbResults results = new CbResults(); try { mOffloadControl.addDownstream(ifname, prefix, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record(logmsg, e); return false; } record(logmsg, results); return results.mSuccess; } /** Remove downstream prefix from offload management process. */ public boolean removeDownstreamPrefix(String ifname, String prefix) { final String logmsg = String.format("removeDownstreamPrefix(%s, %s)", ifname, prefix); final CbResults results = new CbResults(); try { mOffloadControl.removeDownstream(ifname, prefix, (boolean success, String errMsg) -> { results.mSuccess = success; results.mErrMsg = errMsg; }); } catch (RemoteException e) { record(logmsg, e); return false; } record(logmsg, results); return results.mSuccess; } private void record(String msg, Throwable t) { mLog.e(msg + YIELDS + "exception: " + t); } private void record(String msg, CbResults results) { final String logmsg = msg + YIELDS + results; if (!results.mSuccess) { mLog.e(logmsg); } else { mLog.log(logmsg); } } private static class TetheringOffloadCallback extends ITetheringOffloadCallback.Stub { public final Handler handler; public final ControlCallback controlCb; public final SharedLog log; private final int mOffloadControlVersion; TetheringOffloadCallback( Handler h, ControlCallback cb, SharedLog sharedLog, int offloadControlVersion) { handler = h; controlCb = cb; log = sharedLog; this.mOffloadControlVersion = offloadControlVersion; } private void handleOnEvent(int event) { switch (event) { case OffloadCallbackEvent.OFFLOAD_STARTED: controlCb.onStarted(); break; case OffloadCallbackEvent.OFFLOAD_STOPPED_ERROR: controlCb.onStoppedError(); break; case OffloadCallbackEvent.OFFLOAD_STOPPED_UNSUPPORTED: controlCb.onStoppedUnsupported(); break; case OffloadCallbackEvent.OFFLOAD_SUPPORT_AVAILABLE: controlCb.onSupportAvailable(); break; case OffloadCallbackEvent.OFFLOAD_STOPPED_LIMIT_REACHED: controlCb.onStoppedLimitReached(); break; case android.hardware.tetheroffload.control .V1_1.OffloadCallbackEvent.OFFLOAD_WARNING_REACHED: controlCb.onWarningReached(); break; default: log.e("Unsupported OffloadCallbackEvent: " + event); } } @Override public void onEvent(int event) { // The implementation should never call onEvent()) if the event is already reported // through newer callback. if (mOffloadControlVersion > OFFLOAD_HAL_VERSION_1_0) { Log.wtf(TAG, "onEvent(" + event + ") fired on HAL " + halVerToString(mOffloadControlVersion)); } handler.post(() -> { handleOnEvent(event); }); } @Override public void onEvent_1_1(int event) { if (mOffloadControlVersion < OFFLOAD_HAL_VERSION_1_1) { Log.wtf(TAG, "onEvent_1_1(" + event + ") fired on HAL " + halVerToString(mOffloadControlVersion)); return; } handler.post(() -> { handleOnEvent(event); }); } @Override public void updateTimeout(NatTimeoutUpdate params) { handler.post(() -> { controlCb.onNatTimeoutUpdate( networkProtocolToOsConstant(params.proto), params.src.addr, uint16(params.src.port), params.dst.addr, uint16(params.dst.port)); }); } } private static int networkProtocolToOsConstant(int proto) { switch (proto) { case NetworkProtocol.TCP: return OsConstants.IPPROTO_TCP; case NetworkProtocol.UDP: return OsConstants.IPPROTO_UDP; default: // The caller checks this value and will log an error. Just make // sure it won't collide with valid OsContants.IPPROTO_* values. return -Math.abs(proto); } } private static class CbResults { boolean mSuccess; String mErrMsg; @Override public String toString() { if (mSuccess) { return "ok"; } else { return "fail: " + mErrMsg; } } } }