unofficial mirror of guix-devel@gnu.org 
 help / color / mirror / code / Atom feed
From: Rohan Prinja <rohan.prinja@gmail.com>
To: "Ludovic Courtès" <ludo@gnu.org>
Cc: guix-devel <guix-devel@gnu.org>
Subject: Re: [PATCH] getifaddrs wrapper
Date: Fri, 17 Jul 2015 16:27:16 +0530	[thread overview]
Message-ID: <CAAT4Lc7wEqLNSXR8ADGQ_xW-FUuEn+-p-3B7H_36Do86t-s5Zw@mail.gmail.com> (raw)
In-Reply-To: <87wpy0un8l.fsf@gnu.org>

[-- Attachment #1: Type: text/plain, Size: 935 bytes --]

Hi,

'git rebase' kept failing after a pull, so I squashed my 8 commits
into one. That got the rebase to work. Sorry for the giant patch...

Ludo: the tests for syscalls.scm are included in the patch as well.

Thank you,
Rohan

On 16 July 2015 at 21:08, Ludovic Courtès <ludo@gnu.org> wrote:
> Rohan Prinja <rohan.prinja@gmail.com> skribis:
>
>> Added a convenience macro to make the filtering out of unneeded
>> interfaces cleaner.
>
>>> On 2 July 2015 at 17:53, Ludovic Courtès <ludo@gnu.org> wrote:
>
> [...]
>
>>>> Could you send the updated patch against master, that includes a simple
>>>> test in tests/syscalls.scm that makes sure that ‘getifaddrs’ returns a
>>>> possibly empty list of <interface-address>?
>
> Could you please do both things ↑, or at least the first item (I’ll
> finish polishing it)?
>
> ‘git diff origin/master’ should produce the patch.
>
> TIA!
>
> Ludo’.

[-- Attachment #2: getifaddrs-patch.patch --]
[-- Type: text/x-patch, Size: 16008 bytes --]

diff --git a/guix/build/syscalls.scm b/guix/build/syscalls.scm
index dcca5fc..8262e0b 100644
--- a/guix/build/syscalls.scm
+++ b/guix/build/syscalls.scm
@@ -1,6 +1,7 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2014, 2015 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;; Copyright © 2015 Rohan Prinja <rohan.prinja@gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -21,6 +22,7 @@
   #:use-module (system foreign)
   #:use-module (rnrs bytevectors)
   #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-9)
   #:use-module (ice-9 rdelim)
   #:use-module (ice-9 regex)
   #:use-module (ice-9 match)
@@ -57,6 +59,26 @@
             clone
             setns
 
+            getifaddrs
+
+            <interface-address>
+            interface-address?
+            interface-address-name
+            interface-address-flags
+            interface-address-data
+
+            interface-address-addr
+            interface-address-netmask
+            interface-address-broadaddr
+
+            ;; Wrappers around the above three functions. Each
+            ;; of these returns either a socket address or #f.
+            interface-address-address
+            interface-address-broadcast-addr
+            interface-address-netmask-addr
+
+            remove-if-netmask-null
+
             IFF_UP
             IFF_BROADCAST
             IFF_LOOPBACK
@@ -478,6 +500,202 @@ the C structure with the given TYPES."
   (address   (int128 ~ big))
   (scopeid   int32))
 
+(define-c-struct ifaddrs                          ;<ifaddrs.h>
+  read-ifaddrs
+  write-ifaddrs!
+  (ifa-next '*)
+  (ifa-name '*)
+  (ifa-flags unsigned-int)
+  (ifa-addr '*)
+  (ifa-netmask '*)
+  (ifu-broadcastaddr '*)
+  (ifa-data '*))
+
+(define-record-type <interface-address>
+  (make-interface-address name flags addr netmask broadaddr data)
+  interface-address?
+  (name interface-address-name)
+  (flags interface-address-flags)
+  (addr interface-address-addr)
+  (netmask interface-address-netmask)
+  (broadaddr interface-address-broadaddr)
+  (data interface-address-data))
+
+(define (bytevector-slice bv start len)
+  "Return a new bytevector (not a view into the old one)
+containing the elements from BV from index START upto
+index START + LEN - 1"
+  (let* ((res (make-bytevector len 0)))
+    (bytevector-copy! bv start res 0 len)
+    res))
+
+;; FFI type for 'struct ifaddrs'.
+(define %struct-ifaddrs-type
+  `(* * ,unsigned-int * * * *))
+
+;; Size of 'struct sockaddr' in bytes.
+;; See also: bind (2).
+(define %sizeof-struct-sockaddr
+  (+ 14 (sizeof unsigned-short)))
+
+(define (ifaddrs-pointer->bv ptr)
+  "Return a bytevector aliasing the memory pointed to by a
+'struct ifaddrs' pointer, passed as a pointer object PTR."
+  (pointer->bytevector ptr (sizeof %struct-ifaddrs-type)))
+
+;; Initializer for 'struct ifaddrs'.
+(define %struct-ifaddrs-init
+  (list %null-pointer
+        %null-pointer
+        0
+        %null-pointer
+        %null-pointer
+        %null-pointer
+        %null-pointer))
+
+(define (next-ifaddr-ptr bv)
+  "Return a bytevector aliasing the memory pointed to by the
+ifa_next field of a struct ifaddrs* pointer passed as a
+bytevector BV."
+  (let* ((ptr-size (sizeof '*))
+         (address (cond ((= ptr-size 4) (bytevector-u32-native-ref bv 0))
+                        ((= ptr-size 8) (bytevector-u64-native-ref bv 0)))))
+    (make-pointer address)))
+
+;; Return the bytevector aliasing the memory pointed to by
+;; the ifa-next field in a 'struct ifaddrs' pointer passed in
+;; as a bytevector.
+(define next-ifaddr
+  (compose ifaddrs-pointer->bv
+           next-ifaddr-ptr))
+
+(define %getifaddrs
+  (let* ((func-ptr (dynamic-func "getifaddrs" (dynamic-link)))
+         (proc (pointer->procedure int func-ptr (list '*))))
+    (lambda ()
+      "Wrapper around getifaddrs (3)."
+      (let* ((ptr (make-c-struct %struct-ifaddrs-type
+                                  %struct-ifaddrs-init))
+             (ret (proc ptr))
+             (err (errno)))
+        (if (zero? ret)
+            (next-ifaddr (ifaddrs-pointer->bv ptr))
+            (throw 'system-error "getifaddrs" "~S: ~A"
+                   (list ptr (strerror err))
+                   (list err)))))))
+
+(define (make-ifaddrs bv)
+  "Convert a bytevector aliasing the memory pointed to by a
+'struct ifaddrs' pointer into a <interface-address> record."
+  (match (read-ifaddrs bv 0)
+    ((next name-ptr flags addr netmask broadaddr data)
+     (make-interface-address (pointer->string (make-pointer name-ptr))
+                             flags
+                             (make-pointer addr)
+                             (make-pointer netmask)
+                             (make-pointer broadaddr)
+                             (make-pointer data)))))
+
+;; Is an interface the last in the intrusive linked list of struct ifaddrs?
+;; Here, the only argument is a bytevector aliasing the memory pointed to by
+;; a 'struct ifaddrs' pointer.
+(define last-interface?
+  (compose null-pointer? next-ifaddr-ptr))
+
+(define (pack-ifaddrs bv)
+  "Strip out the needless 4-byte padding after the
+unsigned-int ifa-flags field"
+  (if (and (= 8 (sizeof '*))
+           (= 4 (sizeof unsigned-int)))
+      (let* ((res (make-bytevector 52 0)))
+        (bytevector-copy! bv 0 res 0 20)
+        (bytevector-copy! bv 24 res 20 32)
+        res)
+      bv))
+
+(define (getifaddrs)
+  "Return the list of network interfaces on the local system."
+  (let ((ifaddrs (%getifaddrs)))
+    (let loop ((curr ifaddrs) (res '()))
+      (if (last-interface? curr)
+          (map (compose make-ifaddrs pack-ifaddrs)
+               (reverse res))
+          (loop (next-ifaddr curr)
+                (cons curr res))))))
+
+;; Given a bytevector aliasing the memory pointed to by
+;; a 'struct sockaddr' pointer, return a socket address.
+(define-syntax-rule (bytevector->sockaddr bv)
+  (match (read-sockaddr-in bv 0)
+      ((family port address)
+       (if (member family (list AF_INET AF_INET6 AF_UNIX))
+           (inet-ntop family address)
+           #f))))
+
+;; Note: getifaddrs returns multiple interfaces with the same
+;; e.g. on my system I see multiple "eth0"s. The difference is
+;; that for one of the eth0's, the family of the address
+;; pointed to by the ifu.ifa-broadaddr field is 17, which is
+;; not an AF_* constant. Hence the check for "(member family ...)".
+
+(define (extract-address-field iface field)
+  "Extract a field corresponding to an IPv4 address from a 'struct
+sockaddr' from an <interface-address> record type."
+  (let* ((addr (field iface))
+         (bv (pointer->bytevector addr %sizeof-struct-sockaddr)))
+    (bytevector->sockaddr bv)))
+
+;; Note: address fields in 'struct getifaddrs' are pointers to
+;; 'struct sockaddr'. In 'extract-address-field' we are
+;; implicitly typecasting this 'sockaddr' pointer to a
+;; 'sockaddr_in' pointer.
+
+;; Utility macro to remove all ifaces from the output IFACES of
+;; (getifaddrs) that have a null-pointer in the 'netmask' field.
+(define-syntax-rule (remove-if-netmask-null ifaces)
+  (remove (compose null-pointer? interface-address-netmask) ifaces))
+
+;; Given an <interface-address> record IFACE, return its
+;; address field as a sockaddr if it exists, otherwise return #f.
+(define (interface-address-address iface)
+  (extract-address-field iface interface-address-addr))
+
+;; Given an <interface-address> record IFACE, return its broadcast
+;; address field as a sockaddr if it exists, otherwise return #f.
+(define (interface-address-broadcast-addr iface)
+  (extract-address-field iface interface-address-broadaddr))
+
+;; Given an <interface-address> record IFACE, return its netmask
+;; address field as a sockaddr if it exists, otherwise return #f.
+(define (interface-address-netmask-addr iface)
+  (extract-address-field iface interface-address-netmask))
+
+;; Retrieve the ifa-next-ptr field from a 'struct ifaddrs'
+;; pointer passed in as a bytevector BV.
+(define-syntax-rule (ifaddr-next-ptr bv)
+  (match (read-ifaddrs bv 0)
+    ((next name-ptr flags addr netmask broadaddr data)
+     next)))
+
+;; Retrieve the bytes corresponding to the ifa-name field
+;; from a 'struct ifaddrs' pointer passed in as a bytevector BV.
+(define-syntax-rule (ifaddr-name-bytes bv)
+  (match (read-ifaddrs bv 0)
+    ((next name-ptr flags addr netmask broadaddr data)
+     name-ptr)))
+
+;; Retrieve the string pointed to by the ifa-name field
+;; from a 'struct ifaddrs' pointer passed in as a bytevector BV.
+(define-syntax-rule (ifaddr-name bv)
+  (pointer->string (make-pointer (ifaddr-name-bytes bv))))
+
+;; Retrieve the ifa-flags field from a 'struct ifaddrs'
+;; pointer passed in as a bytevector BV.
+(define-syntax-rule (ifaddr-flags bv)
+  (match (read-ifaddrs bv 0)
+    ((next name-ptr flags addr netmask broadaddr data)
+     flags)))
+
 (define (write-socket-address! sockaddr bv index)
   "Write SOCKADDR, a socket address as returned by 'make-socket-address', to
 bytevector BV at INDEX."
diff --git a/tests/syscalls.scm b/tests/syscalls.scm
index 6b614a5..73105a5 100644
--- a/tests/syscalls.scm
+++ b/tests/syscalls.scm
@@ -23,11 +23,98 @@
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-64)
-  #:use-module (ice-9 match))
+  #:use-module (ice-9 match)
+  #:use-module ((ice-9 popen) #:select (open-pipe*))
+  #:use-module ((ice-9 rdelim) #:select (read-line))
+  #:use-module ((ice-9 regex) #:select (string-match match:substring))
+  #:use-module (rnrs bytevectors)
+  #:use-module (system foreign)
+  #:use-module ((rnrs io ports) #:select (port-eof?)))
 
 ;; Test the (guix build syscalls) module, although there's not much that can
 ;; actually be tested without being root.
 
+;; Is the first character of a string #\space?
+(define-syntax-rule (first-char-is-space? string)
+  (eq? #\space (string-ref string 0)))
+
+;; In the output produced by ifconfig (8), is a line
+;; one that starts a new interface description?
+(define-syntax-rule (line-contains-iface-name? line)
+    (not (or (string-null? line)
+	     (first-char-is-space? line))))
+
+(define (ifconfig-find-all-interfaces)
+  "List all the network interfaces as identified
+by ifconfig (8)."
+  (let ((pipe (open-pipe* OPEN_READ "ifconfig")))
+    (let lp ((line (read-line pipe))
+	     (res '()))
+      (cond ((port-eof? pipe) (reverse res))
+	    ((line-contains-iface-name? line)
+	     (let* ((trimmed-line (string-trim-both line))
+		    (split-line (string-split trimmed-line #\space))
+		    (iface-name (car split-line)))
+	       (lp (read-line pipe)
+		   (cons iface-name res))))
+	    (else (lp (read-line pipe) res))))))
+
+(define (extract-iface-name line)
+  "Extract the name of the interface from a line in the output of
+ifconfig (8) which is known to be the first line describing said
+interface."
+  (let ((str-ls (string->list line)))
+    (let lp ((ls str-ls) (res '()))
+      (if (eq? #\space (car ls))
+          (apply string (reverse res))
+          (lp (cdr ls) (cons (car ls) res))))))
+
+(define (ifconfig-extract-addr-of iface-name type)
+  "Call ifconfig (8) to find out the broadcast address of the
+interface whose name is a prefix of the string IFACE-NAME. The
+broadcast address is returned as a printable string."
+  (let ((pipe (open-pipe* OPEN_READ "ifconfig")))
+    (let lp ((line (read-line pipe)))
+      (if (eof-object? line)
+          #f
+          (if (and (line-contains-iface-name? line)
+                   (string-prefix? iface-name
+                                   (extract-iface-name line)))
+              (let* ((next-line (read-line pipe))
+                     (search-string (cond ((eq? type 'broadcast) "Bcast:")
+                                          ((eq? type 'netmask) "Mask:")
+                                          (else "inet addr:")))
+                     (str-byte "[0-9]([0-9][0-9])?")
+                     (ipaddr-regex (string-append search-string
+                                                   str-byte "\\."
+                                                   str-byte "\\."
+                                                   str-byte "\\."
+                                                   str-byte))
+                     (match (string-match ipaddr-regex next-line)))
+                (if match
+                    (string-drop (match:substring match) (cond ((eq? type 'broadcast) 6)
+                                                               ((eq? type 'netmask) 5)
+                                                               (else 10)))
+                    (lp (read-line pipe))))
+              (lp (read-line pipe)))))))
+
+(define (prefix? ls1 ls2)
+  "Is list LS1 a prefix of list LS2?. This procedure
+assumes that (length ls1) <= (length ls2)."
+  (or (null? ls1)
+      (and (equal? (car ls1) (car ls2))
+	   (prefix? (cdr ls1) (cdr ls2)))))
+
+(define (remove-duplicates ls)
+  "Remove consecutive duplicate elements from a list LS.
+For example, (4 2 2 2 2 1 3 3) => (4 2 1 3)."
+  (cond ((< (length ls) 2)
+         ls)
+        ((equal? (car ls) (cadr ls))
+         (remove-duplicates (cdr ls)))
+        (else
+         (cons (car ls) (remove-duplicates (cdr ls))))))
+
 (test-begin "syscalls")
 
 (test-equal "mount, ENOENT"
@@ -211,6 +298,51 @@
         ;; We get EPERM with Linux 3.18ish and EACCES with 2.6.32.
         (memv (system-error-errno args) (list EPERM EACCES))))))
 
+(test-assert "getifaddrs"
+             (let* ((ifaddrs (getifaddrs))
+                    (names (map interface-address-name ifaddrs)))
+               (member "lo" names)))
+
+(test-assert "ifconfig-result-is-subset-of-getifaddrs-result"
+             (let* ((ifaddrs (getifaddrs))
+                    (names (map interface-address-name ifaddrs))
+                    (sorted-names (sort names string<?))
+                    (unique-names (remove-duplicates sorted-names))
+                    (ifconfig (ifconfig-find-all-interfaces)))
+               (prefix?
+                (sort (ifconfig-find-all-interfaces) string<?)
+                unique-names)))
+
+(test-assert "getifaddrs-address"
+             (let* ((is-eth-iface? (lambda (i)
+                                     (string-prefix? "eth"
+                                                     (interface-address-name i))))
+                    (ifaddrs (remove-if-netmask-null (getifaddrs)))
+                    (eth-ifaces (filter is-eth-iface? ifaddrs))
+                    (getifaddrs-result (map interface-address-address eth-ifaces))
+                    (ifconfig-result (ifconfig-extract-addr-of "eth" 'address)))
+               (member ifconfig-result getifaddrs-result)))
+
+(test-assert "getifaddrs-broadcast-address"
+             (let* ((is-eth-iface? (lambda (i)
+                                     (string-prefix? "eth"
+                                                     (interface-address-name i))))
+                    (ifaddrs (remove-if-netmask-null (getifaddrs)))
+                    (eth-ifaces (filter is-eth-iface? ifaddrs))
+                    (getifaddrs-result (map interface-address-broadcast-addr eth-ifaces))
+                    (ifconfig-result (ifconfig-extract-addr-of "eth" 'broadcast)))
+               (member ifconfig-result getifaddrs-result)))
+
+(test-assert "getifaddrs-netmask-address"
+             (let* ((is-eth-iface? (lambda (i)
+                                     (string-prefix? "eth"
+                                                     (interface-address-name i))))
+                    (ifaddrs (remove-if-netmask-null (getifaddrs)))
+                    (eth-ifaces (filter is-eth-iface? ifaddrs))
+                    (getifaddrs-result (map interface-address-netmask-addr eth-ifaces))
+                    (ifconfig-result (ifconfig-extract-addr-of "eth" 'netmask)))
+               (member ifconfig-result getifaddrs-result)))
+
 (test-end)
 
 \f

  reply	other threads:[~2015-07-17 10:57 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2015-06-19  9:50 [PATCH] getifaddrs wrapper Rohan Prinja
2015-06-19 12:03 ` Ludovic Courtès
2015-07-02 10:59   ` Rohan Prinja
2015-07-02 12:23     ` Ludovic Courtès
2015-07-16  8:30       ` Rohan Prinja
2015-07-16 13:50         ` Rohan Prinja
2015-07-16 15:38           ` Ludovic Courtès
2015-07-17 10:57             ` Rohan Prinja [this message]
2015-07-25 12:59               ` Ludovic Courtès

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://guix.gnu.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=CAAT4Lc7wEqLNSXR8ADGQ_xW-FUuEn+-p-3B7H_36Do86t-s5Zw@mail.gmail.com \
    --to=rohan.prinja@gmail.com \
    --cc=guix-devel@gnu.org \
    --cc=ludo@gnu.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/guix.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).