Merge changes I8cd6e49b,Ibb52c7b7

* changes:
  [NFCT.TETHER.10] Add/delete IPv4 offload BPF rules to/from BPF map
  [NFCT.TETHER.9] Build IPv4 offload BPF rules for raw ip
diff --git a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
index eafa3ea..90b9b3f 100644
--- a/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/30/com/android/networkstack/tethering/apishim/api30/BpfCoordinatorShimImpl.java
@@ -28,7 +28,11 @@
 
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.TetherDownstream4Key;
+import com.android.networkstack.tethering.TetherDownstream4Value;
 import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream4Key;
+import com.android.networkstack.tethering.TetherUpstream4Value;
 
 /**
  * Bpf coordinator class for API shims.
@@ -132,6 +136,32 @@
     }
 
     @Override
+    public boolean tetherOffloadRuleAdd(@NonNull TetherDownstream4Key key,
+            @NonNull TetherDownstream4Value value) {
+        /* no op */
+        return true;
+    }
+
+    @Override
+    public boolean tetherOffloadRuleRemove(@NonNull TetherDownstream4Key key) {
+        /* no op */
+        return true;
+    }
+
+    @Override
+    public boolean tetherOffloadRuleAdd(@NonNull TetherUpstream4Key key,
+            @NonNull TetherUpstream4Value value) {
+        /* no op */
+        return true;
+    }
+
+    @Override
+    public boolean tetherOffloadRuleRemove(@NonNull TetherUpstream4Key key) {
+        /* no op */
+        return true;
+    }
+
+    @Override
     public String toString() {
         return "Netd used";
     }
diff --git a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
index c0d85ae..b9ce769 100644
--- a/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
+++ b/Tethering/apishim/31/com/android/networkstack/tethering/apishim/api31/BpfCoordinatorShimImpl.java
@@ -30,12 +30,16 @@
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.BpfMap;
+import com.android.networkstack.tethering.TetherDownstream4Key;
+import com.android.networkstack.tethering.TetherDownstream4Value;
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherDownstream6Value;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
 import com.android.networkstack.tethering.TetherStatsKey;
 import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream4Key;
+import com.android.networkstack.tethering.TetherUpstream4Value;
 
 import java.io.FileDescriptor;
 
@@ -54,6 +58,16 @@
     @NonNull
     private final SharedLog mLog;
 
+    // BPF map of ingress queueing discipline which pre-processes the packets by the IPv4
+    // downstream rules.
+    @Nullable
+    private final BpfMap<TetherDownstream4Key, TetherDownstream4Value> mBpfDownstream4Map;
+
+    // BPF map of ingress queueing discipline which pre-processes the packets by the IPv4
+    // upstream rules.
+    @Nullable
+    private final BpfMap<TetherUpstream4Key, TetherUpstream4Value> mBpfUpstream4Map;
+
     // BPF map of ingress queueing discipline which pre-processes the packets by the IPv6
     // forwarding rules.
     @Nullable
@@ -69,6 +83,8 @@
 
     public BpfCoordinatorShimImpl(@NonNull final Dependencies deps) {
         mLog = deps.getSharedLog().forSubComponent(TAG);
+        mBpfDownstream4Map = deps.getBpfDownstream4Map();
+        mBpfUpstream4Map = deps.getBpfUpstream4Map();
         mBpfDownstream6Map = deps.getBpfDownstream6Map();
         mBpfStatsMap = deps.getBpfStatsMap();
         mBpfLimitMap = deps.getBpfLimitMap();
@@ -76,7 +92,8 @@
 
     @Override
     public boolean isInitialized() {
-        return mBpfDownstream6Map != null && mBpfStatsMap != null  && mBpfLimitMap != null;
+        return mBpfDownstream4Map != null && mBpfUpstream4Map != null && mBpfDownstream6Map != null
+                && mBpfStatsMap != null && mBpfLimitMap != null;
     }
 
     @Override
@@ -233,14 +250,85 @@
     }
 
     @Override
+    public boolean tetherOffloadRuleAdd(@NonNull TetherDownstream4Key key,
+            @NonNull TetherDownstream4Value value) {
+        if (!isInitialized()) return false;
+
+        try {
+            // The last used time field of the value is updated by the bpf program. Adding the same
+            // map pair twice causes the unexpected refresh. Must be fixed before starting the
+            // conntrack timeout extension implementation.
+            // TODO: consider using insertEntry.
+            mBpfDownstream4Map.updateEntry(key, value);
+        } catch (ErrnoException e) {
+            mLog.e("Could not update entry: ", e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean tetherOffloadRuleRemove(@NonNull TetherDownstream4Key key) {
+        if (!isInitialized()) return false;
+
+        try {
+            mBpfDownstream4Map.deleteEntry(key);
+        } catch (ErrnoException e) {
+            // Silent if the rule did not exist.
+            if (e.errno != OsConstants.ENOENT) {
+                mLog.e("Could not delete entry: ", e);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public boolean tetherOffloadRuleAdd(@NonNull TetherUpstream4Key key,
+            @NonNull TetherUpstream4Value value) {
+        if (!isInitialized()) return false;
+
+        try {
+            // The last used time field of the value is updated by the bpf program. Adding the same
+            // map pair twice causes the unexpected refresh. Must be fixed before starting the
+            // conntrack timeout extension implementation.
+            // TODO: consider using insertEntry.
+            mBpfUpstream4Map.updateEntry(key, value);
+        } catch (ErrnoException e) {
+            mLog.e("Could not update entry: ", e);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean tetherOffloadRuleRemove(@NonNull TetherUpstream4Key key) {
+        if (!isInitialized()) return false;
+
+        try {
+            mBpfUpstream4Map.deleteEntry(key);
+        } catch (ErrnoException e) {
+            // Silent if the rule did not exist.
+            if (e.errno != OsConstants.ENOENT) {
+                mLog.e("Could not delete entry: ", e);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
     public String toString() {
-        return "mBpfDownstream6Map{"
+        return "mBpfDownstream4Map{"
+                + (mBpfDownstream4Map != null ? "initialized" : "not initialized") + "}, "
+                + "mBpfUpstream4Map{"
+                + (mBpfUpstream4Map != null ? "initialized" : "not initialized") + "}, "
+                + "mBpfDownstream6Map{"
                 + (mBpfDownstream6Map != null ? "initialized" : "not initialized") + "}, "
                 + "mBpfStatsMap{"
                 + (mBpfStatsMap != null ? "initialized" : "not initialized") + "}, "
                 + "mBpfLimitMap{"
-                + (mBpfLimitMap != null ? "initialized" : "not initialized") + "} "
-                + "}";
+                + (mBpfLimitMap != null ? "initialized" : "not initialized") + "} ";
     }
 
     /**
diff --git a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
index 61abfa3..36d2de1 100644
--- a/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
+++ b/Tethering/apishim/common/com/android/networkstack/tethering/apishim/common/BpfCoordinatorShim.java
@@ -23,7 +23,11 @@
 
 import com.android.networkstack.tethering.BpfCoordinator.Dependencies;
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
+import com.android.networkstack.tethering.TetherDownstream4Key;
+import com.android.networkstack.tethering.TetherDownstream4Value;
 import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream4Key;
+import com.android.networkstack.tethering.TetherUpstream4Value;
 
 /**
  * Bpf coordinator class for API shims.
@@ -108,5 +112,27 @@
      */
     @Nullable
     public abstract TetherStatsValue tetherOffloadGetAndClearStats(int ifIndex);
+
+    /**
+     * Adds a tethering IPv4 downstream offload rule to BPF map.
+     */
+    public abstract boolean tetherOffloadRuleAdd(@NonNull TetherDownstream4Key key,
+            @NonNull TetherDownstream4Value value);
+
+    /**
+     * Deletes a tethering IPv4 downstream offload rule from the BPF map.
+     */
+    public abstract boolean tetherOffloadRuleRemove(@NonNull TetherDownstream4Key key);
+
+    /**
+     * Adds a tethering IPv4 upstream offload rule to BPF map.
+     */
+    public abstract boolean tetherOffloadRuleAdd(@NonNull TetherUpstream4Key key,
+            @NonNull TetherUpstream4Value value);
+
+    /**
+     * Deletes a tethering IPv4 upstream offload rule from the BPF map.
+     */
+    public abstract boolean tetherOffloadRuleRemove(@NonNull TetherUpstream4Key key);
 }
 
diff --git a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
index 3268e94..e4216d8 100644
--- a/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
+++ b/Tethering/src/com/android/networkstack/tethering/BpfCoordinator.java
@@ -23,7 +23,9 @@
 import static android.net.NetworkStats.TAG_NONE;
 import static android.net.NetworkStats.UID_ALL;
 import static android.net.NetworkStats.UID_TETHERING;
+import static android.net.ip.ConntrackMonitor.ConntrackEvent;
 import static android.net.netstats.provider.NetworkStatsProvider.QUOTA_UNLIMITED;
+import static android.system.OsConstants.ETH_P_IP;
 import static android.system.OsConstants.ETH_P_IPV6;
 
 import static com.android.networkstack.tethering.TetheringConfiguration.DEFAULT_TETHER_OFFLOAD_POLL_INTERVAL_MS;
@@ -38,6 +40,7 @@
 import android.net.ip.ConntrackMonitor;
 import android.net.ip.ConntrackMonitor.ConntrackEventConsumer;
 import android.net.ip.IpServer;
+import android.net.netlink.NetlinkConstants;
 import android.net.netstats.provider.NetworkStatsProvider;
 import android.net.util.InterfaceParams;
 import android.net.util.SharedLog;
@@ -82,6 +85,12 @@
 public class BpfCoordinator {
     private static final String TAG = BpfCoordinator.class.getSimpleName();
     private static final int DUMP_TIMEOUT_MS = 10_000;
+    private static final MacAddress NULL_MAC_ADDRESS = MacAddress.fromString(
+            "00:00:00:00:00:00");
+    private static final String TETHER_DOWNSTREAM4_MAP_PATH =
+            "/sys/fs/bpf/tethering/map_offload_tether_downstream4_map";
+    private static final String TETHER_UPSTREAM4_MAP_PATH =
+            "/sys/fs/bpf/tethering/map_offload_tether_upstream4_map";
     private static final String TETHER_DOWNSTREAM6_FS_PATH =
             "/sys/fs/bpf/tethering/map_offload_tether_downstream6_map";
     private static final String TETHER_STATS_MAP_PATH =
@@ -174,6 +183,9 @@
     // - Must only be modified by that IpServer.
     // - Is created when the IpServer adds its first client, and deleted when the IpServer deletes
     //   its last client.
+    // Note that relying on the client address for finding downstream is okay for now because the
+    // client address is unique. See PrivateAddressCoordinator#requestDownstreamAddress.
+    // TODO: Refactor if any possible that the client address is not unique.
     private final HashMap<IpServer, HashMap<Inet4Address, ClientInfo>>
             mTetherClients = new HashMap<>();
 
@@ -224,6 +236,30 @@
             return SdkLevel.isAtLeastS();
         }
 
+        /** Get downstream4 BPF map. */
+        @Nullable public BpfMap<TetherDownstream4Key, TetherDownstream4Value>
+                getBpfDownstream4Map() {
+            try {
+                return new BpfMap<>(TETHER_DOWNSTREAM4_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, TetherDownstream4Key.class, TetherDownstream4Value.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create downstream4 map: " + e);
+                return null;
+            }
+        }
+
+        /** Get upstream4 BPF map. */
+        @Nullable public BpfMap<TetherUpstream4Key, TetherUpstream4Value>
+                getBpfUpstream4Map() {
+            try {
+                return new BpfMap<>(TETHER_UPSTREAM4_MAP_PATH,
+                    BpfMap.BPF_F_RDWR, TetherUpstream4Key.class, TetherUpstream4Value.class);
+            } catch (ErrnoException e) {
+                Log.e(TAG, "Cannot create upstream4 map: " + e);
+                return null;
+            }
+        }
+
         /** Get downstream6 BPF map. */
         @Nullable public BpfMap<TetherDownstream6Key, TetherDownstream6Value>
                 getBpfDownstream6Map() {
@@ -854,8 +890,97 @@
         }
     }
 
+    @Nullable
+    private ClientInfo getClientInfo(@NonNull Inet4Address clientAddress) {
+        for (HashMap<Inet4Address, ClientInfo> clients : mTetherClients.values()) {
+            for (ClientInfo client : clients.values()) {
+                if (clientAddress.equals(client.clientAddress)) {
+                    return client;
+                }
+            }
+        }
+        return null;
+    }
+
+    // Support raw ip only.
+    // TODO: add ether ip support.
     private class BpfConntrackEventConsumer implements ConntrackEventConsumer {
-        public void accept(ConntrackMonitor.ConntrackEvent e) { /* TODO */ }
+        @NonNull
+        private TetherUpstream4Key makeTetherUpstream4Key(
+                @NonNull ConntrackEvent e, @NonNull ClientInfo c) {
+            return new TetherUpstream4Key(c.downstreamIfindex, c.downstreamMac,
+                    e.tupleOrig.protoNum, e.tupleOrig.srcIp.getAddress(),
+                    e.tupleOrig.dstIp.getAddress(), e.tupleOrig.srcPort, e.tupleOrig.dstPort);
+        }
+
+        @NonNull
+        private TetherDownstream4Key makeTetherDownstream4Key(
+                @NonNull ConntrackEvent e, @NonNull ClientInfo c, int upstreamIndex) {
+            return new TetherDownstream4Key(upstreamIndex, NULL_MAC_ADDRESS /* dstMac (rawip) */,
+                    e.tupleReply.protoNum, e.tupleReply.srcIp.getAddress(),
+                    e.tupleReply.dstIp.getAddress(), e.tupleReply.srcPort, e.tupleReply.dstPort);
+        }
+
+        @NonNull
+        private TetherUpstream4Value makeTetherUpstream4Value(@NonNull ConntrackEvent e,
+                int upstreamIndex) {
+            return new TetherUpstream4Value(upstreamIndex,
+                    NULL_MAC_ADDRESS /* ethDstMac (rawip) */,
+                    NULL_MAC_ADDRESS /* ethSrcMac (rawip) */, ETH_P_IP,
+                    NetworkStackConstants.ETHER_MTU, toIpv4MappedAddressBytes(e.tupleReply.dstIp),
+                    toIpv4MappedAddressBytes(e.tupleReply.srcIp), e.tupleReply.dstPort,
+                    e.tupleReply.srcPort, 0 /* lastUsed, filled by bpf prog only */);
+        }
+
+        @NonNull
+        private TetherDownstream4Value makeTetherDownstream4Value(@NonNull ConntrackEvent e,
+                @NonNull ClientInfo c, int upstreamIndex) {
+            return new TetherDownstream4Value(c.downstreamIfindex,
+                    c.clientMac, c.downstreamMac, ETH_P_IP, NetworkStackConstants.ETHER_MTU,
+                    e.tupleOrig.dstIp.getAddress(), e.tupleOrig.srcIp.getAddress(),
+                    e.tupleOrig.dstPort, e.tupleOrig.srcPort,
+                    0 /* lastUsed, filled by bpf prog only */);
+        }
+
+        @NonNull
+        private byte[] toIpv4MappedAddressBytes(Inet4Address ia4) {
+            final byte[] addr4 = ia4.getAddress();
+            final byte[] addr6 = new byte[16];
+            addr6[10] = (byte) 0xff;
+            addr6[11] = (byte) 0xff;
+            addr6[12] = addr4[0];
+            addr6[13] = addr4[1];
+            addr6[14] = addr4[2];
+            addr6[15] = addr4[3];
+            return addr6;
+        }
+
+        public void accept(ConntrackEvent e) {
+            final ClientInfo tetherClient = getClientInfo(e.tupleOrig.srcIp);
+            if (tetherClient == null) return;
+
+            final Integer upstreamIndex = mIpv4UpstreamIndices.get(e.tupleReply.dstIp);
+            if (upstreamIndex == null) return;
+
+            final TetherUpstream4Key upstream4Key = makeTetherUpstream4Key(e, tetherClient);
+            final TetherDownstream4Key downstream4Key = makeTetherDownstream4Key(e,
+                    tetherClient, upstreamIndex);
+
+            if (e.msgType == (NetlinkConstants.NFNL_SUBSYS_CTNETLINK << 8
+                    | NetlinkConstants.IPCTNL_MSG_CT_DELETE)) {
+                mBpfCoordinatorShim.tetherOffloadRuleRemove(upstream4Key);
+                mBpfCoordinatorShim.tetherOffloadRuleRemove(downstream4Key);
+                return;
+            }
+
+            final TetherUpstream4Value upstream4Value = makeTetherUpstream4Value(e,
+                    upstreamIndex);
+            final TetherDownstream4Value downstream4Value = makeTetherDownstream4Value(e,
+                    tetherClient, upstreamIndex);
+
+            mBpfCoordinatorShim.tetherOffloadRuleAdd(upstream4Key, upstream4Value);
+            mBpfCoordinatorShim.tetherOffloadRuleAdd(downstream4Key, downstream4Value);
+        }
     }
 
     private boolean isBpfEnabled() {
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDownstream4Key.java b/Tethering/src/com/android/networkstack/tethering/TetherDownstream4Key.java
new file mode 100644
index 0000000..51b1f76
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDownstream4Key.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 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 android.net.MacAddress;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** The key of BpfMap which is used for IPv4 bpf offload. */
+public class TetherDownstream4Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif;
+
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress dstMac;
+
+    @Field(order = 2, type = Type.U8, padding = 1)
+    public final short l4proto;
+
+    @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+    public final byte[] src4;
+
+    @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+    public final byte[] dst4;
+
+    @Field(order = 5, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 6, type = Type.UBE16)
+    public final int dstPort;
+
+    public TetherDownstream4Key(final long iif, final MacAddress dstMac, final short l4proto,
+            final byte[] src4, final byte[] dst4, final int srcPort,
+            final int dstPort) {
+        Objects.requireNonNull(dstMac);
+
+        this.iif = iif;
+        this.dstMac = dstMac;
+        this.l4proto = l4proto;
+        this.src4 = src4;
+        this.dst4 = dst4;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+                            + "srcPort: %d, dstPort: %d",
+                    iif, dstMac, l4proto,
+                    Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherDownstream4Value.java b/Tethering/src/com/android/networkstack/tethering/TetherDownstream4Value.java
new file mode 100644
index 0000000..56ea566
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherDownstream4Value.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 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 android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** The value of BpfMap which is used for IPv4 bpf offload. */
+public class TetherDownstream4Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif;
+
+    // The ethhdr struct which is defined in uapi/linux/if_ether.h
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress ethDstMac;
+    @Field(order = 2, type = Type.EUI48)
+    public final MacAddress ethSrcMac;
+    @Field(order = 3, type = Type.UBE16)
+    public final int ethProto;  // Packet type ID field.
+
+    @Field(order = 4, type = Type.U16)
+    public final int pmtu;
+
+    @Field(order = 5, type = Type.ByteArray, arraysize = 4)
+    public final byte[] src4;
+
+    @Field(order = 6, type = Type.ByteArray, arraysize = 4)
+    public final byte[] dst4;
+
+    @Field(order = 7, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 8, type = Type.UBE16)
+    public final int dstPort;
+
+    // TODO: consider using U64.
+    @Field(order = 9, type = Type.U63)
+    public final long lastUsed;
+
+    public TetherDownstream4Value(final long oif, @NonNull final MacAddress ethDstMac,
+            @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+            final byte[] src4, final byte[] dst4, final int srcPort,
+            final int dstPort, final long lastUsed) {
+        Objects.requireNonNull(ethDstMac);
+        Objects.requireNonNull(ethSrcMac);
+
+        this.oif = oif;
+        this.ethDstMac = ethDstMac;
+        this.ethSrcMac = ethSrcMac;
+        this.ethProto = ethProto;
+        this.pmtu = pmtu;
+        this.src4 = src4;
+        this.dst4 = dst4;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.lastUsed = lastUsed;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+                            + "src4: %s, dst4: %s, srcPort: %d, dstPort: %d, "
+                            + "lastUsed: %d",
+                    oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+                    InetAddress.getByAddress(src4), InetAddress.getByAddress(dst4),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+                    lastUsed);
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream4Key.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream4Key.java
new file mode 100644
index 0000000..77cfa99
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream4Key.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 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 android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** The key of BpfMap which is used for IPv4 bpf offload. */
+public class TetherUpstream4Key extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long iif;
+
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress dstMac;
+
+    @Field(order = 2, type = Type.U8, padding = 1)
+    public final short l4proto;
+
+    @Field(order = 3, type = Type.ByteArray, arraysize = 4)
+    public final byte[] src4;
+
+    @Field(order = 4, type = Type.ByteArray, arraysize = 4)
+    public final byte[] dst4;
+
+    @Field(order = 5, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 6, type = Type.UBE16)
+    public final int dstPort;
+
+    public TetherUpstream4Key(final long iif, @NonNull final MacAddress dstMac, final short l4proto,
+            final byte[] src4, final byte[] dst4, final int srcPort,
+            final int dstPort) {
+        Objects.requireNonNull(dstMac);
+
+        this.iif = iif;
+        this.dstMac = dstMac;
+        this.l4proto = l4proto;
+        this.src4 = src4;
+        this.dst4 = dst4;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "iif: %d, dstMac: %s, l4proto: %d, src4: %s, dst4: %s, "
+                            + "srcPort: %d, dstPort: %d",
+                    iif, dstMac, l4proto,
+                    Inet4Address.getByAddress(src4), Inet4Address.getByAddress(dst4),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort));
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/Tethering/src/com/android/networkstack/tethering/TetherUpstream4Value.java b/Tethering/src/com/android/networkstack/tethering/TetherUpstream4Value.java
new file mode 100644
index 0000000..e1ff688
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/TetherUpstream4Value.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2020 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 android.net.MacAddress;
+
+import androidx.annotation.NonNull;
+
+import com.android.net.module.util.Struct;
+import com.android.net.module.util.Struct.Field;
+import com.android.net.module.util.Struct.Type;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/** The value of BpfMap which is used for IPv4 bpf offload. */
+public class TetherUpstream4Value extends Struct {
+    @Field(order = 0, type = Type.U32)
+    public final long oif;
+
+    // The ethhdr struct which is defined in uapi/linux/if_ether.h
+    @Field(order = 1, type = Type.EUI48)
+    public final MacAddress ethDstMac;
+    @Field(order = 2, type = Type.EUI48)
+    public final MacAddress ethSrcMac;
+    @Field(order = 3, type = Type.UBE16)
+    public final int ethProto;  // Packet type ID field.
+
+    @Field(order = 4, type = Type.U16)
+    public final int pmtu;
+
+    @Field(order = 5, type = Type.ByteArray, arraysize = 16)
+    public final byte[] src46;
+
+    @Field(order = 6, type = Type.ByteArray, arraysize = 16)
+    public final byte[] dst46;
+
+    @Field(order = 7, type = Type.UBE16)
+    public final int srcPort;
+
+    @Field(order = 8, type = Type.UBE16)
+    public final int dstPort;
+
+    // TODO: consider using U64.
+    @Field(order = 9, type = Type.U63)
+    public final long lastUsed;
+
+    public TetherUpstream4Value(final long oif, @NonNull final MacAddress ethDstMac,
+            @NonNull final MacAddress ethSrcMac, final int ethProto, final int pmtu,
+            final byte[] src46, final byte[] dst46, final int srcPort,
+            final int dstPort, final long lastUsed) {
+        Objects.requireNonNull(ethDstMac);
+        Objects.requireNonNull(ethSrcMac);
+
+        this.oif = oif;
+        this.ethDstMac = ethDstMac;
+        this.ethSrcMac = ethSrcMac;
+        this.ethProto = ethProto;
+        this.pmtu = pmtu;
+        this.src46 = src46;
+        this.dst46 = dst46;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.lastUsed = lastUsed;
+    }
+
+    @Override
+    public String toString() {
+        try {
+            return String.format(
+                    "oif: %d, ethDstMac: %s, ethSrcMac: %s, ethProto: %d, pmtu: %d, "
+                            + "src46: %s, dst46: %s, srcPort: %d, dstPort: %d, "
+                            + "lastUsed: %d",
+                    oif, ethDstMac, ethSrcMac, ethProto, pmtu,
+                    InetAddress.getByAddress(src46), InetAddress.getByAddress(dst46),
+                    Short.toUnsignedInt((short) srcPort), Short.toUnsignedInt((short) dstPort),
+                    lastUsed);
+        } catch (UnknownHostException | IllegalArgumentException e) {
+            return String.format("Invalid IP address", e);
+        }
+    }
+}
diff --git a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
index e20e011..9b42c73 100644
--- a/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
+++ b/Tethering/tests/unit/src/android/net/ip/IpServerTest.java
@@ -104,12 +104,16 @@
 import com.android.networkstack.tethering.BpfCoordinator.Ipv6ForwardingRule;
 import com.android.networkstack.tethering.BpfMap;
 import com.android.networkstack.tethering.PrivateAddressCoordinator;
+import com.android.networkstack.tethering.TetherDownstream4Key;
+import com.android.networkstack.tethering.TetherDownstream4Value;
 import com.android.networkstack.tethering.TetherDownstream6Key;
 import com.android.networkstack.tethering.TetherDownstream6Value;
 import com.android.networkstack.tethering.TetherLimitKey;
 import com.android.networkstack.tethering.TetherLimitValue;
 import com.android.networkstack.tethering.TetherStatsKey;
 import com.android.networkstack.tethering.TetherStatsValue;
+import com.android.networkstack.tethering.TetherUpstream4Key;
+import com.android.networkstack.tethering.TetherUpstream4Value;
 import com.android.networkstack.tethering.TetheringConfiguration;
 import com.android.testutils.DevSdkIgnoreRule;
 import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
@@ -173,6 +177,8 @@
     @Mock private NetworkStatsManager mStatsManager;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
+    @Mock private BpfMap<TetherDownstream4Key, TetherDownstream4Value> mBpfDownstream4Map;
+    @Mock private BpfMap<TetherUpstream4Key, TetherUpstream4Value> mBpfUpstream4Map;
     @Mock private BpfMap<TetherDownstream6Key, TetherDownstream6Value> mBpfDownstream6Map;
     @Mock private BpfMap<TetherStatsKey, TetherStatsValue> mBpfStatsMap;
     @Mock private BpfMap<TetherLimitKey, TetherLimitValue> mBpfLimitMap;
@@ -303,6 +309,18 @@
                     }
 
                     @Nullable
+                    public BpfMap<TetherDownstream4Key, TetherDownstream4Value>
+                            getBpfDownstream4Map() {
+                        return mBpfDownstream4Map;
+                    }
+
+                    @Nullable
+                    public BpfMap<TetherUpstream4Key, TetherUpstream4Value>
+                            getBpfUpstream4Map() {
+                        return mBpfUpstream4Map;
+                    }
+
+                    @Nullable
                     public BpfMap<TetherDownstream6Key, TetherDownstream6Value>
                             getBpfDownstream6Map() {
                         return mBpfDownstream6Map;
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
index 764e651..30b4bf4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/BpfCoordinatorTest.java
@@ -158,6 +158,8 @@
     @Mock private IpServer mIpServer2;
     @Mock private TetheringConfiguration mTetherConfig;
     @Mock private ConntrackMonitor mConntrackMonitor;
+    @Mock private BpfMap<TetherDownstream4Key, TetherDownstream4Value> mBpfDownstream4Map;
+    @Mock private BpfMap<TetherUpstream4Key, TetherUpstream4Value> mBpfUpstream4Map;
     @Mock private BpfMap<TetherDownstream6Key, TetherDownstream6Value> mBpfDownstream6Map;
 
     // Late init since methods must be called by the thread that created this object.
@@ -203,6 +205,18 @@
                     }
 
                     @Nullable
+                    public BpfMap<TetherDownstream4Key, TetherDownstream4Value>
+                            getBpfDownstream4Map() {
+                        return mBpfDownstream4Map;
+                    }
+
+                    @Nullable
+                    public BpfMap<TetherUpstream4Key, TetherUpstream4Value>
+                            getBpfUpstream4Map() {
+                        return mBpfUpstream4Map;
+                    }
+
+                    @Nullable
                     public BpfMap<TetherDownstream6Key, TetherDownstream6Value>
                             getBpfDownstream6Map() {
                         return mBpfDownstream6Map;