Byeo

4. P4 - P4runtime 본문

프로그래밍 (Programming)/P4

4. P4 - P4runtime

BKlee 2024. 6. 25. 23:20
반응형

우리는 지금까지 data plane의 packet의 format과 처리 방식을 정의하고 작성해왔습니다. 하지만 여기서 끝난다면 의미가 없죠. Control plane과 data plane이 어떻게 상호작용을 하는지도 알아야 P4의 의미가 더 강해질 것 같습니다. 이번 예제는 P4의 control plane과 관련해서 다룰 것으로 보입니다.

 

https://github.com/p4lang/tutorials/tree/master/exercises/p4runtime

 

tutorials/exercises/p4runtime at master · p4lang/tutorials

P4 language tutorials. Contribute to p4lang/tutorials development by creating an account on GitHub.

github.com

 

 

0. Topology

 

1. skeleton code

코드 시작은 2번에서 다뤘던 basic_tunnel.p4를 기반으로 합니다. 여기에 ingressTunnelCounter와 egressTunnelCounter, 그리고 myTunnelIngress와 myTunnelEgress action가 추가되어있습니다.

 

폴더에 mycontroller.py라는 파이썬 파일이 있습니다. 이 코드를 일부 살펴보면 몇몇의 ingress rule을 넣어주고 있는 것을 볼 수 있습니다.

 

    # 1) Tunnel Ingress Rule
    table_entry = p4info_helper.buildTableEntry(
        table_name="MyIngress.ipv4_lpm",
        match_fields={
            "hdr.ipv4.dstAddr": (dst_ip_addr, 32)
        },
        action_name="MyIngress.myTunnel_ingress",
        action_params={
            "dst_id": tunnel_id,
        })
    ingress_sw.WriteTableEntry(table_entry)

 

이 rule이 잘 들어가는지 확인해봅시다!

 

 

 

2. Test Run

make

mininet> h1 ping h2
PING 10.0.2.2 (10.0.2.2) 56(84) bytes of data.
^C
--- 10.0.2.2 ping statistics ---
3 packets transmitted, 0 received, 100% packet loss, time 2056ms

 

이 상황에서 또 다른 터미널을 열어 mycontroller.py를 실행해봅니다.

 

 

 

----- Reading tunnel counters -----
s1 MyIngress.ingressTunnelCounter 100: 6 packets (588 bytes)
s2 MyIngress.egressTunnelCounter 100: 0 packets (0 bytes)
s2 MyIngress.ingressTunnelCounter 200: 0 packets (0 bytes)
s1 MyIngress.egressTunnelCounter 200: 0 packets (0 bytes)

 

신기하죠! mycontroller.py에서 패킷 상태를 읽어서 통계를 표현하고 있습니다. 여기서 mininet이 data plane이고 mycontroller.py가 control plane이라고 보면 되겠습니다.

 

 

3. mycontroller.py

우선, controller.py와 P4 switch pipeline 사이에서 어떻게 주고받는지 짧게 알아보고 가려고 합니다. mycontroller.py에서는 p4runtime을 제어하기 위해서 p4runtime_lib.helper.P4InfoHelper라는 함수를 사용합니다.

p4info_helper = p4runtime_lib.helper.P4InfoHelper(p4info_file_path)

 

p4info_file_path는 ./build/advanced_tunnel.p4.p4info.txt 입니다. P4InfoHelper함수는 다음 깃허브에서 찾아볼 수 있었습니다: https://github.com/p4lang/tutorials/blob/master/utils/p4runtime_lib/helper.py

 

# 1) Tunnel Ingress Rule
    table_entry = p4info_helper.buildTableEntry(
        table_name="MyIngress.ipv4_lpm",
        match_fields={
            "hdr.ipv4.dstAddr": (dst_ip_addr, 32)
        },
        action_name="MyIngress.myTunnel_ingress",
        action_params={
            "dst_id": tunnel_id,
        })

 

buildTableEntry 함수도 같은 파일 (p4runtime_lib/helper.py)에 있습니다. 해당 함수 내에서는 table_entry = p4runtime_pb2.TableEntry() 를 통해 공간을 할당하고 내부에 적절한 match, action을 채워주는 것으로 보입니다.

 

마지막으로 ingress_sw.WriteTableEntry를 통해서 해당 함수를 switch에 추가합니다.

 

ingress_sw

ingress_sw는 뭘까요? switch는 main 함수에서 Bmv2SwitchConnection함수를 통해서 초기화합니다.

        s1 = p4runtime_lib.bmv2.Bmv2SwitchConnection(
            name='s1',
            address='127.0.0.1:50051',
            device_id=0,
            proto_dump_file='logs/s1-p4runtime-requests.txt')

 

이런식으로 스위치와 연결을 맺어주는 object를 생성하고, 이 object를 통해서 switch들을 자유롭게 제어합니다.

 

tutorial 문서에 설명된 mycontroller.py의 요약본을 번역하면 다음과 같습니다:

1. P4Runtime 서비스를 위해서 switch와 gRPC 연결을 맺는다.

2. P4 program을 각각의 스위치에 추가한다.

3. ingress tunnel과 egress tunnel을 h1과 h2 사이에 추가한다. 

4. ingress counter와 egress counter를 2초마다 읽어서 출력한다.

 

 

4. switch to switch

우선 table entry를 python에서 정의하고 어떻게 switch에 추가하는지 까지의 코드를 대략적으로 확인했습니다. 그러면 현재 추가하고 있는 룰은 뭘까요?

 

먼저 table apply 구조입니다.

    apply {
        if (hdr.ipv4.isValid() && !hdr.myTunnel.isValid()) {
            // Process only non-tunneled IPv4 packets.
            ipv4_lpm.apply();
        }

        if (hdr.myTunnel.isValid()) {
            // Process all tunneled packets.
            myTunnel_exact.apply();
        }
    }

 

일반 ip packet이 들어오면 ipv4_lpm table을 적용합니다. 그리고 ipv4_lpm table은 myTunnel_ingress action을 참조합니다.

4-1. ingress rule

    # 1) Tunnel Ingress Rule
    table_entry = p4info_helper.buildTableEntry(
        table_name="MyIngress.ipv4_lpm",
        match_fields={
            "hdr.ipv4.dstAddr": (dst_ip_addr, 32)
        },
        action_name="MyIngress.myTunnel_ingress",
        action_params={
            "dst_id": tunnel_id,
        })
    ingress_sw.WriteTableEntry(table_entry)
    print("Installed ingress tunnel rule on %s" % ingress_sw.name)

 

첫 번째로 추가되는 이 룰은 ipv4_lpm에 적용됩니다. 매칭 조건은 destination ip기준이며, 만약 매칭된다면 myTunnel_ingress action을 적용합니다. 

 

    action myTunnel_ingress(bit<16> dst_id) {
        hdr.myTunnel.setValid();
        hdr.myTunnel.dst_id = dst_id;
        hdr.myTunnel.proto_id = hdr.ethernet.etherType;
        hdr.ethernet.etherType = TYPE_MYTUNNEL;
        ingressTunnelCounter.count((bit<32>) hdr.myTunnel.dst_id);
    }

 

myTunnel_ingress는 hdr의 myTunnel field를 활성화 하고, packet header를 채워줍니다.

 

즉, 일반 | ethernet | ipv4 | packet을| ethernet | TUNNEL | ipv4 | packet으로 터널링하는 작업을 수행하는 것이죠.

 

 

4-2. switch to switch

현재 writeTunnelRules에는 switch to switch가 빠져있습니다. tunnel 된 packet을 수신했을 때 처리과정이 없죠. 설명에 맞게 작성해줍니다.

    # 2) Tunnel Transit Rule
    # The rule will need to be added to the myTunnel_exact table and match on
    # the tunnel ID (hdr.myTunnel.dst_id). Traffic will need to be forwarded
    # using the myTunnel_forward action on the port connected to the next switch.
    #
    # For our simple topology, switch 1 and switch 2 are connected using a
    # link attached to port 2 on both switches. We have defined a variable at
    # the top of the file, SWITCH_TO_SWITCH_PORT, that you can use as the output
    # port for this action.
    #
    # We will only need a transit rule on the ingress switch because we are
    # using a simple topology. In general, you'll need on transit rule for
    # each switch in the path (except the last switch, which has the egress rule),
    # and you will need to select the port dynamically for each switch based on
    # your topology.

    # TODO build the transit rule
    # TODO install the transit rule on the ingress switch
    table_entry = p4info_helper.buildTableEntry(
        table_name = "MyIngress.myTunnel_exact",
        match_fields={
            "hdr.myTunnel.dst_id": tunnel_id
        },
        action_name="MyIngress.myTunnel_forward",
        action_params={
            "port": SWITCH_TO_SWITCH_PORT,
        })
    ingress_sw.WriteTableEntry(table_entry)
    
    print("Installed ingress tunnel rule on %s" % ingress_sw.name)

 

  • table_name은 MyIngress control의 myTunnel_exact입니다. 여기에 추가해야 하죠.
  • match_fields는 hdr.myTunnel.dst_id를 보고 결정합니다. 
  • action_name은 advanced_tunnel.p4의 코드에 이미 작성되어있는 myTunnel_forward를 사용합니다. 
    • myTunnel_forward action을 참조해보면 parameter는 egressSpec_t port 하나입니다. 주석에 설명된대로 "port": SWITCH_TO_SWITCH_PORT (2)를 작성해줍니다.
  • 마지막으로 ingress_sw에 이 table entry를 추가해줍니다.

 

ingress switch, egress switch가 뭘 의미하는 것인지 약간 의문이 듭니다. 이 예제에서는 다소 비직관적이긴 하나, 단방향을 처리하기 위해서 table entry를 다음과 같이 나눠서 추가하고 있는 상황입니다. 

figure

 

4-3. egress rule

    # 3) Tunnel Egress Rule
    # For our simple topology, the host will always be located on the
    # SWITCH_TO_HOST_PORT (port 1).
    # In general, you will need to keep track of which port the host is
    # connected to.
    table_entry = p4info_helper.buildTableEntry(
        table_name="MyIngress.myTunnel_exact",
        match_fields={
            "hdr.myTunnel.dst_id": tunnel_id
        },
        action_name="MyIngress.myTunnel_egress",
        action_params={
            "dstAddr": dst_eth_addr,
            "port": SWITCH_TO_HOST_PORT
        })
    egress_sw.WriteTableEntry(table_entry)
    print("Installed egress tunnel rule on %s" % egress_sw.name)

 

마지막으로 egress switch에 egress rule을 추가해줍니다. 해당 table entry는 유입된 packet의 dest_id를 검사한 뒤, MyIngress control의 myTunnel_egerss action을 실행합니다.

 

    action myTunnel_egress(macAddr_t dstAddr, egressSpec_t port) {
        standard_metadata.egress_spec = port;
        hdr.ethernet.dstAddr = dstAddr;
        hdr.ethernet.etherType = hdr.myTunnel.proto_id;
        hdr.myTunnel.setInvalid();
        egressTunnelCounter.count((bit<32>) hdr.myTunnel.dst_id);
    }

 

myTunnel_egress action은 myTunnel을 invalidate하고 L2를 원래의 etherType으로 복구하는 작업을 수행해줍니다.

 

 

4-4. Bidirectionality

지금 이 코드는 단방향만 고려했습니다. 그러면 ping이 될리가 없겠죠? 하지만 mycontroller.py에서는 이미 양방향으로 rule을 추가해주고 있습니다.

 

        # Write the rules that tunnel traffic from h1 to h2
        writeTunnelRules(p4info_helper, ingress_sw=s1, egress_sw=s2, tunnel_id=100,
                         dst_eth_addr="08:00:00:00:02:22", dst_ip_addr="10.0.2.2")

        # Write the rules that tunnel traffic from h2 to h1
        writeTunnelRules(p4info_helper, ingress_sw=s2, egress_sw=s1, tunnel_id=200,
                         dst_eth_addr="08:00:00:00:01:11", dst_ip_addr="10.0.1.1")

 

 

5. Test

아직 작업이 몇 개 더 남았으나, 일단 통신이 되는지 테스트해봅니다. 동일하게 make run 후에 다른 쉘에서 mycontroller.py를 실행합니다.

 

 

통신이 잘 되는 것을 확인할 수 있었습니다!

 

 

6. readTableRules

마지막으로 readTableRules todo를 해결하러 가봅시다.

 

먼저, 다음을 uncomment 합니다.

        # TODO Uncomment the following two lines to read table entries from s1 and s2
        readTableRules(p4info_helper, s1)
        readTableRules(p4info_helper, s2)

 

 

그리고 readTableRules를 채워줍니다. 이 코드를 적당하게 작성하는데는 약간의 시행착오가 필요합니다.

def readTableRules(p4info_helper, sw):
    """
    Reads the table entries from all tables on the switch.

    :param p4info_helper: the P4Info helper
    :param sw: the switch connection
    """
    print('\n----- Reading tables rules for %s -----' % sw.name)
    for response in sw.ReadTableEntries():
        for entity in response.entities:
            entry = entity.table_entry
            # TODO For extra credit, you can use the p4info_helper to translate
            #      the IDs in the entry to names
            #print(p4info_helper.get_match_field_name("MyIngress.myTunnel_exact", entry.match[0].field_id))
            #print(entry.match[0].field_id)
            print(entry)


            table_name = p4info_helper.get_tables_name(entry.table_id)
            print('table_name: ' + table_name)

            field_name = p4info_helper.get_match_field_name(table_name, entry.match[0].field_id)
            print('field_name: ' + field_name)

            action_name = p4info_helper.get_actions_name(entry.action.action.action_id)
            print('action_name: ' + action_name)
            print('-----')

 

 

실행시 다음과 같이 출력되는 것을 확인할 수 있습니다.

-----
table_id: 35862311
match {
  field_id: 1
  exact {
    value: "\310"
  }
}
action {
  action {
    action_id: 27454791
    params {
      param_id: 1
      value: "\002"
    }
  }
}

table_name: MyIngress.myTunnel_exact
field_name: hdr.myTunnel.dst_id
action_name: MyIngress.myTunnel_forward
-----

 

 

solution 

solution의 readTableRules는 다음과 같은 format으로 출력되니 참고바랍니다.

 

 

Extra Questions

  • What assumptions about the topology are baked into your implementation? How would you need to change it for a more realistic network?
    • 위에서 언급했던 단방향 rule insertion을 문제삼을 수 있을 것으로 보입니다. 현재는 h1에서 h2로 향할 때, dst_id는 2입니다. 이에 맞춰서 s1에는 10.0.2.2에 대한 ingress_rule, dst_id=2에 대한 forward rule, 그리고 s2에는 dst_id=2에 대한 egress_rule만 들어가있는 상황입니다. 
    • 즉, 각 스위치가 범용적으로 packet을 보낼 수 있는 것이 아니라 단일 경로만 처리할 수 있게 구현이 되어있어서 매 경로마다 writeTunnelRules를 추가해줘야 합니다. 
    • 추가로, 현재 구현은 호스트 사이에 스위치가 2개인 경우만 커버가 가능합니다. 
  • Why are the byte counters different between the ingress and egress counters?
    • ingress counter가 egress counter보다 packet당 4 bytes가 적은 것으로 확인됩니다. 이는 tunnel의 byte만큼의 차이입니다.
  • What is the TTL in the ICMP replies? Why is it the value that it is? Hint: The default TTL is 64 for packets sent by the hosts.
    • ipv4_forward action에 ttl을 깎는 코드가 있으나, ipv4_forward action이 실행되는 케이스가 없습니다.
반응형

'프로그래밍 (Programming) > P4' 카테고리의 다른 글

6. P4 - mri  (0) 2024.06.29
5. P4 - ecn  (0) 2024.06.29
3. P4 - calc  (0) 2024.06.21
2. P4 - basic_tunnel  (0) 2024.06.19
1. P4 - Basic  (0) 2024.06.17
Comments