Something on my phone is waiting for me to go to sleep to contact ad servers
I realized last week that my phone's DNS wasn't going through pihole -- part of this is Comcast xFI's fault, part of this is Android's fault, and some of it was simply my fault by making some overly-effective firewall rules to prevent my server from being an open DNS relay. Ultimately, I "fixed" it properly by pointing Tailscale's MagicDNS at one of my Tailscale nodes running a Pi Hole server and leaving Tailscale running on my phone. Bit of extra battery usage but not significant enough to care unless I'm using my server also as an Exit Node.
But then I looked at my traffic graphs:
i plugged my phone back in to my pihole and ... something is misbehaving :) making thousands of attempts to look up doubleclick and adsense.
— rrix (@rrrrrrrix) April 12, 2022pic.twitter.com/axIwC9FFfU
Something on my phone makes thousands of requests overnight to look up googleadservices.com and ad.doubleclick.net ... I smell an ad impression fraud playbook, or at least someone doing too many DNS lookups. While my pihole does have a nearly constant background chatter of ad-server lookups on the order of 1-3 per minute, overnight these jump up to nearly 25 per minute.
On the one hand, this is expected and accepted behavior on mobile devices despite being a huge freakin' problem, but this one in particular raised my hackles: those periods where it went crazy are more or less the exact times I was asleep, which is really disturbing that something might be monitoring me specifically to hide its activity. This is, at the very least spooky and at most a pretty blatant violation of Privacy norms. Or maybe it's only running when my phone is plugged in so that someone doesn't worry about battery usage.
But what is normal after all?
Can I figure out which app is doing this?
I poked around in network data usage screens and battery usage screens, but of course that didn't elucidate much. Maybe tomorrow night i'll leave my phone unplugged and see if the battery usage reflects that.
But is there anything in the phone's debug logs? I ran adb logcat and saved the output to a file for analysis:
link to adb logs attachment link to adb logs attachment
It's easy enough to slurp this file in to Python to analyze it. If I want to do more I can pull in Pandas and Numpy to do some jupyter style exploration, but I want to see some simple distributions of the messages. Logcat can give me some of this.
shell source: :session *shell* :results noneexport LOG_FILE=~/org/data/20/220412T115028.569214/adb-output adb logcat -d -v long > $LOG_FILE
shell source: :session *shell* :results verbatimadb logcat -S 2>/dev/null
size/num main system crash kernel Total Total 3318391645/41278326781755684/1344098 10428/55 0/0 4100157757/42622479 Now 248415/2199 236848/4023 10428/55 495691/6277 Logspan 3:46.125(9.6%) 5:09:02.418(3.2%) 5:09:08.87 Overhead 371559 462136 13508 2294731 Chattiest UIDs in main log buffer: Size +/- Pruned UID PACKAGE BYTES NUM 706 1011 PID/UID COMMAND LINE " " 652/1000 /system/bin/servicemanager 19760 997 1360/1000 /system/vendor/bin/cnss-daemon 14672 14 1765/1000 system_server 3632 653/1000 /system/bin/hwservicemanager 554 100 24 10206 com.google.android.inputmethod.latin 28125 +61% 10457 com.google.android.providers.media.module 21991 +60% 1002 bluetooth 11673 +59% 1010 wifi 10036 +60% 10167 com.google.android.gms 4527 +64% 10202 com.google.android.youtube 4017 +60% 1010451 com.qualcomm.qti.devicestatisticsservice 3706 +56% 10451 com.qualcomm.qti.devicestatisticsservice 3612 +56% 0 root 2073 +60% 10316 com.nextcloud.client 1340 +67% 10327 com.digibites.calendarplus 965 +50% 1036 auditd 828 +50% 1001 radio 819 +50% 1010167 com.google.android.gms 697 2.0X 10325 com.jumboprivacy 637 2.0X 1 10269 com.getsomeheadspace.android 480 10123 com.android.vending 457 Chattiest UIDs in system log buffer: Size +/- Pruned UID PACKAGE BYTES NUM 24886 10263 com.nutomic.syncthingandroid 4726 20X 10062 com.android.calllogbackup 3051 13X 10135 com.android.systemui 2842 12X Chattiest UIDs in crash log buffer: Size UID PACKAGE BYTES 10206 com.google.android.inputmethod.latin 7692 10286 appyweather.appyweather 1601 1000 system 1009 0 root 126
None of this is particularly ... exciting or elucidating is it? the biggest logging systems which jump out to me are my keyboard input method, the media scanner which gets confused trying to analyze all the files I plug in to the device via Syncthing , YouTube, my calendar app... hmm. not much "there there"
python source: :session *adb-logcat-exploration*import pathlib in_file = pathlib.Path("data/20/220412T115028.569214/adb-output") assert in_file.exists() output = '' with open(in_file, "r") as f: output = f.read() assert len(output) > 0 blocks = output.split("\n\n") [[block] for block in blocks[0:5]]
| --------- beginning of crash\n[ 03-31 23:02:11.196 5259: 5259 F/libc ]\nFatal signal 6 (SIGABRT), code -1 (SIQUEUE) in tid 5259 (init), pid 5259 (init) |
| [ 03-31 23:02:11.232 5259: 5259 F/libc ]\ncrashdump helper failed to exec |
| [ 04-11 13:42:13.418 25327:25327 E/AndroidRuntime ]\nFATAL EXCEPTION: main\nProcess: appyweather.appyweather, PID: 25327\nandroid.runtime.JavaProxyThrowable: System.Collections.Generic.KeyNotFoundException: The given key '13' was not present in the dictionary.\n at System.Collections.Generic.Dictionary`2[TKey,TValue].getItem (TKey key) [0x0001e] in <4eb26d78e5e04787a2ae957cc1fe14cd>:0 \n at appyweather.Droid.ActivityRadar.Play () [0x000bb] in <d99165a0237041e28f844ea541fa47a6>:0 \n at System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b_7_0 (System.Object state) [0x00000] in <4eb26d78e5e04787a2ae957cc1fe14cd>:0 \n at Android.App.SyncContext+<>c_DisplayClass2_0.<Post>b_0 () [0x00000] in <528406e5e6ea4c1db04fb6d9d6b70bd0>:0 \n at Java.Lang.Thread+RunnableImplementor.Run () [0x00008] in <528406e5e6ea4c1db04fb6d9d6b70bd0>:0 \n at Java.Lang.IRunnableInvoker.nRun (System.IntPtr jnienv, System.IntPtr native_this) [0x00008] in <528406e5e6ea4c1db04fb6d9d6b70bd0>:0 \n at (wrapper dynamic-method) Android.Runtime.DynamicMethodNameCounter.24(intptr,intptr)\n\tat mono.java.lang.RunnableImplementor.nrun(Native Method)\n\tat mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:30)\n\tat android.os.Handler.handleCallback(Handler.java:938)\n\tat android.os.Handler.dispatchMessage(Handler.java:99)\n\tat android.os.Looper.loop(Looper.java:250)\n\tat android.app.ActivityThread.main(ActivityThread.java:7868)\n\tat java.lang.reflect.Method.invoke(Native Method)\n\tat com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)\n\tat com.android.internal.os.ZygoteInit.main(ZygoteInit.java:958) |
| \n[ 04-11 22:05:12.988 31417:31515 F/libc ]\nFatal signal 11 (SIGSEGV), code 1 (SEGVMAPERR), fault addr 0x1b0 in tid 31515 (DecoderWrapper-), pid 31417 (putmethod.latin) |
| [ 04-11 22:05:13.146 10585:10585 F/DEBUG ]\n*** * * * * * * * * * * * * * * * |
so using adb with -v long like i did breaks each message in to blocks separated by a blank line. good enough to parse like this, especially since all I really care about is the metadata header, usually the first line -- the first record looks corrupted but who knows.. let's see:
python source: :session *adb-logcat-exploration*headers = [] import re for block in blocks: lines = re.split("\n+", block) header = list(filter(lambda line: line.startswith("[") and line.endswith("]"), lines)) second_line = ("\n".join(lines[2:])) headers.append(header + [second_line]) # only print the uhh header [[header[0]] for header in headers[0:10]]
| [ 03-31 23:02:11.196 5259: 5259 F/libc ] |
| [ 03-31 23:02:11.232 5259: 5259 F/libc ] |
| [ 04-11 13:42:13.418 25327:25327 E/AndroidRuntime ] |
| [ 04-11 22:05:12.988 31417:31515 F/libc ] |
| [ 04-11 22:05:13.146 10585:10585 F/DEBUG ] |
| [ 04-11 22:05:13.147 10585:10585 F/DEBUG ] |
| [ 04-11 22:05:13.147 10585:10585 F/DEBUG ] |
| [ 04-11 22:05:13.147 10585:10585 F/DEBUG ] |
| [ 04-11 22:05:13.148 10585:10585 F/DEBUG ] |
| [ 04-11 22:05:13.148 10585:10585 F/DEBUG ] |
Cool so now all my headers are extracted, let's parse this! it's not exactly trivial, of course these lines aren't regular, you can see that they are generally set to the same column, especially the fields I care about (time, and that last field, the emitting module). we'll be ugly about it.
python source: :session *adb-logcat-exploration*import re from datetime import datetime export = [] for header in headers: try: working_header = header[0] except IndexError: print("bad header: {}".format(header)) continue parts = re.split(r"\s+", working_header) try: burp = "2022-{} {}000".format(parts[1], parts[2]) # (ref:burp) assembly = [datetime.strptime(burp, "%Y-%m-%d %H:%M:%S.%f"), parts[-2], header[-1]] export.append(assembly) except IndexError: continue len(export)
7476
I am lazy with intermediate variable names.. (burp) takes the date and time and crams them in to a thing I can call datetime.strptime on & parts[-2] is the second to last element from that header, the Android module which emitted it.
And so I want all the messages between 21:00 and 09:00... I could do this within the logcat invocation, I probably should, but let's do some more nasty shit with datetime since i'm already invoking strptime...
python source: :session *adb-logcat-exploration* :results nonestart_window = datetime(2022, 4, 11, 22, 0) end_window = datetime(2022, 4, 12, 9, 0) def date_filter_fn(pair): dt = pair[0] if dt > start_window and dt < end_window: return True return False
python source: :session *adb-logcat-exploration*filtered = list(filter(date_filter_fn, export)) len(filtered)
1660
That's not as much I would have hoped!! Let's see where they come from
python source: :session *adb-logcat-exploration* :results verbatimsum_map = {} for msg in filtered: module = msg[1] sum_map[module] = sum_map.get(module, 0) + 1 sum_map
{'F/libc': 1, 'F/DEBUG': 50, 'I/chatty': 1576, 'D/ActivityThread': 12, 'W/Looper': 4, 'W/ActivityThread': 4, 'W/SocketClient': 1, 'D/KeyguardViewMediator': 6, 'E/ActivityThread': 2, 'V/FingerprintManager': 2, 'V/MotoFingerprintManager': 2}
So chatty is .. chatty? grr.
shell source: :session *shell* :results verbatimgrep -A1 "I/chatty" $LOG_FILE | head
[ 04-12 07:38:15.795 1765: 5526 I/chatty ] uid=1000(system) Binder:1765_C expire 2 lines -- [ 04-12 07:38:16.708 1765: 4195 I/chatty ] uid=1000(system) Binder:1765_7 expire 2 lines -- [ 04-12 07:38:16.858 1765: 6374 I/chatty ] uid=1000(system) Binder:1765_16 expire 2 lines -- [ 04-12 07:38:17.119 1765: 1765 I/chatty ]
Well that's not so useful is it... some things are being too chatty and being elided from the logs
Same string extraction gymnastics... Tired of ugly python? so am i... enjoy an ugly shell pipeline! one day maybe i'll just learn awk outright.
shell source: :session *shell* :results verbatimgrep -A1 "I/chatty" $LOG_FILE | grep expire | awk '{print $2}' | sed -e 's/:.*//' | sort | uniq -c | sort -nr
2177 Binder
478 ActivityManager
383 UEventObserver
250 ConnectivitySer
240 system_server
72 android.anim
71 android.display
65 batterystats-wo
53 android.ui
50 android.fg
48 PowerManagerSer
44 android.bg
26 expire
20 /system/bin/servicemanager
11 SyncManager
9 onProviders.ECP
6 Thread-8
6 ranker
6 HwBinder
6 /data/app/~~bXf7VcDuNg7_iWAGE_Bdiw==/com.nutomic.syncthingandroid-vk_6lW7Ia1yF2y1eCZsdjg==/lib/arm64/libsyncthing.so
5 InputReader
4 NetworkStats
4 backup
3 /system/bin/ip
2 Thread-48341
2 android.io
2 AlarmManager
1 Thread-48342
1 Thread-48340
1 Thread-48336
1 /system/vendor/bin/cnss-daemon
1 NetworkWatchlis
1 com.fastmail.app
1 backup-0
1 AdbDebuggingMan
so this is a dead end to tracking this down, it seems -- I'm sure I gave "informed consent" when I installed whatever app is tracking my sleep schedule to run advertising fraud schemes.
What's next, must I install network monitoring software directly on my phone to track this down?
What about dumpsys output?
Okay that was a dead end ... Poking around on some poorly stack overflow questions and SEO'd android dev blog google results, i found that there is a command in Android systems called dumpsys which will ... dump system information to the terminal. In particular, it can dump detailed network statistics broken down by the process UID responsible for the traffic. Since my phone isn't doing a whole lot over night, in theory those 20000+ DNS requests should wind up in here somehow one hopes...
shell source: :session *shell*adb shell dumpsys netstats detail > /tmp/netstats echo "[[file:/tmp/netstats]]"
| file:/tmp/netstats |
The output of the dumpsys command is just barely structured...
what if i yaml load it lul:
python source:import yaml try: with open("/tmp/netstats", "r") as f: dictmaybe = yaml.safe_load(f.read()) except Exception: dictmaybe = "lol no" dictmaybe
lol no
cool so i gotta parse this wordsoup myself
python source: :session *adb-logcat-exploration*import pathlib import re from datetime import datetime lines = pathlib.Path("/tmp/netstats").read_text().split("\n") PARSE_STATE = None curr_uid = None parsed_data = [] for line in lines: if line.startswith("UID stats"): PARSE_STATE = "UID" continue if PARSE_STATE is not None: match = re.search(r"^(?P<whitespace> +)", line) if match is None: print("nope") level = 0 PARSE_STATE = None curr_uid = None continue level = int(len(match.group("whitespace")) / 2) if level == 1 and line.startswith(" ident"): # extract UID m = re.search(r"uid=(?P<uid>\d+)", line) if not m: pass #print("line bad {}".format(line)) else: curr_uid = m.group("uid") if level == 3: # example line # st=1648792800 rb=0 rp=0 tb=76 tp=1 op=0 m = re.search(r"st=(?P<st>\d+) rb=(?P<rb>\d+).*tb=(?P<tb>\d+).*", line) if m is None: print("uhh {}".format(line)) continue ts = datetime.fromtimestamp(int(m.group("st"))) parsed_data.append([ts, curr_uid, m.group("rb"), m.group("tb")]) len(parsed_data)
8245
Now we can shove the dataframe in to Pandas and plot the output...
python source: :session *adb-logcat-exploration* :results verbatimimport pandas as pd time_index = [r[0] for r in parsed_data] uid_index = [r[1] for r in parsed_data ] midx = pd.MultiIndex.from_arrays([time_index, uid_index], names=["ts", "uid"]) df = pd.DataFrame(data=parsed_data, columns=["ts", "uid", "rx", "tx"], index=time_index) df = df.sort_index() df['ts'] = pd.to_datetime(df['ts']) df['rx'] = pd.to_numeric(df['rx']) df['tx'] = pd.to_numeric(df['tx']) df['uid'] = pd.to_numeric(df['uid']) df.head()
ts uid rx tx 2022-03-31 23:00:00 2022-03-31 23:00:00 10187 25594 8944 2022-03-31 23:00:00 2022-03-31 23:00:00 10316 137686 31767 2022-03-31 23:00:00 2022-03-31 23:00:00 10315 67770 24965 2022-03-31 23:00:00 2022-03-31 23:00:00 1010223 275681 77330 2022-03-31 23:00:00 2022-03-31 23:00:00 10307 87947 42552
python source: :session *adb-logcat-exploration* :results verbatimdownsize = df.loc["2022/04/11 02:00":"2022/04/12 09:00"] downsize.head()
ts uid rx tx 2022-04-11 03:00:00 2022-04-11 03:00:00 10009 845261 626572 2022-04-11 03:00:00 2022-04-11 03:00:00 10125 736 560 2022-04-11 03:00:00 2022-04-11 03:00:00 10459 60067 26615 2022-04-11 03:00:00 2022-04-11 03:00:00 10264 553 250 2022-04-11 03:00:00 2022-04-11 03:00:00 10205 9992 4444
python source: :session *adb-logcat-exploration* :results noneimport matplotlib import matplotlib.pyplot as plt import seaborn
python source: :session *adb-logcat-exploration* :results fileuids = downsize["uid"].unique() fig, ax = plt.subplots() seaborn.set_style("ticks") for uid in uids: this_uid_df = downsize.loc[downsize["uid"]==uid] total_bytes = this_uid_df["tx"].sum() if total_bytes > 64000: seaborn.lineplot( ax=ax, data=this_uid_df.groupby("ts").sum(), legend="full", y="tx", x="ts", palette="deep", label=f"{uid} ({total_bytes})", ) h,l = ax.get_legend_handles_labels() ax.legend_.remove() fig.autofmt_xdate() fig.legend(h, l, loc="center right", ncol=4, mode="tight", fontsize=6) loc = 'data/20/220412T115028.569214/plt1.png' fig.savefig(loc, bbox_inches=matplotlib.transforms.Bbox([[0,0],[11,5]])) loc
some interesting outliers, but no idea which... Let's see
python source: :session *adb-logcat-exploration*maxis = downsize.groupby("uid")["tx"].sum().sort_values().iloc[::-1] maxis[0:19]
uid 10009 38107508 10316 14349564 10329 13844479 10263 11445899 10195 7216042 10282 6528796 10167 5088949 10276 4504974 10325 4177685 0 3882100 10123 3757039 10250 3587922 10125 2573676 10281 2539344 10202 1615213 1010123 1572069 1010125 1437782 10274 1423099 10218 1410081 Name: tx, dtype: int64
And the top 10 UIDs are....
python source: :session *adb-logcat-exploration*top10 = maxis.index[0:50].astype(str) "\n".join(top10)
10009 10316 10329 10263 10195 10282 10167 10276 10325 0 10123 10250 10125 10281 10202 1010123 1010125 10274 10218 1010228 10318 1010167 10247 10191 10264 10343 10286 10272 10307 1073 10185 10186 10285 1010205 10270 10278 1010185 10337 10287 10136 10279 10269 10204 10205 10254 10112 10459 10209 1000 1010223
Okay so ... this gives me the maximum transmitted bytes in the 2 hour sampling window, which is quite useful. Using a different dumpsys command I can figure out which will export information about the packages including the UID mapping.
run in [[shell:adb shell dumpsys package > packagesdump.txt]]
shell source: :noweb yes :results verbatimexport PKG_FILE=data/20/220412T115028.569214/packages_dump.txt function finduid() { echo -ne "$1 " (grep -F -B1 "userId=$1" $PKG_FILE || echo "unknown") | head -1 } finduid <<top-10-network-uids()>>
10009 Package [com.tailscale.ipn] (15c63bb): 10316 Package [com.nextcloud.client] (8557d8e): 10329 Package [com.urbandroid.sleep.addon.port] (9289605): 10263 Package [com.nutomic.syncthingandroid] (62758f2): 10195 Package [com.google.android.apps.youtube.music] (2d9cbd): 10282 Package [org.mozilla.firefox] (3eb3ec8): 10167 Package [com.google.android.gms] (34fe9b0): 10276 Package [reddit.news] (e0e810b): 10325 Package [com.jumboprivacy] (182b16): 0 Session 3187895: 10123 Package [com.android.vending] (1e9a0f3): 10250 Package [im.vector.app] (a9196df): 10125 Package [com.google.android.googlequicksearchbox] (aa27695): 10281 Package [com.urbandroid.sleep] (72e44f5): 10202 Package [com.google.android.youtube] (cde6cf2): 1010123 unknown 1010125 unknown 10274 Package [com.fastmail.app] (2950a72): 10218 Package [com.google.android.apps.maps] (5781bd6): 1010228 unknown 10318 Package [com.amtrak.rider] (15aeca7): 1010167 unknown 10247 Package [org.wikipedia] (4eb34cd): 10191 Package [com.google.android.apps.messaging] (964e000): 10264 Package [org.thoughtcrime.securesms] (d548d43): 10343 Package [com.joelapenna.foursquared] (da3d527): 10286 Package [appyweather.appyweather] (c92a9d2): 10272 Package [net.daylio] (cc56e43): 10307 Package [au.com.shiftyjelly.pocketcasts] (fd687df): 1073 Package [com.google.android.networkstack.tethering] (8b64538): 10185 Package [com.google.android.gm] (c22cfc7): 10186 Package [com.google.android.apps.photos] (3a6bf15): 10285 Package [com.google.android.apps.googlevoice] (7af19a6): 1010205 unknown 10270 Package [com.thetransitapp.droid] (464161b): 10278 Package [com.google.android.apps.translate] (d9af303): 1010185 unknown 10337 Package [com.discord] (ac80e48): 10287 Package [org.owntracks.android] (bc7083a): 10136 Package [com.google.android.dialer] (66db079): 10279 Package [com.bandcamp.android] (f39fab3): 10269 Package [com.getsomeheadspace.android] (208cd9): 10204 Package [com.google.android.apps.docs] (43008d0): 10205 Package [com.google.android.calendar] (dc1f0dc): 10254 Package [com.peterjosling.scroball] (a00421): 10112 Package [com.motorola.gamemode] (1226818): 10459 Package [gov.wa.doh.exposurenotifications] (6c2971b): 10209 Package [com.google.android.apps.tachyon] (c3a03b6): 1000 Package [com.tailscale.ipn] (15c63bb): 1010223 unknown
Analyzing the list of "network-noisy" packages
Okay, so we've narrowed it down --
let's skim through this list and see if anything that shouldn't be there hops out at me...
com.urbandroid.sleep.addon.port is the sleep tracker i use, and it does make a shitload of network calls. This isn't surprising as I have their cloud backup enabled with data going back to 2012. I've been paying for it, so I'll be pretty sour if they're running ad-fraud on my phone.
Tailscale, Nextcloud, Syncthing, Firefox (with ublock origin, etc addons installed), shouldn't be doing these things and will have a lot of natural network traffic. I sure hope I can trust these things as I can't feasibly replace them.
reddit.news is Relay for reddit which is the reddit mobile app I use , I wouldn't put it past them or some JS loaded in a site's web frame, but it happens regularly.
com.amtrak.rider could be doing mean things, their site already runs a bunch of spy-ing bullshit... but background ad-fraud? idk.
net.daylio is a mood/habit tracking app which has a nightly automated backup function. I hope they're not doing anything untoward
Some com.google packages, they wouldn't ad fraud themselves right?
com.jumboprivacy which is a paid service to run little privacy preserving scripts against web services on your device... uh huh...
The google quick search is a bit surprising to me, I suppose.
And of course there are some things being run as root (uid 0), and some other high-level UIDs which aren't reflected in my package list....
Something approaching a conclusion
All of this leaves me where I started, confused and a bit disoriented, unsure of what is happening on my own device in my own home and basically powerless to do anything about it.
But it raises a lot of questions, of course the main one being "what did I learn"
I learned that:
the android system environment is always doing "something", and is usually too chatty about what those "something"s are for you to be able to really see why.
adb dumpsyshas a lot of useful information about your phone including timeseries data.there is always basically unsurprising data usage "at scale" in my phone.
Another question: did i actually learn anything useful in these stats? Remember that this all started with tracking down DNS traffic, which of course is miniscule compared to even a single JPG or /r/formula1 comment thread loaded by Relay. In reality, any app on my system could be running a campaign like this, and there are not a lot of tools to explore this data usage.
Of course, you gave your "informed consent" when you installed these apps!
What is there to do for someone who doesn't consent?
dark laughter
how do you feel about the unabomber manifesto?
...
I'll disable some of these apps or at least force close them tonight and see how many times we try to load ad-impressions overnight.
I also noticed within the dumpsys service was a way to dump the active "activities" on an Android device. I'll set up ADB in a cron job and see what sort of interesting things run overnight, perhaps. write some awful plain-text parser again to extract the useful bits of data out of it.
My Librem 5 will supposably ship soon, and while it'll be nice to have something approaching a libre or at least introspectable userspace, and a set of mostly functional free apps, there are parts of my life that won't fit on this device. Even if it's living in a desk drawer 18 hours a day, if it's just running weird ad campaigns while I sleep or while it charges, should I feel okay with that?
How should I?
Note to self on opening this doc:
Make sure to hack in a python for the session, don't feel like putting this somewhere for direnv
emacs-lisp source:(setq org-babel-python-command (concat (s-chomp (shell-command-to-string "nix-build ~/org/nix-shells/python-pandas.nix")) "/bin/python")))
/nix/store/jnvmbh39cfr8hsifc6ql40np0a1bmaxx-python3-3.9.10-env/bin/python

