Skip to content

Commit

Permalink
more work on bridge/VLAN Linux stack integration
Browse files Browse the repository at this point in the history
  • Loading branch information
lhoward committed Aug 5, 2024
1 parent a5d2387 commit 4ff23ba
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 48 deletions.
13 changes: 11 additions & 2 deletions Examples/nldump/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,17 @@ import NetLink
struct nldump {
public static func main() async throws {
let socket = try NLSocket(protocol: NETLINK_ROUTE)
for try await link in try await socket.getRtLinks() {
debugPrint("found link \(link)")
for try await link in try await socket.getLinks(family: sa_family_t(AF_BRIDGE)) {
debugPrint(
"@\(link.index) found link \(link) MTU \(link.mtu) master \(link.master) slaveOf \(link.slaveOf)"
)
if let bridge = link as? RTNLLinkBridge {
debugPrint(
" bridge flags \(bridge.bridgeFlags) pvid \(String(describing: bridge.bridgePVID)) hasVLAN \(bridge.bridgeHasVLAN) tagged \(String(describing: bridge.bridgeTaggedVLANs)) untagged \(String(describing: bridge.bridgeUntaggedVLANs))"
)
} else if let vlan = link as? RTNLLinkVLAN {
debugPrint(" vlan ID \(vlan.vlanID ?? 0) flags \(vlan.vlanFlags)")
}
}
}
}
2 changes: 1 addition & 1 deletion Examples/nlmonitor/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import NetLink
struct nlmonitor {
public static func main() async throws {
let socket = try NLSocket(protocol: NETLINK_ROUTE)
try socket.notifyRtLinks()
try socket.subscribeLinks()
for try await link in socket.notifications {
debugPrint("found link \(link)")
}
Expand Down
4 changes: 2 additions & 2 deletions Examples/portmon/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct portmon {
.arguments[1] : "br0"
)
print("Ports at startup on bridge \(bridge.name):")
for port in try await bridge.ports {
for port in try await bridge.getPorts() {
print("\(port)")
do {
try port.addFilter(for: groupAddress, etherType: etherType)
Expand All @@ -47,7 +47,7 @@ struct portmon {
}
group.addTask { @Sendable in
print("Now monitoring for packets...")
for port in try await bridge.ports {
for port in try await bridge.getPorts() {
do {
for try await packet in try await port.rxPackets {
print("received packet \(packet)\n\(packet.data.hexEncodedString())")
Expand Down
18 changes: 12 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,25 @@ let PlatformPackageDependencies: [Package.Dependency]
let PlatformTargetDependencies: [Target.Dependency]
let PlatformProducts: [Product]
let PlatformTargets: [Target]
let PlatformCSettings: [CSetting]
let PlatformLinkerSettings: [LinkerSetting]
var PlatformCSettings: [CSetting] = []
var PlatformLinkerSettings: [LinkerSetting] = []

#if os(Linux)
PlatformPackageDependencies = [.package(
url: "https://github.com/PADL/IORingSwift",
branch: "main"
)]

PlatformCSettings = [.unsafeFlags(["-I", "/usr/include/libnl3"])]
PlatformLinkerSettings = [.linkedLibrary("nl-3"), .linkedLibrary("nl-route-3")]
let LocalLibNL = false // use locally built libnl, for debugging

if LocalLibNL {
PlatformCSettings = [.unsafeFlags(["-I", "/usr/local/include/libnl3"])]
PlatformLinkerSettings = [.unsafeFlags(["-L", "/usr/local/lib"])]
} else {
PlatformCSettings = [.unsafeFlags(["-I", "/usr/include/libnl3"])]
}

PlatformLinkerSettings += [.linkedLibrary("nl-3"), .linkedLibrary("nl-route-3")]

PlatformTargetDependencies = [
"NetLink",
Expand Down Expand Up @@ -93,8 +101,6 @@ PlatformPackageDependencies = []
PlatformTargetDependencies = []
PlatformProducts = []
PlatformTargets = []
PlatformCSettings = []
PlatformLinkerSettings = []
#endif

let CommonPackageDependencies: [Package.Dependency] = [
Expand Down
2 changes: 1 addition & 1 deletion Sources/MRP/Model/Controller.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ actor Controller<P: Port> {
let logger: Logger

public init(bridgePort: P, bridge: some Bridge<P>) async throws {
ports = try await Set(bridge.ports)
ports = try await Set(bridge.getPorts())
self.bridge = bridge
var logger = Logger(label: "com.padl.SwiftMRP")
logger.logLevel = .trace
Expand Down
9 changes: 5 additions & 4 deletions Sources/MRP/Model/Port.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,22 @@ public protocol Bridge<P>: Sendable {
associatedtype P: Port

var defaultPVid: UInt16? { get }
var vlans: [VLAN] { get }
var ports: [P] { get async throws }
var vlans: Set<VLAN> { get async throws }
var notifications: AnyAsyncSequence<PortNotification<P>> { get }

func getPorts() async throws -> Set<P>
}

extension Bridge {
func port(name: String) async throws -> P {
guard let port = try await ports.first(where: { $0.name == name }) else {
guard let port = try await getPorts().first(where: { $0.name == name }) else {
throw MRPError.portNotFound
}
return port
}

func port(id: P.ID) async throws -> P {
guard let port = try await ports.first(where: { $0.id == id }) else {
guard let port = try await getPorts().first(where: { $0.id == id }) else {
throw MRPError.portNotFound
}
return port
Expand Down
88 changes: 64 additions & 24 deletions Sources/MRP/Platform/LinuxPlatform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,51 +173,91 @@ public struct LinuxPort: Port, Sendable {
}
}

public struct LinuxBridge: Bridge, Sendable {
private extension LinuxPort {
var _vlans: Set<UInt16>? {
(_rtnl as! RTNLLinkBridge).bridgeTaggedVLANs
}

var _pvid: UInt16? {
(_rtnl as! RTNLLinkBridge).bridgePVID
}
}

public final class LinuxBridge: Bridge, @unchecked Sendable {
public typealias Port = LinuxPort

private let _socket: NLSocket
private var _bridgePort: Port!
private let _bridgePort = ManagedCriticalState<Port?>(nil)
private var _task: Task<(), Error>!
private let _portNotificationChannel = AsyncChannel<PortNotification<Port>>()

public init(name: String) async throws {
_socket = try NLSocket(protocol: NETLINK_ROUTE)
try _socket.notifyRtLinks()
let bridgePorts = try await _ports.filter { $0._isBridge && $0.name == name }
try _socket.subscribeLinks()
let bridgePorts = try await _getPorts(family: sa_family_t(AF_BRIDGE))
.filter { $0._isBridge && $0.name == name }
guard bridgePorts.count == 1 else {
throw MRPError.invalidBridgeIdentity
}
_bridgePort = bridgePorts.first!
_bridgePort.withCriticalRegion { $0 = bridgePorts.first! }

_task = Task<(), Error> { [self] in
for try await notification in _socket.notifications {
do {
var portNotification: PortNotification<Port>!
try _bridgePort.withCriticalRegion { bridgePort in
let bridgeIndex = bridgePort!._rtnl.index
let linkMessage = notification as! RTNLLinkMessage
let port = try Port(rtnl: linkMessage.link)
switch linkMessage {
case .new:
if let bridge = port._rtnl as? RTNLLinkBridge,
bridge.index == bridgeIndex
{
bridgePort = port
} else if port._rtnl.master == bridgeIndex {
portNotification = .added(port)
}
case .del:
if port._rtnl.master == bridgeIndex {
portNotification = .removed(port)
} // else what if the bridge is deleted?
}
}
await _portNotificationChannel.send(portNotification)
} catch {}
}
}
}

public var defaultPVid: UInt16? {
nil
_bridgePort.criticalState!._pvid
}

public var vlans: [VLAN] {
[]
public var vlans: Set<VLAN> {
if let vlans = _bridgePort.criticalState!._vlans {
Set(vlans.map { VLAN(vid: $0) })
} else {
Set()
}
}

public var name: String {
_bridgePort.name
_bridgePort.criticalState!.name
}

private var _ports: [Port] {
get async throws {
try await _socket.getRtLinks().compactMap { link in
if case let .new(link) = link {
try Port(rtnl: link)
} else {
nil
}
}.collect()
}
private func _getPorts(family: sa_family_t = sa_family_t(AF_UNSPEC)) async throws -> Set<Port> {
try await Set(_socket.getLinks(family: family).map { try Port(rtnl: $0) }.collect())
}

public var ports: [Port] {
get async throws {
try await _ports.filter {
!$0._isBridge && $0._rtnl.master == _bridgePort._rtnl.index
}
private var _bridgeIndex: Int {
_bridgePort.criticalState!._rtnl.index
}

public func getPorts() async throws -> Set<Port> {
let bridgeIndex = _bridgeIndex
return try await _getPorts().filter {
!$0._isBridge && $0._rtnl.master == bridgeIndex
}
}

Expand Down
91 changes: 83 additions & 8 deletions Sources/NetLink/RTNetLink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,63 @@ public final class RTNLLinkBridge: RTNLLink {
rtnl_link_bridge_has_vlan(_obj) != 0
}

public var bridgePortVLAN: rtnl_link_bridge_vlan? {
private var _bridgePortVLAN: rtnl_link_bridge_vlan? {
guard let p = rtnl_link_bridge_get_port_vlan(_obj) else { return nil }
return p.pointee
}

private func _findNextBit(index: inout Int, in bitmap: UInt32) {
var ret: Int
if index < 0 {
ret = Int(ffs(Int32(bitPattern: bitmap)))
} else {
ret = Int(ffs(Int32(bitPattern: bitmap >> index)))
if ret > 0 { ret += index }
else { ret = 0 }
}
index = ret
}

private func _expandBitmap(_ bitmap: [UInt32]) -> Set<UInt16> {
var ret = Set<UInt16>()

for k in 0..<bitmap.count {
var index: Int = -1
repeat {
_findNextBit(index: &index, in: bitmap[k])
guard index > 0 else { break }
ret.insert(UInt16(k * 32 + index) - 1)
} while true
}

return ret
}

public var bridgeTaggedVLANs: Set<UInt16>? {
guard let bpv = _bridgePortVLAN else { return nil }

return withUnsafePointer(to: bpv.vlan_bitmap) { pointer in
let start = pointer.propertyBasePointer(to: \.0)!
let bitmap = [UInt32](UnsafeBufferPointer(
start: start,
count: Int(RTNL_LINK_BRIDGE_VLAN_BITMAP_LEN)
))
return _expandBitmap(bitmap)
}
}

public var bridgeUntaggedVLANs: Set<UInt16>? {
guard let bpv = _bridgePortVLAN else { return nil }

return withUnsafePointer(to: bpv.untagged_bitmap) { pointer in
let start = pointer.propertyBasePointer(to: \.0)!
let bitmap = [UInt32](UnsafeBufferPointer(
start: start,
count: Int(RTNL_LINK_BRIDGE_VLAN_BITMAP_LEN)
))
return _expandBitmap(bitmap)
}
}
}

public final class RTNLLinkVLAN: RTNLLink {
Expand Down Expand Up @@ -315,21 +368,43 @@ public enum RTNLLinkMessage: NLObjectConstructible, Sendable {
throw Errno.invalidArgument
}
}

public var link: RTNLLink {
switch self {
case let .new(link):
link
case let .del(link):
link
}
}
}

public extension NLSocket {
func getRtLinks() async throws -> AnyAsyncSequence<RTNLLinkMessage> {
let message = try NLMessage(socket: self, type: RTM_GETLINK, flags: NLM_F_REQUEST | NLM_F_DUMP)
var ifinfo = ifinfomsg()
try withUnsafeBytes(of: &ifinfo) {
func getLinks(family: sa_family_t) async throws -> AnyAsyncSequence<RTNLLink> {
let message = try NLMessage(socket: self, type: RTM_GETLINK, flags: NLM_F_DUMP)
var hdr = ifinfomsg()
hdr.ifi_family = UInt8(family)
try withUnsafeBytes(of: &hdr) {
try message.append(Array($0))
}
try message.put(u32: UInt32(IFLA_EXT_MASK), for: RTEXT_FILTER_BRVLAN)
return try streamRequest(message: message).map { $0 as! RTNLLinkMessage }
try message.put(
u32: UInt32(RTEXT_FILTER_VF | RTEXT_FILTER_BRVLAN | RTEXT_FILTER_MRP),
for: CInt(IFLA_EXT_MASK)
)
return try streamRequest(message: message).map { ($0 as! RTNLLinkMessage).link }
.eraseToAnyAsyncSequence()
}

func notifyRtLinks() throws {
func subscribeLinks() throws {
try add(membership: RTNLGRP_LINK)
}
}

extension UnsafePointer {
func propertyBasePointer<Property>(to property: KeyPath<Pointee, Property>)
-> UnsafePointer<Property>?
{
guard let offset = MemoryLayout<Pointee>.offset(of: property) else { return nil }
return (UnsafeRawPointer(self) + offset).assumingMemoryBound(to: Property.self)
}
}

0 comments on commit 4ff23ba

Please sign in to comment.