├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── api
├── proto
│ └── v1
│ │ ├── hyper.proto
│ │ ├── l2.proto
│ │ ├── l2Controller.proto
│ │ ├── machinery.proto
│ │ ├── volumes.proto
│ │ └── vpc.proto
└── swagger
│ └── v1
│ ├── hyper.swagger.json
│ ├── l2.swagger.json
│ ├── l2Controller.swagger.json
│ ├── machinery.swagger.json
│ ├── volumes.swagger.json
│ └── vpc.swagger.json
├── cmd
├── hyper
│ └── main.go
├── l2agent
│ └── main.go
├── l3agent
│ └── main.go
├── sbs
│ └── main.go
└── vpc
│ └── main.go
├── docs
└── res
│ └── vpc.jpg
├── go.mod
├── go.sum
├── pkg
├── api
│ └── v1
│ │ ├── hyper
│ │ ├── hyper.pb.go
│ │ └── hyper_grpc.pb.go
│ │ ├── l2
│ │ ├── l2.pb.go
│ │ └── l2_grpc.pb.go
│ │ ├── l2Controller
│ │ ├── l2Controller.pb.go
│ │ └── l2Controller_grpc.pb.go
│ │ ├── machinery
│ │ ├── machinery.pb.go
│ │ └── machinery_grpc.pb.go
│ │ ├── volumes
│ │ └── volumes.pb.go
│ │ └── vpc
│ │ ├── vpc.pb.go
│ │ └── vpc_grpc.pb.go
├── hyper
│ ├── cloudinit.go
│ ├── cloudinit_test.go
│ ├── cmd
│ │ ├── cmd.go
│ │ └── list.go
│ ├── disks.go
│ ├── domain.go
│ ├── domain_test.go
│ ├── hyper.go
│ ├── libvirt.go
│ ├── power.go
│ ├── stats.go
│ ├── templates.go
│ ├── xmddesc_test.go
│ └── xmldesc.go
├── l2
│ ├── api.go
│ ├── bridge.go
│ ├── cmd
│ │ ├── cmd.go
│ │ ├── serve.go
│ │ └── watch.go
│ ├── config.go
│ ├── controller
│ │ ├── bgp
│ │ │ ├── README.md
│ │ │ └── bgp.go
│ │ ├── controller.go
│ │ └── nullController.go
│ ├── gc.go
│ ├── miss.go
│ ├── nic.go
│ ├── pprof.go
│ ├── stack.go
│ ├── transport
│ │ ├── tap
│ │ │ ├── arp.go
│ │ │ ├── arp_test.go
│ │ │ ├── fdb.go
│ │ │ ├── fdb_test.go
│ │ │ ├── icmp6.go
│ │ │ ├── icmp6_test.go
│ │ │ ├── linux.go
│ │ │ ├── listener.go
│ │ │ ├── listener_test.go
│ │ │ ├── protocol
│ │ │ │ ├── protocol.go
│ │ │ │ ├── quic
│ │ │ │ │ ├── frames.go
│ │ │ │ │ ├── quic.go
│ │ │ │ │ └── quic_test.go
│ │ │ │ ├── vpctp
│ │ │ │ │ ├── ebpf.go
│ │ │ │ │ ├── vpctp.go
│ │ │ │ │ ├── vpctp_test.go
│ │ │ │ │ └── xdp
│ │ │ │ │ │ ├── Makefile
│ │ │ │ │ │ ├── README.md
│ │ │ │ │ │ ├── bpf_helper_defs.h
│ │ │ │ │ │ ├── bpf_helpers.h
│ │ │ │ │ │ ├── types.h
│ │ │ │ │ │ ├── vpc_kern.c
│ │ │ │ │ │ └── vpc_kern.h
│ │ │ │ └── vxlan
│ │ │ │ │ ├── hash.go
│ │ │ │ │ ├── packet.go
│ │ │ │ │ ├── packet_test.go
│ │ │ │ │ ├── vxlan.go
│ │ │ │ │ └── vxlan_test.go
│ │ │ ├── tap.go
│ │ │ ├── tap_test.go
│ │ │ └── vlan.go
│ │ ├── transport.go
│ │ └── vtep
│ │ │ └── vtep.go
│ └── xdp
│ │ ├── xdp.go
│ │ └── xdptap.go
├── l3
│ ├── bgp.go
│ ├── cmd
│ │ ├── cmd.go
│ │ └── create.go
│ ├── config.go
│ ├── dhcp.go
│ ├── l2.go
│ ├── netfilter.go
│ ├── ns
│ │ └── netns.go
│ └── router.go
├── sbs
│ ├── blockCache.go
│ ├── blockCache_test.go
│ ├── blockStore.go
│ ├── blockStore_test.go
│ ├── cmd
│ │ ├── agent.go
│ │ ├── cmd.go
│ │ └── start.go
│ ├── config
│ │ └── config.go
│ ├── control.go
│ ├── control
│ │ ├── InMemController.go
│ │ └── controller.go
│ ├── control_test.go
│ ├── errors.go
│ ├── listen.go
│ ├── nbd.go
│ ├── nbdProto.go
│ ├── nbdProto.md
│ ├── nbdSession.go
│ ├── ndbSession_test.go
│ ├── peers.go
│ ├── raft.go
│ ├── raftStores.go
│ ├── raftStores_test.go
│ ├── raft_test.go
│ ├── rpc.go
│ ├── rpc_test.go
│ ├── server.go
│ ├── stats.go
│ ├── test_helpers.go
│ └── volumes.go
├── utils
│ ├── logs.go
│ ├── machinery.go
│ └── utils.go
└── vpc
│ ├── api.go
│ ├── cmd
│ ├── cmd.go
│ ├── migrate.go
│ └── serve.go
│ ├── db.go
│ └── migrations
│ ├── 201911210823_create_vpc.sql
│ └── 201911211000_create_subnet.sql
└── scripts
└── protoc-gen.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, build with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | #Ignore object files
15 | *.o
16 |
17 | build/*
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Tom Worrall
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | FLAGS=-ldflags="-w -s"
2 | GO=go
3 | GO115=go1.15beta1
4 | GO_BUILD=${GO} build ${FLAGS}
5 | GO115_BUILD=${GO115} build ${FLAGS}
6 |
7 | all: l2 l3 vpc sbs
8 | 115: l2_go115 l3_go115 sbs_go115 vpc
9 |
10 | .PHONY: l3
11 | l3:
12 | ${GO_BUILD} -o ./build/l3-agent ./cmd/l3agent
13 |
14 | l3_go115:
15 | ${GO115_BUILD} -o ./build/l3-agent ./cmd/l3agent
16 |
17 | .PHONY: l2
18 | l2:
19 | ${GO_BUILD} -o ./build/l2-agent ./cmd/l2agent
20 |
21 | l2_go115:
22 | ${GO115_BUILD} -o ./build/l2-agent ./cmd/l2agent
23 |
24 | sbs_go115:
25 | ${GO115_BUILD} -o ./build/sbs ./cmd/sbs
26 |
27 | .PHONY: vpc
28 | vpc:
29 | ${GO_BUILD} -o ./build/vpc ./cmd/vpc
30 |
31 | .PHONY: sbs
32 | sbs:
33 | ${GO_BUILD} -o ./build/sbs ./cmd/sbs
34 |
35 | .PHONY: strip
36 | strip:
37 | strip --strip-unneeded ./build/l2-agent
38 | strip --strip-unneeded ./build/l3-agent
39 | strip --strip-unneeded ./build/vpc
40 | strip --strip-unneeded ./build/sbs
41 |
42 | protos:
43 | ./scripts/protoc-gen.sh
44 |
45 | clean:
46 | rm -f ./build/l3-agent
47 | rm -f ./build/l2-agent
48 | rm -f ./build/vpc
49 | rm -f ./build/sbs
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
VPC
3 | Create simple VxLAN based VPC's written in (mostly) Go using Linux bridges/netlink, iptables & network namespaces.
4 |
5 |
6 |
7 |
8 |
9 |
10 | ## Why?
11 | For fun and to learn!
12 |
13 | # Schematic
14 | 
15 |
16 | ## Host Types
17 | - Compute hosts facilitate the creation and management of VM's or containers
18 | - Route hosts provide virtual routers
19 | - Management hosts (not in diagram) provide management facilities such as BGP route reflection and configuration management.
20 |
21 | > There is no technical reason why a compute host can also be a route host and vice-versa. This simply provides better security, bandwidth and segregation of duties on hosts.
22 |
23 | ## Connectivity
24 | Each VM, namespace or container is connected to a Linux bridge (with VLAN filtering enabled) on a compute host. Compute hosts are connected via Linux VxLAN devices (VTEPs).
25 |
26 | ## VTEP Learning
27 | VxLAN learning is disabled by default. Learning is derived from an ML-BGP-L2VPN-EVPN client (via [frr](https://github.com/FRRouting/frr)) on each compute host and route reflectors on management hosts.
28 |
29 | ## Segregation
30 | Each 'tenant' is separated by VxLAN VNI's and each Subnet is protected via inner VLAN tagging on a Linux bridge per tenant.
31 |
32 | # Agents
33 | ## L2
34 | The L2 agent provides a GRPC API to create bridges, VxLAN VTEPs and manage VLAN tagging on the bridges.
35 |
36 | ### Transports
37 | Can set up to use a linux VxLAN device, or use a TAP device with VxLAN encapsulation. The TAP device allows easier handling of ARP/ICMPv6 soliciations in the future.
38 |
39 | ## L3
40 | The L3 agent provides the functionality to create the virtual router namespaces and provide simple DHCP/NAT & routing capabilities.
41 |
42 | ## SBS
43 | Simple block storage - raft based replicated block storage medium exposing NBD endpoints
44 |
45 | # Similar architectures
46 | [Openstacks Neutron](https://wiki.openstack.org/wiki/Neutron) in Linux bridge mode.
47 |
--------------------------------------------------------------------------------
/api/proto/v1/hyper.proto:
--------------------------------------------------------------------------------
1 | syntax="proto3";
2 |
3 | package vpc.hyper;
4 | option go_package = ".;hyper";
5 |
6 | enum VMStatus {
7 | PENDING = 0;
8 | CREATING = 1;
9 | AVAILABLE = 2;
10 | DELETING = 3;
11 | DELETED = 4;
12 |
13 | ERROR = 99;
14 | }
15 |
16 | message VM {
17 | int32 account = 1;
18 | string id = 2;
19 | VMStatus status = 17;
20 | map metadata = 4;
21 |
22 | string template_id = 3;
23 | string placement_id = 5;
24 | string hyper_id = 6;
25 | string subnet_id = 7;
26 |
27 | repeated StorageDevice storage = 8;
28 | repeated string fw_rule_sets = 9;
29 | repeated string nics = 10;
30 |
31 | PowerState power_state = 11;
32 | string power_state_last_update = 12;
33 |
34 | PowerState desired_power_state = 13;
35 | string desired_power_state_last_update = 14;
36 |
37 | string ssh_key_ref = 15;
38 | bytes user_data = 16;
39 |
40 | string created_at = 18;
41 | string updated_at = 19;
42 | string availability_area = 20;
43 |
44 | repeated VolumeAttachment volumes = 21;
45 | }
46 |
47 | enum PowerState {
48 | NONE = 0;
49 | RUNNING = 1;
50 | SHUTDOWN = 4;
51 | SHUTOFF = 5;
52 | CRASHED = 6;
53 | MIGRATING = 7;
54 | }
55 |
56 | message VMTemplate {
57 | string id = 1;
58 | int32 vcpu = 2;
59 | int64 ram = 3;
60 | string libvirt_xml = 4;
61 | }
62 |
63 | message VolumeAttachment {
64 | string volume = 1;
65 | string dev = 2;
66 | }
67 |
68 | message StorageDevice {
69 | string id = 1;
70 | string device = 2;
71 |
72 | enum Driver {
73 | LOCAL = 0;
74 | RDB = 1;
75 | }
76 |
77 | Driver driver = 3;
78 | int64 size = 4;
79 | }
80 |
81 | message StatsRequest {
82 | string id = 1;
83 | }
84 |
85 | message VCPUStats {
86 | uint32 id = 1;
87 | uint64 time = 2;
88 | float usage = 3;
89 | }
90 |
91 | message MemStats {
92 | uint64 majors_faults = 1;
93 | uint64 minor_faults = 2;
94 |
95 | uint64 unused = 3;
96 | uint64 available = 4;
97 | uint64 usable = 5;
98 |
99 | uint64 swap_in = 6;
100 | uint64 swap_out = 7;
101 |
102 | uint64 last_update = 8;
103 | }
104 |
105 | message NetStats {
106 | string id = 1;
107 |
108 | uint64 rx_bytes = 2;
109 | uint64 rx_pkts = 3;
110 | uint64 rx_errs = 4;
111 | uint64 rx_drops = 5;
112 |
113 | uint64 tx_bytes = 6;
114 | uint64 tx_pkts = 7;
115 | uint64 tx_errs = 8;
116 | uint64 tx_drops = 9;
117 | }
118 |
119 | message DiskStats {
120 | string id = 1;
121 |
122 | uint64 rd_reqs = 2;
123 | uint64 rd_bytes = 3;
124 | uint64 rd_times = 4;
125 |
126 | uint64 wr_reqs = 5;
127 | uint64 wr_bytes = 6;
128 | uint64 wr_times = 7;
129 |
130 | uint64 alloc = 8;
131 | uint64 cap = 9;
132 | uint64 phy = 10;
133 | }
134 |
135 | message VMStats {
136 | repeated VCPUStats vcpus = 1;
137 | repeated NetStats nets = 2;
138 | repeated DiskStats disks = 3;
139 | MemStats mem = 4;
140 | }
141 |
142 | message StatsResponse {
143 | repeated VMStats stats = 1;
144 | }
145 |
146 | message ListRequest {
147 | string id = 1;
148 | }
149 |
150 | message ListResponse {
151 | repeated VM vms = 1;
152 | }
153 |
154 | message PowerRequest {
155 | enum ForcePowerType {
156 | REBOOT = 0;
157 | SHUTDOWN = 1;
158 | FORCE_REBOOT = 2;
159 | FORCE_SHUTDOWN = 3;
160 | }
161 |
162 | string id = 1;
163 | ForcePowerType type = 2;
164 | }
165 |
166 | message PowerResponse {}
167 |
168 | service HyperService {
169 | rpc Stats(StatsRequest) returns (StatsResponse) {};
170 | rpc List(ListRequest) returns (ListResponse) {};
171 | rpc Power(PowerRequest) returns (PowerResponse) {};
172 | }
--------------------------------------------------------------------------------
/api/proto/v1/l2.proto:
--------------------------------------------------------------------------------
1 | syntax="proto3";
2 |
3 | package vpc.l2;
4 | option go_package = ".;l2";
5 |
6 | message StackRequest {
7 | int32 vpc_id = 1;
8 | }
9 |
10 | message Stack {
11 | int32 vpc_id = 1;
12 | string bridge_link_name = 2;
13 | int32 bridge_link_index = 3;
14 | }
15 |
16 | message StackResponse {
17 | Stack stack = 1;
18 | StackStatusResponse status = 2;
19 | }
20 |
21 | enum LinkStatus {
22 | DOWN = 0;
23 | UP = 1;
24 | MISSING = 2;
25 | }
26 |
27 | message StackStatusResponse {
28 | LinkStatus bridge = 1;
29 | LinkStatus transport = 2;
30 | }
31 |
32 | message StackChange {
33 | int32 vpc_id = 1;
34 | string action = 2;
35 | StackStatusResponse status = 3;
36 | }
37 |
38 | message NicRequest {
39 | int32 vpc_id = 1;
40 | string id = 2;
41 | uint32 subnet_vlan_id = 3;
42 | bool manually_added = 4;
43 | string manual_hwaddr = 5;
44 | repeated string ip = 6;
45 | }
46 |
47 | message Nic {
48 | int32 vpc_id = 1;
49 | string hwaddr = 2;
50 | string id = 3;
51 | string name = 4;
52 | int32 index = 5;
53 | uint32 vlan = 6;
54 | repeated string ip = 7;
55 | }
56 |
57 | message NicStatusResponse {
58 | LinkStatus status = 1;
59 | }
60 |
61 | message Empty {}
62 |
63 | service L2Service {
64 | rpc AddStack(StackRequest) returns (StackResponse) {};
65 | rpc GetStack(StackRequest) returns (StackResponse) {};
66 | rpc StackStatus(StackRequest) returns (StackStatusResponse) {};
67 | rpc DeleteStack(StackRequest) returns (Empty) {};
68 | rpc WatchStacks(Empty) returns (stream StackChange) {};
69 |
70 | rpc AddNIC(NicRequest) returns (Nic) {};
71 | rpc DeleteNIC(Nic) returns (Empty) {};
72 | rpc NICStatus(Nic) returns (NicStatusResponse) {};
73 | // rpc UpdateNIC(NicRequest) returns (Nic) {};
74 | }
--------------------------------------------------------------------------------
/api/proto/v1/l2Controller.proto:
--------------------------------------------------------------------------------
1 | syntax="proto3";
2 |
3 | package vpc.l2Controller;
4 | option go_package = ".;l2Controller";
5 |
6 | message MACIPRequest {
7 | uint32 VNID = 1;
8 | uint32 VLAN = 2;
9 | string MAC = 3;
10 | string IP = 4;
11 | }
12 |
13 | message MACIPResp {
14 | }
15 |
16 | enum LookupType {
17 | MAC = 0;
18 | IP = 1;
19 | }
20 |
21 | message LookupRequest {
22 | LookupType LookupType = 1;
23 |
24 | uint32 VNID = 2;
25 | uint32 VLAN = 3;
26 | string MAC = 4;
27 | string IP = 5;
28 | }
29 |
30 | message LookupResponse {
31 | string MAC = 1;
32 | string IP = 2;
33 | }
34 |
35 | message VNIDRequest {
36 | uint32 VNID = 1;
37 | }
38 |
39 | message RegResponse {}
40 |
41 | message BroadcastEndpointResponse {
42 | repeated string IP = 1;
43 | }
44 |
45 | service ControllerService {
46 | rpc RegisterMacIP(MACIPRequest) returns (MACIPResp) {};
47 | rpc DeregisterMacIP(MACIPRequest) returns (MACIPResp) {};
48 | rpc LookupIP(LookupRequest) returns (LookupResponse) {};
49 | rpc LookupMac(LookupRequest) returns (LookupResponse) {};
50 | rpc BroadcastEndpoints(VNIDRequest) returns (BroadcastEndpointResponse) {};
51 | rpc RegisterEP(VNIDRequest) returns (RegResponse) {};
52 | rpc DeregisterEP(VNIDRequest) returns (RegResponse) {};
53 | }
--------------------------------------------------------------------------------
/api/proto/v1/machinery.proto:
--------------------------------------------------------------------------------
1 | syntax="proto3";
2 |
3 | package vpc.machinery;
4 | option go_package = ".;machinery";
5 |
6 | message Machine {
7 | string id = 1;
8 |
9 | enum Capability {
10 | ORCHEST = 0;
11 | COMPUTE = 1;
12 | NETWORK = 2;
13 | STORAGE = 3;
14 | }
15 |
16 | repeated Capability capabilities = 2;
17 | string last_checkin = 3;
18 | string availability_zone = 4;
19 |
20 | map versions = 5;
21 | map limits = 6;
22 | }
23 |
24 | enum DeviceType {
25 | ROUTER = 0;
26 | VM = 1;
27 | BLOCK = 2;
28 | }
29 |
30 | message Placement {
31 | string id = 1;
32 | string machine_id = 2;
33 |
34 | DeviceType device_type = 4;
35 | string device_id = 5;
36 | }
37 |
38 | message PlacementsRequest {
39 | string machine_id = 1;
40 | }
41 |
42 | message PlacementsResponse {
43 | repeated Placement placements = 1;
44 | repeated DeviceType types = 2;
45 | }
46 |
47 | message PingResponse {}
48 |
49 | message MachinesRequest {}
50 |
51 | message MachinesResponse {
52 | repeated Machine machines = 1;
53 | }
54 |
55 | enum PlacementAction {
56 | ASSIGN = 0;
57 | REPLACE = 1;
58 | UNASSIGN = 2;
59 | }
60 |
61 | message PlaceRequest {
62 | PlacementAction action = 1;
63 | Placement placement = 2;
64 | }
65 |
66 | message PlaceResponse {}
67 |
68 | service MachineryService {
69 | rpc Ping(Machine) returns (PingResponse) {};
70 | rpc Placements(PlacementsRequest) returns (PlacementsResponse) {};
71 | rpc Machines(MachinesRequest) returns (MachinesResponse) {};
72 | rpc Place(PlaceRequest) returns (PlaceResponse) {};
73 | }
--------------------------------------------------------------------------------
/api/proto/v1/volumes.proto:
--------------------------------------------------------------------------------
1 | syntax="proto3";
2 |
3 | package vpc.volumes;
4 | option go_package = ".;volumes";
5 |
6 | enum VolumeStatus {
7 | PENDING = 0;
8 | CREATING = 1;
9 | AVAILABLE = 2;
10 | INUSE = 3;
11 | BACKINGUP = 4;
12 | RESTORING = 5;
13 | RESIZING = 6;
14 | DELETING = 7;
15 | DELETED = 8;
16 |
17 | ERROR = 99;
18 | }
19 |
20 | message Volume {
21 | int32 account = 1;
22 | string id = 2;
23 | VolumeStatus status = 3;
24 | map metadata = 4;
25 | string created_at = 5;
26 | string updated_at = 6;
27 |
28 | int64 size = 7;
29 | string store = 8;
30 | string availability_area = 9;
31 | }
32 |
33 | message Snapshot {
34 | string id = 1;
35 | string volume = 2;
36 | VolumeStatus status = 3;
37 | map metadata = 4;
38 | string created_at = 5;
39 | string updated_at = 6;
40 |
41 | int64 size = 7;
42 | string store = 8;
43 | string availability_area = 9;
44 | }
45 |
46 | message PeerNegotiate {
47 | string IAm = 1;
48 | string YouAre = 2;
49 | }
50 |
51 | message RaftRPC {
52 | string volume = 1;
53 | int32 type = 2;
54 | bytes command = 3;
55 | string error = 4;
56 | }
57 |
58 | message PeerPing {
59 | int64 ts = 1;
60 | }
61 |
62 | message BlockCommandRequest {
63 | enum Cmd {
64 | WRITE = 0;
65 | READ = 1;
66 | ZERO = 2;
67 | }
68 |
69 | string volume = 1;
70 | Cmd cmd = 2;
71 | int64 offset = 3;
72 | int32 length = 4;
73 | bytes data = 5;
74 | }
75 |
76 | message BlockCommandResponse {
77 | string volume = 1;
78 | string retryAt = 2; //peer ID to retry to request at - i.e. peer not leader
79 | bytes data = 3;
80 | string error = 4;
81 | }
82 |
83 | message RPCMetadata {
84 | bool leaderOnly = 1; //to prevent stale reads if no caching on client side - all writes must still go to leader
85 | bool allowForwarding = 2;
86 | int32 TTL = 3;
87 | }
88 |
89 | message PeerRPC {
90 | RPCMetadata metadata = 1;
91 | oneof RPC {
92 | RaftRPC RaftRPC = 2;
93 | PeerPing PeerPing = 3;
94 | BlockCommandRequest BlockCommand = 4;
95 | BlockCommandResponse BlockCommandResponse = 5;
96 | }
97 | reserved 6 to 100; //future RPCs
98 | bool async = 101;
99 | }
--------------------------------------------------------------------------------
/api/proto/v1/vpc.proto:
--------------------------------------------------------------------------------
1 | syntax="proto3";
2 |
3 | package vpc;
4 | option go_package = ".;vpc";
5 |
6 | message VPC {
7 | int32 id = 1;
8 | int32 account = 2;
9 | string cidr = 3;
10 | map metadata = 4;
11 | int32 asn = 5;
12 | int32 vni = 6;
13 | }
14 |
15 | message Subnet {
16 | string id = 1;
17 | int32 vpc_id = 2;
18 | string region = 3;
19 | string cidr = 4;
20 | map metadata = 5;
21 | int32 inner_vlan = 6;
22 | }
23 |
24 | message InternetGW {
25 | string id = 1;
26 | int32 vpc_id = 2;
27 | }
28 |
29 | message PublicIP {
30 | string id = 1;
31 | int32 vpc_id = 2;
32 | string ip = 3;
33 | bool reserved = 4;
34 | }
35 |
36 | message FWRule {
37 | string proto = 2;
38 | string cidr = 3;
39 | int32 min_port = 4;
40 | int32 max_port = 5;
41 | }
42 |
43 | message FWRuleSet {
44 | string id = 1;
45 | string name = 2;
46 | repeated FWRule rules = 3;
47 |
48 | enum Direction {
49 | INBOUND = 0;
50 | OUTBOUND = 1;
51 | }
52 |
53 | Direction direction = 4;
54 | }
55 |
56 | message VPCsRequest {}
57 | message VPCsResponse {
58 | repeated VPC VPCs = 1;
59 | }
60 |
61 | message VPCInfoRequest {
62 | int32 vpc_id = 1;
63 | }
64 |
65 | message VPCInfoResponse {
66 | VPC vpc = 1;
67 | repeated Subnet subnets = 2;
68 | InternetGW internetGW = 3;
69 | }
70 |
71 | message SubnetsResponse {
72 | repeated Subnet subnets = 1;
73 | }
74 |
75 | message InternetGWsRespones {
76 | repeated InternetGW internet_gws = 1;
77 | }
78 |
79 | service VPCService {
80 | rpc VPCs(VPCsRequest) returns (VPCsResponse) {};
81 | rpc VPCInfo(VPCInfoRequest) returns (VPCInfoResponse) {};
82 | rpc Subnets(VPCInfoRequest) returns (SubnetsResponse) {};
83 | rpc InternetGWs(VPCInfoRequest) returns (InternetGWsRespones) {};
84 | }
--------------------------------------------------------------------------------
/api/swagger/v1/l2.swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "l2.proto",
5 | "version": "version not set"
6 | },
7 | "schemes": [
8 | "http",
9 | "https"
10 | ],
11 | "consumes": [
12 | "application/json"
13 | ],
14 | "produces": [
15 | "application/json"
16 | ],
17 | "paths": {},
18 | "definitions": {
19 | "l2Empty": {
20 | "type": "object"
21 | },
22 | "l2LinkStatus": {
23 | "type": "string",
24 | "enum": [
25 | "DOWN",
26 | "UP",
27 | "MISSING"
28 | ],
29 | "default": "DOWN"
30 | },
31 | "l2Nic": {
32 | "type": "object",
33 | "properties": {
34 | "vpc_id": {
35 | "type": "integer",
36 | "format": "int32"
37 | },
38 | "hwaddr": {
39 | "type": "string"
40 | },
41 | "id": {
42 | "type": "string"
43 | },
44 | "name": {
45 | "type": "string"
46 | },
47 | "index": {
48 | "type": "integer",
49 | "format": "int32"
50 | },
51 | "vlan": {
52 | "type": "integer",
53 | "format": "int64"
54 | },
55 | "ip": {
56 | "type": "array",
57 | "items": {
58 | "type": "string"
59 | }
60 | }
61 | }
62 | },
63 | "l2NicStatusResponse": {
64 | "type": "object",
65 | "properties": {
66 | "status": {
67 | "$ref": "#/definitions/l2LinkStatus"
68 | }
69 | }
70 | },
71 | "l2Stack": {
72 | "type": "object",
73 | "properties": {
74 | "vpc_id": {
75 | "type": "integer",
76 | "format": "int32"
77 | },
78 | "bridge_link_name": {
79 | "type": "string"
80 | },
81 | "bridge_link_index": {
82 | "type": "integer",
83 | "format": "int32"
84 | }
85 | }
86 | },
87 | "l2StackChange": {
88 | "type": "object",
89 | "properties": {
90 | "vpc_id": {
91 | "type": "integer",
92 | "format": "int32"
93 | },
94 | "action": {
95 | "type": "string"
96 | },
97 | "status": {
98 | "$ref": "#/definitions/l2StackStatusResponse"
99 | }
100 | }
101 | },
102 | "l2StackResponse": {
103 | "type": "object",
104 | "properties": {
105 | "stack": {
106 | "$ref": "#/definitions/l2Stack"
107 | },
108 | "status": {
109 | "$ref": "#/definitions/l2StackStatusResponse"
110 | }
111 | }
112 | },
113 | "l2StackStatusResponse": {
114 | "type": "object",
115 | "properties": {
116 | "bridge": {
117 | "$ref": "#/definitions/l2LinkStatus"
118 | },
119 | "transport": {
120 | "$ref": "#/definitions/l2LinkStatus"
121 | }
122 | }
123 | },
124 | "protobufAny": {
125 | "type": "object",
126 | "properties": {
127 | "type_url": {
128 | "type": "string"
129 | },
130 | "value": {
131 | "type": "string",
132 | "format": "byte"
133 | }
134 | }
135 | },
136 | "runtimeStreamError": {
137 | "type": "object",
138 | "properties": {
139 | "grpc_code": {
140 | "type": "integer",
141 | "format": "int32"
142 | },
143 | "http_code": {
144 | "type": "integer",
145 | "format": "int32"
146 | },
147 | "message": {
148 | "type": "string"
149 | },
150 | "http_status": {
151 | "type": "string"
152 | },
153 | "details": {
154 | "type": "array",
155 | "items": {
156 | "$ref": "#/definitions/protobufAny"
157 | }
158 | }
159 | }
160 | }
161 | },
162 | "x-stream-definitions": {
163 | "l2StackChange": {
164 | "type": "object",
165 | "properties": {
166 | "result": {
167 | "$ref": "#/definitions/l2StackChange"
168 | },
169 | "error": {
170 | "$ref": "#/definitions/runtimeStreamError"
171 | }
172 | },
173 | "title": "Stream result of l2StackChange"
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/api/swagger/v1/l2Controller.swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "l2Controller.proto",
5 | "version": "version not set"
6 | },
7 | "schemes": [
8 | "http",
9 | "https"
10 | ],
11 | "consumes": [
12 | "application/json"
13 | ],
14 | "produces": [
15 | "application/json"
16 | ],
17 | "paths": {},
18 | "definitions": {
19 | "l2ControllerBroadcastEndpointResponse": {
20 | "type": "object",
21 | "properties": {
22 | "IP": {
23 | "type": "array",
24 | "items": {
25 | "type": "string"
26 | }
27 | }
28 | }
29 | },
30 | "l2ControllerLookupResponse": {
31 | "type": "object",
32 | "properties": {
33 | "MAC": {
34 | "type": "string"
35 | },
36 | "IP": {
37 | "type": "string"
38 | }
39 | }
40 | },
41 | "l2ControllerLookupType": {
42 | "type": "string",
43 | "enum": [
44 | "MAC",
45 | "IP"
46 | ],
47 | "default": "MAC"
48 | },
49 | "l2ControllerMACIPResp": {
50 | "type": "object"
51 | },
52 | "l2ControllerRegResponse": {
53 | "type": "object"
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/api/swagger/v1/machinery.swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "machinery.proto",
5 | "version": "version not set"
6 | },
7 | "schemes": [
8 | "http",
9 | "https"
10 | ],
11 | "consumes": [
12 | "application/json"
13 | ],
14 | "produces": [
15 | "application/json"
16 | ],
17 | "paths": {},
18 | "definitions": {
19 | "MachineCapability": {
20 | "type": "string",
21 | "enum": [
22 | "ORCHEST",
23 | "COMPUTE",
24 | "NETWORK",
25 | "STORAGE"
26 | ],
27 | "default": "ORCHEST"
28 | },
29 | "machineryDeviceType": {
30 | "type": "string",
31 | "enum": [
32 | "ROUTER",
33 | "VM",
34 | "BLOCK"
35 | ],
36 | "default": "ROUTER"
37 | },
38 | "machineryMachine": {
39 | "type": "object",
40 | "properties": {
41 | "id": {
42 | "type": "string"
43 | },
44 | "capabilities": {
45 | "type": "array",
46 | "items": {
47 | "$ref": "#/definitions/MachineCapability"
48 | }
49 | },
50 | "last_checkin": {
51 | "type": "string"
52 | },
53 | "availability_zone": {
54 | "type": "string"
55 | },
56 | "versions": {
57 | "type": "object",
58 | "additionalProperties": {
59 | "type": "string"
60 | }
61 | },
62 | "limits": {
63 | "type": "object",
64 | "additionalProperties": {
65 | "type": "integer",
66 | "format": "int32"
67 | }
68 | }
69 | }
70 | },
71 | "machineryMachinesResponse": {
72 | "type": "object",
73 | "properties": {
74 | "machines": {
75 | "type": "array",
76 | "items": {
77 | "$ref": "#/definitions/machineryMachine"
78 | }
79 | }
80 | }
81 | },
82 | "machineryPingResponse": {
83 | "type": "object"
84 | },
85 | "machineryPlaceResponse": {
86 | "type": "object"
87 | },
88 | "machineryPlacement": {
89 | "type": "object",
90 | "properties": {
91 | "id": {
92 | "type": "string"
93 | },
94 | "machine_id": {
95 | "type": "string"
96 | },
97 | "device_type": {
98 | "$ref": "#/definitions/machineryDeviceType"
99 | },
100 | "device_id": {
101 | "type": "string"
102 | }
103 | }
104 | },
105 | "machineryPlacementAction": {
106 | "type": "string",
107 | "enum": [
108 | "ASSIGN",
109 | "REPLACE",
110 | "UNASSIGN"
111 | ],
112 | "default": "ASSIGN"
113 | },
114 | "machineryPlacementsResponse": {
115 | "type": "object",
116 | "properties": {
117 | "placements": {
118 | "type": "array",
119 | "items": {
120 | "$ref": "#/definitions/machineryPlacement"
121 | }
122 | },
123 | "types": {
124 | "type": "array",
125 | "items": {
126 | "$ref": "#/definitions/machineryDeviceType"
127 | }
128 | }
129 | }
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/api/swagger/v1/volumes.swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "volumes.proto",
5 | "version": "version not set"
6 | },
7 | "schemes": [
8 | "http",
9 | "https"
10 | ],
11 | "consumes": [
12 | "application/json"
13 | ],
14 | "produces": [
15 | "application/json"
16 | ],
17 | "paths": {},
18 | "definitions": {}
19 | }
20 |
--------------------------------------------------------------------------------
/api/swagger/v1/vpc.swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "swagger": "2.0",
3 | "info": {
4 | "title": "vpc.proto",
5 | "version": "version not set"
6 | },
7 | "schemes": [
8 | "http",
9 | "https"
10 | ],
11 | "consumes": [
12 | "application/json"
13 | ],
14 | "produces": [
15 | "application/json"
16 | ],
17 | "paths": {},
18 | "definitions": {
19 | "vpcInternetGW": {
20 | "type": "object",
21 | "properties": {
22 | "id": {
23 | "type": "string"
24 | },
25 | "vpc_id": {
26 | "type": "integer",
27 | "format": "int32"
28 | }
29 | }
30 | },
31 | "vpcInternetGWsRespones": {
32 | "type": "object",
33 | "properties": {
34 | "internet_gws": {
35 | "type": "array",
36 | "items": {
37 | "$ref": "#/definitions/vpcInternetGW"
38 | }
39 | }
40 | }
41 | },
42 | "vpcSubnet": {
43 | "type": "object",
44 | "properties": {
45 | "id": {
46 | "type": "string"
47 | },
48 | "vpc_id": {
49 | "type": "integer",
50 | "format": "int32"
51 | },
52 | "region": {
53 | "type": "string"
54 | },
55 | "cidr": {
56 | "type": "string"
57 | },
58 | "metadata": {
59 | "type": "object",
60 | "additionalProperties": {
61 | "type": "string"
62 | }
63 | },
64 | "inner_vlan": {
65 | "type": "integer",
66 | "format": "int32"
67 | }
68 | }
69 | },
70 | "vpcSubnetsResponse": {
71 | "type": "object",
72 | "properties": {
73 | "subnets": {
74 | "type": "array",
75 | "items": {
76 | "$ref": "#/definitions/vpcSubnet"
77 | }
78 | }
79 | }
80 | },
81 | "vpcVPC": {
82 | "type": "object",
83 | "properties": {
84 | "id": {
85 | "type": "integer",
86 | "format": "int32"
87 | },
88 | "account": {
89 | "type": "integer",
90 | "format": "int32"
91 | },
92 | "cidr": {
93 | "type": "string"
94 | },
95 | "metadata": {
96 | "type": "object",
97 | "additionalProperties": {
98 | "type": "string"
99 | }
100 | },
101 | "asn": {
102 | "type": "integer",
103 | "format": "int32"
104 | },
105 | "vni": {
106 | "type": "integer",
107 | "format": "int32"
108 | }
109 | }
110 | },
111 | "vpcVPCInfoResponse": {
112 | "type": "object",
113 | "properties": {
114 | "vpc": {
115 | "$ref": "#/definitions/vpcVPC"
116 | },
117 | "subnets": {
118 | "type": "array",
119 | "items": {
120 | "$ref": "#/definitions/vpcSubnet"
121 | }
122 | },
123 | "internetGW": {
124 | "$ref": "#/definitions/vpcInternetGW"
125 | }
126 | }
127 | },
128 | "vpcVPCsResponse": {
129 | "type": "object",
130 | "properties": {
131 | "VPCs": {
132 | "type": "array",
133 | "items": {
134 | "$ref": "#/definitions/vpcVPC"
135 | }
136 | }
137 | }
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/cmd/hyper/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/tcfw/vpc/pkg/hyper/cmd"
8 | )
9 |
10 | func main() {
11 | cmd := cmd.NewDefaultCommand()
12 |
13 | if err := cmd.Execute(); err != nil {
14 | fmt.Fprintf(os.Stderr, "%v\n", err)
15 | os.Exit(1)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/l2agent/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/tcfw/vpc/pkg/l2/cmd"
8 | )
9 |
10 | func main() {
11 | cmd := cmd.NewDefaultCommand()
12 |
13 | if err := cmd.Execute(); err != nil {
14 | fmt.Fprintf(os.Stderr, "%v\n", err)
15 | os.Exit(1)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/l3agent/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/tcfw/vpc/pkg/l3/cmd"
8 | )
9 |
10 | func main() {
11 | dcmd := cmd.NewDefaultCommand()
12 |
13 | if err := dcmd.Execute(); err != nil {
14 | fmt.Fprintf(os.Stderr, "%v\n", err)
15 | os.Exit(1)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/sbs/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/tcfw/vpc/pkg/sbs/cmd"
8 | )
9 |
10 | func main() {
11 | cmd := cmd.NewDefaultCommand()
12 |
13 | if err := cmd.Execute(); err != nil {
14 | fmt.Fprintf(os.Stderr, "%v\n", err)
15 | os.Exit(1)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/cmd/vpc/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/tcfw/vpc/pkg/vpc/cmd"
8 | )
9 |
10 | func main() {
11 | cmd := cmd.NewDefaultCommand()
12 |
13 | if err := cmd.Execute(); err != nil {
14 | fmt.Fprintf(os.Stderr, "%v\n", err)
15 | os.Exit(1)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/docs/res/vpc.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tcfw/vpc/a9890c3e7311f7e6ac464bf83db1ea83caa4c2fb/docs/res/vpc.jpg
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/tcfw/vpc
2 |
3 | go 1.13
4 |
5 | require (
6 | aqwari.net/xml v0.0.0-20200619145941-6c62842e69c1
7 | github.com/DataDog/zstd v1.4.5 // indirect
8 | github.com/apparentlymart/go-cidr v1.0.1
9 | github.com/cilium/ebpf v0.0.0-20200617135954-7acf5cc039f4
10 | github.com/coreos/go-iptables v0.4.5
11 | github.com/denisbrodbeck/machineid v1.0.1
12 | github.com/dgraph-io/badger/v2 v2.0.3
13 | github.com/dgraph-io/ristretto v0.0.3 // indirect
14 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
15 | github.com/diskfs/go-diskfs v1.1.1
16 | github.com/eapache/queue v1.1.0 // indirect
17 | github.com/esiqveland/gogfapi v0.0.0-20200421115158-22e95f628744
18 | github.com/golang/mock v1.4.3 // indirect
19 | github.com/golang/protobuf v1.4.2
20 | github.com/google/gopacket v1.1.17
21 | github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect
22 | github.com/hashicorp/raft v1.1.2
23 | github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 // indirect
24 | github.com/insomniacslk/dhcp v0.0.0-20200621044212-d74cd86ad5b8
25 | github.com/juliengk/go-netfilter v0.0.0-20190717112259-89f054e40987
26 | github.com/k-sone/critbitgo v1.4.0 // indirect
27 | github.com/kpango/fastime v1.0.16
28 | github.com/kr/text v0.2.0 // indirect
29 | github.com/lib/pq v1.7.0
30 | github.com/lorenzosaino/go-sysctl v0.1.1
31 | github.com/lucas-clemente/quic-go v0.17.3
32 | github.com/marten-seemann/qtls v0.10.0 // indirect
33 | github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect
34 | github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect
35 | github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
36 | github.com/mitchellh/mapstructure v1.3.2 // indirect
37 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
38 | github.com/onsi/ginkgo v1.13.0 // indirect
39 | github.com/osrg/gobgp v0.0.0-20200602125502-33e3bb6b2756
40 | github.com/pelletier/go-toml v1.8.0 // indirect
41 | github.com/pkg/errors v0.9.1 // indirect
42 | github.com/sirupsen/logrus v1.6.0
43 | github.com/smartystreets/assertions v1.1.1 // indirect
44 | github.com/songgao/packets v0.0.0-20160404182456-549a10cd4091
45 | github.com/spf13/afero v1.3.1 // indirect
46 | github.com/spf13/cast v1.3.1 // indirect
47 | github.com/spf13/cobra v1.0.0
48 | github.com/spf13/jwalterweatherman v1.1.0 // indirect
49 | github.com/spf13/pflag v1.0.5 // indirect
50 | github.com/spf13/viper v1.7.0
51 | github.com/src-d/envconfig v1.0.0 // indirect
52 | github.com/stretchr/testify v1.6.1
53 | github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
54 | github.com/u-root/u-root v6.0.0+incompatible // indirect
55 | github.com/vishvananda/netlink v1.1.0
56 | github.com/vishvananda/netns v0.0.0-20200520041808-52d707b772fe
57 | github.com/vmihailenco/msgpack v4.0.4+incompatible
58 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
59 | golang.org/x/mod v0.3.0 // indirect
60 | golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
61 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
62 | golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666
63 | golang.org/x/text v0.3.3 // indirect
64 | golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f // indirect
65 | google.golang.org/genproto v0.0.0-20200626011028-ee7919e894b5 // indirect
66 | google.golang.org/grpc v1.30.0
67 | google.golang.org/protobuf v1.25.0
68 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
69 | gopkg.in/ini.v1 v1.57.0 // indirect
70 | gopkg.in/src-d/go-log.v1 v1.0.2
71 | gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
72 | libvirt.org/libvirt-go v6.4.0+incompatible
73 | )
74 |
--------------------------------------------------------------------------------
/pkg/hyper/cloudinit.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "os"
7 |
8 | diskfs "github.com/diskfs/go-diskfs"
9 | "github.com/diskfs/go-diskfs/disk"
10 | "github.com/diskfs/go-diskfs/filesystem"
11 | "github.com/diskfs/go-diskfs/filesystem/iso9660"
12 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
13 | )
14 |
15 | const (
16 | cloudName = "vpc"
17 | diskStore = "/var/local/vpc/cidisks"
18 | )
19 |
20 | //InstanceMetadata basic instance info
21 | type InstanceMetadata struct {
22 | InstanceID string `json:"instance-id"`
23 | CloudName string `json:"cloud-name"`
24 | }
25 |
26 | func createCloudInitISO(vm *hyperAPI.VM) (*os.File, error) {
27 | instanceMD := &InstanceMetadata{
28 | InstanceID: vm.Id,
29 | CloudName: cloudName,
30 | }
31 |
32 | diskImg, err := ciDataDisk(vm)
33 | if err != nil {
34 | return nil, err
35 | }
36 |
37 | mdbytes, err := json.Marshal(instanceMD)
38 | if err != nil {
39 | defer os.Remove(diskImg.Name())
40 | return diskImg, err
41 | }
42 |
43 | size := int64(len(vm.UserData) + len(mdbytes) + 1024)
44 | if size < 32768 {
45 | //min ISO file size
46 | size = 32768 + 2048 + 2048 + 2048
47 | }
48 |
49 | diskImg.Truncate(size)
50 |
51 | ciDisk, err := diskfs.OpenWithMode(diskImg.Name(), diskfs.ReadWriteExclusive)
52 | if err != nil {
53 | defer os.Remove(diskImg.Name())
54 | return diskImg, err
55 | }
56 |
57 | ciDisk.LogicalBlocksize = 2048
58 | fspec := disk.FilesystemSpec{Partition: 0, FSType: filesystem.TypeISO9660, VolumeLabel: "cidata"}
59 |
60 | fs, err := ciDisk.CreateFilesystem(fspec)
61 | if err != nil {
62 | defer os.Remove(diskImg.Name())
63 | return diskImg, err
64 | }
65 |
66 | //Metadata file
67 | fmd, err := fs.OpenFile("meta-data", os.O_CREATE|os.O_RDWR)
68 | if err != nil {
69 | defer os.Remove(diskImg.Name())
70 | return diskImg, fmt.Errorf("failed to open md: %s", err)
71 | }
72 | if _, err := fmd.Write(mdbytes); err != nil {
73 | defer os.Remove(diskImg.Name())
74 | return nil, fmt.Errorf("failed to write md: %s", err)
75 | }
76 |
77 | //User-data
78 | if vm.UserData != nil {
79 | fud, err := fs.OpenFile("user-data", os.O_CREATE|os.O_RDWR)
80 | if err != nil {
81 | defer os.Remove(diskImg.Name())
82 | return nil, fmt.Errorf("failed to write ud: %s", err)
83 | }
84 |
85 | if _, err := fud.Write(vm.UserData); err != nil {
86 | defer os.Remove(diskImg.Name())
87 | return nil, err
88 | }
89 | }
90 |
91 | //Finalise
92 | iso, ok := fs.(*iso9660.FileSystem)
93 | isows := iso.Workspace()
94 | if !ok {
95 | defer os.Remove(diskImg.Name())
96 | return nil, fmt.Errorf("not an iso9660 filesystem")
97 | }
98 | if err = iso.Finalize(iso9660.FinalizeOptions{}); err != nil {
99 | defer os.Remove(diskImg.Name())
100 | return nil, err
101 | }
102 |
103 | //Remove the temp workspace since diskfs does not
104 | os.RemoveAll(isows)
105 |
106 | return diskImg, diskImg.Sync()
107 |
108 | }
109 |
110 | func ciDataDisk(vm *hyperAPI.VM) (*os.File, error) {
111 | if _, err := os.Stat(diskStore); os.IsNotExist(err) {
112 | err := os.MkdirAll(diskStore, os.ModePerm)
113 | if err != nil {
114 | return nil, fmt.Errorf("failed to create local disk store: %s", err)
115 | }
116 | }
117 |
118 | return os.OpenFile(ciDiskName(vm), os.O_RDWR|os.O_CREATE, os.ModePerm)
119 | }
120 |
121 | func ciDiskName(vm *hyperAPI.VM) string {
122 | return fmt.Sprintf("%s/ci.%s.iso", diskStore, vm.Id)
123 | }
124 |
--------------------------------------------------------------------------------
/pkg/hyper/cloudinit_test.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "bufio"
5 | "os"
6 | "strings"
7 | "testing"
8 |
9 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
10 | )
11 |
12 | func TestCreateCloudInitISO(t *testing.T) {
13 | vm := &hyperAPI.VM{
14 | Id: "abcdef-test",
15 | UserData: []byte(`
16 | #cloud-init
17 | password: test
18 | chpasswd: { expire: False }
19 | ssh_pwauth: True
20 | `),
21 | }
22 |
23 | isoFile, err := createCloudInitISO(vm)
24 | if err != nil {
25 | t.Fatal(err)
26 | }
27 |
28 | defer func() {
29 | isoFile.Close()
30 | os.Remove(isoFile.Name())
31 | }()
32 |
33 | t.Logf("ISO filename: %s", isoFile.Name())
34 |
35 | //TODO(tcfw): find some way to validate ISO file properly
36 |
37 | scanner := bufio.NewScanner(isoFile)
38 | loc := false
39 | for scanner.Scan() {
40 | if strings.Contains(scanner.Text(), "#cloud-init") {
41 | loc = true
42 | break
43 | }
44 | }
45 |
46 | if loc == false {
47 | t.Fatal("failed to find cloud-init user-data header")
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/hyper/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | // NewDefaultCommand creates the `hyper` command and its nested children.
8 | func NewDefaultCommand() *cobra.Command {
9 | // Parent command to which all subcommands are added.
10 | cmds := &cobra.Command{
11 | Use: "hyper",
12 | Short: "hyper",
13 | Long: `hyper providers hypervised compute resources`,
14 | Run: func(cmd *cobra.Command, args []string) {
15 | cmd.Help()
16 | },
17 | }
18 |
19 | cmds.AddCommand(newListCmd())
20 |
21 | return cmds
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/hyper/cmd/list.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "log"
6 |
7 | "github.com/spf13/cobra"
8 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
9 | "github.com/tcfw/vpc/pkg/hyper"
10 | )
11 |
12 | //newListCmd provides a command to delete vpcs
13 | func newListCmd() *cobra.Command {
14 | cmd := &cobra.Command{
15 | Use: "list",
16 | Short: "lists local vms",
17 | Run: func(cmd *cobra.Command, args []string) {
18 | l, err := hyper.NewLocalLibVirtConn()
19 | if err != nil {
20 | log.Fatalf("failed: %s", err)
21 | }
22 |
23 | defer l.Close()
24 |
25 | if err := hyper.ApplyDesiredState(l, "7897fad0-e6ff-44df-9e3c-cb6c939f8bb6", hyperAPI.PowerState_SHUTDOWN); err != nil {
26 | log.Fatalf("%s", err)
27 | }
28 |
29 | f, err := hyper.AvailableDisks()
30 | if err != nil {
31 | log.Fatalf("%s", err)
32 | }
33 | fmt.Printf("%+v", f)
34 |
35 | // d, _ := l.LookupDomainByUUIDString("7897fad0-e6ff-44df-9e3c-cb6c939f8bb6")
36 | // if err := hyper.Snapshot(d); err != nil {
37 | // fmt.Printf("ERR: %s", err)
38 | // }
39 | },
40 | }
41 |
42 | return cmd
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/hyper/disks.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 |
7 | "github.com/esiqveland/gogfapi/gfapi"
8 |
9 | libvirt "libvirt.org/libvirt-go"
10 | )
11 |
12 | //AvailableDisks lists available disks
13 | func AvailableDisks() ([]string, error) {
14 | files := []string{}
15 |
16 | vol := &gfapi.Volume{}
17 | if err := vol.Init("disks", "localhost"); err != nil {
18 | return nil, fmt.Errorf("failed to connect: %s", err)
19 | }
20 |
21 | if err := vol.Mount(); err != nil {
22 | return nil, fmt.Errorf("failed to mount: %s", err)
23 | }
24 | defer vol.Unmount()
25 |
26 | f, err := vol.Open("/")
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | volFiles, err := f.Readdir(0)
32 | if err != nil {
33 | return nil, err
34 | }
35 |
36 | for _, f := range volFiles {
37 | name := f.Name()
38 | if name == "." || name == ".." {
39 | continue
40 | }
41 |
42 | files = append(files, name)
43 | }
44 |
45 | return files, nil
46 | }
47 |
48 | //Snapshot attempts to create a snapshot of a running VM
49 | func Snapshot(d *libvirt.Domain) error {
50 | desc := &DomainBackup{
51 | Disks: []DomainBackupDisk{
52 | {
53 | Name: "vda",
54 | Type: "file",
55 | Target: DomainBackupDiskTarget{
56 | File: "/home/tom/Desktop/backup.backup",
57 | },
58 | Driver: DomainBackupDiskDriver{
59 | Type: "raw",
60 | },
61 | },
62 | },
63 | }
64 |
65 | checkpoint := &DomainCheckpoint{
66 | Description: "test",
67 | Disks: []DomainCheckpointDisk{
68 | {
69 | Name: "vda",
70 | },
71 | },
72 | }
73 |
74 | descBytes, _ := xml.Marshal(desc)
75 | checkpointBytes, _ := xml.Marshal(checkpoint)
76 |
77 | fmt.Printf("BU: %s\nCP: %s", string(descBytes), string(checkpointBytes))
78 |
79 | return d.BackupBegin(string(descBytes), string(checkpointBytes), libvirt.DOMAIN_BACKUP_BEGIN_REUSE_EXTERNAL)
80 | }
81 |
--------------------------------------------------------------------------------
/pkg/hyper/domain.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "encoding/hex"
5 | "encoding/xml"
6 | "fmt"
7 | "os"
8 |
9 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
10 | "libvirt.org/libvirt-go"
11 | )
12 |
13 | //DefineVM takes in an API desc and creates a hypervisor accepted VM
14 | func DefineVM(c *libvirt.Connect, vm *hyperAPI.VM) error {
15 | if vm.Id == "" {
16 | return fmt.Errorf("vm has no id")
17 | }
18 |
19 | if vm.HyperId != "" {
20 | d, err := c.LookupDomainByUUIDString(vm.HyperId)
21 | if err != nil {
22 | return fmt.Errorf("failed to check for existing: %s", err)
23 | }
24 | if d != nil {
25 | return fmt.Errorf("vm already defined")
26 | }
27 | }
28 |
29 | //construct cloud-init disk
30 | var cidata *os.File
31 | cidiskn := ciDiskName(vm)
32 | if _, err := os.Stat(cidiskn); os.IsExist(err) {
33 | cidata, err = os.OpenFile(cidiskn, os.O_EXCL|os.O_RDWR, os.ModePerm)
34 | if err != nil {
35 | return fmt.Errorf("failed to open cidata: %s", err)
36 | }
37 | } else {
38 | cidata, err = createCloudInitISO(vm)
39 | if err != nil {
40 | return fmt.Errorf("failed to create cidata: %s", err)
41 | }
42 | }
43 |
44 | //Closed for IO
45 | cidata.Close()
46 |
47 | desc, err := GetTemplate(vm)
48 | if err != nil {
49 | return err
50 | }
51 |
52 | desc.Devices.Disks = append(desc.Devices.Disks, DomainDisk{
53 | Type: "file",
54 | Device: "disk",
55 | ReadOnly: &DomainDiskReadOnly{},
56 | Target: DomainDiskTarget{Bus: "virtio", Dev: "vdz"},
57 | Driver: DomainDiskDriver{Name: "qemu", Type: "raw"},
58 | Source: DomainDiskSource{File: cidata.Name()},
59 | Address: DomainDeviceAddr{ //TODO(tcfw) check if PCI slot is available
60 | Type: "pci",
61 | Domain: "0x0000",
62 | Bus: "0x09",
63 | Slot: "0x00",
64 | Function: "0x00",
65 | },
66 | IOTune: &DomainDiskIOTune{
67 | TotalIopsSec: 100,
68 | TotalIopsSecMax: 200,
69 | TotalIopsSecMaxLength: 10,
70 | },
71 | })
72 |
73 | uuid, err := createDomain(c, desc)
74 | if err != nil {
75 | return fmt.Errorf("failed to define vm: %s", err)
76 | }
77 | vm.HyperId = hex.EncodeToString(uuid)
78 | return nil
79 |
80 | }
81 |
82 | func createDomain(c *libvirt.Connect, desc interface{}) ([]byte, error) {
83 | switch desc.(type) {
84 | case *DomainDesc:
85 | descBytes, err := xml.Marshal(desc)
86 | if err != nil {
87 | return nil, fmt.Errorf("failed to marshal to XML desc: %s", err)
88 | }
89 | return createXMLDomain(c, string(descBytes))
90 | case []byte:
91 | return createXMLDomain(c, string(desc.([]byte)))
92 | case string:
93 | return createXMLDomain(c, desc.(string))
94 | }
95 |
96 | return nil, fmt.Errorf("unknown desc format")
97 | }
98 |
99 | func createXMLDomain(c *libvirt.Connect, descXML string) ([]byte, error) {
100 | d, err := c.DomainDefineXML(descXML)
101 | if err != nil {
102 | return nil, fmt.Errorf("failed to define domain: %s", err)
103 | }
104 |
105 | uuid, err := d.GetUUID()
106 | if err != nil {
107 | return nil, fmt.Errorf("failed to retrieve uuid from defined domain: %s", err)
108 | }
109 |
110 | return uuid, nil
111 | }
112 |
--------------------------------------------------------------------------------
/pkg/hyper/domain_test.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "log"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
9 | )
10 |
11 | func TestDefineVM(t *testing.T) {
12 | vm := &hyperAPI.VM{
13 | Id: "test-1",
14 | TemplateId: `
15 | 4194304
16 | 4194304
17 | 2
18 |
19 | hvm
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | destroy
34 | restart
35 | restart
36 |
37 |
38 |
39 |
40 |
41 | /usr/bin/qemu-system-x86_64
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 | /dev/urandom
103 |
104 |
105 |
106 | `,
107 | }
108 |
109 | c, err := testLibVirtConn()
110 | if err != nil {
111 | log.Fatalf("failed: %s", err)
112 | }
113 |
114 | defer c.Close()
115 |
116 | err = DefineVM(c, vm)
117 | if err != nil {
118 | log.Fatal(err)
119 | }
120 |
121 | assert.NotEmpty(t, vm.HyperId)
122 | }
123 |
--------------------------------------------------------------------------------
/pkg/hyper/hyper.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "time"
7 |
8 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
9 | libvirt "libvirt.org/libvirt-go"
10 | )
11 |
12 | //List provides a list of running VMs
13 | func List(c *libvirt.Connect) {
14 | domains, err := c.ListAllDomains(libvirt.CONNECT_LIST_DOMAINS_ACTIVE | libvirt.CONNECT_LIST_DOMAINS_INACTIVE)
15 | if err != nil {
16 | log.Fatalf("failed to retrieve domains: %v", err)
17 | }
18 |
19 | fmt.Println("ID\tName\t\tUUID\tState")
20 | fmt.Printf("--------------------------------------------------------\n")
21 | for _, d := range domains {
22 | state, _, err := d.GetState()
23 | if err != nil {
24 | log.Fatalln(err)
25 | }
26 | id, _ := d.GetID()
27 | name, _ := d.GetName()
28 | uuid, _ := d.GetUUID()
29 | fmt.Printf("%d\t%s\t%x\t%v\n", id, name, uuid, state)
30 | }
31 | }
32 |
33 | //ApplyDesiredState takes in the desired state of the VM and attempts to apply the desired state
34 | func ApplyDesiredState(c *libvirt.Connect, uuid string, desiredState hyperAPI.PowerState) error {
35 | d, err := c.LookupDomainByUUIDString(uuid)
36 | if err != nil {
37 | return fmt.Errorf("failed to lookup: %s", err)
38 | }
39 |
40 | current, err := setDesirePower(d, desiredState)
41 |
42 | fmt.Printf("%+v", current)
43 |
44 | return err
45 | }
46 |
47 | //Stats gets a current running VM's io/resource stats
48 | func Stats(c *libvirt.Connect, uuid string) error {
49 | //Check exists
50 | d, err := c.LookupDomainByUUIDString(uuid)
51 | if err != nil {
52 | return fmt.Errorf("failed to lookup: %s", err)
53 | }
54 |
55 | //Check is running
56 | s, _, err := d.GetState()
57 | if err != nil {
58 | return fmt.Errorf("failed check state: %s", err)
59 | }
60 | if s != libvirt.DOMAIN_RUNNING {
61 | return fmt.Errorf("domain not running: %d", s)
62 | }
63 |
64 | memStats, err := memStats(d)
65 | if err != nil {
66 | return err
67 | }
68 |
69 | fmt.Printf("STAT: %+v\n", memStats)
70 | fmt.Printf("STAT Used: %+v\n", memStats.Available-memStats.Unused)
71 | fmt.Printf("STAT Last: %s\n", time.Unix(int64(memStats.LastUpdate), 0))
72 |
73 | cpuStats, total, err := cpuStats(d)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | fmt.Printf("%+v\n", cpuStats)
79 | fmt.Printf("Total: %+#v\n", total)
80 |
81 | netStats, err := netStats(d)
82 | if err != nil {
83 | return err
84 | }
85 |
86 | fmt.Printf("NET: %+v\n", netStats)
87 |
88 | _, dTotal, err := diskStats(d)
89 | if err != nil {
90 | return err
91 | }
92 |
93 | fmt.Printf("DISK: %+v\n", dTotal)
94 |
95 | return nil
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/hyper/libvirt.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "fmt"
5 |
6 | libvirt "libvirt.org/libvirt-go"
7 | )
8 |
9 | //NewLocalLibVirtConn connects to the local libvirt daemon
10 | func NewLocalLibVirtConn() (*libvirt.Connect, error) {
11 | l, err := libvirt.NewConnect("qemu:///system")
12 | if err != nil {
13 | return nil, fmt.Errorf("failed to connect: %v", err)
14 | }
15 |
16 | return l, nil
17 | }
18 |
19 | func testLibVirtConn() (*libvirt.Connect, error) {
20 | return libvirt.NewConnect("test:///default")
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/hyper/power.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "fmt"
5 |
6 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
7 | libvirt "libvirt.org/libvirt-go"
8 | )
9 |
10 | func setDesirePower(d *libvirt.Domain, desiredState hyperAPI.PowerState) (hyperAPI.PowerState, error) {
11 | current, _, err := d.GetState()
12 | if err != nil {
13 | return hyperAPI.PowerState_NONE, err
14 | }
15 |
16 | switch current {
17 | case libvirt.DOMAIN_CRASHED:
18 | case libvirt.DOMAIN_BLOCKED:
19 | switch desiredState {
20 | case hyperAPI.PowerState_RUNNING:
21 | return appliedState(hyperAPI.PowerState_CRASHED, d.Reboot(libvirt.DOMAIN_REBOOT_DEFAULT))
22 | case hyperAPI.PowerState_SHUTDOWN:
23 | return appliedState(hyperAPI.PowerState_SHUTOFF, d.Destroy())
24 | }
25 | case libvirt.DOMAIN_RUNNING:
26 | switch desiredState {
27 | case hyperAPI.PowerState_SHUTDOWN:
28 | return appliedState(hyperAPI.PowerState_SHUTDOWN, d.ShutdownFlags(libvirt.DOMAIN_SHUTDOWN_ACPI_POWER_BTN))
29 | case hyperAPI.PowerState_SHUTOFF:
30 | return appliedState(hyperAPI.PowerState_SHUTOFF, d.DestroyFlags(libvirt.DOMAIN_DESTROY_GRACEFUL))
31 | }
32 | case libvirt.DOMAIN_PAUSED:
33 | switch desiredState {
34 | case hyperAPI.PowerState_SHUTDOWN:
35 | return appliedState(hyperAPI.PowerState_SHUTOFF, d.Shutdown())
36 | case hyperAPI.PowerState_RUNNING:
37 | return appliedState(hyperAPI.PowerState_RUNNING, d.Resume())
38 | }
39 | case libvirt.DOMAIN_SHUTDOWN:
40 | switch desiredState {
41 | case hyperAPI.PowerState_SHUTOFF:
42 | return appliedState(hyperAPI.PowerState_SHUTOFF, d.DestroyFlags(libvirt.DOMAIN_DESTROY_DEFAULT))
43 | }
44 | case libvirt.DOMAIN_SHUTOFF:
45 | switch desiredState {
46 | case hyperAPI.PowerState_RUNNING:
47 | return appliedState(hyperAPI.PowerState_RUNNING, d.Create())
48 | }
49 | }
50 |
51 | return libVirtToAPI(current), nil
52 | }
53 |
54 | func appliedState(endCurrent hyperAPI.PowerState, err error) (hyperAPI.PowerState, error) {
55 | if err != nil {
56 | return hyperAPI.PowerState_NONE, fmt.Errorf("failed to apply state: %s", err)
57 | }
58 |
59 | return endCurrent, nil
60 | }
61 |
62 | func libVirtToAPI(state libvirt.DomainState) hyperAPI.PowerState {
63 | switch state {
64 | case libvirt.DOMAIN_CRASHED:
65 | case libvirt.DOMAIN_BLOCKED:
66 | return hyperAPI.PowerState_CRASHED
67 | case libvirt.DOMAIN_RUNNING:
68 | return hyperAPI.PowerState_RUNNING
69 | case libvirt.DOMAIN_SHUTOFF:
70 | return hyperAPI.PowerState_SHUTOFF
71 | case libvirt.DOMAIN_SHUTDOWN:
72 | return hyperAPI.PowerState_SHUTDOWN
73 | case libvirt.DOMAIN_PAUSED:
74 | case libvirt.DOMAIN_PMSUSPENDED:
75 | return hyperAPI.PowerState_MIGRATING
76 | case libvirt.DOMAIN_NOSTATE:
77 | default:
78 | //
79 | }
80 |
81 | return hyperAPI.PowerState_NONE
82 | }
83 |
--------------------------------------------------------------------------------
/pkg/hyper/stats.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "math"
7 | "time"
8 |
9 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
10 | libvirt "libvirt.org/libvirt-go"
11 | )
12 |
13 | func memStats(d *libvirt.Domain) (*hyperAPI.MemStats, error) {
14 | mStats, err := d.MemoryStats(15, 0)
15 | if err != nil {
16 | return nil, fmt.Errorf("failed to get mem stats: %s", err)
17 | }
18 |
19 | stats := &hyperAPI.MemStats{}
20 |
21 | for _, stat := range mStats {
22 | switch libvirt.DomainMemoryStatTags(stat.Tag) {
23 | case libvirt.DOMAIN_MEMORY_STAT_SWAP_IN:
24 | stats.SwapIn = stat.Val
25 | break
26 | case libvirt.DOMAIN_MEMORY_STAT_SWAP_OUT:
27 | stats.SwapOut = stat.Val
28 | break
29 | case libvirt.DOMAIN_MEMORY_STAT_AVAILABLE:
30 | stats.Available = stat.Val
31 | break
32 | case libvirt.DOMAIN_MEMORY_STAT_UNUSED:
33 | stats.Unused = stat.Val
34 | break
35 | case libvirt.DOMAIN_MEMORY_STAT_MAJOR_FAULT:
36 | stats.MajorsFaults = stat.Val
37 | break
38 | case libvirt.DOMAIN_MEMORY_STAT_MINOR_FAULT:
39 | stats.MinorFaults = stat.Val
40 | break
41 | case libvirt.DOMAIN_MEMORY_STAT_USABLE:
42 | stats.Usable = stat.Val
43 | break
44 | case libvirt.DOMAIN_MEMORY_STAT_LAST_UPDATE:
45 | stats.LastUpdate = stat.Val
46 | break
47 | }
48 | }
49 |
50 | return stats, nil
51 | }
52 |
53 | func cpuStats(d *libvirt.Domain) ([]*hyperAPI.VCPUStats, *hyperAPI.VCPUStats, error) {
54 | stats := []*hyperAPI.VCPUStats{}
55 | total := &hyperAPI.VCPUStats{}
56 |
57 | tn := 1 * time.Second
58 | n, _ := d.GetMaxVcpus()
59 |
60 | p1, err := d.GetCPUStats(0, n, 0)
61 | if err != nil {
62 | return nil, nil, err
63 | }
64 |
65 | time.Sleep(tn)
66 |
67 | p2, err := d.GetCPUStats(0, n, 0)
68 | if err != nil {
69 | return nil, nil, err
70 | }
71 |
72 | for i := uint(0); i < n; i++ {
73 | useTimeDiff := math.Abs(float64(p2[i].CpuTime - p1[i].CpuTime))
74 | total.Time += uint64(useTimeDiff)
75 |
76 | vCPUStat := &hyperAPI.VCPUStats{
77 | Id: uint32(i / 2),
78 | Time: p1[i].CpuTime,
79 | Usage: float32(100*useTimeDiff) / float32(tn),
80 | }
81 |
82 | stats = append(stats, vCPUStat)
83 | }
84 |
85 | total.Usage = float32(100*total.Time) / float32(tn) / float32(n)
86 |
87 | return stats, total, nil
88 | }
89 |
90 | func netStats(d *libvirt.Domain) ([]*hyperAPI.NetStats, error) {
91 | stats := []*hyperAPI.NetStats{}
92 |
93 | ifaces, err := d.ListAllInterfaceAddresses(libvirt.DOMAIN_INTERFACE_ADDRESSES_SRC_ARP)
94 | if err != nil {
95 | return nil, err
96 | }
97 |
98 | for _, iface := range ifaces {
99 | ifaceStats, err := d.InterfaceStats(iface.Name)
100 | if err != nil {
101 | return nil, err
102 | }
103 |
104 | stats = append(stats, &hyperAPI.NetStats{
105 | Id: iface.Name,
106 | RxBytes: uint64(ifaceStats.RxBytes),
107 | RxPkts: uint64(ifaceStats.RxPackets),
108 | RxErrs: uint64(ifaceStats.RxErrs),
109 | RxDrops: uint64(ifaceStats.RxDrop),
110 | TxBytes: uint64(ifaceStats.TxBytes),
111 | TxPkts: uint64(ifaceStats.TxPackets),
112 | TxErrs: uint64(ifaceStats.TxErrs),
113 | TxDrops: uint64(ifaceStats.TxDrop),
114 | })
115 | }
116 |
117 | return stats, nil
118 | }
119 |
120 | func diskStats(d *libvirt.Domain) ([]*hyperAPI.DiskStats, *hyperAPI.DiskStats, error) {
121 | stats := []*hyperAPI.DiskStats{}
122 | total := &hyperAPI.DiskStats{}
123 |
124 | r, err := d.GetXMLDesc(libvirt.DOMAIN_XML_MIGRATABLE)
125 | if err != nil {
126 | return nil, nil, err
127 | }
128 |
129 | desc := &DomainDesc{}
130 |
131 | xml.Unmarshal([]byte(r), desc)
132 |
133 | for _, disk := range desc.Devices.Disks {
134 | dStats, err := d.BlockStats(disk.Target.Dev)
135 | if err != nil {
136 | return nil, nil, err
137 | }
138 |
139 | stats = append(stats, &hyperAPI.DiskStats{
140 | Id: disk.Target.Dev,
141 | RdReqs: uint64(dStats.RdReq),
142 | RdBytes: uint64(dStats.RdBytes),
143 | RdTimes: uint64(dStats.RdTotalTimes),
144 | WrReqs: uint64(dStats.WrReq),
145 | WrBytes: uint64(dStats.WrBytes),
146 | WrTimes: uint64(dStats.WrTotalTimes),
147 | })
148 |
149 | total.RdReqs += uint64(dStats.RdReq)
150 | total.RdBytes += uint64(dStats.RdBytes)
151 | total.RdTimes += uint64(dStats.RdTotalTimes)
152 | total.WrReqs += uint64(dStats.WrReq)
153 | total.WrBytes += uint64(dStats.WrBytes)
154 | total.WrTimes += uint64(dStats.WrTotalTimes)
155 | }
156 |
157 | return stats, total, nil
158 | }
159 |
--------------------------------------------------------------------------------
/pkg/hyper/templates.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "strings"
7 |
8 | hyperAPI "github.com/tcfw/vpc/pkg/api/v1/hyper"
9 | )
10 |
11 | //GetTemplate converts the VM desc template to a DomainDesc struct
12 | func GetTemplate(vm *hyperAPI.VM) (*DomainDesc, error) {
13 | if strings.TrimSpace(vm.TemplateId)[0] == '<' {
14 | desc := &DomainDesc{}
15 | desc.Name = vm.Id
16 |
17 | if err := xml.Unmarshal([]byte(vm.TemplateId), desc); err != nil {
18 | return nil, fmt.Errorf("failed to marshal desc: %s", err)
19 | }
20 |
21 | return desc, nil
22 | }
23 |
24 | return nil, fmt.Errorf("unknown template reference")
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/hyper/xmddesc_test.go:
--------------------------------------------------------------------------------
1 | package hyper
2 |
3 | import (
4 | "encoding/xml"
5 | "testing"
6 |
7 | "aqwari.net/xml/xmltree"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestMarshalUnmarshal(t *testing.T) {
12 | obj := &DomainDesc{}
13 | sample := []byte(`
14 | vs-1
15 | f5a921ce-eedd-4d99-8a2a-2a98f4470ca8
16 |
17 |
18 |
19 |
20 |
21 | 2097152
22 | 2097152
23 | 2
24 |
25 | hvm
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | destroy
40 | restart
41 | destroy
42 |
43 |
44 |
45 |
46 |
47 | /usr/bin/qemu-system-x86_64
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 |
101 |
102 |
103 |
104 |
105 | /dev/urandom
106 |
107 |
108 |
109 |
110 | `)
111 |
112 | err := xml.Unmarshal(sample, obj)
113 | if err != nil {
114 | t.Fatal(err)
115 | }
116 |
117 | b, err := xml.Marshal(obj)
118 | if err != nil {
119 | t.Fatal(err)
120 | }
121 |
122 | xmlEla, err := xmltree.Parse(sample)
123 | if err != nil {
124 | t.Fatal(err)
125 | }
126 | xmlElb, err := xmltree.Parse(b)
127 | if err != nil {
128 | t.Fatal(err)
129 | }
130 |
131 | assert.True(t, xmltree.Equal(xmlEla, xmlElb))
132 | }
133 |
--------------------------------------------------------------------------------
/pkg/l2/bridge.go:
--------------------------------------------------------------------------------
1 | package l2
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 |
7 | "github.com/vishvananda/netlink"
8 | )
9 |
10 | const (
11 | bridgePattern = "b-%d"
12 | )
13 |
14 | //getBridge finds the bridge associated with a VPC
15 | func getBridge(vpcID int32) (netlink.Link, error) {
16 | handle, err := netlink.NewHandle(netlink.FAMILY_ALL)
17 | if err != nil {
18 | return nil, err
19 | }
20 | defer func() {
21 | handle.Delete()
22 | }()
23 |
24 | links, err := handle.LinkList()
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | ifaceName := fmt.Sprintf(bridgePattern, vpcID)
30 |
31 | for _, link := range links {
32 | if link.Type() == "bridge" && link.Attrs().Name == ifaceName {
33 | return link, nil
34 | }
35 | }
36 | return nil, nil
37 | }
38 |
39 | //hasBridge checks if a bridge exists by trying to get it
40 | func hasBridge(vpcID int32) (bool, error) {
41 | br, err := getBridge(vpcID)
42 | if err != nil {
43 | return false, err
44 | }
45 | return br != nil, nil
46 | }
47 |
48 | //createBridge creates a new linux bridge for a VPC
49 | func createBridge(vpcID int32) (*netlink.Bridge, error) {
50 | if ok, _ := hasBridge(vpcID); ok {
51 | return nil, fmt.Errorf("vpc %d already has a bridge", vpcID)
52 | }
53 |
54 | la := netlink.NewLinkAttrs()
55 | la.Name = fmt.Sprintf(bridgePattern, vpcID)
56 | la.MTU = 2000
57 |
58 | br := &netlink.Bridge{LinkAttrs: la}
59 |
60 | if err := netlink.LinkAdd(br); err != nil {
61 | return nil, err
62 | }
63 |
64 | // if err := enableBridgeVlanFiltering(br.Name); err != nil {
65 | // return nil, fmt.Errorf("Failed to enable VLAN filtering on bridge: %s", err)
66 | // }
67 |
68 | err := netlink.LinkSetUp(br)
69 |
70 | return br, err
71 | }
72 |
73 | //enableBridgeVlanFiltering sets vlan filtering on the bridge
74 | func enableBridgeVlanFiltering(bridgeName string) error {
75 | return ioutil.WriteFile(fmt.Sprintf("/sys/devices/virtual/net/%s/bridge/vlan_filtering", bridgeName), []byte("1"), 0644)
76 | }
77 |
78 | //deleteBridge deletes a linux bridge for a VPC
79 | func deleteBridge(vpcID int32) error {
80 | if ok, _ := hasBridge(vpcID); !ok {
81 | return fmt.Errorf("vpc %d bridge does not exist", vpcID)
82 | }
83 |
84 | br, err := getBridge(vpcID)
85 | if err != nil {
86 | return err
87 | }
88 |
89 | return netlink.LinkDel(br)
90 | }
91 |
92 | func getBridgeLinks(brIndex int, vpcID int32) ([]netlink.Link, error) {
93 | slaveLinks := []netlink.Link{}
94 |
95 | handle, err := netlink.NewHandle(netlink.FAMILY_ALL)
96 | if err != nil {
97 | return nil, err
98 | }
99 | defer func() {
100 | handle.Delete()
101 | }()
102 |
103 | links, err := handle.LinkList()
104 | if err != nil {
105 | return nil, err
106 | }
107 |
108 | vtepName := fmt.Sprintf("t-%d", vpcID)
109 |
110 | for _, link := range links {
111 | if link.Attrs().MasterIndex == brIndex && link.Attrs().Name != vtepName {
112 | slaveLinks = append(slaveLinks, link)
113 | }
114 | }
115 |
116 | return slaveLinks, nil
117 | }
118 |
--------------------------------------------------------------------------------
/pkg/l2/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "github.com/spf13/viper"
6 | )
7 |
8 | // NewDefaultCommand creates the `l2` command and its nested children.
9 | func NewDefaultCommand() *cobra.Command {
10 | // Parent command to which all subcommands are added.
11 | cmds := &cobra.Command{
12 | Use: "l2",
13 | Short: "l2",
14 | Long: `l2 providers vxlan bridges`,
15 | Run: func(cmd *cobra.Command, args []string) {
16 | cmd.Help()
17 | },
18 | }
19 |
20 | cmds.AddCommand(NewServeCmd())
21 | cmds.AddCommand(NewWatchCmd())
22 |
23 | cmds.PersistentFlags().StringP("vtepdev", "i", "eth0", "Device for VTEP endpoint")
24 | viper.BindPFlag("vtepdev", cmds.PersistentFlags().Lookup("vtepdev"))
25 |
26 | return cmds
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/l2/cmd/serve.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "github.com/spf13/viper"
6 | "github.com/tcfw/vpc/pkg/l2"
7 | )
8 |
9 | //NewServeCmd provides a command to delete vpcs
10 | func NewServeCmd() *cobra.Command {
11 | cmd := &cobra.Command{
12 | Use: "serve",
13 | Short: "Starts the l2 agent in daemon",
14 | Run: func(cmd *cobra.Command, args []string) {
15 | go l2.StartPProf(8024)
16 |
17 | port, _ := cmd.Flags().GetUint("port")
18 | l2.Serve(port)
19 | },
20 | }
21 |
22 | cmd.Flags().UintP("port", "p", 18254, "GRPC port")
23 | cmd.Flags().StringSlice("bgp", []string{}, "initial BGP peer")
24 | viper.BindPFlag("bgp_peers", cmd.Flags().Lookup("bgp"))
25 |
26 | return cmd
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/l2/cmd/watch.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 |
8 | "github.com/spf13/cobra"
9 | l2API "github.com/tcfw/vpc/pkg/api/v1/l2"
10 | "google.golang.org/grpc"
11 | )
12 |
13 | //NewWatchCmd provides a command to delete vpcs
14 | func NewWatchCmd() *cobra.Command {
15 | cmd := &cobra.Command{
16 | Use: "watch",
17 | Short: "Watches for changes in VPC stacks",
18 | Run: func(cmd *cobra.Command, args []string) {
19 | port, _ := cmd.Flags().GetUint("port")
20 |
21 | conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithInsecure())
22 | if err != nil {
23 | log.Println(err)
24 | return
25 | }
26 | defer conn.Close()
27 |
28 | cli := l2API.NewL2ServiceClient(conn)
29 | stream, err := cli.WatchStacks(context.Background(), &l2API.Empty{})
30 | if err != nil {
31 | log.Println(err)
32 | return
33 | }
34 |
35 | for {
36 | change, err := stream.Recv()
37 | if err != nil {
38 | log.Println(err)
39 | return
40 | }
41 |
42 | log.Printf("%d %s", change.VpcId, change.Action)
43 | }
44 | },
45 | }
46 |
47 | cmd.Flags().UintP("port", "p", 18254, "GRPC port")
48 |
49 | return cmd
50 | }
51 |
--------------------------------------------------------------------------------
/pkg/l2/config.go:
--------------------------------------------------------------------------------
1 | package l2
2 |
3 | import "github.com/spf13/viper"
4 |
5 | //vtepDev provides the iface for the vtep dev attachment from viper config
6 | func vtepDev() string {
7 | return viper.GetString("vtepdev")
8 | }
9 |
10 | //bgpPeers list of BGP peers from viper config
11 | func bgpPeers() []string {
12 | return viper.GetStringSlice("bgp_peers")
13 | }
14 |
--------------------------------------------------------------------------------
/pkg/l2/controller/bgp/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 | Running GoBGP in standalone `(sudo) gobgpd -f ./reflector.yaml`
3 | ```
4 | global:
5 | config:
6 | as: 65000
7 | router-id: 192.168.122.1
8 | local-address-list:
9 | - 192.168.122.1
10 | peer-groups:
11 | - config:
12 | peer-group-name: l2vpn
13 | afi-safis:
14 | - config:
15 | afi-safi-name: l2vpn-evpn
16 | route-reflector:
17 | config:
18 | route-reflector-client: true
19 | route-reflector-cluster-id: 192.168.122.1
20 | dynamic-neighbors:
21 | - config:
22 | peer-group: l2vpn
23 | prefix: 192.168.122.0/24
24 | ```
--------------------------------------------------------------------------------
/pkg/l2/controller/controller.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "net"
5 | )
6 |
7 | //Controller s allows storing and publishing of endpoints and IPs
8 | type Controller interface {
9 | Start() error
10 | Stop()
11 |
12 | RegisterMacIP(vnid uint32, vlan uint32, mac net.HardwareAddr, ip net.IP) error
13 | DeregisterMacIP(vnid uint32, vlan uint32, mac net.HardwareAddr, ip net.IP) error
14 | LookupIP(vnid uint32, vlan uint16, ip net.IP) (net.HardwareAddr, net.IP, error)
15 | LookupMac(vnid uint32, mac net.HardwareAddr) (net.IP, error)
16 |
17 | BroadcastEndpoints(vnid uint32) ([]net.IP, error)
18 | RegisterEP(vnid uint32) error
19 | DeregisterEP(vnid uint32) error
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/l2/controller/nullController.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import "net"
4 |
5 | //NullController an empty controller
6 | type NullController struct{}
7 |
8 | //Start should start the controller
9 | func (n *NullController) Start() error {
10 | return nil
11 | }
12 |
13 | //Stop should stop the controller
14 | func (n *NullController) Stop() {
15 | return
16 | }
17 |
18 | //RegisterMacIP usually would register the mac ip address combo
19 | func (n *NullController) RegisterMacIP(vnid uint32, vlan uint32, mac net.HardwareAddr, ip net.IP) error {
20 | return nil
21 | }
22 |
23 | //DeregisterMacIP usually would remove a mac/ip combo
24 | func (n *NullController) DeregisterMacIP(vnid uint32, vlan uint32, mac net.HardwareAddr, ip net.IP) error {
25 | return nil
26 | }
27 |
28 | //LookupIP usually looks up a mac address for a given IP.
29 | //Hard coded to return 82:2f:00:00:00:ff
30 | func (n *NullController) LookupIP(vnid uint32, vlan uint16, ip net.IP) (net.HardwareAddr, net.IP, error) {
31 | //return a test mac address
32 | return net.HardwareAddr{0x82, 0x2f, 0x00, 0x00, 0x00, 0xff}, ip, nil
33 | }
34 |
35 | //LookupMac usually would look up an IP for a given mac
36 | //Hard coded to return net.IP{10.4.0.5}
37 | func (n *NullController) LookupMac(vnid uint32, mac net.HardwareAddr) (net.IP, error) {
38 | return net.ParseIP("10.4.0.5"), nil
39 | }
40 |
41 | //BroadcastEndpoints usually would return a list of VTEP endpoints for a given VNID
42 | //Hard coded to return []net.IP{192.168.1.2}
43 | func (n *NullController) BroadcastEndpoints(vnid uint32) ([]net.IP, error) {
44 | return []net.IP{net.ParseIP("192.168.1.2")}, nil
45 | }
46 |
47 | //RegisterEP usually register a VTEP
48 | func (n *NullController) RegisterEP(vnid uint32) error {
49 | return nil
50 | }
51 |
52 | //DeregisterEP usually remove a given VTEP
53 | func (n *NullController) DeregisterEP(vnid uint32) error {
54 | return nil
55 | }
56 |
--------------------------------------------------------------------------------
/pkg/l2/gc.go:
--------------------------------------------------------------------------------
1 | package l2
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | l2API "github.com/tcfw/vpc/pkg/api/v1/l2"
8 | )
9 |
10 | //Gc watches for vpcs which have no nics anymore and closes them off
11 | func (s *Server) Gc() {
12 | for {
13 | s.m.Lock()
14 |
15 | for _, stack := range s.stacks {
16 | if stack.Bridge == nil {
17 | continue
18 | }
19 | links, err := getBridgeLinks(stack.Bridge.Index, stack.VPCID)
20 | if err != nil {
21 | log.Println(err)
22 | continue
23 | }
24 | if len(links) == 0 {
25 | //delete the stack since no devices apart from vtep remaining
26 | vpcID := stack.VPCID
27 | err = deleteStack(stack)
28 | if err != nil {
29 | log.Println(err)
30 | continue
31 | }
32 |
33 | s.transport.DelEP(uint32(vpcID))
34 | s.sdn.DeregisterEP(uint32(vpcID))
35 | delete(s.stacks, vpcID)
36 |
37 | s.logChange(&l2API.StackChange{
38 | VpcId: vpcID,
39 | Action: "gc",
40 | })
41 | }
42 | }
43 |
44 | s.m.Unlock()
45 | time.Sleep(5 * time.Second)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/pkg/l2/miss.go:
--------------------------------------------------------------------------------
1 | package l2
2 |
3 | import (
4 | "log"
5 | "net"
6 | "time"
7 |
8 | "github.com/tcfw/vpc/pkg/l2/transport"
9 | )
10 |
11 | //HandleMisses monitors for subscription errors
12 | func (s *Server) HandleMisses(vpcID uint32, miss <-chan transport.ForwardingMiss) {
13 | ticker := time.Tick(10 * time.Second)
14 | for {
15 | select {
16 | case <-ticker:
17 | go s.handleEPPoll(uint32(vpcID))
18 | break
19 | case miss, ok := <-miss:
20 | if !ok {
21 | return
22 | }
23 | go s.handleMacLookup(uint32(vpcID), miss.HwAddr)
24 | break
25 | }
26 | }
27 | }
28 |
29 | //handleEPPoll applies zero'd hwaddrs to bridge forwarding
30 | func (s *Server) handleEPPoll(vpcID uint32) {
31 | endpoints, err := s.sdn.BroadcastEndpoints(vpcID)
32 | if err != nil {
33 | log.Printf("VTEP linking failed: %s", err)
34 | }
35 |
36 | for _, endpoint := range endpoints {
37 | hwaddr, _ := net.ParseMAC("00:00:00:00:00:00")
38 | s.transport.AddForwardEntry(vpcID, hwaddr, endpoint)
39 | }
40 | }
41 |
42 | //handleMacLookup looks up mac addresses from FDB misses and adds them back into the FDB
43 | func (s *Server) handleMacLookup(vpcID uint32, mac net.HardwareAddr) {
44 | gw, err := s.sdn.LookupMac(vpcID, mac)
45 | if err != nil || gw == nil {
46 | log.Printf("Failed to find VTEP for %s", mac)
47 | return
48 | }
49 |
50 | s.transport.AddForwardEntry(vpcID, mac, gw)
51 | }
52 |
--------------------------------------------------------------------------------
/pkg/l2/nic.go:
--------------------------------------------------------------------------------
1 | package l2
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/vishvananda/netlink"
7 | )
8 |
9 | const (
10 | nicPattern = "n-%s"
11 | )
12 |
13 | //VNic holds the state of known NICs
14 | type VNic struct {
15 | id string
16 | vlan uint16
17 | link netlink.Link
18 | manual bool
19 | }
20 |
21 | //CreateNIC creates a new tap device attached to a VPC bridge
22 | func CreateNIC(stack *Stack, id string, subnetVlan uint16) (netlink.Link, error) {
23 | if subnetVlan > 4096 {
24 | return nil, fmt.Errorf("subnet out of range")
25 | }
26 |
27 | nic := &netlink.Macvtap{
28 | Macvlan: netlink.Macvlan{
29 | LinkAttrs: netlink.LinkAttrs{
30 | Name: fmt.Sprintf(nicPattern, id),
31 | MTU: 1500,
32 | ParentIndex: stack.Bridge.Index,
33 | },
34 | Mode: netlink.MACVLAN_MODE_BRIDGE,
35 | },
36 | }
37 |
38 | if err := netlink.LinkAdd(nic); err != nil {
39 | return nil, fmt.Errorf("Failed to add tap device: %s", err)
40 | }
41 |
42 | // if err := netlink.BridgeVlanAdd(nic, subnetVlan, false, true, false, false); err != nil {
43 | // return nil, fmt.Errorf("Failed to add VLAN to nic: %s", err)
44 | // }
45 |
46 | if err := netlink.LinkSetUp(nic); err != nil {
47 | return nic, fmt.Errorf("failed to set nic to up: %s", err)
48 | }
49 |
50 | stack.Nics[id] = &VNic{id: id, vlan: subnetVlan, link: nic}
51 |
52 | return nic, nil
53 | }
54 |
55 | //GetNIC finds a tap interface given a stack and expected id
56 | func GetNIC(stack *Stack, id string) (netlink.Link, error) {
57 | handle, err := netlink.NewHandle(netlink.FAMILY_ALL)
58 | if err != nil {
59 | return nil, fmt.Errorf("could no get netlink handle: %s", err)
60 | }
61 | defer func() {
62 | handle.Delete()
63 | }()
64 |
65 | links, err := handle.LinkList()
66 | if err != nil {
67 | return nil, fmt.Errorf("could no get netlink list: %s", err)
68 | }
69 | for _, link := range links {
70 | if link.Attrs().Name == fmt.Sprintf(nicPattern, id) {
71 | return link, nil
72 | }
73 | }
74 | return nil, nil
75 | }
76 |
77 | //HasNIC checks if a nic exists by trying to get it
78 | func HasNIC(stack *Stack, id string) (bool, error) {
79 | nic, err := GetNIC(stack, id)
80 | if err != nil {
81 | return false, err
82 | }
83 | return nic != nil, nil
84 | }
85 |
86 | //DeleteNIC deletes the tap from
87 | func DeleteNIC(stack *Stack, id string) error {
88 | if ok, _ := HasNIC(stack, id); !ok {
89 | return fmt.Errorf("nic %s does not exist", id)
90 | }
91 |
92 | nic, err := GetNIC(stack, id)
93 | if err != nil {
94 | return err
95 | }
96 |
97 | if _, ok := stack.Nics[id]; ok {
98 | delete(stack.Nics, id)
99 | }
100 |
101 | if err := netlink.LinkDel(nic); err != nil {
102 | return err
103 | }
104 |
105 | return nil
106 | }
107 |
108 | //getNicVlans gets the vlans of a particular nic
109 | func getNicVlans(index int32) uint16 {
110 | links, _ := netlink.BridgeVlanList()
111 | for linkIndex, linkVlans := range links {
112 | if linkIndex == index && len(linkVlans) > 0 {
113 | return linkVlans[0].Vid
114 | }
115 | }
116 |
117 | return 1
118 | }
119 |
--------------------------------------------------------------------------------
/pkg/l2/pprof.go:
--------------------------------------------------------------------------------
1 | package l2
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 |
7 | //pprof http handler
8 | _ "net/http/pprof"
9 | )
10 |
11 | //StartPProf opens the http pprof handler to desired port
12 | func StartPProf(port int) {
13 | http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", port), nil)
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/l2/stack.go:
--------------------------------------------------------------------------------
1 | package l2
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/vishvananda/netlink"
7 | )
8 |
9 | //Stack stores references to the various devices for VPC connectivity
10 | type Stack struct {
11 | VPCID int32
12 | Bridge *netlink.Bridge
13 | Nics map[string]*VNic
14 | }
15 |
16 | //createStack creates a linux bridge and vtep to construct the VPC
17 | func createStack(vpcID int32) (*Stack, error) {
18 | br, err := createBridge(vpcID)
19 | if err != nil {
20 | deleteBridge(vpcID)
21 | return nil, fmt.Errorf("failed to create bridge: %s", err)
22 | }
23 |
24 | return &Stack{VPCID: vpcID, Bridge: br, Nics: map[string]*VNic{}}, nil
25 | }
26 |
27 | //getStack finds the linux bridge and vtep assocated with the VPC id
28 | func getStack(vpcID int32) (*Stack, error) {
29 | stack := &Stack{VPCID: vpcID, Nics: map[string]*VNic{}}
30 |
31 | br, err := getBridge(vpcID)
32 | if err != nil {
33 | return nil, err
34 | }
35 | if br != nil {
36 | stack.Bridge = br.(*netlink.Bridge)
37 | }
38 |
39 | return stack, nil
40 | }
41 |
42 | //deleteStack deletes both the linux bridge and vtep
43 | func deleteStack(stack *Stack) error {
44 | if err := deleteBridge(stack.VPCID); err != nil {
45 | return err
46 | }
47 |
48 | return nil
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/arp.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 |
8 | "github.com/google/gopacket"
9 | "github.com/google/gopacket/layers"
10 | "github.com/tcfw/vpc/pkg/l2/transport/tap/protocol"
11 | )
12 |
13 | //arpReduce proxies any ARP request to the SDN and provides injects a reply if available
14 | func (s *Listener) arpReduce(packet *protocol.Packet) error {
15 | defer func() {
16 | //@TODO fix the panics caused by misreading ARP requests
17 | if r := recover(); r != nil {
18 | log.Println("paniced in arp reduce", r)
19 | }
20 | }()
21 |
22 | if s.sdn == nil {
23 | return fmt.Errorf("no SDN attached")
24 | }
25 |
26 | _, arpRequest, err := s.decodeARPFrame(packet.Frame)
27 | if err != nil {
28 | return fmt.Errorf("failed to decode ARP request: %s", err)
29 | }
30 |
31 | mac, _, err := s.sdn.LookupIP(packet.VNID, 1, net.IP(arpRequest.DstProtAddress))
32 | if err != nil {
33 | return fmt.Errorf("failed to find IP for %s: %s", net.IP(arpRequest.DstProtAddress).String(), err)
34 | } else if len(mac) == 0 || mac == nil {
35 | return fmt.Errorf("invalid mac returned from SDN")
36 | }
37 |
38 | resp, err := s.buildARPResponse(1, arpRequest, mac)
39 | if err != nil {
40 | return fmt.Errorf("failed to build arp response: %s", err)
41 | }
42 |
43 | log.Printf("ARP REPLY: %s - % X", mac, resp)
44 | _, err = s.taps[packet.VNID].Write(resp)
45 |
46 | return err
47 | }
48 |
49 | //decodeARPFrame decodes dot1q and arp layers and validates accordingly
50 | func (s *Listener) decodeARPFrame(frame []byte) (*layers.Dot1Q, *layers.ARP, error) {
51 | packetData := gopacket.NewPacket(frame, layers.LayerTypeEthernet, gopacket.Default)
52 |
53 | // vlan := &layers.Dot1Q{}
54 | // dot1qLayer := packetData.Layer(layers.LayerTypeDot1Q)
55 | // if err := vlan.DecodeFromBytes(dot1qLayer.LayerContents(), gopacket.NilDecodeFeedback); err != nil {
56 | // return nil, nil, fmt.Errorf("failed to decode dot1q frame: %s", err)
57 | // }
58 |
59 | arp := &layers.ARP{}
60 | arpLayer := packetData.Layer(layers.LayerTypeARP)
61 | if err := arp.DecodeFromBytes(arpLayer.LayerContents(), gopacket.NilDecodeFeedback); err != nil {
62 | return nil, nil, fmt.Errorf("failed to decode arp frame: %s", err)
63 | }
64 |
65 | if arp.Protocol != layers.EthernetTypeIPv4 {
66 | return nil, nil, fmt.Errorf("unsupported addr type: %s", arp.AddrType)
67 | }
68 |
69 | if arp.Operation != layers.ARPRequest {
70 | return nil, nil, fmt.Errorf("unsupported ARP type: %d", arp.Operation)
71 | }
72 |
73 | if arp.HwAddressSize != 6 || arp.ProtAddressSize != 4 {
74 | return nil, nil, fmt.Errorf("unsupported address length: %d, %d", arp.HwAddressSize, arp.ProtAddressSize)
75 | }
76 |
77 | return nil, arp, nil
78 | }
79 |
80 | //buildARPResponse uses the original arp request to create an arp response
81 | func (s *Listener) buildARPResponse(vlanID uint16, arpRequest *layers.ARP, mac net.HardwareAddr) ([]byte, error) {
82 | eth := &layers.Ethernet{
83 | SrcMAC: mac,
84 | DstMAC: arpRequest.SourceHwAddress,
85 | EthernetType: layers.EthernetTypeARP,
86 | }
87 | // vlan := &layers.Dot1Q{
88 | // VLANIdentifier: vlanID,
89 | // Type: layers.EthernetTypeARP,
90 | // }
91 | arp := &layers.ARP{
92 | AddrType: layers.LinkTypeEthernet,
93 | Protocol: layers.EthernetTypeIPv4,
94 | Operation: layers.ARPReply,
95 | HwAddressSize: 6, //48-bit MAC
96 | ProtAddressSize: 4, //IPv4 (4 bytes)
97 | SourceHwAddress: mac,
98 | SourceProtAddress: arpRequest.DstProtAddress,
99 | DstHwAddress: arpRequest.SourceHwAddress,
100 | DstProtAddress: arpRequest.SourceProtAddress,
101 | }
102 |
103 | buf := gopacket.NewSerializeBuffer()
104 | err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true},
105 | eth,
106 | // vlan,
107 | arp)
108 |
109 | //Fix eth padding
110 | pack := buf.Bytes()[:42]
111 | return pack, err
112 | }
113 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/fdb.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "bytes"
5 | "hash/maphash"
6 | "log"
7 | "net"
8 | "sync"
9 | "time"
10 |
11 | "github.com/kpango/fastime"
12 | )
13 |
14 | //FDBEntry forwarding DB entry
15 | type FDBEntry struct {
16 | vnid uint32
17 | mac net.HardwareAddr
18 | rdst net.IP
19 | updated time.Time
20 | }
21 |
22 | //FDB forwarding DB
23 | type FDB struct {
24 | entries map[uint64]map[int]*FDBEntry
25 | mu sync.RWMutex
26 |
27 | broadcastMac net.HardwareAddr
28 | }
29 |
30 | //NewFDB inits a new FDB table and start GC timer
31 | func NewFDB() *FDB {
32 | bc, _ := net.ParseMAC("00:00:00:00:00:00")
33 |
34 | tbl := &FDB{
35 | entries: map[uint64]map[int]*FDBEntry{},
36 | broadcastMac: bc,
37 | }
38 | go tbl.gc()
39 |
40 | return tbl
41 | }
42 |
43 | //LookupMac finds dst VTEP for a given MAC
44 | func (tbl *FDB) LookupMac(vnid uint32, mac net.HardwareAddr) net.IP {
45 | var rdst net.IP
46 |
47 | tbl.mu.RLock()
48 | defer tbl.mu.RUnlock()
49 |
50 | hkey := hmackey(vnid, mac)
51 |
52 | if entries, ok := tbl.entries[hkey]; ok {
53 | for _, val := range entries {
54 | if bytes.Compare(val.mac, mac) == 0 && val.vnid == vnid && bytes.Compare(val.mac, tbl.broadcastMac) != 0 {
55 | //Update cache TS
56 | val.updated = fastime.Now()
57 | // tbl.entries[hkey][k] = val
58 |
59 | rdst = val.rdst
60 |
61 | break
62 | }
63 | }
64 | }
65 |
66 | return rdst
67 | }
68 |
69 | //ListBroadcast finds all broadcast dst VTEPs
70 | func (tbl *FDB) ListBroadcast(vnid uint32) []net.IP {
71 | tbl.mu.RLock()
72 | defer tbl.mu.RUnlock()
73 |
74 | dsts := []net.IP{}
75 |
76 | for _, entries := range tbl.entries {
77 | for _, val := range entries {
78 | if bytes.Compare(val.mac, tbl.broadcastMac) == 0 && val.vnid == vnid {
79 | dsts = append(dsts, val.rdst)
80 | }
81 | }
82 | }
83 |
84 | return dsts
85 | }
86 |
87 | //AddEntry adds a forwarding entry to the table
88 | func (tbl *FDB) AddEntry(vnid uint32, mac net.HardwareAddr, rdst net.IP) {
89 | loc := -1
90 |
91 | tbl.mu.RLock()
92 |
93 | hkey := hmackey(vnid, mac)
94 |
95 | if _, ok := tbl.entries[hkey]; !ok {
96 | tbl.entries[hkey] = map[int]*FDBEntry{}
97 | }
98 |
99 | for k, entry := range tbl.entries[hkey] {
100 | if entry.vnid == vnid && bytes.Compare(entry.mac, mac) == 0 && entry.rdst.Equal(rdst) {
101 | loc = k
102 | break
103 | }
104 | }
105 |
106 | tbl.mu.RUnlock()
107 |
108 | if loc == -1 {
109 | // log.Printf("added FDB rec: %s %s", mac, rdst)
110 | }
111 |
112 | tbl.mu.Lock()
113 | defer tbl.mu.Unlock()
114 |
115 | entry := &FDBEntry{
116 | vnid: vnid,
117 | rdst: rdst,
118 | mac: mac,
119 | updated: fastime.Now(),
120 | }
121 |
122 | if loc == -1 {
123 | tbl.entries[hkey][len(tbl.entries[hkey])] = entry
124 | } else {
125 | tbl.entries[hkey][loc] = entry
126 | }
127 | }
128 |
129 | func (tbl *FDB) gc() {
130 | for {
131 | time.Sleep(2 * time.Minute)
132 | tbl.gcOnce()
133 | }
134 | }
135 |
136 | func (tbl *FDB) gcOnce() {
137 | tbl.mu.Lock()
138 |
139 | for hkey, entries := range tbl.entries {
140 | for k, entry := range entries {
141 | //Delete entries older than 1 minute
142 | if entry.updated.Unix() < fastime.Now().Add(-3*time.Minute).Unix() {
143 | log.Printf("FDB GC: %s %s", entry.mac, entry.rdst)
144 | tbl.delEntry(hkey, k)
145 | }
146 | }
147 | if len(entries) == 0 {
148 | delete(tbl.entries, hkey)
149 | }
150 | }
151 |
152 | tbl.mu.Unlock()
153 | }
154 |
155 | func (tbl *FDB) delEntry(hkey uint64, i int) {
156 | delete(tbl.entries[hkey], i)
157 | if len(tbl.entries[hkey]) == 0 {
158 | delete(tbl.entries, hkey)
159 | }
160 | }
161 |
162 | var (
163 | hbase maphash.Hash
164 | )
165 |
166 | func hmackey(v uint32, mac net.HardwareAddr) uint64 {
167 | hbase.Reset()
168 | hbase.WriteByte(byte(v))
169 | hbase.WriteByte(byte(v >> 8))
170 | hbase.WriteByte(byte(v >> 16))
171 | hbase.WriteByte(byte(v >> 24))
172 | hbase.WriteByte(mac[0])
173 | hbase.WriteByte(mac[1])
174 | hbase.WriteByte(mac[2])
175 | hbase.WriteByte(mac[3])
176 | hbase.WriteByte(mac[4])
177 | hbase.WriteByte(mac[5])
178 | return hbase.Sum64()
179 | }
180 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/fdb_test.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "net"
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestFDBAddEntry(t *testing.T) {
13 | fdb := NewFDB()
14 |
15 | rdst := net.ParseIP("1.1.1.1")
16 | mac, _ := net.ParseMAC("10:20:30:40:50:60")
17 | var vnid uint32 = 1
18 |
19 | fdb.AddEntry(vnid, mac, rdst)
20 |
21 | ip := fdb.LookupMac(vnid, mac)
22 |
23 | assert.Equal(t, rdst, ip)
24 | assert.Len(t, fdb.entries, 1)
25 | }
26 |
27 | func TestFDBListBroadcast(t *testing.T) {
28 | fdb := NewFDB()
29 |
30 | rdst := net.ParseIP("1.1.1.1")
31 | mac, _ := net.ParseMAC("00:00:00:00:00:00")
32 | var vnid uint32 = 1
33 |
34 | fdb.AddEntry(vnid, mac, rdst)
35 |
36 | ips := fdb.ListBroadcast(vnid)
37 |
38 | assert.Contains(t, ips, rdst)
39 | }
40 |
41 | func TestFDBLookupMacWithBroadcast(t *testing.T) {
42 | fdb := NewFDB()
43 |
44 | rdst1 := net.ParseIP("1.1.1.1")
45 | rdst2 := net.ParseIP("2.2.2.2")
46 | mac, _ := net.ParseMAC("10:20:30:40:50:60")
47 | bcmac, _ := net.ParseMAC("00:00:00:00:00:00")
48 | var vnid uint32 = 1
49 |
50 | fdb.AddEntry(vnid, bcmac, rdst2)
51 | fdb.AddEntry(vnid, mac, rdst1)
52 |
53 | ip := fdb.LookupMac(vnid, mac)
54 |
55 | assert.Equal(t, rdst1, ip)
56 | }
57 |
58 | func BenchmarkFDBLoopkup(b *testing.B) {
59 | fdb := NewFDB()
60 |
61 | mac, _ := net.ParseMAC("10:20:30:40:50:60")
62 | var vnid uint32 = 2
63 |
64 | n := 10000
65 | for i := 0; i < n; i++ {
66 | fdb.AddEntry(vnid, randomMac(), randomIP4())
67 | }
68 | fdb.AddEntry(vnid, mac, randomIP4())
69 |
70 | b.ResetTimer()
71 |
72 | b.Run("Lookup", func(b *testing.B) {
73 | for i := 0; i < b.N; i++ {
74 | fdb.LookupMac(vnid, mac)
75 | }
76 | })
77 | }
78 |
79 | func BenchmarkHMacKey(b *testing.B) {
80 | mac := net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
81 | vnid := uint32(1)
82 |
83 | b.Run("hkey", func(b *testing.B) {
84 | for i := 0; i < b.N; i++ {
85 | hmackey(vnid, mac)
86 | }
87 | })
88 | }
89 |
90 | func randomIP4() net.IP {
91 | return net.ParseIP(fmt.Sprintf("%d.%d.%d.%d", rand.Intn(255), rand.Intn(255), rand.Intn(255), rand.Intn(255)))
92 | }
93 |
94 | func randomMac() net.HardwareAddr {
95 | buf := make([]byte, 6)
96 | _, err := rand.Read(buf)
97 | if err != nil {
98 | fmt.Println("error:", err)
99 | return nil
100 | }
101 | // Set the local bit
102 | buf[0] |= 2
103 |
104 | return net.HardwareAddr(buf)
105 | }
106 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/icmp6.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 |
8 | "github.com/google/gopacket"
9 | "github.com/google/gopacket/layers"
10 |
11 | "github.com/tcfw/vpc/pkg/l2/transport/tap/protocol"
12 | )
13 |
14 | const (
15 | icmpv6NeighborAdvertisementFlagsSolicated = 1 << 6
16 | icmpv6NeighborAdvertisementFlagsOverride = 1 << 5
17 | )
18 |
19 | type icmpv6Layers struct {
20 | ethernet *layers.Ethernet
21 | vlan *layers.Dot1Q
22 | ip *layers.IPv6
23 | icmp *layers.ICMPv6NeighborSolicitation
24 | }
25 |
26 | //icmp6NDPReduce proxies any ICMPv6 Neighbor Solicitation requests to the SDN and provides injects a neighbor advertisement if available
27 | //NOTE: Router solicification is handled by the layer-3 agent router still
28 | func (s *Listener) icmp6NDPReduce(packet *protocol.Packet) error {
29 | log.Printf("ICMPv6 NDP")
30 | log.Printf("%x", packet.Frame)
31 |
32 | if packet.Frame[58] != layers.ICMPv6TypeNeighborSolicitation {
33 | return fmt.Errorf("NDP type %d is not supported", packet.Frame[58])
34 | }
35 |
36 | reqLayers, err := s.decodeICMPv6Frame(packet.Frame)
37 | if err != nil {
38 | return fmt.Errorf("failed to decode frame: %s", err)
39 | }
40 |
41 | mac, _, err := s.sdn.LookupIP(packet.VNID, 1, reqLayers.icmp.TargetAddress)
42 | if err != nil {
43 | return fmt.Errorf("failed to find IP: %s", err)
44 | }
45 |
46 | resp, err := s.buildICMPv6NAResponse(reqLayers, mac)
47 | if err != nil {
48 | return fmt.Errorf("failed to build arp response: %s", err)
49 | }
50 |
51 | log.Printf("Sending ICMPv6 response %x", resp)
52 |
53 | s.taps[packet.VNID].Write(resp)
54 |
55 | return nil
56 | }
57 |
58 | //decodeICMPv6Frame decodes dot1q, ipv6 and ICMPv6 layers and validates accordingly
59 | func (s *Listener) decodeICMPv6Frame(frame []byte) (*icmpv6Layers, error) {
60 | eth := &layers.Ethernet{}
61 | // vlan := &layers.Dot1Q{}
62 | ipv6 := &layers.IPv6{}
63 | icmp := &layers.ICMPv6{}
64 | icmpv6 := &layers.ICMPv6NeighborSolicitation{}
65 |
66 | parser := gopacket.NewDecodingLayerParser(layers.LayerTypeEthernet,
67 | eth,
68 | // vlan,
69 | ipv6, icmp, icmpv6)
70 |
71 | respLayers := make([]gopacket.LayerType, 0)
72 | if err := parser.DecodeLayers(frame, &respLayers); err != nil {
73 | return nil, err
74 | }
75 |
76 | //RFC4861 hop limit should still be 255
77 | if ipv6.HopLimit != 255 {
78 | goto invalidFrame
79 | }
80 |
81 | //RFC4861 neighbor solicitations cannot be to multicast addrs
82 | if icmpv6.TargetAddress.IsMulticast() {
83 | goto invalidFrame
84 | }
85 |
86 | return &icmpv6Layers{ethernet: eth, ip: ipv6, icmp: icmpv6}, nil
87 |
88 | invalidFrame:
89 | return nil, fmt.Errorf("Invalid ICMPv6")
90 |
91 | }
92 |
93 | func (s *Listener) buildICMPv6NAResponse(reqLayers *icmpv6Layers, mac net.HardwareAddr) ([]byte, error) {
94 | eth := &layers.Ethernet{
95 | SrcMAC: mac,
96 | DstMAC: reqLayers.ethernet.SrcMAC,
97 | EthernetType: layers.EthernetTypeDot1Q,
98 | }
99 | // vlan := &layers.Dot1Q{
100 | // VLANIdentifier: reqLayers.vlan.VLANIdentifier,
101 | // Type: layers.EthernetTypeIPv6,
102 | // }
103 | ipv6 := &layers.IPv6{
104 | Version: 6,
105 | DstIP: reqLayers.ip.SrcIP,
106 | SrcIP: reqLayers.icmp.TargetAddress,
107 | HopLimit: 255,
108 | NextHeader: layers.IPProtocolICMPv6,
109 | }
110 | icmpv6 := &layers.ICMPv6{
111 | TypeCode: layers.CreateICMPv6TypeCode(layers.ICMPv6TypeNeighborAdvertisement, 0),
112 | }
113 | icmpv6.SetNetworkLayerForChecksum(ipv6)
114 | icmpv6NA := &layers.ICMPv6NeighborAdvertisement{
115 | Flags: icmpv6NeighborAdvertisementFlagsSolicated | icmpv6NeighborAdvertisementFlagsOverride,
116 | TargetAddress: reqLayers.icmp.TargetAddress,
117 | Options: layers.ICMPv6Options{
118 | layers.ICMPv6Option{
119 | Type: layers.ICMPv6OptTargetAddress,
120 | Data: mac,
121 | },
122 | },
123 | }
124 |
125 | buf := gopacket.NewSerializeBuffer()
126 | err := gopacket.SerializeLayers(buf, gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true},
127 | eth,
128 | // vlan,
129 | ipv6,
130 | icmpv6,
131 | icmpv6NA)
132 |
133 | return buf.Bytes(), err
134 | }
135 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/icmp6_test.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "log"
5 | "net"
6 | "testing"
7 |
8 | "github.com/google/gopacket/layers"
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestICMPv6NSDecode(t *testing.T) {
13 | listener, err := NewListener()
14 | if !assert.NoError(t, err) {
15 | return
16 | }
17 |
18 | frame := []byte{
19 | 0x33, 0x33, 0xff, 0x00, 0x00, 0x01, 0xd6, 0xde, 0x5b, 0x62, 0x52, 0x6a,
20 | 0x86, 0xdd, 0x60, 0x00, 0x00, 0x00, 0x00, 0x20, 0x3a, 0xff, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00,
21 | 0x00, 0x00, 0xd4, 0xde, 0x5b, 0xff, 0xfe, 0x62, 0x52, 0x6a, 0xff, 0x02, 0x00, 0x00, 0x00, 0x00,
22 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x01, 0x87, 0x00, 0x76, 0x44, 0x00, 0x00,
23 | 0x00, 0x00, 0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
24 | 0x00, 0x01, 0x01, 0x01, 0xd6, 0xde, 0x5b, 0x62, 0x52, 0x6a,
25 | }
26 |
27 | layers, err := listener.decodeICMPv6Frame(frame)
28 | if !assert.NoError(t, err) {
29 | return
30 | }
31 |
32 | // assert.Equal(t, uint16(5), layers.vlan.VLANIdentifier)
33 | assert.Equal(t, net.ParseIP("fe80::1"), layers.icmp.TargetAddress)
34 | }
35 |
36 | func TestICMPv6NSEncode(t *testing.T) {
37 | listener, err := NewListener()
38 | if !assert.NoError(t, err) {
39 | return
40 | }
41 |
42 | layers := &icmpv6Layers{
43 | ethernet: &layers.Ethernet{
44 | SrcMAC: net.HardwareAddr{0x82, 0x11, 0x0, 0x0, 0x0, 0xff},
45 | DstMAC: net.HardwareAddr{0x33, 0x33, 0x0, 0x0, 0x0, 0x0},
46 | },
47 | vlan: &layers.Dot1Q{
48 | VLANIdentifier: 5,
49 | },
50 | ip: &layers.IPv6{
51 | SrcIP: net.ParseIP("fe80::1"),
52 | DstIP: net.ParseIP("ff02::1:ff00:1"),
53 | },
54 | icmp: &layers.ICMPv6NeighborSolicitation{
55 | TargetAddress: net.ParseIP("fe80::2"),
56 | },
57 | }
58 |
59 | raw, err := listener.buildICMPv6NAResponse(layers, net.HardwareAddr{0x82, 0x11, 0x22, 0x33, 0x44, 0x55})
60 | if !assert.NoError(t, err) {
61 | return
62 | }
63 |
64 | log.Printf("%x", raw)
65 |
66 | assert.NotEmpty(t, raw)
67 | }
68 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/linux.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "strings"
8 | "unsafe"
9 |
10 | "golang.org/x/sys/unix"
11 | )
12 |
13 | type tuntapDev struct {
14 | io.ReadWriteCloser
15 | fd int
16 | Device string
17 | }
18 |
19 | const (
20 | cIFFTUN = 0x0001
21 | cIFFTAP = 0x0002
22 | cIFFNOPI = 0x1000
23 | cIFFMULTIQUEUE = 0x0100
24 | )
25 |
26 | type ifReq struct {
27 | Name [16]byte
28 | Flags uint16
29 | pad [8]byte
30 | }
31 |
32 | func ioctl(a1, a2, a3 uintptr) error {
33 | _, _, errno := unix.Syscall(unix.SYS_IOCTL, a1, a2, a3)
34 | if errno != 0 {
35 | return os.NewSyscallError("ioctl", errno)
36 | }
37 | return nil
38 | }
39 |
40 | func (c *tuntapDev) WriteRaw(b []byte) (int, error) {
41 | var nn int
42 | for {
43 | max := len(b)
44 | n, err := unix.Write(c.fd, b[nn:max])
45 | if n > 0 {
46 | nn += n
47 | }
48 | if nn == len(b) {
49 | return nn, err
50 | }
51 |
52 | if err != nil {
53 | return nn, err
54 | }
55 |
56 | if n == 0 {
57 | return nn, io.ErrUnexpectedEOF
58 | }
59 | }
60 | }
61 |
62 | func (v *Tap) openDev(deviceName string) (ifce *tuntapDev, err error) {
63 | fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
64 | if err != nil {
65 | return nil, fmt.Errorf("failed to open linux tun dev: %s", err)
66 | }
67 |
68 | var req ifReq
69 | req.Flags = cIFFNOPI | cIFFTAP | cIFFMULTIQUEUE
70 | copy(req.Name[:], deviceName)
71 |
72 | if err = ioctl(uintptr(fd), unix.TUNSETIFF, uintptr(unsafe.Pointer(&req))); err != nil {
73 | return nil, err
74 | }
75 |
76 | name := strings.Trim(string(req.Name[:]), "\x00")
77 |
78 | file := os.NewFile(uintptr(fd), "/dev/net/tun")
79 |
80 | ifce = &tuntapDev{
81 | ReadWriteCloser: file,
82 | fd: int(file.Fd()),
83 | Device: name,
84 | }
85 |
86 | return
87 | }
88 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/listener_test.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "net"
5 | "testing"
6 |
7 | "github.com/google/gopacket"
8 | "github.com/google/gopacket/layers"
9 | "github.com/tcfw/vpc/pkg/l2/transport/tap/protocol"
10 | )
11 |
12 | func BenchmarkL2Fwd(b *testing.B) {
13 | var vnid uint32 = 1
14 |
15 | handler := &protocol.TestHandler{}
16 | fdb := NewFDB()
17 | tap := &testTap{
18 | vnid: vnid,
19 | }
20 |
21 | lis := &Listener{
22 | taps: map[uint32]Nic{},
23 | mtu: 1500,
24 | FDB: fdb,
25 | conn: handler,
26 | }
27 | lis.taps[vnid] = tap
28 |
29 | lis.Start()
30 | tap.SetHandler(lis.Send)
31 | fdb.AddEntry(vnid, net.HardwareAddr{0xCC, 0x03, 0x04, 0xDC, 0x00, 0x10}, net.ParseIP("1.1.1.1"))
32 |
33 | frame, _ := constructPacket()
34 | frameLen := int64(len(frame))
35 |
36 | frames := []protocol.Packet{{VNID: vnid, Frame: frame[:]}}
37 |
38 | b.Run("Send", func(b *testing.B) {
39 | for i := 0; i < b.N; i++ {
40 | tap.Receive(frames)
41 | b.SetBytes(frameLen)
42 | }
43 | })
44 | }
45 |
46 | func constructPacket() ([]byte, error) {
47 | srcMAC := []byte{0xb2, 0x96, 0x81, 0x75, 0xb2, 0x11}
48 | dstMAC := []byte{0xe6, 0x15, 0x9c, 0x38, 0xf6, 0xe7}
49 | SrcIP := "127.0.0.1"
50 | DstIP := "8.8.8.8"
51 | SrcPort := 87654
52 | DstPort := 87654
53 |
54 | eth := &layers.Ethernet{
55 | SrcMAC: net.HardwareAddr(srcMAC),
56 | DstMAC: net.HardwareAddr(dstMAC),
57 | EthernetType: layers.EthernetTypeDot1Q,
58 | }
59 | vlan := &layers.Dot1Q{
60 | VLANIdentifier: 5,
61 | Type: layers.EthernetTypeIPv4,
62 | }
63 | ip := &layers.IPv4{
64 | Version: 4,
65 | IHL: 5,
66 | TTL: 64,
67 | Id: 0,
68 | Protocol: layers.IPProtocolUDP,
69 | SrcIP: net.ParseIP(SrcIP).To4(),
70 | DstIP: net.ParseIP(DstIP).To4(),
71 | }
72 | udp := &layers.UDP{
73 | SrcPort: layers.UDPPort(SrcPort),
74 | DstPort: layers.UDPPort(DstPort),
75 | }
76 | udp.SetNetworkLayerForChecksum(ip)
77 | buffer := gopacket.NewSerializeBuffer()
78 | err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{
79 | FixLengths: true,
80 | ComputeChecksums: true,
81 | },
82 | eth, vlan, ip, udp,
83 | gopacket.Payload([]byte("123456789abcdefghi123456789abcdefghi")),
84 | )
85 |
86 | return buffer.Bytes(), err
87 | }
88 |
89 | func constructVPCPacket() ([]byte, error) {
90 | srcMAC := []byte{0xb2, 0x96, 0x81, 0x75, 0xb2, 0x11}
91 | dstMAC := []byte{0xe6, 0x15, 0x9c, 0x38, 0xf6, 0xe7}
92 | SrcIP := "127.0.0.1"
93 | DstIP := "8.8.8.8"
94 | SrcPort := 87654
95 | DstPort := 87654
96 |
97 | eth := &layers.Ethernet{
98 | SrcMAC: net.HardwareAddr(srcMAC),
99 | DstMAC: net.HardwareAddr(dstMAC),
100 | EthernetType: layers.EthernetTypeIPv4,
101 | }
102 | ip := &layers.IPv4{
103 | Version: 4,
104 | IHL: 5,
105 | TTL: 64,
106 | Id: 0,
107 | Protocol: 172,
108 | SrcIP: net.ParseIP(SrcIP).To4(),
109 | DstIP: net.ParseIP(DstIP).To4(),
110 | }
111 | udp := &layers.UDP{
112 | SrcPort: layers.UDPPort(SrcPort),
113 | DstPort: layers.UDPPort(DstPort),
114 | }
115 | udp.SetNetworkLayerForChecksum(ip)
116 | buffer := gopacket.NewSerializeBuffer()
117 | err := gopacket.SerializeLayers(buffer, gopacket.SerializeOptions{
118 | FixLengths: true,
119 | ComputeChecksums: true,
120 | },
121 | eth, ip, udp,
122 | gopacket.Payload([]byte("123456789abcdefghi123456789")),
123 | )
124 |
125 | return buffer.Bytes(), err
126 | }
127 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/protocol.go:
--------------------------------------------------------------------------------
1 | package protocol
2 |
3 | import "net"
4 |
5 | //Handler provides methods of forwarding frames to another endpoint
6 | type Handler interface {
7 | Start() error
8 | Stop() error
9 |
10 | Send([]Packet, net.IP) (int, error)
11 | SetHandler(HandlerFunc)
12 | }
13 |
14 | //Packet holds the ethernet frame and the desired VNID
15 | type Packet struct {
16 | VNID uint32
17 | Source []byte
18 | Frame []byte
19 | }
20 |
21 | //NewPacket constructs a new packet give an ethernet frame and desired VNID
22 | func NewPacket(vnid uint32, frame []byte) Packet {
23 | return Packet{VNID: vnid, Frame: frame}
24 | }
25 |
26 | //HandlerFunc allows callbacks when receiving packets from a remote source
27 | type HandlerFunc func([]Packet)
28 |
29 | //TestHandler provides a way to track and drop output packets
30 | //and simulate inbound packets
31 | type TestHandler struct {
32 | recv HandlerFunc
33 | handledCount int
34 | handled []*Packet
35 | StoreOutbound bool
36 | }
37 |
38 | //Stats provides the handled packet count and any stored packets
39 | func (th *TestHandler) Stats() (int, []*Packet) {
40 | return th.handledCount, th.handled
41 | }
42 |
43 | //Start - Fulfil interface
44 | func (th *TestHandler) Start() error {
45 | return nil
46 | }
47 |
48 | //Stop - Fulfil interface
49 | func (th *TestHandler) Stop() error {
50 | return nil
51 | }
52 |
53 | //Send records to the handled
54 | func (th *TestHandler) Send(packets []Packet, rdst net.IP) (int, error) {
55 | for _, packet := range packets {
56 | th.SendOne(&packet, rdst)
57 | }
58 | return 0, nil
59 | }
60 |
61 | //SendOne a single record to the handled
62 | func (th *TestHandler) SendOne(packet *Packet, rdst net.IP) (int, error) {
63 | th.handledCount++
64 | if th.StoreOutbound {
65 | th.handled = append(th.handled, packet)
66 | }
67 | return 0, nil
68 | }
69 |
70 | //SetHandler sets the TX handler for l2 forwarding
71 | func (th *TestHandler) SetHandler(h HandlerFunc) {
72 | th.recv = h
73 | }
74 |
75 | //Receive allows for packet injection into the tap as if coming from OS
76 | func (th *TestHandler) Receive(p []Packet) error {
77 | th.recv(p)
78 | return nil
79 | }
80 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/quic/frames.go:
--------------------------------------------------------------------------------
1 | package quic
2 |
3 | type helloFrame struct {
4 | vnid uint32
5 | }
6 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/quic/quic_test.go:
--------------------------------------------------------------------------------
1 | package quic
2 |
3 | import (
4 | "net"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | "github.com/tcfw/vpc/pkg/l2/transport/tap/protocol"
9 | )
10 |
11 | func TestSendRecv(t *testing.T) {
12 | handler := NewHandler()
13 |
14 | cliHandler := &Handler{
15 | epConns: map[string]*epConn{},
16 | port: 18443,
17 | }
18 |
19 | if !assert.NoError(t, handler.Start()) {
20 | return
21 | }
22 |
23 | var recvPacket *protocol.Packet
24 | handler.SetHandler(func(packets []protocol.Packet) {
25 | if len(packets) == 0 {
26 | return
27 | }
28 |
29 | recvPacket = &packets[0]
30 | })
31 |
32 | frame := []byte{
33 | 0xCC, 0x03, 0x04, 0xDC, 0x00, 0x10, 0xFF, 0xAA, 0xFA, 0xAA, 0xFF, 0xAA, 0x81, 0x00,
34 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
35 | 0x00, 0x00, 0x7F, 0x00, 0x00, 0x01, 0x08, 0x08, 0x08, 0x08, 0x00, 0x08, 0x00, 0x00,
36 | 0x00, 0x01, 0x00, 0x01, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
37 | 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
38 | }
39 | var vnid uint32 = 5
40 | sendPacket := protocol.Packet{VNID: vnid, Frame: frame}
41 | rdst := net.ParseIP("::1")
42 |
43 | _, err := cliHandler.Send([]protocol.Packet{sendPacket}, rdst)
44 | if !assert.NoError(t, err) {
45 | return
46 | }
47 |
48 | //Wait for something to arrive
49 | for {
50 | if recvPacket != nil {
51 | break
52 | }
53 | }
54 |
55 | assert.Equal(t, frame, []byte(recvPacket.Frame))
56 | assert.Equal(t, vnid, recvPacket.VNID)
57 | }
58 |
59 | func BenchmarkQuicSend(b *testing.B) {
60 | handler := NewHandler()
61 | handler.Start()
62 |
63 | cliHandler := &Handler{
64 | epConns: map[string]*epConn{},
65 | port: 18443,
66 | }
67 |
68 | frame := []byte{
69 | 0xCC, 0x03, 0x04, 0xDC, 0x00, 0x10, 0xFF, 0xAA, 0xFA, 0xAA, 0xFF, 0xAA, 0x81, 0x00,
70 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
71 | 0x00, 0x00, 0x7F, 0x00, 0x00, 0x01, 0x08, 0x08, 0x08, 0x08, 0x00, 0x08, 0x00, 0x00,
72 | 0x00, 0x01, 0x00, 0x01, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,
73 | 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
74 | }
75 | packets := []protocol.Packet{{VNID: 5, Frame: frame}}
76 | rdst := net.ParseIP("::1")
77 |
78 | received := 0
79 |
80 | handler.SetHandler(func(packets []protocol.Packet) {
81 | for _, packet := range packets {
82 | received += len(packet.Frame)
83 | }
84 | })
85 |
86 | b.Run("send", func(b *testing.B) {
87 | b.SetBytes(int64(len(frame)))
88 | for i := 0; i < b.N; i++ {
89 | cliHandler.Send(packets, rdst)
90 | }
91 | })
92 | }
93 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vpctp/ebpf.go:
--------------------------------------------------------------------------------
1 | package vpctp
2 |
3 | import (
4 | "github.com/cilium/ebpf"
5 | "github.com/cilium/ebpf/asm"
6 | )
7 |
8 | func vpctpEbpfProg(xsksMap *ebpf.Map, qidMap *ebpf.Map) (*ebpf.Program, error) {
9 | return ebpf.NewProgram(&ebpf.ProgramSpec{
10 | Name: "vpc_xsk_ebpf",
11 | Type: ebpf.XDP,
12 | License: "LGPL-2.1 or BSD-2-Clause",
13 | KernelVersion: 0,
14 | Instructions: asm.Instructions{
15 | //# int xdp_sock_prog(struct xdp_md *ctx) {
16 | {OpCode: 0xbf, Src: 0x1, Dst: 0x6}, //0: r6 = r1
17 | //# int *qidconf, index = ctx->rx_queue_index;
18 | {OpCode: 0x61, Src: 0x6, Dst: 0x1, Offset: 0x10}, //1: r1 = *(u32 *)(r6 + 16)
19 | {OpCode: 0x63, Src: 0x1, Dst: 0xa, Offset: -4}, //2: *(u32 *)(r10 - 4) = r1
20 | {OpCode: 0xbf, Src: 0xa, Dst: 0x2}, //3: r2 = r10
21 | {OpCode: 0x07, Dst: 0x2, Constant: -4}, //4: r2 += -4
22 | //# qidconf = bpf_map_lookup_elem(&qidconf_map, &index);
23 | {OpCode: 0x18, Src: 0x1, Dst: 0x1, Constant: int64(qidMap.FD())}, //5: r1 = 0 ll
24 | {OpCode: 0x85, Constant: 0x1}, //7: call 1
25 | {OpCode: 0xbf, Src: 0x0, Dst: 0x1}, //8: r1 = r0
26 | {OpCode: 0xb7, Dst: 0x0, Constant: 0}, //9: r0 = 0
27 | //# if (!qidconf)
28 | {OpCode: 0x15, Dst: 0x1, Offset: 0x1f}, //10: if r1 == 0 goto +31
29 | {OpCode: 0xb7, Dst: 0x0, Constant: 0x02}, //11: r0 = 2
30 | //# if (!*qidconf)
31 | {OpCode: 0x61, Src: 0x1, Dst: 0x1, Constant: 0}, //12: r1 = *(u32 *)(r1 + 0)
32 | {OpCode: 0x15, Dst: 0x1, Offset: 0x1c}, //13: if r1 == 0 goto +28
33 | //# void *data_end = (void *)(long)ctx->data_end;
34 | {OpCode: 0x61, Src: 0x6, Dst: 0x2, Offset: 0x04}, //14: r2 = *(u32 *)(r6 + 4)
35 | //# void *data = (void *)(long)ctx->data;
36 | {OpCode: 0x61, Src: 0x6, Dst: 0x1, Constant: 0}, //15: r1 = *(u32 *)(r6 + 0)
37 | //# if (data + nh_off > data_end)
38 | {OpCode: 0xbf, Src: 0x1, Dst: 0x3}, //16: r3 = r1
39 | {OpCode: 0x07, Dst: 0x3, Constant: 0x0e}, //17: r3 += 14
40 | {OpCode: 0x2d, Src: 0x2, Dst: 0x3, Offset: 0x17}, //18: if r3 > r2 goto +23
41 | //# h_proto = eth->h_proto;
42 | {OpCode: 0x71, Src: 0x1, Dst: 0x4, Offset: 0x0c}, //19: r4 = *(u8 *)(r1 + 12)
43 | {OpCode: 0x71, Src: 0x1, Dst: 0x3, Offset: 0x0d}, //20: r3 = *(u8 *)(r1 + 13)
44 | {OpCode: 0x67, Dst: 0x3, Constant: 0x08}, //21: r3 <<= 8
45 | {OpCode: 0x4f, Src: 0x4, Dst: 0x3}, //22: r3 |= r4
46 | //# if (h_proto == __bpf_constant_htons(ETH_P_IP))
47 | {OpCode: 0x15, Dst: 0x3, Offset: 0x06, Constant: 0x86dd}, //23: if r3 == 56710 goto +6
48 | {OpCode: 0x55, Dst: 0x3, Offset: 0x11, Constant: 0x08}, //24: if r3 != 8 goto +17
49 | {OpCode: 0xb7, Dst: 0x3, Constant: 0x17}, //25: r3 = 23
50 | //# if (iph + 1 > data_end)
51 | {OpCode: 0xbf, Src: 0x1, Dst: 0x4}, //26: r4 = r1
52 | {OpCode: 0x07, Dst: 0x4, Constant: 0x22}, //27: r4 += 34
53 | {OpCode: 0x2d, Src: 0x2, Dst: 0x4, Offset: 0x0d}, //28: if r4 > r2 goto +13
54 | {OpCode: 0x05, Offset: 0x04}, //29: goto +4
55 | {OpCode: 0xb7, Dst: 0x3, Constant: 0x14}, //30: r3 = 20
56 | //# if (ip6h + 1 > data_end)
57 | {OpCode: 0xbf, Src: 0x1, Dst: 0x4}, //31: r4 = r1
58 | {OpCode: 0x07, Dst: 0x4, Constant: 0x36}, //32: r4 += 54
59 | {OpCode: 0x2d, Src: 0x2, Dst: 0x4, Offset: 0x08}, //33: if r4 > r2 goto +8
60 | {OpCode: 0x0f, Src: 0x3, Dst: 0x1}, //34: r1 += r3
61 | {OpCode: 0x71, Src: 0x1, Dst: 0x1, Constant: 0}, //35: r1 = *(u8 *)(r1 + 0)
62 | //# if (ipproto != VPC_PROTO)
63 | {OpCode: 0x55, Dst: 0x1, Offset: 0x05, Constant: IPPROTO}, //36: if r1 != 172 goto +5
64 | //# return bpf_redirect_map(&xsks_map, index, 0);
65 | {OpCode: 0x61, Src: 0xa, Dst: 0x2, Offset: -4}, //37: r2 = *(u32 *)(r10 - 4)
66 | {OpCode: 0x18, Src: 0x1, Dst: 0x1, Constant: int64(xsksMap.FD())}, //38: r1 = 0 ll
67 | {OpCode: 0xb7, Dst: 0x3, Constant: 0}, //40: r3 = 0
68 | {OpCode: 0x85, Constant: 0x33}, //41: call 51
69 | //# }
70 | {OpCode: 0x95}, //42: exit
71 | },
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vpctp/xdp/Makefile:
--------------------------------------------------------------------------------
1 | OBJS = vpc_kern.o
2 |
3 | LLC ?= llc
4 | CLANG ?= clang
5 | INC_FLAGS = -nostdinc -isystem `$(CLANG) -print-file-name=include`
6 | EXTRA_CFLAGS ?= -O2 -emit-llvm -g
7 |
8 | # In case up-to-date headers are not installed locally in /usr/include,
9 | # use source build.
10 | linuxhdrs ?= /usr/src/linux-headers-`uname -r`
11 |
12 | LINUXINCLUDE = -I$(linuxhdrs)/arch/x86/include/uapi \
13 | -I$(linuxhdrs)/arch/x86/include/generated/uapi \
14 | -I$(linuxhdrs)/arch/x86/include/ \
15 | -I$(linuxhdrs)/include/generated/uapi \
16 | -I$(linuxhdrs)/include/uapi \
17 | -I$(linuxhdrs)/include
18 |
19 | all: $(OBJS)
20 |
21 | .PHONY: clean
22 | clean:
23 | rm -f $(OBJS)
24 |
25 | $(OBJS): %.o:%.c
26 | $(CLANG) $(INC_FLAGS) \
27 | -D__KERNEL__ -D__EXPORTED_HEADERS__ \
28 | -I../include $(LINUXINCLUDE) \
29 | -Wno-unused-value -Wno-pointer-sign \
30 | -Wno-compare-distinct-pointer-types \
31 | -Wno-gnu-variable-sized-type-not-at-end \
32 | -Wno-address-of-packed-member -Wno-tautological-compare \
33 | -Wno-unknown-warning-option \
34 | $(EXTRA_CFLAGS) -c $< -o -| $(LLC) -march=bpf -filetype=obj -o $@
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vpctp/xdp/README.md:
--------------------------------------------------------------------------------
1 | # Converting object files to BPF ASM
2 | Usually you can just load the object file into the network interface by using iproute2.
3 | This became problematic when trying to use the file either having invlade opcodes due to LLVM
4 | or bad references to BPF/XDP maps.
5 |
6 | To read an translate the object file to opcodes, use something like `llvm-objdump -Ss vpc_kern.o` to
7 | view the BPF opcodes and painfully convert them to the raw op codes found in the go code.
8 | This can be sped up using a good IDE.
9 |
10 | ## BPF Opcode Basic structure
11 | More info here: https://github.com/iovisor/bpf-docs/blob/master/eBPF.md and https://www.kernel.org/doc/Documentation/networking/filter.txt
12 | ```
13 | msb lsb
14 | +------------------------+----------------+----+----+--------+
15 | |immediate |offset |src |dst |opcode |
16 | +------------------------+----------------+----+----+--------+
17 | ```
18 | From least significant to most significant bit:
19 |
20 | - 8 bit opcode
21 | - 4 bit destination register (dst)
22 | - 4 bit source register (src)
23 | - 16 bit offset
24 | - 32 bit immediate/constant (imm)
25 |
26 | ## Notes from LLVM
27 | There are certain versions of llc/llvm in which Opcode 0x85 (call imm or "Function call") has the src & dst registers are set to R1. In most
28 | cases, this causes the program to be rejected and needs to be updated with a src & dst registers of R0.
29 |
30 | Future results may differ!
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vpctp/xdp/bpf_helpers.h:
--------------------------------------------------------------------------------
1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
2 | #ifndef __BPF_HELPERS__
3 | #define __BPF_HELPERS__
4 |
5 | /*
6 | * Note that bpf programs need to include either
7 | * vmlinux.h (auto-generated from BTF) or linux/types.h
8 | * in advance since bpf_helper_defs.h uses such types
9 | * as __u64.
10 | */
11 | #include "bpf_helper_defs.h"
12 |
13 | #define __uint(name, val) int(*name)[val]
14 | #define __type(name, val) typeof(val) *name
15 | #define __array(name, val) typeof(val) *name[]
16 |
17 | /* Helper macro to print out debug messages */
18 | #define bpf_printk(fmt, ...) \
19 | ({ \
20 | char ____fmt[] = fmt; \
21 | bpf_trace_printk(____fmt, sizeof(____fmt), \
22 | ##__VA_ARGS__); \
23 | })
24 |
25 | /*
26 | * Helper macro to place programs, maps, license in
27 | * different sections in elf_bpf file. Section names
28 | * are interpreted by elf_bpf loader
29 | */
30 | #define SEC(NAME) __attribute__((section(NAME), used))
31 |
32 | #ifndef __always_inline
33 | #define __always_inline __attribute__((always_inline))
34 | #endif
35 | #ifndef __weak
36 | #define __weak __attribute__((weak))
37 | #endif
38 |
39 | /*
40 | * Helper macro to manipulate data structures
41 | */
42 | #ifndef offsetof
43 | #define offsetof(TYPE, MEMBER) ((size_t) & ((TYPE *)0)->MEMBER)
44 | #endif
45 | #ifndef container_of
46 | #define container_of(ptr, type, member) \
47 | ({ \
48 | void *__mptr = (void *)(ptr); \
49 | ((type *)(__mptr - offsetof(type, member))); \
50 | })
51 | #endif
52 |
53 | /*
54 | * Helper structure used by eBPF C program
55 | * to describe BPF map attributes to libbpf loader
56 | */
57 | struct bpf_map_def
58 | {
59 | unsigned int type;
60 | unsigned int key_size;
61 | unsigned int value_size;
62 | unsigned int max_entries;
63 | unsigned int map_flags;
64 | };
65 |
66 | enum libbpf_pin_type
67 | {
68 | LIBBPF_PIN_NONE,
69 | /* PIN_BY_NAME: pin maps by name (in /sys/fs/bpf by default) */
70 | LIBBPF_PIN_BY_NAME,
71 | };
72 |
73 | enum libbpf_tristate
74 | {
75 | TRI_NO = 0,
76 | TRI_YES = 1,
77 | TRI_MODULE = 2,
78 | };
79 |
80 | #define __kconfig __attribute__((section(".kconfig")))
81 |
82 | #endif
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vpctp/xdp/types.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #define u8 __u8
5 | #define u16 __u16
6 | #define s32 __s32
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vpctp/xdp/vpc_kern.c:
--------------------------------------------------------------------------------
1 | #define KBUILD_MODNAME "vpc_l2"
2 | #include "types.h"
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include "bpf_helpers.h"
13 | #include "vpc_kern.h"
14 |
15 | #define __bpf_constant_htons(x) ___constant_swab16(x)
16 |
17 | //Ensure map references are available.
18 | /*
19 | These will be initiated from go and
20 | referenced in the end BPF opcodes by file descriptor
21 | */
22 | struct {
23 | __uint(type, BPF_MAP_TYPE_XSKMAP);
24 | __type(key, int);
25 | __type(value, int);
26 | __uint(max_entries, MAX_SOCKS);
27 | } xsks_map SEC(".maps");
28 |
29 | struct {
30 | __uint(type, BPF_MAP_TYPE_ARRAY);
31 | __type(key, int);
32 | __type(value, int);
33 | __uint(max_entries, 2);
34 | } qidconf_map SEC(".maps");
35 |
36 | //Return the IPv4 next protocol
37 | static int parse_ipv4(void *data, __u64 nh_off, void *data_end)
38 | {
39 | struct iphdr *iph = data + nh_off;
40 |
41 | if (iph + 1 > data_end)
42 | return 0;
43 |
44 | return iph->protocol;
45 | }
46 |
47 | //Return the IPv6 next header (protocol)
48 | static int parse_ipv6(void *data, __u64 nh_off, void *data_end)
49 | {
50 | struct ipv6hdr *ip6h = data + nh_off;
51 |
52 | if (ip6h + 1 > data_end)
53 | return 0;
54 |
55 | return ip6h->nexthdr;
56 | }
57 |
58 | //Our main BPF code
59 | SEC("xdp_sock")
60 | int xdp_sock_prog(struct xdp_md *ctx) {
61 | int *qidconf, index = ctx->rx_queue_index;
62 |
63 | // A set entry here means that the correspnding queue_id
64 | // has an active AF_XDP socket bound to it.
65 | qidconf = bpf_map_lookup_elem(&qidconf_map, &index);
66 | if (!qidconf)
67 | return XDP_ABORTED;
68 |
69 | if (!*qidconf)
70 | return XDP_PASS;
71 |
72 | void *data_end = (void *)(long)ctx->data_end;
73 | void *data = (void *)(long)ctx->data;
74 | struct ethhdr *eth = data;
75 |
76 | __be16 h_proto;
77 | __u64 nh_off;
78 | __u32 ipproto;
79 |
80 | nh_off = sizeof(*eth);
81 | if (data + nh_off > data_end)
82 | return XDP_PASS;
83 |
84 | h_proto = eth->h_proto;
85 |
86 | //TODO(@tcfw) future support for VLAN tagging
87 | //Handle tagged packets
88 | // if (h_proto == __bpf_constant_htons(ETH_P_8021Q) ||
89 | // h_proto == __bpf_constant_htons(ETH_P_8021AD)) {
90 | // struct vlan_hdr *vhdr;
91 |
92 | // vhdr = data + nh_off;
93 | // nh_off += sizeof(struct vlan_hdr);
94 | // if (data + nh_off > data_end)
95 | // return XDP_PASS;
96 | // h_proto = vhdr->h_vlan_encapsulated_proto;
97 | // }
98 |
99 | //Get IP next protocol
100 | if (h_proto == __bpf_constant_htons(ETH_P_IP))
101 | ipproto = parse_ipv4(data, nh_off, data_end);
102 | else if (h_proto == __bpf_constant_htons(ETH_P_IPV6))
103 | ipproto = parse_ipv6(data, nh_off, data_end);
104 | else
105 | ipproto = 0;
106 |
107 | //Check if a VPC packet
108 | if (ipproto != VPC_PROTO)
109 | return XDP_PASS;
110 |
111 | return bpf_redirect_map(&xsks_map, index, 0);
112 | // }
113 | }
114 |
115 | //Basic license just for compiling the object code
116 | char __license[] SEC("license") = "LGPL-2.1 or BSD-2-Clause";
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vpctp/xdp/vpc_kern.h:
--------------------------------------------------------------------------------
1 | #define VPC_PROTO 172
2 | #define MAX_SOCKS 16
3 |
4 | struct vlan_hdr {
5 | __be16 h_vlan_TCI;
6 | __be16 h_vlan_encapsulated_proto;
7 | };
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vxlan/hash.go:
--------------------------------------------------------------------------------
1 | package vxlan
2 |
3 | import (
4 | "hash/crc32"
5 | "net"
6 |
7 | "github.com/tcfw/vpc/pkg/l2/transport/tap/protocol"
8 | )
9 |
10 | func hash(packet *protocol.Packet, rdst net.IP, mod int) int {
11 | n := len(packet.Frame)
12 | if n > 57 {
13 | n = 57
14 | }
15 |
16 | i := crc32.ChecksumIEEE(packet.Frame[:n])
17 |
18 | return int(i % uint32(mod))
19 | }
20 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vxlan/packet.go:
--------------------------------------------------------------------------------
1 | package vxlan
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "fmt"
7 | "io"
8 |
9 | "github.com/songgao/packets/ethernet"
10 | )
11 |
12 | //Packet represents the VxLAN headers and inner packet
13 | type Packet struct {
14 | Flags uint8
15 | GroupPolicy uint32 //wire: uint24
16 | VNID uint32 //wire: uint24
17 | resv uint8
18 | InnerFrame ethernet.Frame
19 |
20 | _buf bytes.Buffer
21 | }
22 |
23 | //NewPacket encaps a inner packet with VXlan headers
24 | func NewPacket(vnid uint32, innerFrame []byte) Packet {
25 | return Packet{
26 | Flags: 0x08,
27 | GroupPolicy: 0,
28 | VNID: vnid,
29 | resv: 0,
30 | InnerFrame: innerFrame,
31 | }
32 | }
33 |
34 | //FromBytes converts a byte array to a vxlan packet
35 | func FromBytes(bytesReader io.Reader) (*Packet, error) {
36 | p := &Packet{}
37 | //Flags
38 | flags := uint8(0)
39 | binary.Read(bytesReader, binary.BigEndian, &flags)
40 | p.Flags = flags
41 |
42 | if p.Flags != 0x8 {
43 | return nil, fmt.Errorf("invalid magic integer")
44 | }
45 |
46 | //Group Policy
47 | groupPolicy := make([]byte, 3)
48 | binary.Read(bytesReader, binary.BigEndian, &groupPolicy)
49 | groupPolicy = append(groupPolicy, []byte{0x0}...)
50 | p.GroupPolicy = binary.BigEndian.Uint32(groupPolicy)
51 |
52 | //VNID expanding 3 bytes to 4
53 | vnid := make([]byte, 3)
54 | binary.Read(bytesReader, binary.BigEndian, &vnid)
55 | vnid = append([]byte{0x0}, vnid...)
56 | p.VNID = binary.BigEndian.Uint32(vnid)
57 |
58 | //Resv
59 | resv := uint8(0)
60 | binary.Read(bytesReader, binary.BigEndian, &resv)
61 | p.resv = resv
62 |
63 | //Inner frame
64 | var innerFrame bytes.Buffer
65 | io.Copy(&innerFrame, bytesReader)
66 | p.InnerFrame = innerFrame.Bytes()
67 |
68 | return p, nil
69 | }
70 |
71 | //Bytes converts the packet to raw bytes
72 | func (p *Packet) Bytes() []byte {
73 | p._buf.Reset()
74 | p.WriteTo(&p._buf)
75 |
76 | return p._buf.Bytes()
77 | }
78 |
79 | //WriteTo writes a pakcet to a give writer
80 | func (p *Packet) WriteTo(w *bytes.Buffer) (int64, error) {
81 | w.WriteByte(p.Flags)
82 |
83 | w.WriteByte(byte(p.GroupPolicy >> 16))
84 | w.WriteByte(byte(p.GroupPolicy >> 8))
85 | w.WriteByte(byte(p.GroupPolicy))
86 |
87 | w.WriteByte(byte(p.VNID >> 16))
88 | w.WriteByte(byte(p.VNID >> 8))
89 | w.WriteByte(byte(p.VNID))
90 |
91 | w.WriteByte(0) //resv
92 |
93 | n, _ := w.Write(p.InnerFrame)
94 |
95 | return int64(n + 8), nil
96 | }
97 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vxlan/packet_test.go:
--------------------------------------------------------------------------------
1 | package vxlan
2 |
3 | import (
4 | "bytes"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestPacketFromBytes(t *testing.T) {
11 | rawBytes := []byte{
12 | 0x8, //flags
13 | 0x0, 0x0, 0x0, //group policy
14 | 0x0, 0x0, 0x5, //vnid
15 | 0x0, //resv 2
16 | 0x1, 0x2, 0x3, //inner packet
17 | }
18 |
19 | p, err := FromBytes(bytes.NewBuffer(rawBytes))
20 | assert.NoError(t, err, "failed to parse packet")
21 |
22 | assert.Equal(t, uint32(5), p.VNID, "VNID not same")
23 | assert.Equal(t, uint8(0), p.resv, "RES not same")
24 | assert.Equal(t, rawBytes[8:], []byte(p.InnerFrame))
25 | }
26 |
27 | func TestInvalidHeader(t *testing.T) {
28 | rawBytes := []byte{
29 | 0x0, //flags
30 | 0x0, 0x0, 0x0, //group policy
31 | 0x0, 0x0, 0x5, //vnid
32 | 0x0, //resv 2
33 | 0x1, 0x2, 0x3, //inner packet
34 | }
35 |
36 | _, err := FromBytes(bytes.NewBuffer(rawBytes))
37 | assert.Error(t, err)
38 | }
39 |
40 | func TestPacketToBytes(t *testing.T) {
41 | p := &Packet{
42 | Flags: 0x08,
43 | VNID: 5,
44 | InnerFrame: []byte{0x1, 0x2, 0x3},
45 | }
46 |
47 | rawBytes := p.Bytes()
48 |
49 | assert.Equal(t, []byte{
50 | 0x8, //flags
51 | 0x0, 0x0, 0x0, //group policy
52 | 0x0, 0x0, 0x5, //vnid
53 | 0x0, //resv 2
54 | 0x1, 0x2, 0x3, //inner packet
55 | }, rawBytes)
56 | }
57 |
58 | func TestPacketNewPacketToBytes(t *testing.T) {
59 | p := NewPacket(5, []byte{
60 | 0x1, 0x2, 0x3,
61 | })
62 | raw := p.Bytes()
63 | assert.Equal(t, []byte{
64 | 0x8, //flags
65 | 0x0, 0x0, 0x0, //group policy
66 | 0x0, 0x0, 0x5, //vnid
67 | 0x0, //resv 2
68 | 0x1, 0x2, 0x3, //inner packet
69 | }, raw)
70 | }
71 |
72 | func BenchmarkToByte(b *testing.B) {
73 | innerFrame := []byte{
74 | 0xff, 0x2, 0x3, 0x1, 0x2, 0x3, 0x1, 0x2, 0x3, 0x1, 0x2, 0x3, 0x1, 0x2, 0x3, 0xf4,
75 | 0xff, 0x2, 0x3, 0x1, 0x2, 0x3, 0x1, 0x2, 0x3, 0x1, 0x2, 0x3, 0x1, 0x2, 0x3, 0xf4,
76 | }
77 | vni := uint32(5)
78 |
79 | b.Run("convert", func(b *testing.B) {
80 | c := int64(0)
81 | for i := 0; i < b.N; i++ {
82 | p := NewPacket(vni, innerFrame)
83 | bytes := p.Bytes()
84 | c = c + int64(len(bytes))
85 | }
86 | b.SetBytes(c)
87 | })
88 | }
89 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vxlan/vxlan.go:
--------------------------------------------------------------------------------
1 | package vxlan
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "fmt"
7 | "net"
8 | "runtime"
9 | "syscall"
10 |
11 | "golang.org/x/sys/unix"
12 |
13 | "github.com/tcfw/vpc/pkg/l2/transport/tap/protocol"
14 | )
15 |
16 | //Handler VxLAN endpoint protocol
17 | type Handler struct {
18 | conn []net.PacketConn
19 | port int
20 | recv protocol.HandlerFunc
21 |
22 | connCount int
23 | }
24 |
25 | //NewHandler creates a new VxLAN handler
26 | func NewHandler() *Handler {
27 | return &Handler{
28 | connCount: runtime.NumCPU(),
29 | conn: []net.PacketConn{},
30 | recv: func(_ []protocol.Packet) {},
31 | port: 4789,
32 | }
33 | }
34 |
35 | //Start opens a UDP endpoint for VxLAN packets
36 | func (p *Handler) Start() error {
37 | for i := 0; i < p.connCount; i++ {
38 | lisConfig := net.ListenConfig{
39 | Control: func(network string, address string, c syscall.RawConn) error {
40 | var err error
41 | c.Control(func(fd uintptr) {
42 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
43 | if err != nil {
44 | return
45 | }
46 |
47 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
48 | if err != nil {
49 | return
50 | }
51 |
52 | err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_PRIORITY, 0)
53 | if err != nil {
54 | return
55 | }
56 | })
57 | return err
58 | },
59 | }
60 |
61 | pc, err := lisConfig.ListenPacket(context.Background(), "udp", fmt.Sprintf(":%d", p.port))
62 | if err != nil {
63 | return fmt.Errorf("failed to add packet listener; %s", err)
64 | }
65 |
66 | p.conn = append(p.conn, pc)
67 | }
68 |
69 | p.handleIn()
70 |
71 | return nil
72 | }
73 |
74 | //Stop ends the UDP endpoint
75 | func (p *Handler) Stop() error {
76 | for _, conn := range p.conn {
77 | if err := conn.Close(); err != nil {
78 | return err
79 | }
80 | }
81 | return nil
82 | }
83 |
84 | //Send sends a group of packets to a dest endpoint
85 | func (p *Handler) Send(packets []protocol.Packet, rdst net.IP) (int, error) {
86 | for _, packet := range packets {
87 | _, err := p.SendOne(&packet, rdst)
88 | if err != nil {
89 | return 0, err
90 | }
91 | }
92 | return 0, nil
93 | }
94 |
95 | //SendOne sends a single packet to a dest endpoint
96 | func (p *Handler) SendOne(packet *protocol.Packet, rdst net.IP) (int, error) {
97 | vxlanFrame := NewPacket(packet.VNID, packet.Frame)
98 | addr := &net.UDPAddr{IP: rdst, Port: p.port}
99 | i := hash(packet, rdst, p.connCount)
100 | return p.conn[i].WriteTo(vxlanFrame.Bytes(), addr)
101 | }
102 |
103 | //SetHandler sets the receiving callback
104 | func (p *Handler) SetHandler(handle protocol.HandlerFunc) {
105 | p.recv = handle
106 | }
107 |
108 | //handleIn handles picking up pakcets from the underlying udp connection per thread
109 | func (p *Handler) handleIn() {
110 | for _, conn := range p.conn {
111 | go func(c net.PacketConn) {
112 | buff := make([]byte, 81920)
113 | for {
114 | n, _, err := c.ReadFrom(buff)
115 | if err != nil {
116 | return
117 | }
118 |
119 | br := bytes.NewBuffer(buff[:n])
120 | packet, _ := FromBytes(br)
121 |
122 | p.recv([]protocol.Packet{protocol.NewPacket(packet.VNID, packet.InnerFrame[:])})
123 | }
124 | }(conn)
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/protocol/vxlan/vxlan_test.go:
--------------------------------------------------------------------------------
1 | package vxlan
2 |
3 | import (
4 | "net"
5 | "runtime"
6 | "testing"
7 |
8 | "github.com/stretchr/testify/assert"
9 | "github.com/tcfw/vpc/pkg/l2/transport/tap/protocol"
10 | )
11 |
12 | func TestSendRecv(t *testing.T) {
13 | handler := NewHandler()
14 |
15 | if !assert.NoError(t, handler.Start()) {
16 | return
17 | }
18 |
19 | var recvPacket *protocol.Packet
20 |
21 | cliHandler := &Handler{
22 | connCount: runtime.NumCPU() * 2,
23 | port: 4790,
24 | }
25 | if !assert.NoError(t, cliHandler.Start()) {
26 | return
27 | }
28 |
29 | handler.SetHandler(func(ps []protocol.Packet) {
30 | if len(ps) == 0 {
31 | return
32 | }
33 |
34 | recvPacket = &ps[0]
35 | })
36 |
37 | cliHandler.SetHandler(func(ps []protocol.Packet) {
38 | if len(ps) == 0 {
39 | return
40 | }
41 |
42 | recvPacket = &ps[0]
43 | })
44 |
45 | frame := []byte{0x10, 0x20}
46 | var vnid uint32 = 5
47 | sendPacket := protocol.Packet{VNID: vnid, Frame: frame}
48 | rdst := net.ParseIP("::1")
49 |
50 | _, err := cliHandler.Send([]protocol.Packet{sendPacket}, rdst)
51 | if !assert.NoError(t, err) {
52 | return
53 | }
54 |
55 | //Wait for a packet to arrive
56 | for {
57 | if recvPacket != nil {
58 | break
59 | }
60 | }
61 |
62 | assert.Equal(t, frame, []byte(recvPacket.Frame))
63 | assert.Equal(t, vnid, recvPacket.VNID)
64 |
65 | }
66 |
67 | func BenchmarkSend(b *testing.B) {
68 | handler := NewHandler()
69 | handler.Start()
70 |
71 | received := 0
72 |
73 | handler.SetHandler(func(ps []protocol.Packet) {
74 | received += len(ps)
75 | })
76 |
77 | frame := []byte{
78 | 0xCC, 0x03, 0x04, 0xDC, 0x00, 0x10, 0xFF, 0xAA, 0xFA, 0xAA, 0xFF, 0xAA, 0x81, 0x00, 0x00,
79 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
80 | 0x7F, 0x00, 0x00, 0x01, 0x08, 0x08, 0x08, 0x08, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00,
81 | 0x01, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x61, 0x62, 0x63, 0x64,
82 | }
83 |
84 | var vnid uint32 = 5
85 | sendPacket := []protocol.Packet{{VNID: vnid, Frame: frame}}
86 | rdst := net.ParseIP("::1")
87 |
88 | b.Run("send", func(b *testing.B) {
89 | b.SetBytes(int64(len(frame) + 10)) //See header format
90 | for i := 0; i < b.N; i++ {
91 | handler.Send(sendPacket, rdst)
92 | }
93 | })
94 | }
95 |
--------------------------------------------------------------------------------
/pkg/l2/transport/tap/vlan.go:
--------------------------------------------------------------------------------
1 | package tap
2 |
3 | import (
4 | "fmt"
5 |
6 | "github.com/vishvananda/netlink"
7 | )
8 |
9 | //AddVLAN adds a VLAN to the vtep on the bridge
10 | func (s *Listener) AddVLAN(vnid uint32, vlan uint16) error {
11 | _, ok := s.vlans[vlan]
12 | if !ok {
13 | s.vlans[vlan] = 1
14 | } else {
15 | s.vlans[vlan]++
16 | }
17 |
18 | // br, err := netlink.LinkByName(fmt.Sprintf("b-%d", vnid))
19 | // if err != nil {
20 | // return err
21 | // }
22 |
23 | // return netlink.BridgeVlanAdd(s.taps[vnid].IFace(), vlan, false, false, false, false)
24 | // return netlink.BridgeVlanAdd(br, vlan, false, false, false, false)
25 | return nil
26 | }
27 |
28 | //DelVLAN removes the VLAN to the vtep on the bridge
29 | func (s *Listener) DelVLAN(vnid uint32, vlan uint16) error {
30 | _, ok := s.vlans[vlan]
31 | if !ok {
32 | return fmt.Errorf("vlan already removed")
33 | }
34 |
35 | s.vlans[vlan]--
36 |
37 | br, err := netlink.LinkByName(fmt.Sprintf("b-%d", vnid))
38 | if err != nil {
39 | return err
40 | }
41 |
42 | if s.vlans[vlan] <= 0 {
43 | // return netlink.BridgeVlanDel(s.taps[vnid].IFace(), vlan, false, false, false, false)
44 | return netlink.BridgeVlanDel(br, vlan, false, false, false, false)
45 | }
46 |
47 | return nil
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/l2/transport/transport.go:
--------------------------------------------------------------------------------
1 | package transport
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/tcfw/vpc/pkg/l2/controller"
7 | "github.com/vishvananda/netlink"
8 | )
9 |
10 | //Transport are drivers for handling lower level packet actions
11 | type Transport interface {
12 | //Start begins the listener
13 | Start() error
14 |
15 | //SetMTU sets the default MTU of added interfaces
16 | SetMTU(mtu int32) error
17 |
18 | //SetSDN allows the transport to use SDN functions like IP/MAC lookups
19 | SetSDN(sdn controller.Controller) error
20 |
21 | //AddEP adds a new VNID endpoint device to a given bridge
22 | AddEP(vnid uint32, br *netlink.Bridge) error
23 |
24 | //DelEP removes a VNID endpoint device
25 | DelEP(vnid uint32) error
26 |
27 | //Status gives the device status of the VNID endpoint device
28 | Status(vnid uint32) EPStatus
29 |
30 | //AddVLAN adds a vlan to the trunking device
31 | AddVLAN(vnid uint32, vlan uint16) error
32 |
33 | //DelVLAN removes a vlan from the trunking device
34 | DelVLAN(vnid uint32, vlan uint16) error
35 |
36 | //AddForwardEntry adds a forwarding entry for the trunking devices to relay packets to other endpoints
37 | AddForwardEntry(vnid uint32, mac net.HardwareAddr, ip net.IP) error
38 |
39 | //ForwardingMiss provides a readonly channel to react to missing forwarding entries
40 | ForwardingMiss(vnid uint32) (<-chan ForwardingMiss, error)
41 | }
42 |
43 | //ForwardingMiss contains info for missed forwarding info
44 | type ForwardingMiss struct {
45 | Type ForwardingMissType
46 | HwAddr net.HardwareAddr
47 | IP net.IP
48 | }
49 |
50 | //ForwardingMissType type of forwarding miss
51 | type ForwardingMissType int
52 |
53 | const (
54 | //MissTypeEP L2 Miss or FDB Miss
55 | MissTypeEP ForwardingMissType = iota
56 |
57 | //MissTypeARP L3 Miss or ARP proxy
58 | MissTypeARP
59 | )
60 |
61 | func (m ForwardingMissType) String() string {
62 | return [...]string{"End Point", "ARP"}[m]
63 | }
64 |
65 | //EPStatus provides finite state of an endpoint
66 | type EPStatus int
67 |
68 | const (
69 | //EPStatusUP is available and live
70 | EPStatusUP EPStatus = iota
71 |
72 | //EPStatusDOWN is available but administratively down
73 | EPStatusDOWN
74 |
75 | //EPStatusMISSING is missing
76 | EPStatusMISSING
77 | )
78 |
--------------------------------------------------------------------------------
/pkg/l3/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | // NewDefaultCommand creates the `l3` command and its nested children.
8 | func NewDefaultCommand() *cobra.Command {
9 | // Parent command to which all subcommands are added.
10 | cmds := &cobra.Command{
11 | Use: "l3",
12 | Short: "l3",
13 | Long: `l3 providers routing for vxlans`,
14 | Run: func(cmd *cobra.Command, args []string) {
15 | cmd.Help()
16 | },
17 | }
18 |
19 | cmds.AddCommand(NewCreateCmd())
20 |
21 | return cmds
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/l3/cmd/create.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "log"
7 | "os"
8 | "os/signal"
9 | "strings"
10 |
11 | "github.com/spf13/viper"
12 |
13 | "github.com/spf13/cobra"
14 | l2API "github.com/tcfw/vpc/pkg/api/v1/l2"
15 | l3 "github.com/tcfw/vpc/pkg/l3"
16 | "google.golang.org/grpc"
17 | )
18 |
19 | //NewCreateCmd provides a command to create vpcs
20 | func NewCreateCmd() *cobra.Command {
21 | cmd := &cobra.Command{
22 | Use: "create",
23 | Short: "Creates a l3 router on a VPC",
24 | Run: func(cmd *cobra.Command, args []string) {
25 | vpcs, _ := cmd.Flags().GetIntSlice("vpc")
26 | port, _ := cmd.Flags().GetUint("port")
27 |
28 | for _, vpcID := range vpcs {
29 | if vpcID > 16777215 {
30 | fmt.Printf("VPC %d id out of range\n", vpcID)
31 | continue
32 | }
33 |
34 | l2conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithInsecure())
35 | if err != nil {
36 | log.Println(err)
37 | return
38 | }
39 | defer l2conn.Close()
40 |
41 | l2cli := l2API.NewL2ServiceClient(l2conn)
42 |
43 | ctx := context.Background()
44 |
45 | var resp *l2API.StackResponse
46 |
47 | resp, err = l2cli.GetStack(ctx, &l2API.StackRequest{VpcId: int32(vpcID)})
48 | if err != nil {
49 | if strings.Contains(err.Error(), "Stack not created") {
50 | resp, err = l2cli.AddStack(ctx, &l2API.StackRequest{VpcId: int32(vpcID)})
51 | if err != nil {
52 | log.Println("Failed to create stack: ", err)
53 | return
54 | }
55 | } else {
56 | log.Println("Failed to fetch stack: ", err)
57 | return
58 | }
59 | }
60 |
61 | stack, err := l3.L2APIToStack(resp.Stack)
62 | if err != nil {
63 | log.Println("Failed to convert stack: ", err)
64 | return
65 | }
66 |
67 | id := l3.NewID()
68 |
69 | r, err := l3.CreateRouter(l2cli, stack, id)
70 | if err != nil {
71 | log.Println("Failed to create router: ", err)
72 | return
73 | }
74 |
75 | defer func() {
76 | r.Delete()
77 | }()
78 |
79 | fmt.Printf("Created Router %s on %d\n", r.ID, vpcID)
80 | }
81 |
82 | waitForExit()
83 | },
84 | }
85 |
86 | cmd.Flags().IntSlice("vpc", []int{}, "VPC to add local router too")
87 | cmd.Flags().UintP("port", "p", 18254, "GRPC port")
88 | cmd.Flags().StringP("bridge", "b", "virbr0", "bridge for public network")
89 | cmd.Flags().Bool("dhcp", false, "Enable DHCP")
90 | cmd.Flags().Bool("nat", true, "Enable NAT")
91 | cmd.Flags().StringSlice("dhcpdns", []string{"1.1.1.1", "1.0.0.1"}, "DHCP DNS servers")
92 | cmd.Flags().StringSlice("bgp", []string{"192.168.222.1"}, "BGP peers")
93 | cmd.Flags().StringSlice("ip", []string{"10.4.0.1/24:5", "10.4.1.1/24:6"}, `Subnets/IPs (format "cidr:vlan" e.g. "10.4.0.1/24:5)`)
94 |
95 | viper.BindPFlag("br", cmd.Flags().Lookup("bridge"))
96 | viper.BindPFlag("bgp", cmd.Flags().Lookup("bgp"))
97 | viper.BindPFlag("dhcp", cmd.Flags().Lookup("dhcp"))
98 | viper.BindPFlag("ips", cmd.Flags().Lookup("ip"))
99 |
100 | cmd.MarkFlagRequired("vpc")
101 |
102 | return cmd
103 | }
104 |
105 | func waitForExit() {
106 | var signalChannel chan os.Signal
107 | signalChannel = make(chan os.Signal, 1)
108 | signal.Notify(signalChannel, os.Interrupt)
109 | <-signalChannel
110 | }
111 |
--------------------------------------------------------------------------------
/pkg/l3/config.go:
--------------------------------------------------------------------------------
1 | package l3
2 |
3 | import (
4 | "net"
5 |
6 | "github.com/spf13/viper"
7 | )
8 |
9 | func configPublicBridge() string {
10 | return viper.GetString("br")
11 | }
12 |
13 | func configDHCP() bool {
14 | return viper.GetBool("dhcp")
15 | }
16 |
17 | func configNAT() bool {
18 | return viper.GetBool("nat")
19 | }
20 |
21 | func configDHCPDNS() []net.IP {
22 | ips := []net.IP{}
23 |
24 | sDNS := viper.GetStringSlice("dns")
25 |
26 | for _, ip := range sDNS {
27 | ips = append(ips, net.ParseIP(ip))
28 | }
29 | return ips
30 | }
31 |
32 | func configBGPPeers() []string {
33 | return viper.GetStringSlice("bgp")
34 | }
35 |
36 | func configSubnets() []string {
37 | return viper.GetStringSlice("ips")
38 | }
39 |
--------------------------------------------------------------------------------
/pkg/l3/dhcp.go:
--------------------------------------------------------------------------------
1 | package l3
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net"
7 | "sync"
8 | "time"
9 |
10 | "github.com/apparentlymart/go-cidr/cidr"
11 | "github.com/insomniacslk/dhcp/dhcpv4"
12 | "github.com/insomniacslk/dhcp/dhcpv4/server4"
13 | )
14 |
15 | //RouterDHCP provides DHCP capabilities to routers
16 | type RouterDHCP struct {
17 | mu sync.Mutex
18 |
19 | leaseTime uint32
20 | ip net.IP
21 | router net.IP
22 | subnet *net.IPNet
23 | dnsServers []net.IP
24 | iface string
25 | }
26 |
27 | //NewDHCPv4Server consturcts a new DHCPv4 server
28 | func NewDHCPv4Server(iface string, subnet *net.IPNet, dns []net.IP) (*RouterDHCP, error) {
29 | srv := &RouterDHCP{
30 | subnet: subnet,
31 | dnsServers: dns,
32 | leaseTime: uint32(2 * 7 * 24 * time.Hour / time.Second),
33 | iface: iface,
34 | }
35 |
36 | DHCPIP, err := cidr.Host(subnet, 2)
37 | if err != nil {
38 | return nil, err
39 | }
40 | srv.ip = DHCPIP
41 | RouterIP, err := cidr.Host(subnet, 1)
42 | if err != nil {
43 | return nil, err
44 | }
45 | srv.router = RouterIP
46 |
47 | return srv, nil
48 | }
49 |
50 | //DHCPV4OnSubnet starts a DHCPv4 Server on a particular subnet
51 | func (rd *RouterDHCP) DHCPV4OnSubnet() error {
52 | fmt.Println("Starting DHCPv4")
53 |
54 | laddr := net.UDPAddr{
55 | Port: 67,
56 | }
57 |
58 | server, err := server4.NewServer(rd.iface, &laddr, rd.HandleV4)
59 | if err != nil {
60 | return err
61 | }
62 |
63 | return server.Serve()
64 | }
65 |
66 | //HandleV4 handles DHCP v4
67 | func (rd *RouterDHCP) HandleV4(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
68 | fmt.Println(m.Summary())
69 | switch m.MessageType() {
70 | case dhcpv4.MessageTypeDiscover:
71 | rd.handleDiscoveryRequest(conn, peer, m, dhcpv4.MessageTypeOffer)
72 | break
73 | case dhcpv4.MessageTypeRequest:
74 | rd.handleDiscoveryRequest(conn, peer, m, dhcpv4.MessageTypeAck)
75 | break
76 | }
77 | }
78 |
79 | //handleDiscoveryRequest processes discovery and request packets
80 | func (rd *RouterDHCP) handleDiscoveryRequest(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4, replyType dhcpv4.MessageType) {
81 | vmIP, err := rd.macToIP(m.ClientHWAddr)
82 | if err != nil {
83 | rd.sendDecline(conn, peer, m)
84 | return
85 | }
86 |
87 | if m.MessageType() == dhcpv4.MessageTypeRequest && !m.RequestedIPAddress().Equal(vmIP) {
88 | rd.sendDecline(conn, peer, m)
89 | log.Println("DHCPv4 sent decline due to req ip mismatch")
90 | return
91 | }
92 |
93 | mods := []dhcpv4.Modifier{
94 | dhcpv4.WithLeaseTime(rd.leaseTime),
95 | dhcpv4.WithServerIP(rd.ip),
96 | dhcpv4.WithYourIP(vmIP),
97 | dhcpv4.WithMessageType(replyType),
98 | dhcpv4.WithRouter(rd.router),
99 | dhcpv4.WithNetmask(rd.subnet.Mask),
100 | }
101 |
102 | mods = append(mods, rd.dhcpOptions()...)
103 |
104 | resp, err := dhcpv4.NewReplyFromRequest(m, mods...)
105 | if err != nil {
106 | fmt.Println("DHCPv4 Failed to construct DHCP offer: ", err)
107 | return
108 | }
109 |
110 | _, err = conn.WriteTo(resp.ToBytes(), peer)
111 | if err != nil {
112 | fmt.Println("DHCPv4 Failed to write DHCP offer: ", err)
113 | }
114 | }
115 |
116 | //sendDecline sends an DHCP decline based on a request
117 | func (rd *RouterDHCP) sendDecline(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
118 | mods := []dhcpv4.Modifier{
119 | dhcpv4.WithMessageType(dhcpv4.MessageTypeDecline),
120 | }
121 |
122 | resp, err := dhcpv4.NewReplyFromRequest(m, mods...)
123 | if err != nil {
124 | log.Println("DHCPv4 Failed to construct decline: ", err)
125 | return
126 | }
127 | conn.WriteTo(resp.ToBytes(), peer)
128 | }
129 |
130 | //dhcpOptions encaps default DHCP options
131 | func (rd *RouterDHCP) dhcpOptions() []dhcpv4.Modifier {
132 | return []dhcpv4.Modifier{
133 | dhcpv4.WithDNS(rd.dnsServers...),
134 | }
135 | }
136 |
137 | //macToIP converts the originating mac address to the correct IP
138 | func (rd *RouterDHCP) macToIP(hwaddr net.HardwareAddr) (net.IP, error) {
139 | //TODO(tcfw) link to hyper/vpc service
140 | IP, err := cidr.Host(rd.subnet, 4)
141 | if err != nil {
142 | return nil, err
143 | }
144 | return IP, nil
145 | }
146 |
--------------------------------------------------------------------------------
/pkg/l3/l2.go:
--------------------------------------------------------------------------------
1 | package l3
2 |
3 | import (
4 | "fmt"
5 |
6 | l2API "github.com/tcfw/vpc/pkg/api/v1/l2"
7 | l2 "github.com/tcfw/vpc/pkg/l2"
8 | "github.com/vishvananda/netlink"
9 | )
10 |
11 | //L2APIToStack converts the l2-agent API stack into the more useful stack
12 | func L2APIToStack(l2Stack *l2API.Stack) (*l2.Stack, error) {
13 | stack := &l2.Stack{
14 | VPCID: l2Stack.VpcId,
15 | }
16 |
17 | br, err := netlink.LinkByIndex(int(l2Stack.BridgeLinkIndex))
18 | if err != nil {
19 | return nil, fmt.Errorf("Failed to find bridge: %s", err)
20 | }
21 |
22 | stack.Bridge = br.(*netlink.Bridge)
23 |
24 | return stack, nil
25 | }
26 |
--------------------------------------------------------------------------------
/pkg/l3/netfilter.go:
--------------------------------------------------------------------------------
1 | package l3
2 |
3 | import (
4 | "github.com/juliengk/go-netfilter/iptables"
5 | )
6 |
7 | const (
8 | iptTFILTER = "filter"
9 |
10 | iptCINPUT = "INPUT"
11 | iptCFORWARD = "FORWARD"
12 | iptCOUTPUT = "OUTPUT"
13 | )
14 |
15 | //SetDefaultFWRules sets the DROP policy on input and output, allows forwarding
16 | func (r *Router) SetDefaultFWRules() error {
17 | ipt4, _ := iptables.New(4, false)
18 | ipt6, _ := iptables.New(6, false)
19 |
20 | ipts := []*iptables.IPTables{ipt4, ipt6}
21 |
22 | //apply same rules for both ipv4 and ipv6
23 | for _, ipt := range ipts {
24 | // ipt.Policy(iptTFILTER, iptCINPUT, "DROP")
25 | ipt.Policy(iptTFILTER, iptCFORWARD, "ACCEPT")
26 | // ipt.Policy(iptTFILTER, iptCOUTPUT, "DROP")
27 |
28 | //Allow any loopback
29 | ipt.Append(iptTFILTER, iptCINPUT, "-i", "lo", "-j", "ACCEPT")
30 | ipt.Append(iptTFILTER, iptCOUTPUT, "-o", "lo", "-j", "ACCEPT")
31 |
32 | //Allow BGP outbound
33 | ipt.Append(iptTFILTER, iptCOUTPUT, "-p", "tcp", "--dport", "179", "-m", "conntrack", "--ctstate", "NEW,ESTABLISHED", "-j", "ACCEPT")
34 | ipt.Append(iptTFILTER, iptCINPUT, "-p", "tcp", "--sport", "179", "-m", "conntrack", "--ctstate", "ESTABLISHED", "-j", "ACCEPT")
35 |
36 | //Allow DHCP packets in/out
37 | ipt.Append(iptTFILTER, iptCINPUT, "-p", "udp", "--dport", "67:68", "--sport", "67:68", "-j", "ACCEPT")
38 | ipt.Append(iptTFILTER, iptCOUTPUT, "-p", "udp", "--dport", "67:68", "--sport", "67:68", "-j", "ACCEPT")
39 | //Block DHCP from public iface
40 | ipt.Append(iptTFILTER, iptCINPUT, "-i", "eth0", "-p", "udp", "--dport", "67:68", "--sport", "67:68", "-j", "DROP")
41 | }
42 | return nil
43 | }
44 |
--------------------------------------------------------------------------------
/pkg/l3/ns/netns.go:
--------------------------------------------------------------------------------
1 | package ns
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "os"
7 | "path"
8 | "runtime"
9 | "syscall"
10 |
11 | virshnetns "github.com/vishvananda/netns"
12 | )
13 |
14 | const (
15 | netnsDir = "/var/run/netns/"
16 | )
17 |
18 | //NetNS allows communicating with a network namespace
19 | type NetNS struct {
20 | handle virshnetns.NsHandle
21 | name string
22 | }
23 |
24 | //Fd provides the file decriptor of the namespace
25 | func (n *NetNS) Fd() int {
26 | return int(n.handle)
27 | }
28 |
29 | //CreateNetNS creates a new network namespace and sets a name
30 | func CreateNetNS(name string) (*NetNS, error) {
31 | runtime.LockOSThread()
32 | defer runtime.UnlockOSThread()
33 |
34 | origns, _ := virshnetns.Get()
35 | defer origns.Close()
36 |
37 | routerNetNs, err := virshnetns.New()
38 | if err != nil {
39 | return nil, err
40 | }
41 |
42 | ns := &NetNS{
43 | handle: routerNetNs,
44 | }
45 |
46 | if len(name) != 0 {
47 | if err := ns.SetNSName(name); err != nil {
48 | log.Printf("Failed to add a name to NS: %s\n", err)
49 | return ns, fmt.Errorf("failed to add name to ns: %s", err)
50 | }
51 | }
52 |
53 | virshnetns.Set(origns)
54 |
55 | return ns, nil
56 | }
57 |
58 | //SetNSName sets a name for the netns which can be accessible via 'ip netns' commands
59 | func (n *NetNS) SetNSName(name string) error {
60 | //Attempt to make the alias dir if does not exists
61 | if _, err := os.Stat(netnsDir); os.IsNotExist(err) {
62 | os.MkdirAll(netnsDir, 0777)
63 | }
64 |
65 | p := path.Join(netnsDir, name)
66 |
67 | f, err := os.OpenFile(p, os.O_CREATE|os.O_EXCL, 0444)
68 | if err != nil {
69 | return fmt.Errorf("failed to create netns name: %s", err)
70 | }
71 |
72 | f.Close()
73 |
74 | nspath := fmt.Sprintf("/proc/%d/task/%d/ns/net", os.Getpid(), syscall.Gettid())
75 |
76 | if err := syscall.Mount(nspath, p, "bind", syscall.MS_BIND, ""); err != nil {
77 | return err
78 | }
79 |
80 | n.name = name
81 |
82 | return nil
83 | }
84 |
85 | //Unbind removes the mount created from setNSName
86 | func (n *NetNS) Unbind() error {
87 | if len(n.name) == 0 {
88 | return fmt.Errorf("ns has no name to unbind")
89 | }
90 |
91 | p := path.Join(netnsDir, n.name)
92 | if err := syscall.Unmount(p, 0); err != nil {
93 | return err
94 | }
95 |
96 | return os.Remove(p)
97 | }
98 |
99 | //Exec switches to the specified netns and executes the closure
100 | //WARNING: CANNOT EXECUTE WITH GOROUTINES _INSIDE_ THE CLOSURE
101 | func (n *NetNS) Exec(fn func() error) error {
102 | runtime.LockOSThread()
103 | defer runtime.UnlockOSThread()
104 |
105 | origns, _ := virshnetns.Get()
106 | defer origns.Close()
107 |
108 | //Switch to our namespace
109 | virshnetns.Set(n.handle)
110 |
111 | err := fn()
112 |
113 | //Switch back to original namespace
114 | virshnetns.Set(origns)
115 |
116 | return err
117 | }
118 |
119 | //Close unbinds and closes the netns
120 | func (n *NetNS) Close() error {
121 | n.Unbind()
122 | return n.handle.Close()
123 | }
124 |
--------------------------------------------------------------------------------
/pkg/sbs/blockCache.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "sync"
5 | )
6 |
7 | const (
8 | maxEntries = 12800
9 | )
10 |
11 | type blockCache struct {
12 | blocks []*blockCacheEntry
13 | mu sync.RWMutex
14 | bufAlloc *bufAlloc
15 | }
16 |
17 | type blockCacheEntry struct {
18 | offset uint64
19 | data []byte
20 | }
21 |
22 | func newBlockCache() *blockCache {
23 | bc := &blockCache{
24 | blocks: make([]*blockCacheEntry, maxEntries),
25 | bufAlloc: newBufAlloc(BlockSize * 2),
26 | }
27 | return bc
28 | }
29 |
30 | func (bc *blockCache) get(offset uint64) *blockCacheEntry {
31 | bc.mu.RLock()
32 | defer bc.mu.RUnlock()
33 |
34 | k := offset % uint64(len(bc.blocks))
35 | entry := bc.blocks[k]
36 | if entry == nil || entry.offset != offset {
37 | return nil
38 | }
39 |
40 | return entry
41 | }
42 |
43 | func (bc *blockCache) set(offset uint64, data []byte) {
44 | bc.mu.Lock()
45 | defer bc.mu.Unlock()
46 |
47 | buf := make([]byte, len(data))
48 | copy(buf, data)
49 |
50 | k := offset % uint64(len(bc.blocks))
51 | bc.blocks[k] = &blockCacheEntry{
52 | offset: offset,
53 | data: buf,
54 | }
55 | }
56 |
57 | func (bc *blockCache) invalidate(offset uint64) {
58 | bc.mu.Lock()
59 | defer bc.mu.Unlock()
60 |
61 | k := offset % uint64(len(bc.blocks))
62 | bc.blocks[k] = nil
63 | }
64 |
--------------------------------------------------------------------------------
/pkg/sbs/blockCache_test.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestBlockCacheGetSetGet(t *testing.T) {
10 | data := []byte{1, 2, 3, 4}
11 | bc := newBlockCache()
12 |
13 | d := bc.get(1234567890)
14 | assert.Nil(t, d)
15 |
16 | bc.set(1234567890, data)
17 |
18 | d = bc.get(1234567890)
19 | assert.Equal(t, data, d.data)
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/sbs/cmd/agent.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/spf13/cobra"
7 | "github.com/tcfw/vpc/pkg/sbs/config"
8 | )
9 |
10 | //newAgentCmd provides a command to delete vpcs
11 | func newAgentCmd() *cobra.Command {
12 | cmd := &cobra.Command{
13 | Use: "agent",
14 | Short: "Starts listening for NBD connections",
15 | Run: func(cmd *cobra.Command, args []string) {
16 | if err := config.Read(); err != nil {
17 | log.Fatalf("Failed to read config files: %s", err)
18 | }
19 | },
20 | }
21 |
22 | return cmd
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/sbs/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "github.com/spf13/viper"
6 | )
7 |
8 | // NewDefaultCommand creates the `hyper` command and its nested children.
9 | func NewDefaultCommand() *cobra.Command {
10 | // Parent command to which all subcommands are added.
11 | cmds := &cobra.Command{
12 | Use: "sbs",
13 | Short: "sbs",
14 | Long: `sbs providers simple block storage resources`,
15 | Run: func(cmd *cobra.Command, args []string) {
16 | cmd.Help()
17 | },
18 | }
19 |
20 | cmds.AddCommand(newStartCmd())
21 | cmds.AddCommand(newAgentCmd())
22 |
23 | cmds.PersistentFlags().Int16P("log-level", "v", 3, "log level (5=trace, 4=debug, 3=warning, 2=error, 1=fatal, 0=panic")
24 | viper.BindPFlag("LogLevel", cmds.PersistentFlags().Lookup("log-level"))
25 |
26 | return cmds
27 | }
28 |
--------------------------------------------------------------------------------
/pkg/sbs/cmd/start.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "github.com/spf13/cobra"
8 | "github.com/tcfw/vpc/pkg/sbs"
9 | "github.com/tcfw/vpc/pkg/sbs/config"
10 | "github.com/tcfw/vpc/pkg/utils"
11 |
12 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
13 | )
14 |
15 | //newStartCmd provides a command to delete vpcs
16 | func newStartCmd() *cobra.Command {
17 | cmd := &cobra.Command{
18 | Use: "start",
19 | Short: "Starts listening for cluster connections",
20 | Run: func(cmd *cobra.Command, args []string) {
21 | if err := config.Read(); err != nil {
22 | log.Fatalf("Failed to read config files: %s", err)
23 | }
24 |
25 | port, _ := cmd.Flags().GetInt("port")
26 | id, _ := cmd.Flags().GetString("id")
27 | bootPeers, _ := cmd.Flags().GetStringSlice("boot-peer")
28 |
29 | s := sbs.NewServerWithPort(port)
30 | if id != "" {
31 | s.SetPeerID(id)
32 | }
33 | defer s.Shutdown()
34 |
35 | if err := s.Listen(); err != nil {
36 | log.Fatalf("failed to start listening to remote connections: %s", err)
37 | return
38 | }
39 |
40 | s.BootPeers(bootPeers)
41 |
42 | time.Sleep(2 * time.Second)
43 |
44 | if err := s.AddVolume(&volumesAPI.Volume{
45 | Id: "vol-test",
46 | Account: 1,
47 | Size: 10,
48 | }, s.PeerIDs()); err != nil {
49 | log.Fatalf("failed to start volume: %s", err)
50 | return
51 | }
52 |
53 | s.Export("vol-test")
54 |
55 | //Wait and gracefully shutdown
56 | utils.BlockUntilSigTerm()
57 | },
58 | }
59 |
60 | cmd.Flags().IntP("port", "p", sbs.DefaultListenPort, "quic listening port")
61 | cmd.Flags().String("id", "", "manual peer ID. Defaults to machine ID")
62 | cmd.Flags().StringSlice("boot-peer", []string{}, "a list of peers to connect to on boot in format 'peerID@addr:port'")
63 |
64 | return cmd
65 | }
66 |
--------------------------------------------------------------------------------
/pkg/sbs/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "log"
5 | "os"
6 | "reflect"
7 |
8 | "github.com/spf13/viper"
9 | )
10 |
11 | const (
12 | defaultBaseDir = "/var/local/vpc/sbs/"
13 | )
14 |
15 | func init() {
16 | viper.SetConfigFile("sbs")
17 | viper.SetConfigType("yaml")
18 |
19 | viper.AddConfigPath("/etc/vpc")
20 | viper.AddConfigPath("/var/local/etc/vpc")
21 | viper.AddConfigPath("$HOME/.vpc")
22 | }
23 |
24 | //Read reads the config files
25 | func Read() error {
26 | err := viper.ReadInConfig()
27 | if err != nil {
28 | _, isNotFound := err.(viper.ConfigFileNotFoundError)
29 | _, isPathNotFound := err.(*os.PathError)
30 | if isNotFound || isPathNotFound {
31 | return nil
32 | }
33 | log.Printf("type %s", reflect.TypeOf(err))
34 | return err
35 | }
36 |
37 | return nil
38 | }
39 |
40 | //BlockStoreDir storage location for persistent data
41 | func BlockStoreDir() string {
42 | dir := viper.GetString("block-store-dir")
43 | if dir == "" {
44 | return defaultBaseDir
45 | }
46 |
47 | return dir
48 | }
49 |
--------------------------------------------------------------------------------
/pkg/sbs/control.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import "github.com/tcfw/vpc/pkg/sbs/control"
4 |
5 | func (s *Server) watchController(once bool) {
6 | attachments := s.controller.AttachmentsCh(true)
7 | volumes := s.controller.VolumesCh(true)
8 |
9 | for {
10 | select {
11 | case <-s.shutdownCh:
12 | return
13 | case attachAction := <-attachments:
14 | s.handleAttachmentUpdate(attachAction)
15 | break
16 | case volAction := <-volumes:
17 | s.handleVolumeUpdate(volAction)
18 | break
19 | }
20 |
21 | if once {
22 | break
23 | }
24 | }
25 | }
26 |
27 | func (s *Server) handleVolumeUpdate(action control.VolumeAction) {
28 | switch action.Op {
29 | case control.OpAdd:
30 | if _, ok := s.volumes[action.Desc.Id]; ok {
31 | return
32 | }
33 | s.log.WithField("vol", action.Desc.Id).Info("got new volume")
34 | if err := s.AddVolume(action.Desc, action.Peers); err != nil {
35 | s.log.WithField("vol", action.Desc.Id).WithError(err).Error("failed to add new volume")
36 | }
37 | }
38 | }
39 |
40 | func (s *Server) handleAttachmentUpdate(action control.VolumeAction) {
41 | s.log.Infof("got new attachment %s", action.Desc.Id)
42 |
43 | if s.nbd == nil {
44 | s.nbd = NewNBDServer(s, NBDDefaultPort, s.controller)
45 | }
46 |
47 | peers := s.GetPeers(action.Peers)
48 |
49 | if vol, isLocal := s.volumes[action.Desc.Id]; isLocal {
50 | err := s.nbd.Attach(vol, peers)
51 | if err != nil {
52 | s.log.WithError(err).Error("failed to attach volume")
53 | }
54 |
55 | return
56 | }
57 |
58 | //add a half configured volume
59 | vol := &Volume{
60 | id: action.Desc.Id,
61 | PlacementPeers: map[string]struct{}{},
62 | desc: action.Desc,
63 | }
64 |
65 | err := s.nbd.Attach(vol, peers)
66 | if err != nil {
67 | s.log.WithError(err).Error("failed to attach volume")
68 | return
69 | }
70 |
71 | s.mu.Lock()
72 | defer s.mu.Unlock()
73 |
74 | s.volumes[vol.id] = vol
75 | }
76 |
--------------------------------------------------------------------------------
/pkg/sbs/control/InMemController.go:
--------------------------------------------------------------------------------
1 | package control
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "sync"
7 |
8 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
9 | )
10 |
11 | //InMemController in-memory controller; useful for tests
12 | //Volumes will be assigned to all peers
13 | type InMemController struct {
14 | Volumes map[string]*volumesAPI.Volume
15 | Attachments map[string]*volumesAPI.Volume
16 |
17 | volumeChs []chan VolumeAction
18 | attachmentChs []chan VolumeAction
19 | peers map[string]*net.UDPAddr
20 | mu sync.Mutex
21 | }
22 |
23 | //NewInMemController provides a new in-memory controller instance for testing
24 | func NewInMemController() *InMemController {
25 | return &InMemController{
26 | Volumes: map[string]*volumesAPI.Volume{},
27 | Attachments: map[string]*volumesAPI.Volume{},
28 | volumeChs: []chan VolumeAction{},
29 | attachmentChs: []chan VolumeAction{},
30 | peers: map[string]*net.UDPAddr{},
31 | }
32 | }
33 |
34 | //VolumesCh provides updates to storage nodes for adding or removing volumes
35 | func (imc *InMemController) VolumesCh(existing bool) <-chan VolumeAction {
36 | ch := make(chan VolumeAction)
37 | imc.volumeChs = append(imc.volumeChs, ch)
38 |
39 | if existing {
40 | go imc.sendExisting(ch)
41 | }
42 |
43 | return ch
44 | }
45 |
46 | //AttachmentsCh provides updates to agent nodes for adding or removing volume attachments
47 | func (imc *InMemController) AttachmentsCh(existing bool) <-chan VolumeAction {
48 | ch := make(chan VolumeAction)
49 | imc.attachmentChs = append(imc.attachmentChs, ch)
50 | if existing {
51 | go imc.sendExisting(ch)
52 |
53 | }
54 | return ch
55 | }
56 |
57 | func (imc *InMemController) sendExisting(ch chan VolumeAction) {
58 | imc.mu.Lock()
59 | defer imc.mu.Unlock()
60 |
61 | peerIDs := imc.PeerIDs()
62 |
63 | for _, vol := range imc.Volumes {
64 | ch <- VolumeAction{
65 | Op: OpAdd,
66 | Desc: vol,
67 | Peers: peerIDs,
68 | }
69 | }
70 | }
71 |
72 | //PeerIDs provides a list of connected peers
73 | func (imc *InMemController) PeerIDs() []string {
74 | peerIDs := []string{}
75 | for peer := range imc.peers {
76 | peerIDs = append(peerIDs, peer)
77 | }
78 | return peerIDs
79 | }
80 |
81 | //Discover provides a mechanism for finding peer endpoints
82 | func (imc *InMemController) Discover(peer string) (*net.UDPAddr, error) {
83 | imc.mu.Lock()
84 | defer imc.mu.Unlock()
85 |
86 | addr, ok := imc.peers[peer]
87 | if !ok {
88 | return nil, fmt.Errorf("not found")
89 | }
90 |
91 | return addr, nil
92 | }
93 |
94 | //Register allows a server to register it's endpoint
95 | func (imc *InMemController) Register(id string, addr *net.UDPAddr) error {
96 | imc.mu.Lock()
97 | defer imc.mu.Unlock()
98 |
99 | imc.peers[id] = addr
100 | return nil
101 | }
102 |
103 | //DefineVolume adds a new volume and notifies all peers of new volume
104 | func (imc *InMemController) DefineVolume(vol *volumesAPI.Volume, isUpdate bool) {
105 | imc.mu.Lock()
106 | defer imc.mu.Unlock()
107 |
108 | imc.Volumes[vol.Id] = vol
109 |
110 | peerIDs := imc.PeerIDs()
111 |
112 | op := OpAdd
113 | if isUpdate {
114 | op = OpUpdate
115 | }
116 | for _, ch := range imc.volumeChs {
117 | ch <- VolumeAction{
118 | Op: op,
119 | Desc: vol,
120 | Peers: peerIDs,
121 | }
122 | }
123 | }
124 |
125 | //DefineAttachment adds a new attachment and notifies all peers of new volume
126 | func (imc *InMemController) DefineAttachment(vol *volumesAPI.Volume, isUpdate bool) {
127 | imc.mu.Lock()
128 | defer imc.mu.Unlock()
129 |
130 | imc.Attachments[vol.Id] = vol
131 |
132 | peerIDs := imc.PeerIDs()
133 |
134 | op := OpAdd
135 | if isUpdate {
136 | op = OpUpdate
137 | }
138 | for _, ch := range imc.attachmentChs {
139 | ch <- VolumeAction{
140 | Op: op,
141 | Desc: vol,
142 | Peers: peerIDs,
143 | }
144 | }
145 | }
146 |
147 | //RemoveVolume removes a volme along with attachments and notifies all peers of the change
148 | func (imc *InMemController) RemoveVolume(id string) {
149 | imc.mu.Lock()
150 | defer imc.mu.Unlock()
151 |
152 | vol, ok := imc.Volumes[id]
153 | if !ok {
154 | return
155 | }
156 |
157 | delete(imc.Volumes, vol.Id)
158 | delete(imc.Attachments, vol.Id)
159 |
160 | for _, ch := range imc.attachmentChs {
161 | ch <- VolumeAction{
162 | Op: OpRemove,
163 | Desc: vol,
164 | }
165 | }
166 |
167 | for _, ch := range imc.volumeChs {
168 | ch <- VolumeAction{
169 | Op: OpRemove,
170 | Desc: vol,
171 | }
172 | }
173 | }
174 |
175 | //Peers provides information about which peers should contain volumes
176 | func (imc *InMemController) Peers(volumeID string) ([]*PeerInfo, error) {
177 | peerInfo := []*PeerInfo{}
178 |
179 | for peer, addr := range imc.peers {
180 | peerInfo = append(peerInfo, &PeerInfo{
181 | ID: peer,
182 | Addr: addr,
183 | })
184 | }
185 |
186 | return peerInfo, nil
187 | }
188 |
--------------------------------------------------------------------------------
/pkg/sbs/control/controller.go:
--------------------------------------------------------------------------------
1 | package control
2 |
3 | import (
4 | "net"
5 |
6 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
7 | )
8 |
9 | //Controller provides cluster level management of individual SBS nodes
10 | type Controller interface {
11 | //VolumesCh provides updates to storage nodes for adding or removing volumes
12 | //If existing is true, all existing volumes wil be streamed to the chan
13 | VolumesCh(exiting bool) <-chan VolumeAction
14 |
15 | //AttachmentsCh provides updates to agent nodes for adding or removing volume attachments
16 | //If existing is true, all existing attachments wil be streamed to the chan
17 | AttachmentsCh(exiting bool) <-chan VolumeAction
18 |
19 | //Discover provides a mechanism for finding peer endpoints
20 | Discover(peer string) (*net.UDPAddr, error)
21 |
22 | //Register allows a server to register it's endpoint
23 | Register(peer string, addr *net.UDPAddr) error
24 |
25 | //Peers provides information about which peers should contain volumes
26 | Peers(volumeID string) ([]*PeerInfo, error)
27 | }
28 |
29 | //ActionOp operation being applied to a volume
30 | type ActionOp int32
31 |
32 | const (
33 | //OpAdd add volume
34 | OpAdd ActionOp = iota
35 | //OpRemove remove volume
36 | OpRemove
37 | //OpUpdate update config of volume
38 | OpUpdate
39 | )
40 |
41 | //VolumeAction provides update data of volume changes
42 | type VolumeAction struct {
43 | Op ActionOp
44 | Desc *volumesAPI.Volume
45 | Peers []string
46 | }
47 |
48 | //PeerInfo contains basic info about contacting a peer
49 | type PeerInfo struct {
50 | ID string
51 | Addr *net.UDPAddr
52 | }
53 |
--------------------------------------------------------------------------------
/pkg/sbs/control_test.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "math/rand"
5 | "testing"
6 |
7 | "github.com/stretchr/testify/assert"
8 |
9 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
10 | "github.com/tcfw/vpc/pkg/sbs/control"
11 | )
12 |
13 | func TestRegister(t *testing.T) {
14 | controller := control.NewInMemController()
15 |
16 | srv := NewServerWithPort(rand.Intn(1750) + 34000)
17 | srv.SetPeerID(randomID())
18 | srv.SetController(controller)
19 | srv.Listen()
20 |
21 | assert.NotEmpty(t, controller.PeerIDs())
22 | }
23 |
24 | func TestDiscovery(t *testing.T) {
25 | controller := control.NewInMemController()
26 | ids, servers, err := makeTestDisconnectedCluster(2, controller)
27 | if err != nil {
28 | t.Fatal(err)
29 | }
30 |
31 | peer, err := servers[0].discoverPeer(ids[1])
32 | if assert.NoError(t, err) {
33 | assert.Equal(t, peer.PeerID, ids[1])
34 | assert.Len(t, servers[0].peers, 1)
35 | }
36 | }
37 |
38 | func TestAddVolume(t *testing.T) {
39 | controller := control.NewInMemController()
40 |
41 | controller.DefineVolume(&volumesAPI.Volume{
42 | Id: "vol-test",
43 | Size: 10,
44 | }, false)
45 |
46 | srv := NewServerWithPort(rand.Intn(1750) + 34000)
47 | srv.SetPeerID(randomID())
48 | srv.SetController(controller)
49 |
50 | //One pass to get the update
51 | srv.watchController(true)
52 |
53 | assert.NotEmpty(t, srv.volumes)
54 | }
55 |
--------------------------------------------------------------------------------
/pkg/sbs/errors.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "github.com/lucas-clemente/quic-go"
5 | )
6 |
7 | const (
8 | errHandshakeRejected quic.ErrorCode = iota
9 | errDisconnect
10 | )
11 |
12 | type quicError interface {
13 | IsApplicationError() bool
14 | }
15 |
--------------------------------------------------------------------------------
/pkg/sbs/listen.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "context"
5 | "crypto/rand"
6 | "crypto/rsa"
7 | "crypto/tls"
8 | "crypto/x509"
9 | "encoding/pem"
10 | "fmt"
11 | "math/big"
12 | "net"
13 |
14 | "github.com/lucas-clemente/quic-go"
15 | "github.com/sirupsen/logrus"
16 | )
17 |
18 | //Listen starts listening for remote connections
19 | func (s *Server) Listen() error {
20 | return s.listen(s.listenPort)
21 | }
22 |
23 | //Listen starts listening
24 | func (s *Server) listen(port int) error {
25 | s.log.Printf("PeerID: %s", s.peerID)
26 |
27 | tls, err := tlsConfig()
28 | if err != nil {
29 | return fmt.Errorf("failed to provision tls: %s", err)
30 | }
31 |
32 | pc, err := net.ListenPacket("udp", fmt.Sprintf(":%d", port))
33 | if err != nil {
34 | return fmt.Errorf("failed to open port: %s", err)
35 | }
36 | s.pConn = pc
37 |
38 | l, err := quic.Listen(s.pConn, tls, defaultQuicConfig())
39 | if err != nil {
40 | return fmt.Errorf("failed to listen: %s", err)
41 | }
42 |
43 | s.listener = l
44 |
45 | s.log.WithFields(logrus.Fields{"port": port}).Info("Listening for peers")
46 |
47 | go s.accept()
48 |
49 | if s.controller != nil {
50 | err := s.controller.Register(s.peerID, s.listener.Addr().(*net.UDPAddr))
51 | if err != nil {
52 | s.log.WithError(err).Error("failed to register with controller")
53 | }
54 | }
55 |
56 | return nil
57 | }
58 |
59 | //defaultQuicConfig used for setting up peers
60 | func defaultQuicConfig() *quic.Config {
61 | return &quic.Config{
62 | MaxIncomingStreams: 10000,
63 | KeepAlive: true,
64 | }
65 | }
66 |
67 | //tlsConfig TODO(tcfw) use actual tls certs
68 | func tlsConfig() (*tls.Config, error) {
69 | key, err := rsa.GenerateKey(rand.Reader, 2048)
70 | if err != nil {
71 | panic(err)
72 | }
73 | template := x509.Certificate{SerialNumber: big.NewInt(1)}
74 | certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
75 | if err != nil {
76 | panic(err)
77 | }
78 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
79 | certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
80 |
81 | tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
82 | if err != nil {
83 | panic(err)
84 | }
85 | return &tls.Config{
86 | InsecureSkipVerify: true,
87 | Certificates: []tls.Certificate{tlsCert},
88 | NextProtos: []string{"quic-vpc-sbs"},
89 | }, nil
90 | }
91 |
92 | //accept handles new QUIC connections - pre-handshake
93 | func (s *Server) accept() {
94 | for {
95 | select {
96 | case <-s.shutdownCh:
97 | return
98 | default:
99 | }
100 | sess, err := s.listener.Accept(context.Background())
101 | if err != nil {
102 | s.log.WithFields(logrus.Fields{"err": err}).Warn("failed to accept conn to peer")
103 | continue
104 | }
105 |
106 | s.log.WithFields(logrus.Fields{"addr": sess.RemoteAddr().String()}).Debug("new connection")
107 |
108 | if sess.RemoteAddr().(*net.UDPAddr).IP.IsLoopback() {
109 | s.log.Warnf("connecting with local peer")
110 | go s.negotiateLocalPeer(sess)
111 | } else {
112 | go s.negotiate(sess, false)
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/pkg/sbs/nbd.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "net"
8 | "sync"
9 |
10 | "github.com/sirupsen/logrus"
11 | "github.com/tcfw/vpc/pkg/sbs/control"
12 | "github.com/tcfw/vpc/pkg/utils"
13 | )
14 |
15 | //NBDServer provides Linux NBD connectivity to a SBS cluster
16 | type NBDServer struct {
17 | volumeServer *Server
18 | log *logrus.Logger
19 | listenPort int
20 | listener net.Listener
21 | controller control.Controller
22 | attachments map[string]*Volume
23 | mu sync.Mutex
24 |
25 | ctx context.Context
26 |
27 | sessCount sync.WaitGroup
28 | }
29 |
30 | //NewNBDServer provisions a new NBD listening server attached to the volume server
31 | func NewNBDServer(s *Server, port int, c control.Controller) *NBDServer {
32 | nbd := &NBDServer{
33 | volumeServer: s,
34 | log: utils.DefaultLogger(),
35 | listenPort: port,
36 | controller: c,
37 | attachments: map[string]*Volume{},
38 | ctx: context.Background(),
39 | }
40 |
41 | go nbd.Listen()
42 |
43 | return nbd
44 | }
45 |
46 | //Listen starts listening for and accepts new connections
47 | func (s *NBDServer) Listen() error {
48 | nli, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", s.listenPort))
49 | if err != nil {
50 | return fmt.Errorf("failed to listen: %s", err)
51 | }
52 |
53 | defer nli.Close()
54 |
55 | s.listener = nli
56 |
57 | s.log.WithField("port", s.listenPort).Info("Listening for NBD connections")
58 |
59 | for {
60 | select {
61 | case <-s.ctx.Done():
62 | return nil
63 | default:
64 | }
65 | if conn, err := s.listener.Accept(); err != nil {
66 | if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
67 | continue
68 | }
69 | s.log.WithError(err).Error("failed to accept connection")
70 | } else {
71 | go s.accept(conn)
72 | }
73 | }
74 | }
75 |
76 | //accept new connections
77 | func (s *NBDServer) accept(conn net.Conn) {
78 | s.sessCount.Add(1)
79 | defer s.sessCount.Done()
80 |
81 | sess := newSession(s, s.volumeServer, conn, s.log)
82 | sess.handle(s.ctx)
83 | }
84 |
85 | //Shutdown terminates the NBD listener
86 | func (s *NBDServer) Shutdown() {
87 |
88 | }
89 |
90 | //Attach adds a volume as a NBD export
91 | func (s *NBDServer) Attach(vol *Volume, peers []*Peer) error {
92 | if _, exists := s.attachments[vol.ID()]; exists {
93 | return errors.New("already exists")
94 | }
95 |
96 | s.mu.Lock()
97 | defer s.mu.Unlock()
98 |
99 | s.attachments[vol.ID()] = vol
100 |
101 | return nil
102 | }
103 |
--------------------------------------------------------------------------------
/pkg/sbs/nbdProto.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | /*
4 | Details taken originally from
5 | https://github.com/abligh/gonbdserver/blob/master/nbd/protocol.go
6 | */
7 |
8 | // this section is in essence a transcription of the protocol from
9 | // NBD's proto.md; note that that file is *not* GPL. For details of
10 | // what the options mean, see proto.md
11 |
12 | // NBD commands
13 | const (
14 | NBDCMDRead = iota
15 | NBDCMDWrite
16 | NBDCMDDisc
17 | NBDCMDFlush
18 | NBDCMDTrim
19 | NBDCMDCache
20 | NBDCMDWriteZeroes
21 | NBDCMDBlockStatus
22 | )
23 |
24 | // NBD command flags
25 | const (
26 | NBDCMDFlagFUA = uint16(1 << iota)
27 | NBDCMDFlagMayTrim
28 | NBDCMDFlagDF
29 | )
30 |
31 | // NBD negotiation flags
32 | const (
33 | NBDFLAGHasFlags = uint16(1 << iota)
34 | NBDFLAGReadOnly
35 | NBDFLAGSendFlush
36 | NBDFLAGSendFUA
37 | NBDFLAGRotational
38 | NBDFLAGSendTrim
39 | NBDFLAGSendWriteZeroes
40 | NBDFLAGSendDF
41 | NBDFLAGMultiCon
42 | NBDFLAGSendResize
43 | NBDFLAGSendCache
44 | NBDFLAGFastZero
45 | )
46 |
47 | // NBD magic numbers
48 | const (
49 | NBDMAGIC = 0x4e42444d41474943
50 | NBDMAGICRequest = 0x25609513
51 | NBDMAGICReply = 0x67446698
52 | NBDMAGICCliserv = 0x00420281861253
53 | NBDMAGICOpts = 0x49484156454F5054
54 | NBDMAGICRep = 0x3e889045565a9
55 | NBDMAGICStructuredReply = 0x668e33ef
56 |
57 | // NBD default port
58 | NBDDefaultPort = 10809
59 | )
60 |
61 | // NBD options
62 | const (
63 | NBDOPTExportName = iota + 1
64 | NBDOPTAbort
65 | NBDOPTList
66 | NBDOPTPeekExport
67 | NBDOPTStartTLS
68 | NBDOPTInfo
69 | NBDOPTGo
70 | NBDOPTStructuredReply
71 |
72 | // NBD option reply types
73 | NBDREPAck = uint32(1)
74 | NBDREPServer = uint32(2)
75 | NBDREPInfo = uint32(3)
76 | NBDREPFlagError = uint32(1 << 31)
77 | NBDREPErrUnsup = uint32(1 | NBDREPFlagError)
78 | NBDREPErrPolicy = uint32(2 | NBDREPFlagError)
79 | NBDREPErrInvalid = uint32(3 | NBDREPFlagError)
80 | NBDREPErrPlatform = uint32(4 | NBDREPFlagError)
81 | NBDREPErrTLSReqd = uint32(5 | NBDREPFlagError)
82 | NBDREPErrUnknown = uint32(6 | NBDREPFlagError)
83 | NBDREPErrShutdown = uint32(7 | NBDREPFlagError)
84 | NBDREPErrBlockSizeReqd = uint32(8 | NBDREPFlagError)
85 |
86 | // NBD reply flags
87 | NBDReplyFlagDone = 1 << 0
88 | )
89 |
90 | // NBD reply types
91 | const (
92 | NBDREPLYTYPENone = iota
93 | NBDREPLYTYPEError
94 | NBDREPLYTYPEErrorOffset
95 | NBDREPLYTYPEOffsetData
96 | NBDREPLYTYPEOffsetHole
97 | )
98 |
99 | // NBD hanshake flags
100 | const (
101 | NBDFLAGFixedNewstyle = 1 << iota
102 | NBDFLAGNoZeroes
103 | )
104 |
105 | // NBD client flags
106 | const (
107 | NBDFLAGCFixedNewstyle = 1 << iota
108 | NBDFLAGCNoZeroes
109 |
110 | // NBD errors
111 | NBDEPERM = 1
112 | NBDEIO = 5
113 | NBDENOMEM = 12
114 | NBDEINVAL = 22
115 | NBDENOSPC = 28
116 | NBDEOVERFLOW = 75
117 | )
118 |
119 | // NBD info types
120 | const (
121 | NBDINFOExport = iota
122 | NBDINFOName
123 | NBDINFODescription
124 | NBDINFOBlockSize
125 | )
126 |
127 | // NBD new style header
128 | type nbdNewStyleHeader struct {
129 | NbdMagic uint64
130 | NbdOptsMagic uint64
131 | NbdGlobalFlags uint16
132 | }
133 |
134 | // NBD client flags
135 | type nbdClientFlags struct {
136 | NbdClientFlags uint32
137 | }
138 |
139 | // NBD client options
140 | type nbdClientOpt struct {
141 | NbdOptMagic uint64
142 | NbdOptID uint32
143 | NbdOptLen uint32
144 | }
145 |
146 | // NBD export details
147 | type nbdExportDetails struct {
148 | NbdExportSize uint64
149 | NbdExportFlags uint16
150 | }
151 |
152 | // NBD option reply
153 | type nbdOptReply struct {
154 | NbdOptReplyMagic uint64
155 | NbdOptID uint32
156 | NbdOptReplyType uint32
157 | NbdOptReplyLength uint32
158 | }
159 |
160 | // NBD request
161 | type nbdRequest struct {
162 | NbdRequestMagic uint32
163 | NbdCommandFlags uint16
164 | NbdCommandType uint16
165 | NbdHandle uint64
166 | NbdOffset uint64
167 | NbdLength uint32
168 | }
169 |
170 | // NBD simple reply
171 | type nbdReply struct {
172 | NbdReplyMagic uint32
173 | NbdError uint32
174 | NbdHandle uint64
175 | }
176 |
177 | type nbdStructuredReply struct {
178 | NbdReplyMagic uint32
179 | NbdFlags uint16
180 | NbdType uint16
181 | NbdHandle uint64
182 | NbdLength uint32
183 | }
184 |
185 | type nbdStructuredError struct {
186 | NbdError uint32
187 | NbdLength uint16
188 | }
189 |
190 | // NBD info export
191 | type nbdInfoExport struct {
192 | NbdInfoType uint16
193 | NbdExportSize uint64
194 | NbdTransmissionFlags uint16
195 | }
196 |
197 | // NBD info blocksize
198 | type nbdInfoBlockSize struct {
199 | NbdInfoType uint16
200 | NbdMinimumBlockSize uint32
201 | NbdPreferredBlockSize uint32
202 | NbdMaximumBlockSize uint32
203 | }
204 |
--------------------------------------------------------------------------------
/pkg/sbs/ndbSession_test.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/binary"
7 | "net"
8 | "os"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
13 | "github.com/tcfw/vpc/pkg/utils"
14 | )
15 |
16 | func TestNBDSendCmdReply(t *testing.T) {
17 | sConn, cConn := net.Pipe()
18 |
19 | s := &session{
20 | conn: sConn,
21 | log: utils.DefaultLogger(),
22 | }
23 |
24 | go func() {
25 | err := s.sendCmdReply(&nbdWork{
26 | nbdReq: nbdRequest{
27 | NbdCommandType: NBDCMDWrite,
28 | NbdHandle: 1,
29 | },
30 | }, nil, 0)
31 | if err != nil {
32 | t.Fatal(err)
33 | return
34 | }
35 | }()
36 |
37 | buf := make([]byte, 8096)
38 |
39 | n, err := cConn.Read(buf)
40 | if err != nil {
41 | t.Fatal(err)
42 | }
43 |
44 | reply := &nbdReply{}
45 | binary.Read(bytes.NewReader(buf[:n]), binary.BigEndian, reply)
46 |
47 | assert.Equal(t, uint32(NBDMAGICReply), reply.NbdReplyMagic)
48 | assert.Equal(t, uint64(1), reply.NbdHandle)
49 | assert.Equal(t, uint32(0), reply.NbdError)
50 | }
51 |
52 | func TestNBDReadCmd(t *testing.T) {
53 | sConn, cConn := net.Pipe()
54 |
55 | volDesc := &volumesAPI.Volume{
56 | Id: "vol-test",
57 | Size: 10, //GB
58 | }
59 |
60 | dir := os.TempDir()
61 | defer os.RemoveAll(dir + "/" + volDesc.Id)
62 |
63 | bs := NewBlockStore("", volDesc, dir, utils.DefaultLogger())
64 |
65 | data := make([]byte, BlockSize)
66 |
67 | data[0] = 0x1
68 |
69 | err := bs.write(&StoreCommand{
70 | Op: OpWrite,
71 | Offset: 0,
72 | Length: BlockSize,
73 | Data: data,
74 | })
75 | if err != nil {
76 | t.Fatal(err)
77 | }
78 |
79 | vol := &Volume{
80 | Blocks: bs,
81 | }
82 |
83 | s := &session{
84 | conn: sConn,
85 | log: utils.DefaultLogger(),
86 | volume: vol,
87 | workCh: make(chan *nbdWork, 2),
88 | bufAlloc: newBufAlloc(maxBlockAlloc),
89 | }
90 |
91 | go s.dispatch(context.Background(), 1)
92 |
93 | //Attempt multiple reads
94 | s.inFlight.Add(1)
95 | s.workCh <- &nbdWork{
96 | nbdReq: nbdRequest{
97 | NbdHandle: 1234,
98 | NbdCommandType: NBDCMDRead,
99 | NbdOffset: 0,
100 | NbdLength: BlockSize,
101 | },
102 | }
103 |
104 | //Read reply
105 | buf := make([]byte, BlockSize)
106 | n, err := cConn.Read(buf)
107 | if err != nil {
108 | t.Fatal(err)
109 | }
110 |
111 | reply := &nbdReply{}
112 | binary.Read(bytes.NewReader(buf[:n]), binary.BigEndian, reply)
113 | assert.Equal(t, uint32(NBDMAGICReply), reply.NbdReplyMagic)
114 |
115 | //Read payload
116 | n, err = cConn.Read(buf)
117 | if err != nil {
118 | t.Fatal(err)
119 | }
120 |
121 | assert.Equal(t, data, buf[:n])
122 | }
123 |
124 | func BenchmarkNBDReadCmd(b *testing.B) {
125 | sConn, cConn := net.Pipe()
126 |
127 | volDesc := &volumesAPI.Volume{
128 | Id: "vol-test",
129 | Size: 10, //GB
130 | }
131 |
132 | dir := os.TempDir()
133 | defer os.RemoveAll(dir + "/" + volDesc.Id)
134 |
135 | bs := NewBlockStore("", volDesc, dir, utils.DefaultLogger())
136 |
137 | data := make([]byte, BlockSize)
138 |
139 | data[0] = 0x1
140 |
141 | err := bs.write(&StoreCommand{
142 | Op: OpWrite,
143 | Offset: 0,
144 | Length: BlockSize,
145 | Data: data,
146 | })
147 | if err != nil {
148 | b.Fatal(err)
149 | }
150 |
151 | vol := &Volume{
152 | Blocks: bs,
153 | }
154 |
155 | go func() {
156 | buf := make([]byte, BlockSize)
157 | //recv
158 | for {
159 | cConn.Read(buf)
160 | cConn.Read(buf)
161 | }
162 | }()
163 |
164 | b.Run("no-cached", func(b *testing.B) {
165 | b.SetBytes(BlockSize)
166 | s := &session{
167 | conn: sConn,
168 | log: utils.DefaultLogger(),
169 | volume: vol,
170 | workCh: make(chan *nbdWork, 200),
171 | bufAlloc: newBufAlloc(maxBlockAlloc),
172 | }
173 |
174 | b.RunParallel(func(pb *testing.PB) {
175 | go s.dispatch(context.Background(), 0)
176 | for pb.Next() {
177 | s.inFlight.Add(1)
178 | s.workCh <- &nbdWork{
179 | nbdReq: nbdRequest{
180 | NbdHandle: 1234,
181 | NbdCommandType: NBDCMDRead,
182 | NbdOffset: 0,
183 | NbdLength: BlockSize,
184 | },
185 | }
186 | }
187 | })
188 | })
189 |
190 | b.Run("cached", func(b *testing.B) {
191 | b.SetBytes(BlockSize)
192 | s := &session{
193 | conn: sConn,
194 | log: utils.DefaultLogger(),
195 | volume: vol,
196 | workCh: make(chan *nbdWork, 200),
197 | bufAlloc: newBufAlloc(maxBlockAlloc),
198 | blockCache: newBlockCache(),
199 | }
200 |
201 | b.RunParallel(func(pb *testing.PB) {
202 | go s.dispatch(context.Background(), 0)
203 | for pb.Next() {
204 | s.inFlight.Add(1)
205 | s.workCh <- &nbdWork{
206 | nbdReq: nbdRequest{
207 | NbdHandle: 1234,
208 | NbdCommandType: NBDCMDRead,
209 | NbdOffset: 0,
210 | NbdLength: BlockSize,
211 | },
212 | }
213 | }
214 | })
215 | })
216 | }
217 |
--------------------------------------------------------------------------------
/pkg/sbs/raftStores.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "bytes"
5 | "encoding/binary"
6 | "encoding/gob"
7 | "fmt"
8 |
9 | badger "github.com/dgraph-io/badger/v2"
10 | "github.com/hashicorp/raft"
11 | )
12 |
13 | //LogStore provides a way for raft to store logs
14 | type LogStore struct {
15 | volumeID string
16 | db *badger.DB
17 | }
18 |
19 | //NewLogStore creates a new log store used in raft
20 | func NewLogStore(id string, db *badger.DB) *LogStore {
21 | return &LogStore{
22 | volumeID: id,
23 | db: db,
24 | }
25 | }
26 |
27 | const (
28 | keyFirstIndex = "fidx"
29 | keyLastIndex = "lidx"
30 | )
31 |
32 | //FirstIndex returns the first index written. 0 for no entries.
33 | func (rls *LogStore) FirstIndex() (uint64, error) {
34 | var v uint64
35 |
36 | err := rls.db.View(func(tx *badger.Txn) error {
37 | vRaw, err := tx.Get(rls.key(keyFirstIndex))
38 | if err == badger.ErrKeyNotFound {
39 | return nil
40 | }
41 | if err != nil {
42 | return err
43 | }
44 | buf, err := vRaw.ValueCopy(nil)
45 | if err != nil {
46 | return err
47 | }
48 | v = binary.LittleEndian.Uint64(buf)
49 | return nil
50 | })
51 |
52 | return v, err
53 | }
54 |
55 | //LastIndex returns the last index written. 0 for no entries.
56 | func (rls *LogStore) LastIndex() (uint64, error) {
57 | var v uint64
58 |
59 | err := rls.db.View(func(tx *badger.Txn) error {
60 | vRaw, err := tx.Get(rls.key(keyLastIndex))
61 | if err == badger.ErrKeyNotFound {
62 | return nil
63 | }
64 | if err != nil {
65 | return err
66 | }
67 | buf, err := vRaw.ValueCopy(nil)
68 | if err != nil {
69 | return err
70 | }
71 | v = binary.LittleEndian.Uint64(buf)
72 | return nil
73 | })
74 |
75 | return v, err
76 | }
77 |
78 | //GetLog gets a log entry at a given index.
79 | func (rls *LogStore) GetLog(index uint64, l *raft.Log) error {
80 | var lRaw []byte
81 |
82 | err := rls.db.View(func(tx *badger.Txn) error {
83 | vRaw, err := tx.Get(rls.indexKey(index))
84 | if err != nil {
85 | return err
86 | }
87 | buf, err := vRaw.ValueCopy(nil)
88 | if err != nil {
89 | return err
90 | }
91 | lRaw = buf
92 | return nil
93 | })
94 | if err != nil {
95 | return err
96 | }
97 |
98 | return rls.decode(lRaw, l)
99 | }
100 |
101 | //StoreLog stores a log entry.
102 | func (rls *LogStore) StoreLog(log *raft.Log) error {
103 | return rls.StoreLogs([]*raft.Log{log})
104 | }
105 |
106 | //StoreLogs stores multiple log entries.
107 | func (rls *LogStore) StoreLogs(logs []*raft.Log) error {
108 | err := rls.db.Update(func(tx *badger.Txn) error {
109 | for _, l := range logs {
110 | fIndex := rls.key(keyFirstIndex)
111 | lIndex := rls.key(keyLastIndex)
112 | //Set first index if it doesn't exist
113 | _, err := tx.Get(fIndex)
114 | if err == badger.ErrKeyNotFound {
115 | fInb := make([]byte, 8)
116 | binary.LittleEndian.PutUint64(fInb, l.Index)
117 | err = tx.Set(fIndex, fInb)
118 | }
119 | if err != nil {
120 | return err
121 | }
122 |
123 | //Update last index
124 | lInb := make([]byte, 8)
125 | binary.LittleEndian.PutUint64(lInb, l.Index)
126 | if err := tx.Set(lIndex, lInb); err != nil {
127 | return err
128 | }
129 |
130 | lRaw, err := rls.encode(l)
131 | if err != nil {
132 | return err
133 | }
134 | err = tx.Set(rls.indexKey(l.Index), lRaw)
135 | if err != nil {
136 | return err
137 | }
138 | }
139 |
140 | return nil
141 | })
142 | return err
143 | }
144 |
145 | //DeleteRange deletes a range of log entries. The range is inclusive.
146 | func (rls *LogStore) DeleteRange(min, max uint64) error {
147 | return rls.db.Update(func(tx *badger.Txn) error {
148 | for i := min; i <= max; i++ {
149 | tx.Delete(rls.indexKey(i))
150 | }
151 |
152 | //Update first index assuming last index is further ahead
153 | fInb := make([]byte, 8)
154 | binary.LittleEndian.PutUint64(fInb, max+1)
155 | if err := tx.Set(rls.key(keyFirstIndex), fInb); err != nil {
156 | return err
157 | }
158 |
159 | return nil
160 | })
161 | }
162 |
163 | func (rls *LogStore) indexKey(index uint64) []byte {
164 | return []byte(fmt.Sprintf("%s:i:%d", rls.volumeID, index))
165 | }
166 |
167 | func (rls *LogStore) key(k string) []byte {
168 | return []byte(fmt.Sprintf("%s:%s", rls.volumeID, k))
169 | }
170 |
171 | func (rls *LogStore) keyRaw(k []byte) []byte {
172 | return rls.key(string(k))
173 | }
174 |
175 | func (rls *LogStore) encode(log *raft.Log) ([]byte, error) {
176 | var buf bytes.Buffer
177 | enc := gob.NewEncoder(&buf)
178 | err := enc.Encode(log)
179 | if err != nil {
180 | return nil, err
181 | }
182 |
183 | return buf.Bytes(), nil
184 | }
185 |
186 | func (rls *LogStore) decode(b []byte, l *raft.Log) error {
187 | buf := bytes.NewBuffer(b)
188 | enc := gob.NewDecoder(buf)
189 |
190 | err := enc.Decode(l)
191 | return err
192 | }
193 |
--------------------------------------------------------------------------------
/pkg/sbs/raftStores_test.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "testing"
5 |
6 | badger "github.com/dgraph-io/badger/v2"
7 | "github.com/hashicorp/raft"
8 | "github.com/stretchr/testify/assert"
9 | )
10 |
11 | func TestLogAdd(t *testing.T) {
12 | db, err := badger.Open(badger.DefaultOptions("").WithInMemory(true))
13 | if err != nil {
14 | t.Fatal(err)
15 | }
16 |
17 | logStore := &LogStore{
18 | volumeID: "vol-test",
19 | db: db,
20 | }
21 |
22 | //Check index bounds
23 | i, err := logStore.FirstIndex()
24 | if err != nil {
25 | t.Fatal(err)
26 | }
27 | assert.Zero(t, i)
28 |
29 | i, err = logStore.LastIndex()
30 | if err != nil {
31 | t.Fatal(err)
32 | }
33 | assert.Zero(t, i)
34 |
35 | err = logStore.StoreLog(&raft.Log{
36 | Term: 1,
37 | Index: 1,
38 | Type: raft.LogCommand,
39 | Data: []byte{1, 2, 3, 4, 5},
40 | })
41 |
42 | if assert.NoError(t, err) {
43 | //Check index bounds after store
44 | i, err = logStore.FirstIndex()
45 | if err != nil {
46 | t.Fatal(err)
47 | }
48 | assert.Equal(t, uint64(1), i)
49 |
50 | i, err = logStore.LastIndex()
51 | if err != nil {
52 | t.Fatal(err)
53 | }
54 | assert.Equal(t, uint64(1), i)
55 |
56 | err = logStore.StoreLog(&raft.Log{
57 | Term: 1,
58 | Index: 2,
59 | Type: raft.LogCommand,
60 | Data: []byte{1, 2, 3, 4, 5},
61 | })
62 |
63 | if assert.NoError(t, err) {
64 | i, err = logStore.FirstIndex()
65 | if err != nil {
66 | t.Fatal(err)
67 | }
68 | assert.Equal(t, uint64(1), i)
69 |
70 | i, err = logStore.LastIndex()
71 | if err != nil {
72 | t.Fatal(err)
73 | }
74 | assert.Equal(t, uint64(2), i)
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/pkg/sbs/raft_test.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "log"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
10 | )
11 |
12 | func TestRaftTransport(t *testing.T) {
13 | ids, servers, err := makeTestCluster(3, nil)
14 | if err != nil {
15 | t.Fatal(err)
16 | }
17 | defer cleanUpServerData(servers)
18 |
19 | obsCh, observer := makePeerObserver()
20 |
21 | for i, srv := range servers {
22 | if err := srv.AddVolume(&volumesAPI.Volume{
23 | Id: "vol-test",
24 | Size: 10,
25 | }, ids); err != nil {
26 | t.Fatal(err)
27 | }
28 | srv.Volume("vol-test").Raft.RegisterObserver(observer)
29 | log.Printf("added volume to %d", i)
30 | }
31 |
32 | waitForNPeers(2, obsCh)
33 | }
34 |
35 | func TestRaftApply(t *testing.T) {
36 | ids, servers, err := makeTestCluster(3, nil)
37 | if err != nil {
38 | t.Fatal(err)
39 | }
40 | defer cleanUpServerData(servers)
41 |
42 | obsCh, observer := makePeerObserver()
43 |
44 | for i, srv := range servers {
45 | if err := srv.AddVolume(&volumesAPI.Volume{
46 | Id: "vol-test",
47 | Size: 10,
48 | }, ids); err != nil {
49 | t.Fatal(err)
50 | }
51 | srv.Volume("vol-test").Raft.RegisterObserver(observer)
52 | log.Printf("added volume to %d", i)
53 | }
54 |
55 | waitForNPeers(2, obsCh)
56 |
57 | leader := getLeader(servers)
58 |
59 | cmd := &StoreCommand{
60 | Op: OpWrite,
61 | Offset: 0,
62 | Length: BlockSize,
63 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
64 | }
65 |
66 | cmdBytes, _ := cmd.Encode()
67 |
68 | apply := leader.Volume("vol-test").Raft.Apply(cmdBytes, 5*time.Second)
69 | err = apply.Error()
70 | if assert.NoError(t, err) {
71 | err, isErr := apply.Response().(error)
72 | if isErr {
73 | assert.NoError(t, err)
74 | } else {
75 | assert.Nil(t, err)
76 | }
77 | }
78 | }
79 |
80 | func TestRaftSnapshot(t *testing.T) {
81 | ids, servers, err := makeTestCluster(3, nil)
82 | if err != nil {
83 | t.Fatal(err)
84 | }
85 | defer cleanUpServerData(servers)
86 |
87 | obsCh, observer := makePeerObserver()
88 |
89 | for i, srv := range servers {
90 | if err := srv.AddVolume(&volumesAPI.Volume{
91 | Id: "vol-test",
92 | Size: 1,
93 | }, ids); err != nil {
94 | t.Fatal(err)
95 | }
96 | srv.Volume("vol-test").Raft.RegisterObserver(observer)
97 | log.Printf("added volume to %d", i)
98 | }
99 |
100 | waitForNPeers(2, obsCh)
101 |
102 | leader := getLeader(servers)
103 |
104 | cmd := &StoreCommand{
105 | Op: OpWrite,
106 | Offset: 0,
107 | Length: BlockSize,
108 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
109 | }
110 |
111 | cmdBytes, _ := cmd.Encode()
112 |
113 | apply := leader.Volume("vol-test").Raft.Apply(cmdBytes, 5*time.Second)
114 | err = apply.Error()
115 | if assert.NoError(t, err) {
116 | future := leader.Volume("vol-test").Raft.Snapshot()
117 | if err := future.Error(); err != nil {
118 | t.Fatal(err)
119 | }
120 | }
121 | }
122 |
123 | func BenchmarkRaftApply(b *testing.B) {
124 | ids, servers, err := makeTestCluster(3, nil)
125 | if err != nil {
126 | b.Fatal(err)
127 | }
128 | defer cleanUpServerData(servers)
129 |
130 | obsCh, observer := makePeerObserver()
131 |
132 | for i, srv := range servers {
133 | if err := srv.AddVolume(&volumesAPI.Volume{
134 | Id: "vol-test",
135 | Size: 10,
136 | }, ids); err != nil {
137 | b.Fatal(err)
138 | }
139 | srv.Volume("vol-test").Raft.RegisterObserver(observer)
140 | log.Printf("added volume to %d", i)
141 | }
142 |
143 | waitForNPeers(2, obsCh)
144 | leader := getLeader(servers)
145 |
146 | cmd := &StoreCommand{
147 | Op: OpWrite,
148 | Offset: 0,
149 | Length: BlockSize,
150 | Data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 0},
151 | }
152 |
153 | cmdBytes, _ := cmd.Encode()
154 |
155 | b.Run("apply", func(b *testing.B) {
156 | for i := 0; i < b.N; i++ {
157 | leader.Volume("vol-test").Raft.Apply(cmdBytes, 5*time.Second)
158 | }
159 | })
160 | }
161 |
--------------------------------------------------------------------------------
/pkg/sbs/rpc_test.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "log"
5 | "testing"
6 | "time"
7 |
8 | "github.com/stretchr/testify/assert"
9 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
10 | )
11 |
12 | func TestPing(t *testing.T) {
13 | ids, servers, err := makeTestCluster(2, nil)
14 | if err != nil {
15 | t.Fatal(err)
16 | }
17 | defer cleanUpServerData(servers)
18 |
19 | resp, err := servers[1].sendReq(servers[1].peers[ids[0]], &volumesAPI.PeerPing{Ts: time.Now().Unix()})
20 | if err != nil {
21 | t.Fatal(err)
22 | }
23 |
24 | assert.NotEmpty(t, resp.(*volumesAPI.PeerPing).Ts)
25 | }
26 |
27 | func BenchmarkPing(b *testing.B) {
28 | ids, servers, err := makeTestCluster(2, nil)
29 | if err != nil {
30 | b.Fatal(err)
31 | }
32 | defer cleanUpServerData(servers)
33 |
34 | peer := servers[1].peers[ids[0]]
35 | msg := &volumesAPI.PeerPing{Ts: time.Now().Unix()}
36 |
37 | failures := 0
38 |
39 | b.Run("ping", func(b *testing.B) {
40 | for i := 0; i < b.N; i++ {
41 | _, err := servers[1].SendReqViaChannel(peer, "ping", msg)
42 | if err != nil {
43 | failures++
44 | }
45 | }
46 | })
47 |
48 | b.Logf("failures: %d", failures)
49 | }
50 |
51 | func TestBlockCommandWrite(t *testing.T) {
52 | ids, servers, err := makeTestCluster(2, nil)
53 | if err != nil {
54 | t.Fatal(err)
55 | }
56 | defer cleanUpServerData(servers)
57 |
58 | obsCh, observer := makePeerObserver()
59 |
60 | for i, srv := range servers {
61 | if err := srv.AddVolume(&volumesAPI.Volume{
62 | Id: "vol-test",
63 | Size: 10,
64 | }, ids); err != nil {
65 | t.Fatal(err)
66 | }
67 | srv.volumes["vol-test"].Raft.RegisterObserver(observer)
68 | log.Printf("added volume to %d", i)
69 | }
70 |
71 | waitForNPeers(1, obsCh)
72 | leader := getLeader(servers)
73 | notLeader := getNotLeader(servers)
74 |
75 | //Send request from not leader to leader
76 | resp, err := notLeader.sendReq(notLeader.peers[leader.PeerID()], &volumesAPI.BlockCommandRequest{
77 | Volume: "vol-test",
78 | })
79 | if err != nil {
80 | t.Fatal(err)
81 | }
82 |
83 | assert.NotNil(t, resp)
84 | bcr, isBCR := resp.(*volumesAPI.BlockCommandResponse)
85 | assert.True(t, isBCR)
86 | assert.NotNil(t, bcr.Volume)
87 | assert.Empty(t, bcr.Error)
88 |
89 | }
90 |
91 | func TestBlockCommandWriteForwarding(t *testing.T) {
92 | ids, servers, err := makeTestCluster(2, nil)
93 | if err != nil {
94 | t.Fatal(err)
95 | }
96 | defer cleanUpServerData(servers)
97 |
98 | obsCh, observer := makePeerObserver()
99 |
100 | for i, srv := range servers {
101 | if err := srv.AddVolume(&volumesAPI.Volume{
102 | Id: "vol-test",
103 | Size: 10,
104 | }, ids); err != nil {
105 | t.Fatal(err)
106 | }
107 | srv.volumes["vol-test"].Raft.RegisterObserver(observer)
108 | log.Printf("added volume to %d", i)
109 | }
110 |
111 | waitForNPeers(1, obsCh)
112 | leader := getLeader(servers)
113 | notLeader := getNotLeader(servers)
114 |
115 | //Send request from leader to not leader - this should then
116 | //be forwarded back to the leader for processing
117 | resp, err := leader.sendReq(leader.peers[notLeader.PeerID()], &volumesAPI.PeerRPC{
118 | Metadata: &volumesAPI.RPCMetadata{
119 | AllowForwarding: true,
120 | TTL: 2,
121 | },
122 | RPC: &volumesAPI.PeerRPC_BlockCommand{BlockCommand: &volumesAPI.BlockCommandRequest{
123 | Volume: "vol-test",
124 | },
125 | }})
126 | if err != nil {
127 | t.Fatal(err)
128 | }
129 |
130 | assert.NotNil(t, resp)
131 | bcr, isBCR := resp.(*volumesAPI.BlockCommandResponse)
132 | assert.True(t, isBCR)
133 | assert.NotNil(t, bcr.Volume)
134 | assert.Empty(t, bcr.Error)
135 | }
136 |
--------------------------------------------------------------------------------
/pkg/sbs/server.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "net"
5 | "os"
6 | "sync"
7 |
8 | "github.com/lucas-clemente/quic-go"
9 | "github.com/sirupsen/logrus"
10 |
11 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
12 | "github.com/tcfw/vpc/pkg/sbs/control"
13 | "github.com/tcfw/vpc/pkg/utils"
14 | )
15 |
16 | const (
17 | //DefaultListenPort main peer listening port
18 | DefaultListenPort = 32546
19 | )
20 |
21 | //Server Simple Block Storage server instance
22 | type Server struct {
23 | listenPort int
24 | pConn net.PacketConn
25 | listener quic.Listener
26 | controller control.Controller
27 | peerID string
28 |
29 | peers map[string]*Peer
30 | volumes map[string]*Volume
31 |
32 | mu sync.Mutex
33 | connCount sync.WaitGroup
34 | log *logrus.Logger
35 | shutdownCh chan struct{}
36 |
37 | nbd *NBDServer
38 | }
39 |
40 | //NewServer constructs a new SBS server
41 | func NewServer() *Server {
42 | s := &Server{
43 | peerID: utils.MachineID(),
44 | listenPort: DefaultListenPort,
45 | log: utils.DefaultLogger(),
46 | peers: map[string]*Peer{},
47 | volumes: map[string]*Volume{},
48 | shutdownCh: make(chan struct{}),
49 | }
50 |
51 | return s
52 | }
53 |
54 | //NewServerWithPort constructs a new SBS server on a specific listening port
55 | func NewServerWithPort(port int) *Server {
56 | s := NewServer()
57 | s.listenPort = port
58 |
59 | return s
60 | }
61 |
62 | //SetPeerID overwrites the local peer id
63 | func (s *Server) SetPeerID(id string) {
64 | s.peerID = id
65 | }
66 |
67 | //PeerID returns the current peer ID of the server
68 | func (s *Server) PeerID() string {
69 | return s.peerID
70 | }
71 |
72 | //SetController provides the server with a controller for volume assignments
73 | func (s *Server) SetController(cont control.Controller) {
74 | s.controller = cont
75 | }
76 |
77 | //Shutdown gracefully
78 | func (s *Server) Shutdown() {
79 | s.log.Warnln("Starting graceful shutdown. Press ctrl+c again to force shutdown")
80 | go func() {
81 | utils.BlockUntilSigTerm()
82 | os.Exit(1)
83 | }()
84 |
85 | //NBD first
86 | if s.nbd != nil {
87 | s.nbd.Shutdown()
88 | }
89 |
90 | //Tell peers we're shutting down
91 | s.log.Debug("removing peers from store via disconnect")
92 | for _, peer := range s.peers {
93 | s.disconnectPeer(peer)
94 | }
95 |
96 | s.log.Debug("closing all block stores")
97 | for _, vol := range s.volumes {
98 | vol.Shutdown()
99 | }
100 |
101 | close(s.shutdownCh)
102 |
103 | s.log.Infoln("Bye!")
104 | }
105 |
106 | //PeerIDs provides a list of peer IDs
107 | func (s *Server) PeerIDs() []string {
108 | ids := make([]string, 0, len(s.peers))
109 | for id := range s.peers {
110 | ids = append(ids, id)
111 | }
112 | return ids
113 | }
114 |
115 | //LocalAddr provides the server listening address
116 | func (s *Server) LocalAddr() *net.UDPAddr {
117 | return s.listener.Addr().(*net.UDPAddr)
118 | }
119 |
120 | //Volume returns the volume associated with the volume ID
121 | func (s *Server) Volume(id string) *Volume {
122 | if vol, ok := s.volumes[id]; ok {
123 | return vol
124 | }
125 |
126 | return nil
127 | }
128 |
129 | //AddVolume attaches a volume to the server
130 | func (s *Server) AddVolume(d *volumesAPI.Volume, placementPeers []string) error {
131 | if _, ok := s.volumes[d.Id]; ok {
132 | return nil
133 | }
134 |
135 | vol, err := NewVolume(s, d, placementPeers)
136 | if err != nil {
137 | return err
138 | }
139 |
140 | s.mu.Lock()
141 | defer s.mu.Unlock()
142 |
143 | s.volumes[d.Id] = vol
144 |
145 | return nil
146 | }
147 |
148 | //Export exposes a volume directly via NBD without attachment
149 | func (s *Server) Export(volume string) {
150 | if s.nbd == nil {
151 | s.nbd = NewNBDServer(s, NBDDefaultPort, s.controller)
152 | }
153 |
154 | peers := s.GetPeers(s.PeerIDs())
155 |
156 | if vol, isLocal := s.volumes[s.volumes[volume].ID()]; isLocal {
157 | err := s.nbd.Attach(vol, peers)
158 | if err != nil {
159 | s.log.WithError(err).Error("failed to attach volume")
160 | }
161 |
162 | s.log.WithField("vol", volume).Info("Started NBD exposure")
163 | return
164 | }
165 |
166 | s.log.WithField("vol", volume).Warn("Refusing to export a volume we're not a member of")
167 | }
168 |
169 | func (s *Server) newLocalConn() (*Peer, error) {
170 |
171 | tlsconfig, _ := tlsConfig()
172 | sess, err := quic.DialAddr(s.LocalAddr().String(), tlsconfig, defaultQuicConfig())
173 | if err != nil {
174 | return nil, err
175 | }
176 |
177 | p := &Peer{
178 | PeerID: s.peerID,
179 | Status: PeerStatusConnected,
180 | RemoteAddr: s.LocalAddr(),
181 | Channels: map[string]*streamChannel{},
182 | conn: sess,
183 | }
184 |
185 | return p, nil
186 | }
187 |
--------------------------------------------------------------------------------
/pkg/sbs/stats.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "syscall"
5 |
6 | "github.com/tcfw/vpc/pkg/sbs/config"
7 | )
8 |
9 | //BlockStoreStats provides OS status on the block store/volume
10 | type BlockStoreStats struct {
11 | Available uint64
12 | Used uint64
13 | Allocated uint64
14 | }
15 |
16 | //BlockStoreStats provides status for the block store overall
17 | func (s *Server) BlockStoreStats() (*BlockStoreStats, error) {
18 | stats := &BlockStoreStats{}
19 |
20 | //Allocated space from volume sizes
21 | for _, vol := range s.volumes {
22 | volSize, err := vol.Blocks.SizeOnDisk()
23 | if err != nil {
24 | return nil, err
25 | }
26 |
27 | stats.Allocated += uint64(vol.Size() * VolDescSizeMultiplier)
28 | stats.Used += volSize
29 | }
30 |
31 | available, err := s.blockStoreAvailability()
32 | if err == nil {
33 | stats.Available = available
34 | }
35 |
36 | return stats, nil
37 | }
38 |
39 | //blockStoreAvailability gets the OS' interpretation of available space
40 | //for the block store dir
41 | func (s *Server) blockStoreAvailability() (uint64, error) {
42 | var fStats syscall.Statfs_t
43 | if err := syscall.Statfs(config.BlockStoreDir(), &fStats); err != nil {
44 | return 0, err
45 | }
46 |
47 | return uint64(fStats.Bsize) * fStats.Bavail, nil
48 | }
49 |
50 | //BlockVolumeStats provides status for specific volumes
51 | func (s *Server) BlockVolumeStats(vol *Volume) (*BlockStoreStats, error) {
52 | stats := &BlockStoreStats{}
53 |
54 | stats.Allocated = uint64(vol.Size() * VolDescSizeMultiplier)
55 | available, err := s.blockStoreAvailability()
56 | if err == nil {
57 | stats.Available = available
58 | }
59 | used, err := vol.Blocks.SizeOnDisk()
60 | if err != nil {
61 | return nil, err
62 | }
63 | stats.Used = used
64 |
65 | return stats, nil
66 | }
67 |
--------------------------------------------------------------------------------
/pkg/sbs/test_helpers.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "crypto/sha256"
5 | "fmt"
6 | "math/rand"
7 | "os"
8 | "time"
9 |
10 | "github.com/hashicorp/raft"
11 | "github.com/tcfw/vpc/pkg/sbs/config"
12 | "github.com/tcfw/vpc/pkg/sbs/control"
13 | )
14 |
15 | //randomID generates a random ID to be used for a server
16 | func randomID() string {
17 | h := sha256.New()
18 | h.Write([]byte(time.Now().String()))
19 | return fmt.Sprintf("%x", h.Sum(nil))
20 | }
21 |
22 | //getLeader waits for a nd gets the current raft leader
23 | func getLeader(servers []*Server) *Server {
24 | var id string
25 |
26 | upper:
27 | for {
28 | for _, srv := range servers {
29 | leaderID := srv.Volume("vol-test").Raft.Leader()
30 | if leaderID != "" {
31 | id = string(leaderID)
32 | break upper
33 | }
34 | }
35 | time.Sleep(10 * time.Millisecond)
36 | }
37 |
38 | for _, srv := range servers {
39 | if srv.PeerID() == id {
40 | return srv
41 | }
42 | }
43 |
44 | return nil
45 | }
46 |
47 | //getNotLeader waits for a leader and then returns the first server
48 | //that is not the leader
49 | func getNotLeader(servers []*Server) *Server {
50 | var id string
51 |
52 | //Actually find a leader or at least make sure one exists
53 | upper:
54 | for {
55 | for _, srv := range servers {
56 | leaderID := srv.Volume("vol-test").Raft.Leader()
57 | if leaderID != "" {
58 | id = string(leaderID)
59 | break upper
60 | }
61 | }
62 | time.Sleep(10 * time.Millisecond)
63 | }
64 |
65 | for _, srv := range servers {
66 | if srv.PeerID() != id {
67 | return srv
68 | }
69 | }
70 |
71 | return nil
72 | }
73 |
74 | //waitForNPeers waits until the required number of raft peers is connected together
75 | func waitForNPeers(n int, obsCh <-chan raft.Observation) {
76 | atleastOne := make(chan struct{})
77 |
78 | go func() {
79 | for ch := range obsCh {
80 | if !ch.Data.(raft.PeerObservation).Removed {
81 | atleastOne <- struct{}{}
82 | }
83 | }
84 | }()
85 |
86 | for i := n; i > 0; i-- {
87 | <-atleastOne
88 | }
89 | }
90 |
91 | //makePeerObserver constructs a new raft observer that is filtered to peer observations
92 | func makePeerObserver() (<-chan raft.Observation, *raft.Observer) {
93 | obsCh := make(chan raft.Observation, 10)
94 | observer := raft.NewObserver(obsCh, false, func(o *raft.Observation) bool {
95 | _, isPeerOb := o.Data.(raft.PeerObservation)
96 | return isPeerOb
97 | })
98 |
99 | return obsCh, observer
100 | }
101 |
102 | //makeTestCluster creates a small set of servers on random ports and connects them all together
103 | //in a star topology
104 | func makeTestCluster(n int, controller control.Controller) ([]string, []*Server, error) {
105 | ids, servers, err := makeTestDisconnectedCluster(n, controller)
106 | if err != nil {
107 | return nil, nil, err
108 | }
109 |
110 | //Fully connect all peers
111 | for _, srv := range servers {
112 | for _, peerSrv := range servers {
113 | if srv.PeerID() == peerSrv.PeerID() {
114 | continue
115 | }
116 | if err := srv.AddPeer(peerSrv.PeerID(), peerSrv.LocalAddr()); err != nil {
117 | return nil, nil, err
118 | }
119 | }
120 | }
121 |
122 | return ids, servers, nil
123 | }
124 |
125 | //makeTestDisconnectedCluster creates a small set of servers on random ports, but does not connect them to each other
126 | func makeTestDisconnectedCluster(n int, controller control.Controller) ([]string, []*Server, error) {
127 | ids := []string{}
128 | servers := []*Server{}
129 |
130 | for i := 0; i < n; i++ {
131 | id := randomID()
132 | srv := NewServerWithPort(rand.Intn(1750) + 34000)
133 | srv.SetPeerID(id)
134 | srv.SetController(controller)
135 |
136 | if err := srv.Listen(); err != nil {
137 | return nil, nil, err
138 | }
139 |
140 | ids = append(ids, id)
141 | servers = append(servers, srv)
142 | }
143 |
144 | return ids, servers, nil
145 | }
146 |
147 | //cleanUpServerData deletes all local data related to a test cluster
148 | func cleanUpServerData(servers []*Server) {
149 | for _, serv := range servers {
150 | os.RemoveAll(fmt.Sprintf("%s/%s", config.BlockStoreDir(), serv.PeerID()))
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/pkg/sbs/volumes.go:
--------------------------------------------------------------------------------
1 | package sbs
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | badger "github.com/dgraph-io/badger/v2"
8 | badgerOptions "github.com/dgraph-io/badger/v2/options"
9 | "github.com/hashicorp/raft"
10 | "github.com/sirupsen/logrus"
11 |
12 | volumesAPI "github.com/tcfw/vpc/pkg/api/v1/volumes"
13 | "github.com/tcfw/vpc/pkg/sbs/config"
14 | "github.com/tcfw/vpc/pkg/utils"
15 | )
16 |
17 | //Volume instance
18 | type Volume struct {
19 | id string
20 | PlacementPeers map[string]struct{}
21 | Blocks *BlockStore
22 | Raft *raft.Raft
23 | Transport *RaftTransport
24 | server *Server
25 | desc *volumesAPI.Volume
26 | logStore *badger.DB
27 |
28 | mu sync.Mutex
29 | log *logrus.Logger
30 | }
31 |
32 | //NewVolume constructs a raft-backed distributed volume
33 | func NewVolume(s *Server, d *volumesAPI.Volume, peers []string) (*Volume, error) {
34 | transport := NewRaftTransport(s, d.Id)
35 | bs := NewBlockStore(s.PeerID(), d, config.BlockStoreDir(), utils.DefaultLogger())
36 |
37 | options := badger.DefaultOptions(fmt.Sprintf("%s/log", bs.BaseDir))
38 | options.SyncWrites = false
39 | options.ZSTDCompressionLevel = 0
40 | options.BlockSize = 16 * 1 << 10
41 | options.WithCompression(badgerOptions.None)
42 | options.WithMaxCacheSize(6400)
43 | options.WithMaxBfCacheSize(6400)
44 | logBadger, err := badger.Open(options)
45 | if err != nil {
46 | return nil, err
47 | }
48 | // logStore := NewLogStore(d.Id, logBadger)
49 | logStore := raft.NewInmemStore()
50 | cachedLogStore, err := raft.NewLogCache(3000, logStore)
51 | if err != nil {
52 | return nil, err
53 | }
54 |
55 | r, err := NewRaft(s, bs, cachedLogStore, transport)
56 | if err != nil {
57 | return nil, err
58 | }
59 |
60 | vol := &Volume{
61 | id: d.Id,
62 | PlacementPeers: map[string]struct{}{},
63 | desc: d,
64 | Blocks: bs,
65 | Transport: transport,
66 | server: s,
67 | logStore: logBadger,
68 | Raft: r,
69 | log: utils.DefaultLogger(),
70 | }
71 |
72 | if err := vol.init(peers); err != nil {
73 | vol.Raft.Shutdown()
74 | return nil, err
75 | }
76 |
77 | go vol.watchLeaderChange()
78 |
79 | return vol, nil
80 | }
81 |
82 | //ID of the volume
83 | func (v *Volume) ID() string {
84 | return v.id
85 | }
86 |
87 | //Size of the volume in GB
88 | func (v *Volume) Size() int64 {
89 | return v.desc.Size
90 | }
91 |
92 | func (v *Volume) watchLeaderChange() {
93 | for l := range v.Raft.LeaderCh() {
94 | if l {
95 | v.log.Info("Became leader of ", v.id)
96 | } else {
97 | v.log.Info("Lost leadership of ", v.id)
98 | }
99 | }
100 | }
101 |
102 | //Shutdown closes the volume
103 | func (v *Volume) Shutdown() error {
104 | v.Raft.Shutdown().Error()
105 | v.logStore.Close()
106 | v.Blocks.Close()
107 | return nil
108 | }
109 |
110 | func (v *Volume) init(peers []string) error {
111 | bootConfig := raft.Configuration{
112 | Servers: []raft.Server{{
113 | Suffrage: raft.Voter,
114 | ID: raft.ServerID(v.server.PeerID()),
115 | Address: raft.ServerAddress(v.server.PeerID()),
116 | }},
117 | }
118 |
119 | for _, peer := range peers {
120 | v.server.GetPeer(peer)
121 | if peer == v.server.PeerID() {
122 | continue
123 | }
124 |
125 | v.PlacementPeers[peer] = struct{}{}
126 |
127 | bootConfig.Servers = append(bootConfig.Servers, raft.Server{
128 | Suffrage: raft.Voter,
129 | ID: raft.ServerID(peer),
130 | Address: raft.ServerAddress(peer),
131 | })
132 |
133 | v.log.WithField("vol", v.id).Debugf("added peer %s", peer)
134 | }
135 |
136 | //Ignore bootstrap errors
137 | v.Raft.BootstrapCluster(bootConfig).Error()
138 |
139 | return nil
140 | }
141 |
142 | func (v *Volume) writePTable() error {
143 |
144 | return nil
145 | }
146 |
147 | //AddPeer adds a placment peer where the volume is expected to reside
148 | func (v *Volume) AddPeer(p *Peer) error {
149 | v.mu.Lock()
150 | defer v.mu.Unlock()
151 |
152 | f := v.Raft.AddVoter(raft.ServerID(p.ID()), raft.ServerAddress(""), 0, 0)
153 | if err := f.Error(); err != nil {
154 | return err
155 | }
156 |
157 | v.PlacementPeers[p.ID()] = struct{}{}
158 | return nil
159 | }
160 |
161 | //RemovePeer adds a placment peer where the volume is expected to reside
162 | func (v *Volume) RemovePeer(p *Peer) error {
163 | v.mu.Lock()
164 | defer v.mu.Unlock()
165 |
166 | f := v.Raft.RemovePeer(raft.ServerAddress(p.ID()))
167 | if err := f.Error(); err != nil {
168 | return err
169 | }
170 |
171 | delete(v.PlacementPeers, p.ID())
172 | return nil
173 | }
174 |
--------------------------------------------------------------------------------
/pkg/utils/logs.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 |
6 | log "github.com/sirupsen/logrus"
7 | "github.com/spf13/viper"
8 | )
9 |
10 | var (
11 | defaultLogger *log.Logger
12 | )
13 |
14 | //DefaultLogger provides the main log instance
15 | func DefaultLogger() *log.Logger {
16 | if defaultLogger != nil {
17 | return defaultLogger
18 | }
19 | defaultLogger = log.New()
20 |
21 | defaultLogger.SetOutput(os.Stdout)
22 | logLevel := viper.GetInt("LogLevel")
23 | if logLevel >= 5 {
24 | defaultLogger.SetLevel(log.TraceLevel)
25 | } else if logLevel >= 4 {
26 | defaultLogger.SetLevel(log.DebugLevel)
27 | } else if logLevel >= 3 {
28 | defaultLogger.SetLevel(log.WarnLevel)
29 | } else if logLevel >= 2 {
30 | defaultLogger.SetLevel(log.ErrorLevel)
31 | } else if logLevel >= 1 {
32 | defaultLogger.SetLevel(log.FatalLevel)
33 | } else if logLevel >= 0 {
34 | defaultLogger.SetLevel(log.PanicLevel)
35 | }
36 | if logLevel > 3 {
37 | defaultLogger.Infof("Log level set %v", defaultLogger.GetLevel())
38 | }
39 |
40 | return defaultLogger
41 | }
42 |
--------------------------------------------------------------------------------
/pkg/utils/machinery.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "github.com/denisbrodbeck/machineid"
5 | )
6 |
7 | //MachineID gets the static machine ID
8 | func MachineID() string {
9 | id, _ := machineid.ProtectedID("VPC")
10 | return id
11 | }
12 |
--------------------------------------------------------------------------------
/pkg/utils/utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "os"
5 | "os/signal"
6 | "syscall"
7 | )
8 |
9 | //BlockUntilSigTerm waits until the application receives a SIGTERM
10 | //usually sent to the application via ctrl+c
11 | func BlockUntilSigTerm() {
12 | c := make(chan os.Signal)
13 | signal.Notify(c, os.Interrupt, syscall.SIGTERM)
14 | <-c
15 | }
16 |
--------------------------------------------------------------------------------
/pkg/vpc/cmd/cmd.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | )
6 |
7 | // NewDefaultCommand creates the `l2` command and its nested children.
8 | func NewDefaultCommand() *cobra.Command {
9 | // Parent command to which all subcommands are added.
10 | cmds := &cobra.Command{
11 | Use: "vpc",
12 | Short: "vpc",
13 | Long: `vpc providers vpc and subnet configuration`,
14 | Run: func(cmd *cobra.Command, args []string) {
15 | cmd.Help()
16 | },
17 | }
18 |
19 | cmds.AddCommand(NewServeCmd())
20 | cmds.AddCommand(NewMigrateCmd())
21 |
22 | return cmds
23 | }
24 |
--------------------------------------------------------------------------------
/pkg/vpc/cmd/migrate.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/spf13/cobra"
7 | "github.com/tcfw/vpc/pkg/vpc"
8 | )
9 |
10 | //NewMigrateCmd provides a command to update schema
11 | func NewMigrateCmd() *cobra.Command {
12 | cmd := &cobra.Command{
13 | Use: "migrate",
14 | Short: "Runs all schema migrations",
15 | Run: func(cmd *cobra.Command, args []string) {
16 | db, err := vpc.DBConn()
17 | if err != nil {
18 | log.Fatalf("Failed to open DB: %s", err)
19 | }
20 |
21 | path, _ := cmd.Flags().GetString("dir")
22 |
23 | if err := vpc.Migrate(db, path); err != nil {
24 | log.Fatalln(err)
25 | }
26 | },
27 | }
28 |
29 | cmd.Flags().StringP("dir", "d", "./migrations", "Directory of migrations to run")
30 |
31 | return cmd
32 | }
33 |
--------------------------------------------------------------------------------
/pkg/vpc/cmd/serve.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "github.com/spf13/cobra"
5 | "github.com/tcfw/vpc/pkg/vpc"
6 | )
7 |
8 | //NewServeCmd provides a command to delete vpcs
9 | func NewServeCmd() *cobra.Command {
10 | cmd := &cobra.Command{
11 | Use: "serve",
12 | Short: "Starts the vpc api service",
13 | Run: func(cmd *cobra.Command, args []string) {
14 | port, _ := cmd.Flags().GetUint("port")
15 | vpc.Serve(port)
16 | },
17 | }
18 |
19 | cmd.Flags().UintP("port", "p", 18254, "GRPC port")
20 |
21 | return cmd
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/vpc/db.go:
--------------------------------------------------------------------------------
1 | package vpc
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "io/ioutil"
7 | "log"
8 | "os"
9 | "time"
10 |
11 | //postgresql DB driver
12 | _ "github.com/lib/pq"
13 | )
14 |
15 | const (
16 | dbName = "vpc"
17 | )
18 |
19 | //DBConn opens a connection to a postgresql db server
20 | //and attempts to validate the connection
21 | func DBConn() (*sql.DB, error) {
22 | dbEP := os.Getenv("DB")
23 | if dbEP == "" {
24 | dbEP = "postgresql://localhost:26257"
25 | }
26 |
27 | db, err := sql.Open("postgres", dbEP)
28 | if err != nil {
29 | return nil, err
30 | }
31 |
32 | if err := db.Ping(); err != nil {
33 | return nil, err
34 | }
35 |
36 | return db, nil
37 | }
38 |
39 | type migration struct {
40 | file string
41 | migratedAt time.Time
42 | }
43 |
44 | //Migrate runs through each sql file in the migrations folder
45 | //and attempts to execute it in the DB
46 | func Migrate(db *sql.DB, dir string) error {
47 | statements := []string{
48 | `CREATE DATABASE IF NOT EXISTS ` + dbName,
49 | `CREATE TABLE IF NOT EXISTS ` + dbName + `.migrations (
50 | file STRING,
51 | migrated_at TIMESTAMP,
52 | PRIMARY KEY (file)
53 | )`,
54 | }
55 |
56 | for _, stmt := range statements {
57 | if _, err := db.Exec(stmt); err != nil {
58 | return err
59 | }
60 | }
61 |
62 | files, err := ioutil.ReadDir(dir)
63 | if err != nil {
64 | log.Fatal(err)
65 | }
66 |
67 | migratedRows, err := db.Query("select file from migrations;")
68 | if err != nil {
69 | log.Fatal(err)
70 | }
71 | defer migratedRows.Close()
72 |
73 | migrations := map[string]bool{}
74 | if migratedRows != nil {
75 | for migratedRows.Next() {
76 | var file string
77 | if err := migratedRows.Scan(&file); err != nil {
78 | // Check for a scan error.
79 | // Query rows will be closed with defer.
80 | log.Fatal(err)
81 | }
82 | migrations[file] = true
83 | }
84 | }
85 |
86 | if len(files) == len(migrations) {
87 | log.Println("Nothing to migrate")
88 | return nil
89 | }
90 |
91 | log.Println("Starting migrations...")
92 |
93 | tx, err := db.BeginTx(context.Background(), nil)
94 | if err != nil {
95 | return err
96 | }
97 |
98 | for _, f := range files {
99 | if _, ok := migrations[f.Name()]; ok {
100 | continue
101 | }
102 |
103 | log.Printf("Running %s...\n", f.Name())
104 |
105 | migBytes, err := ioutil.ReadFile(dir + f.Name())
106 | if err != nil {
107 | tx.Rollback()
108 | return err
109 | }
110 |
111 | if _, err := tx.Exec(string(migBytes)); err != nil {
112 | tx.Rollback()
113 | return err
114 | }
115 |
116 | if _, err := tx.Exec(`insert into `+dbName+`.migrations VALUES ($1, NOW())`, f.Name()); err != nil {
117 | tx.Rollback()
118 | return err
119 | }
120 | }
121 |
122 | return tx.Commit()
123 | }
124 |
--------------------------------------------------------------------------------
/pkg/vpc/migrations/201911210823_create_vpc.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE vpcs (
2 | id SERIAL PRIMARY KEY,
3 | account_id INT NOT NULL,
4 | cidr INET NOT NULL,
5 | asn INT NOT NULL,
6 | vni INT NOT NULL,
7 | created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
8 | metadata JSONB
9 | );
--------------------------------------------------------------------------------
/pkg/vpc/migrations/201911211000_create_subnet.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE subnets (
2 | id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3 | vpc_id INT NOT NULL,
4 | region STRING NOT NULL,
5 | cidr INET NOT NULL,
6 | inner_vlan INT NOT NULL,
7 | created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
8 | metadata JSONB
9 | );
--------------------------------------------------------------------------------
/scripts/protoc-gen.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | services='machinery vpc l2 hyper volumes l2Controller'
4 |
5 | for service in ${services}
6 | do
7 | echo "Generating service ${service}...";
8 | mkdir -p pkg/api/v1/${service}
9 | protoc --proto_path=api/proto/v1 --go_out=pkg/api/v1/${service} --go-grpc_out=pkg/api/v1/${service} ${service}.proto
10 | protoc --proto_path=api/proto/v1 --swagger_out=logtostderr=true:api/swagger/v1 ${service}.proto
11 | done
--------------------------------------------------------------------------------