Skip to content

Commit a1706a4

Browse files
authored
Make lcl cli be able to bind to device and various improvements (#1)
1 parent f24196b commit a1706a4

15 files changed

+206
-227
lines changed

Package.swift

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,29 @@ let package = Package(
99
.macOS(.v11)
1010
],
1111
dependencies: [
12-
.package(url: "https://github.com/Local-Connectivity-Lab/lcl-ping.git", from: "1.0.0"),
12+
.package(url: "https://github.com/Local-Connectivity-Lab/lcl-ping.git", from: "1.0.3"),
1313
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.3"),
14-
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.6"),
1514
.package(url: "https://github.com/scottrhoyt/SwiftyTextTable.git", from: "0.9.0"),
16-
.package(url: "https://github.com/johnnzhou/lcl-auth.git", branch: "main"),
17-
.package(url: "https://github.com/apple/swift-nio.git", from: "2.63.0"),
15+
.package(url: "https://github.com/Local-Connectivity-Lab/lcl-auth.git", branch: "main"),
16+
.package(url: "https://github.com/apple/swift-nio.git", from: "2.73.0"),
1817
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.25.0"),
19-
.package(url: "https://github.com/johnnzhou/lcl-speedtest.git", branch: "main"),
20-
.package(url: "https://github.com/pakLebah/ANSITerminal", from: "0.0.3")
18+
.package(url: "https://github.com/Local-Connectivity-Lab/lcl-speedtest.git", from: "1.0.4"),
19+
.package(url: "https://github.com/johnnzhou/ANSITerminal.git", from: "0.0.4"),
2120
],
2221
targets: [
2322
.executableTarget(
2423
name: "lcl",
2524
dependencies: [
2625
.product(name: "ArgumentParser", package: "swift-argument-parser"),
2726
.product(name: "LCLPing", package: "lcl-ping"),
28-
.product(name: "Yams", package: "Yams"),
2927
.product(name: "SwiftyTextTable", package: "SwiftyTextTable"),
3028
.product(name: "LCLAuth", package: "lcl-auth"),
3129
.product(name: "NIO", package: "swift-nio"),
3230
.product(name: "NIOPosix", package: "swift-nio"),
3331
.product(name: "NIOCore", package: "swift-nio"),
3432
.product(name: "NIOHTTP1", package: "swift-nio"),
3533
.product(name: "NIOSSL", package: "swift-nio-ssl"),
36-
.product(name: "LCLSpeedTest", package: "lcl-speedtest"),
34+
.product(name: "LCLSpeedtest", package: "lcl-speedtest"),
3735
.product(name: "ANSITerminal", package: "ANSITerminal")
3836
],
3937
path: "Sources")

README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,42 @@
33
</div>
44

55
---
6-
# lcl CLI
6+
# LCL CLI
7+
LCL CLI is a cross-platform cli tool written in Swift. It is designed to measure the network performance and latency through LCL's cellular network measurement testbed. While this tool is design for Local Connectivity Lab researchers and Seattle Community Network volunteers and users, everyone is welcome to use this tool to measure their network performance.
8+
9+
## Usage
10+
11+
```
12+
OVERVIEW: A command-line tool from Local Connectivity Lab @UWCSE
13+
14+
USAGE: lcl <subcommand>
15+
16+
OPTIONS:
17+
-h, --help Show help information.
18+
19+
SUBCOMMANDS:
20+
register Register with SCN server to report test data.
21+
ping Run Ping Latency and Reachability Test.
22+
speedtest Run speedtest using the NDT test infrastructure.
23+
measure Run SCN test suite and optionally report the measurement result to SCN.
24+
interfaces List available network interfaces on the machine.
25+
cellular-sites Get info on SCN cellular sites
26+
27+
See 'lcl help <subcommand>' for detailed help.
28+
```
29+
30+
## Features
31+
- ICMP and HTTP test with `Server-Timing` support.
32+
- Speedtest on top of NDT7 protocol with TCP-level and Application-level measurement
33+
- Automatically upload test result to SCN's backend server (this option is available to SCN users and volunteers).
34+
- Check available interfaces on the machine.
35+
36+
## Platform Support
37+
LCL CLI is designed to support various platforms that Swift supports, including Linux (Debian and Ubuntu), macOS. Those who are interested in other platforms can download and compile the binary from the source.
38+
39+
40+
## Contributing
41+
Any contribution and pull requests are welcome! However, before you plan to implement some features or try to fix an uncertain issue, it is recommended to open a discussion first. You can also join our [Discord channel](https://discord.com/invite/gn4DKF83bP), or visit our [website](https://seattlecommunitynetwork.org/).
42+
43+
## License
44+
LCL CLI is released under Apache License. See [LICENSE](/LICENSE) for more details.

Sources/Command/Interfaces.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
//
1212

1313
import ArgumentParser
14-
import LCLPing
14+
import NIOCore
1515

1616
struct NetworkInterfaceCommand: AsyncParsableCommand {
1717
static var configuration: CommandConfiguration = CommandConfiguration(
@@ -24,7 +24,7 @@ struct NetworkInterfaceCommand: AsyncParsableCommand {
2424
for interface in availableInterfaces {
2525
let sortedHostNames = interface.value.sorted()
2626
let interfaceName = interface.key
27-
print("\(interfaceName) \(sortedHostNames)")
27+
print("\(interfaceName)\t\(sortedHostNames)")
2828
}
2929
}
3030
}

Sources/Command/Measure.swift

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ import ArgumentParser
1515
import LCLPing
1616
import LCLAuth
1717
import Crypto
18+
import LCLSpeedtest
1819

1920
extension LCLCLI {
2021
struct MeasureCommand: AsyncParsableCommand {
2122

23+
@Option(name: .long, help: "Specify the device name to which the data will be sent.")
24+
var deviceName: String?
25+
2226
@Option(name: .shortAndLong, help: "Show datapoint on SCN's public visualization. Your contribution will help others better understand our coverage.")
2327
var showData: Bool = false
2428

@@ -36,6 +40,7 @@ extension LCLCLI {
3640
let result: Result<[CellularSite]?, CLIError> = await NetworkingAPI.get(from: NetworkingAPI.Endpoint.site.url)
3741
switch result {
3842
case .failure(let error):
43+
3944
throw error
4045
case .success(let cs):
4146
if let s = cs {
@@ -47,24 +52,16 @@ extension LCLCLI {
4752
}
4853
var picker = Picker<CellularSite>(title: "Choose the cellular site you are currently at.", options: sites)
4954

50-
let homeURL = FileIO.default.home.appendingPathComponent(".lcl")
55+
let homeURL = FileIO.default.home.appendingPathComponent(Constants.cliDirectory)
5156
let skURL = homeURL.appendingPathComponent("sk")
5257
let sigURL = homeURL.appendingPathComponent("sig")
5358
let rURL = homeURL.appendingPathComponent("r")
5459
let hpkrURL = homeURL.appendingPathComponent("hpkr")
55-
let keyURL = homeURL.appendingPathComponent("key")
56-
let keyData = try loadData(keyURL)
57-
58-
let symmetricKeyRecovered = SymmetricKey(data: keyData)
59-
60-
let skDataEncrypted = try loadData(skURL)
61-
let skData = try decrypt(cipher: skDataEncrypted, key: symmetricKeyRecovered)
62-
let sigDataEncrypted = try loadData(sigURL)
63-
let sigData = try decrypt(cipher: sigDataEncrypted, key: symmetricKeyRecovered)
64-
let rDataEncrypted = try loadData(rURL)
65-
let rData = try decrypt(cipher: rDataEncrypted, key: symmetricKeyRecovered)
66-
let hpkrDataEncrypted = try loadData(hpkrURL)
67-
let hpkrData = try decrypt(cipher: hpkrDataEncrypted, key: symmetricKeyRecovered)
60+
61+
let skData = try loadData(skURL)
62+
let sigData = try loadData(sigURL)
63+
let rData = try loadData(rURL)
64+
let hpkrData = try loadData(hpkrURL)
6865
let validationResultJSON = try encoder.encode(ValidationResult(R: rData, skT: skData, hPKR: hpkrData))
6966

7067
let ecPrivateKey = try ECDSA.deserializePrivateKey(raw: skData)
@@ -73,7 +70,7 @@ extension LCLCLI {
7370
throw CLIError.contentCorrupted
7471
}
7572

76-
let pingConfig = try ICMPPingClient.Configuration(endpoint: .ipv4("google.com", 0))
73+
let pingConfig = try ICMPPingClient.Configuration(endpoint: .ipv4("google.com", 0), deviceName: deviceName)
7774
let outputFormats: Set<OutputFormat> = [.default]
7875

7976
let client = ICMPPingClient(configuration: pingConfig)
@@ -98,9 +95,12 @@ extension LCLCLI {
9895

9996
let summary = try await client.start().get()
10097

101-
let speedTestResults = try await speedTest.run()
102-
let downloadSummary = prepareSpeedTestSummary(data: speedTestResults.download, unit: .Mbps)
103-
let uploadSummary = prepareSpeedTestSummary(data: speedTestResults.upload, unit: .Mbps)
98+
let speedTestResults = try await speedTest.run(deviceName: deviceName)
99+
let downloadMeasurement = computeLatencyAndRetransmission(speedTestResults.downloadTCPMeasurement, for: .download)
100+
let uploadMeasurement = computeLatencyAndRetransmission(speedTestResults.uploadTCPMeasurement, for: .upload)
101+
102+
let downloadSummary = prepareSpeedTestSummary(data: speedTestResults.downloadSpeed, tcpInfos: speedTestResults.downloadTCPMeasurement, for: .download, unit: .Mbps)
103+
let uploadSummary = prepareSpeedTestSummary(data: speedTestResults.uploadSpeed, tcpInfos: speedTestResults.uploadTCPMeasurement, for: .upload, unit: .Mbps)
104104

105105
generatePingSummary(summary, for: .icmp, formats: outputFormats)
106106
generateSpeedTestSummary(downloadSummary, kind: .download, formats: outputFormats, unit: .Mbps)
@@ -116,10 +116,10 @@ extension LCLCLI {
116116
uploadSpeed: uploadSummary.avg,
117117
latitude: selectedSite.latitude,
118118
longitude: selectedSite.longitude,
119-
packetLoss: Double(summary.timeout.count) / Double(summary.totalCount),
120-
ping: summary.avg,
119+
packetLoss: (downloadMeasurement.retransmit + uploadMeasurement.retransmit) / 2,
120+
ping: (downloadMeasurement.latency + uploadMeasurement.latency) / 2,
121121
timestamp: Date.getCurrentTime(),
122-
jitter: summary.jitter
122+
jitter: (downloadMeasurement.variance + uploadMeasurement.variance) / 2
123123
)
124124
let serialized = try encoder.encode(report)
125125
let sig_m = try ECDSA.sign(message: serialized, privateKey: ECDSA.deserializePrivateKey(raw: skData))

Sources/Command/PingCommand.swift

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ extension LCLCLI {
4949
@Flag(help: "Export the Ping result in JSON format.")
5050
var json: Bool = false
5151

52-
@Flag(help: "Export the Ping result in YAML format.")
53-
var yaml: Bool = false
54-
5552
static var configuration: CommandConfiguration = CommandConfiguration(
5653
commandName: "icmp",
5754
abstract: "Run ICMP Ping Latency and Reachability Test."
@@ -78,10 +75,6 @@ extension LCLCLI {
7875
outputFormats.insert(.json)
7976
}
8077

81-
if yaml {
82-
outputFormats.insert(.yaml)
83-
}
84-
8578
if outputFormats.isEmpty {
8679
outputFormats.insert(.default)
8780
}
@@ -131,19 +124,19 @@ extension LCLCLI {
131124
@Flag(help: "Use URLSession on Apple platform for underlying networking and measurement. This flag has no effect on Linux platform.")
132125
var useURLSession: Bool = false
133126

127+
@Option(name: .long, help: "Specify the device name to which the data will be sent.")
128+
var deviceName: String?
129+
134130
@Flag(help: "Export the Ping result in JSON format.")
135131
var json: Bool = false
136132

137-
@Flag(help: "Export the Ping result in YAML format.")
138-
var yaml: Bool = false
139-
140133
static var configuration: CommandConfiguration = CommandConfiguration(
141134
commandName: "http",
142135
abstract: "Run ICMP Ping Latency and Reachability Test."
143136
)
144137

145138
func run() throws {
146-
var config = try HTTPPingClient.Configuration(url: url)
139+
var config = try HTTPPingClient.Configuration(url: url, deviceName: deviceName)
147140
if let count = count {
148141
config.count = Int(count)
149142
}
@@ -177,10 +170,6 @@ extension LCLCLI {
177170
outputFormats.insert(.json)
178171
}
179172

180-
if yaml {
181-
outputFormats.insert(.yaml)
182-
}
183-
184173
if outputFormats.isEmpty {
185174
outputFormats.insert(.default)
186175
}

Sources/Command/RegisterCommand.swift

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@ extension LCLCLI {
2121
@Option(name: .shortAndLong, help: "Path to the SCN credential file given by the SCN administrator.")
2222
var filePath: String
2323

24-
static let configuration = CommandConfiguration(
25-
commandName: "register",
26-
abstract: "Register with SCN server to report test data."
27-
)
24+
static let configuration = CommandConfiguration(commandName: "register",
25+
abstract: "Register with SCN server to report test data.")
2826

2927
func run() async throws {
3028
guard let filePathURL = URL(string: filePath), let credentialCode = try FileIO.default.loadFrom(filePathURL) else {
3129
throw CLIError.failedToReadFile("Fail to read content from path '\(filePath)'. Exit.")
3230
}
33-
let homeURL = FileIO.default.home.appendingPathComponent(".lcl")
31+
32+
let homeURL = FileIO.default.home.appendingPathComponent(Constants.cliDirectory)
3433
let skURL = homeURL.appendingPathComponent("sk")
3534
let sigURL = homeURL.appendingPathComponent("sig")
3635
let rURL = homeURL.appendingPathComponent("r")
@@ -79,14 +78,14 @@ extension LCLCLI {
7978
let sk_t = try ECDSA.deserializePrivateKey(raw: validationResult.skT)
8079
let pk_t = ECDSA.derivePublicKey(from: sk_t)
8180
outputData.append(pk_t.derRepresentation)
82-
let h_sec = digest(data: outputData, algorithm: .SHA256)
81+
var h_sec = digest(data: outputData, algorithm: .SHA256)
8382
outputData.removeAll()
8483
outputData.append(validationResult.hPKR)
8584
outputData.append(h_sec)
86-
let h_concat = Data(outputData)
87-
let sigma_r = try ECDSA.sign(message: h_concat, privateKey: sk_t)
85+
var h_concat = Data(outputData)
86+
var sigma_r = try ECDSA.sign(message: h_concat, privateKey: sk_t)
8887
let registration = RegistrationModel(sigmaR: sigma_r.hex, h: h_concat.hex, R: validationResult.R.hex)
89-
let registrationJson = try encoder.encode(registration)
88+
var registrationJson = try encoder.encode(registration)
9089
switch await NetworkingAPI.send(to: NetworkingAPI.Endpoint.register.url, using: registrationJson) {
9190
case .success:
9291
print("Registration complete!")
@@ -97,24 +96,18 @@ extension LCLCLI {
9796
let validationJson = try encoder.encode(validationResult)
9897
let privateKey = try ECDSA.deserializePrivateKey(raw: validationResult.skT)
9998
let signature = try ECDSA.sign(message: validationJson, privateKey: privateKey)
100-
101-
let symmetricKey = SymmetricKey(size: .bits256)
102-
try encryptAndWriteData(validationResult.R, to: rURL, using: symmetricKey)
103-
try encryptAndWriteData(validationResult.hPKR, to: hpkrURL, using: symmetricKey)
104-
try encryptAndWriteData(validationResult.skT, to: skURL, using: symmetricKey)
105-
try encryptAndWriteData(signature, to: sigURL, using: symmetricKey)
106-
107-
let symmetricKeyData = symmetricKey.withUnsafeBytes { pointer in
108-
return Data(pointer)
109-
}
110-
111-
try FileIO.default.write(data: symmetricKeyData, to: keyURL)
112-
}
113-
114-
private func encryptAndWriteData(_ data: Data, to fileURL: URL, using key: SymmetricKey) throws {
115-
var encrypted = try LCLAuth.encrypt(plainText: data, key: key)
116-
try FileIO.default.write(data: encrypted, to: fileURL)
117-
encrypted.removeAll()
99+
100+
try FileIO.default.write(data: validationResult.R, to: rURL)
101+
try FileIO.default.write(data: validationResult.hPKR, to: hpkrURL)
102+
try FileIO.default.write(data: validationResult.skT, to: skURL)
103+
try FileIO.default.write(data: signature, to: sigURL)
104+
105+
// cleanup
106+
outputData.removeAll()
107+
h_concat.removeAll()
108+
sigma_r.removeAll()
109+
h_sec.removeAll()
110+
registrationJson.removeAll()
118111
}
119112
}
120113
}

Sources/Command/SpeedTestCommand.swift

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import Foundation
1414
import ArgumentParser
15-
import LCLSpeedTest
15+
import LCLSpeedtest
1616

1717
extension LCLCLI {
1818
struct SpeedTestCommand: AsyncParsableCommand {
@@ -23,12 +23,12 @@ extension LCLCLI {
2323
@Option(name: .shortAndLong, help: "Specify the direction of the test. A test can be of three types: download, upload or downloadAndUpload")
2424
var type: TestType
2525

26+
@Option(name: .long, help: "Specify the device name to which the data will be sent.")
27+
var deviceName: String?
28+
2629
@Flag(help: "Export the Speed Test result in JSON format.")
2730
var json: Bool = false
2831

29-
@Flag(help: "Export the Speed Test result in YAML format.")
30-
var yaml: Bool = false
31-
3232
static let configuration = CommandConfiguration(commandName: "speedtest", abstract: "Run speedtest using the NDT test infrastructure.")
3333

3434
func run() async throws {
@@ -47,27 +47,23 @@ extension LCLCLI {
4747
signal(SIGINT, SIG_IGN)
4848
let stopSignal = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
4949
stopSignal.setEventHandler {
50-
print("Exit from Speed Test")
50+
print("Exit from Speedtest")
5151
speedTest.stop()
5252
return
5353
}
5454

5555
stopSignal.resume()
5656

57-
let speedTestResults = try await speedTest.run()
57+
let speedTestResults = try await speedTest.run(deviceName: deviceName)
5858

59-
let downloadSummary = prepareSpeedTestSummary(data: speedTestResults.download, unit: speedTestUnit)
60-
let uploadSummary = prepareSpeedTestSummary(data: speedTestResults.upload, unit: speedTestUnit)
59+
let downloadSummary = prepareSpeedTestSummary(data: speedTestResults.downloadSpeed, tcpInfos: speedTestResults.downloadTCPMeasurement, for: .download, unit: speedTestUnit)
60+
let uploadSummary = prepareSpeedTestSummary(data: speedTestResults.uploadSpeed, tcpInfos: speedTestResults.uploadTCPMeasurement, for: .upload, unit: speedTestUnit)
6161

6262
var outputFormats: Set<OutputFormat> = []
6363
if json {
6464
outputFormats.insert(.json)
6565
}
6666

67-
if yaml {
68-
outputFormats.insert(.yaml)
69-
}
70-
7167
if outputFormats.isEmpty {
7268
outputFormats.insert(.default)
7369
}

0 commit comments

Comments
 (0)