summaryrefslogtreecommitdiff
path: root/common/testutils/devicetests/com/android/testutils/NatPacketForwarderBase.java
blob: 0a2b5d418c5ab983be7803b7ea0374f6ba335bb7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
/*
 * Copyright (C) 2023 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.testutils;

import static com.android.testutils.PacketReflector.IPPROTO_TCP;
import static com.android.testutils.PacketReflector.IPPROTO_UDP;
import static com.android.testutils.PacketReflector.IPV4_HEADER_LENGTH;
import static com.android.testutils.PacketReflector.IPV6_HEADER_LENGTH;
import static com.android.testutils.PacketReflector.IPV6_PROTO_OFFSET;
import static com.android.testutils.PacketReflector.TCP_HEADER_LENGTH;
import static com.android.testutils.PacketReflector.UDP_HEADER_LENGTH;

import android.annotation.NonNull;
import android.net.TestNetworkInterface;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;

import androidx.annotation.GuardedBy;

import java.io.FileDescriptor;
import java.io.IOException;
import java.net.InetAddress;
import java.util.Objects;

/**
 * A class that forwards packets from a {@link TestNetworkInterface} to another
 * {@link TestNetworkInterface} with NAT.
 *
 * For testing purposes, a {@link TestNetworkInterface} provides a {@link FileDescriptor}
 * which allows content injection on the test network. However, this could be hard to use
 * because the callers need to compose IP packets in order to inject content to the
 * test network.
 *
 * In order to remove the need of composing the IP packets, this class forwards IP packets to
 * the {@link FileDescriptor} of another {@link TestNetworkInterface} instance. Thus,
 * the TCP/IP headers could be parsed/composed automatically by the protocol stack of this
 * additional {@link TestNetworkInterface}, while the payload is supplied by the
 * servers run on the interface.
 *
 * To make it work, an internal interface and an external interface are defined, where
 * the client might send packets from the internal interface which are originated from
 * multiple addresses to a server that listens on the external address.
 *
 * When forwarding the outgoing packet on the internal interface, a simple NAT mechanism
 * is implemented during forwarding, which will swap the source and destination,
 * but replacing the source address with the external address,
 * e.g. 192.168.1.1:1234 -> 8.8.8.8:80 will be translated into 8.8.8.8:1234 -> 1.2.3.4:80.
 *
 * For the above example, a client who sends http request will have a hallucination that
 * it is talking to a remote server at 8.8.8.8. Also, the server listens on 1.2.3.4 will
 * have a different hallucination that the request is sent from a remote client at 8.8.8.8,
 * to a local address 1.2.3.4.
 *
 * And a NAT mapping is created at the time when the outgoing packet is forwarded.
 * With a different internal source port, the instance learned that when a response with the
 * destination port 1234, it should forward the packet to the internal address 192.168.1.1.
 *
 * For the incoming packet received from external interface, for example a http response sent
 * from the http server, the same mechanism is applied but in a different direction,
 * where the source and destination will be swapped, and the source address will be replaced
 * with the internal address, which is obtained from the NAT mapping described above.
 */
public abstract class NatPacketForwarderBase extends Thread {
    private static final String TAG = "NatPacketForwarder";
    static final int DESTINATION_PORT_OFFSET = 2;

    // The source fd to read packets from.
    @NonNull
    final FileDescriptor mSrcFd;
    // The buffer to temporarily hold the entire packet after receiving.
    @NonNull
    final byte[] mBuf;
    // The destination fd to write packets to.
    @NonNull
    final FileDescriptor mDstFd;
    // The NAT mapping table shared between two NatPacketForwarder instances to map from
    // the source port to the associated internal address. The map can be read/write from two
    // different threads on any given time whenever receiving packets on the
    // {@link TestNetworkInterface}. Thus, synchronize on the object when reading/writing is needed.
    @GuardedBy("mNatMap")
    @NonNull
    final PacketBridge.NatMap mNatMap;
    // The address of the external interface. See {@link NatPacketForwarder}.
    @NonNull
    final InetAddress mExtAddr;

    /**
     * Construct a {@link NatPacketForwarderBase}.
     *
     * This class reads packets from {@code srcFd} of a {@link TestNetworkInterface}, and
     * forwards them to the {@code dstFd} of another {@link TestNetworkInterface} with
     * NAT applied. See {@link NatPacketForwarderBase}.
     *
     * To apply NAT, the address of the external interface needs to be supplied through
     * {@code extAddr} to identify the external interface. And a shared NAT mapping table,
     * {@code natMap} is needed to be shared between these two instances.
     *
     * Note that this class is not useful if the instance is not managed by a
     * {@link PacketBridge} to set up a two-way communication.
     *
     * @param srcFd   {@link FileDescriptor} to read packets from.
     * @param mtu     MTU of the test network.
     * @param dstFd   {@link FileDescriptor} to write packets to.
     * @param extAddr the external address, which is the address of the external interface.
     *                See {@link NatPacketForwarderBase}.
     * @param natMap  the NAT mapping table shared between two {@link NatPacketForwarderBase}
     *                instance.
     */
    public NatPacketForwarderBase(@NonNull FileDescriptor srcFd, int mtu,
            @NonNull FileDescriptor dstFd, @NonNull InetAddress extAddr,
            @NonNull PacketBridge.NatMap natMap) {
        super(TAG);
        mSrcFd = Objects.requireNonNull(srcFd);
        mBuf = new byte[mtu];
        mDstFd = Objects.requireNonNull(dstFd);
        mExtAddr = Objects.requireNonNull(extAddr);
        mNatMap = Objects.requireNonNull(natMap);
    }

    /**
     * A method to prepare forwarding packets between two instances of {@link TestNetworkInterface},
     * which includes re-write addresses, ports and fix up checksums.
     * Subclasses should override this method to implement a simple NAT.
     */
    abstract void preparePacketForForwarding(@NonNull byte[] buf, int len, int version, int proto);

    private void forwardPacket(@NonNull byte[] buf, int len) {
        try {
            Os.write(mDstFd, buf, 0, len);
        } catch (ErrnoException | IOException e) {
            Log.e(TAG, "Error writing packet: " + e.getMessage());
        }
    }

    // Reads one packet from mSrcFd, and writes the packet to the mDstFd for supported protocols.
    private void processPacket() {
        final int len = PacketReflectorUtil.readPacket(mSrcFd, mBuf);
        if (len < 1) {
            // Usually happens when socket read is being interrupted, e.g. stopping PacketForwarder.
            return;
        }

        final int version = mBuf[0] >>> 4;
        final int protoPos, ipHdrLen;
        switch (version) {
            case 4:
                ipHdrLen = IPV4_HEADER_LENGTH;
                protoPos = PacketReflector.IPV4_PROTO_OFFSET;
                break;
            case 6:
                ipHdrLen = IPV6_HEADER_LENGTH;
                protoPos = IPV6_PROTO_OFFSET;
                break;
            default:
                throw new IllegalStateException("Unexpected version: " + version);
        }
        if (len < ipHdrLen) {
            throw new IllegalStateException("Unexpected buffer length: " + len);
        }

        final byte proto = mBuf[protoPos];
        final int transportHdrLen;
        switch (proto) {
            case IPPROTO_TCP:
                transportHdrLen = TCP_HEADER_LENGTH;
                break;
            case IPPROTO_UDP:
                transportHdrLen = UDP_HEADER_LENGTH;
                break;
            // TODO: Support ICMP.
            default:
                return; // Unknown protocol, ignored.
        }

        if (len < ipHdrLen + transportHdrLen) {
            throw new IllegalStateException("Unexpected buffer length: " + len);
        }
        // Re-write addresses, ports and fix up checksums.
        preparePacketForForwarding(mBuf, len, version, proto);
        // Send the packet to the destination fd.
        forwardPacket(mBuf, len);
    }

    @Override
    public void run() {
        Log.i(TAG, "starting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
        while (!interrupted() && mSrcFd.valid()) {
            processPacket();
        }
        Log.i(TAG, "exiting fd=" + mSrcFd + " valid=" + mSrcFd.valid());
    }
}